fix(studio): keyframe commit routing for 3D and cross-group edits (#1762)

- pickBestAnimation is group-aware: a rotation/3D edit no longer merges into a
  position tween — a fresh same-group tween with a 0% baseline is created instead
- editing at a playhead past the tween extends it and keyframes there (matches drag)
- update-keyframe MERGES into the existing keyframe instead of overwriting, so
  editing one property no longer drops z/transformPerspective (the lens then
  animated from 0 and the element popped)
- dragging a keyframed element with a constant position tween keyframes rather
  than writing a static set
This commit is contained in:
Miguel Ángel
2026-06-27 11:32:43 -04:00
committed by GitHub
parent 6a729b7e03
commit 2d3b19d255
13 changed files with 467 additions and 125 deletions
+17
View File
@@ -2348,6 +2348,23 @@ export function updateKeyframeInScript(
// `properties` would wipe the primitive — leave the keyframe untouched.
return script;
}
// MERGE edited props into the existing keyframe, preserving props not in this edit
// (z, transformPerspective, rotation, …). A whole-value rebuild drops them, so editing
// one prop at the 0% keyframe strips z/transformPerspective and the element pops.
// Mirrors acorn updateKeyframeInScript; parity-locked by gsapWriterParity.corpus.
const existing = match.prop.value;
if (existing?.type === "ObjectExpression") {
const props = (existing.properties ?? []) as AstNode[];
const upsert = (key: string, valueCode: string) => {
const idx = props.findIndex((p: AstNode) => isObjectProperty(p) && propKeyName(p) === key);
const node = parseExpr(`({ ${safeKey(key)}: ${valueCode} })`).properties[0];
if (idx >= 0) props[idx] = node;
else props.push(node);
};
for (const [k, v] of Object.entries(properties)) upsert(k, valueToCode(v));
if (ease !== undefined) upsert("ease", JSON.stringify(ease));
return recast.print(loc.parsed.ast).code;
}
match.prop.value = buildKeyframeValueNode(properties, ease);
return recast.print(loc.parsed.ast).code;
}
+15 -3
View File
@@ -780,10 +780,22 @@ export function updateKeyframeInScript(
const match = findKfPropByPct(kfPropNode.value, percentage);
if (!match) return script;
const record: Record<string, number | string> = { ...properties };
if (ease) record.ease = ease;
const ms = new MagicString(script);
ms.overwrite(match.prop.value.start, match.prop.value.end, recordToCode(record));
// MERGE the edited props into the existing keyframe, preserving properties already
// keyframed at this percentage (z, transformPerspective, rotation, …). A whole-value
// overwrite DROPS every prop not in this edit — e.g. editing rotationY at the 0%
// keyframe would strip z / transformPerspective, so the lens then animates from 0 and
// the element pops. Mirrors addKeyframeToScript's merge-into-existing branch.
if (match.prop.value?.type === "ObjectExpression") {
for (const [k, v] of Object.entries(properties)) {
upsertProp(ms, match.prop.value, k, v);
}
if (ease !== undefined) upsertProp(ms, match.prop.value, "ease", ease);
} else {
const record: Record<string, number | string> = { ...properties };
if (ease) record.ease = ease;
ms.overwrite(match.prop.value.start, match.prop.value.end, recordToCode(record));
}
return ms.toString();
}