@hyperframes/studio 0.8.143 → 0.8.144

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 (51) hide show
  1. package/dist/assets/{hyperframes-player-BXSuksFh.js → hyperframes-player-Bw_jIaSX.js} +3 -3
  2. package/dist/assets/{index-J7jpL4pj.js → index-BVM9NuWl.js} +141 -141
  3. package/dist/assets/{index-BgOVCqjo.js → index-Cj7W_c7p.js} +1 -1
  4. package/dist/assets/{index-FgyO_5Kv.js → index-DyH9dT7b.js} +1 -1
  5. package/dist/{chunk-TFLU2MGI.js → chunk-3GHTHPZD.js} +8 -1
  6. package/dist/chunk-3GHTHPZD.js.map +1 -0
  7. package/dist/{chunk-H2DUOXTF.js → chunk-G3EDKRZH.js} +3 -3
  8. package/dist/{chunk-UYWON5AX.js → chunk-PEAKCOUW.js} +7 -3
  9. package/dist/chunk-PEAKCOUW.js.map +1 -0
  10. package/dist/{chunk-LOVBLSMR.js → chunk-Y2OHHNM2.js} +35 -19
  11. package/dist/chunk-Y2OHHNM2.js.map +1 -0
  12. package/dist/{domEditingLayers-KBTPQKGA.js → domEditingLayers-PYST4G45.js} +3 -3
  13. package/dist/index.html +1 -1
  14. package/dist/index.js +139 -70
  15. package/dist/index.js.map +1 -1
  16. package/dist/player.js +3 -3
  17. package/dist/ui.d.ts +4 -5
  18. package/dist/ui.js +1 -1
  19. package/package.json +8 -8
  20. package/src/components/editor/DomEditSelectionChrome.test.tsx +103 -0
  21. package/src/components/editor/DomEditSelectionChrome.tsx +42 -16
  22. package/src/components/editor/anchoredResizeCommitFeedsOffset.test.ts +36 -0
  23. package/src/components/editor/domEditOverlayGestures.ts +1 -0
  24. package/src/components/editor/domEditOverlayStartGesture.ts +5 -0
  25. package/src/components/editor/domEditResizeLocal.test.ts +81 -1
  26. package/src/components/editor/domEditResizeLocal.ts +27 -3
  27. package/src/components/editor/domEditingLayers.ts +4 -1
  28. package/src/components/editor/domEditingTextFields.test.ts +18 -0
  29. package/src/components/editor/manualEditsDom.ts +12 -12
  30. package/src/components/editor/previewGestureStarts.ts +14 -5
  31. package/src/components/editor/resizeDraft.ts +13 -4
  32. package/src/components/editor/useDomEditOverlayGestures.ts +5 -3
  33. package/src/components/ui/Menu.test.tsx +67 -5
  34. package/src/components/ui/Menu.tsx +21 -4
  35. package/src/components/ui/Popover.tsx +11 -5
  36. package/src/hooks/gsapResizeIntercept.test.ts +46 -11
  37. package/src/hooks/gsapResizeIntercept.ts +25 -20
  38. package/src/hooks/useThumbnailStripSize.test.tsx +123 -0
  39. package/src/hooks/useThumbnailStripSize.ts +19 -8
  40. package/src/player/components/CompositionThumbnail.test.ts +3 -0
  41. package/src/player/components/ImageThumbnail.test.tsx +4 -1
  42. package/src/player/components/VideoThumbnail.test.tsx +13 -2
  43. package/src/player/hooks/timelineSyncHydration.ts +2 -1
  44. package/src/utils/authoredSource.test.ts +25 -0
  45. package/src/utils/authoredSource.ts +3 -5
  46. package/src/utils/rounding.ts +7 -0
  47. package/dist/chunk-LOVBLSMR.js.map +0 -1
  48. package/dist/chunk-TFLU2MGI.js.map +0 -1
  49. package/dist/chunk-UYWON5AX.js.map +0 -1
  50. /package/dist/{chunk-H2DUOXTF.js.map → chunk-G3EDKRZH.js.map} +0 -0
  51. /package/dist/{domEditingLayers-KBTPQKGA.js.map → domEditingLayers-PYST4G45.js.map} +0 -0
