@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,257 @@
|
|
|
1
|
+
import type { RefObject } from "react";
|
|
2
|
+
import { type DomEditSelection } from "./domEditing";
|
|
3
|
+
import type { GroupOverlayItem, OverlayRect } from "./domEditOverlayGeometry";
|
|
4
|
+
import type { BlockedMoveState, ResizeHandle } from "./domEditOverlayGestures";
|
|
5
|
+
import type { createDomEditOverlayGestureHandlers } from "./useDomEditOverlayGestures";
|
|
6
|
+
import { DomEditCropHandles } from "./DomEditCropHandles";
|
|
7
|
+
import { DomEditRotateHandle } from "./DomEditRotateHandle";
|
|
8
|
+
import { resolveRotatedResizeCursor } from "./domEditResizeLocal";
|
|
9
|
+
|
|
10
|
+
// Corner resize handles, Canva-style: one per corner, diagonal cursors.
|
|
11
|
+
// Corners scale about the element center; the translate keeps the center
|
|
12
|
+
// planted, so they need the manual-offset capability in addition to manual-size.
|
|
13
|
+
const RESIZE_HANDLE_DEFS: Array<{
|
|
14
|
+
handle: ResizeHandle;
|
|
15
|
+
cursor: string;
|
|
16
|
+
x: "left" | "right";
|
|
17
|
+
y: "top" | "bottom";
|
|
18
|
+
}> = [
|
|
19
|
+
{ handle: "nw", cursor: "nwse-resize", x: "left", y: "top" },
|
|
20
|
+
{ handle: "ne", cursor: "nesw-resize", x: "right", y: "top" },
|
|
21
|
+
{ handle: "sw", cursor: "nesw-resize", x: "left", y: "bottom" },
|
|
22
|
+
{ handle: "se", cursor: "nwse-resize", x: "right", y: "bottom" },
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
// Visible dot is 9px; the pointer target is a 16px invisible square centered
|
|
26
|
+
// on the corner so click targets don't shrink with the smaller dot.
|
|
27
|
+
const RESIZE_HANDLE_HIT_PX = 16;
|
|
28
|
+
|
|
29
|
+
type CropInset = { top: number; right: number; bottom: number; left: number };
|
|
30
|
+
const NO_CROP_INSET: CropInset = { top: 0, right: 0, bottom: 0, left: 0 };
|
|
31
|
+
|
|
32
|
+
function resizeHandleStyle(
|
|
33
|
+
def: (typeof RESIZE_HANDLE_DEFS)[number],
|
|
34
|
+
overlayRect: { left: number; top: number; width: number; height: number },
|
|
35
|
+
cropInset?: CropInset,
|
|
36
|
+
): React.CSSProperties {
|
|
37
|
+
const half = RESIZE_HANDLE_HIT_PX / 2;
|
|
38
|
+
const inset = cropInset ?? NO_CROP_INSET;
|
|
39
|
+
const style: React.CSSProperties = { cursor: def.cursor, touchAction: "none" };
|
|
40
|
+
// Position relative to the overlay container (not the selection box).
|
|
41
|
+
// This ensures the dots render as siblings of the box border div — strictly
|
|
42
|
+
// above it — rather than as children where the parent border can visually
|
|
43
|
+
// overlap the dot circle at the corner.
|
|
44
|
+
style.left =
|
|
45
|
+
def.x === "left"
|
|
46
|
+
? overlayRect.left + inset.left - half
|
|
47
|
+
: overlayRect.left + overlayRect.width - inset.right - half;
|
|
48
|
+
style.top =
|
|
49
|
+
def.y === "top"
|
|
50
|
+
? overlayRect.top + inset.top - half
|
|
51
|
+
: overlayRect.top + overlayRect.height - inset.bottom - half;
|
|
52
|
+
return style;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
type GestureHandlers = ReturnType<typeof createDomEditOverlayGestureHandlers>;
|
|
56
|
+
|
|
57
|
+
interface DomEditGroupChromeProps {
|
|
58
|
+
groupOverlayItems: GroupOverlayItem[];
|
|
59
|
+
groupBounds: OverlayRect;
|
|
60
|
+
allowCanvasMovement: boolean;
|
|
61
|
+
groupCanMove: boolean;
|
|
62
|
+
gestures: GestureHandlers;
|
|
63
|
+
onBoxMouseDown: (e: React.MouseEvent) => void;
|
|
64
|
+
onBoxClick: (event: React.MouseEvent<HTMLDivElement>) => void;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Multi-selection chrome: per-member outlines plus a single draggable bounding
|
|
68
|
+
// box spanning the union of the members.
|
|
69
|
+
export function DomEditGroupChrome({
|
|
70
|
+
groupOverlayItems,
|
|
71
|
+
groupBounds,
|
|
72
|
+
allowCanvasMovement,
|
|
73
|
+
groupCanMove,
|
|
74
|
+
gestures,
|
|
75
|
+
onBoxMouseDown,
|
|
76
|
+
onBoxClick,
|
|
77
|
+
}: DomEditGroupChromeProps) {
|
|
78
|
+
return (
|
|
79
|
+
<>
|
|
80
|
+
{groupOverlayItems.map((item) => (
|
|
81
|
+
<div
|
|
82
|
+
key={item.key}
|
|
83
|
+
aria-hidden="true"
|
|
84
|
+
className="pointer-events-none absolute rounded-xl border border-studio-accent/70"
|
|
85
|
+
style={{
|
|
86
|
+
left: item.rect.left,
|
|
87
|
+
top: item.rect.top,
|
|
88
|
+
width: item.rect.width,
|
|
89
|
+
height: item.rect.height,
|
|
90
|
+
}}
|
|
91
|
+
/>
|
|
92
|
+
))}
|
|
93
|
+
<div
|
|
94
|
+
data-dom-edit-selection-box="true"
|
|
95
|
+
className="pointer-events-auto absolute rounded-xl border border-studio-accent shadow-[0_0_0_1px_rgba(60,230,172,0.3)]"
|
|
96
|
+
style={{
|
|
97
|
+
left: groupBounds.left,
|
|
98
|
+
top: groupBounds.top,
|
|
99
|
+
width: groupBounds.width,
|
|
100
|
+
height: groupBounds.height,
|
|
101
|
+
cursor: allowCanvasMovement && groupCanMove ? "move" : "default",
|
|
102
|
+
}}
|
|
103
|
+
onPointerDown={(e) => {
|
|
104
|
+
if (!allowCanvasMovement || !groupCanMove || e.shiftKey) return;
|
|
105
|
+
gestures.startGroupDrag(e);
|
|
106
|
+
}}
|
|
107
|
+
onMouseDown={onBoxMouseDown}
|
|
108
|
+
onClick={onBoxClick}
|
|
109
|
+
/>
|
|
110
|
+
</>
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
interface DomEditSelectionChromeProps {
|
|
115
|
+
selection: DomEditSelection;
|
|
116
|
+
overlayRect: OverlayRect;
|
|
117
|
+
allowCanvasMovement: boolean;
|
|
118
|
+
cropOutlineInsetPx?: { top: number; right: number; bottom: number; left: number };
|
|
119
|
+
boxRef: RefObject<HTMLDivElement | null>;
|
|
120
|
+
boxChromeClass: string;
|
|
121
|
+
boxClipPath: string | undefined;
|
|
122
|
+
selectionKey: string;
|
|
123
|
+
groupSelectionCount: number;
|
|
124
|
+
blockedMoveRef: RefObject<BlockedMoveState | null>;
|
|
125
|
+
gestures: GestureHandlers;
|
|
126
|
+
onStyleCommit?: (property: string, value: string) => Promise<void> | void;
|
|
127
|
+
onBoxMouseDown: (e: React.MouseEvent) => void;
|
|
128
|
+
onBoxClick: (event: React.MouseEvent<HTMLDivElement>) => void;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Oriented selection chrome: a rotation wrapper spanning the overlay, rotated by
|
|
132
|
+
// the element's live angle about the selection box CENTER. Its children (border
|
|
133
|
+
// box, corner dots, and rotate handle) keep their existing
|
|
134
|
+
// overlay-absolute positions — rotating the whole plane about the box center
|
|
135
|
+
// lands them on the element's real transformed corners for free. At angle 0 the
|
|
136
|
+
// transform is a no-op, so the chrome is pixel-identical.
|
|
137
|
+
export function DomEditSelectionChrome({
|
|
138
|
+
selection,
|
|
139
|
+
overlayRect,
|
|
140
|
+
allowCanvasMovement,
|
|
141
|
+
cropOutlineInsetPx,
|
|
142
|
+
boxRef,
|
|
143
|
+
boxChromeClass,
|
|
144
|
+
boxClipPath,
|
|
145
|
+
selectionKey,
|
|
146
|
+
groupSelectionCount,
|
|
147
|
+
blockedMoveRef,
|
|
148
|
+
gestures,
|
|
149
|
+
onStyleCommit,
|
|
150
|
+
onBoxMouseDown,
|
|
151
|
+
onBoxClick,
|
|
152
|
+
}: DomEditSelectionChromeProps) {
|
|
153
|
+
return (
|
|
154
|
+
<>
|
|
155
|
+
<div
|
|
156
|
+
className="pointer-events-none absolute inset-0"
|
|
157
|
+
style={{
|
|
158
|
+
transformOrigin: `${overlayRect.left + overlayRect.width / 2}px ${overlayRect.top + overlayRect.height / 2}px`,
|
|
159
|
+
transform: overlayRect.angle ? `rotate(${overlayRect.angle}deg)` : undefined,
|
|
160
|
+
}}
|
|
161
|
+
>
|
|
162
|
+
{allowCanvasMovement && selection.capabilities.canApplyManualRotation && (
|
|
163
|
+
<DomEditRotateHandle
|
|
164
|
+
overlayRect={overlayRect}
|
|
165
|
+
cropOutlineInsetPx={cropOutlineInsetPx}
|
|
166
|
+
onStartRotate={(e) => {
|
|
167
|
+
e.stopPropagation();
|
|
168
|
+
gestures.startGesture("rotate", e);
|
|
169
|
+
}}
|
|
170
|
+
/>
|
|
171
|
+
)}
|
|
172
|
+
<div
|
|
173
|
+
key={selectionKey}
|
|
174
|
+
ref={boxRef}
|
|
175
|
+
data-dom-edit-selection-box="true"
|
|
176
|
+
className={`pointer-events-auto absolute rounded-md ${boxChromeClass}`}
|
|
177
|
+
style={{
|
|
178
|
+
left: overlayRect.left,
|
|
179
|
+
top: overlayRect.top,
|
|
180
|
+
width: overlayRect.width,
|
|
181
|
+
height: overlayRect.height,
|
|
182
|
+
clipPath: boxClipPath,
|
|
183
|
+
cursor:
|
|
184
|
+
allowCanvasMovement && selection.capabilities.canApplyManualOffset
|
|
185
|
+
? "move"
|
|
186
|
+
: "default",
|
|
187
|
+
}}
|
|
188
|
+
onPointerDown={(e) => {
|
|
189
|
+
if (!allowCanvasMovement || e.shiftKey) return;
|
|
190
|
+
if (selection.capabilities.canApplyManualOffset) {
|
|
191
|
+
gestures.startGesture("drag", e);
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
e.preventDefault();
|
|
195
|
+
e.stopPropagation();
|
|
196
|
+
e.currentTarget.setPointerCapture(e.pointerId);
|
|
197
|
+
blockedMoveRef.current = {
|
|
198
|
+
pointerId: e.pointerId,
|
|
199
|
+
startX: e.clientX,
|
|
200
|
+
startY: e.clientY,
|
|
201
|
+
notified: false,
|
|
202
|
+
};
|
|
203
|
+
}}
|
|
204
|
+
onMouseDown={onBoxMouseDown}
|
|
205
|
+
onClick={onBoxClick}
|
|
206
|
+
>
|
|
207
|
+
{cropOutlineInsetPx && (
|
|
208
|
+
<div
|
|
209
|
+
className="pointer-events-none absolute rounded-md border border-studio-accent/80 shadow-[0_0_0_1px_rgba(60,230,172,0.25)]"
|
|
210
|
+
style={{
|
|
211
|
+
left: cropOutlineInsetPx.left,
|
|
212
|
+
top: cropOutlineInsetPx.top,
|
|
213
|
+
right: cropOutlineInsetPx.right,
|
|
214
|
+
bottom: cropOutlineInsetPx.bottom,
|
|
215
|
+
}}
|
|
216
|
+
/>
|
|
217
|
+
)}
|
|
218
|
+
</div>
|
|
219
|
+
{/* Resize-handle dots rendered as siblings of the selection box, not
|
|
220
|
+
children, so they paint strictly above the box border. Each handle
|
|
221
|
+
is positioned relative to the overlay container using the
|
|
222
|
+
overlayRect origin, matching the old child-relative offsets. */}
|
|
223
|
+
{allowCanvasMovement &&
|
|
224
|
+
selection.capabilities.canApplyManualSize &&
|
|
225
|
+
RESIZE_HANDLE_DEFS.map((def) =>
|
|
226
|
+
def.handle !== "se" && !selection.capabilities.canApplyManualOffset ? null : (
|
|
227
|
+
<div
|
|
228
|
+
key={def.handle}
|
|
229
|
+
className="pointer-events-auto absolute flex h-4 w-4 items-center justify-center"
|
|
230
|
+
style={{
|
|
231
|
+
...resizeHandleStyle(def, overlayRect, cropOutlineInsetPx ?? undefined),
|
|
232
|
+
// Cursor rotates with the object: bucket the corner's base
|
|
233
|
+
// diagonal + element rotation into the 8 CSS resize cursors.
|
|
234
|
+
cursor: resolveRotatedResizeCursor(def.handle, overlayRect.angle ?? 0),
|
|
235
|
+
}}
|
|
236
|
+
onPointerDown={(e) => {
|
|
237
|
+
e.stopPropagation();
|
|
238
|
+
gestures.startGesture("resize", e, { resizeHandle: def.handle });
|
|
239
|
+
}}
|
|
240
|
+
>
|
|
241
|
+
<div className="pointer-events-none h-[12px] w-[12px] rounded-full border-[1.5px] border-studio-accent bg-white shadow-[0_0_3px_rgba(0,0,0,0.45)]" />
|
|
242
|
+
</div>
|
|
243
|
+
),
|
|
244
|
+
)}
|
|
245
|
+
</div>
|
|
246
|
+
{/* Crop owns its element-local oriented frame. Keep it outside the chrome's
|
|
247
|
+
rotated plane or a rotated selection applies the angle twice. */}
|
|
248
|
+
{selection.capabilities.canCrop && groupSelectionCount <= 1 && (
|
|
249
|
+
<DomEditCropHandles
|
|
250
|
+
selection={selection}
|
|
251
|
+
overlayRect={overlayRect}
|
|
252
|
+
onStyleCommit={onStyleCommit}
|
|
253
|
+
/>
|
|
254
|
+
)}
|
|
255
|
+
</>
|
|
256
|
+
);
|
|
257
|
+
}
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
// @vitest-environment happy-dom
|
|
2
2
|
|
|
3
|
-
import { describe, expect, it } from "vitest";
|
|
3
|
+
import { describe, expect, it, vi, beforeEach, afterEach } from "vitest";
|
|
4
4
|
import { Window } from "happy-dom";
|
|
5
5
|
import type { DomEditLayerItem } from "./domEditingTypes";
|
|
6
|
-
import { sortLayersByZIndex } from "./LayersPanel";
|
|
6
|
+
import { createRafThrottle, sortLayersByZIndex } from "./LayersPanel";
|
|
7
7
|
import { isLayerDraggable } from "./useLayerDrag";
|
|
8
|
+
import { liveTime } from "../../player";
|
|
8
9
|
|
|
9
10
|
function makeLayer(
|
|
10
11
|
overrides: Partial<DomEditLayerItem> & { zIndex?: string; locked?: boolean },
|
|
@@ -133,3 +134,66 @@ describe("isLayerDraggable", () => {
|
|
|
133
134
|
expect(isLayerDraggable(layer)).toBe(true);
|
|
134
135
|
});
|
|
135
136
|
});
|
|
137
|
+
|
|
138
|
+
// ── liveTime throttle contract (mirrors the useEffect in LayersPanel) ──────
|
|
139
|
+
// The panel subscribes to liveTime with a rAF + 100 ms trailing throttle so
|
|
140
|
+
// it refreshes during scrubbing without a collectLayers call every frame.
|
|
141
|
+
// These tests exercise the subscribe/unsubscribe contract that the effect
|
|
142
|
+
// relies on.
|
|
143
|
+
|
|
144
|
+
describe("liveTime subscribe / unsubscribe (LayersPanel scrub contract)", () => {
|
|
145
|
+
let rafCallbacks: FrameRequestCallback[];
|
|
146
|
+
let originalRaf: typeof requestAnimationFrame;
|
|
147
|
+
let originalCancelRaf: typeof cancelAnimationFrame;
|
|
148
|
+
|
|
149
|
+
beforeEach(() => {
|
|
150
|
+
rafCallbacks = [];
|
|
151
|
+
originalRaf = globalThis.requestAnimationFrame;
|
|
152
|
+
originalCancelRaf = globalThis.cancelAnimationFrame;
|
|
153
|
+
let nextId = 1;
|
|
154
|
+
globalThis.requestAnimationFrame = (cb) => {
|
|
155
|
+
const id = nextId++;
|
|
156
|
+
rafCallbacks.push(cb);
|
|
157
|
+
return id;
|
|
158
|
+
};
|
|
159
|
+
globalThis.cancelAnimationFrame = vi.fn();
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
afterEach(() => {
|
|
163
|
+
globalThis.requestAnimationFrame = originalRaf;
|
|
164
|
+
globalThis.cancelAnimationFrame = originalCancelRaf;
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it("unsubscribing stops the callback from receiving further notifications", () => {
|
|
168
|
+
const cb = vi.fn();
|
|
169
|
+
const unsubscribe = liveTime.subscribe(cb);
|
|
170
|
+
|
|
171
|
+
liveTime.notify(1);
|
|
172
|
+
expect(cb).toHaveBeenCalledTimes(1);
|
|
173
|
+
|
|
174
|
+
unsubscribe();
|
|
175
|
+
liveTime.notify(2);
|
|
176
|
+
expect(cb).toHaveBeenCalledTimes(1); // no new call after unsubscribe
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it("queuing a rAF on liveTime notify then flushing calls the refresh exactly once", () => {
|
|
180
|
+
const refresh = vi.fn();
|
|
181
|
+
const throttle = createRafThrottle(refresh, 100);
|
|
182
|
+
const unsubscribe = liveTime.subscribe(throttle.invoke);
|
|
183
|
+
|
|
184
|
+
// First notify enqueues one rAF
|
|
185
|
+
liveTime.notify(0.1);
|
|
186
|
+
expect(rafCallbacks).toHaveLength(1);
|
|
187
|
+
expect(refresh).not.toHaveBeenCalled();
|
|
188
|
+
|
|
189
|
+
// Second notify before rAF flush is ignored (rafId is set)
|
|
190
|
+
liveTime.notify(0.2);
|
|
191
|
+
expect(rafCallbacks).toHaveLength(1);
|
|
192
|
+
|
|
193
|
+
// Flush the rAF
|
|
194
|
+
rafCallbacks[0](performance.now());
|
|
195
|
+
expect(refresh).toHaveBeenCalledTimes(1);
|
|
196
|
+
|
|
197
|
+
unsubscribe();
|
|
198
|
+
});
|
|
199
|
+
});
|
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
} from "./domEditing";
|
|
8
8
|
import { useStudioPlaybackContext, useStudioShellContext } from "../../contexts/StudioContext";
|
|
9
9
|
import { useDomEditContext } from "../../contexts/DomEditContext";
|
|
10
|
-
import { usePlayerStore } from "../../player";
|
|
10
|
+
import { usePlayerStore, liveTime } from "../../player";
|
|
11
11
|
import {
|
|
12
12
|
findMatchingTimelineElementId,
|
|
13
13
|
resolveTimelineSelectionSeekTime,
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
import { Layers } from "../../icons/SystemIcons";
|
|
16
16
|
import { useLayerDrag, isLayerDraggable, type LayerReorderEvent } from "./useLayerDrag";
|
|
17
17
|
import { computeReorderZValues, getElementZIndex } from "../../player/lib/layerOrdering";
|
|
18
|
+
import { deriveTimelineStoreKey } from "../../player/lib/timelineElementHelpers";
|
|
18
19
|
|
|
19
20
|
const TAG_ICONS: Record<string, string> = {
|
|
20
21
|
video: "Vi",
|
|
@@ -49,6 +50,34 @@ function isCompositionHost(el: HTMLElement): boolean {
|
|
|
49
50
|
return el.hasAttribute("data-composition-src") || el.hasAttribute("data-composition-file");
|
|
50
51
|
}
|
|
51
52
|
|
|
53
|
+
/**
|
|
54
|
+
* A trailing-rAF + cooldown throttle: `invoke` runs `run` at most once per
|
|
55
|
+
* animation frame and no more often than `throttleMs`. `cancel` clears any
|
|
56
|
+
* pending frame (call on cleanup). Extracted so the throttle can be exercised
|
|
57
|
+
* directly in tests instead of being reconstructed there.
|
|
58
|
+
*/
|
|
59
|
+
export function createRafThrottle(
|
|
60
|
+
run: () => void,
|
|
61
|
+
throttleMs = 100,
|
|
62
|
+
): { invoke: () => void; cancel: () => void } {
|
|
63
|
+
let rafId: number | null = null;
|
|
64
|
+
let lastFired = 0;
|
|
65
|
+
return {
|
|
66
|
+
invoke: () => {
|
|
67
|
+
const now = performance.now();
|
|
68
|
+
if (rafId !== null || now - lastFired < throttleMs) return;
|
|
69
|
+
rafId = requestAnimationFrame(() => {
|
|
70
|
+
rafId = null;
|
|
71
|
+
lastFired = performance.now();
|
|
72
|
+
run();
|
|
73
|
+
});
|
|
74
|
+
},
|
|
75
|
+
cancel: () => {
|
|
76
|
+
if (rafId !== null) cancelAnimationFrame(rafId);
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
52
81
|
interface CollapsedState {
|
|
53
82
|
[key: string]: boolean;
|
|
54
83
|
}
|
|
@@ -122,6 +151,20 @@ export const LayersPanel = memo(function LayersPanel() {
|
|
|
122
151
|
}
|
|
123
152
|
}, [compositionLoading, collectLayers]);
|
|
124
153
|
|
|
154
|
+
// Subscribe to liveTime so the panel refreshes during scrubbing.
|
|
155
|
+
// liveTime bypasses React state (no re-renders per frame), so a plain
|
|
156
|
+
// usePlayerStore(s => s.currentTime) subscription never fires while the
|
|
157
|
+
// RAF loop is running. Throttle with a trailing rAF + 100 ms cooldown to
|
|
158
|
+
// avoid a collectLayers call on every animation frame.
|
|
159
|
+
useEffect(() => {
|
|
160
|
+
const throttle = createRafThrottle(collectLayers, 100);
|
|
161
|
+
const unsubscribe = liveTime.subscribe(throttle.invoke);
|
|
162
|
+
return () => {
|
|
163
|
+
unsubscribe();
|
|
164
|
+
throttle.cancel();
|
|
165
|
+
};
|
|
166
|
+
}, [collectLayers]);
|
|
167
|
+
|
|
125
168
|
const resolveSelection = useCallback(
|
|
126
169
|
(layer: DomEditLayerItem) => {
|
|
127
170
|
// Re-find the element from the live DOM — layer.element may be stale
|
|
@@ -238,9 +281,17 @@ export const LayersPanel = memo(function LayersPanel() {
|
|
|
238
281
|
selector: layer.selector,
|
|
239
282
|
selectorIndex: layer.selectorIndex,
|
|
240
283
|
sourceFile: layer.sourceFile,
|
|
284
|
+
key: deriveTimelineStoreKey({
|
|
285
|
+
domId: layer.id,
|
|
286
|
+
selector: layer.selector,
|
|
287
|
+
selectorIndex: layer.selectorIndex,
|
|
288
|
+
sourceFile: layer.sourceFile,
|
|
289
|
+
}),
|
|
241
290
|
}));
|
|
242
291
|
|
|
243
|
-
|
|
292
|
+
// "layer-drag" keeps consecutive drops of the same sibling set coalescing
|
|
293
|
+
// into one undo step, without merging with a context-menu z action.
|
|
294
|
+
handleDomZIndexReorderCommit(entries, undefined, "layer-drag");
|
|
244
295
|
},
|
|
245
296
|
[handleDomZIndexReorderCommit],
|
|
246
297
|
);
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
|
|
3
|
+
import React, { act } from "react";
|
|
4
|
+
import { createRoot } from "react-dom/client";
|
|
5
|
+
import { expect, it } from "vitest";
|
|
6
|
+
import { OffCanvasIndicators } from "./OffCanvasIndicators";
|
|
7
|
+
|
|
8
|
+
(globalThis as unknown as { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
|
|
9
|
+
|
|
10
|
+
it("rotates an off-canvas indicator with its element", () => {
|
|
11
|
+
const host = document.createElement("div");
|
|
12
|
+
document.body.append(host);
|
|
13
|
+
const root = createRoot(host);
|
|
14
|
+
act(() => {
|
|
15
|
+
root.render(
|
|
16
|
+
<OffCanvasIndicators
|
|
17
|
+
rects={[{ key: "card", left: 80, top: 20, width: 40, height: 20, angle: 30 }]}
|
|
18
|
+
elements={{ current: new Map() }}
|
|
19
|
+
compRect={{ left: 0, top: 0, width: 100, height: 100 }}
|
|
20
|
+
selection={null}
|
|
21
|
+
groupSelections={[]}
|
|
22
|
+
activeCompositionPathRef={{ current: null }}
|
|
23
|
+
onSelectionChangeRef={{ current: () => undefined }}
|
|
24
|
+
/>,
|
|
25
|
+
);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
const indicator = host.querySelector<HTMLElement>('[role="button"]');
|
|
29
|
+
expect(indicator?.parentElement?.style.transform).toBe("rotate(30deg)");
|
|
30
|
+
expect(indicator?.parentElement?.style.transformOrigin).toBe("center");
|
|
31
|
+
|
|
32
|
+
act(() => root.unmount());
|
|
33
|
+
host.remove();
|
|
34
|
+
});
|
|
@@ -7,6 +7,7 @@ export interface OffCanvasRect {
|
|
|
7
7
|
top: number;
|
|
8
8
|
width: number;
|
|
9
9
|
height: number;
|
|
10
|
+
angle?: number;
|
|
10
11
|
}
|
|
11
12
|
|
|
12
13
|
interface OffCanvasIndicatorsProps {
|
|
@@ -21,6 +22,44 @@ interface OffCanvasIndicatorsProps {
|
|
|
21
22
|
>;
|
|
22
23
|
}
|
|
23
24
|
|
|
25
|
+
function clipOutsideCanvas(
|
|
26
|
+
rect: OffCanvasRect,
|
|
27
|
+
compRect: { left: number; top: number; width: number; height: number },
|
|
28
|
+
): string | undefined {
|
|
29
|
+
const angle = rect.angle ?? 0;
|
|
30
|
+
if (!angle) {
|
|
31
|
+
const left = Math.max(0, compRect.left - rect.left);
|
|
32
|
+
const top = Math.max(0, compRect.top - rect.top);
|
|
33
|
+
const right = Math.min(rect.width, compRect.left + compRect.width - rect.left);
|
|
34
|
+
const bottom = Math.min(rect.height, compRect.top + compRect.height - rect.top);
|
|
35
|
+
if (left >= right || top >= bottom) return undefined;
|
|
36
|
+
return `polygon(evenodd, 0 0, ${rect.width}px 0, ${rect.width}px ${rect.height}px, 0 ${rect.height}px, 0 0, ${left}px ${top}px, ${right}px ${top}px, ${right}px ${bottom}px, ${left}px ${bottom}px, ${left}px ${top}px)`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const radians = (-angle * Math.PI) / 180;
|
|
40
|
+
const cos = Math.cos(radians);
|
|
41
|
+
const sin = Math.sin(radians);
|
|
42
|
+
const centerX = rect.left + rect.width / 2;
|
|
43
|
+
const centerY = rect.top + rect.height / 2;
|
|
44
|
+
const toLocal = (x: number, y: number) => {
|
|
45
|
+
const dx = x - centerX;
|
|
46
|
+
const dy = y - centerY;
|
|
47
|
+
return `${rect.width / 2 + dx * cos - dy * sin}px ${rect.height / 2 + dx * sin + dy * cos}px`;
|
|
48
|
+
};
|
|
49
|
+
const left = compRect.left;
|
|
50
|
+
const top = compRect.top;
|
|
51
|
+
const right = left + compRect.width;
|
|
52
|
+
const bottom = top + compRect.height;
|
|
53
|
+
const canvas = [
|
|
54
|
+
toLocal(left, top),
|
|
55
|
+
toLocal(right, top),
|
|
56
|
+
toLocal(right, bottom),
|
|
57
|
+
toLocal(left, bottom),
|
|
58
|
+
toLocal(left, top),
|
|
59
|
+
].join(", ");
|
|
60
|
+
return `polygon(evenodd, 0 0, ${rect.width}px 0, ${rect.width}px ${rect.height}px, 0 ${rect.height}px, 0 0, ${canvas})`;
|
|
61
|
+
}
|
|
62
|
+
|
|
24
63
|
/**
|
|
25
64
|
* Dashed teal indicators for elements whose bounds extend past the composition
|
|
26
65
|
* (the "gray zone"). The in-canvas portion is clipped away so only the
|
|
@@ -49,15 +88,15 @@ export function OffCanvasIndicators({
|
|
|
49
88
|
return !groupSelections.some((g) => g.element === el);
|
|
50
89
|
})
|
|
51
90
|
.map((r) => {
|
|
52
|
-
const pos = {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
91
|
+
const pos = {
|
|
92
|
+
left: r.left,
|
|
93
|
+
top: r.top,
|
|
94
|
+
width: r.width,
|
|
95
|
+
height: r.height,
|
|
96
|
+
transform: r.angle ? `rotate(${r.angle}deg)` : undefined,
|
|
97
|
+
transformOrigin: "center",
|
|
98
|
+
};
|
|
99
|
+
const clipOutside = clipOutsideCanvas(r, compRect);
|
|
61
100
|
const selectOffCanvas = async () => {
|
|
62
101
|
const el = elements.current.get(r.key);
|
|
63
102
|
if (!el) return;
|
|
@@ -86,7 +125,7 @@ export function OffCanvasIndicators({
|
|
|
86
125
|
role="button"
|
|
87
126
|
tabIndex={0}
|
|
88
127
|
aria-label={`Select off-canvas element ${r.key}`}
|
|
89
|
-
className="pointer-events-auto absolute inset-0 border-2 border-dashed border-studio-accent/
|
|
128
|
+
className="pointer-events-auto absolute inset-0 border-2 border-dashed border-studio-accent/10 rounded-md cursor-pointer hover:border-studio-accent hover:bg-studio-accent/10 transition-colors"
|
|
90
129
|
style={clipOutside ? { clipPath: clipOutside } : undefined}
|
|
91
130
|
title={`Off-canvas: ${r.key} — click to select`}
|
|
92
131
|
onClick={handleClick}
|
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
// fallow-ignore-file unused-file
|
|
2
1
|
import { memo, useRef, type RefObject } from "react";
|
|
3
2
|
import { useMountEffect } from "../../hooks/useMountEffect";
|
|
4
|
-
import type
|
|
3
|
+
import { resolveGuideLineRect, type SnapGuide, type SpacingGuide } from "./snapEngine";
|
|
5
4
|
|
|
6
5
|
export interface SnapGuidesState {
|
|
7
6
|
guides: SnapGuide[];
|
|
@@ -17,22 +16,35 @@ const SPACING_BG = "rgba(255, 68, 204, 0.15)";
|
|
|
17
16
|
|
|
18
17
|
interface SnapGuideOverlayProps {
|
|
19
18
|
snapGuidesRef: RefObject<SnapGuidesState | null>;
|
|
20
|
-
|
|
21
|
-
|
|
19
|
+
/** Composition rect in overlay space — guide lines span exactly this rect. */
|
|
20
|
+
compositionLeft: number;
|
|
21
|
+
compositionTop: number;
|
|
22
|
+
compositionWidth: number;
|
|
23
|
+
compositionHeight: number;
|
|
22
24
|
}
|
|
23
25
|
|
|
24
26
|
export const SnapGuideOverlay = memo(function SnapGuideOverlay({
|
|
25
27
|
snapGuidesRef,
|
|
26
|
-
|
|
27
|
-
|
|
28
|
+
compositionLeft,
|
|
29
|
+
compositionTop,
|
|
30
|
+
compositionWidth,
|
|
31
|
+
compositionHeight,
|
|
28
32
|
}: SnapGuideOverlayProps) {
|
|
29
33
|
const guideElsRef = useRef<(HTMLDivElement | null)[]>([]);
|
|
30
34
|
const spacingElsRef = useRef<(HTMLDivElement | null)[]>([]);
|
|
31
35
|
const spacingLabelElsRef = useRef<(HTMLSpanElement | null)[]>([]);
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
+
const compositionRectRef = useRef({
|
|
37
|
+
left: compositionLeft,
|
|
38
|
+
top: compositionTop,
|
|
39
|
+
width: compositionWidth,
|
|
40
|
+
height: compositionHeight,
|
|
41
|
+
});
|
|
42
|
+
compositionRectRef.current = {
|
|
43
|
+
left: compositionLeft,
|
|
44
|
+
top: compositionTop,
|
|
45
|
+
width: compositionWidth,
|
|
46
|
+
height: compositionHeight,
|
|
47
|
+
};
|
|
36
48
|
|
|
37
49
|
useMountEffect(() => {
|
|
38
50
|
let frame = 0;
|
|
@@ -44,8 +56,7 @@ export const SnapGuideOverlay = memo(function SnapGuideOverlay({
|
|
|
44
56
|
const state = snapGuidesRef.current;
|
|
45
57
|
const guides = state?.guides ?? [];
|
|
46
58
|
const spacingGuides = state?.spacingGuides ?? [];
|
|
47
|
-
const
|
|
48
|
-
const h = overlayHeightRef.current;
|
|
59
|
+
const composition = compositionRectRef.current;
|
|
49
60
|
|
|
50
61
|
for (let i = 0; i < MAX_GUIDES; i++) {
|
|
51
62
|
const el = guideElsRef.current[i];
|
|
@@ -58,17 +69,11 @@ export const SnapGuideOverlay = memo(function SnapGuideOverlay({
|
|
|
58
69
|
}
|
|
59
70
|
|
|
60
71
|
el.style.display = "";
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
} else {
|
|
67
|
-
el.style.left = "0";
|
|
68
|
-
el.style.top = `${guide.position}px`;
|
|
69
|
-
el.style.width = `${w}px`;
|
|
70
|
-
el.style.height = "1px";
|
|
71
|
-
}
|
|
72
|
+
const line = resolveGuideLineRect(guide, composition);
|
|
73
|
+
el.style.left = `${line.left}px`;
|
|
74
|
+
el.style.top = `${line.top}px`;
|
|
75
|
+
el.style.width = `${line.width}px`;
|
|
76
|
+
el.style.height = `${line.height}px`;
|
|
72
77
|
}
|
|
73
78
|
|
|
74
79
|
for (let i = 0; i < MAX_SPACING_GUIDES; i++) {
|
|
@@ -34,7 +34,6 @@ function AppHotkeyHarness() {
|
|
|
34
34
|
const leftSidebarRef = useRef<LeftSidebarHandle | null>(null);
|
|
35
35
|
|
|
36
36
|
useAppHotkeys({
|
|
37
|
-
toggleTimelineVisibility: vi.fn(),
|
|
38
37
|
handleTimelineElementDelete: vi.fn(),
|
|
39
38
|
handleTimelineElementSplit: vi.fn(),
|
|
40
39
|
handleDomEditElementDelete: vi.fn(),
|