Skip to content

fix(core): two scenes drawing SVGs with the same ids each show their own - #7251

Merged
miguel-heygen merged 2 commits into
mainfrom
fix/scene-svg-ids
Nov 5, 2026
Merged

miguel-heygen merged 2 commits into
mainfrom
fix/scene-svg-ids

Conversation

@miguel-heygen

Copy link
Copy Markdown
Collaborator

What

Two scenes in one composition can now draw their SVGs with the same ids. Each scene's url(#id) and href="#id" resolve to its own definitions in preview, snapshot and render.

Why

A report: snapshot --at 4,86,141 on a film with two distinct inline SVG character portraits, one per scene, showed the first portrait again in the last scene.

Both portraits were written straight into index.html, one per scene clip, and both used the same ids (a gradient, a clip path, a symbol). The browser resolves url(#skin) and href="#portrait" to the first element with that id in the page, so the second scene painted the first scene's pieces, and its own gradient painted nothing while the first scene was hidden. Render shows the same thing as snapshot. The same portraits as two sub-compositions (data-composition-src) come out right, because loading a sub-composition renames colliding SVG ids; ids written in the main page were never renamed.

Related work

None.

How

svgIdNamespacing.ts already renames colliding SVG ids per scope (each inlined composition), keeps the first definition's id, and rewrites only that scope's references. The new sceneClipRoots adds one more kind of scope: each outermost timed clip (data-start) that holds an <svg>, outside every composition host. The compiler (inlineSubCompositions) and the runtime loader (namespaceMountedSvgIds) both pass those clips to the same renamer, next to the composition hosts.

What it leaves alone:

  • Ids that don't collide: they keep their names, as before.
  • Definitions at page level, outside every clip: they are never renamed, and a clip that references one without defining its own still resolves to it.
  • The first scene: the first definition keeps its id; only later clips' copies are renamed (clip-1--skin) and their own references follow.

Test plan

  • inlineSubCompositions.test.ts "gives each scene clip its own portrait when two scenes reuse the same SVG ids": each scene's fill and href resolve inside that scene, and the first scene keeps its ids. "leaves a page-level definition the scenes share where it is": both scenes still use url(#shared).

  • compositionLoader.test.ts "resolves each top-level scene clip to its own clipPath, and a shared page-level one for both": the same at runtime.

  • snapshot.sceneSvgIds.browser.test.ts: two red/blue portraits with the same ids, plus a corner filled from a page-level gradient, snapshotted at 4 s and 14 s, reading pixels.

  • fails without the fix: with main's three source files, the compiler test, the loader test and the snapshot test fail (at 14 s the face is black and the corner piece red); the page-level test passes either way, as a guard.

  • Before and after, over every registry example, block and component and every producer regression project (549 entries):

    • Compile: each compiles to byte-identical HTML with and without this change. With no sub-compositions the compiler has nothing to inline, so for top-level scenes the change acts in the runtime loader.
    • Runtime: the loader renames only an id that a scene clip defines, references, and that is defined elsewhere too. A census of every compiled page found 24 scene clips holding an SVG and no such id in any of them. Ten producer projects with video fixtures did not compile in that run, and none of them contains an <svg>. The census does flag the repro fixture, so it can see the case it looks for.
  • Unit tests added/updated

  • Manual testing performed

  • Documentation updated (if applicable)

  • Comments follow CONTRIBUTING.md "Comments": they say why, not what, and a bug fix says what the code must do and how to reproduce the bug

Not exercised: the reporter's own project, and full renders of the regression projects; the census above is why none of them can change.

@github-actions

github-actions Bot commented Nov 5, 2026 •

Copy link
Copy Markdown
Contributor

Edit accuracy: accurate 267 (base branch 267), smooth 103 of those

The gate passes.
Smoothness is reported in the artifact, not gated. A case fails only if it fails 2 of 3 runs.

Quarantined, measured but not gated (8)

Unstable (2)

  • seqrepeat-none-center-r0-root-z100: tracking 0.05, pressJump 0, drop 0, reload 0, render 0.02, renderKey -, undo true, teleport true / tracking 0.05, pressJump 0, drop 0, reload 0, render 0.02, renderKey -, undo true, teleport true / tracking 0.05, pressJump 0, drop 0, reload 0, render 0.02, renderKey -, undo true, teleport true
  • seqplainkeys-none-pct-r0-nested-z100: tracking 0.05, pressJump 0, drop 0, reload 0, render 0.02, renderKey -, undo false, teleport true / tracking 0.05, pressJump 0, drop 0, reload 0, render 0.02, renderKey -, undo true, teleport true / tracking 0.05, pressJump 0, drop 0, reload 0, render 0.02, renderKey -, undo true, teleport true

@heygengenesis heygengenesis Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verdict: REQUEST CHANGES · 1 blocker · 0 important · 0 nits · head bfe2db3c

The fix itself looks right, and both named unit tests fail on main's source files when I run them. The blocker is that required CI is red: Typecheck and Test: runtime contract both fail with TS2339 at compositionLoader.ts:917 (↓ inline). It's a one-line type annotation.

Scope

Read all 6 files plus the namespaceCollidingSvgIds / selector-alias paths they feed. Ran inlineSubCompositions.test.ts and compositionLoader.test.ts locally (116/116 pass). With main's three source files restored, the two new scene tests fail and the page-level guard passes, which matches the fails without the fix: line. I checked that the annotation below clears the TS error locally. I didn't run the browser snapshot test locally; the CLI test shards pass in CI. Out of scope: scenes that sit inside a timed wrapper clip, and <svg data-start> used directly as the clip. Both stay in one scope, which is no regression.

const hosts = composed
.filter((scope) => scope.referenceTarget !== "document")
.map(({ host }) => host);
const mounted = [

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

blocker: TypeScript infers mounted as the union MountedSvgScope | { host; namespace; styles: never[] }, so destructuring referenceTarget at L917 fails typecheck: TS2339: Property 'referenceTarget' does not exist. That error fails CI jobs Typecheck and Test: runtime contract.

Suggested change
const mounted = [
const mounted: MountedSvgScope[] = [

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Fixed in 063292b

@heygengenesis heygengenesis Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verdict: APPROVE — 1 resolved, 0 open, 0 new · bfe2db3 → 063292b. The only change since bfe2db3 is the suggested MountedSvgScope[] annotation, and Typecheck and Test: runtime contract now pass.

@miguel-heygen
miguel-heygen added this pull request to the merge queue Nov 5, 2026
Merged via the queue into main with commit b108f85 Nov 5, 2026
152 of 154 checks passed
@miguel-heygen
miguel-heygen deleted the fix/scene-svg-ids branch November 5, 2026 17:38
SanHsien added a commit to SanHsien/hyperframes that referenced this pull request Nov 6, 2026
…view

Adopts heygen-com#7250 (SVG stroke drawing is not a dead stretch), heygen-com#7248
(contract check reads a timeline registered in a local script), heygen-com#7252
(comma-list selectors hidden by authored CSS), heygen-com#7238 (snapshot and
preview show a clip on the render frame), heygen-com#7237 (stalled cloud upload
retries) and heygen-com#7251 (per-scene SVG id namespacing), with prerequisite
heygen-com#6829. Defers heygen-com#7246 (needs the delivery limiter from heygen-com#6940) and heygen-com#7232
(needs the WebKit decode-first pipeline), with conditions in the
ledger. Watermark advances to b108f85.

Co-authored-by: Miguel Ángel <miguel.sierra@heygen.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant