@hyperframes/studio 0.8.124 → 0.8.125

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. package/dist/assets/{hyperframes-player-ColOZ_78.js → hyperframes-player-CmIh-wq9.js} +1 -1
  2. package/dist/assets/{index-jQEqFKMk.js → index-4LRR6o2F.js} +1 -1
  3. package/dist/assets/{index-BuSTrK-Z.js → index-CrYoEZ5p.js} +139 -139
  4. package/dist/assets/{index-CVuUfURE.js → index-yCK8osBg.js} +1 -1
  5. package/dist/index.d.ts +43 -35
  6. package/dist/index.html +1 -1
  7. package/dist/index.js +213 -94
  8. package/dist/index.js.map +1 -1
  9. package/package.json +8 -8
  10. package/src/components/editor/domEditOverlayGestures.ts +2 -0
  11. package/src/components/editor/domEditOverlayStartGesture.ts +6 -0
  12. package/src/components/editor/dragUndoPaint.test.ts +170 -2
  13. package/src/components/editor/gestureUndoRevert.ts +104 -0
  14. package/src/components/editor/manualOffsetDrag.ts +1 -12
  15. package/src/components/editor/useDomEditNudge.ts +1 -1
  16. package/src/components/editor/useDomEditOverlayGestures.ts +9 -5
  17. package/src/hooks/gestureTransaction.test.ts +60 -0
  18. package/src/hooks/gestureTransaction.ts +5 -2
  19. package/src/hooks/gsapGestureOutcome.ts +9 -2
  20. package/src/hooks/gsapResizeDropPoint.test.ts +39 -1
  21. package/src/hooks/gsapResizeElementSize.test.tsx +27 -0
  22. package/src/hooks/gsapResizeIntercept.ts +55 -68
  23. package/src/hooks/gsapScriptCommitTypes.ts +2 -0
  24. package/src/hooks/useGsapAwareEditing.groupPlan.test.tsx +29 -1
  25. package/src/hooks/useGsapAwareEditing.test.tsx +1 -0
  26. package/src/hooks/useGsapAwareEditing.ts +21 -25
  27. package/src/hooks/useGsapAwareGroupMove.ts +4 -6
  28. package/src/hooks/useGsapScriptCommits.test.tsx +42 -1
  29. package/src/hooks/useGsapScriptCommits.ts +12 -3
  30. package/src/utils/studioPendingEdits.test.ts +52 -0
  31. package/src/utils/studioPendingEdits.ts +55 -10
@@ -1 +1 @@
1
- import{g as P}from"./index-BuSTrK-Z.js";function j(c,d){for(var s=0;s<d.length;s++){const a=d[s];if(typeof a!="string"&&!Array.isArray(a)){for(const i in a)if(i!=="default"&&!(i in c)){const l=Object.getOwnPropertyDescriptor(a,i);l&&Object.defineProperty(c,i,l.get?l:{enumerable:!0,get:()=>a[i]})}}}return Object.freeze(Object.defineProperty(c,Symbol.toStringTag,{value:"Module"}))}var v={},w;function k(){if(w)return v;w=1,Object.defineProperty(v,"__esModule",{value:!0}),v.default=d;var c=window.OfflineAudioContext||window.webkitOfflineAudioContext;function d(e){var r=a(e);return r.start(0),[i,y,O(e.sampleRate),s].reduce(function(t,o){return o(t)},r.buffer.getChannelData(0))}function s(e){return e.sort(function(r,t){return t.count-r.count}).splice(0,5)[0].tempo}function a(e){var r=e.length,t=e.numberOfChannels,o=e.sampleRate,n=new c(t,r,o),u=n.createBufferSource();u.buffer=e;var f=n.createBiquadFilter();return f.type="lowpass",u.connect(f),f.connect(n.destination),u}function i(e){for(var r=[],t=.9,o=.3,n=15;r.length<n&&t>=o;)r=l(e,t),t-=.05;if(r.length<n)throw new Error("Could not find enough samples for a reliable detection.");return r}function l(e,r){for(var t=[],o=0,n=e.length;o<n;o+=1)e[o]>r&&(t.push(o),o+=1e4);return t}function y(e){var r=[];return e.forEach(function(t,o){for(var n=function(x){var g=e[o+x]-t,_=r.some(function(h){if(h.interval===g)return h.count+=1});_||r.push({interval:g,count:1})},u=0;u<10;u+=1)n(u)}),r}function O(e){return function(r){var t=[];return r.forEach(function(o){if(o.interval!==0){for(var n=60/(o.interval/e);n<90;)n*=2;for(;n>180;)n/=2;n=Math.round(n);var u=t.some(function(f){if(f.tempo===n)return f.count+=o.count});u||t.push({tempo:n,count:o.count})}}),t}}return v}var p,b;function q(){return b||(b=1,p=k().default),p}var m=q();const A=P(m),D=j({__proto__:null,default:A},[m]);export{D as i};
1
+ import{g as P}from"./index-CrYoEZ5p.js";function j(c,d){for(var s=0;s<d.length;s++){const a=d[s];if(typeof a!="string"&&!Array.isArray(a)){for(const i in a)if(i!=="default"&&!(i in c)){const l=Object.getOwnPropertyDescriptor(a,i);l&&Object.defineProperty(c,i,l.get?l:{enumerable:!0,get:()=>a[i]})}}}return Object.freeze(Object.defineProperty(c,Symbol.toStringTag,{value:"Module"}))}var v={},w;function k(){if(w)return v;w=1,Object.defineProperty(v,"__esModule",{value:!0}),v.default=d;var c=window.OfflineAudioContext||window.webkitOfflineAudioContext;function d(e){var r=a(e);return r.start(0),[i,y,O(e.sampleRate),s].reduce(function(t,o){return o(t)},r.buffer.getChannelData(0))}function s(e){return e.sort(function(r,t){return t.count-r.count}).splice(0,5)[0].tempo}function a(e){var r=e.length,t=e.numberOfChannels,o=e.sampleRate,n=new c(t,r,o),u=n.createBufferSource();u.buffer=e;var f=n.createBiquadFilter();return f.type="lowpass",u.connect(f),f.connect(n.destination),u}function i(e){for(var r=[],t=.9,o=.3,n=15;r.length<n&&t>=o;)r=l(e,t),t-=.05;if(r.length<n)throw new Error("Could not find enough samples for a reliable detection.");return r}function l(e,r){for(var t=[],o=0,n=e.length;o<n;o+=1)e[o]>r&&(t.push(o),o+=1e4);return t}function y(e){var r=[];return e.forEach(function(t,o){for(var n=function(x){var g=e[o+x]-t,_=r.some(function(h){if(h.interval===g)return h.count+=1});_||r.push({interval:g,count:1})},u=0;u<10;u+=1)n(u)}),r}function O(e){return function(r){var t=[];return r.forEach(function(o){if(o.interval!==0){for(var n=60/(o.interval/e);n<90;)n*=2;for(;n>180;)n/=2;n=Math.round(n);var u=t.some(function(f){if(f.tempo===n)return f.count+=o.count});u||t.push({tempo:n,count:o.count})}}),t}}return v}var p,b;function q(){return b||(b=1,p=k().default),p}var m=q();const A=P(m),D=j({__proto__:null,default:A},[m]);export{D as i};
package/dist/index.d.ts CHANGED
@@ -915,6 +915,48 @@ type TimelineMoveUpdates = Pick<TimelineElement, "start" | "track"> & {
915
915
  stackingReorder?: TimelineStackingReorderIntent | null;
916
916
  };
