@hyperframes/studio 0.7.54 → 0.7.56
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.
- package/dist/assets/{index-uBY329wb.js → index-BWnOxAiH.js} +1 -1
- package/dist/assets/{index-CMHYjEZ5.js → index-C4csZims.js} +1 -1
- package/dist/assets/index-CmVCjZjp.js +423 -0
- package/dist/assets/index-D-GyYi2d.css +1 -0
- package/dist/{chunk-SOTCF4DF.js → chunk-5QSIMBEJ.js} +2 -1
- package/dist/chunk-5QSIMBEJ.js.map +1 -0
- package/dist/{domEditingLayers-2ECJK24D.js → domEditingLayers-6LQGKPOI.js} +2 -2
- package/dist/index.d.ts +146 -144
- package/dist/index.html +2 -2
- package/dist/index.js +40582 -37803
- package/dist/index.js.map +1 -1
- package/package.json +7 -7
- package/src/App.tsx +106 -96
- package/src/components/EditorShell.tsx +253 -0
- package/src/components/StudioGlobalDragOverlay.tsx +1 -3
- package/src/components/StudioLeftSidebar.tsx +13 -3
- package/src/components/StudioRightPanel.tsx +8 -3
- package/src/components/TimelineToolbar.test.tsx +2 -2
- package/src/components/TimelineToolbar.tsx +179 -121
- package/src/components/editor/CanvasContextMenu.test.tsx +115 -0
- package/src/components/editor/CanvasContextMenu.tsx +198 -0
- package/src/components/editor/DomEditCropHandles.test.tsx +74 -3
- package/src/components/editor/DomEditCropHandles.tsx +64 -29
- package/src/components/editor/DomEditOverlay.test.ts +245 -279
- package/src/components/editor/DomEditOverlay.tsx +158 -126
- package/src/components/editor/DomEditOverlayHover.test.tsx +90 -0
- package/src/components/editor/DomEditRotateHandle.tsx +38 -18
- package/src/components/editor/DomEditSelectionChrome.test.tsx +80 -0
- package/src/components/editor/DomEditSelectionChrome.tsx +257 -0
- package/src/components/editor/LayersPanel.test.ts +66 -2
- package/src/components/editor/LayersPanel.tsx +43 -1
- package/src/components/editor/OffCanvasIndicators.test.tsx +34 -0
- package/src/components/editor/OffCanvasIndicators.tsx +49 -10
- package/src/components/editor/SnapGuideOverlay.tsx +28 -23
- package/src/components/editor/SnapToolbar.test.tsx +0 -1
- package/src/components/editor/anchoredResizeCommitFeedsOffset.test.ts +214 -0
- package/src/components/editor/anchoredResizeReleaseShift.test.ts +208 -0
- package/src/components/editor/canvasContextMenuZOrder.test.ts +435 -0
- package/src/components/editor/canvasContextMenuZOrder.ts +352 -0
- package/src/components/editor/domEditNudge.test.ts +80 -0
- package/src/components/editor/domEditNudge.ts +44 -0
- package/src/components/editor/domEditOverlayGeometry.test.ts +167 -1
- package/src/components/editor/domEditOverlayGeometry.ts +280 -20
- package/src/components/editor/domEditOverlayGestures.ts +44 -41
- package/src/components/editor/domEditOverlayStartGesture.ts +62 -2
- package/src/components/editor/domEditResizeLocal.test.ts +131 -0
- package/src/components/editor/domEditResizeLocal.ts +125 -0
- package/src/components/editor/domEditingDom.ts +1 -1
- package/src/components/editor/manualEditsDomPatches.test.ts +47 -0
- package/src/components/editor/manualOffsetDrag.ts +55 -12
- package/src/components/editor/offCanvasIndicatorGeometry.ts +31 -10
- package/src/components/editor/offCanvasIndicatorRefresh.test.tsx +25 -1
- package/src/components/editor/resizeDraft.ts +108 -0
- package/src/components/editor/snapEngine.test.ts +12 -78
- package/src/components/editor/snapEngine.ts +13 -53
- package/src/components/editor/useDomEditNudge.test.tsx +228 -0
- package/src/components/editor/useDomEditNudge.ts +254 -0
- package/src/components/editor/useDomEditOverlayGestures.ts +115 -116
- package/src/components/editor/useDomEditOverlayRects.ts +9 -2
- package/src/components/nle/AssetPreviewOverlay.tsx +147 -0
- package/src/components/nle/NLEContext.test.ts +144 -0
- package/src/components/nle/NLEContext.tsx +328 -0
- package/src/components/nle/NLEPreview.tsx +1 -1
- package/src/components/nle/PreviewOverlays.tsx +240 -0
- package/src/components/nle/PreviewPane.tsx +163 -0
- package/src/components/nle/TimelinePane.test.ts +60 -0
- package/src/components/nle/TimelinePane.tsx +293 -0
- package/src/components/nle/TimelineResizeDivider.tsx +8 -2
- package/src/components/nle/useCompositionStack.test.tsx +44 -0
- package/src/components/nle/useCompositionStack.ts +17 -9
- package/src/components/nle/useTimelineEditCallbacks.ts +217 -0
- package/src/components/sidebar/AssetCard.tsx +326 -0
- package/src/components/sidebar/AssetContextMenu.tsx +15 -30
- package/src/components/sidebar/AssetsTab.test.ts +96 -0
- package/src/components/sidebar/AssetsTab.tsx +71 -206
- package/src/components/sidebar/AudioRow.tsx +39 -2
- package/src/components/sidebar/BlocksTab.tsx +8 -1
- package/src/components/sidebar/LeftSidebar.tsx +4 -1
- package/src/components/sidebar/assetHelpers.ts +29 -0
- package/src/contexts/DomEditContext.tsx +9 -0
- package/src/contexts/StudioContext.tsx +9 -8
- package/src/contexts/TimelineEditContext.tsx +3 -7
- package/src/hooks/deleteSelectedKeyframes.ts +35 -0
- package/src/hooks/domEditCommitTypes.ts +12 -1
- package/src/hooks/domSelectionTestHarness.ts +14 -0
- package/src/hooks/gestureTransaction.test.ts +311 -0
- package/src/hooks/gestureTransaction.ts +199 -0
- package/src/hooks/gsapDragCommit.test.ts +141 -41
- package/src/hooks/gsapDragCommit.ts +64 -74
- package/src/hooks/gsapDragStaticSetHelpers.ts +9 -21
- package/src/hooks/gsapResizeIntercept.test.ts +69 -0
- package/src/hooks/gsapResizeIntercept.ts +38 -10
- package/src/hooks/gsapRuntimeBridge.test.ts +49 -13
- package/src/hooks/gsapRuntimeBridge.ts +4 -4
- package/src/hooks/gsapScriptCommitTypes.ts +16 -5
- package/src/hooks/gsapShared.test.ts +19 -1
- package/src/hooks/gsapShared.ts +14 -0
- package/src/hooks/patchDocumentRootDuration.test.ts +71 -0
- package/src/hooks/timelineEditingGsap.ts +14 -0
- package/src/hooks/timelineEditingHelpers.test.ts +34 -1
- package/src/hooks/timelineEditingHelpers.ts +22 -48
- package/src/hooks/timelineMoveAdapter.test.ts +77 -0
- package/src/hooks/timelineMoveAdapter.ts +37 -0
- package/src/hooks/useAnimatedPropertyCommit.test.tsx +36 -8
- package/src/hooks/useAnimatedPropertyCommit.ts +31 -26
- package/src/hooks/useAppHotkeys.ts +16 -25
- package/src/hooks/useBlockHandlers.ts +55 -27
- package/src/hooks/useContextMenuDismiss.ts +31 -7
- package/src/hooks/useDomEditCommits.test.tsx +154 -1
- package/src/hooks/useDomEditCommits.ts +100 -3
- package/src/hooks/useDomEditPositionPatchCommit.ts +2 -0
- package/src/hooks/useDomEditSession.ts +2 -2
- package/src/hooks/useDomEditTextCommits.test.tsx +135 -0
- package/src/hooks/useDomEditTextCommits.ts +8 -3
- package/src/hooks/useDomEditWiring.ts +7 -9
- package/src/hooks/useDomGeometryCommits.ts +17 -2
- package/src/hooks/useDomSelection.test.ts +2 -36
- package/src/hooks/useDomSelection.ts +81 -51
- package/src/hooks/useDomSelectionSelectionGuards.test.ts +276 -0
- package/src/hooks/useElementLifecycleOps.test.tsx +258 -0
- package/src/hooks/useElementLifecycleOps.ts +40 -71
- package/src/hooks/useEnableKeyframes.test.ts +142 -1
- package/src/hooks/useEnableKeyframes.ts +73 -29
- package/src/hooks/useGestureCommit.test.tsx +130 -0
- package/src/hooks/useGestureCommit.ts +32 -8
- package/src/hooks/useGroupCommits.ts +7 -28
- package/src/hooks/useGsapAwareEditing.test.tsx +239 -0
- package/src/hooks/useGsapAwareEditing.ts +115 -20
- package/src/hooks/useGsapKeyframeOps.test.tsx +59 -0
- package/src/hooks/useGsapKeyframeOps.ts +38 -10
- package/src/hooks/useGsapScriptCommits.test.tsx +40 -1
- package/src/hooks/useGsapScriptCommits.ts +134 -50
- package/src/hooks/useGsapSelectionHandlers.ts +49 -16
- package/src/hooks/useMusicBeatAnalysis.ts +72 -36
- package/src/hooks/usePersistentEditHistory.test.ts +33 -0
- package/src/hooks/usePersistentEditHistory.ts +92 -58
- package/src/hooks/usePreviewPersistence.ts +25 -7
- package/src/hooks/useRazorSplit.history.test.tsx +303 -0
- package/src/hooks/useRazorSplit.test.ts +269 -0
- package/src/hooks/useRazorSplit.testHelpers.ts +64 -0
- package/src/hooks/useRazorSplit.ts +107 -36
- package/src/hooks/useRenderClipContent.ts +24 -6
- package/src/hooks/useStudioContextValue.ts +15 -5
- package/src/hooks/useStudioUrlState.ts +2 -4
- package/src/hooks/useTimelineEditing.test.tsx +20 -29
- package/src/hooks/useTimelineEditingTypes.ts +6 -0
- package/src/hooks/useTimelineGroupEditing.ts +18 -4
- package/src/index.ts +1 -1
- package/src/player/components/ImageThumbnail.test.tsx +173 -0
- package/src/player/components/ImageThumbnail.tsx +160 -0
- package/src/player/components/PlayheadIndicator.tsx +50 -5
- package/src/player/components/Timeline.test.ts +102 -19
- package/src/player/components/Timeline.tsx +194 -194
- package/src/player/components/TimelineCanvas.tsx +207 -521
- package/src/player/components/TimelineClip.tsx +4 -1
- package/src/player/components/TimelineClipDiamonds.tsx +9 -40
- package/src/player/components/TimelineEmptyState.tsx +3 -1
- package/src/player/components/TimelineLanes.tsx +487 -0
- package/src/player/components/TimelineOverlays.tsx +122 -0
- package/src/player/components/VideoThumbnail.test.tsx +152 -0
- package/src/player/components/VideoThumbnail.tsx +28 -7
- package/src/player/components/thumbnailUtils.test.ts +127 -0
- package/src/player/components/thumbnailUtils.ts +54 -0
- package/src/player/components/timelineCallbacks.ts +18 -18
- package/src/player/components/timelineClipChildren.tsx +39 -0
- package/src/player/components/timelineClipDragCommit.test.ts +943 -0
- package/src/player/components/timelineClipDragCommit.ts +401 -0
- package/src/player/components/timelineClipDragPreview.test.ts +144 -0
- package/src/player/components/timelineClipDragPreview.ts +323 -38
- package/src/player/components/timelineClipDragTypes.ts +65 -0
- package/src/player/components/timelineCollision.test.ts +477 -0
- package/src/player/components/timelineCollision.ts +263 -0
- package/src/player/components/timelineDragDrop.ts +69 -60
- package/src/player/components/timelineEditCapabilities.ts +52 -0
- package/src/player/components/timelineEditing.test.ts +1 -1
- package/src/player/components/timelineEditing.ts +76 -59
- package/src/player/components/timelineGroupEditing.test.ts +126 -0
- package/src/player/components/timelineGroupEditing.ts +133 -0
- package/src/player/components/timelineGroupResizeCommit.ts +54 -0
- package/src/player/components/timelineLayout.test.ts +95 -23
- package/src/player/components/timelineLayout.ts +120 -61
- package/src/player/components/timelineMarquee.test.ts +197 -0
- package/src/player/components/timelineMarquee.ts +112 -0
- package/src/player/components/timelineMultiDragPreview.test.ts +127 -0
- package/src/player/components/timelineMultiDragPreview.ts +106 -0
- package/src/player/components/timelineOptimisticRevision.ts +42 -0
- package/src/player/components/timelineSnapping.test.ts +134 -0
- package/src/player/components/timelineSnapping.ts +110 -0
- package/src/player/components/timelineStackingSync.test.ts +244 -0
- package/src/player/components/timelineStackingSync.ts +331 -0
- package/src/player/components/timelineTheme.ts +6 -1
- package/src/player/components/timelineZones.test.ts +425 -0
- package/src/player/components/timelineZones.ts +198 -0
- package/src/player/components/timelineZoom.test.ts +67 -0
- package/src/player/components/timelineZoom.ts +51 -0
- package/src/player/components/useResolvedTimelineEditCallbacks.ts +25 -3
- package/src/player/components/useTimelineClipDrag.resize.test.tsx +241 -0
- package/src/player/components/useTimelineClipDrag.ts +380 -386
- package/src/player/components/useTimelineEditPinning.ts +121 -0
- package/src/player/components/useTimelineGeometry.ts +129 -0
- package/src/player/components/useTimelinePlayhead.ts +4 -16
- package/src/player/components/useTimelineRangeSelection.ts +341 -50
- package/src/player/components/useTimelineStackingSync.test.tsx +67 -0
- package/src/player/components/useTimelineStackingSync.ts +83 -0
- package/src/player/hooks/useExpandedTimelineElements.ts +0 -3
- package/src/player/hooks/usePlaybackKeyboard.ts +5 -0
- package/src/player/hooks/useTimelinePlayer.ts +48 -53
- package/src/player/hooks/useTimelinePlayerLoop.ts +85 -0
- package/src/player/hooks/useTimelineSyncCallbacks.test.ts +219 -0
- package/src/player/hooks/useTimelineSyncCallbacks.ts +104 -4
- package/src/player/index.ts +3 -2
- package/src/player/lib/time.test.ts +19 -19
- package/src/player/lib/time.ts +6 -2
- package/src/player/lib/timelineDOM.test.ts +25 -98
- package/src/player/lib/timelineDOM.ts +12 -81
- package/src/player/lib/timelineElementHelpers.ts +54 -5
- package/src/player/lib/timelineIframeHelpers.ts +2 -0
- package/src/player/store/playerStore.ts +62 -3
- package/src/styles/studio.css +27 -0
- package/src/utils/assetClickBehavior.test.ts +104 -0
- package/src/utils/assetClickBehavior.ts +75 -0
- package/src/utils/assetPreviewStore.ts +33 -0
- package/src/utils/blockInstaller.ts +10 -2
- package/src/utils/canvasNudgeGate.test.ts +31 -0
- package/src/utils/canvasNudgeGate.ts +32 -0
- package/src/utils/editHistory.test.ts +35 -0
- package/src/utils/gsapSoftReload.test.ts +119 -1
- package/src/utils/gsapSoftReload.ts +153 -0
- package/src/utils/mediaTypes.ts +3 -2
- package/src/utils/resizeDebug.ts +55 -0
- package/src/utils/rootDuration.test.ts +90 -1
- package/src/utils/rootDuration.ts +76 -13
- package/src/utils/sdkCutover.ts +4 -0
- package/src/utils/studioFileHistory.ts +37 -7
- package/src/utils/studioHelpers.ts +62 -0
- package/src/utils/studioPreviewHelpers.test.ts +65 -8
- package/src/utils/studioPreviewHelpers.ts +10 -0
- package/src/utils/studioTelemetry.ts +4 -1
- package/src/utils/studioUiPreferences.test.ts +38 -0
- package/src/utils/studioUiPreferences.ts +27 -0
- package/src/utils/studioUrlState.test.ts +0 -1
- package/src/utils/timelineAssetDrop.ts +62 -0
- package/src/utils/timelineDiscovery.ts +0 -17
- package/src/utils/timelineInspector.test.ts +121 -0
- package/src/utils/timelineInspector.ts +32 -1
- package/dist/assets/index-Dq7FEg0K.css +0 -1
- package/dist/assets/index-pRhCpGPz.js +0 -423
- package/dist/chunk-SOTCF4DF.js.map +0 -1
- package/src/components/StudioPreviewArea.tsx +0 -500
- package/src/components/nle/NLELayout.test.ts +0 -12
- package/src/components/nle/NLELayout.tsx +0 -591
- package/src/player/components/TimelineLayerGutter.tsx +0 -61
- package/src/player/components/TimelineSelectionOverlays.tsx +0 -55
- package/src/player/components/timelineMarqueeSelection.test.ts +0 -87
- package/src/player/components/timelineSnapTargets.test.ts +0 -144
- package/src/player/components/timelineSnapTargets.ts +0 -164
- package/src/player/components/useTimelineClipDrag.test.tsx +0 -506
- package/src/player/components/useTimelineClipGroupDrag.ts +0 -417
- package/src/player/components/useTimelineMarqueeSelection.test.tsx +0 -231
- package/src/player/components/useTimelineMarqueeSelection.ts +0 -276
- package/src/utils/timelineDiscovery.test.ts +0 -90
- /package/dist/{domEditingLayers-2ECJK24D.js.map → domEditingLayers-6LQGKPOI.js.map} +0 -0
|
@@ -9,6 +9,14 @@ export interface OverlayRect {
|
|
|
9
9
|
height: number;
|
|
10
10
|
editScaleX: number;
|
|
11
11
|
editScaleY: number;
|
|
12
|
+
/**
|
|
13
|
+
* The element's live transform rotation in DEGREES (screen/CSS convention, CW
|
|
14
|
+
* positive), decomposed from its computed transform matrix. Present so the
|
|
15
|
+
* selection chrome can render as an oriented bounding box (OBB) that co-rotates
|
|
16
|
+
* with the element. Omitted (treated as 0) for group/union rects and when the
|
|
17
|
+
* transform is unmeasurable — those render axis-aligned exactly as before.
|
|
18
|
+
*/
|
|
19
|
+
angle?: number;
|
|
12
20
|
}
|
|
13
21
|
|
|
14
22
|
export interface GroupOverlayItem {
|
|
@@ -98,31 +106,115 @@ export function toVisibleOverlayRect(
|
|
|
98
106
|
return rect ? { ...rect, ...hugRectForElement(rect, element) } : null;
|
|
99
107
|
}
|
|
100
108
|
|
|
101
|
-
|
|
109
|
+
/**
|
|
110
|
+
* getComputedStyle(element).transform decomposed into a DOMMatrix, read ONCE.
|
|
111
|
+
* Shared by orientedOverlayRect's rotation gate and elementCornerOverlayPoints
|
|
112
|
+
* so a single measurement pass serves both — constructing this twice per frame
|
|
113
|
+
* (one read per consumer) was redundant work; see orientedOverlayRect below.
|
|
114
|
+
*/
|
|
115
|
+
interface ElementTransformSnapshot {
|
|
116
|
+
matrix: DOMMatrix;
|
|
117
|
+
cs: CSSStyleDeclaration;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function readElementTransformSnapshot(
|
|
121
|
+
win: Window,
|
|
122
|
+
element: HTMLElement,
|
|
123
|
+
): ElementTransformSnapshot | null {
|
|
124
|
+
const DOMMatrixCtor = (win as Window & typeof globalThis).DOMMatrix;
|
|
125
|
+
if (!DOMMatrixCtor) return null;
|
|
126
|
+
const cs = win.getComputedStyle(element);
|
|
127
|
+
try {
|
|
128
|
+
const matrix = new DOMMatrixCtor(cs.transform === "none" ? "" : cs.transform);
|
|
129
|
+
return { matrix, cs };
|
|
130
|
+
} catch {
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* The element's live transform rotation, in DEGREES (screen/CSS convention, CW
|
|
137
|
+
* positive), decomposed from its transform matrix (rotation = atan2(b, a)).
|
|
138
|
+
* GSAP folds rotation and scale into the same matrix; this reads rotation only.
|
|
139
|
+
* Skew is ignored (does not affect atan2(b, a)).
|
|
140
|
+
*/
|
|
141
|
+
function rotationDegreesFromMatrix(matrix: DOMMatrix): number {
|
|
142
|
+
const a = Number.isFinite(matrix.a) ? matrix.a : 1;
|
|
143
|
+
const b = Number.isFinite(matrix.b) ? matrix.b : 0;
|
|
144
|
+
const deg = (Math.atan2(b, a) * 180) / Math.PI;
|
|
145
|
+
return Number.isFinite(deg) ? deg : 0;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** Below this, orientedOverlayRect treats the element as unrotated and returns
|
|
149
|
+
* the AABB directly (see its doc comment) — tight enough to only swallow
|
|
150
|
+
* matrix-decomposition floating-point noise, never an actual rotation. */
|
|
151
|
+
const ROTATION_GATE_EPSILON_DEG = 1e-4;
|
|
152
|
+
|
|
153
|
+
/** iframe→overlay mapping basis shared by every overlay-geometry function. */
|
|
154
|
+
interface OverlayRootScale {
|
|
155
|
+
iframeRect: DOMRect;
|
|
156
|
+
overlayRect: DOMRect;
|
|
157
|
+
rootScaleX: number;
|
|
158
|
+
rootScaleY: number;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** The composition root element inside the preview doc (or null when absent). */
|
|
162
|
+
function findOverlayRootElement(doc: Document | null): HTMLElement | null {
|
|
163
|
+
return doc?.querySelector<HTMLElement>("[data-composition-id]") ?? doc?.documentElement ?? null;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* The root's effective width/height for scaling: prefer the composition's
|
|
168
|
+
* declared dimensions (data-width/data-height), which stay fixed while GSAP
|
|
169
|
+
* transforms mutate the measured rect; fall back to the measured rect. Null when
|
|
170
|
+
* unmeasurable.
|
|
171
|
+
*/
|
|
172
|
+
function resolveRootDimensions(root: HTMLElement | null): { width: number; height: number } | null {
|
|
173
|
+
if (!root) return null;
|
|
174
|
+
const rootRect = root.getBoundingClientRect();
|
|
175
|
+
const width = readPositiveDimension(root.getAttribute("data-width")) ?? rootRect.width;
|
|
176
|
+
const height = readPositiveDimension(root.getAttribute("data-height")) ?? rootRect.height;
|
|
177
|
+
if (!width || !height) return null;
|
|
178
|
+
return { width, height };
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* The iframe/overlay client rects and the iframe→root scale factors. Uses the
|
|
183
|
+
* composition's declared dimensions (data-width/data-height) for the scale
|
|
184
|
+
* instead of rootRect.width/height: when GSAP applies transforms (scale,
|
|
185
|
+
* translate) to the root, rootRect dimensions change but the composition's
|
|
186
|
+
* canonical size stays fixed, and using rootRect misaligns the overlay during
|
|
187
|
+
* animated playback. Returns null when the geometry is unmeasurable.
|
|
188
|
+
*/
|
|
189
|
+
function computeOverlayRootScale(
|
|
102
190
|
overlayEl: HTMLDivElement,
|
|
103
191
|
iframe: HTMLIFrameElement,
|
|
104
|
-
|
|
105
|
-
):
|
|
192
|
+
doc: Document | null,
|
|
193
|
+
): OverlayRootScale | null {
|
|
106
194
|
const iframeRect = iframe.getBoundingClientRect();
|
|
107
195
|
const overlayRect = overlayEl.getBoundingClientRect();
|
|
108
|
-
const
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
196
|
+
const dims = resolveRootDimensions(findOverlayRootElement(doc));
|
|
197
|
+
if (!dims) return null;
|
|
198
|
+
return {
|
|
199
|
+
iframeRect,
|
|
200
|
+
overlayRect,
|
|
201
|
+
rootScaleX: iframeRect.width / dims.width,
|
|
202
|
+
rootScaleY: iframeRect.height / dims.height,
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function toOverlayRect(
|
|
207
|
+
overlayEl: HTMLDivElement,
|
|
208
|
+
iframe: HTMLIFrameElement,
|
|
209
|
+
element: HTMLElement,
|
|
210
|
+
precomputedScale?: OverlayRootScale | null,
|
|
211
|
+
): OverlayRect | null {
|
|
212
|
+
const scale =
|
|
213
|
+
precomputedScale ?? computeOverlayRootScale(overlayEl, iframe, iframe.contentDocument);
|
|
214
|
+
if (!scale) return null;
|
|
215
|
+
const { iframeRect, overlayRect, rootScaleX, rootScaleY } = scale;
|
|
122
216
|
|
|
123
217
|
const elementRect = element.getBoundingClientRect();
|
|
124
|
-
const rootScaleX = iframeRect.width / rootWidth;
|
|
125
|
-
const rootScaleY = iframeRect.height / rootHeight;
|
|
126
218
|
const sourceBoundary = findSourceBoundary(element);
|
|
127
219
|
const sourceBoundaryRect = sourceBoundary?.getBoundingClientRect();
|
|
128
220
|
const editScale = resolveDomEditCoordinateScale({
|
|
@@ -144,7 +236,163 @@ export function toOverlayRect(
|
|
|
144
236
|
};
|
|
145
237
|
}
|
|
146
238
|
|
|
239
|
+
/** Which physical corner of the (possibly rotated) element a resize handle keeps
|
|
240
|
+
* fixed: NW grabs the top-left, so the bottom-right (se) is the anchor, etc. */
|
|
241
|
+
export type FixedCorner = "nw" | "ne" | "sw" | "se";
|
|
242
|
+
|
|
243
|
+
/** Distance between two overlay-px corner points — the edge-length math
|
|
244
|
+
* orientedOverlayRect uses to turn corners into a width/height. Exported so a
|
|
245
|
+
* caller already holding raw corners (e.g. a resize gesture mid-measurement)
|
|
246
|
+
* can derive the same dimensions without a second orientedOverlayRect call. */
|
|
247
|
+
export function cornerEdgeLength(a: { x: number; y: number }, b: { x: number; y: number }): number {
|
|
248
|
+
return Math.hypot(b.x - a.x, b.y - a.y);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* The centroid (rendered center) of the four transformed corners from
|
|
253
|
+
* `elementCornerOverlayPoints`, in overlay px. This is the element's true rotation
|
|
254
|
+
* center — the point a center-anchored resize keeps planted.
|
|
255
|
+
*/
|
|
256
|
+
export function overlayCornersCentroid(corners: Record<FixedCorner, { x: number; y: number }>): {
|
|
257
|
+
x: number;
|
|
258
|
+
y: number;
|
|
259
|
+
} {
|
|
260
|
+
return {
|
|
261
|
+
x: (corners.nw.x + corners.ne.x + corners.se.x + corners.sw.x) / 4,
|
|
262
|
+
y: (corners.nw.y + corners.ne.y + corners.se.y + corners.sw.y) / 4,
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* The element's border-box corners in OVERLAY coordinates, honoring its live
|
|
268
|
+
* transform (rotation/skew/scale) — NOT the axis-aligned getBoundingClientRect.
|
|
269
|
+
* A rotated element's four visual corners are the transformed local box corners.
|
|
270
|
+
* Uses the same iframe→overlay root scale as toOverlayRect so the returned
|
|
271
|
+
* points share that function's coordinate space. Returns null when the
|
|
272
|
+
* geometry is unmeasurable.
|
|
273
|
+
*/
|
|
274
|
+
export function elementCornerOverlayPoints(
|
|
275
|
+
overlayEl: HTMLDivElement,
|
|
276
|
+
iframe: HTMLIFrameElement,
|
|
277
|
+
element: HTMLElement,
|
|
278
|
+
precomputedScale?: OverlayRootScale | null,
|
|
279
|
+
precomputedTransform?: ElementTransformSnapshot | null,
|
|
280
|
+
): Record<FixedCorner, { x: number; y: number }> | null {
|
|
281
|
+
const win = iframe.contentWindow;
|
|
282
|
+
const doc = iframe.contentDocument;
|
|
283
|
+
if (!win || !doc) return null;
|
|
284
|
+
const DOMPointCtor = (win as Window & typeof globalThis).DOMPoint;
|
|
285
|
+
if (!DOMPointCtor) return null;
|
|
286
|
+
|
|
287
|
+
const scale = precomputedScale ?? computeOverlayRootScale(overlayEl, iframe, doc);
|
|
288
|
+
if (!scale) return null;
|
|
289
|
+
const { iframeRect, overlayRect, rootScaleX, rootScaleY } = scale;
|
|
290
|
+
|
|
291
|
+
// The element's local border box maps to viewport coords by the SAME transform
|
|
292
|
+
// matrix the browser used for its BCR. We recover the transform's screen-space
|
|
293
|
+
// action from the BCR: transformPoint(localCorner - origin) gives a corner
|
|
294
|
+
// RELATIVE to the transformed origin. We anchor those relative corners to the
|
|
295
|
+
// BCR by matching the AABB of the transformed corners to the real BCR — the
|
|
296
|
+
// constant offset cancels in the before/after difference the caller takes, but
|
|
297
|
+
// we resolve it fully here so callers can also read absolute overlay positions.
|
|
298
|
+
const transform = precomputedTransform ?? readElementTransformSnapshot(win, element);
|
|
299
|
+
if (!transform) return null;
|
|
300
|
+
const { matrix, cs } = transform;
|
|
301
|
+
const w = element.offsetWidth;
|
|
302
|
+
const h = element.offsetHeight;
|
|
303
|
+
const originParts = cs.transformOrigin.split(" ").map((p) => Number.parseFloat(p));
|
|
304
|
+
const ox = Number.isFinite(originParts[0]!) ? originParts[0]! : w / 2;
|
|
305
|
+
const oy = Number.isFinite(originParts[1]!) ? originParts[1]! : h / 2;
|
|
306
|
+
const rel = (lx: number, ly: number): { x: number; y: number } => {
|
|
307
|
+
const p = matrix.transformPoint(new DOMPointCtor(lx - ox, ly - oy));
|
|
308
|
+
return { x: p.x, y: p.y };
|
|
309
|
+
};
|
|
310
|
+
const relCorners = {
|
|
311
|
+
nw: rel(0, 0),
|
|
312
|
+
ne: rel(w, 0),
|
|
313
|
+
se: rel(w, h),
|
|
314
|
+
sw: rel(0, h),
|
|
315
|
+
};
|
|
316
|
+
// Recover the absolute viewport position by matching to the element's BCR:
|
|
317
|
+
// the relative corners' AABB min corresponds to the BCR's top-left.
|
|
318
|
+
const xs = [relCorners.nw.x, relCorners.ne.x, relCorners.se.x, relCorners.sw.x];
|
|
319
|
+
const ys = [relCorners.nw.y, relCorners.ne.y, relCorners.se.y, relCorners.sw.y];
|
|
320
|
+
const bcr = element.getBoundingClientRect();
|
|
321
|
+
const dx = bcr.left - Math.min(...xs);
|
|
322
|
+
const dy = bcr.top - Math.min(...ys);
|
|
323
|
+
const toOverlay = (pt: { x: number; y: number }): { x: number; y: number } => ({
|
|
324
|
+
x: iframeRect.left - overlayRect.left + (pt.x + dx) * rootScaleX,
|
|
325
|
+
y: iframeRect.top - overlayRect.top + (pt.y + dy) * rootScaleY,
|
|
326
|
+
});
|
|
327
|
+
return {
|
|
328
|
+
nw: toOverlay(relCorners.nw),
|
|
329
|
+
ne: toOverlay(relCorners.ne),
|
|
330
|
+
se: toOverlay(relCorners.se),
|
|
331
|
+
sw: toOverlay(relCorners.sw),
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* The selection chrome's ORIENTED bounding box: the element's UNROTATED border box
|
|
337
|
+
* expressed in overlay coordinates (center-anchored left/top/width/height) plus the
|
|
338
|
+
* live rotation angle. Rendering that rect with `transform: rotate(angle)` about its
|
|
339
|
+
* center reproduces the element's real transformed corners exactly, so the border,
|
|
340
|
+
* corner dots, rotate handle, and crop pills all co-rotate with the object.
|
|
341
|
+
*
|
|
342
|
+
* Built from `elementCornerOverlayPoints` (the real transformed corners): the OBB
|
|
343
|
+
* center is the corner centroid, the unrotated width/height are the edge lengths, and
|
|
344
|
+
* left/top place the unrotated box so that rotating it about its center lands the
|
|
345
|
+
* corners back on the measured points. At angle 0 this equals `toOverlayRect` (the
|
|
346
|
+
* AABB and OBB coincide), so unrotated chrome is pixel-identical to today.
|
|
347
|
+
*
|
|
348
|
+
* Returns the plain AABB rect (angle 0) when the corner geometry can't be measured.
|
|
349
|
+
*
|
|
350
|
+
* Rotation gate: an unrotated element's OBB is numerically identical to its AABB
|
|
351
|
+
* (the comment above), so a cheap rotation read decides up front whether the
|
|
352
|
+
* (much pricier) corner-transform pass runs at all — for the overwhelming
|
|
353
|
+
* majority of selections, which aren't rotated, this call is just `toOverlayRect`
|
|
354
|
+
* plus one getComputedStyle/DOMMatrix read. The root scale and the transform
|
|
355
|
+
* snapshot are each computed once per call and threaded into both the rotation
|
|
356
|
+
* read and the corner math, instead of every helper re-measuring independently.
|
|
357
|
+
*/
|
|
358
|
+
export function orientedOverlayRect(
|
|
359
|
+
overlayEl: HTMLDivElement,
|
|
360
|
+
iframe: HTMLIFrameElement,
|
|
361
|
+
element: HTMLElement,
|
|
362
|
+
): OverlayRect | null {
|
|
363
|
+
const scale = computeOverlayRootScale(overlayEl, iframe, iframe.contentDocument);
|
|
364
|
+
if (!scale) return null;
|
|
365
|
+
const base = toOverlayRect(overlayEl, iframe, element, scale);
|
|
366
|
+
if (!base) return null;
|
|
367
|
+
|
|
368
|
+
const win = iframe.contentWindow;
|
|
369
|
+
const transform = win ? readElementTransformSnapshot(win, element) : null;
|
|
370
|
+
const angle = transform ? rotationDegreesFromMatrix(transform.matrix) : 0;
|
|
371
|
+
if (Math.abs(angle) < ROTATION_GATE_EPSILON_DEG) return base;
|
|
372
|
+
|
|
373
|
+
const corners = elementCornerOverlayPoints(overlayEl, iframe, element, scale, transform);
|
|
374
|
+
if (!corners) return base;
|
|
375
|
+
// Unrotated edge lengths (in overlay px): nw→ne is the width, nw→sw the height.
|
|
376
|
+
const width = cornerEdgeLength(corners.nw, corners.ne);
|
|
377
|
+
const height = cornerEdgeLength(corners.nw, corners.sw);
|
|
378
|
+
const centerX = (corners.nw.x + corners.se.x) / 2;
|
|
379
|
+
const centerY = (corners.nw.y + corners.se.y) / 2;
|
|
380
|
+
if (!Number.isFinite(width) || !Number.isFinite(height) || width <= 0 || height <= 0) {
|
|
381
|
+
return base;
|
|
382
|
+
}
|
|
383
|
+
return {
|
|
384
|
+
left: centerX - width / 2,
|
|
385
|
+
top: centerY - height / 2,
|
|
386
|
+
width,
|
|
387
|
+
height,
|
|
388
|
+
editScaleX: base.editScaleX,
|
|
389
|
+
editScaleY: base.editScaleY,
|
|
390
|
+
angle,
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
|
|
147
394
|
const OVERLAY_RECT_EPSILON_PX = 0.5;
|
|
395
|
+
const OVERLAY_RECT_ANGLE_EPSILON_DEG = 0.1;
|
|
148
396
|
|
|
149
397
|
export function rectsEqual(a: OverlayRect | null, b: OverlayRect | null): boolean {
|
|
150
398
|
if (a === b) return true;
|
|
@@ -155,7 +403,8 @@ export function rectsEqual(a: OverlayRect | null, b: OverlayRect | null): boolea
|
|
|
155
403
|
Math.abs(a.width - b.width) < OVERLAY_RECT_EPSILON_PX &&
|
|
156
404
|
Math.abs(a.height - b.height) < OVERLAY_RECT_EPSILON_PX &&
|
|
157
405
|
Math.abs(a.editScaleX - b.editScaleX) < 0.001 &&
|
|
158
|
-
Math.abs(a.editScaleY - b.editScaleY) < 0.001
|
|
406
|
+
Math.abs(a.editScaleY - b.editScaleY) < 0.001 &&
|
|
407
|
+
Math.abs((a.angle ?? 0) - (b.angle ?? 0)) < OVERLAY_RECT_ANGLE_EPSILON_DEG
|
|
159
408
|
);
|
|
160
409
|
}
|
|
161
410
|
|
|
@@ -228,6 +477,17 @@ export function groupAwareOverlayRect(
|
|
|
228
477
|
return { ...union, editScaleX: rect.editScaleX, editScaleY: rect.editScaleY };
|
|
229
478
|
}
|
|
230
479
|
|
|
480
|
+
/** Groups stay axis-aligned unions; ordinary elements keep their oriented box. */
|
|
481
|
+
export function orientedGroupAwareOverlayRect(
|
|
482
|
+
overlayEl: HTMLDivElement,
|
|
483
|
+
iframe: HTMLIFrameElement,
|
|
484
|
+
el: HTMLElement,
|
|
485
|
+
): OverlayRect | null {
|
|
486
|
+
return el.hasAttribute("data-hf-group")
|
|
487
|
+
? groupAwareOverlayRect(overlayEl, iframe, el)
|
|
488
|
+
: orientedOverlayRect(overlayEl, iframe, el);
|
|
489
|
+
}
|
|
490
|
+
|
|
231
491
|
export function filterNestedDomEditGroupItems<T extends { element: HTMLElement }>(items: T[]): T[] {
|
|
232
492
|
return items.filter(
|
|
233
493
|
(item) => !items.some((other) => other !== item && other.element.contains(item.element)),
|
|
@@ -13,10 +13,20 @@ import type { PreviewMouseDownOptions } from "../../hooks/usePreviewInteraction"
|
|
|
13
13
|
|
|
14
14
|
export type GestureKind = "drag" | "resize" | "rotate";
|
|
15
15
|
|
|
16
|
+
/** Which corner handle initiated a resize gesture. */
|
|
17
|
+
export type ResizeHandle = "nw" | "ne" | "sw" | "se";
|
|
18
|
+
|
|
16
19
|
export const BLOCKED_MOVE_THRESHOLD_PX = 4;
|
|
17
|
-
const MIN_RESIZE_EDGE_PX = 20;
|
|
18
20
|
const ROTATION_COMMIT_EPSILON_DEGREES = 0.05;
|
|
19
21
|
const ROTATION_SNAP_DEGREES = 15;
|
|
22
|
+
/**
|
|
23
|
+
* Above this rotation, resize/move edge-snapping is bypassed. Industry editors
|
|
24
|
+
* (tldraw/Figma) don't edge-snap rotated boxes — the snap targets are axis-aligned
|
|
25
|
+
* AABBs, so snapping a rotated box's AABB to them shifts the box in a way the user
|
|
26
|
+
* can't predict; a wrong snap is worse than none. Rotation ~0 keeps snapping exactly
|
|
27
|
+
* as before.
|
|
28
|
+
*/
|
|
29
|
+
export const ROTATED_SNAP_BYPASS_DEGREES = 0.5;
|
|
20
30
|
|
|
21
31
|
export interface GestureState {
|
|
22
32
|
kind: GestureKind;
|
|
@@ -62,6 +72,19 @@ export interface GestureState {
|
|
|
62
72
|
snapContext?: SnapContext;
|
|
63
73
|
lastSnappedDx?: number;
|
|
64
74
|
lastSnappedDy?: number;
|
|
75
|
+
/** Corner the resize gesture grabbed (resize gestures only). */
|
|
76
|
+
resizeHandle?: ResizeHandle;
|
|
77
|
+
/** Last anchoring translation applied during a corner resize (overlay px). */
|
|
78
|
+
lastResizeAnchor?: { dx: number; dy: number };
|
|
79
|
+
/**
|
|
80
|
+
* The element's rendered CENTER in overlay px at gesture start (the centroid of
|
|
81
|
+
* its four real — possibly rotated — corners). A center-anchored resize keeps this
|
|
82
|
+
* point pinned; the per-frame anchor translation is computed as the shift of this
|
|
83
|
+
* exact center, not an AABB width/height delta (which only holds the center still
|
|
84
|
+
* when the element grows symmetrically from an unrotated layout box). Undefined
|
|
85
|
+
* when the corner geometry can't be measured (member creation still succeeded).
|
|
86
|
+
*/
|
|
87
|
+
resizeFixedCenterStart?: { x: number; y: number };
|
|
65
88
|
}
|
|
66
89
|
|
|
67
90
|
export interface GroupGestureState {
|
|
@@ -89,48 +112,23 @@ export function focusDomEditOverlayElement(element: FocusableDomEditOverlay | nu
|
|
|
89
112
|
element?.focus({ preventScroll: true });
|
|
90
113
|
}
|
|
91
114
|
|
|
92
|
-
|
|
115
|
+
/**
|
|
116
|
+
* Overlay-px translation that keeps the element's CENTER fixed while a corner
|
|
117
|
+
* resizes: a CSS width/height change grows the layout box from its top-left, so
|
|
118
|
+
* the center drifts by half the size change on each axis; translating back by that
|
|
119
|
+
* half-delta re-pins the center. This is the UNROTATED (AABB) fallback used only
|
|
120
|
+
* when the element's real transformed corners can't be measured — the primary path
|
|
121
|
+
* pins the measured center (rotation-safe) in useDomEditOverlayGestures.
|
|
122
|
+
*/
|
|
123
|
+
export function resolveResizeCenterAnchorOffset(input: {
|
|
93
124
|
originWidth: number;
|
|
94
125
|
originHeight: number;
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
scaleY: number;
|
|
99
|
-
// Rendered-per-CSS-pixel factor of the element itself (its live GSAP scale).
|
|
100
|
-
// The CSS width/height the draft writes get multiplied by this on screen, so
|
|
101
|
-
// the cursor delta must be divided by it — otherwise the box outruns the
|
|
102
|
-
// pointer on a rescaled element and snaps back on release. Defaults to 1.
|
|
103
|
-
contentScaleX?: number;
|
|
104
|
-
contentScaleY?: number;
|
|
105
|
-
dx: number;
|
|
106
|
-
dy: number;
|
|
107
|
-
uniform: boolean;
|
|
108
|
-
}): { overlayWidth: number; overlayHeight: number; width: number; height: number } {
|
|
109
|
-
const scaleX = input.scaleX > 0 ? input.scaleX : 1;
|
|
110
|
-
const scaleY = input.scaleY > 0 ? input.scaleY : 1;
|
|
111
|
-
const contentScaleX =
|
|
112
|
-
input.contentScaleX !== undefined && input.contentScaleX > 0 ? input.contentScaleX : 1;
|
|
113
|
-
const contentScaleY =
|
|
114
|
-
input.contentScaleY !== undefined && input.contentScaleY > 0 ? input.contentScaleY : 1;
|
|
115
|
-
|
|
116
|
-
if (input.uniform) {
|
|
117
|
-
const deltaX = input.dx / (scaleX * contentScaleX);
|
|
118
|
-
const deltaY = input.dy / (scaleY * contentScaleY);
|
|
119
|
-
const delta = Math.abs(deltaX) >= Math.abs(deltaY) ? deltaX : deltaY;
|
|
120
|
-
const side = Math.max(1, Math.max(input.actualWidth, input.actualHeight) + delta);
|
|
121
|
-
return {
|
|
122
|
-
overlayWidth: Math.max(MIN_RESIZE_EDGE_PX, side * scaleX * contentScaleX),
|
|
123
|
-
overlayHeight: Math.max(MIN_RESIZE_EDGE_PX, side * scaleY * contentScaleY),
|
|
124
|
-
width: side,
|
|
125
|
-
height: side,
|
|
126
|
-
};
|
|
127
|
-
}
|
|
128
|
-
|
|
126
|
+
overlayWidth: number;
|
|
127
|
+
overlayHeight: number;
|
|
128
|
+
}): { dx: number; dy: number } {
|
|
129
129
|
return {
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
width: Math.max(1, input.actualWidth + input.dx / (scaleX * contentScaleX)),
|
|
133
|
-
height: Math.max(1, input.actualHeight + input.dy / (scaleY * contentScaleY)),
|
|
130
|
+
dx: (input.originWidth - input.overlayWidth) / 2,
|
|
131
|
+
dy: (input.originHeight - input.overlayHeight) / 2,
|
|
134
132
|
};
|
|
135
133
|
}
|
|
136
134
|
|
|
@@ -214,7 +212,12 @@ export type UseDomEditOverlayGesturesOptions = {
|
|
|
214
212
|
(updates: DomEditGroupPathOffsetCommit[]) => Promise<void> | void
|
|
215
213
|
>;
|
|
216
214
|
onBoxSizeCommitRef: RefObject<
|
|
217
|
-
(
|
|
215
|
+
(
|
|
216
|
+
s: DomEditSelection,
|
|
217
|
+
n: { width: number; height: number },
|
|
218
|
+
offset?: { x: number; y: number },
|
|
219
|
+
restore?: () => void,
|
|
220
|
+
) => Promise<void> | void
|
|
218
221
|
>;
|
|
219
222
|
onRotationCommitRef: RefObject<
|
|
220
223
|
(s: DomEditSelection, n: { angle: number }) => Promise<void> | void
|
|
@@ -20,15 +20,19 @@ import {
|
|
|
20
20
|
} from "./manualEdits";
|
|
21
21
|
import {
|
|
22
22
|
type OverlayRect,
|
|
23
|
+
elementCornerOverlayPoints,
|
|
23
24
|
filterNestedDomEditGroupItems,
|
|
25
|
+
overlayCornersCentroid,
|
|
24
26
|
selectionCacheKey,
|
|
25
27
|
} from "./domEditOverlayGeometry";
|
|
26
28
|
import {
|
|
27
29
|
type GestureKind,
|
|
28
30
|
type GestureState,
|
|
31
|
+
type ResizeHandle,
|
|
29
32
|
type UseDomEditOverlayGesturesOptions,
|
|
30
33
|
} from "./domEditOverlayGestures";
|
|
31
34
|
import { collectSnapContext, buildExcludeElements } from "./snapTargetCollection";
|
|
35
|
+
import { logResize, resetResizeMoveLog } from "../../utils/resizeDebug";
|
|
32
36
|
|
|
33
37
|
export function startGroupDrag(
|
|
34
38
|
e: React.PointerEvent<HTMLElement>,
|
|
@@ -100,7 +104,11 @@ export function startGesture(
|
|
|
100
104
|
kind: GestureKind,
|
|
101
105
|
e: React.PointerEvent<HTMLElement>,
|
|
102
106
|
opts: UseDomEditOverlayGesturesOptions,
|
|
103
|
-
options?: {
|
|
107
|
+
options?: {
|
|
108
|
+
selection?: DomEditSelection;
|
|
109
|
+
rect?: OverlayRect | null;
|
|
110
|
+
resizeHandle?: ResizeHandle;
|
|
111
|
+
},
|
|
104
112
|
): boolean {
|
|
105
113
|
const sel = options?.selection ?? opts.selectionRef.current;
|
|
106
114
|
const rect = options?.rect ?? opts.overlayRectRef.current;
|
|
@@ -173,7 +181,29 @@ export function startGesture(
|
|
|
173
181
|
initialPathOffset = result.member.initialPathOffset;
|
|
174
182
|
manualEditDragToken = result.member.gestureToken;
|
|
175
183
|
} else {
|
|
176
|
-
|
|
184
|
+
// Center-anchored corner resize (CapCut model): the element scales about its
|
|
185
|
+
// CENTER, which stays planted. All four corners behave identically, so EVERY
|
|
186
|
+
// corner needs the manual-offset member that translates the element to re-pin
|
|
187
|
+
// its center per frame (the memberless else-branch is only a defensive fallback
|
|
188
|
+
// if member creation fails, e.g. the element can't take a manual offset).
|
|
189
|
+
const needsAnchorOffset = kind === "resize" && sel.capabilities.canApplyManualOffset;
|
|
190
|
+
if (needsAnchorOffset) {
|
|
191
|
+
const result = createManualOffsetDragMember({
|
|
192
|
+
key: selectionCacheKey(sel),
|
|
193
|
+
selection: sel,
|
|
194
|
+
element: sel.element,
|
|
195
|
+
rect,
|
|
196
|
+
});
|
|
197
|
+
if (result.ok) {
|
|
198
|
+
pathOffsetMember = result.member;
|
|
199
|
+
initialPathOffset = result.member.initialPathOffset;
|
|
200
|
+
manualEditDragToken = result.member.gestureToken;
|
|
201
|
+
} else {
|
|
202
|
+
manualEditDragToken = beginStudioManualEditGesture(sel.element);
|
|
203
|
+
}
|
|
204
|
+
} else {
|
|
205
|
+
manualEditDragToken = beginStudioManualEditGesture(sel.element);
|
|
206
|
+
}
|
|
177
207
|
}
|
|
178
208
|
|
|
179
209
|
const overlayBounds = overlayEl?.getBoundingClientRect();
|
|
@@ -181,6 +211,17 @@ export function startGesture(
|
|
|
181
211
|
const centerY = (overlayBounds?.top ?? 0) + rect.top + rect.height / 2;
|
|
182
212
|
|
|
183
213
|
const iframe = opts.iframeRef.current;
|
|
214
|
+
|
|
215
|
+
// For a center-anchored corner resize, capture the element's rendered CENTER (the
|
|
216
|
+
// centroid of its four real, rotation-aware corners) now, so per-frame anchoring
|
|
217
|
+
// can pin that exact point instead of an axis-aligned width/height delta (which
|
|
218
|
+
// only holds the center still when the element grows symmetrically from an
|
|
219
|
+
// unrotated layout box). Present whenever an anchor member exists (all corners).
|
|
220
|
+
let resizeFixedCenterStart: { x: number; y: number } | undefined;
|
|
221
|
+
if (kind === "resize" && pathOffsetMember && overlayEl && iframe) {
|
|
222
|
+
const corners = elementCornerOverlayPoints(overlayEl, iframe, sel.element);
|
|
223
|
+
if (corners) resizeFixedCenterStart = overlayCornersCentroid(corners);
|
|
224
|
+
}
|
|
184
225
|
const snapContext =
|
|
185
226
|
(kind === "drag" || kind === "resize") && overlayEl && iframe
|
|
186
227
|
? collectSnapContext({
|
|
@@ -219,6 +260,25 @@ export function startGesture(
|
|
|
219
260
|
resizeAnchor,
|
|
220
261
|
manualEditDragToken,
|
|
221
262
|
snapContext,
|
|
263
|
+
resizeHandle: kind === "resize" ? (options?.resizeHandle ?? "se") : undefined,
|
|
264
|
+
resizeFixedCenterStart,
|
|
222
265
|
};
|
|
266
|
+
if (kind === "resize") {
|
|
267
|
+
resetResizeMoveLog();
|
|
268
|
+
logResize("start", {
|
|
269
|
+
handle: options?.resizeHandle ?? "se",
|
|
270
|
+
pointer: { x: e.clientX, y: e.clientY },
|
|
271
|
+
center: { x: centerX, y: centerY },
|
|
272
|
+
origin: { left: rect.left, top: rect.top, w: rect.width, h: rect.height },
|
|
273
|
+
actual: { w: actualWidth, h: actualHeight },
|
|
274
|
+
editScale: { x: rect.editScaleX, y: rect.editScaleY },
|
|
275
|
+
contentScale: { x: contentScaleX, y: contentScaleY },
|
|
276
|
+
rotation: rotation.angle,
|
|
277
|
+
hasOffsetMember: !!pathOffsetMember,
|
|
278
|
+
fixedCenterStart: resizeFixedCenterStart ?? null,
|
|
279
|
+
initialBoxSize: opts.gestureRef.current?.initialBoxSize ?? null,
|
|
280
|
+
initialInlineStyle: sel.element.getAttribute("style"),
|
|
281
|
+
});
|
|
282
|
+
}
|
|
223
283
|
return true;
|
|
224
284
|
}
|