@sequent-org/moodboard 1.2.119 → 1.3.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 (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 +82 -1181
  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 +665 -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,466 @@
1
+ import { Events } from '../../../core/events/Events.js';
2
+ import { tryStartAltCloneDuringDrag, resetCloneStateAfterDragEnd } from './CloneFlowController.js';
3
+
4
+ export function handleObjectSelect(objectId, event) {
5
+ if (!this.isMultiSelect) {
6
+ this.clearSelection();
7
+ }
8
+
9
+ if (this.selection.has(objectId)) {
10
+ if (this.isMultiSelect) {
11
+ this.removeFromSelection(objectId);
12
+ } else if (this.selection.size() > 1) {
13
+ // Перетаскивание группы
14
+ this.startGroupDrag(event);
15
+ } else {
16
+ // Начинаем перетаскивание
17
+ this.startDrag(objectId, event);
18
+ }
19
+ } else {
20
+ this.addToSelection(objectId);
21
+ if (this.selection.size() > 1) {
22
+ this.startGroupDrag(event);
23
+ } else {
24
+ this.startDrag(objectId, event);
25
+ }
26
+ }
27
+ }
28
+
29
+ export function startDrag(objectId, event) {
30
+ this.isDragging = true;
31
+ this.dragTarget = objectId;
32
+ // Сообщаем HtmlHandlesLayer о начале перетаскивания одиночного объекта
33
+ this.emit(Events.Tool.DragStart, { object: objectId });
34
+
35
+ // Получаем текущую позицию объекта
36
+ const objectData = { objectId, position: null };
37
+ this.emit(Events.Tool.GetObjectPosition, objectData);
38
+ // Нормализуем координаты в мировые (worldLayer), чтобы убрать влияние зума
39
+ const w = this._toWorld(event.x, event.y);
40
+ // Запоминаем смещение точки захвата курсора относительно левого-верхнего угла объекта (в мировых координатах)
41
+ if (objectData.position) {
42
+ this._dragGrabOffset = {
43
+ x: w.x - objectData.position.x,
44
+ y: w.y - objectData.position.y
45
+ };
46
+ } else {
47
+ this._dragGrabOffset = null;
48
+ }
49
+ const worldEvent = { ...event, x: w.x, y: w.y };
50
+ if (this._dragCtrl) this._dragCtrl.start(objectId, worldEvent);
51
+ }
52
+
53
+ export function updateDrag(event) {
54
+ // Перетаскивание группы
55
+ if (this.isGroupDragging && this._groupDragCtrl) {
56
+ const w = this._toWorld(event.x, event.y);
57
+ this._groupDragCtrl.update({ ...event, x: w.x, y: w.y });
58
+ return;
59
+ }
60
+ tryStartAltCloneDuringDrag.call(this, event);
61
+ // Если ожидаем создание копии — продолжаем двигать текущую цель (исходник)
62
+ if (!this.dragTarget) return;
63
+
64
+ if (this._dragCtrl) {
65
+ const w = this._toWorld(event.x, event.y);
66
+ this._dragCtrl.update({ ...event, x: w.x, y: w.y });
67
+ }
68
+ // Обновление позиции в ядро уже выполняется через SimpleDragController (drag:update)
69
+ // Дополнительный эмит здесь не нужен и приводил к некорректным данным
70
+
71
+ // Обновляем ручки во время перетаскивания
72
+ if (this.resizeHandles && this.selection.has(this.dragTarget)) {
73
+ this.resizeHandles.updateHandles();
74
+ }
75
+ }
76
+
77
+ export function endDrag() {
78
+ if (this.isGroupDragging) {
79
+ const ids = this.selection.toArray();
80
+ this.emit(Events.Tool.GroupDragEnd, { objects: ids });
81
+ if (this._groupDragCtrl) this._groupDragCtrl.end();
82
+ } else if (this.dragTarget) {
83
+ if (this._dragCtrl) this._dragCtrl.end();
84
+ // Сообщаем о завершении перетаскивания одиночного объекта
85
+ this.emit(Events.Tool.DragEnd, { object: this.dragTarget });
86
+ }
87
+
88
+ this.isDragging = false;
89
+ this.isGroupDragging = false;
90
+ this.dragTarget = null;
91
+ this.dragOffset = { x: 0, y: 0 };
92
+ resetCloneStateAfterDragEnd.call(this);
93
+ }
94
+
95
+ export function startResize(handle, objectId) {
96
+ // Групповой resize
97
+ if (objectId === this.groupId && this.selection.size() > 1) {
98
+ this.isGroupResizing = true;
99
+ this.resizeHandle = handle;
100
+ if (this._groupResizeCtrl) this._groupResizeCtrl.start(handle, { x: this.currentX, y: this.currentY });
101
+ this.isResizing = false;
102
+ return;
103
+ }
104
+
105
+ this.isResizing = true;
106
+ this.resizeHandle = handle;
107
+ this.dragTarget = objectId;
108
+ if (this._resizeCtrl) {
109
+ const w = this._toWorld(this.currentX, this.currentY);
110
+ this._resizeCtrl.start(handle, objectId, { x: w.x, y: w.y });
111
+ }
112
+ }
113
+
114
+ export function updateResize(event) {
115
+ // Групповой resize
116
+ if (this.isGroupResizing && this._groupResizeCtrl) {
117
+ const w = this._toWorld(event.x, event.y);
118
+ this._groupResizeCtrl.update({ ...event, x: w.x, y: w.y });
119
+ return;
120
+ }
121
+
122
+ if (this._resizeCtrl) {
123
+ const w = this._toWorld(event.x, event.y);
124
+ this._resizeCtrl.update({ ...event, x: w.x, y: w.y }, {
125
+ calculateNewSize: (handleType, startBounds, dx, dy, keepAR) => {
126
+ const rot = (() => { const d = { objectId: this.dragTarget, rotation: 0 }; this.emit(Events.Tool.GetObjectRotation, d); return d.rotation || 0; })();
127
+ return this.calculateNewSize(handleType, startBounds, dx, dy, keepAR, rot);
128
+ },
129
+ calculatePositionOffset: (handleType, startBounds, newSize, objectRotation) => {
130
+ return this.calculatePositionOffset(handleType, startBounds, newSize, objectRotation);
131
+ }
132
+ });
133
+ }
134
+
135
+ // Обновляем ручки в реальном времени во время resize
136
+ // HTML-ручки обновляются слоем HtmlHandlesLayer
137
+ }
138
+
139
+ export function endResize() {
140
+ if (this.isGroupResizing) {
141
+ if (this._groupResizeCtrl) this._groupResizeCtrl.end();
142
+ this.isGroupResizing = false;
143
+ this.resizeHandle = null;
144
+ this.groupStartBounds = null;
145
+ this.groupStartMouse = null;
146
+ this.groupObjectsInitial = null;
147
+ // Принудительно синхронизируем ручки и рамку после завершения, чтобы отлипли от курсора
148
+ const gb = this.computeGroupBounds();
149
+ this.ensureGroupBoundsGraphics(gb);
150
+ if (this.groupBoundsGraphics) {
151
+ this.groupBoundsGraphics.rotation = 0;
152
+ this.groupBoundsGraphics.pivot.set(0, 0);
153
+ this.groupBoundsGraphics.position.set(gb.x, gb.y);
154
+ }
155
+ if (this.resizeHandles) {
156
+ // Отключаем старые PIXI-ручки
157
+ this.resizeHandles.hideHandles();
158
+ }
159
+ return;
160
+ }
161
+ if (this._resizeCtrl) this._resizeCtrl.end();
162
+
163
+ // Обновляем позицию ручек после resize
164
+ // HTML-ручки обновляются слоем HtmlHandlesLayer
165
+
166
+ this.isResizing = false;
167
+ this.resizeHandle = null;
168
+ this.resizeStartBounds = null;
169
+ this.resizeStartMousePos = null;
170
+ this.resizeStartPosition = null;
171
+ }
172
+
173
+ export function startRotate(objectId) {
174
+ // Групповой поворот
175
+ if (objectId === this.groupId && this.selection.size() > 1) {
176
+ this.isGroupRotating = true;
177
+ const gb = this.computeGroupBounds();
178
+ this.groupRotateBounds = gb;
179
+ this.rotateCenter = { x: gb.x + gb.width / 2, y: gb.y + gb.height / 2 };
180
+ this.rotateStartAngle = 0;
181
+ this.rotateCurrentAngle = 0;
182
+ this.rotateStartMouseAngle = Math.atan2(
183
+ this.currentY - this.rotateCenter.y,
184
+ this.currentX - this.rotateCenter.x
185
+ );
186
+ // Настраиваем целевой прямоугольник для ручек: центр в pivot для корректного вращения
187
+ this.ensureGroupBoundsGraphics(gb);
188
+ if (this.groupBoundsGraphics) {
189
+ this.groupBoundsGraphics.pivot.set(gb.width / 2, gb.height / 2);
190
+ this.groupBoundsGraphics.position.set(this.rotateCenter.x, this.rotateCenter.y);
191
+ this.groupBoundsGraphics.rotation = 0;
192
+ }
193
+ // Подгоняем визуальную рамку под центр
194
+ if (this.groupSelectionGraphics) {
195
+ this.groupSelectionGraphics.pivot.set(0, 0);
196
+ this.groupSelectionGraphics.position.set(0, 0);
197
+ this.groupSelectionGraphics.clear();
198
+ this.groupSelectionGraphics.lineStyle(1, 0x3B82F6, 1);
199
+ // Нарисуем пока осевую рамку, вращение применим в update
200
+ this.groupSelectionGraphics.drawRect(gb.x, gb.y, gb.width, gb.height);
201
+ }
202
+ const ids = this.selection.toArray();
203
+ this.emit('group:rotate:start', { objects: ids, center: this.rotateCenter });
204
+ return;
205
+ }
206
+
207
+ this.isRotating = true;
208
+ this.dragTarget = objectId; // Используем dragTarget для совместимости
209
+ const posData = { objectId, position: null };
210
+ this.emit('get:object:position', posData);
211
+ const sizeData = { objectId, size: null };
212
+ this.emit('get:object:size', sizeData);
213
+ if (posData.position && sizeData.size && this._rotateCtrl) {
214
+ const center = { x: posData.position.x + sizeData.size.width / 2, y: posData.position.y + sizeData.size.height / 2 };
215
+ const w = this._toWorld(this.currentX, this.currentY);
216
+ this._rotateCtrl.start(objectId, { x: w.x, y: w.y }, center);
217
+ }
218
+ }
219
+
220
+ export function updateRotate(event) {
221
+ // Групповой поворот
222
+ if (this.isGroupRotating && this._groupRotateCtrl) {
223
+ const w = this._toWorld(event.x, event.y);
224
+ this._groupRotateCtrl.update({ ...event, x: w.x, y: w.y });
225
+ return;
226
+ }
227
+ if (!this.isRotating || !this._rotateCtrl) return;
228
+ {
229
+ const w = this._toWorld(event.x, event.y);
230
+ this._rotateCtrl.update({ ...event, x: w.x, y: w.y });
231
+ }
232
+
233
+ // Обновляем ручки в реальном времени во время поворота
234
+ // HTML-ручки обновляются слоем HtmlHandlesLayer
235
+ }
236
+
237
+ export function endRotate() {
238
+ if (this.isGroupRotating) {
239
+ if (this._groupRotateCtrl) this._groupRotateCtrl.end();
240
+ this.isGroupRotating = false;
241
+ // Восстановление рамки
242
+ const gb = this.computeGroupBounds();
243
+ this.ensureGroupBoundsGraphics(gb);
244
+ if (this.groupBoundsGraphics) {
245
+ this.groupBoundsGraphics.rotation = 0;
246
+ this.groupBoundsGraphics.pivot.set(0, 0);
247
+ this.groupBoundsGraphics.position.set(gb.x, gb.y);
248
+ }
249
+ if (this.resizeHandles) this.resizeHandles.hideHandles();
250
+ return;
251
+ }
252
+ if (this._rotateCtrl) this._rotateCtrl.end();
253
+
254
+ // Обновляем позицию ручек после поворота
255
+ if (this.resizeHandles) {
256
+ this.resizeHandles.updateHandles(); // Обновляем позицию ручек
257
+ }
258
+
259
+ this.isRotating = false;
260
+ this.rotateCenter = null;
261
+ this.rotateStartAngle = 0;
262
+ this.rotateCurrentAngle = 0;
263
+ this.rotateStartMouseAngle = 0;
264
+ }
265
+
266
+ export function startBoxSelect(event) {
267
+ this.isBoxSelect = true;
268
+ if (this._boxSelect) this._boxSelect.start({ x: event.x, y: event.y }, this.isMultiSelect);
269
+ }
270
+
271
+ export function updateBoxSelect(event) {
272
+ if (this._boxSelect) this._boxSelect.update({ x: event.x, y: event.y });
273
+ }
274
+
275
+ export function endBoxSelect() {
276
+ this.isBoxSelect = false;
277
+ if (this._boxSelect) this._boxSelect.end();
278
+ }
279
+
280
+ export function startGroupDrag(event) {
281
+ const gb = this.computeGroupBounds();
282
+ this.groupStartBounds = gb;
283
+ this.isGroupDragging = true;
284
+ this.isDragging = false; // отключаем одиночный drag, если был
285
+ this.ensureGroupBoundsGraphics(gb);
286
+ if (this.groupBoundsGraphics && this.resizeHandles) {
287
+ this.resizeHandles.hideHandles();
288
+ }
289
+ if (this._groupDragCtrl) {
290
+ const w = this._toWorld(event.x, event.y);
291
+ this._groupDragCtrl.start(gb, { x: w.x, y: w.y });
292
+ }
293
+ this.emit(Events.Tool.GroupDragStart, { objects: this.selection.toArray() });
294
+ }
295
+
296
+ export function prepareAltCloneDrag(objectId, event) {
297
+ // Очищаем текущее выделение и выделяем исходный объект
298
+ this.clearSelection();
299
+ this.addToSelection(objectId);
300
+
301
+ // Включаем режим Alt-клона и запрашиваем дубликат у ядра
302
+ this.isAltCloneMode = true;
303
+ this.clonePending = true;
304
+ this.cloneSourceId = objectId;
305
+
306
+ // Сохраняем текущее положение курсора
307
+ this.currentX = event.x;
308
+ this.currentY = event.y;
309
+
310
+ // Запрашиваем текущую позицию исходного объекта
311
+ const positionData = { objectId, position: null };
312
+ this.emit('get:object:position', positionData);
313
+
314
+ // Сообщаем ядру о необходимости создать дубликат у позиции исходного объекта
315
+ this.emit('duplicate:request', {
316
+ originalId: objectId,
317
+ position: positionData.position || { x: event.x, y: event.y }
318
+ });
319
+
320
+ // Помечаем, что находимся в состоянии drag, но цели пока нет — ждём newId
321
+ this.isDragging = true;
322
+ this.dragTarget = null;
323
+ }
324
+
325
+ export function transformHandleType(handleType, rotationDegrees) {
326
+ // Нормализуем угол поворота к диапазону 0-360
327
+ let angle = rotationDegrees % 360;
328
+ if (angle < 0) angle += 360;
329
+
330
+ // Определяем количество поворотов на 90 градусов
331
+ const rotations = Math.round(angle / 90) % 4;
332
+
333
+ if (rotations === 0) return handleType; // Нет поворота
334
+
335
+ // Карта преобразований для каждого поворота на 90°
336
+ const transformMap = {
337
+ 'nw': ['ne', 'se', 'sw', 'nw'], // nw -> ne -> se -> sw -> nw
338
+ 'n': ['e', 's', 'w', 'n'], // n -> e -> s -> w -> n
339
+ 'ne': ['se', 'sw', 'nw', 'ne'], // ne -> se -> sw -> nw -> ne
340
+ 'e': ['s', 'w', 'n', 'e'], // e -> s -> w -> n -> e
341
+ 'se': ['sw', 'nw', 'ne', 'se'], // se -> sw -> nw -> ne -> se
342
+ 's': ['w', 'n', 'e', 's'], // s -> w -> n -> e -> s
343
+ 'sw': ['nw', 'ne', 'se', 'sw'], // sw -> nw -> ne -> se -> sw
344
+ 'w': ['n', 'e', 's', 'w'] // w -> n -> e -> s -> w
345
+ };
346
+
347
+ return transformMap[handleType] ? transformMap[handleType][rotations - 1] : handleType;
348
+ }
349
+
350
+ export function calculateNewSize(handleType, startBounds, deltaX, deltaY, maintainAspectRatio) {
351
+ let newWidth = startBounds.width;
352
+ let newHeight = startBounds.height;
353
+
354
+ // Получаем угол поворота объекта
355
+ const rotationData = { objectId: this.dragTarget, rotation: 0 };
356
+ this.emit('get:object:rotation', rotationData);
357
+ const objectRotation = rotationData.rotation || 0;
358
+
359
+ // Преобразуем тип ручки с учетом поворота объекта
360
+ const transformedHandleType = this.transformHandleType(handleType, objectRotation);
361
+
362
+ // Вычисляем изменения в зависимости от преобразованного типа ручки
363
+ switch (transformedHandleType) {
364
+ case 'nw': // Северо-запад - левый верхний угол
365
+ newWidth = startBounds.width - deltaX; // влево = меньше ширина
366
+ newHeight = startBounds.height - deltaY; // вверх = меньше высота
367
+ break;
368
+ case 'n': // Север - верхняя сторона
369
+ newHeight = startBounds.height - deltaY; // вверх = меньше высота
370
+ break;
371
+ case 'ne': // Северо-восток - правый верхний угол
372
+ newWidth = startBounds.width + deltaX; // вправо = больше ширина
373
+ newHeight = startBounds.height - deltaY; // вверх = меньше высота
374
+ break;
375
+ case 'e': // Восток - правая сторона
376
+ newWidth = startBounds.width + deltaX; // вправо = больше ширина
377
+ break;
378
+ case 'se': // Юго-восток - правый нижний угол
379
+ newWidth = startBounds.width + deltaX; // вправо = больше ширина
380
+ newHeight = startBounds.height + deltaY; // вниз = больше высота
381
+ break;
382
+ case 's': // Юг - нижняя сторона
383
+ newHeight = startBounds.height + deltaY; // вниз = больше высота
384
+ break;
385
+ case 'sw': // Юго-запад - левый нижний угол
386
+ newWidth = startBounds.width - deltaX; // влево = меньше ширина
387
+ newHeight = startBounds.height + deltaY; // вниз = больше высота
388
+ break;
389
+ case 'w': // Запад - левая сторона
390
+ newWidth = startBounds.width - deltaX; // влево = меньше ширина
391
+ break;
392
+ }
393
+
394
+ // Поддержка пропорционального изменения размера (Shift)
395
+ if (maintainAspectRatio) {
396
+ const aspectRatio = startBounds.width / startBounds.height;
397
+
398
+ // Определяем, какую сторону использовать как основную
399
+ if (['nw', 'ne', 'sw', 'se'].includes(handleType)) {
400
+ // Угловые ручки - используем большее изменение
401
+ const widthChange = Math.abs(newWidth - startBounds.width);
402
+ const heightChange = Math.abs(newHeight - startBounds.height);
403
+
404
+ if (widthChange > heightChange) {
405
+ newHeight = newWidth / aspectRatio;
406
+ } else {
407
+ newWidth = newHeight * aspectRatio;
408
+ }
409
+ } else if (['e', 'w'].includes(handleType)) {
410
+ // Горизонтальные ручки
411
+ newHeight = newWidth / aspectRatio;
412
+ } else if (['n', 's'].includes(handleType)) {
413
+ // Вертикальные ручки
414
+ newWidth = newHeight * aspectRatio;
415
+ }
416
+ }
417
+
418
+ return {
419
+ width: Math.round(newWidth),
420
+ height: Math.round(newHeight)
421
+ };
422
+ }
423
+
424
+ export function calculatePositionOffset(handleType, startBounds, newSize) {
425
+ // Позиция в состоянии — левый верх. Для правых/нижних ручек топ-лев остается на месте.
426
+ // Для левых/верхних ручек топ-лев должен смещаться на полную величину изменения размера.
427
+ // deltaWidth/deltaHeight = изменение размера (может быть отрицательным при уменьшении)
428
+
429
+ const deltaWidth = newSize.width - startBounds.width;
430
+ const deltaHeight = newSize.height - startBounds.height;
431
+
432
+ let offsetX = 0;
433
+ let offsetY = 0;
434
+
435
+ switch (handleType) {
436
+ case 'nw':
437
+ offsetX = -deltaWidth; // левый край смещается на полную величину изменения ширины
438
+ offsetY = -deltaHeight; // верхний край смещается на полную величину изменения высоты
439
+ break;
440
+ case 'n':
441
+ offsetY = -deltaHeight; // только верхний край смещается
442
+ break;
443
+ case 'ne':
444
+ offsetY = -deltaHeight; // верх смещается, правый край — нет
445
+ break;
446
+ case 'e':
447
+ // правый край — левый верх не смещается
448
+ break;
449
+ case 'se':
450
+ // правый нижний — левый верх не смещается
451
+ break;
452
+ case 's':
453
+ // нижний — левый верх не смещается
454
+ break;
455
+ case 'sw':
456
+ offsetX = -deltaWidth; // левый край смещается, низ — нет
457
+ break;
458
+ case 'w':
459
+ offsetX = -deltaWidth; // левый край смещается на полную величину
460
+ break;
461
+ }
462
+
463
+ // Для поворота корректное смещение требует преобразования в локальные координаты объекта
464
+ // и обратно. В данной итерации оставляем смещение в мировых осях для устойчивости без вращения.
465
+ return { x: offsetX, y: offsetY };
466
+ }
@@ -17,42 +17,51 @@ export class FilePropertiesPanel {
17
17
  }
18
18
 
19
19
  _attachEvents() {
20
- // Показываем панель при изменении выделения
21
- this.eventBus.on(Events.Tool.SelectionAdd, () => this.updateFromSelection());
22
- this.eventBus.on(Events.Tool.SelectionRemove, () => this.updateFromSelection());
23
- this.eventBus.on(Events.Tool.SelectionClear, () => this.hide());
24
-
25
- // Скрываем панель при удалении объекта
26
- this.eventBus.on(Events.Object.Deleted, (data) => {
20
+ this._handlers = {};
21
+ this._handlers.onSelectionAdd = () => this.updateFromSelection();
22
+ this._handlers.onSelectionRemove = () => this.updateFromSelection();
23
+ this._handlers.onSelectionClear = () => this.hide();
24
+ this._handlers.onDeleted = (data) => {
27
25
  const objectId = data?.objectId || data;
28
26
  if (this.currentId && objectId === this.currentId) this.hide();
29
- });
30
-
31
- // Обновляем позицию / скрываем во время перетаскивания
32
- this.eventBus.on(Events.Tool.DragStart, () => this.hide());
33
- this.eventBus.on(Events.Tool.DragUpdate, () => this.reposition());
34
- this.eventBus.on(Events.Tool.DragEnd, () => this.updateFromSelection());
35
- this.eventBus.on(Events.Tool.GroupDragUpdate, () => this.reposition());
36
- this.eventBus.on(Events.Tool.GroupDragStart, () => this.hide());
37
- this.eventBus.on(Events.Tool.GroupDragEnd, () => this.updateFromSelection());
38
- this.eventBus.on(Events.Tool.ResizeUpdate, () => this.reposition());
39
- this.eventBus.on(Events.Tool.RotateUpdate, () => this.reposition());
40
-
41
- // Обновляем позицию при зуме/пане
42
- this.eventBus.on(Events.UI.ZoomPercent, () => {
27
+ };
28
+ this._handlers.onDragStart = () => this.hide();
29
+ this._handlers.onDragUpdate = () => this.reposition();
30
+ this._handlers.onDragEnd = () => this.updateFromSelection();
31
+ this._handlers.onGroupDragUpdate = () => this.reposition();
32
+ this._handlers.onGroupDragStart = () => this.hide();
33
+ this._handlers.onGroupDragEnd = () => this.updateFromSelection();
34
+ this._handlers.onResizeUpdate = () => this.reposition();
35
+ this._handlers.onRotateUpdate = () => this.reposition();
36
+ this._handlers.onZoomPercent = () => {
43
37
  if (this.currentId) this.reposition();
44
- });
45
-
46
- this.eventBus.on(Events.Tool.PanUpdate, () => {
38
+ };
39
+ this._handlers.onPanUpdate = () => {
47
40
  if (this.currentId) this.reposition();
48
- });
49
-
50
- // Скрываем панель при активации других инструментов
51
- this.eventBus.on(Events.Tool.Activated, ({ tool }) => {
52
- if (tool !== 'select') {
53
- this.hide();
54
- }
55
- });
41
+ };
42
+ this._handlers.onActivated = ({ tool }) => {
43
+ if (tool !== 'select') this.hide();
44
+ };
45
+ this._handlers.onTransformUpdated = (data) => {
46
+ if (this.currentId && data?.objectId === this.currentId) this.reposition();
47
+ };
48
+
49
+ this.eventBus.on(Events.Tool.SelectionAdd, this._handlers.onSelectionAdd);
50
+ this.eventBus.on(Events.Tool.SelectionRemove, this._handlers.onSelectionRemove);
51
+ this.eventBus.on(Events.Tool.SelectionClear, this._handlers.onSelectionClear);
52
+ this.eventBus.on(Events.Object.Deleted, this._handlers.onDeleted);
53
+ this.eventBus.on(Events.Tool.DragStart, this._handlers.onDragStart);
54
+ this.eventBus.on(Events.Tool.DragUpdate, this._handlers.onDragUpdate);
55
+ this.eventBus.on(Events.Tool.DragEnd, this._handlers.onDragEnd);
56
+ this.eventBus.on(Events.Tool.GroupDragUpdate, this._handlers.onGroupDragUpdate);
57
+ this.eventBus.on(Events.Tool.GroupDragStart, this._handlers.onGroupDragStart);
58
+ this.eventBus.on(Events.Tool.GroupDragEnd, this._handlers.onGroupDragEnd);
59
+ this.eventBus.on(Events.Tool.ResizeUpdate, this._handlers.onResizeUpdate);
60
+ this.eventBus.on(Events.Tool.RotateUpdate, this._handlers.onRotateUpdate);
61
+ this.eventBus.on(Events.UI.ZoomPercent, this._handlers.onZoomPercent);
62
+ this.eventBus.on(Events.Tool.PanUpdate, this._handlers.onPanUpdate);
63
+ this.eventBus.on(Events.Tool.Activated, this._handlers.onActivated);
64
+ this.eventBus.on(Events.Object.TransformUpdated, this._handlers.onTransformUpdated);
56
65
  }
57
66
 
58
67
  updateFromSelection() {
@@ -317,6 +326,26 @@ export class FilePropertiesPanel {
317
326
  }
318
327
 
319
328
  destroy() {
329
+ if (!this.eventBus || !this._handlers) return;
330
+
331
+ this.eventBus.off(Events.Tool.SelectionAdd, this._handlers.onSelectionAdd);
332
+ this.eventBus.off(Events.Tool.SelectionRemove, this._handlers.onSelectionRemove);
333
+ this.eventBus.off(Events.Tool.SelectionClear, this._handlers.onSelectionClear);
334
+ this.eventBus.off(Events.Object.Deleted, this._handlers.onDeleted);
335
+ this.eventBus.off(Events.Tool.DragStart, this._handlers.onDragStart);
336
+ this.eventBus.off(Events.Tool.DragUpdate, this._handlers.onDragUpdate);
337
+ this.eventBus.off(Events.Tool.DragEnd, this._handlers.onDragEnd);
338
+ this.eventBus.off(Events.Tool.GroupDragUpdate, this._handlers.onGroupDragUpdate);
339
+ this.eventBus.off(Events.Tool.GroupDragStart, this._handlers.onGroupDragStart);
340
+ this.eventBus.off(Events.Tool.GroupDragEnd, this._handlers.onGroupDragEnd);
341
+ this.eventBus.off(Events.Tool.ResizeUpdate, this._handlers.onResizeUpdate);
342
+ this.eventBus.off(Events.Tool.RotateUpdate, this._handlers.onRotateUpdate);
343
+ this.eventBus.off(Events.UI.ZoomPercent, this._handlers.onZoomPercent);
344
+ this.eventBus.off(Events.Tool.PanUpdate, this._handlers.onPanUpdate);
345
+ this.eventBus.off(Events.Tool.Activated, this._handlers.onActivated);
346
+ this.eventBus.off(Events.Object.TransformUpdated, this._handlers.onTransformUpdated);
347
+ this._handlers = null;
348
+
320
349
  if (this.panel && this.panel.parentNode) {
321
350
  this.panel.parentNode.removeChild(this.panel);
322
351
  }