package/dist/index.js CHANGED
@@ -40,7 +40,7 @@ import {
40
40
  usePreviewVariablesStore,
41
41
  whenPreviewBooted,
42
42
  writeStudioUiPreferences
43
- } from "./chunk-H2DUOXTF.js";
43
+ } from "./chunk-G3EDKRZH.js";
44
44
  import {
45
45
  Button,
46
46
  ContextMenu,
@@ -72,7 +72,7 @@ import {
72
72
  fieldBase,
73
73
  fieldText,
74
74
  popupSurface
75
- } from "./chunk-LOVBLSMR.js";
75
+ } from "./chunk-Y2OHHNM2.js";
76
76
  import {
77
77
  DOM_EDIT_LAYER_OBSERVER_INIT,
78
78
  STUDIO_FLAT_INSPECTOR_ENABLED,
@@ -105,7 +105,7 @@ import {
105
105
  resolveDomEditSelection,
106
106
  serializeDomEditTextFields,
107
107
  studioApiFetch
108
- } from "./chunk-UYWON5AX.js";
108
+ } from "./chunk-PEAKCOUW.js";
109
109
  import {
110
110
  LAYER_REVEAL_PRIOR_POSITION_ATTR,
111
111
  LAYER_REVEAL_PRIOR_Z_ATTR,
@@ -158,13 +158,14 @@ import {
158
158
  roundTo3,
159
159
  roundToCenti,
160
160
  roundToLayoutPx,
161
+ roundToLayoutSize,
161
162
  runtimeAudioId,
162
163
  sameTimelineText,
163
164
  setCompositionSourceMap,
164
165
  splitTimelineElementKey,
165
166
  toHexColor,
166
167
  wrapTimeline
167
- } from "./chunk-TFLU2MGI.js";
168
+ } from "./chunk-3GHTHPZD.js";
168
169
  import "./chunk-PZ5AY32C.js";
169
170
 
170
171
  // src/components/EditorShell.tsx
@@ -16486,17 +16487,24 @@ var measure = (entries) => isTimelineZoomPreviewing() || commit(
16486
16487
  })
16487
16488
  );
16488
16489
  var onPresence = (entries) => measure(entries.filter((entry2) => entry2.isIntersecting));
16489
- var onResize = (entries) => measure(
16490
- entries.map(({ target, contentRect: { width, height } }) => ({
16491
- target,
16492
- size: { width, height }
16493
- }))
16494
- );
16490
+ var pendingSizes = /* @__PURE__ */ new Map();
16491
+ var pendingSizesFrame = 0;
16492
+ var applyPendingSizes = () => {
16493
+ pendingSizesFrame = 0;
16494
+ const entries = [...pendingSizes].map(([target, size]) => ({ target, size }));
16495
+ pendingSizes.clear();
16496
+ measure(entries);
16497
+ };
16498
+ var applyResizesNextFrame = (entries) => {
16499
+ for (const { target, contentRect } of entries)
16500
+ pendingSizes.set(target, { width: contentRect.width, height: contentRect.height });
16501
+ pendingSizesFrame ||= requestAnimationFrame(applyPendingSizes);
16502
+ };
16495
16503
  var observeIntersections = (callback) => typeof IntersectionObserver === "undefined" ? null : new IntersectionObserver(callback, GAP_WARNING);
