//#region src/preview/FrameController.ts
/**
* Priority for video elements.
* Video renders first as other elements may depend on video frames being ready.
*/
const PRIORITY_VIDEO = 1;
/**
* Priority for captions elements.
* Captions render after video so they can overlay correctly.
*/
const PRIORITY_CAPTIONS = 2;
/**
* Priority for audio elements.
* Audio renders after captions (no visual dependency, but keeps consistent ordering).
*/
const PRIORITY_AUDIO = 3;
/**
* Priority for waveform elements.
* Waveform renders after audio because it depends on audio analysis data.
*/
const PRIORITY_WAVEFORM = 4;
/**
* Priority for image elements.
* Images render with low priority as they're typically static.
*/
const PRIORITY_IMAGE = 5;
/**
* Default priority for elements that don't specify one.
* High number ensures custom elements render after standard elements.
*/
const PRIORITY_DEFAULT = 100;
/**
* Type guard to check if an element implements FrameRenderable.
*/
function isFrameRenderable(element) {
	return typeof element === "object" && element !== null && "getFrameState" in element && "prepareFrame" in element && "renderFrame" in element && typeof element.getFrameState === "function" && typeof element.prepareFrame === "function" && typeof element.renderFrame === "function";
}
/**
* Central controller for frame rendering.
* Lives at the root timegroup and orchestrates all element rendering.
*/
var FrameController = class {
	#rootElement;
	#abortController = null;
	#renderInProgress = false;
	#pendingRenderTime = null;
	/**
	* Last successfully rendered time. Used for deduplication when multiple
	* callers (e.g., PlaybackController RAF loop and canvas render loop)
	* both try to render the same frame within one animation frame.
	*/
	#lastRenderedTimeMs = -1;
	constructor(rootElement) {
		this.#rootElement = rootElement;
	}
	/**
	* Cancel any in-progress render operation and reset deduplication state.
	*/
	abort() {
		this.#abortController?.abort();
		this.#abortController = null;
		this.#lastRenderedTimeMs = -1;
	}
	/**
	* Render a frame at the specified time.
	*
	* This is the main entry point for frame rendering. It:
	* 1. Cancels any previous in-progress render
	* 2. Queries all visible FrameRenderable elements
	* 3. Runs preparation in parallel for elements that need it
	* 4. Runs render in priority order
	*
	* @param timeMs - The time in milliseconds to render
	* @param options - Optional configuration
	*/
	async renderFrame(timeMs, options = {}) {
		const { waitForLitUpdate = true, onAnimationsUpdate } = options;
		if (timeMs === this.#lastRenderedTimeMs) return null;
		if (this.#renderInProgress) {
			this.#pendingRenderTime = timeMs;
			return null;
		}
		this.#abortController?.abort();
		this.#abortController = new AbortController();
		const signal = this.#abortController.signal;
		this.#renderInProgress = true;
		try {
			if (waitForLitUpdate) {
				await this.#rootElement.updateComplete;
				signal.throwIfAborted();
			}
			const tQuery = performance.now();
			const elements = this.#queryVisibleElements(timeMs);
			const queryMs = performance.now() - tQuery;
			signal.throwIfAborted();
			const tPrepare = performance.now();
			const elementsNeedingPreparation = elements.filter((el) => el.getFrameState(timeMs).needsPreparation);
			if (elementsNeedingPreparation.length > 0) {
				await Promise.all(elementsNeedingPreparation.map((el) => el.prepareFrame(timeMs, signal)));
				signal.throwIfAborted();
			}
			const prepareMs = performance.now() - tPrepare;
			const tRender = performance.now();
			const sortedElements = [...elements].sort((a, b) => a.getFrameState(timeMs).priority - b.getFrameState(timeMs).priority);
			for (const element of sortedElements) {
				signal.throwIfAborted();
				element.renderFrame(timeMs);
			}
			const renderMs = performance.now() - tRender;
			const tAnims = performance.now();
			if (onAnimationsUpdate) onAnimationsUpdate(this.#rootElement);
			const animsMs = performance.now() - tAnims;
			this.#lastRenderedTimeMs = timeMs;
			return {
				queryMs,
				prepareMs,
				renderMs,
				animsMs
			};
		} finally {
			this.#renderInProgress = false;
			if (this.#pendingRenderTime !== null) {
				const pendingTime = this.#pendingRenderTime;
				this.#pendingRenderTime = null;
				this.renderFrame(pendingTime, options).catch(() => {});
			}
		}
	}
	/**
	* Query all visible FrameRenderable elements in the tree.
	* Uses temporal visibility to filter out elements not visible at current time.
	*
	* IMPORTANT: For temporal elements, we use temporal visibility (startTimeMs/endTimeMs)
	* instead of CSS visibility. This is because updateAnimations sets display:none on
	* elements outside their time range, but that CSS state is from the PREVIOUS frame.
	* When seeking, we need to evaluate visibility based on the NEW time, not stale CSS.
	*
	* @param timeMs - The time to use for visibility checks. This should be the target
	*                 render time, not read from root element (which may be stale).
	*/
	#queryVisibleElements(timeMs) {
		const result = [];
		const currentTimeMs = timeMs;
		const walk = (element) => {
			if ("startTimeMs" in element && "endTimeMs" in element) {
				const startMs = element.startTimeMs ?? -Infinity;
				const endMs = element.endTimeMs ?? Infinity;
				if (!(currentTimeMs >= startMs && currentTimeMs < endMs)) return;
				if (isFrameRenderable(element)) result.push(element);
			} else {
				if (element instanceof HTMLElement && element.style.display === "none") return;
				if (isFrameRenderable(element)) result.push(element);
			}
			const children = this.#getChildrenIncludingSlotted(element);
			for (const child of children) walk(child);
		};
		walk(this.#rootElement);
		return result;
	}
	/**
	* Gets all child elements including slotted content for shadow DOM elements.
	* For elements with shadow DOM that contain slots, this returns the assigned
	* elements (slotted content) instead of just the shadow DOM children.
	*/
	#getChildrenIncludingSlotted(element) {
		if (element.shadowRoot) {
			const slots = element.shadowRoot.querySelectorAll("slot");
			if (slots.length > 0) {
				const assignedElements = [];
				for (const slot of slots) assignedElements.push(...slot.assignedElements());
				for (const child of element.shadowRoot.children) if (child.tagName !== "SLOT") assignedElements.push(child);
				return assignedElements;
			}
		}
		return element.children;
	}
	/**
	* Check if a render is currently in progress.
	*/
	get isRendering() {
		return this.#renderInProgress;
	}
};

//#endregion
export { FrameController, PRIORITY_AUDIO, PRIORITY_CAPTIONS, PRIORITY_DEFAULT, PRIORITY_IMAGE, PRIORITY_VIDEO, PRIORITY_WAVEFORM };
//# sourceMappingURL=FrameController.js.map