Skip to content

fix(cli): sweep_static geometry fingerprint now covers SVG stroke/dash motion - #3914

Open
miga-heygen wants to merge 1 commit into
magi/fix-sweep-static-text-fingerprintfrom
fix/sweep-static-svg-dash
Open

miga-heygen wants to merge 1 commit into
magi/fix-sweep-static-text-fingerprintfrom
fix/sweep-static-svg-dash

Conversation

@miga-heygen

@miga-heygen miga-heygen commented Sep 13, 2026

Copy link
Copy Markdown
Contributor

Read after #3707 — this PR is stacked on magi/fix-sweep-static-text-fingerprint and adds one channel to the motion classifier that PR introduces.

Summary

hyperframes check's seek-audit fires sweep_static ("Timeline did not advance under seek") when the per-sample motion signature is byte-identical across the whole run. An SVG stroke-dasharray / stroke-dashoffset "draw the line in" entrance — the standard way to animate a connector or diagram line — moves no bounding box and no opacity, so a genuinely animating composition using it was flagged as a frozen timeline.

With #3707 the signature has a single owner (packages/cli/src/commands/motion-signature.browser.js, ELEMENT_CHANNELS), shared by the frozen-sweep guard and keepsMoving liveness. This PR:

  • Appends strokeDashChannel to ELEMENT_CHANNELS. Hashes the computed dash pattern and offset for SVG geometry elements (SVGGeometryElement: path/circle/ellipse/rect/line/polyline/polygon) with a painted stroke. none and all-zero patterns (which render solid), unpainted strokes (none, transparent, zero width, stroke-opacity: 0) and non-SVG elements contribute "", so ordinary compositions gain no payload.
  • Widens isVisibleElement for stroked geometry with one degenerate bbox axis. Chromium's getBoundingClientRect for SVG shapes is the object bounding box without the stroke: a straight horizontal or vertical connector reports 0 height or 0 width regardless of stroke-width, so the box gate dropped exactly the most common shape for this animation. A stroked shape with one non-degenerate axis now counts as visible. As a consequence motion-sample's visible bit for such connectors is corrected too (it previously reported them invisible).
  • Treats <defs>, <clipPath>, <mask>, <pattern>, <marker> and <symbol> as never-painted subtrees in the existing hidden-subtree walk (the same container list layout-audit.browser.js skips for connectors). display is not inherited — a child of a display:none parent computes display: block — so descendants are excluded only through the ancestor walk, which now also starts at these containers rather than relying on one engine's empty-box behaviour for their descendants. The match is namespaced (instanceof SVGElement), so an HTML element that merely shares a name is not pruned.

Test plan

  • motion-signature.browser.test.ts (happy-dom, 36 passing): dash-offset moves on a 290×0 connector change the signature; identical scene hashes identically; dash motion under a display:none ancestor, inside <defs> / <clipPath> / <mask> / <pattern> / <marker> / <symbol>, and on a stroke-opacity: 0 connector is ignored; a fade inside an HTML element named <defs> still changes the signature. Fixtures use the real Chromium bbox of a horizontal stroked path (height 0) so they pin the widened gate, not the pre-existing box path.
  • motion-signature.browser.chromium.test.ts (real Chromium, 37 passing): a stroke-dashoffset draw-in on a straight connector changes both the sweep and the liveness signature; dash motion under display:none and inside all six unpainted containers leaves both identical; a fade inside an HTML <defs> element changes both signatures.
  • motion-sample.browser.test.ts (happy-dom, 8 passing) unchanged.
  • Real-Chromium probes confirmed: straight <path> reports a 290×0 box; display:none descendants compute display: inline; descendants of all six containers (<defs>, <clipPath>, <mask>, <pattern>, <marker>, <symbol>) report 0×0 while checkVisibility returns true for them, so the container rule is what excludes them by intent rather than the box gate; computed stroke-dashoffset is rounded (144.99999999px145px), so the signature cannot vary on its own at a fixed seek.
  • Review passes on the diff surfaced and fixed: stroke: transparent / stroke-opacity: 0 counting as painted; stroke-dasharray: 0 (solid) counting as a pattern; tag-name matching hitting HTML unknown elements (replaced by instanceof SVGGeometryElement).
  • Review passes on the container rule surfaced and fixed: <mask> / <pattern> / <marker> missing from the never-painted set although they meet its definition (and the widened bbox gate would otherwise admit a straight connector inside them); the tag-name match lacking a namespace guard.
  • oxlint, oxfmt --check, and tsc --noEmit clean for packages/cli. The happy-dom suites cannot be collected directly in the local sandbox under bun (No such built-in module: node: from the @vitest-environment happy-dom loader); they were run with --environment node plus a setup file that populates globals from a happy-dom Window, and pass. CI runs them as-is.

