@hyperframes/studio 0.6.0-alpha.9 → 0.6.1

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.
Files changed (111) hide show
  1. package/dist/assets/hyperframes-player-CzwFysqv.js +418 -0
  2. package/dist/assets/index-D1JDq7Gg.css +1 -0
  3. package/dist/assets/index-hYc4aP7M.js +117 -0
  4. package/dist/favicon.svg +14 -0
  5. package/dist/index.html +3 -2
  6. package/package.json +9 -9
  7. package/src/App.tsx +421 -4303
  8. package/src/captions/components/CaptionOverlay.tsx +13 -246
  9. package/src/captions/components/CaptionOverlayUtils.ts +221 -0
  10. package/src/components/AskAgentModal.tsx +120 -0
  11. package/src/components/StudioHeader.tsx +133 -0
  12. package/src/components/StudioLeftSidebar.tsx +125 -0
  13. package/src/components/StudioPreviewArea.tsx +167 -0
  14. package/src/components/StudioRightPanel.tsx +198 -0
  15. package/src/components/TimelineToolbar.tsx +89 -0
  16. package/src/components/editor/DomEditOverlay.tsx +88 -993
  17. package/src/components/editor/EaseCurveEditor.tsx +221 -0
  18. package/src/components/editor/FileTree.tsx +13 -621
  19. package/src/components/editor/FileTreeIcons.tsx +128 -0
  20. package/src/components/editor/FileTreeNodes.tsx +496 -0
  21. package/src/components/editor/MotionPanel.tsx +16 -390
  22. package/src/components/editor/MotionPanelFields.tsx +185 -0
  23. package/src/components/editor/PropertyPanel.test.ts +0 -49
  24. package/src/components/editor/PropertyPanel.tsx +132 -2763
  25. package/src/components/editor/domEditOverlayGeometry.ts +211 -0
  26. package/src/components/editor/domEditOverlayGestures.ts +138 -0
  27. package/src/components/editor/domEditOverlayStartGesture.ts +155 -0
  28. package/src/components/editor/domEditing.ts +44 -1117
  29. package/src/components/editor/domEditingAgentPrompt.ts +97 -0
  30. package/src/components/editor/domEditingDom.ts +266 -0
  31. package/src/components/editor/domEditingElement.ts +329 -0
  32. package/src/components/editor/domEditingLayers.ts +460 -0
  33. package/src/components/editor/domEditingTypes.ts +125 -0
  34. package/src/components/editor/manualEditingAvailability.test.ts +2 -2
  35. package/src/components/editor/manualEditingAvailability.ts +1 -1
  36. package/src/components/editor/manualEdits.ts +84 -1049
  37. package/src/components/editor/manualEditsDom.ts +436 -0
  38. package/src/components/editor/manualEditsParsing.ts +280 -0
  39. package/src/components/editor/manualEditsSnapshot.ts +333 -0
  40. package/src/components/editor/manualEditsTypes.ts +141 -0
  41. package/src/components/editor/propertyPanelColor.tsx +371 -0
  42. package/src/components/editor/propertyPanelFill.tsx +421 -0
  43. package/src/components/editor/propertyPanelFont.tsx +455 -0
  44. package/src/components/editor/propertyPanelHelpers.ts +401 -0
  45. package/src/components/editor/propertyPanelPrimitives.tsx +357 -0
  46. package/src/components/editor/propertyPanelSections.tsx +453 -0
  47. package/src/components/editor/propertyPanelStyleSections.tsx +411 -0
  48. package/src/components/editor/studioMotion.ts +47 -434
  49. package/src/components/editor/studioMotionOps.ts +299 -0
  50. package/src/components/editor/studioMotionTypes.ts +168 -0
  51. package/src/components/editor/useDomEditOverlayGestures.ts +393 -0
  52. package/src/components/editor/useDomEditOverlayRects.ts +207 -0
  53. package/src/components/nle/NLELayout.tsx +68 -155
  54. package/src/components/nle/NLEPreview.tsx +3 -0
  55. package/src/components/nle/useCompositionStack.ts +126 -0
  56. package/src/components/renders/RenderQueue.tsx +102 -31
  57. package/src/components/renders/useRenderQueue.ts +8 -2
  58. package/src/components/sidebar/LeftSidebar.tsx +186 -186
  59. package/src/contexts/DomEditContext.tsx +137 -0
  60. package/src/contexts/FileManagerContext.tsx +110 -0
  61. package/src/contexts/PanelLayoutContext.tsx +68 -0
  62. package/src/contexts/StudioContext.tsx +135 -0
  63. package/src/hooks/useAppHotkeys.ts +326 -0
  64. package/src/hooks/useAskAgentModal.ts +162 -0
  65. package/src/hooks/useCaptionDetection.ts +132 -0
  66. package/src/hooks/useCompositionDimensions.ts +25 -0
  67. package/src/hooks/useConsoleErrorCapture.ts +60 -0
  68. package/src/hooks/useDomEditCommits.ts +437 -0
  69. package/src/hooks/useDomEditSession.ts +342 -0
  70. package/src/hooks/useDomEditTextCommits.ts +330 -0
  71. package/src/hooks/useDomSelection.ts +398 -0
  72. package/src/hooks/useFileManager.ts +431 -0
  73. package/src/hooks/useFrameCapture.ts +77 -0
  74. package/src/hooks/useLintModal.ts +35 -0
  75. package/src/hooks/useManifestPersistence.ts +492 -0
  76. package/src/hooks/usePanelLayout.ts +68 -0
  77. package/src/hooks/usePreviewInteraction.ts +153 -0
  78. package/src/hooks/useRenderClipContent.ts +124 -0
  79. package/src/hooks/useTimelineEditing.ts +472 -0
  80. package/src/hooks/useToast.ts +20 -0
  81. package/src/player/components/Player.tsx +33 -2
  82. package/src/player/components/Timeline.test.ts +0 -8
  83. package/src/player/components/Timeline.tsx +196 -1518
  84. package/src/player/components/TimelineCanvas.tsx +434 -0
  85. package/src/player/components/TimelineClip.tsx +9 -244
  86. package/src/player/components/TimelineEmptyState.tsx +102 -0
  87. package/src/player/components/TimelineRuler.tsx +90 -0
  88. package/src/player/components/timelineIcons.tsx +49 -0
  89. package/src/player/components/timelineLayout.ts +215 -0
  90. package/src/player/components/timelineUtils.ts +211 -0
  91. package/src/player/components/useTimelineClipDrag.ts +388 -0
  92. package/src/player/components/useTimelinePlayhead.ts +200 -0
  93. package/src/player/components/useTimelineRangeSelection.ts +135 -0
  94. package/src/player/hooks/usePlaybackKeyboard.ts +171 -0
  95. package/src/player/hooks/useTimelinePlayer.ts +105 -1371
  96. package/src/player/hooks/useTimelineSyncCallbacks.ts +288 -0
  97. package/src/player/lib/playbackAdapter.ts +145 -0
  98. package/src/player/lib/playbackShortcuts.ts +68 -0
  99. package/src/player/lib/playbackTypes.ts +60 -0
  100. package/src/player/lib/timelineDOM.ts +373 -0
  101. package/src/player/lib/timelineElementHelpers.ts +303 -0
  102. package/src/player/lib/timelineIframeHelpers.ts +269 -0
  103. package/src/utils/domEditHelpers.ts +50 -0
  104. package/src/utils/studioFontHelpers.ts +83 -0
  105. package/src/utils/studioHelpers.ts +214 -0
  106. package/src/utils/studioPreviewHelpers.ts +185 -0
  107. package/src/utils/timelineDiscovery.ts +1 -1
  108. package/dist/assets/hyperframes-player-DjsVzYFP.js +0 -418
  109. package/dist/assets/index-14zH9lqh.css +0 -1
  110. package/dist/assets/index-DYCiFGWQ.js +0 -108
  111. package/src/player/components/TimelineClip.test.ts +0 -92
