fix(studio): re-sync soft-reloaded timelines to the studio's own scrub time

Dragging a motion-path keyframe node committed correctly, but the soft
reload that refreshes the preview re-seeked the freshly rebuilt GSAP
timeline using the iframe's raw __player.getTime(), which can lag the
studio's authoritative currentTime right after a keyframe drag parks the
playhead. The stale seek left the element (and its selection/motion-path
overlay) rendered at an unrelated position after the edit.

applySoftReload now takes the caller's currentTime instead of trusting the
iframe's own clock, and the re-seek runs before __hfForceTimelineRebind so
its internal force-render picks up the correct time.
This commit is contained in:
Miguel Angel Simon Sierra
2026-07-01 21:34:38 -07:00
parent ae50327bdb
commit 70606f840d
4 changed files with 41 additions and 11 deletions
@@ -100,6 +100,18 @@ describe("applySoftReload", () => {
expect(contentWindow.__hfStudioManualEditsApply).toHaveBeenCalled();
});
it("seeks to the caller-supplied currentTime override instead of the iframe's own __player.getTime()", () => {
// Regression: the iframe's raw __player.getTime() (2.0 here, per the mock)
// can desync from the studio's authoritative scrub position — e.g. a
// keyframe-node drag parks the playhead via the store before this reload's
// async commit resolves. The rebuilt timeline must re-seek to the caller's
// value, not the iframe's possibly-stale one.
const { iframe, contentWindow } = buildMockIframe();
const result = applySoftReload(iframe, SCRIPT_TEXT, undefined, 0);
expect(result).toBe("applied");
expect(contentWindow.__player.seek).toHaveBeenCalledWith(0);
});
it("strips a stale inline transform from an orphaned (non-timeline-child) element", () => {
// Repro: an element dragged via gsap.set whose keyframes were then removed is
// no longer a timeline child, so the timeline-children sweep misses it. Its