fix(studio): give the motion path and the fallbacks a one-element target

The narrowing this branch adds missed the motion-path overlay, and every
caller that could not narrow fell back to the exact bare class the narrowing
exists to replace.

- motionPathSelection.selectorFor now goes through writeTargetSelector. It
  feeds both the geometry read and the "set destination" write, so a class
  sibling measured its home off the FIRST sibling and then authored
  add-motion-path onto all of them. The toolbar toggle hides when no
  one-element form exists rather than arming a press that is dropped.
- The five new-tween writers that fell back to the selection's own selector
  now drop the commit instead. A gesture that does not persist reverts on the
  next reload; a tween silently aimed at five elements does not.
- tweenTargetsElement only follows the DOM to a target that matches exactly
  one element. A target the element merely shares with its siblings is a group
  tween, and these callers mutate what they find, so an individual nudge was
  rewriting the group's own tween and moving all five.
This commit is contained in:
Miguel Angel Simon Sierra
2026-07-28 21:21:37 +02:00
parent 3d92436066
commit 1f3fd2800c
9 changed files with 190 additions and 41 deletions
+20 -9
View File
@@ -51,9 +51,14 @@ export interface GsapDragCommitCallbacks {
* write over every sibling sharing the class. Retargets of an EXISTING tween
* must NOT come through here (they keep `anim.targetSelector`, so a tween the
* author aimed at a group stays aimed at it).
*
* Null means no one-element form exists, and every caller drops the commit
* rather than falling back to `selector` (see writeTargetSelector): the drag
* reverts on the next reload, which is recoverable, where a `.group` write is
* not.
*/
function newTweenTarget(selection: DomEditSelection, selector: string): string {
return writeTargetSelector(selection) ?? selector;
function newTweenTarget(selection: DomEditSelection): string | null {
return writeTargetSelector(selection);
}
// Re-export for backward compatibility with existing imports.
@@ -77,17 +82,18 @@ export function parkPlayheadOnKeyframe(anim: GsapAnimation, pct: number): void {
async function replaceKeyframedPositionHold(
selection: DomEditSelection,
selector: string,
existingSet: GsapAnimation,
properties: { x: number; y: number },
commitMutation: GsapDragCommitCallbacks["commitMutation"],
): Promise<void> {
const target = newTweenTarget(selection);
if (!target) return;
const persist = async (commit: GsapDragCommitCallbacks["commitMutation"]) => {
await commit(
selection,
{
type: "add",
targetSelector: newTweenTarget(selection, selector),
targetSelector: target,
method: "set",
position: 0,
properties,
@@ -186,7 +192,6 @@ export async function commitStaticGsapPosition(
// least one hold on disk, then delete the corrupt tween in one transaction.
await replaceKeyframedPositionHold(
selection,
selector,
existingSet,
{ x: newX, y: newY },
callbacks.commitMutation,
@@ -213,7 +218,8 @@ export async function commitStaticGsapPosition(
// an instant patch so the first nudge shows immediately (no soft-reload flash).
// The patch reuses the WRITTEN target so the runtime moves exactly the element
// the source write names.
const target = newTweenTarget(selection, selector);
const target = newTweenTarget(selection);
if (!target) return;
await callbacks.commitMutation(
selection,
{
@@ -270,7 +276,8 @@ export async function commitStaticGsapRotation(
return;
}
// New static hold → off-timeline `gsap.set` (no 0% keyframe marker) + instant patch.
const target = newTweenTarget(selection, selector);
const target = newTweenTarget(selection);
if (!target) return;
await callbacks.commitMutation(
selection,
{
@@ -323,11 +330,13 @@ export async function commitStaticGsapSize(
);
return;
}
const target = newTweenTarget(selection);
if (!target) return;
await callbacks.commitMutation(
selection,
{
type: "add",
targetSelector: newTweenTarget(selection, selector),
targetSelector: target,
method: "set",
position: 0,
properties: { width, height },
@@ -411,11 +420,13 @@ export async function commitKeyframedSizeFromResize(
// transport applies both in one ordered batch; a plain commit fallback keeps the
// same recoverable ordering. Only the transaction's result triggers the reload.
const addLabel = `Resize (size keyframe ${pct.toFixed(0)}%)`;
const target = newTweenTarget(selection);
if (!target) return false;
await callbacks.commitMutation(
selection,
{
type: "add-with-keyframes",
targetSelector: newTweenTarget(selection, selector),
targetSelector: target,
position: roundTo3(ts),
duration: roundTo3(td),
keyframes,