diff --git a/packages/core/src/runtime/init.test.ts b/packages/core/src/runtime/init.test.ts index 340456850c..2d73a7a21e 100644 --- a/packages/core/src/runtime/init.test.ts +++ b/packages/core/src/runtime/init.test.ts @@ -1415,6 +1415,36 @@ describe("initSandboxRuntimeModular", () => { expect(window.__player?.getDuration()).toBe(10); }); + it("resumes readiness when GSAP batching starts after runtime initialization", async () => { + const root = document.createElement("div"); + root.setAttribute("data-composition-id", "main"); + root.setAttribute("data-root", "true"); + root.setAttribute("data-start", "0"); + root.setAttribute("data-width", "1920"); + root.setAttribute("data-height", "1080"); + document.body.appendChild(root); + + let timelineDuration = 0; + const timeline = createMockTimeline(0); + timeline.duration = () => timelineDuration; + window.__timelines = { main: timeline }; + window.__hfTimelinesBuilding = false; + + initSandboxRuntimeModular(); + expect(window.__renderReady).toBe(true); + + window.__hfTimelinesBuilding = true; + await new Promise((resolve) => setTimeout(resolve, 0)); + expect(window.__renderReady).toBe(false); + + timelineDuration = 10; + window.__hfTimelinesBuilding = false; + window.dispatchEvent(new CustomEvent("hf-timelines-built")); + + expect(window.__renderReady).toBe(true); + expect(window.__player?.getDuration()).toBe(10); + }); + it("waits for THREE.DefaultLoadingManager to drain before publishing render readiness", async () => { const root = document.createElement("div"); root.setAttribute("data-composition-id", "main"); diff --git a/packages/core/src/runtime/init.ts b/packages/core/src/runtime/init.ts index 24fb83cb57..b9e812a24a 100644 --- a/packages/core/src/runtime/init.ts +++ b/packages/core/src/runtime/init.ts @@ -2476,11 +2476,33 @@ export function initSandboxRuntimeModular(): void { postState(true); }; + let timelinesBuiltListener: (() => void) | null = null; + const waitForTimelinesBuilt = () => { + if (timelinesBuiltListener) return; + const onTimelinesBuilt = () => { + window.removeEventListener("hf-timelines-built", onTimelinesBuilt); + timelinesBuiltListener = null; + maybePublishRenderReady(); + }; + timelinesBuiltListener = onTimelinesBuilt; + window.addEventListener("hf-timelines-built", onTimelinesBuilt); + }; + registerRuntimeCleanup(() => { + if (!timelinesBuiltListener) return; + window.removeEventListener("hf-timelines-built", timelinesBuiltListener); + timelinesBuiltListener = null; + }); + maybePublishRenderReady = () => { - if (!externalCompositionsReady || window.__hfTimelinesBuilding) { + if (!externalCompositionsReady) { window.__renderReady = false; return; } + if (window.__hfTimelinesBuilding) { + window.__renderReady = false; + waitForTimelinesBuilt(); + return; + } // Re-run discover so adapters can refresh their state from the current // DOM — e.g. the Three.js adapter only hooks `DefaultLoadingManager` once // it sees `window.THREE`, which may have loaded AFTER the initial @@ -2499,14 +2521,6 @@ export function initSandboxRuntimeModular(): void { // synchronously. Wait for the "hf-timelines-built" event before the first // binding attempt so the transport clock receives the finished timeline // duration instead of permanently publishing duration=0. - if (window.__hfTimelinesBuilding) { - window.__renderReady = false; - const onTimelinesBuilt = () => { - window.removeEventListener("hf-timelines-built", onTimelinesBuilt); - maybePublishRenderReady(); - }; - window.addEventListener("hf-timelines-built", onTimelinesBuilt); - } maybePublishRenderReady(); // When the bundler inlines compositions, data-composition-src is removed so