fix(studio): seek ruler clicks to the pressed position and round retime percentages

A ruler press with no pointer movement settled the playhead at t=0 instead
of the clicked time. handlePointerUp replays pendingClientXRef, which only
the pointermove path wrote, so a plain click fell back to the ref's initial
0 and overwrote the correct pointerdown seek. Seed the ref on pointerdown.

The keyframe retime move branch also returned the raw quotient while the
resize branch rounded to 3dp, so values like 74.81203007518799% landed in
the user's source and churned the diff on every drag. Round at the point of
computation so the no-op test and the written value agree.
This commit is contained in:
Miguel Angel Simon Sierra
2026-07-28 16:44:42 +02:00
parent c691869e22
commit 69020699df
4 changed files with 178 additions and 1 deletions
@@ -180,3 +180,45 @@ describe("resolveKeyframeRetime — guards", () => {
expect(r.pctRemap).toEqual([]);
});
});
describe("resolveKeyframeRetime — move percentages are rounded like the resize path", () => {
// The move branch used to return the raw quotient, so `74.81203007518799%`
// landed in the user's source and churned the diff on every drag.
const decimals = (n: number): number => String(n).split(".")[1]?.length ?? 0;
it("rounds a repeating quotient to 3dp", () => {
const r = resolveKeyframeRetime({
tweenStart: 2,
tweenDuration: 3,
keyframes: KEYFRAMES,
draggedTweenPct: 0,
dropAbsTime: 3, // (3-2)/3 = 33.333333333333336%
});
expect(r.kind).toBe("move");
expect(r.toTweenPct).toBe(33.333);
});
it("never emits more than 3 decimal places", () => {
for (const tweenDuration of [3, 7, 9, 11, 133]) {
const r = resolveKeyframeRetime({
tweenStart: 2,
tweenDuration,
keyframes: KEYFRAMES,
draggedTweenPct: 0,
dropAbsTime: 3,
});
expect(r.kind).toBe("move");
expect(decimals(r.toTweenPct ?? 0)).toBeLessThanOrEqual(3);
}
});
it("leaves an already-short percentage untouched", () => {
const r = resolveKeyframeRetime({
...WINDOW,
keyframes: KEYFRAMES,
draggedTweenPct: 0,
dropAbsTime: 3, // (3-2)/4 = exactly 25%
});
expect(r.toTweenPct).toBe(25);
});
});
@@ -121,7 +121,10 @@ export function resolveKeyframeRetime(opts: {
// Within the tween window → plain move (re-key the tween-%).
if (dropAbsTime >= tweenStart - EPSILON_TIME && dropAbsTime <= tweenEnd + EPSILON_TIME) {
const toTweenPct = clamp(((dropAbsTime - tweenStart) / tweenDuration) * 100, 0, 100);
// Round here, not at the return: the no-op test below and the value written
// to source must be the same number. The resize branch already rounds, so
// this keeps both write paths at the authored 3dp precision.
const toTweenPct = round3(clamp(((dropAbsTime - tweenStart) / tweenDuration) * 100, 0, 100));
if (Math.abs(toTweenPct - draggedTweenPct) < NOOP_EPSILON_PCT) return { kind: "noop" };
return { kind: "move", toTweenPct };
}