fix(studio): span the clip on the remaining duration-less tween paths

Two sibling call paths still answered from GSAP's 0.5s default while the
toggle path had moved to the clip-wide fallback.

isPlayheadWithinTween now takes the selection, so the toolbar stops
promising "extends animation" on a duration-less tween whose click actually
toggles an interior keyframe. The arc drag commit resolves its replacement
duration the same way its click-path sibling does, instead of authoring
duration 0.5 and collapsing the arc window.

The flat text section drops its two marker-clearing effects: the autofocus
marker is a ref now, read and cleared by the render that consumes it.
This commit is contained in:
Miguel Angel Simon Sierra
2026-07-28 00:13:46 +02:00
parent d57039882f
commit 62e866eda6
6 changed files with 50 additions and 20 deletions
@@ -4,6 +4,7 @@ import { usePlayerStore } from "../player/store/playerStore";
import { resolveTweenStart, resolveTweenDuration } from "../utils/globalTimeCompiler";
import { roundTo3 } from "../utils/rounding";
import { computeDraggedGsapPosition } from "./draggedGsapPosition";
import { resolveEditableTweenDuration } from "./gsapShared";
import {
type GsapDragCommitCallbacks,
computeCurrentPercentage,
@@ -297,7 +298,9 @@ export async function commitGsapPositionFromDrag(
}
const tweenStart = resolveTweenStart(anim);
const tweenDuration = resolveTweenDuration(anim);
// Clip-wide, same as applyArcKeyframeAtPlayhead: authoring GSAP's 0.5s
// default here would collapse a duration-less arc's window on drag.
const tweenDuration = resolveEditableTweenDuration(anim, selection);
if (tweenStart === null || tweenDuration <= 0 || keyframes.length < 2) return;
const temporalKeyframes = buildTemporalArcKeyframes(anim, pct, { x: newX, y: newY });
await callbacks.commitMutation(
@@ -108,6 +108,19 @@ describe("isPlayheadWithinTween", () => {
it("does not block when the start can't be resolved", () => {
expect(isPlayheadWithinTween(anim({ position: "+=1" }), 99)).toBe(true);
});
// The toolbar's "extends animation" tooltip has to agree with what the edit
// paths do. Those span a duration-less tween across its clip, so answering
// from GSAP's 0.5s default reported the playhead outside a window the click
// then treated as clip-wide.
it("spans the clip for a duration-less tween when given the selection", () => {
const durationless = anim({ position: 0 });
const selection = { dataAttributes: { duration: "16" } } as unknown as DomEditSelection;
expect(isPlayheadWithinTween(durationless, 5)).toBe(false);
expect(isPlayheadWithinTween(durationless, 5, selection)).toBe(true);
expect(isPlayheadWithinTween(durationless, 20, selection)).toBe(false);
});
});
describe("buildExtendedKeyframes", () => {
@@ -77,11 +77,22 @@ export function animatedProps(anim: GsapAnimation | null): string[] {
* Whether the playhead sits inside an animation's tween range. When the tween's
* start can't be resolved we don't block (the percentage falls back to clip range,
* preserving prior behavior for elements without explicit timing).
*
* Pass the selection whenever the caller has one: a duration-less tween spans
* its clip, and answering from GSAP's 0.5s default reports the playhead outside
* a window the edit paths treat as clip-wide.
*/
export function isPlayheadWithinTween(anim: GsapAnimation, currentTime: number): boolean {
export function isPlayheadWithinTween(
anim: GsapAnimation,
currentTime: number,
selection?: DomEditSelection | null,
): boolean {
const start = resolveTweenStart(anim);
if (start === null) return true;
return isTimeWithinTween(currentTime, start, resolveTweenDuration(anim));
const duration = selection
? resolveEditableTweenDuration(anim, selection)
: resolveTweenDuration(anim);
return isTimeWithinTween(currentTime, start, duration);
}
/**