917
917
 
918
+ declare class StudioSaveHttpError extends Error {
919
+ readonly statusCode: number;
920
+ readonly alreadyToasted: boolean;
921
+ constructor(message: string, statusCode: number, options?: {
922
+ alreadyToasted?: boolean;
923
+ });
924
+ }
925
+ declare class StudioFileConflictError extends StudioSaveHttpError {
926
+ readonly filePath: string;
927
+ readonly currentVersion: string | null;
928
+ readonly currentContent: string | null;
929
+ readonly attemptedContent: string;
930
+ constructor(input: {
931
+ filePath: string;
932
+ currentVersion: string | null;
933
+ currentContent: string | null;
934
+ attemptedContent: string;
935
+ });
936
+ }
937
+ type StudioSaveDrainResult<Failure = unknown> = {
938
+ status: "clean";
939
+ } | {
940
+ status: "conflict";
941
+ error: StudioFileConflictError;
942
+ } | {
943
+ status: "failed";
944
+ error: Failure;
945
+ };
946
+
947
+ type StudioPendingEditsDrainResult = StudioSaveDrainResult;
948
+ interface StudioEditInFlight {
949
+ reverted: () => boolean;
950
+ within: <T>(run: () => T) => T;
951
+ drawUnlessUndone: (draw: () => void) => void;
952
+ drawKeepingUndone: <T>(draw: () => T) => T;
953
+ markSaved: () => void;
954
+ }
955
+ declare function revertNewestStudioPendingEdit(): (() => void) | null;
956
+ declare function flushStudioPendingEdits({ onlyCurrent, }?: {
957
+ onlyCurrent?: boolean | undefined;
958
+ }): Promise<StudioPendingEditsDrainResult>;
959
+
918
960
  /** Value-only channels a `tl.set(...)` patch may touch. */
