@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
|
@@ -7,8 +7,23 @@
|
|
|
7
7
|
* the computed value. Treat missing / "auto" as 0 for comparison purposes.
|
|
8
8
|
*
|
|
9
9
|
* "Overlapping siblings" = siblings whose bounding rects intersect the
|
|
10
|
-
* target's bounding rect
|
|
11
|
-
* front/back operate across all
|
|
10
|
+
* target's bounding rect AND are actually visible at the current frame.
|
|
11
|
+
* Forward/backward operate within that set; front/back operate across all
|
|
12
|
+
* siblings (full painting family, visible or not — unchanged semantics).
|
|
13
|
+
*
|
|
14
|
+
* ── Visibility ───────────────────────────────────────────────────────────────
|
|
15
|
+
* In HyperFrames compositions the nearest z-neighbor is often INVISIBLE at the
|
|
16
|
+
* paused frame: the runtime hides time-inactive clips with inline
|
|
17
|
+
* `visibility:hidden` / `display:none` (see core runtime
|
|
18
|
+
* syncTimedElementVisibility), and GSAP timelines park elements at `opacity:0`.
|
|
19
|
+
* Stepping "forward" over such a sibling looks like a silent no-op. The
|
|
20
|
+
* forward/backward comparison set therefore keeps only siblings whose
|
|
21
|
+
* element-level computed style is visible (display ≠ none, visibility ≠
|
|
22
|
+
* hidden, opacity > 0.01) — all runtime hiding signals are inline styles, so
|
|
23
|
+
* computed style covers them. Ancestor-chain checks are unnecessary here:
|
|
24
|
+
* siblings share the target's ancestors. The probe is injectable
|
|
25
|
+
* (ZOrderResolveOptions.isVisible) so the pure-module tests stay meaningful
|
|
26
|
+
* without a real style engine, mirroring how tests stub rect reading.
|
|
12
27
|
*
|
|
13
28
|
* ── Tie-awareness ────────────────────────────────────────────────────────────
|
|
14
29
|
* CSS paint order for elements that share a z-index is DOM document order:
|
|
@@ -26,6 +41,8 @@
|
|
|
26
41
|
* (project convention clamps z ≥ 0).
|
|
27
42
|
*/
|
|
28
43
|
|
|
44
|
+
import { COLOR_GRADING_SOURCE_HIDDEN_ATTR } from "@hyperframes/core/color-grading";
|
|
45
|
+
|
|
29
46
|
export type ZOrderAction = "bring-forward" | "send-backward" | "bring-to-front" | "send-to-back";
|
|
30
47
|
|
|
31
48
|
/** A resolved change: set `element`'s z-index to `zIndex`. */
|
|
@@ -34,6 +51,48 @@ export interface ZOrderPatch {
|
|
|
34
51
|
zIndex: number;
|
|
35
52
|
}
|
|
36
53
|
|
|
54
|
+
/** Injectable knobs for the pure resolver (kept mockable like rect reading). */
|
|
55
|
+
export interface ZOrderResolveOptions {
|
|
56
|
+
/**
|
|
57
|
+
* Element-level visibility probe used to scope the forward/backward
|
|
58
|
+
* comparison set. Defaults to `isElementVisibleForZOrder` (computed-style
|
|
59
|
+
* display/visibility/opacity). Injectable so tests can run without a real
|
|
60
|
+
* style engine.
|
|
61
|
+
*/
|
|
62
|
+
isVisible?: (el: HTMLElement) => boolean;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Default visibility probe: is this element itself visible at the current
|
|
67
|
+
* frame? Element-level only (siblings share the target's ancestor chain).
|
|
68
|
+
* Covers the runtime's inactive-clip hiding (inline `visibility:hidden` /
|
|
69
|
+
* `display:none`) and animation-parked `opacity:0`, all of which surface
|
|
70
|
+
* through computed style. A color-grading source (hidden at opacity:0 while
|
|
71
|
+
* its canvas paints in its place) still counts as visible, matching
|
|
72
|
+
* isElementVisibleThroughAncestors in domEditingDom.
|
|
73
|
+
*/
|
|
74
|
+
export function isElementVisibleForZOrder(el: HTMLElement): boolean {
|
|
75
|
+
try {
|
|
76
|
+
const win = el.ownerDocument?.defaultView;
|
|
77
|
+
if (!win) return true;
|
|
78
|
+
const computed = win.getComputedStyle(el);
|
|
79
|
+
if (computed.display === "none") return false;
|
|
80
|
+
if (computed.visibility === "hidden" || computed.visibility === "collapse") return false;
|
|
81
|
+
const opacity = Number.parseFloat(computed.opacity);
|
|
82
|
+
if (
|
|
83
|
+
Number.isFinite(opacity) &&
|
|
84
|
+
opacity <= 0.01 &&
|
|
85
|
+
!el.hasAttribute(COLOR_GRADING_SOURCE_HIDDEN_ATTR)
|
|
86
|
+
) {
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
return true;
|
|
90
|
+
} catch {
|
|
91
|
+
/* cross-origin / detached — assume visible (fail open, matches rect fallback) */
|
|
92
|
+
return true;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
37
96
|
interface RenderEntry {
|
|
38
97
|
element: HTMLElement;
|
|
39
98
|
zIndex: number;
|
|
@@ -81,8 +140,18 @@ function isElementNode(node: Node): node is HTMLElement {
|
|
|
81
140
|
* z-index onto the qa-clean audio element, and counting it as a sibling skews the
|
|
82
141
|
* renumber for the visible elements. `<script>/<style>/<link>/<meta>` are also
|
|
83
142
|
* non-painting and could otherwise pad the family / eat a z slot.
|
|
143
|
+
* `<template>/<noscript>` never paint either — letting them in meant renumber
|
|
144
|
+
* fallbacks wrote z-index/position into template source markup.
|
|
84
145
|
*/
|
|
85
|
-
const NON_PAINTING_TAGS = new Set([
|
|
146
|
+
const NON_PAINTING_TAGS = new Set([
|
|
147
|
+
"AUDIO",
|
|
148
|
+
"SCRIPT",
|
|
149
|
+
"STYLE",
|
|
150
|
+
"LINK",
|
|
151
|
+
"META",
|
|
152
|
+
"TEMPLATE",
|
|
153
|
+
"NOSCRIPT",
|
|
154
|
+
]);
|
|
86
155
|
|
|
87
156
|
/** A painting element: an element node whose tag actually renders pixels. */
|
|
88
157
|
function isPaintingElement(node: Node): node is HTMLElement {
|
|
@@ -112,7 +181,7 @@ function getFamily(target: HTMLElement): { entries: RenderEntry[]; targetIndex:
|
|
|
112
181
|
return { entries, targetIndex };
|
|
113
182
|
}
|
|
114
183
|
|
|
115
|
-
/** True if two DOM bounding rects
|
|
184
|
+
/** True if two DOM bounding rects strictly overlap (rects that merely touch do NOT intersect). */
|
|
116
185
|
function rectsIntersect(
|
|
117
186
|
a: { left: number; top: number; right: number; bottom: number },
|
|
118
187
|
b: { left: number; top: number; right: number; bottom: number },
|
|
@@ -121,26 +190,35 @@ function rectsIntersect(
|
|
|
121
190
|
}
|
|
122
191
|
|
|
123
192
|
/**
|
|
124
|
-
* Restrict a family to the target plus siblings
|
|
125
|
-
* the target's rect. The target is always retained
|
|
126
|
-
*
|
|
127
|
-
*
|
|
193
|
+
* Restrict a family to the target plus siblings that are VISIBLE and whose
|
|
194
|
+
* bounding rect overlaps the target's rect. The target is always retained
|
|
195
|
+
* (even when itself hidden at the current frame — it is the user's explicit
|
|
196
|
+
* selection). If the target's rect is unavailable or empty (headless /
|
|
197
|
+
* happy-dom returns 0×0), the overlap filter is skipped and all VISIBLE
|
|
198
|
+
* entries are kept — matching the prior rect-fallback behavior.
|
|
128
199
|
*/
|
|
129
|
-
function getOverlappingFamily(
|
|
200
|
+
function getOverlappingFamily(
|
|
201
|
+
target: HTMLElement,
|
|
202
|
+
entries: RenderEntry[],
|
|
203
|
+
isVisible: (el: HTMLElement) => boolean,
|
|
204
|
+
): RenderEntry[] {
|
|
205
|
+
const visibleEntries = entries.filter(
|
|
206
|
+
(entry) => entry.element === target || isVisible(entry.element),
|
|
207
|
+
);
|
|
130
208
|
let targetRect: DOMRect;
|
|
131
209
|
try {
|
|
132
210
|
targetRect = target.getBoundingClientRect();
|
|
133
211
|
} catch {
|
|
134
|
-
return
|
|
212
|
+
return visibleEntries;
|
|
135
213
|
}
|
|
136
|
-
if (targetRect.width === 0 && targetRect.height === 0) return
|
|
214
|
+
if (targetRect.width === 0 && targetRect.height === 0) return visibleEntries;
|
|
137
215
|
const tr = {
|
|
138
216
|
left: targetRect.left,
|
|
139
217
|
top: targetRect.top,
|
|
140
218
|
right: targetRect.right,
|
|
141
219
|
bottom: targetRect.bottom,
|
|
142
220
|
};
|
|
143
|
-
return
|
|
221
|
+
return visibleEntries.filter((entry) => {
|
|
144
222
|
if (entry.element === target) return true;
|
|
145
223
|
try {
|
|
146
224
|
const r = entry.element.getBoundingClientRect();
|
|
@@ -296,28 +374,46 @@ function buildGlobalOrder(
|
|
|
296
374
|
}
|
|
297
375
|
|
|
298
376
|
/**
|
|
299
|
-
*
|
|
300
|
-
*
|
|
301
|
-
*
|
|
302
|
-
* end of its set), otherwise the minimal list of {element, zIndex} changes.
|
|
377
|
+
* The shared scoping pipeline: full painting family for front/back, visible
|
|
378
|
+
* overlapping siblings for forward/backward, sorted into render order with the
|
|
379
|
+
* target's position. Null when the family/scope is too small to act on.
|
|
303
380
|
*/
|
|
304
|
-
|
|
381
|
+
function resolveScopedRenderOrder(
|
|
305
382
|
target: HTMLElement,
|
|
306
383
|
action: ZOrderAction,
|
|
307
|
-
|
|
384
|
+
options?: ZOrderResolveOptions,
|
|
385
|
+
): { entries: RenderEntry[]; order: RenderEntry[]; pos: number } | null {
|
|
308
386
|
const { entries } = getFamily(target);
|
|
309
387
|
// Family always includes the target; fewer than 2 means no siblings at all.
|
|
310
388
|
if (entries.length < 2) return null;
|
|
311
389
|
|
|
390
|
+
const isVisible = options?.isVisible ?? isElementVisibleForZOrder;
|
|
312
391
|
const scoped =
|
|
313
392
|
action === "bring-to-front" || action === "send-to-back"
|
|
314
393
|
? entries
|
|
315
|
-
: getOverlappingFamily(target, entries);
|
|
394
|
+
: getOverlappingFamily(target, entries, isVisible);
|
|
316
395
|
if (scoped.length < 2) return null;
|
|
317
396
|
|
|
318
397
|
const order = toRenderOrder(scoped);
|
|
319
398
|
const pos = order.findIndex((e) => e.element === target);
|
|
320
399
|
if (pos === -1) return null;
|
|
400
|
+
return { entries, order, pos };
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* Resolve the z-order patches for an action.
|
|
405
|
+
*
|
|
406
|
+
* Returns null when the action is a no-op (target already at the relevant
|
|
407
|
+
* end of its set), otherwise the minimal list of {element, zIndex} changes.
|
|
408
|
+
*/
|
|
409
|
+
export function resolveZOrderChange(
|
|
410
|
+
target: HTMLElement,
|
|
411
|
+
action: ZOrderAction,
|
|
412
|
+
options?: ZOrderResolveOptions,
|
|
413
|
+
): ZOrderPatch[] | null {
|
|
414
|
+
const resolved = resolveScopedRenderOrder(target, action, options);
|
|
415
|
+
if (!resolved) return null;
|
|
416
|
+
const { entries, order, pos } = resolved;
|
|
321
417
|
|
|
322
418
|
const desired = [...order];
|
|
323
419
|
const [moved] = desired.splice(pos, 1);
|
|
@@ -343,10 +439,36 @@ export function resolveZOrderChange(
|
|
|
343
439
|
return realizeOrder(order, desired, target, entries);
|
|
344
440
|
}
|
|
345
441
|
|
|
442
|
+
/**
|
|
443
|
+
* The sibling a forward/backward step crosses: the visible overlapping
|
|
444
|
+
* neighbor directly above (bring-forward) or below (send-backward) the target
|
|
445
|
+
* in render order. Null for front/back, for a no-op step, or when the scope is
|
|
446
|
+
* too small. Uses the SAME scoping as resolveZOrderChange, so call it with the
|
|
447
|
+
* same options BEFORE any live styles are applied.
|
|
448
|
+
*/
|
|
449
|
+
export function resolveCrossedNeighbor(
|
|
450
|
+
target: HTMLElement,
|
|
451
|
+
action: ZOrderAction,
|
|
452
|
+
options?: ZOrderResolveOptions,
|
|
453
|
+
): HTMLElement | null {
|
|
454
|
+
if (action !== "bring-forward" && action !== "send-backward") return null;
|
|
455
|
+
const resolved = resolveScopedRenderOrder(target, action, options);
|
|
456
|
+
if (!resolved) return null;
|
|
457
|
+
const { order, pos } = resolved;
|
|
458
|
+
const neighbor = action === "bring-forward" ? order[pos + 1] : order[pos - 1];
|
|
459
|
+
return neighbor?.element ?? null;
|
|
460
|
+
}
|
|
461
|
+
|
|
346
462
|
/**
|
|
347
463
|
* Whether a z-order action is available for the target.
|
|
348
|
-
* "disabled" = the element is already at that limit.
|
|
464
|
+
* "disabled" = the element is already at that limit. Shares the resolver (and
|
|
465
|
+
* its visibility scoping), so enable/disable always matches what the action
|
|
466
|
+
* would actually do.
|
|
349
467
|
*/
|
|
350
|
-
export function isZOrderActionEnabled(
|
|
351
|
-
|
|
468
|
+
export function isZOrderActionEnabled(
|
|
469
|
+
target: HTMLElement,
|
|
470
|
+
action: ZOrderAction,
|
|
471
|
+
options?: ZOrderResolveOptions,
|
|
472
|
+
): boolean {
|
|
473
|
+
return resolveZOrderChange(target, action, options) !== null;
|
|
352
474
|
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
CANVAS_NUDGE_SHIFT_STEP_PX,
|
|
4
|
+
CANVAS_NUDGE_STEP_PX,
|
|
5
|
+
canCanvasNudgeTargets,
|
|
6
|
+
resolveCanvasNudgeDelta,
|
|
7
|
+
} from "./domEditNudge";
|
|
8
|
+
|
|
9
|
+
function mockKeyboardEvent(
|
|
10
|
+
key: string,
|
|
11
|
+
overrides: Partial<Pick<KeyboardEvent, "altKey" | "ctrlKey" | "metaKey" | "shiftKey">> = {},
|
|
12
|
+
): Pick<KeyboardEvent, "altKey" | "ctrlKey" | "metaKey" | "shiftKey" | "key"> {
|
|
13
|
+
return {
|
|
14
|
+
altKey: false,
|
|
15
|
+
ctrlKey: false,
|
|
16
|
+
metaKey: false,
|
|
17
|
+
shiftKey: false,
|
|
18
|
+
key,
|
|
19
|
+
...overrides,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
describe("resolveCanvasNudgeDelta", () => {
|
|
24
|
+
it("maps plain arrows to 1px composition deltas", () => {
|
|
25
|
+
expect(resolveCanvasNudgeDelta(mockKeyboardEvent("ArrowLeft"))).toEqual({
|
|
26
|
+
dx: -CANVAS_NUDGE_STEP_PX,
|
|
27
|
+
dy: 0,
|
|
28
|
+
});
|
|
29
|
+
expect(resolveCanvasNudgeDelta(mockKeyboardEvent("ArrowRight"))).toEqual({
|
|
30
|
+
dx: CANVAS_NUDGE_STEP_PX,
|
|
31
|
+
dy: 0,
|
|
32
|
+
});
|
|
33
|
+
expect(resolveCanvasNudgeDelta(mockKeyboardEvent("ArrowUp"))).toEqual({
|
|
34
|
+
dx: 0,
|
|
35
|
+
dy: -CANVAS_NUDGE_STEP_PX,
|
|
36
|
+
});
|
|
37
|
+
expect(resolveCanvasNudgeDelta(mockKeyboardEvent("ArrowDown"))).toEqual({
|
|
38
|
+
dx: 0,
|
|
39
|
+
dy: CANVAS_NUDGE_STEP_PX,
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("maps Shift+arrow to 10px deltas", () => {
|
|
44
|
+
expect(resolveCanvasNudgeDelta(mockKeyboardEvent("ArrowRight", { shiftKey: true }))).toEqual({
|
|
45
|
+
dx: CANVAS_NUDGE_SHIFT_STEP_PX,
|
|
46
|
+
dy: 0,
|
|
47
|
+
});
|
|
48
|
+
expect(resolveCanvasNudgeDelta(mockKeyboardEvent("ArrowUp", { shiftKey: true }))).toEqual({
|
|
49
|
+
dx: 0,
|
|
50
|
+
dy: -CANVAS_NUDGE_SHIFT_STEP_PX,
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("ignores browser and app shortcut chords", () => {
|
|
55
|
+
expect(resolveCanvasNudgeDelta(mockKeyboardEvent("ArrowLeft", { altKey: true }))).toBeNull();
|
|
56
|
+
expect(resolveCanvasNudgeDelta(mockKeyboardEvent("ArrowLeft", { ctrlKey: true }))).toBeNull();
|
|
57
|
+
expect(resolveCanvasNudgeDelta(mockKeyboardEvent("ArrowLeft", { metaKey: true }))).toBeNull();
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("ignores non-arrow keys", () => {
|
|
61
|
+
expect(resolveCanvasNudgeDelta(mockKeyboardEvent("Escape"))).toBeNull();
|
|
62
|
+
expect(resolveCanvasNudgeDelta(mockKeyboardEvent("a"))).toBeNull();
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
describe("canCanvasNudgeTargets", () => {
|
|
67
|
+
const movable = { capabilities: { canApplyManualOffset: true } };
|
|
68
|
+
const locked = { capabilities: { canApplyManualOffset: false } };
|
|
69
|
+
|
|
70
|
+
it("requires at least one target", () => {
|
|
71
|
+
expect(canCanvasNudgeTargets([])).toBe(false);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("allows only when every target accepts a manual offset", () => {
|
|
75
|
+
expect(canCanvasNudgeTargets([movable])).toBe(true);
|
|
76
|
+
expect(canCanvasNudgeTargets([movable, movable])).toBe(true);
|
|
77
|
+
expect(canCanvasNudgeTargets([locked])).toBe(false);
|
|
78
|
+
expect(canCanvasNudgeTargets([movable, locked])).toBe(false);
|
|
79
|
+
});
|
|
80
|
+
});
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure helpers for the canvas arrow-key nudge (DomEditOverlay).
|
|
3
|
+
*
|
|
4
|
+
* Mirrors captions/keyboard.ts conventions; the two nudge surfaces can't
|
|
5
|
+
* double-fire because PreviewOverlays mounts CaptionOverlay and DomEditOverlay
|
|
6
|
+
* mutually exclusively (caption edit mode returns before the DOM overlay).
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const CANVAS_NUDGE_KEYS = new Set(["ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight"]);
|
|
10
|
+
|
|
11
|
+
export const CANVAS_NUDGE_STEP_PX = 1;
|
|
12
|
+
export const CANVAS_NUDGE_SHIFT_STEP_PX = 10;
|
|
13
|
+
/** One undo entry per key burst: the commit fires after this idle gap. */
|
|
14
|
+
export const CANVAS_NUDGE_COMMIT_DEBOUNCE_MS = 400;
|
|
15
|
+
|
|
16
|
+
type CanvasNudgeKeyEvent = Pick<
|
|
17
|
+
KeyboardEvent,
|
|
18
|
+
"altKey" | "ctrlKey" | "metaKey" | "shiftKey" | "key"
|
|
19
|
+
>;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Arrow key → composition-px delta (Shift = 10). Null when the key is not a
|
|
23
|
+
* plain/Shift arrow, so browser and app shortcut chords pass through.
|
|
24
|
+
*/
|
|
25
|
+
export function resolveCanvasNudgeDelta(
|
|
26
|
+
event: CanvasNudgeKeyEvent,
|
|
27
|
+
): { dx: number; dy: number } | null {
|
|
28
|
+
if (event.metaKey || event.ctrlKey || event.altKey) return null;
|
|
29
|
+
if (!CANVAS_NUDGE_KEYS.has(event.key)) return null;
|
|
30
|
+
const step = event.shiftKey ? CANVAS_NUDGE_SHIFT_STEP_PX : CANVAS_NUDGE_STEP_PX;
|
|
31
|
+
return {
|
|
32
|
+
dx: event.key === "ArrowLeft" ? -step : event.key === "ArrowRight" ? step : 0,
|
|
33
|
+
dy: event.key === "ArrowUp" ? -step : event.key === "ArrowDown" ? step : 0,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface CanvasNudgeTarget {
|
|
38
|
+
capabilities: { canApplyManualOffset: boolean };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** A nudge needs at least one target and every target must accept manual offsets. */
|
|
42
|
+
export function canCanvasNudgeTargets(targets: ReadonlyArray<CanvasNudgeTarget>): boolean {
|
|
43
|
+
return targets.length > 0 && targets.every((t) => t.capabilities.canApplyManualOffset);
|
|
44
|
+
}
|
|
@@ -1,6 +1,37 @@
|
|
|
1
1
|
// @vitest-environment jsdom
|
|
2
2
|
import { describe, expect, it } from "vitest";
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
orientedOverlayRect,
|
|
5
|
+
orientedGroupAwareOverlayRect,
|
|
6
|
+
overlayCornersCentroid,
|
|
7
|
+
selectionCacheKey,
|
|
8
|
+
} from "./domEditOverlayGeometry";
|
|
9
|
+
|
|
10
|
+
describe("overlayCornersCentroid", () => {
|
|
11
|
+
it("averages the four corners (the rendered rotation center)", () => {
|
|
12
|
+
expect(
|
|
13
|
+
overlayCornersCentroid({
|
|
14
|
+
nw: { x: 10, y: 20 },
|
|
15
|
+
ne: { x: 110, y: 20 },
|
|
16
|
+
se: { x: 110, y: 80 },
|
|
17
|
+
sw: { x: 10, y: 80 },
|
|
18
|
+
}),
|
|
19
|
+
).toEqual({ x: 60, y: 50 });
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("is unchanged by rotation — a rotated square's corners average to its center", () => {
|
|
23
|
+
// Unit square centered at (5,5), rotated 45deg about its center: corners land
|
|
24
|
+
// on the axis midpoints, whose average is still the center.
|
|
25
|
+
const c = overlayCornersCentroid({
|
|
26
|
+
nw: { x: 5, y: 5 - Math.SQRT2 / 2 },
|
|
27
|
+
ne: { x: 5 + Math.SQRT2 / 2, y: 5 },
|
|
28
|
+
se: { x: 5, y: 5 + Math.SQRT2 / 2 },
|
|
29
|
+
sw: { x: 5 - Math.SQRT2 / 2, y: 5 },
|
|
30
|
+
});
|
|
31
|
+
expect(c.x).toBeCloseTo(5, 9);
|
|
32
|
+
expect(c.y).toBeCloseTo(5, 9);
|
|
33
|
+
});
|
|
34
|
+
});
|
|
4
35
|
|
|
5
36
|
describe("selectionCacheKey — hfId collision (R7)", () => {
|
|
6
37
|
it("produces distinct keys for two elements that differ only by hfId", () => {
|
|
@@ -11,3 +42,138 @@ describe("selectionCacheKey — hfId collision (R7)", () => {
|
|
|
11
42
|
expect(a).not.toBe(b);
|
|
12
43
|
});
|
|
13
44
|
});
|
|
45
|
+
|
|
46
|
+
describe("orientedOverlayRect — rotation gate (perf fix, V15 18a/18b)", () => {
|
|
47
|
+
// jsdom has no DOMMatrix/DOMPoint; a minimal stand-in is enough to exercise the
|
|
48
|
+
// real (unmocked) orientedOverlayRect, unlike DomEditOverlay.test.ts and
|
|
49
|
+
// anchoredResizeCommitFeedsOffset.test.ts, which mock this module entirely.
|
|
50
|
+
class FakeDOMMatrix {
|
|
51
|
+
a = 1;
|
|
52
|
+
b = 0;
|
|
53
|
+
c = 0;
|
|
54
|
+
d = 1;
|
|
55
|
+
e = 0;
|
|
56
|
+
f = 0;
|
|
57
|
+
constructor(init?: string) {
|
|
58
|
+
const m = init ? /matrix\(([^)]+)\)/.exec(init) : null;
|
|
59
|
+
if (!m) return;
|
|
60
|
+
const parts = m[1]!.split(",").map((s) => Number.parseFloat(s.trim()));
|
|
61
|
+
[this.a, this.b, this.c, this.d, this.e, this.f] = parts as [
|
|
62
|
+
number,
|
|
63
|
+
number,
|
|
64
|
+
number,
|
|
65
|
+
number,
|
|
66
|
+
number,
|
|
67
|
+
number,
|
|
68
|
+
];
|
|
69
|
+
}
|
|
70
|
+
transformPoint(pt: { x: number; y: number }) {
|
|
71
|
+
return {
|
|
72
|
+
x: this.a * pt.x + this.c * pt.y + this.e,
|
|
73
|
+
y: this.b * pt.x + this.d * pt.y + this.f,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
class FakeDOMPoint {
|
|
78
|
+
constructor(
|
|
79
|
+
public x: number,
|
|
80
|
+
public y: number,
|
|
81
|
+
) {}
|
|
82
|
+
}
|
|
83
|
+
// matrix() form of rotate(30deg), so the module's `new DOMMatrix(cs.transform)`
|
|
84
|
+
// parse (which expects "matrix(...)", not "rotate(...)") resolves correctly.
|
|
85
|
+
const ROTATE_30DEG_MATRIX =
|
|
86
|
+
"matrix(0.8660254037844387, 0.49999999999999994, -0.49999999999999994, 0.8660254037844387, 0, 0)";
|
|
87
|
+
|
|
88
|
+
function stubRect(
|
|
89
|
+
el: Element,
|
|
90
|
+
rect: { left: number; top: number; width: number; height: number },
|
|
91
|
+
) {
|
|
92
|
+
(el as unknown as { getBoundingClientRect: () => DOMRect }).getBoundingClientRect = () =>
|
|
93
|
+
({
|
|
94
|
+
left: rect.left,
|
|
95
|
+
top: rect.top,
|
|
96
|
+
width: rect.width,
|
|
97
|
+
height: rect.height,
|
|
98
|
+
right: rect.left + rect.width,
|
|
99
|
+
bottom: rect.top + rect.height,
|
|
100
|
+
x: rect.left,
|
|
101
|
+
y: rect.top,
|
|
102
|
+
toJSON() {
|
|
103
|
+
return this;
|
|
104
|
+
},
|
|
105
|
+
}) as DOMRect;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function buildHarness() {
|
|
109
|
+
const overlayEl = document.createElement("div");
|
|
110
|
+
document.body.appendChild(overlayEl);
|
|
111
|
+
stubRect(overlayEl, { left: 0, top: 0, width: 1000, height: 1000 });
|
|
112
|
+
|
|
113
|
+
const iframe = document.createElement("iframe");
|
|
114
|
+
document.body.appendChild(iframe);
|
|
115
|
+
stubRect(iframe, { left: 0, top: 0, width: 1000, height: 1000 });
|
|
116
|
+
|
|
117
|
+
const doc = iframe.contentDocument!;
|
|
118
|
+
const root = doc.createElement("div");
|
|
119
|
+
root.setAttribute("data-composition-id", "root");
|
|
120
|
+
root.setAttribute("data-width", "1000");
|
|
121
|
+
root.setAttribute("data-height", "1000");
|
|
122
|
+
doc.body.appendChild(root);
|
|
123
|
+
|
|
124
|
+
const el = doc.createElement("div");
|
|
125
|
+
root.appendChild(el);
|
|
126
|
+
stubRect(el, { left: 400, top: 450, width: 200, height: 100 });
|
|
127
|
+
Object.defineProperty(el, "offsetWidth", { value: 200, configurable: true });
|
|
128
|
+
Object.defineProperty(el, "offsetHeight", { value: 100, configurable: true });
|
|
129
|
+
|
|
130
|
+
const win = iframe.contentWindow as unknown as Window & {
|
|
131
|
+
DOMMatrix: unknown;
|
|
132
|
+
DOMPoint: unknown;
|
|
133
|
+
};
|
|
134
|
+
win.DOMMatrix = FakeDOMMatrix;
|
|
135
|
+
win.DOMPoint = FakeDOMPoint;
|
|
136
|
+
|
|
137
|
+
return { overlayEl, iframe, el };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
it("unrotated element takes the cheap AABB path — matches the raw bounding rect, angle 0", () => {
|
|
141
|
+
const { overlayEl, iframe, el } = buildHarness();
|
|
142
|
+
const rect = orientedOverlayRect(overlayEl, iframe, el);
|
|
143
|
+
expect(rect).not.toBeNull();
|
|
144
|
+
expect(rect!.left).toBeCloseTo(400, 5);
|
|
145
|
+
expect(rect!.top).toBeCloseTo(450, 5);
|
|
146
|
+
expect(rect!.width).toBeCloseTo(200, 5);
|
|
147
|
+
expect(rect!.height).toBeCloseTo(100, 5);
|
|
148
|
+
expect(rect!.angle ?? 0).toBe(0);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it("rotated element takes the corner-geometry path — reports the live angle", () => {
|
|
152
|
+
const { overlayEl, iframe, el } = buildHarness();
|
|
153
|
+
el.style.transform = ROTATE_30DEG_MATRIX;
|
|
154
|
+
const rect = orientedOverlayRect(overlayEl, iframe, el);
|
|
155
|
+
expect(rect).not.toBeNull();
|
|
156
|
+
expect(rect!.angle).toBeCloseTo(30, 3);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it("preserves an ordinary element's rotation through the group-aware entry point", () => {
|
|
160
|
+
const { overlayEl, iframe, el } = buildHarness();
|
|
161
|
+
el.style.transform = ROTATE_30DEG_MATRIX;
|
|
162
|
+
const rect = orientedGroupAwareOverlayRect(overlayEl, iframe, el);
|
|
163
|
+
expect(rect!.angle).toBeCloseTo(30, 3);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it("gate re-evaluates every call — editing an element to rotated mid-session flips the path immediately", () => {
|
|
167
|
+
const { overlayEl, iframe, el } = buildHarness();
|
|
168
|
+
const before = orientedOverlayRect(overlayEl, iframe, el);
|
|
169
|
+
expect(before?.angle ?? 0).toBe(0);
|
|
170
|
+
|
|
171
|
+
el.style.transform = ROTATE_30DEG_MATRIX;
|
|
172
|
+
const after = orientedOverlayRect(overlayEl, iframe, el);
|
|
173
|
+
expect(after!.angle).toBeCloseTo(30, 3);
|
|
174
|
+
|
|
175
|
+
el.style.transform = "";
|
|
176
|
+
const restored = orientedOverlayRect(overlayEl, iframe, el);
|
|
177
|
+
expect(restored?.angle ?? 0).toBe(0);
|
|
178
|
+
});
|
|
179
|
+
});
|