@sequent-org/moodboard 1.2.119 → 1.3.0

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 (122) hide show
  1. package/package.json +11 -1
  2. package/src/assets/icons/rotate-icon.svg +1 -1
  3. package/src/core/HistoryManager.js +16 -16
  4. package/src/core/KeyboardManager.js +48 -539
  5. package/src/core/PixiEngine.js +9 -9
  6. package/src/core/SaveManager.js +56 -31
  7. package/src/core/bootstrap/CoreInitializer.js +65 -0
  8. package/src/core/commands/DeleteObjectCommand.js +8 -0
  9. package/src/core/commands/GroupDeleteCommand.js +75 -0
  10. package/src/core/commands/GroupRotateCommand.js +6 -0
  11. package/src/core/commands/UpdateContentCommand.js +52 -0
  12. package/src/core/commands/UpdateFramePropertiesCommand.js +98 -0
  13. package/src/core/commands/UpdateFrameTypeCommand.js +85 -0
  14. package/src/core/commands/UpdateNoteStyleCommand.js +88 -0
  15. package/src/core/commands/UpdateTextStyleCommand.js +90 -0
  16. package/src/core/commands/index.js +6 -0
  17. package/src/core/events/Events.js +6 -0
  18. package/src/core/flows/ClipboardFlow.js +553 -0
  19. package/src/core/flows/LayerAndViewportFlow.js +283 -0
  20. package/src/core/flows/ObjectLifecycleFlow.js +336 -0
  21. package/src/core/flows/SaveFlow.js +34 -0
  22. package/src/core/flows/TransformFlow.js +277 -0
  23. package/src/core/flows/TransformFlowResizeHelpers.js +83 -0
  24. package/src/core/index.js +41 -1773
  25. package/src/core/keyboard/KeyboardClipboardImagePaste.js +190 -0
  26. package/src/core/keyboard/KeyboardContextGuards.js +35 -0
  27. package/src/core/keyboard/KeyboardEventRouter.js +92 -0
  28. package/src/core/keyboard/KeyboardSelectionActions.js +103 -0
  29. package/src/core/keyboard/KeyboardShortcutMap.js +31 -0
  30. package/src/core/keyboard/KeyboardToolSwitching.js +26 -0
  31. package/src/core/rendering/ObjectRenderer.js +3 -7
  32. package/src/grid/BaseGrid.js +26 -0
  33. package/src/grid/CrossGrid.js +7 -6
  34. package/src/grid/DotGrid.js +89 -33
  35. package/src/grid/DotGridZoomPhases.js +42 -0
  36. package/src/grid/LineGrid.js +22 -21
  37. package/src/moodboard/MoodBoard.js +31 -532
  38. package/src/moodboard/bootstrap/MoodBoardInitializer.js +47 -0
  39. package/src/moodboard/bootstrap/MoodBoardManagersFactory.js +38 -0
  40. package/src/moodboard/bootstrap/MoodBoardUiFactory.js +109 -0
  41. package/src/moodboard/integration/MoodBoardEventBindings.js +65 -0
  42. package/src/moodboard/integration/MoodBoardLoadApi.js +82 -0
  43. package/src/moodboard/integration/MoodBoardScreenshotApi.js +33 -0
  44. package/src/moodboard/integration/MoodBoardScreenshotCanvas.js +98 -0
  45. package/src/moodboard/lifecycle/MoodBoardDestroyer.js +97 -0
  46. package/src/objects/FileObject.js +17 -6
  47. package/src/objects/FrameObject.js +50 -10
  48. package/src/objects/NoteObject.js +5 -4
  49. package/src/services/BoardService.js +42 -2
  50. package/src/services/FrameService.js +83 -42
  51. package/src/services/ResizePolicyService.js +152 -0
  52. package/src/services/SettingsApplier.js +7 -2
  53. package/src/services/ZoomPanController.js +35 -9
  54. package/src/tools/ToolManager.js +30 -537
  55. package/src/tools/board-tools/PanTool.js +5 -11
  56. package/src/tools/manager/ToolActivationController.js +49 -0
  57. package/src/tools/manager/ToolEventRouter.js +396 -0
  58. package/src/tools/manager/ToolManagerGuards.js +33 -0
  59. package/src/tools/manager/ToolManagerLifecycle.js +110 -0
  60. package/src/tools/manager/ToolRegistry.js +33 -0
  61. package/src/tools/object-tools/DrawingTool.js +48 -14
  62. package/src/tools/object-tools/PlacementTool.js +50 -1049
  63. package/src/tools/object-tools/PlacementToolV2.js +88 -0
  64. package/src/tools/object-tools/SelectTool.js +174 -2681
  65. package/src/tools/object-tools/placement/GhostController.js +504 -0
  66. package/src/tools/object-tools/placement/PlacementCoordinateResolver.js +20 -0
  67. package/src/tools/object-tools/placement/PlacementEventsBridge.js +91 -0
  68. package/src/tools/object-tools/placement/PlacementInputRouter.js +267 -0
  69. package/src/tools/object-tools/placement/PlacementPayloadFactory.js +111 -0
  70. package/src/tools/object-tools/placement/PlacementSessionStore.js +18 -0
  71. package/src/tools/object-tools/selection/BoxSelectController.js +0 -5
  72. package/src/tools/object-tools/selection/CloneFlowController.js +71 -0
  73. package/src/tools/object-tools/selection/CoordinateMapper.js +10 -0
  74. package/src/tools/object-tools/selection/CursorController.js +78 -0
  75. package/src/tools/object-tools/selection/FileNameInlineEditorController.js +184 -0
  76. package/src/tools/object-tools/selection/HitTestService.js +102 -0
  77. package/src/tools/object-tools/selection/InlineEditorController.js +24 -0
  78. package/src/tools/object-tools/selection/InlineEditorDomFactory.js +50 -0
  79. package/src/tools/object-tools/selection/InlineEditorListenersRegistry.js +14 -0
  80. package/src/tools/object-tools/selection/InlineEditorPositioningService.js +25 -0
  81. package/src/tools/object-tools/selection/NoteInlineEditorController.js +113 -0
  82. package/src/tools/object-tools/selection/SelectInputRouter.js +267 -0
  83. package/src/tools/object-tools/selection/SelectToolLifecycleController.js +128 -0
  84. package/src/tools/object-tools/selection/SelectToolSetup.js +134 -0
  85. package/src/tools/object-tools/selection/SelectionOverlayService.js +81 -0
  86. package/src/tools/object-tools/selection/SelectionStateController.js +91 -0
  87. package/src/tools/object-tools/selection/TextEditorDomFactory.js +65 -0
  88. package/src/tools/object-tools/selection/TextEditorInteractionController.js +266 -0
  89. package/src/tools/object-tools/selection/TextEditorLifecycleRegistry.js +90 -0
  90. package/src/tools/object-tools/selection/TextEditorPositioningService.js +158 -0
  91. package/src/tools/object-tools/selection/TextEditorSyncService.js +110 -0
  92. package/src/tools/object-tools/selection/TextInlineEditorController.js +457 -0
  93. package/src/tools/object-tools/selection/TransformInteractionController.js +466 -0
  94. package/src/ui/FilePropertiesPanel.js +61 -32
  95. package/src/ui/FramePropertiesPanel.js +176 -101
  96. package/src/ui/HtmlHandlesLayer.js +121 -999
  97. package/src/ui/MapPanel.js +12 -7
  98. package/src/ui/NotePropertiesPanel.js +17 -2
  99. package/src/ui/TextPropertiesPanel.js +124 -738
  100. package/src/ui/Toolbar.js +71 -1180
  101. package/src/ui/Topbar.js +23 -25
  102. package/src/ui/ZoomPanel.js +16 -5
  103. package/src/ui/handles/GroupSelectionHandlesController.js +29 -0
  104. package/src/ui/handles/HandlesDomRenderer.js +278 -0
  105. package/src/ui/handles/HandlesEventBridge.js +102 -0
  106. package/src/ui/handles/HandlesInteractionController.js +772 -0
  107. package/src/ui/handles/HandlesPositioningService.js +206 -0
  108. package/src/ui/handles/SingleSelectionHandlesController.js +22 -0
  109. package/src/ui/styles/toolbar.css +2 -0
  110. package/src/ui/styles/workspace.css +13 -6
  111. package/src/ui/text-properties/TextPropertiesPanelBindings.js +92 -0
  112. package/src/ui/text-properties/TextPropertiesPanelEventBridge.js +77 -0
  113. package/src/ui/text-properties/TextPropertiesPanelMapper.js +173 -0
  114. package/src/ui/text-properties/TextPropertiesPanelRenderer.js +434 -0
  115. package/src/ui/text-properties/TextPropertiesPanelState.js +39 -0
  116. package/src/ui/toolbar/ToolbarActionRouter.js +193 -0
  117. package/src/ui/toolbar/ToolbarDialogsController.js +186 -0
  118. package/src/ui/toolbar/ToolbarPopupsController.js +662 -0
  119. package/src/ui/toolbar/ToolbarRenderer.js +97 -0
  120. package/src/ui/toolbar/ToolbarStateController.js +79 -0
  121. package/src/ui/toolbar/ToolbarTooltipController.js +52 -0
  122. package/src/utils/emojiLoaderNoBundler.js +1 -1
