@@ -14,6 +14,7 @@ import {
1414} from "../components/editor/manualEditsTypes" ;
1515import { setElementGsapPosition , setElementGsapScale } from "../utils/elementGsap" ;
1616import { usePlayerStore } from "../player/store/playerStore" ;
17+ import { hasNonHoldTweenForElement } from "./gsapRuntimeKeyframes" ;
1718import { readAllAnimatedProperties , readGsapProperty } from "./gsapRuntimeReaders" ;
1819import {
1920 commitStaticGsapPosition ,
@@ -25,6 +26,7 @@ import {
2526 materializeIfDynamic ,
2627} from "./gsapDragCommit" ;
2728import type { GsapDragCommitCallbacks } from "./gsapDragCommit" ;
29+ import { computeDraggedGsapPosition } from "./draggedGsapPosition" ;
2830import { pickClosestToPlayhead , readGsapPositionFromIframe } from "./gsapPositionDetection" ;
2931import { commitWholePropertyOffset } from "./gsapWholePropertyOffsetCommit" ;
3032import { commitGsapPositionFromDrag } from "./gsapDragPositionCommit" ;
@@ -303,26 +305,44 @@ export async function tryGsapResizeIntercept(
303305 if ( committedScale ) {
304306 setElementGsapScale ( scaleDraftEl , committedScale . x , committedScale . y ) ;
305307 }
308+ // Measure from the pre-gesture position, not the draft one.
309+ //
310+ // The resize draft translates the element to keep the dragged corner under
311+ // the cursor, but the scale route never persists that translation — the
312+ // element renders back at its pre-gesture position as soon as the commit
313+ // lands. Measuring while the draft translation was still applied made the
314+ // residual carry the whole drag distance, and the position commit then
315+ // composed that residual onto the pre-gesture base (it reads the gesture's
316+ // own base attributes, not the live value), so the element landed a full
317+ // drag away from the drop point on every scale resize.
318+ const gsapPos = readGsapPositionFromIframe ( iframe , selector ) ?? { x : 0 , y : 0 } ;
319+ const { baseGsapX, baseGsapY } = computeDraggedGsapPosition (
320+ selection . element ,
321+ { x : 0 , y : 0 } ,
322+ gsapPos ,
323+ ) ;
324+ const base = { x : baseGsapX , y : baseGsapY } ;
325+ setElementGsapPosition ( scaleDraftEl , base . x , base . y ) ;
306326 const post = scaleDraftEl . getBoundingClientRect ( ) ;
307327 const residual = { x : scaleDraftDropPoint . x - post . x , y : scaleDraftDropPoint . y - post . y } ;
308328 if ( ! Number . isFinite ( residual . x ) || ! Number . isFinite ( residual . y ) ) return ;
309329 if ( Math . abs ( residual . x ) < 0.5 && Math . abs ( residual . y ) < 0.5 ) {
310- logResize ( "scale-finalize" , { skipped : "already-on-drop-point" , residual } ) ;
330+ logResize ( "scale-finalize" , { skipped : "already-on-drop-point" , residual, base } ) ;
311331 return ;
312332 }
313- const gsapPos = readGsapPositionFromIframe ( iframe , selector ) ?? { x : 0 , y : 0 } ;
314333 // The ONE corrected position — rounded once so the live runtime and the
315334 // persisted file agree exactly (commitStaticGsapPosition composes the same
316335 // rounded value from this delta).
317336 const corrected = {
318- x : Math . round ( gsapPos . x + residual . x ) ,
319- y : Math . round ( gsapPos . y + residual . y ) ,
337+ x : Math . round ( base . x + residual . x ) ,
338+ y : Math . round ( base . y + residual . y ) ,
320339 } ;
321340 logResize ( "scale-finalize" , {
322341 dropPoint : scaleDraftDropPoint ,
323342 post : { x : post . x , y : post . y } ,
324343 residual,
325344 gsapPos,
345+ base,
326346 corrected,
327347 } ) ;
328348 // Correct the LIVE runtime NOW, synchronously: the soft reload above just
@@ -339,8 +359,8 @@ export async function tryGsapResizeIntercept(
339359 ? await fetchFallbackAnimations ( )
340360 : ( resolved ?. animations ?? animations ) ;
341361 // Delta chosen so the drag-path math composes back to exactly `corrected`
342- // (no drag scratch attrs exist during a resize, so base = gsapPos) .
343- const delta = { x : corrected . x - gsapPos . x , y : corrected . y - gsapPos . y } ;
362+ // — it adds this onto the same base the measurement above used .
363+ const delta = { x : corrected . x - base . x , y : corrected . y - base . y } ;
344364 // An element whose position is animated needs the correction written into
345365 // that animation, at the playhead, or the tween renders its own value a
346366 // frame later and the element leaves the drop point anyway. This used to
@@ -355,14 +375,14 @@ export async function tryGsapResizeIntercept(
355375 ) ;
356376 if ( positionTween ) {
357377 logResize ( "scale-finalize" , { route : "position-keyframe" , tweenId : positionTween . id } ) ;
358- await commitGsapPositionFromDrag ( selection , positionTween , delta , gsapPos , iframe , selector , {
378+ await commitGsapPositionFromDrag ( selection , positionTween , delta , base , iframe , selector , {
359379 commitMutation,
360380 fetchAnimations : fetchFallbackAnimations ,
361381 } ) ;
362382 return ;
363383 }
364384 const existingSet = findExistingPositionWrite ( currentAnimations , selector , selection . element ) ;
365- await commitStaticGsapPosition ( selection , delta , gsapPos , selector , existingSet , {
385+ await commitStaticGsapPosition ( selection , delta , base , selector , existingSet , {
366386 commitMutation,
367387 fetchAnimations : fetchFallbackAnimations ,
368388 } ) ;
0 commit comments