16496
16504
  function acquire() {
16497
16505
  if (users++ === 0) {
16498
16506
  shared = {
16499
- resize: new ResizeObserver(onResize),
16507
+ resize: new ResizeObserver(applyResizesNextFrame),
16500
16508
  presence: observeIntersections(onPresence),
16501
16509
  gaps: observeIntersections(
16502
16510
  (entries) => entries.some((entry2) => entry2.isIntersecting) && scheduleRefresh()
@@ -16517,6 +16525,9 @@ function release() {
16517
16525
  window.removeEventListener("scroll", scheduleRefresh, { capture: true });
16518
16526
  cancelAnimationFrame(frame2);
16519
16527
  frame2 = 0;
16528
+ cancelAnimationFrame(pendingSizesFrame);
16529
+ pendingSizesFrame = 0;
16530
+ pendingSizes.clear();
16520
16531
  }
16521
16532
  var watchGap = (gap) => {
16522
16533
  if (!gap) return;
@@ -16557,6 +16568,7 @@ function useThumbnailStripSize() {
16557
16568
  resize.unobserve(target);
16558
16569
  presence?.unobserve(target);
16559
16570
  strips.delete(target);
16571
+ pendingSizes.delete(target);
16560
16572
  release();
16561
16573
  };
16562
16574
  }, []);
@@ -17575,8 +17587,8 @@ import { motionPathPluginUrl } from "@hyperframes/core/gsap-cdn";
17575
17587
  // src/utils/authoredSource.ts
17576
17588
  import { ensureHfIds, isCompositionTemplate } from "@hyperframes/parsers/hf-ids";
17577
17589
  import {
17578
- STUDIO_PREVIEW_LAZY_ATTR,
17579
- STUDIO_PREVIEW_UPCOMING_ATTR
17590
+ PREVIEW_ONLY_ATTRS,
17591
+ STUDIO_PREVIEW_LAZY_ATTR
17580
17592
  } from "@hyperframes/core/studio-preview-mark";
17581
17593
  function parseSavedSource(html2) {
17582
17594
  return new DOMParser().parseFromString(ensureHfIds(html2), "text/html");
@@ -17657,10 +17669,8 @@ function authoredMarkup(authored, live, sourceFile) {
17657
17669
  function liveMarkupWithoutPreviewMarks(live) {
17658
17670
  const copy = live.cloneNode(true);
17659
17671
  for (const el of [copy, ...Array.from(copy.querySelectorAll("*"))]) {
17660
- el.removeAttribute(STUDIO_PREVIEW_UPCOMING_ATTR);
17661
- if (!el.hasAttribute(STUDIO_PREVIEW_LAZY_ATTR)) continue;
17662
- el.removeAttribute("loading");
17663
- el.removeAttribute(STUDIO_PREVIEW_LAZY_ATTR);
17672
+ if (el.hasAttribute(STUDIO_PREVIEW_LAZY_ATTR)) el.removeAttribute("loading");
17673
+ for (const attr of PREVIEW_ONLY_ATTRS) el.removeAttribute(attr);
17664
17674
  }
17665
17675
  return copy.outerHTML;
17666
17676
  }
@@ -29909,9 +29919,10 @@ function buildStandaloneRootTimelineElement(params) {
29909
29919
 
29910
29920
  // src/player/hooks/timelineSyncHydration.ts
29911
29921
  import { resolveCssStackingContextId as resolveCssStackingContextId2 } from "@hyperframes/core/runtime/stacking-context";
29922
+ import { LOOP_INFLATED_TIMELINE_SECONDS } from "@hyperframes/core/runtime/composition-length";
29912
29923
  import { HF_AUDIO_GROUP_ATTR as HF_AUDIO_GROUP_ATTR3 } from "@hyperframes/core/audio-groups";
29913
29924
  function sanitizeDurationSeconds(value) {
29914
- return Number.isFinite(value) && value > 0 && value < 7200 ? value : 0;
29925
+ return Number.isFinite(value) && value > 0 && value < LOOP_INFLATED_TIMELINE_SECONDS ? value : 0;
29915
29926
  }
29916
29927
  function readChildAudioGroupState(child) {
29917
29928
  const audioGroup = child.getAttribute(HF_AUDIO_GROUP_ATTR3);
@@ -31289,7 +31300,7 @@ function readParentFlexBasisPixels(element, size) {
31289
31300
  const display2 = readStyleOrComputed(parent, "display").trim();
31290
31301
  if (display2 !== "flex" && display2 !== "inline-flex") return null;
31291
31302
  const direction = readStyleOrComputed(parent, "flex-direction").trim();
31292
- return roundToLayoutPx(Math.max(1, direction.startsWith("column") ? size.height : size.width));
31303
+ return direction.startsWith("column") ? size.height : size.width;
31293
31304
  }
31294
31305
  function restoreStaleStudioScaleResize(element) {
31295
31306
  if (!element.hasAttribute(STUDIO_ORIGINAL_SCALE_ATTR)) return;
@@ -31352,17 +31363,16 @@ function writeStudioBoxSizeVars(element, size) {
31352
31363
  element.setAttribute(STUDIO_ORIGINAL_DISPLAY_ATTR, element.style.getPropertyValue("display"));
31353
31364
  }
31354
31365
  element.setAttribute(STUDIO_BOX_SIZE_ATTR, "true");
31355
- element.style.setProperty(STUDIO_WIDTH_PROP, `${roundToLayoutPx(Math.max(1, size.width))}px`);
31356
- element.style.setProperty(STUDIO_HEIGHT_PROP, `${roundToLayoutPx(Math.max(1, size.height))}px`);
31366
+ element.style.setProperty(STUDIO_WIDTH_PROP, `${size.width}px`);
31367
+ element.style.setProperty(STUDIO_HEIGHT_PROP, `${size.height}px`);
31357
31368
  }
31358
- function applyStudioBoxSizeDimensions(element, size) {
31369
+ function applyStudioBoxSizeDimensions(element, wantedSize) {
31370
+ const size = roundToLayoutSize(wantedSize);
31359
31371
  writeStudioBoxSizeVars(element, size);
31360
31372
  restoreStaleStudioScaleResize(element);
31361
- const width = roundToLayoutPx(Math.max(1, size.width));
31362
- const height = roundToLayoutPx(Math.max(1, size.height));
31363
31373
  element.style.setProperty("box-sizing", "border-box");
31364
- element.style.setProperty("width", `${width}px`);
31365
- element.style.setProperty("height", `${height}px`);
31374
+ element.style.setProperty("width", `${size.width}px`);
31375
+ element.style.setProperty("height", `${size.height}px`);
31366
31376
  element.style.setProperty("min-width", "0px");
31367
31377
  element.style.setProperty("min-height", "0px");
31368
31378
  element.style.setProperty("max-width", "none");
@@ -31377,6 +31387,7 @@ function applyStudioBoxSizeDimensions(element, size) {
31377
31387
  if (computedDisplay === "inline") {
31378
31388
  element.style.setProperty("display", "inline-block");
31379
31389
  }
31390
+ return size;
31380
31391
  }
31381
31392
  function applyStudioBoxSize(element, size) {
31382
31393
  promoteInlineForTransform(element);
@@ -31394,7 +31405,7 @@ function applyStudioBoxSizeDraft(element, size) {
31394
31405
  boxSizeDraftBases.set(element, { width: element.offsetWidth, height: element.offsetHeight });
31395
31406
  }
31396
31407
  promoteInlineForTransform(element);
31397
- applyStudioBoxSizeDimensions(element, size);
31408
+ return applyStudioBoxSizeDimensions(element, size);
31398
31409
  }
31399
31410
  function isSimpleRotateAngle(value) {
31400
31411
  return /^-?(?:\d+(?:\.\d+)?|\.\d+)(?:deg|rad|turn|grad)$/.test(value.trim());
@@ -37564,7 +37575,7 @@ function OffCanvasIndicators({
37564
37575
  const selectOffCanvas = async () => {
37565
37576
  const el = elements.current.get(r.key);
37566
37577
  if (!el) return;
37567
- const { resolveDomEditSelection: resolveDomEditSelection2 } = await import("./domEditingLayers-KBTPQKGA.js");
37578
+ const { resolveDomEditSelection: resolveDomEditSelection2 } = await import("./domEditingLayers-PYST4G45.js");
37568
37579
  const acp = activeCompositionPathRef.current ?? "index.html";
37569
37580
  const sel = await resolveDomEditSelection2(el, {
37570
37581
  activeCompositionPath: acp,
@@ -37620,7 +37631,7 @@ function ChildRectOutlines({
37620
37631
 
37621
37632
  // src/components/editor/domEditResizeLocal.ts
37622
37633
  var MIN_RESIZE_LOCAL_PX = 1;
37623
- var DEGENERATE_START_DIST_PX = 3;
37634
+ var DEGENERATE_START_DIST_PX = 0.5;
37624
37635
  function resolveCenterResizeScale(input) {
37625
37636
  const startDist = Math.hypot(
37626
37637
  input.pointerStart.x - input.centerStart.x,
@@ -37675,6 +37686,17 @@ function resolveRotatedResizeCursor(handle, rotationDeg) {
37675
37686
  const bucket = Math.round(normalized / 45) % 8;
37676
37687
  return CURSORS_8[bucket];
37677
37688
  }
37689
+ function resizeRemainderShift(corners, grab, { wanted, written }) {
37690
+ const u = { x: corners.ne.x - corners.nw.x, y: corners.ne.y - corners.nw.y };
37691
+ const v = { x: corners.sw.x - corners.nw.x, y: corners.sw.y - corners.nw.y };
37692
+ const det = u.x * v.y - u.y * v.x;
37693
+ if (!det) return { x: 0, y: 0 };
37694
+ const su = Math.sign((grab.x * v.y - grab.y * v.x) / det);
37695
+ const sv = Math.sign((u.x * grab.y - u.y * grab.x) / det);
37696
+ const kw = su * (wanted.width - written.width) / (2 * written.width);
37697
+ const kh = sv * (wanted.height - written.height) / (2 * written.height);
37698
+ return { x: u.x * kw + v.x * kh, y: u.y * kw + v.y * kh };
37699
+ }
37678
37700
  function computeNextResizeAnchor(prev, fixedStart, fixedNow) {
37679
37701
  const base2 = prev ?? { dx: 0, dy: 0 };
37680
37702
  return { dx: base2.dx + (fixedStart.x - fixedNow.x), dy: base2.dy + (fixedStart.y - fixedNow.y) };
@@ -39602,6 +39624,7 @@ function startGesture(kind, e, opts, options) {
39602
39624
  manualEditDragToken,
39603
39625
  snapContext,
39604
39626
  resizeHandle: kind === "resize" ? options?.resizeHandle ?? "se" : void 0,
39627
+ resizePressFromCorner: kind === "resize" && options?.resizeCorner ? { x: e.clientX - options.resizeCorner.x, y: e.clientY - options.resizeCorner.y } : void 0,
39605
39628
  resizeFixedCenterStart
39606
39629
  };
39607
39630
  if (kind === "resize") {
@@ -39695,7 +39718,7 @@ function createPreviewGestureStarts(opts, moveActiveGesture, releaseActiveGestur
39695
39718
  offerUndo();
39696
39719
  };
39697
39720
  const drawPressedBox = (origin2) => origin2 && ((dx, dy) => opts.setOverlayRect(shiftedOverlayRect(origin2, dx, dy)));
39698
- const drawResizedBox = (origin2, press) => {
39721
+ const drawResizedBox = (origin2, grabbed) => {
39699
39722
  if (!origin2) return null;
39700
39723
  const bounds = opts.overlayRef.current?.getBoundingClientRect();
39701
39724
  const centre = {
@@ -39703,8 +39726,12 @@ function createPreviewGestureStarts(opts, moveActiveGesture, releaseActiveGestur
39703
39726
  y: (bounds?.top ?? 0) + origin2.top + origin2.height / 2
39704
39727
  };
39705
39728
  return (dx, dy) => {
39706
- const pointer = { x: press.x + dx, y: press.y + dy };
39707
- const scale = resolveCenterResizeScale({ pointer, pointerStart: press, centerStart: centre });
39729
+ const pointer = { x: grabbed.x + dx, y: grabbed.y + dy };
39730
+ const scale = resolveCenterResizeScale({
39731
+ pointer,
39732
+ pointerStart: grabbed,
39733
+ centerStart: centre
39734
+ });
39708
39735
  const [width, height] = [origin2.width * scale, origin2.height * scale];
39709
39736
  opts.setOverlayRect({
39710
39737
  ...origin2,
@@ -39823,7 +39850,10 @@ function createPreviewGestureStarts(opts, moveActiveGesture, releaseActiveGestur
39823
39850
  let draw = null;
39824
39851
  if (kind === "drag") draw = drawPressedBox(opts.overlayRectRef.current);
39825
39852
  if (kind === "resize")
39826
- draw = drawResizedBox(opts.overlayRectRef.current, { x: e.clientX, y: e.clientY });
39853
+ draw = drawResizedBox(
39854
+ opts.overlayRectRef.current,
39855
+ options?.resizeCorner ?? { x: e.clientX, y: e.clientY }
39856
+ );
39827
39857
  return startOnShownPreview(
39828
39858
  e,
39829
39859
  () => {
@@ -39838,6 +39868,7 @@ function createPreviewGestureStarts(opts, moveActiveGesture, releaseActiveGestur
39838
39868
  selection,
39839
39869
  rect,
39840
39870
  resizeHandle: options?.resizeHandle,
39871
+ resizeCorner: options?.resizeCorner,
39841
39872
  at
39842
39873
  });
39843
39874
  },
@@ -39848,10 +39879,14 @@ function createPreviewGestureStarts(opts, moveActiveGesture, releaseActiveGestur
39848
39879
  }
39849
39880
 
39850
39881
  // src/components/editor/resizeDraft.ts
39851
- function resolveResizeAnchor(g, corners, measureOrientedRect) {
39882
+ function resolveResizeAnchor(g, corners, measureOrientedRect, sizes) {
39852
39883
  const fixedStart = g.resizeFixedCenterStart;
39853
39884
  if (corners && fixedStart) {
39854
- return computeNextResizeAnchor(g.lastResizeAnchor, fixedStart, overlayCornersCentroid(corners));
39885
+ const press = g.resizePressFromCorner ?? { x: 0, y: 0 };
39886
+ const grab = { x: g.startX - press.x - g.centerX, y: g.startY - press.y - g.centerY };
39887
+ const shift = resizeRemainderShift(corners, grab, sizes);
39888
+ const target = { x: fixedStart.x + shift.x, y: fixedStart.y + shift.y };
39889
+ return computeNextResizeAnchor(g.lastResizeAnchor, target, overlayCornersCentroid(corners));
39855
39890
  }
39856
39891
  const fallbackRect = measureOrientedRect();
39857
39892
  return resolveResizeCenterAnchorOffset({
@@ -39861,9 +39896,9 @@ function resolveResizeAnchor(g, corners, measureOrientedRect) {
39861
39896
  overlayHeight: fallbackRect ? fallbackRect.height : g.originHeight
39862
39897
  });
39863
39898
  }
39864
- function resolveAnchoredResizeDraft(g, member, element, overlayEl, iframe, measureOrientedRect) {
39899
+ function resolveAnchoredResizeDraft(g, member, element, overlayEl, iframe, measureOrientedRect, sizes) {
39865
39900
  const corners = overlayEl && iframe ? elementCornerOverlayPoints(overlayEl, iframe, element) : null;
39866
- const anchor2 = resolveResizeAnchor(g, corners, measureOrientedRect);
39901
+ const anchor2 = resolveResizeAnchor(g, corners, measureOrientedRect, sizes);
39867
39902
  g.lastResizeAnchor = anchor2;
39868
39903
  applyManualOffsetDragDraft(member, anchor2.dx, anchor2.dy);
39869
39904
  return measureOrientedRect() ?? {
@@ -39876,7 +39911,7 @@ function resolveAnchoredResizeDraft(g, member, element, overlayEl, iframe, measu
39876
39911
  angle: g.actualRotation
39877
39912
  };
39878
39913
  }
39879
- function resolveResizeDraftRect(g, element, overlayEl, iframe, measureOrientedRect) {
39914
+ function resolveResizeDraftRect(g, element, overlayEl, iframe, measureOrientedRect, sizes) {
39880
39915
  if (g.pathOffsetMember) {
39881
39916
  return resolveAnchoredResizeDraft(
39882
39917
  g,
@@ -39884,7 +39919,8 @@ function resolveResizeDraftRect(g, element, overlayEl, iframe, measureOrientedRe
39884
39919
  element,
39885
39920
  overlayEl,
39886
39921
  iframe,
39887
- measureOrientedRect
39922
+ measureOrientedRect,
39923
+ sizes
39888
39924
  );
39889
39925
  }
39890
39926
  return measureOrientedRect() ?? {
@@ -40209,14 +40245,15 @@ function createDomEditOverlayGestureHandlers(opts) {
40209
40245
  }
40210
40246
  if (g.pathOffsetMember) applyManualOffsetDragDraft(g.pathOffsetMember, dx, dy);
40211
40247
  } else {
40248
+ const grab = g.resizePressFromCorner ?? { x: 0, y: 0 };
40212
40249
  const nextSize = resolveCenterResizeSize({
40213
40250
  baseWidth: g.actualWidth,
40214
40251
  baseHeight: g.actualHeight,
40215
- pointer: { x: e.clientX, y: e.clientY },
40216
- pointerStart: { x: g.startX, y: g.startY },
40252
+ pointer: { x: e.clientX - grab.x, y: e.clientY - grab.y },
40253
+ pointerStart: { x: g.startX - grab.x, y: g.startY - grab.y },
40217
40254
  centerStart: { x: g.centerX, y: g.centerY }
40218
40255
  });
40219
- applyStudioBoxSizeDraft(sel.element, nextSize);
40256
+ const writtenSize = applyStudioBoxSizeDraft(sel.element, nextSize);
40220
40257
  const overlayEl = opts.overlayRef.current;
40221
40258
  const iframe = opts.iframeRef.current;
40222
40259
  const measureOrientedRect = () => overlayEl && iframe ? orientedOverlayRect(overlayEl, iframe, sel.element) : null;
@@ -40225,7 +40262,8 @@ function createDomEditOverlayGestureHandlers(opts) {
40225
40262
  sel.element,
40226
40263
  overlayEl,
40227
40264
  iframe,
40228
- measureOrientedRect
40265
+ measureOrientedRect,
40266
+ { wanted: nextSize, written: writtenSize }
40229
40267
  );
40230
40268
  logResizeMove({
40231
40269
  pointer: { x: e.clientX, y: e.clientY },
@@ -41302,13 +41340,22 @@ var RESIZE_HANDLE_DEFS = [
41302
41340
  { handle: "se", cursor: "nwse-resize", x: "right", y: "bottom" }
41303
41341
  ];
41304
41342
  var NO_CROP_INSET = { top: 0, right: 0, bottom: 0, left: 0 };
41305
- function resizeHandleStyle(def, overlayRect, cropInset) {
41343
+ function resizeHandleLayout(def, overlayRect, cropInset) {
41306
41344
  const half = RESIZE_HANDLE_HIT_PX / 2;
41307
41345
  const inset = cropInset ?? NO_CROP_INSET;
41308
- const style = { cursor: def.cursor, touchAction: "none" };
41309
- style.left = def.x === "left" ? overlayRect.left + inset.left - half : overlayRect.left + overlayRect.width - inset.right - half;
41310
- style.top = def.y === "top" ? overlayRect.top + inset.top - half : overlayRect.top + overlayRect.height - inset.bottom - half;
41311
- return style;
41346
+ const cornerX = def.x === "left" ? overlayRect.left + inset.left : overlayRect.left + overlayRect.width - inset.right;
41347
+ const cornerY = def.y === "top" ? overlayRect.top + inset.top : overlayRect.top + overlayRect.height - inset.bottom;
41348
+ const slideOutToFreeMiddleX = (def.x === "left" ? -1 : 1) * Math.max(0, half - (overlayRect.width - inset.left - inset.right) / 4);
41349
+ const slideOutToFreeMiddleY = (def.y === "top" ? -1 : 1) * Math.max(0, half - (overlayRect.height - inset.top - inset.bottom) / 4);
41350
+ return {
41351
+ style: {
41352
+ cursor: def.cursor,
41353
+ touchAction: "none",
41354
+ left: cornerX - half + slideOutToFreeMiddleX,
41355
+ top: cornerY - half + slideOutToFreeMiddleY
41356
+ },
41357
+ anchor: { left: half - slideOutToFreeMiddleX, top: half - slideOutToFreeMiddleY }
41358
+ };
41312
41359
  }
41313
41360
  function DomEditGroupChrome({
41314
41361
  groupOverlayItems,
@@ -41440,26 +41487,42 @@ function DomEditSelectionChrome({
41440
41487
  },
41441
41488
  selectionKey
41442
41489
  ),
41443
- canManipulate && !editing && selection.capabilities.canApplyManualSize && RESIZE_HANDLE_DEFS.map(
41444
- (def) => def.handle !== "se" && !selection.capabilities.canApplyManualOffset ? null : /* @__PURE__ */ jsx99(
41490
+ canManipulate && !editing && selection.capabilities.canApplyManualSize && RESIZE_HANDLE_DEFS.map((def) => {
41491
+ if (def.handle !== "se" && !selection.capabilities.canApplyManualOffset) return null;
41492
+ const layout = resizeHandleLayout(def, overlayRect, cropOutlineInsetPx ?? void 0);
41493
+ return /* @__PURE__ */ jsxs69(
41445
41494
  "div",
41446
41495
  {
41447
41496
  className: "pointer-events-auto absolute flex h-4 w-4 items-center justify-center",
41448
41497
  style: {
41449
- ...resizeHandleStyle(def, overlayRect, cropOutlineInsetPx ?? void 0),
41498
+ ...layout.style,
41450
41499
  // Cursor rotates with the object: bucket the corner's base
41451
41500
  // diagonal + element rotation into the 8 CSS resize cursors.
41452
41501
  cursor: resolveRotatedResizeCursor(def.handle, overlayRect.angle ?? 0)
41453
41502
  },
41454
41503
  onPointerDown: (e) => {
41455
41504
  e.stopPropagation();
41456
- gestures.startGesture("resize", e, { resizeHandle: def.handle });
41505
+ const corner = e.currentTarget.querySelector("[data-resize-corner]")?.getBoundingClientRect();
41506
+ gestures.startGesture("resize", e, {
41507
+ resizeHandle: def.handle,
41508
+ resizeCorner: corner && { x: corner.left, y: corner.top }
41509
+ });
41457
41510
  },
41458
- children: /* @__PURE__ */ jsx99("div", { className: "pointer-events-none h-[12px] w-[12px] rounded-full border-[1.5px] border-studio-accent bg-white shadow-[0_0_3px_rgba(0,0,0,0.45)]" })
41511
+ children: [
41512
+ /* @__PURE__ */ jsx99(
41513
+ "span",
41514
+ {
41515
+ "data-resize-corner": true,
41516
+ className: "pointer-events-none absolute",
41517
+ style: layout.anchor
41518
+ }
41519
+ ),
41520
+ /* @__PURE__ */ jsx99("div", { className: "pointer-events-none h-[12px] w-[12px] rounded-full border-[1.5px] border-studio-accent bg-white shadow-[0_0_3px_rgba(0,0,0,0.45)]" })
41521
+ ]
41459
41522
  },
41460
41523
  def.handle
41461
- )
41462
- )
41524
+ );
41525
+ })
41463
41526
  ]
41464
41527
  }
41465
41528
  ),
@@ -84776,6 +84839,9 @@ function tweenUsesScaleLonghands(anim) {
84776
84839
  );
84777
84840
  return inKeyframes || Object.keys(anim?.properties ?? {}).some(isLonghand);
84778
84841
  }
84842
+ function rectCentre(rect) {
84843
+ return { x: rect.x + rect.width / 2, y: rect.y + rect.height / 2 };
84844
+ }
84779
84845
  async function tryGsapResizeIntercept(selection, size, animations, iframe, commitMutation, fetchFallbackAnimations, dragOffset, draw = (run) => run(), stamp) {
84780
84846
  const moment = editMoment(stamp);
84781
84847
  const fetchedAnimations = fetchFallbackAnimations ? await fetchFallbackAnimations() : [];
@@ -84890,8 +84956,7 @@ async function tryGsapResizeIntercept(selection, size, animations, iframe, commi
84890
84956
  scaleDraftEl = el;
84891
84957
  committedScale = useScaleLonghands ? { x: newScaleX, y: newScaleY } : { x: newScaleX, y: newScaleX };
84892
84958
  if (el) {
84893
- const dropRect = draw(() => el.getBoundingClientRect());
84894
- scaleDraftDropPoint = { x: dropRect.x, y: dropRect.y };
84959
+ scaleDraftDropPoint = draw(() => rectCentre(el.getBoundingClientRect()));
84895
84960
  }
84896
84961
  } else {
84897
84962
  resizeProps = {
@@ -84922,31 +84987,31 @@ async function tryGsapResizeIntercept(selection, size, animations, iframe, commi
84922
84987
  );
84923
84988
  const base3 = { x: baseGsapX, y: baseGsapY };
84924
84989
  setElementGsapPosition(draftEl, base3.x, base3.y);
84925
- const post2 = draftEl.getBoundingClientRect();
84926
- const residual = { x: dropPoint.x - post2.x, y: dropPoint.y - post2.y };
84927
- if (!Number.isFinite(residual.x) || !Number.isFinite(residual.y)) return null;
84928
- if (Math.abs(residual.x) < 0.5 && Math.abs(residual.y) < 0.5) {
84929
- logResize("scale-finalize", { skipped: "already-on-drop-point", residual, base: base3 });
84930
- return "settled";
84931
- }
84990
+ const post2 = rectCentre(draftEl.getBoundingClientRect());
84991
+ const residual2 = { x: dropPoint.x - post2.x, y: dropPoint.y - post2.y };
84992
+ if (!Number.isFinite(residual2.x) || !Number.isFinite(residual2.y)) return null;
84932
84993
  const corrected2 = {
84933
- x: roundTo3(base3.x + residual.x),
84934
- y: roundTo3(base3.y + residual.y)
84994
+ x: roundTo3(base3.x + residual2.x),
84995
+ y: roundTo3(base3.y + residual2.y)
84935
84996
  };
84997
+ if (corrected2.x === roundTo3(base3.x) && corrected2.y === roundTo3(base3.y)) {
84998
+ logResize("scale-finalize", { skipped: "already-on-drop-point", residual: residual2, base: base3 });
84999
+ return "settled";
85000
+ }
84936
85001
  logResize("scale-finalize", {
84937
85002
  dropPoint,
84938
- post: { x: post2.x, y: post2.y },
84939
- residual,
85003
+ post: post2,
85004
+ residual: residual2,
84940
85005
  gsapPos,
84941
85006
  base: base3,
84942
85007
  corrected: corrected2
84943
85008
  });
84944
85009
  setElementGsapPosition(draftEl, corrected2.x, corrected2.y);
84945
- return { base: base3, corrected: corrected2 };
85010
+ return { base: base3, corrected: corrected2, residual: residual2 };
84946
85011
  });
84947
85012
  if (measured === null) return false;
84948
85013
  if (measured === "settled") return true;
84949
- const { base: base2, corrected } = measured;
85014
+ const { base: base2, corrected, residual } = measured;
84950
85015
  const currentAnimations = fetchFallbackAnimations ? await fetchFallbackAnimations() : resolved2?.animations ?? animations;
84951
85016
  const delta = { x: corrected.x - base2.x, y: corrected.y - base2.y };
84952
85017
  const positionTween = pickClosestToPlayhead(
@@ -84956,6 +85021,10 @@ async function tryGsapResizeIntercept(selection, size, animations, iframe, commi
84956
85021
  moment.time
84957
85022
  );
84958
85023
  if (positionTween) {
85024
+ if (Math.abs(residual.x) < 0.5 && Math.abs(residual.y) < 0.5) {
85025
+ draw(() => setElementGsapPosition(draftEl, base2.x, base2.y));
85026
+ return true;
85027
+ }
84959
85028
  logResize("scale-finalize", { route: "position-keyframe", tweenId: positionTween.id });
84960
85029
  assertGsapEditPersisted(
84961
85030
  await commitGsapPositionFromDrag(selection, positionTween, delta, base2, iframe, {