Deliberately out of scope: dashing on <text> / <use> (no channel today), and other "moves nothing else" SVG reveals (fill-opacity, stroke-opacity, gradient stop-opacity) — this closes the stroke/dash gap only.

@miga-heygen

Copy link
Copy Markdown
Contributor Author

Addressed in b8abfc7.

@miga-heygen
miga-heygen force-pushed the fix/sweep-static-svg-dash branch from f35dfe3 to b8abfc7 Compare September 14, 2026 18:26
@mintlify

mintlify Bot commented Sep 14, 2026

Copy link
Copy Markdown

Preview deployment for your docs. Learn more about Mintlify Previews.

Project Status Preview Updated
hyperframes 🟢 Ready View Preview Sep 14, 2026, 6:29 PM

💡 Tip: Enable Automations to automatically generate PRs for you.

@miga-heygen
miga-heygen changed the base branch from main to magi/fix-sweep-static-text-fingerprint September 14, 2026 18:26
@miga-heygen

Copy link
Copy Markdown
Contributor Author

Rebased onto bfaf6eb as 582018d.

@miga-heygen

Copy link
Copy Markdown
Contributor Author

Rebased onto 09418ae as 5f4196b.

A "draw the line in" SVG entrance animates stroke-dasharray /
stroke-dashoffset on a shape whose geometry never changes, so the
box+opacity channels read it as a frozen timeline and `hyperframes check`
false-positives sweep_static on a visibly animating composition.

- Append strokeDashChannel to ELEMENT_CHANNELS: hashes the computed dash
  pattern and offset for SVG geometry elements with a painted stroke;
  `none` / all-zero patterns, unpainted (none, transparent, zero-width,
  stroke-opacity 0) strokes and non-SVG elements contribute "".
- Widen isVisibleElement for stroked geometry with one degenerate bbox
  axis: Chromium's getBoundingClientRect for SVG shapes excludes the
  stroke, so a straight horizontal or vertical connector reports 0 height
  or 0 width and was dropped by the box gate — the most common shape for
  this animation. motion-sample's `visible` bit for such connectors is
  corrected as a consequence.
- Treat <defs>, <clipPath> and <symbol> as never-painted subtrees in the
  hidden-subtree walk (display is not inherited, so descendants are hidden
  only through the ancestor walk) rather than relying on one engine's
  empty-box behaviour for their descendants.

Tests: happy-dom pins for the degenerate-bbox connector (moves / frozen),
a display:none ancestor, <defs>/<clipPath> content and a stroke-opacity:0
decoy; real-Chromium pins for the connector draw-in and the ignored cases.

Co-Authored-By: Miguel Ángel <miguel.sierra@heygen.com>
@miga-heygen
miga-heygen force-pushed the fix/sweep-static-svg-dash branch from 5f4196b to 95624bc Compare September 15, 2026 02:34
@miga-heygen

Copy link
Copy Markdown
Contributor Author

Updated as 95624bc: SVG container set and guard.

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