fix(studio): resolve lane-header removal against the clicked element

The lane-header keyframe toggle fires on whichever element owns the lane,
which need not be the selected one. The remove path looked the animation up
in the selected element's animations, so a non-selected element's flat tween
missed and silently took the remove-one-keyframe branch, stranding the tween
instead of deleting it.
This commit is contained in:
Miguel Angel Simon Sierra
2026-07-27 19:52:07 +02:00
parent ba8df2661c
commit 3f93794fa6
2 changed files with 39 additions and 1 deletions
@@ -330,6 +330,41 @@ describe("useTimelineEditCallbacks — flat tween keyframe lanes", () => {
view.unmount(); view.unmount();
}); });
// The lane-header toggle fires on whichever element owns the lane, which need
// not be the selected one. Looking the flat tween up in the selected element's
// animations misses, and the miss silently takes the remove-one-keyframe
// branch, which strands the flat tween instead of deleting it.
it("removes a non-selected element's flat tween through that element's own animations", async () => {
const circle: TimelineElement = {
...element,
id: "circle",
key: "scenes/main.html#circle",
domId: "circle",
};
usePlayerStore.setState({
elements: [element, circle],
gsapAnimations: new Map([["scenes/main.html#circle", [otherFlatAnimation]]]),
});
const view = renderCallbacks();
await act(async () => {
await view.callbacks.onTogglePropertyGroupKeyframe?.(circle, {
animationId: otherFlatAnimation.id,
propertyGroup: "position",
tweenPercentage: 0,
properties: { x: 0 },
remove: true,
});
});
expect(mocks.actions.handleGsapDeleteAnimation).toHaveBeenCalledWith(
otherFlatAnimation.id,
mocks.selection,
);
expect(mocks.actions.handleGsapRemoveKeyframe).not.toHaveBeenCalled();
view.unmount();
});
it("keeps authored interior deletion on the per-keyframe path", () => { it("keeps authored interior deletion on the per-keyframe path", () => {
mocks.animations = [authoredInteriorAnimation()]; mocks.animations = [authoredInteriorAnimation()];
usePlayerStore.setState({ gsapAnimations: new Map([["box", mocks.animations]]) }); usePlayerStore.setState({ gsapAnimations: new Map([["box", mocks.animations]]) });
@@ -324,10 +324,13 @@ export function useTimelineEditCallbacks({
const selection = await buildDomSelectionForTimelineElement(element); const selection = await buildDomSelectionForTimelineElement(element);
if (!selection) return; if (!selection) return;
if (target.remove) { if (target.remove) {
// The clicked element's animations, not the selected element's: this
// lookup decides delete-the-flat-tween vs remove-one-keyframe, and a
// miss silently takes the keyframe branch, stranding the flat tween.
removeKeyframeTarget( removeKeyframeTarget(
target.animationId, target.animationId,
target.tweenPercentage, target.tweenPercentage,
selectedGsapAnimations, resolveElementAnimations(element.key ?? element.id),
selection, selection,
); );
return; return;