mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-05 00:56:23 +00:00
The acorn addKeyframeToScript mixed ms.overwrite + ms.appendLeft on the
same _auto endpoint node, crashing MagicString ("Cannot split a chunk
that has already been edited") whenever an interior keyframe adjacent to
an _auto 0/100 endpoint introduced a new backfilled prop — the common SDK
path. It also replaced (not merged) existing keyframes, dropped ease and
_auto markers, corrupted commas on multi-prop backfill into empty {},
used a <0.001 percentage tolerance instead of recast's PCT_TOLERANCE=2,
and silently no-op'd on flat (non-keyframe) tweens.
Rebuild the node model to mirror recast: compute the FINAL property record
for every changed keyframe value node (target merge, _auto endpoint sync,
backfilled siblings) against the original AST, then emit exactly one
ms.overwrite per changed node (one insert for a brand-new key). No node is
ever both overwritten and appended into, so splices can never overlap.
- Merge: re-touching an existing keyframe merges new props over the
existing record, preserving untouched props, existing ease, and _auto.
- Convert-flat: first keyframe-add on a flat to()/from()/fromTo() tween
rebuilds its vars object to percentage keyframes (ease->easeEach,
ease:"none", from/fromTo->to) matching recast, then re-locates via the
-from-/-fromTo- -> -to- id fallback.
- Tolerance: PCT_TOLERANCE=2 for existing-keyframe detection.
- Shared serializeValue/safeJsKey for keyframe values (recast parity); the
tween-statement path keeps its local serializer for object/boolean extras.
- keyframeBackfill: only backfill props with a real numeric default; skip
unknown/string props so color:0 / filter:0 are never emitted.
- setGsapKeyframe move-path threads the same backfill defaults as the add
path so both entry points behave identically.
Differential tests (acorn vs recast parsed keyframe arrays) cover the
crash (2-endpoint + 0/25/100), empty-{} multi-prop backfill, merge with
extra props + ease, flat to()/fromTo() convert, "50.0%" non-byte-equal
key, near-% tolerance, and _auto-marker preservation onto an endpoint.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
46 lines
1.8 KiB
TypeScript
46 lines
1.8 KiB
TypeScript
/**
|
|
* Backfill defaults for add-keyframe ops.
|
|
*
|
|
* When an add-keyframe op introduces a property absent from the other keyframes,
|
|
* the writer needs a rest value to seed those keyframes with so GSAP interpolates
|
|
* instead of snapping. The SDK derives the numeric-default set here so the acorn
|
|
* writer matches the recast writer the server uses.
|
|
*
|
|
* Only props with a real numeric default get a backfill value. Defaulting an
|
|
* unknown or string-valued prop to 0 (e.g. `color: 0`, `filter: 0`) emits invalid
|
|
* GSAP, so such props are SKIPPED — the writer then leaves them out of the other
|
|
* keyframes (GSAP reads the rest value from the DOM), matching recast (which skips
|
|
* any prop whose default is null).
|
|
*/
|
|
|
|
// Numeric rest values for editable transform/style props. Props absent here have
|
|
// no safe static default and are intentionally omitted from the backfill set.
|
|
//
|
|
// KEEP IN SYNC WITH packages/studio/src/hooks/gsapShared.ts:PROPERTY_DEFAULTS —
|
|
// the studio (recast) and SDK (acorn) paths must derive the same defaults or
|
|
// SDK-written keyframes drift from server-written ones (the exact bug this fixes).
|
|
// TODO: lift the canonical table into @hyperframes/core and import from both.
|
|
const KEYFRAME_PROPERTY_DEFAULTS: Record<string, number> = {
|
|
opacity: 1,
|
|
x: 0,
|
|
y: 0,
|
|
scale: 1,
|
|
scaleX: 1,
|
|
scaleY: 1,
|
|
rotation: 0,
|
|
width: 100,
|
|
height: 100,
|
|
};
|
|
|
|
/** Derive the backfillDefaults for an add-keyframe op (numeric-default props only). */
|
|
export function deriveKeyframeBackfillDefaults(
|
|
value: Record<string, number | string>,
|
|
): Record<string, number | string> {
|
|
const defaults: Record<string, number | string> = {};
|
|
for (const key of Object.keys(value)) {
|
|
const def = KEYFRAME_PROPERTY_DEFAULTS[key];
|
|
if (def !== undefined) defaults[key] = def;
|
|
}
|
|
return defaults;
|
|
}
|