@@ -0,0 +1,393 @@
1
+ /**
2
+ * Gesture handling for DomEditOverlay.
3
+ * Owns: onPointerMove, onPointerUp, clearPointerState.
4
+ * startGesture and startGroupDrag live in domEditOverlayStartGesture.ts.
5
+ */
6
+ import type { RefObject } from "react";
7
+ import { type DomEditSelection } from "./domEditing";
8
+ import {
9
+ applyManualOffsetDragCommit,
10
+ applyManualOffsetDragDraft,
11
+ endManualOffsetDragMembers,
12
+ restoreManualOffsetDragMembers,
13
+ } from "./manualOffsetDrag";
14
+ import {
15
+ applyStudioBoxSize,
16
+ applyStudioBoxSizeDraft,
17
+ applyStudioRotation,
18
+ applyStudioRotationDraft,
19
+ endStudioManualEditGesture,
20
+ isStudioManualEditGestureCurrent,
21
+ readStudioBoxSize,
22
+ restoreStudioBoxSize,
23
+ restoreStudioPathOffset,
24
+ restoreStudioRotation,
25
+ } from "./manualEdits";
26
+ import {
27
+ type GroupOverlayItem,
28
+ type OverlayRect,
29
+ groupOverlayItemsEqual,
30
+ rectsEqual,
31
+ } from "./domEditOverlayGeometry";
32
+ import {
33
+ BLOCKED_MOVE_THRESHOLD_PX,
34
+ type BlockedMoveState,
35
+ type GestureKind,
36
+ type GestureState,
37
+ type GroupGestureState,
38
+ hasDomEditRotationChanged,
39
+ resolveDomEditResizeGesture,
40
+ resolveDomEditRotationGesture,
41
+ } from "./domEditOverlayGestures";
42
+ import type { DomEditGroupPathOffsetCommit } from "./DomEditOverlay";
43
+ import {
44
+ startGesture as _startGesture,
45
+ startGroupDrag as _startGroupDrag,
46
+ } from "./domEditOverlayStartGesture";
47
+
48
+ // Refs are stable across renders; values are read via .current.
49
+ export type UseDomEditOverlayGesturesOptions = {
50
+ overlayRef: RefObject<HTMLDivElement | null>;
51
+ boxRef: RefObject<HTMLDivElement | null>;
52
+ selectionRef: RefObject<DomEditSelection | null>;
53
+ overlayRectRef: RefObject<OverlayRect | null>;
54
+ groupOverlayItemsRef: RefObject<GroupOverlayItem[]>;
55
+ gestureRef: RefObject<GestureState | null>;
56
+ groupGestureRef: RefObject<GroupGestureState | null>;
57
+ blockedMoveRef: RefObject<BlockedMoveState | null>;
58
+ rafPausedRef: RefObject<boolean>;
59
+ suppressNextBoxClickRef: RefObject<boolean>;
60
+ setOverlayRect: (next: OverlayRect | null) => void;
61
+ setGroupOverlayItems: (next: GroupOverlayItem[]) => void;
62
+ onBlockedMoveRef: RefObject<(selection: DomEditSelection) => void>;
63
+ onManualDragStartRef: RefObject<(() => void) | undefined>;
64
+ onPathOffsetCommitRef: RefObject<
65
+ (s: DomEditSelection, n: { x: number; y: number }) => Promise<void> | void
66
+ >;
67
+ onGroupPathOffsetCommitRef: RefObject<
68
+ (updates: DomEditGroupPathOffsetCommit[]) => Promise<void> | void
69
+ >;
70
+ onBoxSizeCommitRef: RefObject<
71
+ (s: DomEditSelection, n: { width: number; height: number }) => Promise<void> | void
72
+ >;
73
+ onRotationCommitRef: RefObject<
74
+ (s: DomEditSelection, n: { angle: number }) => Promise<void> | void
75
+ >;
76
+ onCanvasPointerMoveRef: RefObject<
77
+ (
78
+ e: React.PointerEvent<HTMLDivElement>,
79
+ o?: { preferClipAncestor?: boolean },
80
+ ) => DomEditSelection | null
81
+ >;
82
+ onCanvasMouseDown: (
83
+ e: React.MouseEvent<HTMLDivElement>,
84
+ o?: { preferClipAncestor?: boolean },
85
+ ) => void;
86
+ };
87
+
88
+ export function createDomEditOverlayGestureHandlers(opts: UseDomEditOverlayGesturesOptions) {
89
+ const setDraftOverlayRect = (next: OverlayRect) => {
90
+ if (rectsEqual(opts.overlayRectRef.current, next)) return;
91
+ opts.overlayRectRef.current = next;
92
+ opts.setOverlayRect(next);
93
+ };
94
+ const restoreGestureOverlayRect = (g: GestureState) => {
95
+ setDraftOverlayRect({
96
+ left: g.originLeft,
97
+ top: g.originTop,
98
+ width: g.originWidth,
99
+ height: g.originHeight,
100
+ editScaleX: g.editScaleX,
101
+ editScaleY: g.editScaleY,
102
+ });
103
+ };
104
+ const setDraftGroupOverlayItems = (next: GroupOverlayItem[]) => {
105
+ if (groupOverlayItemsEqual(opts.groupOverlayItemsRef.current, next)) return;
106
+ opts.groupOverlayItemsRef.current = next;
107
+ opts.setGroupOverlayItems(next);
108
+ };
109
+
110
+ const restoreGroupPathOffsets = (g: GroupGestureState) => {
111
+ restoreManualOffsetDragMembers(g.members);
112
+ setDraftGroupOverlayItems(g.originItems);
113
+ };
114
+
115
+ const startGroupDrag = (e: React.PointerEvent<HTMLElement>) => _startGroupDrag(e, opts);
116
+ const startGesture = (
117
+ kind: GestureKind,
118
+ e: React.PointerEvent<HTMLElement>,
119
+ options?: { selection?: DomEditSelection; rect?: OverlayRect | null },
120
+ ) => _startGesture(kind, e, opts, options);
121
+
122
+ const onPointerMove = (e: React.PointerEvent<HTMLDivElement>) => {
123
+ const g = opts.gestureRef.current;
124
+ const groupG = opts.groupGestureRef.current;
125
+ const sel = g?.selection ?? opts.selectionRef.current;
126
+ const box = opts.boxRef.current;
127
+ const blockedMove = opts.blockedMoveRef.current;
128
+ if (!blockedMove && !g && !groupG) {
129
+ opts.onCanvasPointerMoveRef.current(e, { preferClipAncestor: false });
130
+ }
131
+
132
+ if (blockedMove && sel) {
133
+ const dx = e.clientX - blockedMove.startX;
134
+ const dy = e.clientY - blockedMove.startY;
135
+ if (!blockedMove.notified && Math.hypot(dx, dy) >= BLOCKED_MOVE_THRESHOLD_PX) {
136
+ blockedMove.notified = true;
137
+ opts.suppressNextBoxClickRef.current = true;
138
+ opts.onBlockedMoveRef.current(sel);
139
+ }
140
+ return;
141
+ }
142
+
143
+ if (groupG) {
144
+ const dx = e.clientX - groupG.startX;
145
+ const dy = e.clientY - groupG.startY;
146
+ setDraftGroupOverlayItems(
147
+ groupG.originItems.map((item) => ({
148
+ ...item,
149
+ rect: { ...item.rect, left: item.rect.left + dx, top: item.rect.top + dy },
150
+ })),
151
+ );
152
+ for (const member of groupG.members) applyManualOffsetDragDraft(member, dx, dy);
153
+ return;
154
+ }
155
+
156
+ if (!g || !sel) return;
157
+ const dx = e.clientX - g.startX;
158
+ const dy = e.clientY - g.startY;
159
+
160
+ if (g.kind === "rotate") {
161
+ applyStudioRotationDraft(
162
+ sel.element,
163
+ resolveDomEditRotationGesture({
164
+ centerX: g.centerX,
165
+ centerY: g.centerY,
166
+ startX: g.startX,
167
+ startY: g.startY,
168
+ currentX: e.clientX,
169
+ currentY: e.clientY,
170
+ actualAngle: g.actualRotation,
171
+ snap: e.shiftKey,
172
+ }),
173
+ );
174
+ return;
175
+ }
176
+
177
+ if (g.kind === "drag") {
178
+ const nextBoxLeft = g.originLeft + dx;
179
+ const nextBoxTop = g.originTop + dy;
180
+ setDraftOverlayRect({
181
+ left: nextBoxLeft,
182
+ top: nextBoxTop,
183
+ width: g.originWidth,
184
+ height: g.originHeight,
185
+ editScaleX: g.editScaleX,
186
+ editScaleY: g.editScaleY,
187
+ });
188
+ if (box) {
189
+ box.style.left = `${nextBoxLeft}px`;
190
+ box.style.top = `${nextBoxTop}px`;
191
+ }
192
+ if (g.pathOffsetMember) applyManualOffsetDragDraft(g.pathOffsetMember, dx, dy);
193
+ } else {
194
+ if (!box) return;
195
+ const nextSize = resolveDomEditResizeGesture({
196
+ originWidth: g.originWidth,
197
+ originHeight: g.originHeight,
198
+ actualWidth: g.actualWidth,
199
+ actualHeight: g.actualHeight,
200
+ scaleX: g.editScaleX,
201
+ scaleY: g.editScaleY,
202
+ dx,
203
+ dy,
204
+ uniform: e.shiftKey,
205
+ });
206
+ setDraftOverlayRect({
207
+ left: g.originLeft,
208
+ top: g.originTop,
209
+ width: nextSize.overlayWidth,
210
+ height: nextSize.overlayHeight,
211
+ editScaleX: g.editScaleX,
212
+ editScaleY: g.editScaleY,
213
+ });
214
+ box.style.width = `${nextSize.overlayWidth}px`;
215
+ box.style.height = `${nextSize.overlayHeight}px`;
216
+ applyStudioBoxSizeDraft(sel.element, nextSize);
217
+ }
218
+ };
219
+
220
+ const onPointerUp = (e: React.PointerEvent<HTMLDivElement>) => {
221
+ const g = opts.gestureRef.current;
222
+ const groupG = opts.groupGestureRef.current;
223
+ const sel = g?.selection ?? opts.selectionRef.current;
224
+ const box = opts.boxRef.current;
225
+ opts.blockedMoveRef.current = null;
226
+
227
+ if (groupG) {
228
+ opts.groupGestureRef.current = null;
229
+ opts.rafPausedRef.current = false;
230
+ const dx = e.clientX - groupG.startX;
231
+ const dy = e.clientY - groupG.startY;
232
+ if (Math.hypot(dx, dy) < BLOCKED_MOVE_THRESHOLD_PX) {
233
+ restoreGroupPathOffsets(groupG);
234
+ opts.suppressNextBoxClickRef.current = true;
235
+ return;
236
+ }
237
+ setDraftGroupOverlayItems(
238
+ groupG.originItems.map((item) => ({
239
+ ...item,
240
+ rect: { ...item.rect, left: item.rect.left + dx, top: item.rect.top + dy },
241
+ })),
242
+ );
243
+ const updates = groupG.members.map((member) => ({
244
+ selection: member.selection,
245
+ next: applyManualOffsetDragCommit(member, dx, dy),
246
+ }));
247
+ void Promise.resolve(opts.onGroupPathOffsetCommitRef.current(updates))
248
+ .catch(() => {
249
+ for (const member of groupG.members) {
250
+ if (
251
+ member.gestureToken &&
252
+ isStudioManualEditGestureCurrent(member.element, member.gestureToken)
253
+ )
254
+ restoreStudioPathOffset(member.element, member.initialPathOffset);
255
+ }
256
+ })
257
+ .finally(() => endManualOffsetDragMembers(groupG.members));
258
+ return;
259
+ }
260
+
261
+ if (!g || !sel) {
262
+ opts.gestureRef.current = null;
263
+ opts.rafPausedRef.current = false;
264
+ return;
265
+ }
266
+ opts.gestureRef.current = null;
267
+ opts.rafPausedRef.current = false;
268
+ const movedDistance = Math.hypot(e.clientX - g.startX, e.clientY - g.startY);
269
+
270
+ if (g.kind === "drag" && movedDistance < BLOCKED_MOVE_THRESHOLD_PX) {
271
+ restoreStudioPathOffset(sel.element, g.initialPathOffset);
272
+ endStudioManualEditGesture(sel.element, g.manualEditDragToken);
273
+ if (box) {
274
+ box.style.left = `${g.originLeft}px`;
275
+ box.style.top = `${g.originTop}px`;
276
+ }
277
+ restoreGestureOverlayRect(g);
278
+ opts.suppressNextBoxClickRef.current = true;
279
+ opts.onCanvasMouseDown(e as unknown as React.MouseEvent<HTMLDivElement>, {
280
+ preferClipAncestor: false,
281
+ });
282
+ return;
283
+ }
284
+
285
+ if (g.kind === "resize" && movedDistance < BLOCKED_MOVE_THRESHOLD_PX) {
286
+ restoreStudioBoxSize(sel.element, g.initialBoxSize);
287
+ endStudioManualEditGesture(sel.element, g.manualEditDragToken);
288
+ if (box) {
289
+ box.style.width = `${g.originWidth}px`;
290
+ box.style.height = `${g.originHeight}px`;
291
+ }
292
+ restoreGestureOverlayRect(g);
293
+ return;
294
+ }
295
+
296
+ if (g.kind === "rotate") {
297
+ const finalRotation = resolveDomEditRotationGesture({
298
+ centerX: g.centerX,
299
+ centerY: g.centerY,
300
+ startX: g.startX,
301
+ startY: g.startY,
302
+ currentX: e.clientX,
303
+ currentY: e.clientY,
304
+ actualAngle: g.actualRotation,
305
+ snap: e.shiftKey,
306
+ });
307
+ if (!hasDomEditRotationChanged(g.actualRotation, finalRotation.angle)) {
308
+ restoreStudioRotation(sel.element, g.initialRotation);
309
+ endStudioManualEditGesture(sel.element, g.manualEditDragToken);
310
+ return;
311
+ }
312
+ applyStudioRotation(sel.element, finalRotation);
313
+ void Promise.resolve(opts.onRotationCommitRef.current(sel, finalRotation))
314
+ .catch(() => {
315
+ if (
316
+ g.manualEditDragToken &&
317
+ isStudioManualEditGestureCurrent(sel.element, g.manualEditDragToken)
318
+ )
319
+ restoreStudioRotation(sel.element, g.initialRotation);
320
+ })
321
+ .finally(() => endStudioManualEditGesture(sel.element, g.manualEditDragToken));
322
+ } else if (g.kind === "drag") {
323
+ const dx = e.clientX - g.startX;
324
+ const dy = e.clientY - g.startY;
325
+ if (!g.pathOffsetMember) return;
326
+ const finalOffset = applyManualOffsetDragCommit(g.pathOffsetMember, dx, dy);
327
+ const nextBoxLeft = g.originLeft + dx;
328
+ const nextBoxTop = g.originTop + dy;
329
+ setDraftOverlayRect({
330
+ left: nextBoxLeft,
331
+ top: nextBoxTop,
332
+ width: g.originWidth,
333
+ height: g.originHeight,
334
+ editScaleX: g.editScaleX,
335
+ editScaleY: g.editScaleY,
336
+ });
337
+ if (box) {
338
+ box.style.left = `${nextBoxLeft}px`;
339
+ box.style.top = `${nextBoxTop}px`;
340
+ }
341
+ void Promise.resolve(opts.onPathOffsetCommitRef.current(sel, finalOffset))
342
+ .catch(() => {
343
+ if (
344
+ g.pathOffsetMember?.gestureToken &&
345
+ isStudioManualEditGestureCurrent(sel.element, g.pathOffsetMember.gestureToken)
346
+ )
347
+ restoreStudioPathOffset(sel.element, g.initialPathOffset);
348
+ })
349
+ .finally(() => {
350
+ if (g.pathOffsetMember) endManualOffsetDragMembers([g.pathOffsetMember]);
351
+ });
352
+ } else {
353
+ const finalSize = readStudioBoxSize(sel.element);
354
+ applyStudioBoxSize(sel.element, finalSize);
355
+ void Promise.resolve(opts.onBoxSizeCommitRef.current(sel, finalSize))
356
+ .catch(() => {
357
+ if (
358
+ g.manualEditDragToken &&
359
+ isStudioManualEditGestureCurrent(sel.element, g.manualEditDragToken)
360
+ )
361
+ restoreStudioBoxSize(sel.element, g.initialBoxSize);
362
+ })
363
+ .finally(() => endStudioManualEditGesture(sel.element, g.manualEditDragToken));
364
+ }
365
+ };
366
+
367
+ const clearPointerState = (selectionRef: RefObject<DomEditSelection | null>) => {
368
+ const groupG = opts.groupGestureRef.current;
369
+ if (groupG) restoreGroupPathOffsets(groupG);
370
+ const g = opts.gestureRef.current;
371
+ const sel = g?.selection ?? selectionRef.current;
372
+ if (g?.mode === "path-offset" && sel) {
373
+ restoreStudioPathOffset(sel.element, g.initialPathOffset);
374
+ endStudioManualEditGesture(sel.element, g.manualEditDragToken);
375
+ restoreGestureOverlayRect(g);
376
+ }
377
+ if (g?.mode === "box-size" && sel) {
378
+ restoreStudioBoxSize(sel.element, g.initialBoxSize);
379
+ endStudioManualEditGesture(sel.element, g.manualEditDragToken);
380
+ restoreGestureOverlayRect(g);
381
+ }
382
+ if (g?.mode === "rotation" && sel) {
383
+ restoreStudioRotation(sel.element, g.initialRotation);
384
+ endStudioManualEditGesture(sel.element, g.manualEditDragToken);
385
+ }
386
+ opts.blockedMoveRef.current = null;
387
+ opts.groupGestureRef.current = null;
388
+ opts.gestureRef.current = null;
389
+ opts.rafPausedRef.current = false;
390
+ };
391
+
392
+ return { startGesture, startGroupDrag, onPointerMove, onPointerUp, clearPointerState };
393
+ }
@@ -0,0 +1,207 @@
1
+ /**
2
+ * RAF-driven hook that tracks overlay, hover, and group rects from the iframe DOM.
3
+ * Runs a requestAnimationFrame loop and writes React state only when rects change.
4
+ */
5
+ import { useRef, useState, type RefObject } from "react";
6
+ import { useMountEffect } from "../../hooks/useMountEffect";
7
+ import { type DomEditSelection, findElementForSelection } from "./domEditing";
8
+ import {
9
+ type GroupOverlayItem,
10
+ type OverlayRect,
11
+ type ResolvedElementRef,
12
+ groupOverlayItemsEqual,
13
+ isElementVisibleForOverlay,
14
+ rectsEqual,
15
+ resolveElementForOverlay,
16
+ selectionCacheKey,
17
+ toOverlayRect,
18
+ } from "./domEditOverlayGeometry";
19
+
20
+ interface UseDomEditOverlayRectsOptions {
21
+ iframeRef: RefObject<HTMLIFrameElement | null>;
22
+ overlayRef: RefObject<HTMLDivElement | null>;
23
+ selectionRef: RefObject<DomEditSelection | null>;
24
+ activeCompositionPathRef: RefObject<string | null>;
25
+ groupSelectionsRef: RefObject<DomEditSelection[]>;
26
+ hoverSelectionRef: RefObject<DomEditSelection | null>;
27
+ rafPausedRef: RefObject<boolean>;
28
+ }
29
+
30
+ interface UseDomEditOverlayRectsResult {
31
+ overlayRect: OverlayRect | null;
32
+ overlayRectRef: RefObject<OverlayRect | null>;
33
+ setOverlayRect: (next: OverlayRect | null) => void;
34
+ hoverRect: OverlayRect | null;
35
+ hoverRectRef: RefObject<OverlayRect | null>;
36
+ setHoverRect: (next: OverlayRect | null) => void;
37
+ groupOverlayItems: GroupOverlayItem[];
38
+ groupOverlayItemsRef: RefObject<GroupOverlayItem[]>;
39
+ setGroupOverlayItems: (next: GroupOverlayItem[]) => void;
40
+ }
41
+
42
+ export function useDomEditOverlayRects({
43
+ iframeRef,
44
+ overlayRef,
45
+ selectionRef,
46
+ activeCompositionPathRef,
47
+ groupSelectionsRef,
48
+ hoverSelectionRef,
49
+ rafPausedRef,
50
+ }: UseDomEditOverlayRectsOptions): UseDomEditOverlayRectsResult {
51
+ const [overlayRect, setOverlayRectState] = useState<OverlayRect | null>(null);
52
+ const [hoverRect, setHoverRectState] = useState<OverlayRect | null>(null);
53
+ const [groupOverlayItems, setGroupOverlayItemsState] = useState<GroupOverlayItem[]>([]);
54
+
55
+ const overlayRectRef = useRef<OverlayRect | null>(null);
56
+ const hoverRectRef = useRef<OverlayRect | null>(null);
57
+ const groupOverlayItemsRef = useRef<GroupOverlayItem[]>([]);
58
+ const resolvedElementRef = useRef<{ key: string; element: HTMLElement } | null>(null);
59
+ const resolvedHoverElementRef = useRef<{ key: string; element: HTMLElement } | null>(null);
60
+ const resolvedGroupElementRef = useRef<Map<string, HTMLElement>>(new Map());
61
+
62
+ const setOverlayRect = (next: OverlayRect | null) => {
63
+ if (rectsEqual(overlayRectRef.current, next)) return;
64
+ overlayRectRef.current = next;
65
+ setOverlayRectState(next);
66
+ };
67
+
68
+ const setHoverRect = (next: OverlayRect | null) => {
69
+ if (rectsEqual(hoverRectRef.current, next)) return;
70
+ hoverRectRef.current = next;
71
+ setHoverRectState(next);
72
+ };
73
+
74
+ const setGroupOverlayItems = (next: GroupOverlayItem[]) => {
75
+ if (groupOverlayItemsEqual(groupOverlayItemsRef.current, next)) return;
76
+ groupOverlayItemsRef.current = next;
77
+ setGroupOverlayItemsState(next);
78
+ };
79
+
80
+ const resolveGroupElement = (doc: Document, sel: DomEditSelection) => {
81
+ const key = selectionCacheKey(sel);
82
+ const cached = resolvedGroupElementRef.current.get(key);
83
+ if (cached?.isConnected && cached.ownerDocument === doc) return cached;
84
+
85
+ const next = findElementForSelection(doc, sel, activeCompositionPathRef.current);
86
+ if (next) {
87
+ resolvedGroupElementRef.current.set(key, next);
88
+ } else {
89
+ resolvedGroupElementRef.current.delete(key);
90
+ }
91
+ return next;
92
+ };
93
+
94
+ useMountEffect(() => {
95
+ let frame = 0;
96
+
97
+ const clearAll = () => {
98
+ setOverlayRect(null);
99
+ setHoverRect(null);
100
+ setGroupOverlayItems([]);
101
+ };
102
+
103
+ const update = () => {
104
+ frame = requestAnimationFrame(update);
105
+ if (rafPausedRef.current) return;
106
+
107
+ const sel = selectionRef.current;
108
+ const iframe = iframeRef.current;
109
+ const overlayEl = overlayRef.current;
110
+ if (!iframe || !overlayEl) {
111
+ resolvedElementRef.current = null;
112
+ resolvedHoverElementRef.current = null;
113
+ resolvedGroupElementRef.current.clear();
114
+ clearAll();
115
+ return;
116
+ }
117
+
118
+ const doc = iframe.contentDocument;
119
+ if (!doc) {
120
+ resolvedElementRef.current = null;
121
+ resolvedHoverElementRef.current = null;
122
+ resolvedGroupElementRef.current.clear();
123
+ clearAll();
124
+ return;
125
+ }
126
+
127
+ if (sel) {
128
+ const el = resolveElementForOverlay(
129
+ doc,
130
+ sel,
131
+ activeCompositionPathRef.current,
132
+ resolvedElementRef as ResolvedElementRef,
133
+ );
134
+ if (el && isElementVisibleForOverlay(el)) {
135
+ setOverlayRect(toOverlayRect(overlayEl, iframe, el));
136
+ } else {
137
+ setOverlayRect(null);
138
+ }
139
+ } else {
140
+ resolvedElementRef.current = null;
141
+ setOverlayRect(null);
142
+ }
143
+
144
+ const group = groupSelectionsRef.current;
145
+ if (group.length > 0) {
146
+ const nextGroupItems: GroupOverlayItem[] = [];
147
+ const liveGroupKeys = new Set<string>();
148
+ for (const groupSelection of group) {
149
+ const key = selectionCacheKey(groupSelection);
150
+ liveGroupKeys.add(key);
151
+ const el = resolveGroupElement(doc, groupSelection);
152
+ const rect = el ? toOverlayRect(overlayEl, iframe, el) : null;
153
+ if (el && rect)
154
+ nextGroupItems.push({ key, selection: groupSelection, element: el, rect });
155
+ }
156
+ for (const key of resolvedGroupElementRef.current.keys()) {
157
+ if (!liveGroupKeys.has(key)) resolvedGroupElementRef.current.delete(key);
158
+ }
159
+ setGroupOverlayItems(nextGroupItems);
160
+ } else {
161
+ resolvedGroupElementRef.current.clear();
162
+ setGroupOverlayItems([]);
163
+ }
164
+
165
+ const hoverSel = hoverSelectionRef.current;
166
+ const hoverMatchesSelection = Boolean(
167
+ sel && hoverSel && selectionCacheKey(sel) === selectionCacheKey(hoverSel),
168
+ );
169
+ const hoverMatchesGroup = Boolean(
170
+ hoverSel && group.some((entry) => selectionCacheKey(entry) === selectionCacheKey(hoverSel)),
171
+ );
172
+ if (!hoverSel || hoverMatchesSelection || hoverMatchesGroup) {
173
+ resolvedHoverElementRef.current = null;
174
+ setHoverRect(null);
175
+ return;
176
+ }
177
+
178
+ const hoverEl = resolveElementForOverlay(
179
+ doc,
180
+ hoverSel,
181
+ activeCompositionPathRef.current,
182
+ resolvedHoverElementRef as ResolvedElementRef,
183
+ );
184
+ if (!hoverEl) {
185
+ setHoverRect(null);
186
+ return;
187
+ }
188
+
189
+ setHoverRect(toOverlayRect(overlayEl, iframe, hoverEl));
190
+ };
191
+
192
+ frame = requestAnimationFrame(update);
193
+ return () => cancelAnimationFrame(frame);
194
+ });
195
+
196
+ return {
197
+ overlayRect,
198
+ overlayRectRef,
199
+ setOverlayRect,
200
+ hoverRect,
201
+ hoverRectRef,
202
+ setHoverRect,
203
+ groupOverlayItems,
204
+ groupOverlayItemsRef,
205
+ setGroupOverlayItems,
206
+ };
207
+ }