919
961
  interface SetPatchProps {
920
962
  x?: number;
@@ -984,6 +1026,7 @@ interface CommitMutationOptions {
984
1026
  pending: boolean;
985
1027
  };
986
1028
  beforeReload?: () => void;
1029
+ pendingEdit?: StudioEditInFlight;
987
1030
  /**
988
1031
  * Serialize this commit against others sharing the same key. Used to chain
989
1032
  * per-animationId GSAP meta updates. Every commit independently takes the
@@ -2881,41 +2924,6 @@ declare function useTimelineEditing({ projectId, activeCompPath, timelineElement
2881
2924
  restoreLiveLanes: (restore: Parameters<(restore: LiveLaneRestore) => void>[0]) => void;
2882
2925
  };
2883
2926
 
2884
- declare class StudioSaveHttpError extends Error {
2885
- readonly statusCode: number;
2886
- readonly alreadyToasted: boolean;
2887
- constructor(message: string, statusCode: number, options?: {
2888
- alreadyToasted?: boolean;
2889
- });
2890
- }
2891
- declare class StudioFileConflictError extends StudioSaveHttpError {
2892
- readonly filePath: string;
2893
- readonly currentVersion: string | null;
2894
- readonly currentContent: string | null;
2895
- readonly attemptedContent: string;
2896
- constructor(input: {
2897
- filePath: string;
2898
- currentVersion: string | null;
2899
- currentContent: string | null;
2900
- attemptedContent: string;
2901
- });
2902
- }
2903
- type StudioSaveDrainResult<Failure = unknown> = {
2904
- status: "clean";
2905
- } | {
2906
- status: "conflict";
2907
- error: StudioFileConflictError;
2908
- } | {
2909
- status: "failed";
2910
- error: Failure;
2911
- };
2912
-
2913
- type StudioPendingEditsDrainResult = StudioSaveDrainResult;
2914
- declare function revertNewestStudioPendingEdit(): (() => void) | null;
2915
- declare function flushStudioPendingEdits({ onlyCurrent, }?: {
2916
- onlyCurrent?: boolean | undefined;
2917
- }): Promise<StudioPendingEditsDrainResult>;
2918
-
2919
2927
  interface HistoryResult {
2920
2928
  ok: boolean;
2921
2929
  reason?: string;
package/dist/index.html CHANGED
@@ -14,7 +14,7 @@
14
14
  document.documentElement.dataset.theme = "paper";
15
15
  }
16
16
  </script>
17
- <script type="module" crossorigin src="/assets/index-BuSTrK-Z.js"></script>
17
+ <script type="module" crossorigin src="/assets/index-CrYoEZ5p.js"></script>
18
18
  <link rel="stylesheet" crossorigin href="/assets/index-D11atkBt.css">
19
19
  </head>
20
20
  <body>
package/dist/index.js CHANGED
@@ -11834,7 +11834,10 @@ function asElement(target) {
11834
11834
  var STUDIO_FLUSH_PENDING_EDITS_EVENT = "hf-studio-flush-pending-edits";
11835
11835
  var pendingEdits = /* @__PURE__ */ new Map();
11836
11836
  var NOT_SAVED = () => Promise.resolve(false);
11837
- var adopting = false;
11837
+ var adopting = null;
11838
+ function adoptingStudioPendingEdit() {
11839
+ return adopting;
11840
+ }
11838
11841
  function waitForPostBlurEffects() {
11839
11842
  return new Promise((resolve) => setTimeout(resolve, 0));
11840
11843
  }
@@ -11877,7 +11880,7 @@ function trackStudioPendingEdit(result) {
11877
11880
  if (!result) return void 0;
11878
11881
  const promise = Promise.resolve(result);
11879
11882
  if (adopting) return promise;
11880
- pendingEdits.set(promise, { revert: null, landed: NOT_SAVED });
11883
+ pendingEdits.set(promise, { revert: null, landed: NOT_SAVED, redraws: [], showAgain: null });
11881
11884
  promise.then(
11882
11885
  () => pendingEdits.delete(promise),
11883
11886
  () => pendingEdits.delete(promise)
@@ -11897,25 +11900,49 @@ function beginStudioPendingEdit(revert) {
11897
11900
  const entry2 = pendingEdits.get(promise);
11898
11901
  entry2.revert = revert;
11899
11902
  let landed = Promise.resolve(false);
11903
+ let saved2 = false;
11900
11904
  entry2.landed = () => landed;
11905
+ const inFlight = {
11906
+ reverted: () => entry2.revert === null && revert !== null,
11907
+ within(run) {
11908
+ const outer = adopting;
11909
+ adopting = inFlight;
11910
+ try {
11911
+ return run();
11912
+ } finally {
11913
+ adopting = outer;
11914
+ }
11915
+ },
11916
+ drawUnlessUndone(draw) {
11917
+ if (inFlight.reverted()) entry2.redraws.push(draw);
11918
+ else draw();
11919
+ },
11920
+ drawKeepingUndone(draw) {
11921
+ if (!inFlight.reverted()) return draw();
11922
+ entry2.showAgain?.();
11923
+ try {
11924
+ return draw();
11925
+ } finally {
11926
+ entry2.showAgain = revert();
11927
+ }
11928
+ },
11929
+ markSaved: () => void (saved2 = true)
11930
+ };
11901
11931
  return {
11902
11932
  settle,
11903
- reverted: () => entry2.revert === null && revert !== null,
11904
- // Only what `start` registers synchronously is adopted; a registration after an await is a newer edit.
11933
+ reverted: inFlight.reverted,
11934
+ // Only what `start` registers synchronously is adopted; a later write joins only through `within`.
11905
11935
  adopt(start) {
11906
- adopting = true;
11907
11936
  try {
11908
- const committed = start();
11937
+ const committed = inFlight.within(start);
11909
11938
  landed = Promise.resolve(committed).then(
11910
11939
  () => true,
11911
- () => false
11940
+ () => saved2
11912
11941
  );
11913
11942
  return committed;
11914
11943
  } catch (error) {
11915
11944
  settle();
11916
11945
  throw error;
11917
- } finally {
11918
- adopting = false;
11919
11946
  }
11920
11947
  }
11921
11948
  };
@@ -11925,7 +11952,14 @@ function paintBackNewestStudioPendingEdit() {
11925
11952
  const revert = newest?.revert;
11926
11953
  if (!newest || !revert) return null;
11927
11954
  newest.revert = null;
11928
- return { showAgain: revert(), landed: newest.landed };
11955
+ newest.showAgain = revert();
11956
+ return {
11957
+ showAgain: () => {
11958
+ newest.showAgain?.();
11959
+ for (const redraw of newest.redraws.splice(0)) redraw();
11960
+ },
11961
+ landed: newest.landed
11962
+ };
11929
11963
  }
11930
11964
  function revertNewestStudioPendingEdit() {
11931
11965
  return paintBackNewestStudioPendingEdit()?.showAgain ?? null;
@@ -37150,14 +37184,6 @@ function restoreManualOffsetDragMember(member) {
37150
37184
  }
37151
37185
  endStudioManualEditGesture(member.element, member.gestureToken);
37152
37186
  }
37153
- function manualOffsetMoveRevert(members) {
37154
- if (!members.every((member) => member.plainTranslate)) return null;
37155
- return () => {
37156
- const shown2 = members.map((member) => captureStudioPathOffset(member.element));
37157
- for (const member of members) restoreStudioPathOffset(member.element, member.initialPathOffset);
37158
- return () => members.forEach((member, i) => restoreStudioPathOffset(member.element, shown2[i]));
37159
- };
37160
- }
37161
37187
  function restoreManualOffsetDragMembers(members) {
37162
37188
  for (const member of members) {
37163
37189
  restoreManualOffsetDragMember(member);
@@ -37202,6 +37228,70 @@ function resumeGsapTimelines(element) {
37202
37228
  win.__player?.seek?.(t2);
37203
37229
  }
37204
37230
 
37231
+ // src/components/editor/gestureUndoRevert.ts
37232
+ var PLACEMENT = ["position", "left", "top"];
37233
+ var placementOf = (element) => PLACEMENT.map((prop) => element.style.getPropertyValue(prop));
37234
+ function showPlacement(element, placement) {
37235
+ PLACEMENT.forEach((prop, i) => {
37236
+ if (placement[i]) element.style.setProperty(prop, placement[i]);
37237
+ else element.style.removeProperty(prop);
37238
+ });
37239
+ }
37240
+ function readMemberPosition(member) {
37241
+ const gsap3 = member.plainTranslate ? null : getOffsetDragGsap(member.element);
37242
+ return {
37243
+ offset: captureStudioPathOffset(member.element),
37244
+ gsap: gsap3 && {
37245
+ x: Number(gsap3.getProperty(member.element, "x")),
37246
+ y: Number(gsap3.getProperty(member.element, "y"))
37247
+ },
37248
+ placement: placementOf(member.element)
37249
+ };
37250
+ }
37251
+ function showMemberPosition(member, position) {
37252
+ restoreStudioPathOffset(member.element, position.offset);
37253
+ showPlacement(member.element, position.placement);
37254
+ if (position.gsap) getOffsetDragGsap(member.element)?.set(member.element, { ...position.gsap });
37255
+ if (!member.plainTranslate)
37256
+ stampGestureBase(member.element, member.initialOffset, member.baseGsap);
37257
+ }
37258
+ function manualOffsetMoveRevert(members) {
37259
+ const startPlacement = members.map((member) => placementOf(member.element));
37260
+ return () => {
37261
+ const shown2 = members.map(readMemberPosition);
37262
+ members.forEach(
37263
+ (member, i) => showMemberPosition(member, {
37264
+ offset: member.initialPathOffset,
37265
+ gsap: member.plainTranslate ? null : member.baseGsap,
37266
+ placement: startPlacement[i]
37267
+ })
37268
+ );
37269
+ return () => members.forEach((member, i) => showMemberPosition(member, shown2[i]));
37270
+ };
37271
+ }
37272
+ var GSAP_LOOK_PROPS = ["x", "y", "rotation", "scaleX", "scaleY"];
37273
+ function readElementLook(element, gsapOwned) {
37274
+ const gsap3 = gsapOwned ? getOffsetDragGsap(element) : null;
37275
+ return {
37276
+ style: element.getAttribute("style"),
37277
+ gsap: gsap3 && Object.fromEntries(
37278
+ GSAP_LOOK_PROPS.map((prop) => [prop, Number(gsap3.getProperty(element, prop))])
37279
+ )
37280
+ };
37281
+ }
37282
+ function showElementLook(element, look) {
37283
+ if (look.style === null) element.removeAttribute("style");
37284
+ else element.setAttribute("style", look.style);
37285
+ if (look.gsap) getOffsetDragGsap(element)?.set(element, { ...look.gsap });
37286
+ }
37287
+ function elementLookRevert(element, start) {
37288
+ return () => {
37289
+ const shown2 = readElementLook(element, start.gsap !== null);
37290
+ showElementLook(element, start);
37291
+ return () => showElementLook(element, shown2);
37292
+ };
37293
+ }
37294
+
37205
37295
  // src/components/editor/rotationDraft.ts
37206
37296
  function lastRuleTransform(element) {
37207
37297
  let value = "";
@@ -37980,6 +38070,10 @@ function startGesture(kind, e, opts, options) {
37980
38070
  const rawContentScaleY = rect.height / (rect.editScaleY * actualHeight);
37981
38071
  const contentScaleX = Number.isFinite(rawContentScaleX) && rawContentScaleX > 0 ? rawContentScaleX : 1;
37982
38072
  const contentScaleY = Number.isFinite(rawContentScaleY) && rawContentScaleY > 0 ? rawContentScaleY : 1;
38073
+ const initialLook = readElementLook(
38074
+ sel.element,
38075
+ kind !== "drag" && !editsPlainCss(sel.element, kind === "rotate" ? "rotate" : "resize")
38076
+ );
37983
38077
  let initialPathOffset = captureStudioPathOffset(sel.element);
37984
38078
  let manualEditDragToken;
37985
38079
  let pathOffsetMember;
@@ -38059,6 +38153,7 @@ function startGesture(kind, e, opts, options) {
38059
38153
  initialPathOffset,
38060
38154
  initialRotation: captureStudioRotation(sel.element),
38061
38155
  initialBoxSize: captureStudioBoxSize(sel.element),
38156
+ initialLook,
38062
38157
  pathOffsetMember,
38063
38158
  originLeft: rect.left,
38064
38159
  originTop: rect.top,
@@ -38560,11 +38655,13 @@ function createDomEditOverlayGestureHandlers(opts) {
38560
38655
  }
38561
38656
  applyRotationDraft(sel.element, finalRotation.angle, g.plainRotation);
38562
38657
  const commit = { ...finalRotation, plain: g.plainRotation };
38563
- void Promise.resolve(opts.onRotationCommitRef.current(sel, commit)).then((result) => trackPreviewEditResult("rotate", "drag", result)).catch((error) => {
38658
+ const edit = beginStudioPendingEdit(elementLookRevert(sel.element, g.initialLook));
38659
+ const saved2 = Promise.resolve(edit.adopt(() => opts.onRotationCommitRef.current(sel, commit))).then((result) => trackPreviewEditResult("rotate", "drag", result)).catch((error) => {
38564
38660
  logGestureCommitFailure("rotate commit failed", error);
38565
38661
  if (g.manualEditDragToken && isStudioManualEditGestureCurrent(sel.element, g.manualEditDragToken))
38566
38662
  restoreRotation();
38567
38663
  }).finally(() => endStudioManualEditGesture(sel.element, g.manualEditDragToken));
38664
+ edit.settle(saved2);
38568
38665
  } else if (g.kind === "drag") {
38569
38666
  opts.suppressNextBoxClickRef.current = true;
38570
38667
  const dx = g.lastSnappedDx ?? e.clientX - g.startX;
@@ -38624,14 +38721,15 @@ function createDomEditOverlayGestureHandlers(opts) {
38624
38721
  restoreStudioBoxSize(sel.element, g.initialBoxSize);
38625
38722
  if (finalOffset) restoreStudioPathOffset(sel.element, g.initialPathOffset);
38626
38723
  };
38627
- void Promise.resolve(
38628
- opts.onBoxSizeCommitRef.current(sel, finalSize, finalOffset ?? void 0, restore2, member)
38629
- ).then((result) => trackPreviewEditResult("resize", "drag", result)).catch((error) => {
38724
+ const commitSize = () => opts.onBoxSizeCommitRef.current(sel, finalSize, finalOffset ?? void 0, restore2, member);
38725
+ const edit = beginStudioPendingEdit(elementLookRevert(sel.element, g.initialLook));
38726
+ const saved2 = Promise.resolve(edit.adopt(commitSize)).then((result) => trackPreviewEditResult("resize", "drag", result)).catch((error) => {
38630
38727
  logGestureCommitFailure("resize commit failed", error);
38631
38728
  }).finally(() => {
38632
38729
  if (member) endManualOffsetDragMembers([member]);
38633
38730
  else endStudioManualEditGesture(sel.element, g.manualEditDragToken);
38634
38731
  });
38732
+ edit.settle(saved2);
38635
38733
  logResizeSettle(sel.element, "post-release");
38636
38734
  }
38637
38735
  };
@@ -44465,6 +44563,8 @@ function trackKeyframeCommit(mutations, result, options, memberOptions) {
44465
44563
 
44466
44564
  // src/hooks/gsapGestureOutcome.ts
44467
44565
  function observeGsapGesture(writer) {
44566
+ const edit = adoptingStudioPendingEdit();
44567
+ const join = (run) => edit ? edit.within(run) : run();
44468
44568
  let changed = false;
44469
44569
  let pendingResults = 0;
44470
44570
  const actions = /* @__PURE__ */ new Set();
@@ -44472,12 +44572,14 @@ function observeGsapGesture(writer) {
44472
44572
  pendingResults += 1;
44473
44573
  return {
44474
44574
  ...options,
44575
+ ...edit && { pendingEdit: edit },
44475
44576
  keyframeTelemetry: false,
44476
44577
  onResult: (result) => {
44477
44578
  pendingResults -= 1;
44478
44579
  options.onResult?.(result);
44479
44580
  if (!result.ok || result.changed !== true) return;
44480
44581
  changed = true;
44582
+ edit?.markSaved();
44481
44583
  const members = changedMutationIndices(
44482
44584
  result,
44483
44585
  calls.length,
@@ -44493,16 +44595,18 @@ function observeGsapGesture(writer) {
44493
44595
  };
44494
44596
  let commit = null;
44495
44597
  if (writer) {
44496
- commit = (selection, mutation, options) => writer(selection, mutation, observe([{ selection, mutation, options }], options));
44598
+ commit = (selection, mutation, options) => join(() => writer(selection, mutation, observe([{ selection, mutation, options }], options)));
44497
44599
  if (writer.batch) {
44498
44600
  const batch = writer.batch;
44499
- commit.batch = (calls, options) => batch(calls, observe(calls, options));
44601
+ commit.batch = (calls, options) => join(() => batch(calls, observe(calls, options)));
44500
44602
  }
44501
44603
  }
44502
44604
  return {
44503
44605
  commit,
44606
+ drawKeepingUndone: (draw) => edit ? edit.drawKeepingUndone(draw) : draw(),
44504
44607
  recordDomResult: (result) => {
44505
44608
  changed ||= result?.changed === true;
44609
+ if (result?.changed) edit?.markSaved();
44506
44610
  },
44507
44611
  finish: (domChanged = false) => {
44508
44612
  if (pendingResults !== 0) return { ok: true, changed: false };
@@ -44746,7 +44850,8 @@ function runGestureTransaction(tx) {
44746
44850
  logCommit("start", { label: tx.label, coalesceKey });
44747
44851
  tx.settle();
44748
44852
  logCommit("settled", { label: tx.label, coalesceKey });
44749
- const before = !tx.skipPixelAssert ? readPixelRect(tx.element) : null;
44853
+ const measure = () => tx.draw ? tx.draw(() => readPixelRect(tx.element)) : readPixelRect(tx.element);
44854
+ const before = !tx.skipPixelAssert ? measure() : null;
44750
44855
  const commit = (commitMutation) => {
44751
44856
  const wrapped = (selection, mutation, options) => {
44752
44857
  mutationCount += 1;
@@ -44767,7 +44872,7 @@ function runGestureTransaction(tx) {
44767
44872
  const durationMs = Math.round(performance.now() - startedAt2);
44768
44873
  logCommit("persisted", { label: tx.label, coalesceKey });
44769
44874
  if (before) {
44770
- const after = readPixelRect(tx.element);
44875
+ const after = measure();
44771
44876
  const delta = pixelDelta(before, after);
44772
44877
  if (exceedsPixelTolerance(delta)) {
44773
44878
  logCommit("persist-changed-pixels", { label: tx.label, before, after, delta });
@@ -81180,13 +81285,22 @@ async function runMutationRequest(mutations, options, showToast, request) {
81180
81285
  function finishUnchangedMutation(iframe, result, options, reloadPreview) {
81181
81286
  if (result.changed !== false) return false;
81182
81287
  if (!options.skipReload && (instantPatchesFor(options).length > 0 || options.previewFallbackLatch?.pending)) {
81183
- applyPreviewSync(iframe, result, options, reloadPreview);
81288
+ redrawUnlessPaintedBack(
81289
+ options,
81290
+ () => applyPreviewSync(iframe, result, options, reloadPreview)
81291
+ );
81184
81292
  }
81185
81293
  return true;
81186
81294
  }
81295
+ function redrawUnlessPaintedBack(options, redraw) {
81296
+ if (options.pendingEdit) options.pendingEdit.drawUnlessUndone(redraw);
81297
+ else redraw();
81298
+ }
81187
81299
  function refreshMutationPreview(iframe, result, options, reloadPreview, onCacheInvalidate, nestedFiles) {
81188
- options.beforeReload?.();
81189
- applyPreviewSync(iframe, result, options, reloadPreview, nestedFiles);
81300
+ redrawUnlessPaintedBack(options, () => {
81301
+ options.beforeReload?.();
81302
+ applyPreviewSync(iframe, result, options, reloadPreview, nestedFiles);
81303
+ });
81190
81304
  onCacheInvalidate();
81191
81305
  }
81192
81306
  function isActiveCommitTarget(projectIdRef, activeCompPathRef, projectId, compositionPath) {
@@ -82208,12 +82322,10 @@ function useGsapAwareGroupMove({
82208
82322
  renderOnCommit = index === lastScriptWrite;
82209
82323
  const plain = offsetMembers.get(selection);
82210
82324
  if (plain !== void 0) {
82211
- const result = await stageElementPositionOffset(
82212
- selection,
82213
- next,
82214
- plain,
82215
- coalesceKey
82216
- ).save();
82325
+ const staged = writes.drawKeepingUndone(
82326
+ () => stageElementPositionOffset(selection, next, plain, coalesceKey)
82327
+ );
82328
+ const result = await staged.save();
82217
82329
  writes.recordDomResult(result);
82218
82330
  continue;
82219
82331
  }
@@ -82341,7 +82453,7 @@ function tweenUsesScaleLonghands(anim) {
82341
82453
  );
82342
82454
  return inKeyframes || Object.keys(anim?.properties ?? {}).some(isLonghand);
82343
82455
  }
82344
- async function tryGsapResizeIntercept(selection, size, animations, iframe, commitMutation, fetchFallbackAnimations, dragOffset) {
82456
+ async function tryGsapResizeIntercept(selection, size, animations, iframe, commitMutation, fetchFallbackAnimations, dragOffset, draw = (run) => run()) {
82345
82457
  const fetchedAnimations = fetchFallbackAnimations ? await fetchFallbackAnimations() : [];
82346
82458
  const outcome = preflightGsapResizeIntercept(selection, animations, iframe, fetchedAnimations);
82347
82459
  if (outcome.status === "blocked") return outcome;
@@ -82437,7 +82549,7 @@ async function tryGsapResizeIntercept(selection, size, animations, iframe, commi
82437
82549
  scaleDraftEl = el;
82438
82550
  committedScale = useScaleLonghands ? { x: newScaleX, y: newScaleY } : { x: newScaleX, y: newScaleX };
82439
82551
  if (el) {
82440
- const dropRect = el.getBoundingClientRect();
82552
+ const dropRect = draw(() => el.getBoundingClientRect());
82441
82553
  scaleDraftDropPoint = { x: dropRect.x, y: dropRect.y };
82442
82554
  }
82443
82555
  } else {
@@ -82449,39 +82561,50 @@ async function tryGsapResizeIntercept(selection, size, animations, iframe, commi
82449
82561
  }
82450
82562
  const finalizeScaleResizeCommit = async () => {
82451
82563
  if (!scaleDraftEl) return false;
82452
- clearStudioBoxSize(scaleDraftEl);
82453
- if (!scaleDraftDropPoint || !selector) return false;
82454
- if (committedScale) {
82455
- setElementGsapScale(scaleDraftEl, committedScale.x, committedScale.y);
82456
- }
82457
- const gsapPos = readGsapPositionFromIframe(iframe, selector) ?? { x: 0, y: 0 };
82458
- const { baseGsapX, baseGsapY } = computeDraggedGsapPosition(
82459
- selection.element,
82460
- { x: 0, y: 0 },
82461
- gsapPos
82462
- );
82463
- const base2 = { x: baseGsapX, y: baseGsapY };
82464
- setElementGsapPosition(scaleDraftEl, base2.x, base2.y);
82465
- const post2 = scaleDraftEl.getBoundingClientRect();
82466
- const residual = { x: scaleDraftDropPoint.x - post2.x, y: scaleDraftDropPoint.y - post2.y };
82467
- if (!Number.isFinite(residual.x) || !Number.isFinite(residual.y)) return false;
82468
- if (Math.abs(residual.x) < 0.5 && Math.abs(residual.y) < 0.5) {
82469
- logResize("scale-finalize", { skipped: "already-on-drop-point", residual, base: base2 });
82470
- return true;
82564
+ const draftEl = scaleDraftEl;
82565
+ if (!scaleDraftDropPoint || !selector) {
82566
+ draw(() => clearStudioBoxSize(draftEl));
82567
+ return false;
82471
82568
  }
82472
- const corrected = {
82473
- x: roundTo3(base2.x + residual.x),
82474
- y: roundTo3(base2.y + residual.y)
82475
- };
82476
- logResize("scale-finalize", {
82477
- dropPoint: scaleDraftDropPoint,
82478
- post: { x: post2.x, y: post2.y },
82479
- residual,
82480
- gsapPos,
82481
- base: base2,
82482
- corrected
82569
+ const dropPoint = scaleDraftDropPoint;
82570
+ const measured = draw(() => {
82571
+ clearStudioBoxSize(draftEl);
82572
+ if (committedScale) {
82573
+ setElementGsapScale(draftEl, committedScale.x, committedScale.y);
82574
+ }
82575
+ const gsapPos = readGsapPositionFromIframe(iframe, selector) ?? { x: 0, y: 0 };
82576
+ const { baseGsapX, baseGsapY } = computeDraggedGsapPosition(
82577
+ selection.element,
82578
+ { x: 0, y: 0 },
82579
+ gsapPos
82580
+ );
82581
+ const base3 = { x: baseGsapX, y: baseGsapY };
82582
+ setElementGsapPosition(draftEl, base3.x, base3.y);
82583
+ const post2 = draftEl.getBoundingClientRect();
82584
+ const residual = { x: dropPoint.x - post2.x, y: dropPoint.y - post2.y };
82585
+ if (!Number.isFinite(residual.x) || !Number.isFinite(residual.y)) return null;
82586
+ if (Math.abs(residual.x) < 0.5 && Math.abs(residual.y) < 0.5) {
82587
+ logResize("scale-finalize", { skipped: "already-on-drop-point", residual, base: base3 });
82588
+ return "settled";
82589
+ }
82590
+ const corrected2 = {
82591
+ x: roundTo3(base3.x + residual.x),
82592
+ y: roundTo3(base3.y + residual.y)
82593
+ };
82594
+ logResize("scale-finalize", {
82595
+ dropPoint,
82596
+ post: { x: post2.x, y: post2.y },
82597
+ residual,
82598
+ gsapPos,
82599
+ base: base3,
82600
+ corrected: corrected2
82601
+ });
82602
+ setElementGsapPosition(draftEl, corrected2.x, corrected2.y);
82603
+ return { base: base3, corrected: corrected2 };
82483
82604
  });
82484
- setElementGsapPosition(scaleDraftEl, corrected.x, corrected.y);
82605
+ if (measured === null) return false;
82606
+ if (measured === "settled") return true;
82607
+ const { base: base2, corrected } = measured;
82485
82608
  const currentAnimations = fetchFallbackAnimations ? await fetchFallbackAnimations() : resolved2?.animations ?? animations;
82486
82609
  const delta = { x: corrected.x - base2.x, y: corrected.y - base2.y };
82487
82610
  const positionTween = pickClosestToPlayhead(
@@ -82932,8 +83055,10 @@ function useGsapAwareEditing({
82932
83055
  modifiers
82933
83056
  );
82934
83057
  await saveMove(outcome, async () => {
82935
- const result = await stageElementPositionOffset(selection, next, false).save();
82936
- writes.recordDomResult(result);
83058
+ const staged = writes.drawKeepingUndone(
83059
+ () => stageElementPositionOffset(selection, next, false)
83060
+ );
83061
+ writes.recordDomResult(await staged.save());
82937
83062
  });
82938
83063
  return writes.finish();
82939
83064
  } catch (error) {
@@ -83006,11 +83131,8 @@ function useGsapAwareEditing({
83006
83131
  );
83007
83132
  await saveMove(dragOutcome, async () => {
83008
83133
  const plainAfterSettle = editsPlainCss(selection.element, "move");
83009
- anchorMove = stageElementPositionOffset(
83010
- selection,
83011
- offset,
83012
- plainAfterSettle,
83013
- coalesceKey
83134
+ anchorMove = writes.drawKeepingUndone(
83135
+ () => stageElementPositionOffset(selection, offset, plainAfterSettle, coalesceKey)
83014
83136
  );
83015
83137
  });
83016
83138
  }
@@ -83018,15 +83140,18 @@ function useGsapAwareEditing({
83018
83140
  await runGestureTransaction({
83019
83141
  element: selection.element,
83020
83142
  label: "Resize layer",
83143
+ draw: writes.drawKeepingUndone,
83021
83144
  settle: () => {
83022
83145
  if (!offset || scaleRoute || !selector) return;
83023
- const gsapPos = readGsapPositionFromIframe(previewIframeRef.current, selector) ?? {
83024
- x: 0,
83025
- y: 0
83026
- };
83027
- const { newX, newY } = computeDraggedGsapPosition(selection.element, offset, gsapPos);
83028
- logResize("sync-settle", { gsapPos, offset, newX, newY });
83029
- setElementGsapPosition(selection.element, newX, newY);
83146
+ writes.drawKeepingUndone(() => {
83147
+ const gsapPos = readGsapPositionFromIframe(previewIframeRef.current, selector) ?? {
83148
+ x: 0,
83149
+ y: 0
83150
+ };
83151
+ const { newX, newY } = computeDraggedGsapPosition(selection.element, offset, gsapPos);
83152
+ logResize("sync-settle", { gsapPos, offset, newX, newY });
83153
+ setElementGsapPosition(selection.element, newX, newY);
83154
+ });
83030
83155
  },
83031
83156
  persist: async (commit, coalesceKey) => {
83032
83157
  if (writes.commit) {
@@ -83039,16 +83164,13 @@ function useGsapAwareEditing({
83039
83164
  previewIframeRef.current,
83040
83165
  commitMutation2,
83041
83166
  makeFetchFallback(selection),
83042
- offset
83167
+ offset,
83168
+ writes.drawKeepingUndone
83043
83169
  );
83044
83170
  assertGsapEditPersisted(outcome);
83045
83171
  if (outcome.status === "element-size") {
83046
- const result = await handleDomBoxSizeCommit(
83047
- selection,
83048
- next,
83049
- void 0,
83050
- void 0,
83051
- coalesceKey
83172
+ const result = await writes.drawKeepingUndone(
83173
+ () => handleDomBoxSizeCommit(selection, next, void 0, void 0, coalesceKey)
83052
83174
  );
83053
83175
  writes.recordDomResult(result);
83054
83176
  } else cropUndoKey = coalesceKey;
@@ -83072,11 +83194,8 @@ function useGsapAwareEditing({
83072
83194
  const anchorResult = await anchorMove?.save();
83073
83195
  writes.recordDomResult(anchorResult);
83074
83196
  if (cropUndoKey) {
83075
- const cropResult = await saveCropResize(
83076
- stageCrop,
83077
- selection,
83078
- commitPositionPatchToHtml,
83079
- cropUndoKey
83197
+ const cropResult = await writes.drawKeepingUndone(
83198
+ () => saveCropResize(stageCrop, selection, commitPositionPatchToHtml, cropUndoKey)
83080
83199
  );
83081
83200
  writes.recordDomResult(cropResult);
83082
83201
  }