@hyperframes/studio 0.7.55 → 0.7.57
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/hyperframes-player--Z69cEkE.js +459 -0
- package/dist/assets/{index-CPetwHFV.js → index-Bf1x1y8H.js} +1 -1
- package/dist/assets/index-DfmYkU44.js +423 -0
- package/dist/assets/{index-BXaqaVKt.js → index-KsfE1bUu.js} +1 -1
- package/dist/assets/index-_pqzyxB1.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 +172 -144
- package/dist/index.html +2 -2
- package/dist/index.js +43613 -40346
- 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 +152 -1
- package/src/components/editor/CanvasContextMenu.tsx +33 -18
- 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 +126 -125
- 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 +53 -2
- 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 +106 -10
- package/src/components/editor/canvasContextMenuZOrder.test.ts +186 -2
- package/src/components/editor/canvasContextMenuZOrder.ts +144 -22
- 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/propertyPanelColorGradingSection.tsx +15 -21
- 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/useCanvasContextMenuState.ts +92 -0
- 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/editor/useZOrderCrossedFlash.tsx +68 -0
- package/src/components/nle/AssetPreviewOverlay.test.tsx +106 -0
- package/src/components/nle/AssetPreviewOverlay.tsx +171 -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 +260 -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.test.tsx +106 -0
- package/src/components/sidebar/AssetCard.tsx +343 -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 +56 -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 +23 -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 +125 -1
- package/src/hooks/timelineEditingHelpers.ts +53 -292
- package/src/hooks/timelineMoveAdapter.test.ts +81 -0
- package/src/hooks/timelineMoveAdapter.ts +39 -0
- package/src/hooks/timelineTimingSync.test.ts +150 -0
- package/src/hooks/timelineTimingSync.ts +412 -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/useCaptionDetection.ts +2 -0
- package/src/hooks/useCompositionDimensions.ts +32 -5
- package/src/hooks/useContextMenuDismiss.ts +31 -7
- package/src/hooks/useDomEditCommits.test.tsx +245 -1
- package/src/hooks/useDomEditCommits.ts +162 -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 +423 -0
- package/src/hooks/useElementLifecycleOps.ts +53 -71
- package/src/hooks/useElementPicker.ts +7 -8
- 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 +148 -18
- 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/useTimelineAssetDropOps.ts +175 -0
- package/src/hooks/useTimelineEditing.test.tsx +398 -241
- package/src/hooks/useTimelineEditing.ts +181 -265
- package/src/hooks/useTimelineEditingTypes.ts +6 -0
- package/src/hooks/useTimelineGroupEditing.ts +167 -132
- 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/PlayerControls.tsx +5 -5
- package/src/player/components/PlayheadIndicator.tsx +55 -5
- package/src/player/components/Timeline.test.ts +183 -23
- package/src/player/components/Timeline.tsx +194 -194
- package/src/player/components/TimelineCanvas.tsx +218 -523
- 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/TimelineRuler.tsx +58 -48
- 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 +1026 -0
- package/src/player/components/timelineClipDragCommit.ts +498 -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 +42 -2
- 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 +207 -75
- package/src/player/components/timelineMarquee.test.ts +197 -0
- package/src/player/components/timelineMarquee.ts +112 -0
- package/src/player/components/timelineOptimisticRevision.ts +42 -0
- package/src/player/components/timelineRevealScroll.test.ts +133 -0
- package/src/player/components/timelineRevealScroll.ts +91 -0
- package/src/player/components/timelineStackingSync.test.ts +60 -0
- package/src/player/components/timelineStackingSync.ts +27 -2
- package/src/player/components/timelineTheme.ts +6 -1
- package/src/player/components/timelineTrackPersistPipeline.test.ts +168 -0
- package/src/player/components/timelineZones.test.ts +122 -310
- package/src/player/components/timelineZones.ts +82 -135
- 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/useTimelineRevealClip.ts +56 -0
- package/src/player/components/useTimelineStackingSync.test.tsx +77 -0
- package/src/player/components/useTimelineStackingSync.ts +94 -0
- package/src/player/hooks/useExpandedTimelineElements.ts +7 -3
- package/src/player/hooks/usePlaybackKeyboard.ts +5 -0
- package/src/player/hooks/useTimelinePlayer.seek.test.ts +55 -0
- package/src/player/hooks/useTimelinePlayer.test.ts +36 -3
- package/src/player/hooks/useTimelinePlayer.ts +52 -53
- package/src/player/hooks/useTimelinePlayerLoop.ts +85 -0
- package/src/player/hooks/useTimelineSyncCallbacks.ts +9 -1
- package/src/player/index.ts +5 -2
- package/src/player/lib/runtimeProtocol.test.ts +48 -0
- package/src/player/lib/runtimeProtocol.ts +65 -0
- 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 +21 -81
- package/src/player/lib/timelineElementHelpers.ts +73 -5
- package/src/player/lib/timelineIframeHelpers.ts +4 -4
- package/src/player/store/playerStore.test.ts +38 -13
- package/src/player/store/playerStore.ts +99 -3
- package/src/styles/studio.css +27 -0
- package/src/utils/assetPreviewDismiss.test.ts +27 -0
- package/src/utils/assetPreviewDismiss.ts +29 -0
- package/src/utils/assetPreviewStore.ts +33 -0
- package/src/utils/blockInstaller.ts +10 -2
- 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/studioUrlState.test.ts +0 -1
- package/src/utils/timelineAssetDrop.test.ts +144 -52
- package/src/utils/timelineAssetDrop.ts +67 -29
- package/src/utils/timelineDiscovery.ts +0 -17
- package/dist/assets/hyperframes-player-BBrKzTGd.js +0 -459
- package/dist/assets/index-BRwkMj0w.js +0 -423
- package/dist/assets/index-Dq7FEg0K.css +0 -1
- 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
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canvas arrow-key nudge for DomEditOverlay: arrows move the selected
|
|
3
|
+
* element(s) 1 composition px, Shift = 10. Each keypress previews through the
|
|
4
|
+
* same GSAP/CSS channel as a drag draft, and the burst commits ONCE through
|
|
5
|
+
* the same onPathOffsetCommit / onGroupPathOffsetCommit path a drag drop uses
|
|
6
|
+
* — so a nudge burst is exactly a tiny drag: one source patch, one undo entry.
|
|
7
|
+
*/
|
|
8
|
+
import { useCallback, useEffect, useRef, type RefObject } from "react";
|
|
9
|
+
import { useMountEffect } from "../../hooks/useMountEffect";
|
|
10
|
+
import { isEditableTarget } from "../../utils/timelineDiscovery";
|
|
11
|
+
import { acquireCanvasNudgeKeys } from "../../utils/canvasNudgeGate";
|
|
12
|
+
import type { DomEditSelection } from "./domEditing";
|
|
13
|
+
import {
|
|
14
|
+
type GroupOverlayItem,
|
|
15
|
+
type OverlayRect,
|
|
16
|
+
filterNestedDomEditGroupItems,
|
|
17
|
+
} from "./domEditOverlayGeometry";
|
|
18
|
+
import type {
|
|
19
|
+
BlockedMoveState,
|
|
20
|
+
DomEditGroupPathOffsetCommit,
|
|
21
|
+
GestureState,
|
|
22
|
+
GroupGestureState,
|
|
23
|
+
} from "./domEditOverlayGestures";
|
|
24
|
+
import {
|
|
25
|
+
applyManualOffsetNudgeCommit,
|
|
26
|
+
applyManualOffsetNudgeDraft,
|
|
27
|
+
createManualOffsetDragMember,
|
|
28
|
+
endManualOffsetDragMembers,
|
|
29
|
+
restoreManualOffsetDragMembers,
|
|
30
|
+
type ManualOffsetDragMember,
|
|
31
|
+
} from "./manualOffsetDrag";
|
|
32
|
+
import { isStudioManualEditGestureCurrent, restoreStudioPathOffset } from "./manualEdits";
|
|
33
|
+
import {
|
|
34
|
+
CANVAS_NUDGE_COMMIT_DEBOUNCE_MS,
|
|
35
|
+
canCanvasNudgeTargets,
|
|
36
|
+
resolveCanvasNudgeDelta,
|
|
37
|
+
} from "./domEditNudge";
|
|
38
|
+
|
|
39
|
+
interface NudgeSession {
|
|
40
|
+
members: ManualOffsetDragMember[];
|
|
41
|
+
isGroup: boolean;
|
|
42
|
+
/** Accumulated delta of the burst, in composition px. */
|
|
43
|
+
accum: { x: number; y: number };
|
|
44
|
+
timer: ReturnType<typeof setTimeout> | null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface UseDomEditNudgeParams {
|
|
48
|
+
selection: DomEditSelection | null;
|
|
49
|
+
groupSelections: DomEditSelection[];
|
|
50
|
+
allowCanvasMovement: boolean;
|
|
51
|
+
selectionRef: RefObject<DomEditSelection | null>;
|
|
52
|
+
overlayRectRef: RefObject<OverlayRect | null>;
|
|
53
|
+
groupOverlayItemsRef: RefObject<GroupOverlayItem[]>;
|
|
54
|
+
gestureRef: RefObject<GestureState | null>;
|
|
55
|
+
groupGestureRef: RefObject<GroupGestureState | null>;
|
|
56
|
+
blockedMoveRef: RefObject<BlockedMoveState | null>;
|
|
57
|
+
onManualDragStartRef: RefObject<(() => void) | undefined>;
|
|
58
|
+
onPathOffsetCommitRef: RefObject<
|
|
59
|
+
(
|
|
60
|
+
s: DomEditSelection,
|
|
61
|
+
n: { x: number; y: number },
|
|
62
|
+
m?: { altKey?: boolean },
|
|
63
|
+
) => Promise<void> | void
|
|
64
|
+
>;
|
|
65
|
+
onGroupPathOffsetCommitRef: RefObject<
|
|
66
|
+
(updates: DomEditGroupPathOffsetCommit[]) => Promise<void> | void
|
|
67
|
+
>;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
type NudgeTarget = {
|
|
71
|
+
key: string;
|
|
72
|
+
selection: DomEditSelection;
|
|
73
|
+
element: HTMLElement;
|
|
74
|
+
rect: OverlayRect;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* A selection's stable identity, not its object reference. A parent that
|
|
79
|
+
* doesn't memoize the selection it passes down hands us a new object every
|
|
80
|
+
* render for the SAME element — keying an effect on the object itself would
|
|
81
|
+
* then re-fire on every render instead of on an actual selection change.
|
|
82
|
+
*
|
|
83
|
+
* All discriminators are folded in (id / hfId / selector / selectorIndex /
|
|
84
|
+
* label), not just the first present one: two id-less siblings can share a
|
|
85
|
+
* selector, so `id ?? selector ?? label` collapsed them to the same key —
|
|
86
|
+
* selecting sibling B mid-burst then didn't flush A's pending nudge, so the
|
|
87
|
+
* next arrow kept moving A. selectorIndex is the discriminator that separates
|
|
88
|
+
* them. The parts are only ever compared for equality (never parsed back),
|
|
89
|
+
* so they're joined on a NUL delimiter — a byte no discriminator can contain,
|
|
90
|
+
* so distinct selections stay distinct even when a value (e.g. a label) holds
|
|
91
|
+
* a space. It's written as the escape "\u0000", not a literal 0x00 byte, so
|
|
92
|
+
* this source file stays text (a raw NUL makes git treat it as binary).
|
|
93
|
+
*/
|
|
94
|
+
function selectionIdentityKey(selection: DomEditSelection | null): string {
|
|
95
|
+
if (!selection) return "";
|
|
96
|
+
return [
|
|
97
|
+
selection.id ?? "",
|
|
98
|
+
selection.hfId ?? "",
|
|
99
|
+
selection.selector ?? "",
|
|
100
|
+
selection.selectorIndex ?? "",
|
|
101
|
+
selection.label,
|
|
102
|
+
].join("\u0000");
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function groupSelectionsIdentityKey(selections: DomEditSelection[]): string {
|
|
106
|
+
return selections.map(selectionIdentityKey).join(" ");
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Drag members for a multi-selection nudge (same snapshot a group drag uses). */
|
|
110
|
+
function resolveGroupNudgeTargets(groupItems: GroupOverlayItem[]): NudgeTarget[] | null {
|
|
111
|
+
if (!canCanvasNudgeTargets(groupItems.map((item) => item.selection))) return null;
|
|
112
|
+
return filterNestedDomEditGroupItems(groupItems);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Drag member for a single-selection nudge, or null when it can't be moved. */
|
|
116
|
+
function resolveSingleNudgeTarget(
|
|
117
|
+
sel: DomEditSelection | null,
|
|
118
|
+
rect: OverlayRect | null,
|
|
119
|
+
): NudgeTarget[] | null {
|
|
120
|
+
if (!sel || !rect || !sel.capabilities.canApplyManualOffset || !sel.element.isConnected) {
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
return [{ key: sel.id ?? sel.selector ?? sel.label, selection: sel, element: sel.element, rect }];
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* True when a keydown must not start/extend a nudge: canvas movement disabled,
|
|
128
|
+
* a pointer gesture already owns the element, or the user is typing in a field.
|
|
129
|
+
*/
|
|
130
|
+
function shouldIgnoreNudgeKey(p: UseDomEditNudgeParams, event: KeyboardEvent): boolean {
|
|
131
|
+
if (!p.allowCanvasMovement || event.defaultPrevented) return true;
|
|
132
|
+
if (p.gestureRef.current || p.groupGestureRef.current || p.blockedMoveRef.current) return true;
|
|
133
|
+
return isEditableTarget(event.target);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function useDomEditNudge(params: UseDomEditNudgeParams): { flushNudge: () => void } {
|
|
137
|
+
const sessionRef = useRef<NudgeSession | null>(null);
|
|
138
|
+
const paramsRef = useRef(params);
|
|
139
|
+
paramsRef.current = params;
|
|
140
|
+
|
|
141
|
+
// Commit the pending burst: one source write per burst = one undo entry.
|
|
142
|
+
// Mirrors the drag's onPointerUp — same commit callbacks, same failure
|
|
143
|
+
// restore, same member teardown.
|
|
144
|
+
const commitSession = () => {
|
|
145
|
+
const session = sessionRef.current;
|
|
146
|
+
if (!session) return;
|
|
147
|
+
sessionRef.current = null;
|
|
148
|
+
if (session.timer) clearTimeout(session.timer);
|
|
149
|
+
const updates: DomEditGroupPathOffsetCommit[] = session.members.map((member) => ({
|
|
150
|
+
selection: member.selection,
|
|
151
|
+
next: applyManualOffsetNudgeCommit(member, session.accum),
|
|
152
|
+
}));
|
|
153
|
+
const p = paramsRef.current;
|
|
154
|
+
const commit = session.isGroup
|
|
155
|
+
? p.onGroupPathOffsetCommitRef.current(updates)
|
|
156
|
+
: p.onPathOffsetCommitRef.current(updates[0].selection, updates[0].next);
|
|
157
|
+
void Promise.resolve(commit)
|
|
158
|
+
.catch(() => {
|
|
159
|
+
for (const member of session.members) {
|
|
160
|
+
if (isStudioManualEditGestureCurrent(member.element, member.gestureToken)) {
|
|
161
|
+
restoreStudioPathOffset(member.element, member.initialPathOffset);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
})
|
|
165
|
+
.finally(() => endManualOffsetDragMembers(session.members));
|
|
166
|
+
};
|
|
167
|
+
const commitSessionRef = useRef(commitSession);
|
|
168
|
+
commitSessionRef.current = commitSession;
|
|
169
|
+
|
|
170
|
+
// Build drag members for the current target set — the same member snapshot a
|
|
171
|
+
// pointer drag starts from (startGesture / startGroupDrag), so the nudge
|
|
172
|
+
// commit converts offsets → GSAP x/y with identical math.
|
|
173
|
+
const beginSession = (): NudgeSession | null => {
|
|
174
|
+
const p = paramsRef.current;
|
|
175
|
+
const groupItems = p.groupOverlayItemsRef.current;
|
|
176
|
+
const isGroup = groupItems.length > 1;
|
|
177
|
+
const targets = isGroup
|
|
178
|
+
? resolveGroupNudgeTargets(groupItems)
|
|
179
|
+
: resolveSingleNudgeTarget(p.selectionRef.current, p.overlayRectRef.current);
|
|
180
|
+
if (!targets) return null;
|
|
181
|
+
const members: ManualOffsetDragMember[] = [];
|
|
182
|
+
for (const target of targets) {
|
|
183
|
+
const result = createManualOffsetDragMember(target);
|
|
184
|
+
if (!result.ok) {
|
|
185
|
+
restoreManualOffsetDragMembers(members);
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
members.push(result.member);
|
|
189
|
+
}
|
|
190
|
+
if (members.length === 0) return null;
|
|
191
|
+
// Same side effect a drag start has (pauses preview playback).
|
|
192
|
+
p.onManualDragStartRef.current?.();
|
|
193
|
+
return { members, isGroup, accum: { x: 0, y: 0 }, timer: null };
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
const handleKeyDown = (event: KeyboardEvent) => {
|
|
197
|
+
const p = paramsRef.current;
|
|
198
|
+
if (shouldIgnoreNudgeKey(p, event)) return;
|
|
199
|
+
const delta = resolveCanvasNudgeDelta(event);
|
|
200
|
+
if (!delta) return;
|
|
201
|
+
const session = sessionRef.current ?? beginSession();
|
|
202
|
+
if (!session) return;
|
|
203
|
+
sessionRef.current = session;
|
|
204
|
+
event.preventDefault();
|
|
205
|
+
session.accum = { x: session.accum.x + delta.dx, y: session.accum.y + delta.dy };
|
|
206
|
+
for (const member of session.members) applyManualOffsetNudgeDraft(member, session.accum);
|
|
207
|
+
if (session.timer) clearTimeout(session.timer);
|
|
208
|
+
session.timer = setTimeout(() => commitSessionRef.current(), CANVAS_NUDGE_COMMIT_DEBOUNCE_MS);
|
|
209
|
+
};
|
|
210
|
+
const handleKeyDownRef = useRef(handleKeyDown);
|
|
211
|
+
handleKeyDownRef.current = handleKeyDown;
|
|
212
|
+
|
|
213
|
+
useMountEffect(() => {
|
|
214
|
+
const listener = (event: KeyboardEvent) => handleKeyDownRef.current(event);
|
|
215
|
+
// Capture, like the other app-level key handlers, so a focused panel
|
|
216
|
+
// can't swallow the nudge before it reaches us.
|
|
217
|
+
window.addEventListener("keydown", listener, true);
|
|
218
|
+
return () => {
|
|
219
|
+
window.removeEventListener("keydown", listener, true);
|
|
220
|
+
commitSessionRef.current();
|
|
221
|
+
};
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
// Selection change ends the burst: commit to the OLD target before the
|
|
225
|
+
// arrows start moving the new one. Keyed on the selection's stable identity
|
|
226
|
+
// (id/selector/label), NOT the object reference — a parent that re-creates
|
|
227
|
+
// the selection object on every render (without memoizing it) must not
|
|
228
|
+
// flush a burst that's still in progress for the same element.
|
|
229
|
+
const selectionKey = selectionIdentityKey(params.selection);
|
|
230
|
+
const groupSelectionsKey = groupSelectionsIdentityKey(params.groupSelections);
|
|
231
|
+
// eslint-disable-next-line no-restricted-syntax
|
|
232
|
+
useEffect(() => () => commitSessionRef.current(), [selectionKey, groupSelectionsKey]);
|
|
233
|
+
|
|
234
|
+
// Claim the arrow keys from the playback frame-step while the selection is
|
|
235
|
+
// nudgeable (see canvasNudgeGate — listener order is mount-dependent, so
|
|
236
|
+
// preventDefault alone can't arbitrate).
|
|
237
|
+
// eslint-disable-next-line no-restricted-syntax
|
|
238
|
+
useEffect(() => {
|
|
239
|
+
const targets =
|
|
240
|
+
params.groupSelections.length > 1
|
|
241
|
+
? params.groupSelections
|
|
242
|
+
: params.selection
|
|
243
|
+
? [params.selection]
|
|
244
|
+
: [];
|
|
245
|
+
if (!params.allowCanvasMovement || !canCanvasNudgeTargets(targets)) return;
|
|
246
|
+
return acquireCanvasNudgeKeys();
|
|
247
|
+
}, [params.selection, params.groupSelections, params.allowCanvasMovement]);
|
|
248
|
+
|
|
249
|
+
// A pointer gesture supersedes a pending burst — DomEditOverlay flushes on
|
|
250
|
+
// pointerdown-capture so the drag's member snapshot starts from the nudged,
|
|
251
|
+
// committed position.
|
|
252
|
+
const flushNudge = useCallback(() => commitSessionRef.current(), []);
|
|
253
|
+
return { flushNudge };
|
|
254
|
+
}
|
|
@@ -4,7 +4,6 @@
|
|
|
4
4
|
* Owns: onPointerMove, onPointerUp, clearPointerState.
|
|
5
5
|
* startGesture and startGroupDrag live in domEditOverlayStartGesture.ts.
|
|
6
6
|
*/
|
|
7
|
-
import { setElementGsapPosition } from "../../utils/elementGsap";
|
|
8
7
|
import type { RefObject } from "react";
|
|
9
8
|
import { type DomEditSelection } from "./domEditing";
|
|
10
9
|
import {
|
|
@@ -30,36 +29,29 @@ import {
|
|
|
30
29
|
import {
|
|
31
30
|
type GroupOverlayItem,
|
|
32
31
|
type OverlayRect,
|
|
32
|
+
orientedOverlayRect,
|
|
33
33
|
resolveDomEditGroupOverlayRect,
|
|
34
|
-
toOverlayRect,
|
|
35
34
|
} from "./domEditOverlayGeometry";
|
|
36
35
|
import {
|
|
37
36
|
BLOCKED_MOVE_THRESHOLD_PX,
|
|
38
37
|
type GestureKind,
|
|
39
38
|
type GestureState,
|
|
40
39
|
type GroupGestureState,
|
|
40
|
+
type ResizeHandle,
|
|
41
41
|
type UseDomEditOverlayGesturesOptions,
|
|
42
|
+
ROTATED_SNAP_BYPASS_DEGREES,
|
|
42
43
|
hasDomEditRotationChanged,
|
|
43
|
-
resolveDomEditResizeGesture,
|
|
44
44
|
resolveDomEditRotationGesture,
|
|
45
45
|
} from "./domEditOverlayGestures";
|
|
46
|
+
import { resolveCenterResizeSize } from "./domEditResizeLocal";
|
|
47
|
+
import { resolveResizeDraftRect } from "./resizeDraft";
|
|
46
48
|
import {
|
|
47
49
|
startGesture as _startGesture,
|
|
48
50
|
startGroupDrag as _startGroupDrag,
|
|
49
51
|
} from "./domEditOverlayStartGesture";
|
|
50
52
|
import { hugRectForElement } from "./domEditOverlayCrop";
|
|
51
|
-
import {
|
|
52
|
-
|
|
53
|
-
resolveResizeSnapAdjustment,
|
|
54
|
-
resolveEquidistanceGuides,
|
|
55
|
-
SNAP_THRESHOLD_PX,
|
|
56
|
-
} from "./snapEngine";
|
|
57
|
-
/** Undo the resize draft's anchor pin: snap GSAP x/y back to the gesture base. */
|
|
58
|
-
function restoreResizeAnchorPin(element: HTMLElement, g: GestureState): void {
|
|
59
|
-
const anchor = g.resizeAnchor;
|
|
60
|
-
if (!anchor || (anchor.pinX === 0 && anchor.pinY === 0)) return;
|
|
61
|
-
setElementGsapPosition(element, anchor.baseGsapX, anchor.baseGsapY);
|
|
62
|
-
}
|
|
53
|
+
import { resolveSnapAdjustment, resolveEquidistanceGuides, SNAP_THRESHOLD_PX } from "./snapEngine";
|
|
54
|
+
import { logResize, logResizeMove, logResizeSettle } from "../../utils/resizeDebug";
|
|
63
55
|
|
|
64
56
|
export function createDomEditOverlayGestureHandlers(opts: UseDomEditOverlayGesturesOptions) {
|
|
65
57
|
const setDraftOverlayRect = (next: OverlayRect) => {
|
|
@@ -73,6 +65,10 @@ export function createDomEditOverlayGestureHandlers(opts: UseDomEditOverlayGestu
|
|
|
73
65
|
height: g.originHeight,
|
|
74
66
|
editScaleX: g.editScaleX,
|
|
75
67
|
editScaleY: g.editScaleY,
|
|
68
|
+
// Every draft rect must carry the element's rotation: the rotation wrapper
|
|
69
|
+
// renders rotate(overlayRect.angle), so an omitted angle straightens the
|
|
70
|
+
// chrome for the duration of the draft (the "straightens while moving" bug).
|
|
71
|
+
angle: g.actualRotation,
|
|
76
72
|
});
|
|
77
73
|
};
|
|
78
74
|
const setDraftGroupOverlayItems = (next: GroupOverlayItem[]) => {
|
|
@@ -88,7 +84,11 @@ export function createDomEditOverlayGestureHandlers(opts: UseDomEditOverlayGestu
|
|
|
88
84
|
const startGesture = (
|
|
89
85
|
kind: GestureKind,
|
|
90
86
|
e: React.PointerEvent<HTMLElement>,
|
|
91
|
-
options?: {
|
|
87
|
+
options?: {
|
|
88
|
+
selection?: DomEditSelection;
|
|
89
|
+
rect?: OverlayRect | null;
|
|
90
|
+
resizeHandle?: ResizeHandle;
|
|
91
|
+
},
|
|
92
92
|
) => _startGesture(kind, e, opts, options);
|
|
93
93
|
|
|
94
94
|
// fallow-ignore-next-line complexity
|
|
@@ -183,8 +183,7 @@ export function createDomEditOverlayGestureHandlers(opts: UseDomEditOverlayGestu
|
|
|
183
183
|
actualAngle: g.actualRotation,
|
|
184
184
|
snap: e.shiftKey,
|
|
185
185
|
});
|
|
186
|
-
|
|
187
|
-
if (!draftViaGsap) {
|
|
186
|
+
if (!applyRotationDraftViaGsap(sel.element, rotated.angle)) {
|
|
188
187
|
applyStudioRotationDraft(sel.element, rotated);
|
|
189
188
|
}
|
|
190
189
|
return;
|
|
@@ -192,7 +191,11 @@ export function createDomEditOverlayGestureHandlers(opts: UseDomEditOverlayGestu
|
|
|
192
191
|
|
|
193
192
|
if (g.kind === "drag") {
|
|
194
193
|
const sc = g.snapContext;
|
|
195
|
-
|
|
194
|
+
// Bypass edge-snapping for rotated elements — the snap targets and the
|
|
195
|
+
// snapped rect are axis-aligned, so snapping a rotated box's AABB shifts it
|
|
196
|
+
// unpredictably. Rotation ~0 keeps snapping exactly as before.
|
|
197
|
+
const dragRotated = Math.abs(g.actualRotation) >= ROTATED_SNAP_BYPASS_DEGREES;
|
|
198
|
+
if (!dragRotated && sc?.snapEnabled && sc.targets.length > 0) {
|
|
196
199
|
// Snap the element's VISIBLE (crop-hugged) edges, not the full bounds.
|
|
197
200
|
const movingRect = hugRectForElement(
|
|
198
201
|
{
|
|
@@ -246,6 +249,7 @@ export function createDomEditOverlayGestureHandlers(opts: UseDomEditOverlayGestu
|
|
|
246
249
|
height: g.originHeight,
|
|
247
250
|
editScaleX: g.editScaleX,
|
|
248
251
|
editScaleY: g.editScaleY,
|
|
252
|
+
angle: g.actualRotation,
|
|
249
253
|
});
|
|
250
254
|
if (box) {
|
|
251
255
|
box.style.left = `${nextBoxLeft}px`;
|
|
@@ -255,90 +259,49 @@ export function createDomEditOverlayGestureHandlers(opts: UseDomEditOverlayGestu
|
|
|
255
259
|
} else {
|
|
256
260
|
if (!box) return;
|
|
257
261
|
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
gridEdges: sc.gridEdges ?? undefined,
|
|
275
|
-
threshold: SNAP_THRESHOLD_PX,
|
|
276
|
-
disabled: e.altKey,
|
|
277
|
-
});
|
|
278
|
-
dx = snap.dx;
|
|
279
|
-
dy = snap.dy;
|
|
280
|
-
opts.snapGuidesRef.current = { guides: snap.guides, spacingGuides: [] };
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
const nextSize = resolveDomEditResizeGesture({
|
|
284
|
-
originWidth: g.originWidth,
|
|
285
|
-
originHeight: g.originHeight,
|
|
286
|
-
actualWidth: g.actualWidth,
|
|
287
|
-
actualHeight: g.actualHeight,
|
|
288
|
-
scaleX: g.editScaleX,
|
|
289
|
-
scaleY: g.editScaleY,
|
|
290
|
-
contentScaleX: g.contentScaleX,
|
|
291
|
-
contentScaleY: g.contentScaleY,
|
|
292
|
-
dx,
|
|
293
|
-
dy,
|
|
294
|
-
uniform: e.shiftKey,
|
|
262
|
+
// CENTER-ANCHORED size (CapCut model): the element scales proportionally
|
|
263
|
+
// about its CENTER — the scale is the pointer's RADIAL distance from the
|
|
264
|
+
// element center now over its distance at gesture start. Rotation-invariant
|
|
265
|
+
// (a distance ignores the angle) and continuous, so all four corners behave
|
|
266
|
+
// identically and there is no per-axis projection or edge-snapping. Base size
|
|
267
|
+
// is the element-local px size at gesture start (actualWidth/Height,
|
|
268
|
+
// GSAP-scale-aware). Corner drag is ALWAYS proportional; there is no
|
|
269
|
+
// free-form stretch gesture. Edge-snapping is intentionally NOT applied:
|
|
270
|
+
// with center anchoring both edges move symmetrically, so the corner-anchored
|
|
271
|
+
// snap math no longer holds — CapCut does not edge-snap during scale either.
|
|
272
|
+
const nextSize = resolveCenterResizeSize({
|
|
273
|
+
baseWidth: g.actualWidth,
|
|
274
|
+
baseHeight: g.actualHeight,
|
|
275
|
+
pointer: { x: e.clientX, y: e.clientY },
|
|
276
|
+
pointerStart: { x: g.startX, y: g.startY },
|
|
277
|
+
centerStart: { x: g.centerX, y: g.centerY },
|
|
295
278
|
});
|
|
296
279
|
applyStudioBoxSizeDraft(sel.element, nextSize);
|
|
297
|
-
|
|
298
|
-
// size change shifts the rendered box around the element center. Measure
|
|
299
|
-
// the drift of the gesture-start corner and counter it via GSAP x/y —
|
|
300
|
-
// accumulated onto the previous pin so the correction converges instead
|
|
301
|
-
// of oscillating. The release-time position compensation re-measures the
|
|
302
|
-
// drop, so the pin composes with the commit.
|
|
303
|
-
const anchor = g.resizeAnchor;
|
|
304
|
-
if (anchor) {
|
|
305
|
-
const pinned = sel.element.getBoundingClientRect();
|
|
306
|
-
const nextPinX = anchor.pinX + (anchor.anchorX - pinned.x);
|
|
307
|
-
const nextPinY = anchor.pinY + (anchor.anchorY - pinned.y);
|
|
308
|
-
if (
|
|
309
|
-
setElementGsapPosition(
|
|
310
|
-
sel.element,
|
|
311
|
-
anchor.baseGsapX + nextPinX,
|
|
312
|
-
anchor.baseGsapY + nextPinY,
|
|
313
|
-
)
|
|
314
|
-
) {
|
|
315
|
-
anchor.pinX = nextPinX;
|
|
316
|
-
anchor.pinY = nextPinY;
|
|
317
|
-
}
|
|
318
|
-
}
|
|
319
|
-
// Re-read BCR after applying dimensions. For elements with a GSAP
|
|
320
|
-
// scale transform and centered transform-origin the visual top-left
|
|
321
|
-
// drifts and the visual size diverges from the raw CSS size, so BCR
|
|
322
|
-
// is the only accurate source for both.
|
|
280
|
+
|
|
323
281
|
const overlayEl = opts.overlayRef.current;
|
|
324
282
|
const iframe = opts.iframeRef.current;
|
|
325
|
-
const
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
const
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
283
|
+
const measureOrientedRect = () =>
|
|
284
|
+
overlayEl && iframe ? orientedOverlayRect(overlayEl, iframe, sel.element) : null;
|
|
285
|
+
|
|
286
|
+
const draftRect = resolveResizeDraftRect(
|
|
287
|
+
g,
|
|
288
|
+
sel.element,
|
|
289
|
+
overlayEl,
|
|
290
|
+
iframe,
|
|
291
|
+
measureOrientedRect,
|
|
292
|
+
);
|
|
293
|
+
logResizeMove({
|
|
294
|
+
pointer: { x: e.clientX, y: e.clientY },
|
|
295
|
+
nextSize,
|
|
296
|
+
anchor: g.lastResizeAnchor ?? null,
|
|
297
|
+
draftRect,
|
|
298
|
+
liveInlineStyle: sel.element.getAttribute("style"),
|
|
341
299
|
});
|
|
300
|
+
box.style.left = `${draftRect.left}px`;
|
|
301
|
+
box.style.top = `${draftRect.top}px`;
|
|
302
|
+
box.style.width = `${draftRect.width}px`;
|
|
303
|
+
box.style.height = `${draftRect.height}px`;
|
|
304
|
+
setDraftOverlayRect(draftRect);
|
|
342
305
|
}
|
|
343
306
|
};
|
|
344
307
|
|
|
@@ -414,9 +377,12 @@ export function createDomEditOverlayGestureHandlers(opts: UseDomEditOverlayGestu
|
|
|
414
377
|
}
|
|
415
378
|
|
|
416
379
|
if (g.kind === "resize" && movedDistance < BLOCKED_MOVE_THRESHOLD_PX) {
|
|
417
|
-
restoreResizeAnchorPin(sel.element, g);
|
|
418
380
|
restoreStudioBoxSize(sel.element, g.initialBoxSize);
|
|
419
|
-
|
|
381
|
+
if (g.pathOffsetMember) {
|
|
382
|
+
restoreManualOffsetDragMembers([g.pathOffsetMember]);
|
|
383
|
+
} else {
|
|
384
|
+
endStudioManualEditGesture(sel.element, g.manualEditDragToken);
|
|
385
|
+
}
|
|
420
386
|
if (box) {
|
|
421
387
|
box.style.width = `${g.originWidth}px`;
|
|
422
388
|
box.style.height = `${g.originHeight}px`;
|
|
@@ -444,8 +410,7 @@ export function createDomEditOverlayGestureHandlers(opts: UseDomEditOverlayGestu
|
|
|
444
410
|
restoreStudioRotation(sel.element, g.initialRotation);
|
|
445
411
|
}
|
|
446
412
|
};
|
|
447
|
-
|
|
448
|
-
if (!rotationChanged) {
|
|
413
|
+
if (!hasDomEditRotationChanged(g.actualRotation, finalRotation.angle)) {
|
|
449
414
|
restoreRotation();
|
|
450
415
|
endStudioManualEditGesture(sel.element, g.manualEditDragToken);
|
|
451
416
|
return;
|
|
@@ -464,10 +429,13 @@ export function createDomEditOverlayGestureHandlers(opts: UseDomEditOverlayGestu
|
|
|
464
429
|
)
|
|
465
430
|
restoreRotation();
|
|
466
431
|
})
|
|
467
|
-
.finally(() =>
|
|
468
|
-
endStudioManualEditGesture(sel.element, g.manualEditDragToken);
|
|
469
|
-
});
|
|
432
|
+
.finally(() => endStudioManualEditGesture(sel.element, g.manualEditDragToken));
|
|
470
433
|
} else if (g.kind === "drag") {
|
|
434
|
+
// A moved drag (taps returned earlier) must not let the release click
|
|
435
|
+
// re-select whatever now sits under the pointer — dropping over a
|
|
436
|
+
// higher-z element should keep the dragged element selected, not select
|
|
437
|
+
// the drop target. Mirrors the resize branch below.
|
|
438
|
+
opts.suppressNextBoxClickRef.current = true;
|
|
471
439
|
const dx = g.lastSnappedDx ?? e.clientX - g.startX;
|
|
472
440
|
const dy = g.lastSnappedDy ?? e.clientY - g.startY;
|
|
473
441
|
if (!g.pathOffsetMember) {
|
|
@@ -483,6 +451,7 @@ export function createDomEditOverlayGestureHandlers(opts: UseDomEditOverlayGestu
|
|
|
483
451
|
height: g.originHeight,
|
|
484
452
|
editScaleX: g.editScaleX,
|
|
485
453
|
editScaleY: g.editScaleY,
|
|
454
|
+
angle: g.actualRotation,
|
|
486
455
|
});
|
|
487
456
|
if (box) {
|
|
488
457
|
box.style.left = `${nextBoxLeft}px`;
|
|
@@ -505,18 +474,45 @@ export function createDomEditOverlayGestureHandlers(opts: UseDomEditOverlayGestu
|
|
|
505
474
|
opts.suppressNextBoxClickRef.current = true;
|
|
506
475
|
const finalSize = readStudioBoxSize(sel.element);
|
|
507
476
|
applyStudioBoxSize(sel.element, finalSize);
|
|
508
|
-
|
|
477
|
+
// Anchored corner resize (NW/NE/SW) also moved the element to keep the
|
|
478
|
+
// center planted. Land the size AND the anchor offset in a SINGLE
|
|
479
|
+
// box-size commit (one persist, one undo entry). The prior two-commit
|
|
480
|
+
// sequence re-stamped the element from source after the size-only persist
|
|
481
|
+
// but before the offset persist landed — that one frame (new size, old
|
|
482
|
+
// offset) was the release "jump". SE has no anchor member → size only.
|
|
483
|
+
const member = g.pathOffsetMember;
|
|
484
|
+
const anchor = g.lastResizeAnchor;
|
|
485
|
+
const finalOffset =
|
|
486
|
+
member && anchor && (anchor.dx !== 0 || anchor.dy !== 0)
|
|
487
|
+
? applyManualOffsetDragCommit(member, anchor.dx, anchor.dy)
|
|
488
|
+
: null;
|
|
489
|
+
logResize("release", {
|
|
490
|
+
finalSize,
|
|
491
|
+
anchor: anchor ?? null,
|
|
492
|
+
finalOffset: finalOffset ?? null,
|
|
493
|
+
hasMember: !!member,
|
|
494
|
+
inlineStyle: sel.element.getAttribute("style"),
|
|
495
|
+
});
|
|
496
|
+
const restore = () => {
|
|
497
|
+
if (
|
|
498
|
+
!g.manualEditDragToken ||
|
|
499
|
+
!isStudioManualEditGestureCurrent(sel.element, g.manualEditDragToken)
|
|
500
|
+
)
|
|
501
|
+
return;
|
|
502
|
+
restoreStudioBoxSize(sel.element, g.initialBoxSize);
|
|
503
|
+
if (finalOffset) restoreStudioPathOffset(sel.element, g.initialPathOffset);
|
|
504
|
+
};
|
|
505
|
+
void Promise.resolve(
|
|
506
|
+
opts.onBoxSizeCommitRef.current(sel, finalSize, finalOffset ?? undefined, restore),
|
|
507
|
+
)
|
|
509
508
|
.catch((error) => {
|
|
510
509
|
console.error("resize commit failed", error);
|
|
511
|
-
if (
|
|
512
|
-
g.manualEditDragToken &&
|
|
513
|
-
isStudioManualEditGestureCurrent(sel.element, g.manualEditDragToken)
|
|
514
|
-
) {
|
|
515
|
-
restoreResizeAnchorPin(sel.element, g);
|
|
516
|
-
restoreStudioBoxSize(sel.element, g.initialBoxSize);
|
|
517
|
-
}
|
|
518
510
|
})
|
|
519
|
-
.finally(() =>
|
|
511
|
+
.finally(() => {
|
|
512
|
+
if (member) endManualOffsetDragMembers([member]);
|
|
513
|
+
else endStudioManualEditGesture(sel.element, g.manualEditDragToken);
|
|
514
|
+
});
|
|
515
|
+
logResizeSettle(sel.element, "post-release");
|
|
520
516
|
}
|
|
521
517
|
};
|
|
522
518
|
|
|
@@ -534,9 +530,12 @@ export function createDomEditOverlayGestureHandlers(opts: UseDomEditOverlayGestu
|
|
|
534
530
|
restoreGestureOverlayRect(g);
|
|
535
531
|
}
|
|
536
532
|
if (g?.mode === "box-size" && sel) {
|
|
537
|
-
restoreResizeAnchorPin(sel.element, g);
|
|
538
533
|
restoreStudioBoxSize(sel.element, g.initialBoxSize);
|
|
539
|
-
|
|
534
|
+
if (g.pathOffsetMember) {
|
|
535
|
+
restoreManualOffsetDragMembers([g.pathOffsetMember]);
|
|
536
|
+
} else {
|
|
537
|
+
endStudioManualEditGesture(sel.element, g.manualEditDragToken);
|
|
538
|
+
}
|
|
540
539
|
restoreGestureOverlayRect(g);
|
|
541
540
|
}
|
|
542
541
|
if (g?.mode === "rotation" && sel) {
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
groupOverlayItemsEqual,
|
|
14
14
|
isElementVisibleForOverlay,
|
|
15
15
|
groupAwareOverlayRect,
|
|
16
|
+
orientedGroupAwareOverlayRect,
|
|
16
17
|
rectsEqual,
|
|
17
18
|
resolveElementForOverlay,
|
|
18
19
|
selectionCacheKey,
|
|
@@ -157,7 +158,13 @@ export function useDomEditOverlayRects({
|
|
|
157
158
|
// backgroundless full-bleed scene above a subcomposition), which would wrongly
|
|
158
159
|
// hide the selection box. Occlusion stays for hover, where a false hide is cheap.
|
|
159
160
|
if (el && isElementVisibleForOverlay(el)) {
|
|
160
|
-
|
|
161
|
+
// Groups render as an AABB union of their members (a group OBB is out of
|
|
162
|
+
// scope); a single element renders as an oriented box that co-rotates
|
|
163
|
+
// with its transform. orientedOverlayRect gates on rotation internally
|
|
164
|
+
// (a cheap per-call check) and only pays for the full corner-transform
|
|
165
|
+
// measurement when the element is actually rotated — this RAF loop runs
|
|
166
|
+
// every frame for any single selection, so that gate matters here most.
|
|
167
|
+
const nextRect = orientedGroupAwareOverlayRect(overlayEl, iframe, el);
|
|
161
168
|
setOverlayRect(nextRect);
|
|
162
169
|
const descendants = el.querySelectorAll("*");
|
|
163
170
|
if (descendants.length > 0 && descendants.length <= 60) {
|
|
@@ -242,7 +249,7 @@ export function useDomEditOverlayRects({
|
|
|
242
249
|
return;
|
|
243
250
|
}
|
|
244
251
|
|
|
245
|
-
setHoverRect(
|
|
252
|
+
setHoverRect(orientedGroupAwareOverlayRect(overlayEl, iframe, hoverEl));
|
|
246
253
|
};
|
|
247
254
|
|
|
248
255
|
frame = requestAnimationFrame(update);
|