@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
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
|
|
3
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
4
|
+
import type { DomEditSelection } from "./domEditing";
|
|
5
|
+
import type { GestureState, UseDomEditOverlayGesturesOptions } from "./domEditOverlayGestures";
|
|
6
|
+
|
|
7
|
+
// Origin box (overlay px). The gesture-start center is its centroid; the
|
|
8
|
+
// center-anchored resize must keep THIS point planted, which means the release
|
|
9
|
+
// must commit a nonzero offset equal to minus half the size growth.
|
|
10
|
+
const ORIGIN = { left: 0, top: 0, width: 200, height: 100 };
|
|
11
|
+
const ORIGIN_CENTER = {
|
|
12
|
+
x: ORIGIN.left + ORIGIN.width / 2,
|
|
13
|
+
y: ORIGIN.top + ORIGIN.height / 2,
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
// Consistent geometry stub: model the physical truth the real DOM would report.
|
|
17
|
+
// A CSS width/height change grows the box from its top-left, so the rendered
|
|
18
|
+
// center drifts by half the size delta; the manual offset the gesture applies
|
|
19
|
+
// (read back from the element's studio vars) pulls it back. `elementCornerOverlayPoints`
|
|
20
|
+
// returns the four corners of that drifted box; `overlayCornersCentroid` (kept
|
|
21
|
+
// real) averages them so the anchor loop can measure the true center each frame.
|
|
22
|
+
vi.mock("./domEditOverlayGeometry", async () => {
|
|
23
|
+
const actual = await vi.importActual<typeof import("./domEditOverlayGeometry")>(
|
|
24
|
+
"./domEditOverlayGeometry",
|
|
25
|
+
);
|
|
26
|
+
const { readStudioBoxSize, readStudioPathOffset } = await import("./manualEditsDom");
|
|
27
|
+
const physicalCenter = (element: HTMLElement) => {
|
|
28
|
+
const size = readStudioBoxSize(element);
|
|
29
|
+
const width = size.width > 0 ? size.width : ORIGIN.width;
|
|
30
|
+
const height = size.height > 0 ? size.height : ORIGIN.height;
|
|
31
|
+
const offset = readStudioPathOffset(element);
|
|
32
|
+
return {
|
|
33
|
+
x: ORIGIN_CENTER.x + (width - ORIGIN.width) / 2 + offset.x,
|
|
34
|
+
y: ORIGIN_CENTER.y + (height - ORIGIN.height) / 2 + offset.y,
|
|
35
|
+
width,
|
|
36
|
+
height,
|
|
37
|
+
};
|
|
38
|
+
};
|
|
39
|
+
return {
|
|
40
|
+
...actual,
|
|
41
|
+
elementCornerOverlayPoints: (_o: unknown, _i: unknown, element: HTMLElement) => {
|
|
42
|
+
const c = physicalCenter(element);
|
|
43
|
+
const hw = c.width / 2;
|
|
44
|
+
const hh = c.height / 2;
|
|
45
|
+
return {
|
|
46
|
+
nw: { x: c.x - hw, y: c.y - hh },
|
|
47
|
+
ne: { x: c.x + hw, y: c.y - hh },
|
|
48
|
+
sw: { x: c.x - hw, y: c.y + hh },
|
|
49
|
+
se: { x: c.x + hw, y: c.y + hh },
|
|
50
|
+
};
|
|
51
|
+
},
|
|
52
|
+
orientedOverlayRect: (_o: unknown, _i: unknown, element: HTMLElement) => {
|
|
53
|
+
const c = physicalCenter(element);
|
|
54
|
+
return {
|
|
55
|
+
left: c.x - c.width / 2,
|
|
56
|
+
top: c.y - c.height / 2,
|
|
57
|
+
width: c.width,
|
|
58
|
+
height: c.height,
|
|
59
|
+
editScaleX: 1,
|
|
60
|
+
editScaleY: 1,
|
|
61
|
+
angle: 0,
|
|
62
|
+
};
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
const { createDomEditOverlayGestureHandlers } = await import("./useDomEditOverlayGestures");
|
|
68
|
+
|
|
69
|
+
function ref<T>(current: T) {
|
|
70
|
+
return { current };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
interface CommitCall {
|
|
74
|
+
size: { width: number; height: number };
|
|
75
|
+
offset: { x: number; y: number } | undefined;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function buildHarness() {
|
|
79
|
+
const element = document.createElement("div");
|
|
80
|
+
document.body.append(element);
|
|
81
|
+
|
|
82
|
+
const selection = {
|
|
83
|
+
element,
|
|
84
|
+
id: "box",
|
|
85
|
+
selector: "#box",
|
|
86
|
+
selectorIndex: 0,
|
|
87
|
+
sourceFile: "index.html",
|
|
88
|
+
tagName: "div",
|
|
89
|
+
label: "Box",
|
|
90
|
+
textContent: "",
|
|
91
|
+
textFields: [],
|
|
92
|
+
capabilities: {
|
|
93
|
+
canEditText: false,
|
|
94
|
+
canEditLayout: true,
|
|
95
|
+
canMove: true,
|
|
96
|
+
canApplyManualOffset: true,
|
|
97
|
+
canApplyManualSize: true,
|
|
98
|
+
canApplyManualRotation: false,
|
|
99
|
+
canAdjustOpacity: true,
|
|
100
|
+
canAdjustFill: true,
|
|
101
|
+
canAdjustBorderRadius: true,
|
|
102
|
+
canAdjustStroke: true,
|
|
103
|
+
canAdjustShadow: true,
|
|
104
|
+
canAdjustZIndex: true,
|
|
105
|
+
},
|
|
106
|
+
computedStyle: { display: "block", position: "absolute" },
|
|
107
|
+
} as unknown as DomEditSelection;
|
|
108
|
+
|
|
109
|
+
const commits: CommitCall[] = [];
|
|
110
|
+
const overlayEl = document.createElement("div");
|
|
111
|
+
const iframe = document.createElement("iframe");
|
|
112
|
+
|
|
113
|
+
const opts: UseDomEditOverlayGesturesOptions = {
|
|
114
|
+
overlayRef: ref<HTMLDivElement | null>(overlayEl),
|
|
115
|
+
iframeRef: ref<HTMLIFrameElement | null>(iframe),
|
|
116
|
+
boxRef: ref<HTMLDivElement | null>(document.createElement("div")),
|
|
117
|
+
selectionRef: ref<DomEditSelection | null>(selection),
|
|
118
|
+
hoverSelectionRef: ref<DomEditSelection | null>(null),
|
|
119
|
+
overlayRectRef: ref<OverlayRectLike | null>({
|
|
120
|
+
left: ORIGIN.left,
|
|
121
|
+
top: ORIGIN.top,
|
|
122
|
+
width: ORIGIN.width,
|
|
123
|
+
height: ORIGIN.height,
|
|
124
|
+
editScaleX: 1,
|
|
125
|
+
editScaleY: 1,
|
|
126
|
+
}) as never,
|
|
127
|
+
groupOverlayItemsRef: ref([]),
|
|
128
|
+
gestureRef: ref<GestureState | null>(null),
|
|
129
|
+
groupGestureRef: ref(null),
|
|
130
|
+
blockedMoveRef: ref(null),
|
|
131
|
+
rafPausedRef: ref(false),
|
|
132
|
+
suppressNextBoxClickRef: ref(false),
|
|
133
|
+
setOverlayRect: () => {},
|
|
134
|
+
setGroupOverlayItems: () => {},
|
|
135
|
+
onBlockedMoveRef: ref(() => {}),
|
|
136
|
+
onManualDragStartRef: ref(() => {}),
|
|
137
|
+
onPathOffsetCommitRef: ref(() => {}),
|
|
138
|
+
onGroupPathOffsetCommitRef: ref(() => {}),
|
|
139
|
+
onBoxSizeCommitRef: ref((_s, size, offset) => {
|
|
140
|
+
commits.push({ size, offset });
|
|
141
|
+
}),
|
|
142
|
+
onRotationCommitRef: ref(() => {}),
|
|
143
|
+
onCanvasPointerMoveRef: ref(() => Promise.resolve(null)),
|
|
144
|
+
onCanvasMouseDown: () => {},
|
|
145
|
+
snapGuidesRef: ref(null),
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
const handlers = createDomEditOverlayGestureHandlers(opts);
|
|
149
|
+
return { handlers, commits, selection };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
type OverlayRectLike = {
|
|
153
|
+
left: number;
|
|
154
|
+
top: number;
|
|
155
|
+
width: number;
|
|
156
|
+
height: number;
|
|
157
|
+
editScaleX: number;
|
|
158
|
+
editScaleY: number;
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
function evt(clientX: number, clientY: number) {
|
|
162
|
+
return {
|
|
163
|
+
clientX,
|
|
164
|
+
clientY,
|
|
165
|
+
pointerId: 1,
|
|
166
|
+
button: 0,
|
|
167
|
+
altKey: false,
|
|
168
|
+
shiftKey: false,
|
|
169
|
+
preventDefault() {},
|
|
170
|
+
stopPropagation() {},
|
|
171
|
+
currentTarget: { setPointerCapture() {} },
|
|
172
|
+
} as unknown as React.PointerEvent<HTMLDivElement>;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
afterEach(() => {
|
|
176
|
+
document.body.innerHTML = "";
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
describe("anchored corner resize — the release commit feeds the center-pin offset", () => {
|
|
180
|
+
it("onPointerUp passes a nonzero offset that keeps the center planted", () => {
|
|
181
|
+
const { handlers, commits } = buildHarness();
|
|
182
|
+
|
|
183
|
+
// Start an SE corner resize. Pointer starts 100px right of the center.
|
|
184
|
+
handlers.startGesture("resize", evt(ORIGIN_CENTER.x + 100, ORIGIN_CENTER.y), {
|
|
185
|
+
resizeHandle: "se",
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
// Drag outward to radial scale 1.5 (dist 150 / 100). Several frames so the
|
|
189
|
+
// per-frame center-pin anchor accumulates and converges into g.lastResizeAnchor.
|
|
190
|
+
for (let i = 0; i < 5; i++) {
|
|
191
|
+
handlers.onPointerMove(evt(ORIGIN_CENTER.x + 150, ORIGIN_CENTER.y));
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
handlers.onPointerUp(evt(ORIGIN_CENTER.x + 150, ORIGIN_CENTER.y));
|
|
195
|
+
|
|
196
|
+
expect(commits).toHaveLength(1);
|
|
197
|
+
const { size, offset } = commits[0]!;
|
|
198
|
+
|
|
199
|
+
// Proportional 1.5x growth of the 200x100 base.
|
|
200
|
+
expect(size.width).toBeCloseTo(300, 0);
|
|
201
|
+
expect(size.height).toBeCloseTo(150, 0);
|
|
202
|
+
|
|
203
|
+
// The committed offset must be present and nonzero — the open question.
|
|
204
|
+
expect(offset).toBeDefined();
|
|
205
|
+
if (!offset) return;
|
|
206
|
+
expect(offset.x).not.toBe(0);
|
|
207
|
+
expect(offset.y).not.toBe(0);
|
|
208
|
+
|
|
209
|
+
// And it must equal minus half the size growth, i.e. it re-pins the center to
|
|
210
|
+
// exactly the gesture-start center (offset = -(finalSize - origin)/2).
|
|
211
|
+
expect(offset.x).toBeCloseTo(-(size.width - ORIGIN.width) / 2, 0);
|
|
212
|
+
expect(offset.y).toBeCloseTo(-(size.height - ORIGIN.height) / 2, 0);
|
|
213
|
+
});
|
|
214
|
+
});
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import { afterEach, describe, expect, it } from "vitest";
|
|
3
|
+
import {
|
|
4
|
+
applyStudioBoxSize,
|
|
5
|
+
applyStudioPathOffset,
|
|
6
|
+
readStudioBoxSize,
|
|
7
|
+
reapplyPositionEditsAfterSeek,
|
|
8
|
+
} from "./manualEditsDom";
|
|
9
|
+
import { buildBoxSizePatches, buildPathOffsetPatches } from "./manualEditsDomPatches";
|
|
10
|
+
import { createManualOffsetDragMember, applyManualOffsetDragCommit } from "./manualOffsetDrag";
|
|
11
|
+
import { computeNextResizeAnchor } from "./domEditResizeLocal";
|
|
12
|
+
import type { PatchOperation } from "../../utils/sourcePatcher";
|
|
13
|
+
import { splitTopLevelWhitespace } from "./manualEditsStyleHelpers";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Center-anchored corner resize (CapCut model): the element scales about its
|
|
17
|
+
* CENTER, which must stay planted across the whole gesture — including after
|
|
18
|
+
* release, on every corner and at any rotation.
|
|
19
|
+
*
|
|
20
|
+
* Root cause of the original release "shift" (proved with a real-layout Chromium
|
|
21
|
+
* replay, see the anchor-loop test below): during a resize drag the per-frame
|
|
22
|
+
* anchor is derived from the element's LIVE measured center — which already carries
|
|
23
|
+
* the offset applied on the PREVIOUS frame — while `applyManualOffsetDragDraft`
|
|
24
|
+
* treats that anchor as the ABSOLUTE offset. So `fixedStart - centerNow` is really
|
|
25
|
+
* only the RESIDUAL correction, and using it as the absolute value makes the anchor
|
|
26
|
+
* OSCILLATE between the correct value and zero every frame:
|
|
27
|
+
* frame 0: offset 0 → center shifted by the resize → anchor = full amount → apply
|
|
28
|
+
* frame 1: offset applied → center back at fixedStart → anchor = 0 → apply 0 (un-pin!)
|
|
29
|
+
* frame 2: offset 0 again → anchor = full amount → ...
|
|
30
|
+
* Release commits `g.lastResizeAnchor` from whichever parity the last pointermove
|
|
31
|
+
* landed on, so the element lands EITHER pinned OR un-pinned — an unpredictable
|
|
32
|
+
* post-release "shift".
|
|
33
|
+
*
|
|
34
|
+
* Fix (useDomEditOverlayGestures pointermove, resize branch, fa4f39168): accumulate
|
|
35
|
+
* the residual onto the previously-applied anchor instead of using it as the
|
|
36
|
+
* absolute offset, so the loop converges to a stable value on every frame. The
|
|
37
|
+
* per-frame accumulation is the exported `computeNextResizeAnchor` helper (the one
|
|
38
|
+
* call site in the pointermove resize branch); tests 1 & 2 drive it directly.
|
|
39
|
+
*/
|
|
40
|
+
|
|
41
|
+
afterEach(() => {
|
|
42
|
+
document.body.innerHTML = "";
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
/** Apply a built PatchOperation[] to a live element, mirroring sourcePatcher's
|
|
46
|
+
* inline-style / attribute application — i.e. what the persisted source carries
|
|
47
|
+
* when it is re-parsed into the DOM on the next preview load. */
|
|
48
|
+
function applyPatchesToElement(el: HTMLElement, ops: PatchOperation[]): void {
|
|
49
|
+
for (const op of ops) {
|
|
50
|
+
if (op.type === "inline-style") {
|
|
51
|
+
if (op.value === null) el.style.removeProperty(op.property);
|
|
52
|
+
else el.style.setProperty(op.property, op.value);
|
|
53
|
+
} else if (op.type === "attribute") {
|
|
54
|
+
if (op.value === null) el.removeAttribute(op.property);
|
|
55
|
+
else el.setAttribute(op.property, op.value);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Net translate applied to an element, resolving the studio offset var()
|
|
61
|
+
* expression to its px value so we compare the actually-rendered translation. */
|
|
62
|
+
function resolvedTranslatePx(el: HTMLElement): { x: number; y: number } {
|
|
63
|
+
const raw = el.style.getPropertyValue("translate").trim();
|
|
64
|
+
if (!raw || raw === "none") return { x: 0, y: 0 };
|
|
65
|
+
const vx = Number.parseFloat(el.style.getPropertyValue("--hf-studio-offset-x")) || 0;
|
|
66
|
+
const vy = Number.parseFloat(el.style.getPropertyValue("--hf-studio-offset-y")) || 0;
|
|
67
|
+
const parts = splitTopLevelWhitespace(raw);
|
|
68
|
+
const parseAxis = (part: string, varVal: number): number => {
|
|
69
|
+
if (part && part.includes("--hf-studio-offset")) return varVal;
|
|
70
|
+
const n = Number.parseFloat(part);
|
|
71
|
+
return Number.isFinite(n) ? n : 0;
|
|
72
|
+
};
|
|
73
|
+
return {
|
|
74
|
+
x: parseAxis(parts[0] ?? "", vx),
|
|
75
|
+
y: parseAxis(parts[1] ?? "", vy),
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
describe("center-anchored corner resize — no shift after release", () => {
|
|
80
|
+
it("the per-frame center anchor converges (does NOT oscillate) — the release-shift root cause", () => {
|
|
81
|
+
// Model the pointermove anchor loop that pins the element's CENTER. The physical
|
|
82
|
+
// truth (confirmed in a real browser): the measured center sits at
|
|
83
|
+
// `fixedStart - appliedOffset` shifted by the resize — i.e. applying the offset
|
|
84
|
+
// moves the center back toward its gesture-start position. Here scale=1 so
|
|
85
|
+
// screen px == offset px. `trueAnchor` is the compensating offset that pins it.
|
|
86
|
+
const trueAnchor = { dx: -60, dy: -27 };
|
|
87
|
+
const fixedStart = { x: 500, y: 270 };
|
|
88
|
+
|
|
89
|
+
// `appliedOffset` mirrors what applyManualOffsetDragDraft set last frame.
|
|
90
|
+
let appliedOffset = { x: 0, y: 0 };
|
|
91
|
+
// `lastResizeAnchor` accumulator, exactly as g.lastResizeAnchor in the fix.
|
|
92
|
+
let lastResizeAnchor: { dx: number; dy: number } | undefined;
|
|
93
|
+
|
|
94
|
+
const anchorsSeen: Array<{ dx: number; dy: number }> = [];
|
|
95
|
+
for (let frame = 0; frame < 8; frame++) {
|
|
96
|
+
// Live measured center: the resize would put it at fixedStart + trueAnchor
|
|
97
|
+
// (un-anchored), and the currently-applied offset pulls it back by that
|
|
98
|
+
// offset. So centerNow = fixedStart - trueAnchor + appliedOffset.
|
|
99
|
+
const centerNow = {
|
|
100
|
+
x: fixedStart.x - trueAnchor.dx + appliedOffset.x,
|
|
101
|
+
y: fixedStart.y - trueAnchor.dy + appliedOffset.y,
|
|
102
|
+
};
|
|
103
|
+
// ── The fixed logic (accumulate residual onto the previous anchor) ──
|
|
104
|
+
const anchor = computeNextResizeAnchor(lastResizeAnchor, fixedStart, centerNow);
|
|
105
|
+
lastResizeAnchor = anchor;
|
|
106
|
+
anchorsSeen.push(anchor);
|
|
107
|
+
// applyManualOffsetDragDraft sets the absolute offset (scale 1) = anchor.
|
|
108
|
+
appliedOffset = { x: anchor.dx, y: anchor.dy };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Every frame must report the same, correct anchor — no oscillation, so the
|
|
112
|
+
// committed value is parity-independent.
|
|
113
|
+
for (const a of anchorsSeen) {
|
|
114
|
+
expect(a).toEqual(trueAnchor);
|
|
115
|
+
}
|
|
116
|
+
// Guard against the OLD absolute formula regressing: with `anchor =
|
|
117
|
+
// fixedStart - centerNow` (no accumulation) the sequence would be
|
|
118
|
+
// [trueAnchor, 0, trueAnchor, 0, ...]; assert the last two frames agree.
|
|
119
|
+
expect(anchorsSeen.at(-1)).toEqual(anchorsSeen.at(-2));
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it("the center stays fixed for every corner at any rotation (loop converges)", () => {
|
|
123
|
+
// The pin loop is handle- and rotation-independent: it always measures the
|
|
124
|
+
// element CENTER and drives it back to fixedStart. Simulate the loop for all
|
|
125
|
+
// four corners across unrotated + rotated gestures; the resize's raw center
|
|
126
|
+
// shift varies with corner/rotation (modelled as `rawShift`), but the loop must
|
|
127
|
+
// converge the measured center onto fixedStart every time.
|
|
128
|
+
const fixedStart = { x: 640, y: 360 };
|
|
129
|
+
const HANDLES = ["nw", "ne", "sw", "se"] as const;
|
|
130
|
+
const DEGS = [0, 30, 90, 137];
|
|
131
|
+
for (const handle of HANDLES) {
|
|
132
|
+
for (const deg of DEGS) {
|
|
133
|
+
const t = (deg * Math.PI) / 180;
|
|
134
|
+
// Raw (un-pinned) center shift the size write would cause this frame —
|
|
135
|
+
// a corner/rotation-dependent vector. Its exact value is irrelevant; the
|
|
136
|
+
// loop only needs to cancel it.
|
|
137
|
+
const seed = (HANDLES.indexOf(handle) + 1) * 11;
|
|
138
|
+
const rawShift = {
|
|
139
|
+
dx: Math.cos(t) * seed - Math.sin(t) * (seed / 2),
|
|
140
|
+
dy: Math.sin(t) * seed + Math.cos(t) * (seed / 2),
|
|
141
|
+
};
|
|
142
|
+
let appliedOffset = { x: 0, y: 0 };
|
|
143
|
+
let lastResizeAnchor: { dx: number; dy: number } | undefined;
|
|
144
|
+
let centerNow = { x: fixedStart.x, y: fixedStart.y };
|
|
145
|
+
for (let frame = 0; frame < 6; frame++) {
|
|
146
|
+
centerNow = {
|
|
147
|
+
x: fixedStart.x + rawShift.dx + appliedOffset.x,
|
|
148
|
+
y: fixedStart.y + rawShift.dy + appliedOffset.y,
|
|
149
|
+
};
|
|
150
|
+
const anchor = computeNextResizeAnchor(lastResizeAnchor, fixedStart, centerNow);
|
|
151
|
+
lastResizeAnchor = anchor;
|
|
152
|
+
appliedOffset = { x: anchor.dx, y: anchor.dy };
|
|
153
|
+
}
|
|
154
|
+
// After convergence the pinned center equals the gesture-start center.
|
|
155
|
+
const pinnedCenter = {
|
|
156
|
+
x: fixedStart.x + rawShift.dx + appliedOffset.x,
|
|
157
|
+
y: fixedStart.y + rawShift.dy + appliedOffset.y,
|
|
158
|
+
};
|
|
159
|
+
expect(pinnedCenter.x).toBeCloseTo(fixedStart.x, 9);
|
|
160
|
+
expect(pinnedCenter.y).toBeCloseTo(fixedStart.y, 9);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it("net translate after persist+reload equals the committed anchor offset (non-GSAP)", () => {
|
|
166
|
+
// The committed offset flows through the real apply → persist → reload chain
|
|
167
|
+
// unchanged (this hop was proved clean; the shift is upstream in the anchor
|
|
168
|
+
// loop above, not in persistence).
|
|
169
|
+
const el = document.createElement("div");
|
|
170
|
+
el.style.setProperty("width", "200px");
|
|
171
|
+
el.style.setProperty("height", "100px");
|
|
172
|
+
document.body.appendChild(el);
|
|
173
|
+
|
|
174
|
+
const anchorDx = -30;
|
|
175
|
+
const anchorDy = -18;
|
|
176
|
+
const finalSize = { width: 240, height: 130 };
|
|
177
|
+
|
|
178
|
+
applyStudioBoxSize(el, finalSize);
|
|
179
|
+
const memberResult = createManualOffsetDragMember({
|
|
180
|
+
key: "k",
|
|
181
|
+
selection: { element: el } as never,
|
|
182
|
+
element: el,
|
|
183
|
+
rect: { left: 0, top: 0, width: 240, height: 130, editScaleX: 1, editScaleY: 1 },
|
|
184
|
+
});
|
|
185
|
+
expect(memberResult.ok).toBe(true);
|
|
186
|
+
if (!memberResult.ok) return;
|
|
187
|
+
|
|
188
|
+
const finalOffset = applyManualOffsetDragCommit(memberResult.member, anchorDx, anchorDy);
|
|
189
|
+
|
|
190
|
+
applyStudioBoxSize(el, finalSize);
|
|
191
|
+
const patches = buildBoxSizePatches(el);
|
|
192
|
+
applyStudioPathOffset(el, finalOffset);
|
|
193
|
+
patches.push(...buildPathOffsetPatches(el));
|
|
194
|
+
|
|
195
|
+
expect(resolvedTranslatePx(el)).toEqual({ x: anchorDx, y: anchorDy });
|
|
196
|
+
|
|
197
|
+
// Persist → fresh element re-parsed from source → reload re-stamp.
|
|
198
|
+
const reloaded = document.createElement("div");
|
|
199
|
+
reloaded.style.setProperty("width", "200px");
|
|
200
|
+
reloaded.style.setProperty("height", "100px");
|
|
201
|
+
document.body.appendChild(reloaded);
|
|
202
|
+
applyPatchesToElement(reloaded, patches);
|
|
203
|
+
reapplyPositionEditsAfterSeek(reloaded.ownerDocument);
|
|
204
|
+
|
|
205
|
+
expect(resolvedTranslatePx(reloaded)).toEqual({ x: anchorDx, y: anchorDy });
|
|
206
|
+
expect(readStudioBoxSize(reloaded)).toEqual(finalSize);
|
|
207
|
+
});
|
|
208
|
+
});
|