From 8e393bc18805d017cf3e152b43f35afb94692c4b Mon Sep 17 00:00:00 2001 From: Carlos Alcaraz <193642530+calcarazgre646@users.noreply.github.com> Date: Thu, 18 Jun 2026 22:16:52 -0300 Subject: [PATCH] fix(runtime): size a clip from data-end when it has no data-duration The timeline clip loop read only data-duration, then fell through to the inherited (composition) duration when it was absent. A clip authored with data-start/data-end but no data-duration (a valid, compiler-emitted shape the linter only warns about) was sized start..compositionEnd instead of start..end: a 2s..8s clip under a 20s composition rendered as an ~18s bar overrunning every later clip on its track. Read data-end as the duration source before the inherited fallback, mirroring the scene loop and resolveDurationForElement which already do. --- packages/core/src/runtime/timeline.test.ts | 20 ++++++++++++++++++++ packages/core/src/runtime/timeline.ts | 8 ++++++++ 2 files changed, 28 insertions(+) diff --git a/packages/core/src/runtime/timeline.test.ts b/packages/core/src/runtime/timeline.test.ts index 333e0a08c5..3746861319 100644 --- a/packages/core/src/runtime/timeline.test.ts +++ b/packages/core/src/runtime/timeline.test.ts @@ -41,6 +41,26 @@ describe("collectRuntimeTimelinePayload", () => { expect(result.clips[0].kind).toBe("element"); }); + it("derives a clip's duration from data-end when data-duration is absent", () => { + const root = document.createElement("div"); + root.setAttribute("data-composition-id", "main"); + root.setAttribute("data-duration", "20"); + document.body.appendChild(root); + + const clip = document.createElement("div"); + clip.id = "text-1"; + clip.setAttribute("data-start", "2"); + clip.setAttribute("data-end", "8"); + clip.setAttribute("data-track-index", "0"); + root.appendChild(clip); + + const result = collectRuntimeTimelinePayload(defaultParams); + expect(result.clips).toHaveLength(1); + expect(result.clips[0].start).toBe(2); + // 8 - 2 = 6, not the inherited root duration (20 - 2 = 18) + expect(result.clips[0].duration).toBe(6); + }); + it("identifies video clips by tag", () => { const root = document.createElement("div"); root.setAttribute("data-composition-id", "main"); diff --git a/packages/core/src/runtime/timeline.ts b/packages/core/src/runtime/timeline.ts index e6328d73a8..57366cd1a8 100644 --- a/packages/core/src/runtime/timeline.ts +++ b/packages/core/src/runtime/timeline.ts @@ -389,6 +389,14 @@ export function collectRuntimeTimelinePayload(params: { ); const nodeCompositionId = node.getAttribute("data-composition-id"); let duration = parseElementDurationAttr(node); + if (duration == null || duration <= 0) { + // Mirror the scene loop: an explicit data-end sets the clip's duration + // when no data-duration is present. Without this the clip fell through to + // the inherited (composition) duration and was sized start..rootEnd, so a + // 2s..8s clip rendered as start..20s on the timeline. + const endAttr = parseElementEndAttr(node); + if (endAttr != null) duration = Math.max(0, endAttr - start); + } if ( (duration == null || duration <= 0) && nodeCompositionId &&