@@ -0,0 +1,772 @@
1
+ import { Events } from '../../core/events/Events.js';
2
+
3
+ export class HandlesInteractionController {
4
+ constructor(host) {
5
+ this.host = host;
6
+ }
7
+
8
+ _parseBoxRotation(box) {
9
+ const transform = box?.style?.transform || '';
10
+ const match = transform.match(/rotate\(([-0-9.]+)deg\)/);
11
+ return match ? Number.parseFloat(match[1]) || 0 : 0;
12
+ }
13
+
14
+ _rotateVector(x, y, angleDegrees) {
15
+ const angleRad = angleDegrees * Math.PI / 180;
16
+ const cos = Math.cos(angleRad);
17
+ const sin = Math.sin(angleRad);
18
+ return {
19
+ x: x * cos - y * sin,
20
+ y: x * sin + y * cos,
21
+ };
22
+ }
23
+
24
+ _cssPointFromClient(clientX, clientY) {
25
+ const containerRect = this.host.container.getBoundingClientRect();
26
+ return {
27
+ x: clientX - containerRect.left,
28
+ y: clientY - containerRect.top,
29
+ };
30
+ }
31
+
32
+ _readBoxCss(box) {
33
+ return {
34
+ left: parseFloat(box.style.left),
35
+ top: parseFloat(box.style.top),
36
+ width: parseFloat(box.style.width),
37
+ height: parseFloat(box.style.height),
38
+ };
39
+ }
40
+
41
+ _cssRectToWorld(cssRect, offsetLeft, offsetTop, rendererRes, tx, ty, s) {
42
+ const screenX = cssRect.left - offsetLeft;
43
+ const screenY = cssRect.top - offsetTop;
44
+ return {
45
+ x: ((screenX * rendererRes) - tx) / s,
46
+ y: ((screenY * rendererRes) - ty) / s,
47
+ width: (cssRect.width * rendererRes) / s,
48
+ height: (cssRect.height * rendererRes) / s,
49
+ };
50
+ }
51
+
52
+ _computeRotatedResizeBox(startCSS, pointerCss, rotationDegrees, handleType, maintainAspectRatio = false, dominantAxis = null) {
53
+ const startWidth = startCSS.width;
54
+ const startHeight = startCSS.height;
55
+ const startCenter = {
56
+ x: startCSS.left + startWidth / 2,
57
+ y: startCSS.top + startHeight / 2,
58
+ };
59
+ const localPointer = this._rotateVector(
60
+ pointerCss.x - startCenter.x,
61
+ pointerCss.y - startCenter.y,
62
+ -rotationDegrees
63
+ );
64
+
65
+ const startBounds = {
66
+ x: -startWidth / 2,
67
+ y: -startHeight / 2,
68
+ width: startWidth,
69
+ height: startHeight,
70
+ };
71
+ const handlePoint = {
72
+ x: handleType.includes('w') ? startBounds.x : handleType.includes('e') ? startBounds.x + startBounds.width : 0,
73
+ y: handleType.includes('n') ? startBounds.y : handleType.includes('s') ? startBounds.y + startBounds.height : 0,
74
+ };
75
+ const deltaX = localPointer.x - handlePoint.x;
76
+ const deltaY = localPointer.y - handlePoint.y;
77
+
78
+ let x = startBounds.x;
79
+ let y = startBounds.y;
80
+ let width = startBounds.width;
81
+ let height = startBounds.height;
82
+
83
+ switch (handleType) {
84
+ case 'e': width = startBounds.width + deltaX; break;
85
+ case 'w': width = startBounds.width - deltaX; x = startBounds.x + deltaX; break;
86
+ case 's': height = startBounds.height + deltaY; break;
87
+ case 'n': height = startBounds.height - deltaY; y = startBounds.y + deltaY; break;
88
+ case 'se': width = startBounds.width + deltaX; height = startBounds.height + deltaY; break;
89
+ case 'ne': width = startBounds.width + deltaX; height = startBounds.height - deltaY; y = startBounds.y + deltaY; break;
90
+ case 'sw': width = startBounds.width - deltaX; x = startBounds.x + deltaX; height = startBounds.height + deltaY; break;
91
+ case 'nw': width = startBounds.width - deltaX; x = startBounds.x + deltaX; height = startBounds.height - deltaY; y = startBounds.y + deltaY; break;
92
+ }
93
+
94
+ let resolvedDominantAxis = dominantAxis;
95
+ if (maintainAspectRatio && startHeight !== 0) {
96
+ const aspectRatio = startWidth / startHeight;
97
+ if (['nw', 'ne', 'sw', 'se'].includes(handleType)) {
98
+ const widthChange = Math.abs(width - startWidth);
99
+ const heightChange = Math.abs(height - startHeight);
100
+ if (!resolvedDominantAxis) {
101
+ resolvedDominantAxis = widthChange > heightChange ? 'width' : 'height';
102
+ }
103
+ if (resolvedDominantAxis === 'width') {
104
+ height = width / aspectRatio;
105
+ if (['n', 'ne', 'nw'].includes(handleType)) y = startBounds.y + (startHeight - height);
106
+ else y = startBounds.y;
107
+ } else {
108
+ width = height * aspectRatio;
109
+ if (['w', 'sw', 'nw'].includes(handleType)) x = startBounds.x + (startWidth - width);
110
+ else x = startBounds.x;
111
+ }
112
+ } else if (['e', 'w'].includes(handleType)) {
113
+ height = width / aspectRatio;
114
+ y = startBounds.y + (startHeight - height) / 2;
115
+ if (handleType === 'w') x = startBounds.x + (startWidth - width);
116
+ } else if (['n', 's'].includes(handleType)) {
117
+ width = height * aspectRatio;
118
+ x = startBounds.x + (startWidth - width) / 2;
119
+ if (handleType === 'n') y = startBounds.y + (startHeight - height);
120
+ }
121
+ }
122
+
123
+ if (width < 1) {
124
+ if (['w', 'sw', 'nw'].includes(handleType)) x += (width - 1);
125
+ width = 1;
126
+ }
127
+ if (height < 1) {
128
+ if (['n', 'ne', 'nw'].includes(handleType)) y += (height - 1);
129
+ height = 1;
130
+ }
131
+
132
+ const localCenter = {
133
+ x: x + width / 2,
134
+ y: y + height / 2,
135
+ };
136
+ const worldCenterOffset = this._rotateVector(localCenter.x, localCenter.y, rotationDegrees);
137
+ const worldCenter = {
138
+ x: startCenter.x + worldCenterOffset.x,
139
+ y: startCenter.y + worldCenterOffset.y,
140
+ };
141
+
142
+ return {
143
+ left: worldCenter.x - width / 2,
144
+ top: worldCenter.y - height / 2,
145
+ width,
146
+ height,
147
+ center: worldCenter,
148
+ dominantAxis: resolvedDominantAxis,
149
+ };
150
+ }
151
+
152
+ onHandleDown(e, box) {
153
+ e.preventDefault();
154
+ e.stopPropagation();
155
+ const dir = e.currentTarget.dataset.dir;
156
+ const id = e.currentTarget.dataset.id;
157
+ const isGroup = id === '__group__';
158
+ const world = this.host.core.pixi.worldLayer || this.host.core.pixi.app.stage;
159
+ const s = world?.scale?.x || 1;
160
+ const tx = world?.x || 0;
161
+ const ty = world?.y || 0;
162
+ const rendererRes = (this.host.core.pixi.app.renderer?.resolution) || 1;
163
+ const containerRect = this.host.container.getBoundingClientRect();
164
+ const view = this.host.core.pixi.app.view;
165
+ const viewRect = view.getBoundingClientRect();
166
+ const offsetLeft = viewRect.left - containerRect.left;
167
+ const offsetTop = viewRect.top - containerRect.top;
168
+
169
+ let startCSS = this._readBoxCss(box);
170
+ let startWorld = this._cssRectToWorld(startCSS, offsetLeft, offsetTop, rendererRes, tx, ty, s);
171
+
172
+ let objects = [id];
173
+ if (isGroup) {
174
+ const req = { selection: [] };
175
+ this.host.eventBus.emit(Events.Tool.GetSelection, req);
176
+ objects = req.selection || [];
177
+ this.host.eventBus.emit(Events.Tool.GroupResizeStart, { objects, startBounds: { ...startWorld } });
178
+ } else {
179
+ this.host.eventBus.emit(Events.Tool.ResizeStart, { object: id, handle: dir });
180
+ }
181
+
182
+ let startMouse = { x: e.clientX, y: e.clientY };
183
+ const startRotation = isGroup ? this._parseBoxRotation(box) : 0;
184
+ let previousMaintainAspectRatio = !!e.shiftKey;
185
+ let aspectLockDominantAxis = null;
186
+ let isTextTarget = false;
187
+ let isNoteTarget = false;
188
+ {
189
+ const req = { objectId: id, pixiObject: null };
190
+ this.host.eventBus.emit(Events.Tool.GetObjectPixi, req);
191
+ const mbType = req.pixiObject && req.pixiObject._mb && req.pixiObject._mb.type;
192
+ isTextTarget = (mbType === 'text' || mbType === 'simple-text');
193
+ isNoteTarget = (mbType === 'note');
194
+ }
195
+
196
+ const onMove = (ev) => {
197
+ const maintainAspectRatio = !!ev.shiftKey;
198
+ if (isGroup && maintainAspectRatio !== previousMaintainAspectRatio) {
199
+ startCSS = this._readBoxCss(box);
200
+ startWorld = this._cssRectToWorld(startCSS, offsetLeft, offsetTop, rendererRes, tx, ty, s);
201
+ startMouse = { x: ev.clientX, y: ev.clientY };
202
+ previousMaintainAspectRatio = maintainAspectRatio;
203
+ aspectLockDominantAxis = null;
204
+ this.host.eventBus.emit(Events.Tool.GroupResizeStart, {
205
+ objects,
206
+ startBounds: { ...startWorld },
207
+ });
208
+ return;
209
+ }
210
+
211
+ const dx = ev.clientX - startMouse.x;
212
+ const dy = ev.clientY - startMouse.y;
213
+ let newLeft = startCSS.left;
214
+ let newTop = startCSS.top;
215
+ let newW = startCSS.width;
216
+ let newH = startCSS.height;
217
+
218
+ if (isGroup && Math.abs(startRotation) > 0.001) {
219
+ const rotatedBox = this._computeRotatedResizeBox(
220
+ startCSS,
221
+ this._cssPointFromClient(ev.clientX, ev.clientY),
222
+ startRotation,
223
+ dir,
224
+ maintainAspectRatio,
225
+ aspectLockDominantAxis
226
+ );
227
+ if (maintainAspectRatio && rotatedBox.dominantAxis) {
228
+ aspectLockDominantAxis = rotatedBox.dominantAxis;
229
+ }
230
+ newLeft = rotatedBox.left;
231
+ newTop = rotatedBox.top;
232
+ newW = rotatedBox.width;
233
+ newH = rotatedBox.height;
234
+ } else {
235
+ if (dir.includes('e')) newW = Math.max(1, startCSS.width + dx);
236
+ if (dir.includes('s')) newH = Math.max(1, startCSS.height + dy);
237
+ if (dir.includes('w')) {
238
+ newW = Math.max(1, startCSS.width - dx);
239
+ newLeft = startCSS.left + dx;
240
+ }
241
+ if (dir.includes('n')) {
242
+ newH = Math.max(1, startCSS.height - dy);
243
+ newTop = startCSS.top + dy;
244
+ }
245
+ }
246
+
247
+ if (isNoteTarget) {
248
+ const sNote = Math.max(newW, newH);
249
+ newW = sNote;
250
+ newH = sNote;
251
+ if (dir.includes('w')) newLeft = startCSS.left + (startCSS.width - sNote);
252
+ if (dir.includes('n')) newTop = startCSS.top + (startCSS.height - sNote);
253
+ }
254
+
255
+ if (isTextTarget) {
256
+ try {
257
+ const textLayer = (typeof window !== 'undefined') ? window.moodboardHtmlTextLayer : null;
258
+ const el = textLayer && textLayer.idToEl ? textLayer.idToEl.get && textLayer.idToEl.get(id) : null;
259
+ if (el && typeof window.getComputedStyle === 'function') {
260
+ const cs = window.getComputedStyle(el);
261
+ const meas = document.createElement('span');
262
+ meas.style.position = 'absolute';
263
+ meas.style.visibility = 'hidden';
264
+ meas.style.whiteSpace = 'pre';
265
+ meas.style.fontFamily = cs.fontFamily;
266
+ meas.style.fontSize = cs.fontSize;
267
+ meas.style.fontWeight = cs.fontWeight;
268
+ meas.style.fontStyle = cs.fontStyle;
269
+ meas.style.letterSpacing = cs.letterSpacing || 'normal';
270
+ meas.textContent = 'WWW';
271
+ document.body.appendChild(meas);
272
+ const minWidthPx = Math.max(1, Math.ceil(meas.getBoundingClientRect().width));
273
+ meas.remove();
274
+ if (newW < minWidthPx) {
275
+ if (dir.includes('w')) {
276
+ newLeft = startCSS.left + (startCSS.width - minWidthPx);
277
+ }
278
+ newW = minWidthPx;
279
+ }
280
+ }
281
+ } catch (_) {}
282
+ }
283
+
284
+ if (isTextTarget) {
285
+ try {
286
+ const textLayer = (typeof window !== 'undefined') ? window.moodboardHtmlTextLayer : null;
287
+ const el = textLayer && textLayer.idToEl ? textLayer.idToEl.get && textLayer.idToEl.get(id) : null;
288
+ if (el) {
289
+ let minWidthPx = 0;
290
+ try {
291
+ const cs = window.getComputedStyle(el);
292
+ const meas = document.createElement('span');
293
+ meas.style.position = 'absolute';
294
+ meas.style.visibility = 'hidden';
295
+ meas.style.whiteSpace = 'pre';
296
+ meas.style.fontFamily = cs.fontFamily;
297
+ meas.style.fontSize = cs.fontSize;
298
+ meas.style.fontWeight = cs.fontWeight;
299
+ meas.style.fontStyle = cs.fontStyle;
300
+ meas.style.letterSpacing = cs.letterSpacing || 'normal';
301
+ meas.textContent = 'WWW';
302
+ document.body.appendChild(meas);
303
+ minWidthPx = Math.max(1, Math.ceil(meas.getBoundingClientRect().width));
304
+ meas.remove();
305
+ } catch (_) {}
306
+
307
+ if (minWidthPx > 0 && newW < minWidthPx) {
308
+ if (dir.includes('w')) {
309
+ newLeft = startCSS.left + (startCSS.width - minWidthPx);
310
+ }
311
+ newW = minWidthPx;
312
+ }
313
+ el.style.width = `${Math.max(1, Math.round(newW))}px`;
314
+ el.style.height = 'auto';
315
+ const measured = Math.max(1, Math.round(el.scrollHeight));
316
+ newH = measured;
317
+ }
318
+ } catch (_) {}
319
+ }
320
+
321
+ box.style.left = `${Math.round(newLeft)}px`;
322
+ box.style.top = `${Math.round(newTop)}px`;
323
+ box.style.width = `${Math.round(newW)}px`;
324
+ box.style.height = `${Math.round(newH)}px`;
325
+ this.host._repositionBoxChildren(box);
326
+
327
+ const screenX = (newLeft - offsetLeft);
328
+ const screenY = (newTop - offsetTop);
329
+ const screenW = newW;
330
+ const screenH = newH;
331
+ const worldX = ((screenX * rendererRes) - tx) / s;
332
+ const worldY = ((screenY * rendererRes) - ty) / s;
333
+ const worldW = (screenW * rendererRes) / s;
334
+ const worldH = (screenH * rendererRes) / s;
335
+
336
+ if (isGroup) {
337
+ this.host.eventBus.emit(Events.Tool.GroupResizeUpdate, {
338
+ objects,
339
+ startBounds: { ...startWorld },
340
+ newBounds: { x: worldX, y: worldY, width: worldW, height: worldH },
341
+ rotation: startRotation,
342
+ });
343
+ } else {
344
+ let isFrameTarget = false;
345
+ {
346
+ const req = { objectId: id, pixiObject: null };
347
+ this.host.eventBus.emit(Events.Tool.GetObjectPixi, req);
348
+ const mbType = req.pixiObject && req.pixiObject._mb && req.pixiObject._mb.type;
349
+ isFrameTarget = mbType === 'frame';
350
+ }
351
+ const isLeftOrTop = dir.includes('w') || dir.includes('n');
352
+ const resizeData = {
353
+ object: id,
354
+ size: { width: worldW, height: worldH },
355
+ position: isFrameTarget ? null : (isLeftOrTop ? { x: worldX, y: worldY } : { x: startWorld.x, y: startWorld.y }),
356
+ };
357
+ this.host.eventBus.emit(Events.Tool.ResizeUpdate, resizeData);
358
+ }
359
+ };
360
+
361
+ const onUp = () => {
362
+ document.removeEventListener('mousemove', onMove);
363
+ document.removeEventListener('mouseup', onUp);
364
+ const endCSS = {
365
+ left: parseFloat(box.style.left),
366
+ top: parseFloat(box.style.top),
367
+ width: parseFloat(box.style.width),
368
+ height: parseFloat(box.style.height),
369
+ };
370
+ const screenX = (endCSS.left - offsetLeft);
371
+ const screenY = (endCSS.top - offsetTop);
372
+ const screenW = endCSS.width;
373
+ const screenH = endCSS.height;
374
+ const worldX = ((screenX * rendererRes) - tx) / s;
375
+ const worldY = ((screenY * rendererRes) - ty) / s;
376
+ const worldW = (screenW * rendererRes) / s;
377
+ const worldH = (screenH * rendererRes) / s;
378
+
379
+ if (isGroup) {
380
+ this.host.eventBus.emit(Events.Tool.GroupResizeEnd, { objects });
381
+ } else {
382
+ const isEdgeLeftOrTop = dir.includes('w') || dir.includes('n');
383
+ let isFrameTarget = false;
384
+ {
385
+ const req = { objectId: id, pixiObject: null };
386
+ this.host.eventBus.emit(Events.Tool.GetObjectPixi, req);
387
+ const mbType = req.pixiObject && req.pixiObject._mb && req.pixiObject._mb.type;
388
+ isFrameTarget = mbType === 'frame';
389
+ }
390
+ const resizeEndData = {
391
+ object: id,
392
+ oldSize: { width: startWorld.width, height: startWorld.height },
393
+ newSize: { width: worldW, height: worldH },
394
+ oldPosition: { x: startWorld.x, y: startWorld.y },
395
+ newPosition: isFrameTarget ? null : (isEdgeLeftOrTop ? { x: worldX, y: worldY } : { x: startWorld.x, y: startWorld.y }),
396
+ };
397
+ this.host.eventBus.emit(Events.Tool.ResizeEnd, resizeEndData);
398
+ try {
399
+ const req2 = { objectId: id, pixiObject: null };
400
+ this.host.eventBus.emit(Events.Tool.GetObjectPixi, req2);
401
+ const mbType2 = req2.pixiObject && req2.pixiObject._mb && req2.pixiObject._mb.type;
402
+ if (mbType2 === 'text' || mbType2 === 'simple-text') {
403
+ const textLayer = (typeof window !== 'undefined') ? window.moodboardHtmlTextLayer : null;
404
+ const el = textLayer && textLayer.idToEl ? textLayer.idToEl.get && textLayer.idToEl.get(id) : null;
405
+ if (el) {
406
+ el.style.width = `${Math.max(1, Math.round(endCSS.width))}px`;
407
+ el.style.height = 'auto';
408
+ const measured = Math.max(1, Math.round(el.scrollHeight));
409
+ const worldH2 = (measured * rendererRes) / s;
410
+ const fixData = {
411
+ object: id,
412
+ size: { width: worldW, height: worldH2 },
413
+ position: isFrameTarget ? null : (isEdgeLeftOrTop ? { x: worldX, y: worldY } : { x: startWorld.x, y: startWorld.y }),
414
+ };
415
+ this.host.eventBus.emit(Events.Tool.ResizeUpdate, fixData);
416
+ }
417
+ }
418
+ } catch (_) {}
419
+ }
420
+ };
421
+
422
+ document.addEventListener('mousemove', onMove);
423
+ document.addEventListener('mouseup', onUp);
424
+ }
425
+
426
+ onEdgeResizeDown(e) {
427
+ e.preventDefault();
428
+ e.stopPropagation();
429
+ const id = e.currentTarget.dataset.id;
430
+ const isGroup = id === '__group__';
431
+ const edge = e.currentTarget.dataset.edge;
432
+ const world = this.host.core.pixi.worldLayer || this.host.core.pixi.app.stage;
433
+ const s = world?.scale?.x || 1;
434
+ const tx = world?.x || 0;
435
+ const ty = world?.y || 0;
436
+ const rendererRes = (this.host.core.pixi.app.renderer?.resolution) || 1;
437
+ const containerRect = this.host.container.getBoundingClientRect();
438
+ const view = this.host.core.pixi.app.view;
439
+ const viewRect = view.getBoundingClientRect();
440
+ const offsetLeft = viewRect.left - containerRect.left;
441
+ const offsetTop = viewRect.top - containerRect.top;
442
+
443
+ const box = e.currentTarget.parentElement;
444
+ let startCSS = this._readBoxCss(box);
445
+ let startWorld = this._cssRectToWorld(startCSS, offsetLeft, offsetTop, rendererRes, tx, ty, s);
446
+
447
+ let objects = [id];
448
+ if (isGroup) {
449
+ const req = { selection: [] };
450
+ this.host.eventBus.emit(Events.Tool.GetSelection, req);
451
+ objects = req.selection || [];
452
+ this.host.eventBus.emit(Events.Tool.GroupResizeStart, { objects, startBounds: { ...startWorld } });
453
+ } else {
454
+ this.host.eventBus.emit(Events.Tool.ResizeStart, { object: id, handle: edge === 'top' ? 'n' : edge === 'bottom' ? 's' : edge === 'left' ? 'w' : 'e' });
455
+ }
456
+
457
+ let startMouse = { x: e.clientX, y: e.clientY };
458
+ const startRotation = isGroup ? this._parseBoxRotation(box) : 0;
459
+ let previousMaintainAspectRatio = !!e.shiftKey;
460
+ let aspectLockDominantAxis = null;
461
+ const edgeHandleType = edge === 'top' ? 'n' : edge === 'bottom' ? 's' : edge === 'left' ? 'w' : 'e';
462
+ let isTextTarget = false;
463
+ let isNoteTarget = false;
464
+ {
465
+ const req = { objectId: id, pixiObject: null };
466
+ this.host.eventBus.emit(Events.Tool.GetObjectPixi, req);
467
+ const mbType = req.pixiObject && req.pixiObject._mb && req.pixiObject._mb.type;
468
+ isTextTarget = (mbType === 'text' || mbType === 'simple-text');
469
+ isNoteTarget = (mbType === 'note');
470
+ }
471
+
472
+ const onMove = (ev) => {
473
+ const maintainAspectRatio = !!ev.shiftKey;
474
+ if (isGroup && maintainAspectRatio !== previousMaintainAspectRatio) {
475
+ startCSS = this._readBoxCss(box);
476
+ startWorld = this._cssRectToWorld(startCSS, offsetLeft, offsetTop, rendererRes, tx, ty, s);
477
+ startMouse = { x: ev.clientX, y: ev.clientY };
478
+ previousMaintainAspectRatio = maintainAspectRatio;
479
+ aspectLockDominantAxis = null;
480
+ this.host.eventBus.emit(Events.Tool.GroupResizeStart, {
481
+ objects,
482
+ startBounds: { ...startWorld },
483
+ });
484
+ return;
485
+ }
486
+
487
+ const dxCSS = ev.clientX - startMouse.x;
488
+ const dyCSS = ev.clientY - startMouse.y;
489
+ let newLeft = startCSS.left;
490
+ let newTop = startCSS.top;
491
+ let newW = startCSS.width;
492
+ let newH = startCSS.height;
493
+ if (isGroup && Math.abs(startRotation) > 0.001) {
494
+ const rotatedBox = this._computeRotatedResizeBox(
495
+ startCSS,
496
+ this._cssPointFromClient(ev.clientX, ev.clientY),
497
+ startRotation,
498
+ edgeHandleType,
499
+ maintainAspectRatio,
500
+ aspectLockDominantAxis
501
+ );
502
+ if (maintainAspectRatio && rotatedBox.dominantAxis) {
503
+ aspectLockDominantAxis = rotatedBox.dominantAxis;
504
+ }
505
+ newLeft = rotatedBox.left;
506
+ newTop = rotatedBox.top;
507
+ newW = rotatedBox.width;
508
+ newH = rotatedBox.height;
509
+ } else {
510
+ if (edge === 'right') newW = Math.max(1, startCSS.width + dxCSS);
511
+ if (edge === 'bottom') newH = Math.max(1, startCSS.height + dyCSS);
512
+ if (edge === 'left') {
513
+ newW = Math.max(1, startCSS.width - dxCSS);
514
+ newLeft = startCSS.left + dxCSS;
515
+ }
516
+ if (edge === 'top') {
517
+ newH = Math.max(1, startCSS.height - dyCSS);
518
+ newTop = startCSS.top + dyCSS;
519
+ }
520
+ }
521
+
522
+ if (isNoteTarget) {
523
+ const sNote = Math.max(newW, newH);
524
+ switch (edge) {
525
+ case 'right':
526
+ newW = sNote;
527
+ newH = sNote;
528
+ newTop = startCSS.top + Math.round((startCSS.height - sNote) / 2);
529
+ break;
530
+ case 'left':
531
+ newW = sNote;
532
+ newH = sNote;
533
+ newLeft = startCSS.left + (startCSS.width - sNote);
534
+ newTop = startCSS.top + Math.round((startCSS.height - sNote) / 2);
535
+ break;
536
+ case 'bottom':
537
+ newW = sNote;
538
+ newH = sNote;
539
+ newLeft = startCSS.left + Math.round((startCSS.width - sNote) / 2);
540
+ break;
541
+ case 'top':
542
+ newW = sNote;
543
+ newH = sNote;
544
+ newTop = startCSS.top + (startCSS.height - sNote);
545
+ newLeft = startCSS.left + Math.round((startCSS.width - sNote) / 2);
546
+ break;
547
+ }
548
+ }
549
+
550
+ if (isTextTarget) {
551
+ try {
552
+ const textLayer = (typeof window !== 'undefined') ? window.moodboardHtmlTextLayer : null;
553
+ const el = textLayer && textLayer.idToEl ? textLayer.idToEl.get && textLayer.idToEl.get(id) : null;
554
+ if (el && typeof window.getComputedStyle === 'function') {
555
+ const cs = window.getComputedStyle(el);
556
+ const meas = document.createElement('span');
557
+ meas.style.position = 'absolute';
558
+ meas.style.visibility = 'hidden';
559
+ meas.style.whiteSpace = 'pre';
560
+ meas.style.fontFamily = cs.fontFamily;
561
+ meas.style.fontSize = cs.fontSize;
562
+ meas.style.fontWeight = cs.fontWeight;
563
+ meas.style.fontStyle = cs.fontStyle;
564
+ meas.style.letterSpacing = cs.letterSpacing || 'normal';
565
+ meas.textContent = 'WWW';
566
+ document.body.appendChild(meas);
567
+ const minWidthPx = Math.max(1, Math.ceil(meas.getBoundingClientRect().width));
568
+ meas.remove();
569
+ if (newW < minWidthPx) {
570
+ if (edge === 'left') {
571
+ newLeft = startCSS.left + (startCSS.width - minWidthPx);
572
+ }
573
+ newW = minWidthPx;
574
+ }
575
+ }
576
+ } catch (_) {}
577
+ }
578
+
579
+ const widthChanged = (edge === 'left' || edge === 'right');
580
+ if (isTextTarget && widthChanged) {
581
+ try {
582
+ const textLayer = (typeof window !== 'undefined') ? window.moodboardHtmlTextLayer : null;
583
+ const el = textLayer && textLayer.idToEl ? textLayer.idToEl.get && textLayer.idToEl.get(id) : null;
584
+ if (el) {
585
+ el.style.width = `${Math.max(1, Math.round(newW))}px`;
586
+ el.style.height = 'auto';
587
+ const measured = Math.max(1, Math.round(el.scrollHeight));
588
+ newH = measured;
589
+ }
590
+ } catch (_) {}
591
+ }
592
+
593
+ box.style.left = `${newLeft}px`;
594
+ box.style.top = `${newTop}px`;
595
+ box.style.width = `${newW}px`;
596
+ box.style.height = `${newH}px`;
597
+ this.host._repositionBoxChildren(box);
598
+
599
+ const screenX = (newLeft - offsetLeft);
600
+ const screenY = (newTop - offsetTop);
601
+ const screenW = newW;
602
+ const screenH = newH;
603
+ const worldX = ((screenX * rendererRes) - tx) / s;
604
+ const worldY = ((screenY * rendererRes) - ty) / s;
605
+ const worldW = (screenW * rendererRes) / s;
606
+ const worldH = (screenH * rendererRes) / s;
607
+ const edgePositionChanged = (newLeft !== startCSS.left) || (newTop !== startCSS.top);
608
+
609
+ if (isGroup) {
610
+ this.host.eventBus.emit(Events.Tool.GroupResizeUpdate, {
611
+ objects,
612
+ startBounds: { ...startWorld },
613
+ newBounds: { x: worldX, y: worldY, width: worldW, height: worldH },
614
+ rotation: startRotation,
615
+ });
616
+ } else {
617
+ const edgeResizeData = {
618
+ object: id,
619
+ size: { width: worldW, height: worldH },
620
+ position: edgePositionChanged ? { x: worldX, y: worldY } : { x: startWorld.x, y: startWorld.y },
621
+ };
622
+ this.host.eventBus.emit(Events.Tool.ResizeUpdate, edgeResizeData);
623
+ }
624
+ };
625
+
626
+ const onUp = () => {
627
+ document.removeEventListener('mousemove', onMove);
628
+ document.removeEventListener('mouseup', onUp);
629
+ const endCSS = {
630
+ left: parseFloat(box.style.left),
631
+ top: parseFloat(box.style.top),
632
+ width: parseFloat(box.style.width),
633
+ height: parseFloat(box.style.height),
634
+ };
635
+ const screenX = (endCSS.left - offsetLeft);
636
+ const screenY = (endCSS.top - offsetTop);
637
+ const screenW = endCSS.width;
638
+ const screenH = endCSS.height;
639
+ const worldX = ((screenX * rendererRes) - tx) / s;
640
+ const worldY = ((screenY * rendererRes) - ty) / s;
641
+ const worldW = (screenW * rendererRes) / s;
642
+ const worldH = (screenH * rendererRes) / s;
643
+
644
+ if (isGroup) {
645
+ this.host.eventBus.emit(Events.Tool.GroupResizeEnd, { objects });
646
+ } else {
647
+ const edgeFinalPositionChanged = (endCSS.left !== startCSS.left) || (endCSS.top !== startCSS.top);
648
+ let finalWorldH = worldH;
649
+ if (isTextTarget && (edge === 'left' || edge === 'right')) {
650
+ try {
651
+ const textLayer = (typeof window !== 'undefined') ? window.moodboardHtmlTextLayer : null;
652
+ const el = textLayer && textLayer.idToEl ? textLayer.idToEl.get && textLayer.idToEl.get(id) : null;
653
+ if (el) {
654
+ el.style.width = `${Math.max(1, Math.round(endCSS.width))}px`;
655
+ el.style.height = 'auto';
656
+ const measured = Math.max(1, Math.round(el.scrollHeight));
657
+ finalWorldH = (measured * rendererRes) / s;
658
+ }
659
+ } catch (_) {}
660
+ }
661
+
662
+ const edgeResizeEndData = {
663
+ object: id,
664
+ oldSize: { width: startWorld.width, height: startWorld.height },
665
+ newSize: { width: worldW, height: finalWorldH },
666
+ oldPosition: { x: startWorld.x, y: startWorld.y },
667
+ newPosition: edgeFinalPositionChanged ? { x: worldX, y: worldY } : { x: startWorld.x, y: startWorld.y },
668
+ };
669
+ this.host.eventBus.emit(Events.Tool.ResizeEnd, edgeResizeEndData);
670
+ }
671
+ };
672
+
673
+ document.addEventListener('mousemove', onMove);
674
+ document.addEventListener('mouseup', onUp);
675
+ }
676
+
677
+ onRotateHandleDown(e, box) {
678
+ e.preventDefault();
679
+ e.stopPropagation();
680
+
681
+ const handleElement = e.currentTarget;
682
+ const id = handleElement?.dataset?.id;
683
+ if (!id) return;
684
+ const isGroup = id === '__group__';
685
+
686
+ const boxLeft = parseFloat(box.style.left);
687
+ const boxTop = parseFloat(box.style.top);
688
+ const boxWidth = parseFloat(box.style.width);
689
+ const boxHeight = parseFloat(box.style.height);
690
+ const centerX = boxLeft + boxWidth / 2;
691
+ const centerY = boxTop + boxHeight / 2;
692
+ const startAngle = Math.atan2(e.clientY - centerY, e.clientX - centerX);
693
+
694
+ let startRotation = 0;
695
+ if (!isGroup) {
696
+ const rotationData = { objectId: id, rotation: 0 };
697
+ this.host.eventBus.emit(Events.Tool.GetObjectRotation, rotationData);
698
+ startRotation = (rotationData.rotation || 0) * Math.PI / 180;
699
+ }
700
+
701
+ if (handleElement) {
702
+ handleElement.style.cursor = 'grabbing';
703
+ }
704
+
705
+ if (isGroup) {
706
+ const req = { selection: [] };
707
+ this.host.eventBus.emit(Events.Tool.GetSelection, req);
708
+ const objects = req.selection || [];
709
+ let centerWorldX = centerX;
710
+ let centerWorldY = centerY;
711
+ try {
712
+ const centerWorld = this.host.positioningService.cssPointToWorld(centerX, centerY);
713
+ centerWorldX = centerWorld.x;
714
+ centerWorldY = centerWorld.y;
715
+ } catch (_) {}
716
+ this.host.eventBus.emit(Events.Tool.GroupRotateStart, {
717
+ objects,
718
+ center: { x: centerWorldX, y: centerWorldY },
719
+ });
720
+ }
721
+
722
+ const onRotateMove = (ev) => {
723
+ const currentAngle = Math.atan2(ev.clientY - centerY, ev.clientX - centerX);
724
+ const deltaAngle = currentAngle - startAngle;
725
+ const newRotation = startRotation + deltaAngle;
726
+
727
+ if (isGroup) {
728
+ const req = { selection: [] };
729
+ this.host.eventBus.emit(Events.Tool.GetSelection, req);
730
+ const objects = req.selection || [];
731
+ this.host.eventBus.emit(Events.Tool.GroupRotateUpdate, {
732
+ objects,
733
+ angle: newRotation * 180 / Math.PI,
734
+ });
735
+ } else {
736
+ this.host.eventBus.emit(Events.Tool.RotateUpdate, {
737
+ object: id,
738
+ angle: newRotation * 180 / Math.PI,
739
+ });
740
+ }
741
+ };
742
+
743
+ const onRotateUp = (ev) => {
744
+ document.removeEventListener('mousemove', onRotateMove);
745
+ document.removeEventListener('mouseup', onRotateUp);
746
+
747
+ if (handleElement) {
748
+ handleElement.style.cursor = 'grab';
749
+ }
750
+
751
+ const finalAngle = Math.atan2(ev.clientY - centerY, ev.clientX - centerX);
752
+ const finalDeltaAngle = finalAngle - startAngle;
753
+ const finalRotation = startRotation + finalDeltaAngle;
754
+
755
+ if (isGroup) {
756
+ const req = { selection: [] };
757
+ this.host.eventBus.emit(Events.Tool.GetSelection, req);
758
+ const objects = req.selection || [];
759
+ this.host.eventBus.emit(Events.Tool.GroupRotateEnd, { objects });
760
+ } else {
761
+ this.host.eventBus.emit(Events.Tool.RotateEnd, {
762
+ object: id,
763
+ oldAngle: startRotation * 180 / Math.PI,
764
+ newAngle: finalRotation * 180 / Math.PI,
765
+ });
766
+ }
767
+ };
768
+
769
+ document.addEventListener('mousemove', onRotateMove);
770
+ document.addEventListener('mouseup', onRotateUp);
771
+ }
772
+ }