@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,267 @@
1
+ import { Events } from '../../../core/events/Events.js';
2
+
3
+ export function onMouseDown(event) {
4
+ // Если активен текстовый редактор, закрываем его при клике вне
5
+ if (this.textEditor.active) {
6
+ if (this.textEditor.objectType === 'file') {
7
+ this._closeFileNameEditor(true);
8
+ } else {
9
+ this._closeTextEditor(true);
10
+ }
11
+ return; // Прерываем выполнение, чтобы не обрабатывать клик дальше
12
+ }
13
+
14
+ this.isMultiSelect = event.originalEvent.ctrlKey || event.originalEvent.metaKey || event.originalEvent.shiftKey;
15
+
16
+ // Проверяем, что под курсором
17
+ const hitResult = this.hitTest(event.x, event.y);
18
+
19
+ if (hitResult.type === 'resize-handle') {
20
+ this.startResize(hitResult.handle, hitResult.object);
21
+ } else if (hitResult.type === 'rotate-handle') {
22
+ this.startRotate(hitResult.object);
23
+ } else if (this.selection.size() > 1) {
24
+ // Особая логика для группового выделения: клики внутри общей рамки не снимают выделение
25
+ const gb = this.computeGroupBounds();
26
+ const insideGroup = this.isPointInBounds({ x: event.x, y: event.y }, { x: gb.x, y: gb.y, width: gb.width, height: gb.height });
27
+ if (insideGroup) {
28
+ // Если клик внутри группы (по объекту или пустому месту), сохраняем выделение и начинаем перетаскивание группы
29
+ this.startGroupDrag(event);
30
+ return;
31
+ }
32
+ // Вне группы — обычная логика
33
+ if (hitResult.type === 'object') {
34
+ this.handleObjectSelect(hitResult.object, event);
35
+ } else {
36
+ this.startBoxSelect(event);
37
+ }
38
+ } else if (hitResult.type === 'object') {
39
+ // Особая логика для фреймов: если у фрейма есть дети и клик внутри внутренней области (без 20px рамки),
40
+ // то не начинаем drag фрейма, а запускаем box-select для выбора объектов внутри
41
+ const req = { objectId: hitResult.object, pixiObject: null };
42
+ this.emit(Events.Tool.GetObjectPixi, req);
43
+ const mbType = req.pixiObject && req.pixiObject._mb && req.pixiObject._mb.type;
44
+ if (mbType === 'frame') {
45
+ // Получаем данные фрейма и его экранные границы
46
+ const objects = this.core?.state?.getObjects ? this.core.state.getObjects() : [];
47
+ const frameObj = objects.find(o => o.id === hitResult.object);
48
+ const hasChildren = !!(objects && objects.some(o => o.properties && o.properties.frameId === hitResult.object));
49
+ if (req.pixiObject && hasChildren && frameObj) {
50
+ const bounds = req.pixiObject.getBounds(); // экранные координаты
51
+ const inner = { x: bounds.x + 20, y: bounds.y + 20, width: Math.max(0, bounds.width - 40), height: Math.max(0, bounds.height - 40) };
52
+ const insideInner = this.isPointInBounds({ x: event.x, y: event.y }, inner);
53
+ // Если клик внутри внутренней области — запускаем box-select и выходим
54
+ if (insideInner) {
55
+ // Запускаем рамку выделения вместо drag фрейма
56
+ this.startBoxSelect(event);
57
+ return;
58
+ }
59
+ // Если клик на 20px рамке — позволяем перетягивать фрейм. Но запрещаем box-select от рамки.
60
+ // Здесь ничего не делаем: ниже пойдёт обычная логика handleObjectSelect
61
+ }
62
+ }
63
+ // Обычная логика: начинаем drag выбранного объекта
64
+ this.handleObjectSelect(hitResult.object, event);
65
+ } else {
66
+ // Клик по пустому месту — если есть одиночное выделение, разрешаем drag за пределами объекта в пределах рамки
67
+ if (this.selection.size() === 1) {
68
+ const selId = this.selection.toArray()[0];
69
+ // Если выбран фрейм с детьми и клик внутри внутренней области — не начинаем drag, а box-select
70
+ const req = { objectId: selId, pixiObject: null };
71
+ this.emit(Events.Tool.GetObjectPixi, req);
72
+ const isFrame = !!(req.pixiObject && req.pixiObject._mb && req.pixiObject._mb.type === 'frame');
73
+ if (isFrame) {
74
+ const objects = this.core?.state?.getObjects ? this.core.state.getObjects() : [];
75
+ const frameObj = objects.find(o => o.id === selId);
76
+ const hasChildren = !!(objects && objects.some(o => o.properties && o.properties.frameId === selId));
77
+ if (frameObj && hasChildren) {
78
+ const b = { x: frameObj.position.x, y: frameObj.position.y, width: frameObj.width || 0, height: frameObj.height || 0 };
79
+ const inner = { x: b.x + 20, y: b.y + 20, width: Math.max(0, b.width - 40), height: Math.max(0, b.height - 40) };
80
+ const insideInner = this.isPointInBounds({ x: event.x, y: event.y }, inner);
81
+ if (insideInner) {
82
+ this.startBoxSelect(event);
83
+ return;
84
+ }
85
+ }
86
+ }
87
+ // Обычная логика: если клик внутри рамки выбранного — начинаем drag
88
+ const boundsReq = { objects: [] };
89
+ this.emit(Events.Tool.GetAllObjects, boundsReq);
90
+ const map = new Map(boundsReq.objects.map(o => [o.id, o.bounds]));
91
+ const b = map.get(selId);
92
+ if (b && this.isPointInBounds({ x: event.x, y: event.y }, b)) {
93
+ // Для фрейма c детьми: отфильтруем клики внутри внутренней области (box-select)
94
+ const req2 = { objectId: selId, pixiObject: null };
95
+ this.emit(Events.Tool.GetObjectPixi, req2);
96
+ const isFrame2 = !!(req2.pixiObject && req2.pixiObject._mb && req2.pixiObject._mb.type === 'frame');
97
+ if (isFrame2) {
98
+ const os = this.core?.state?.getObjects ? this.core.state.getObjects() : [];
99
+ const fr = os.find(o => o.id === selId);
100
+ const hasChildren2 = !!(os && os.some(o => o.properties && o.properties.frameId === selId));
101
+ if (req2.pixiObject && fr && hasChildren2) {
102
+ const bounds2 = req2.pixiObject.getBounds();
103
+ const inner2 = { x: bounds2.x + 20, y: bounds2.y + 20, width: Math.max(0, bounds2.width - 40), height: Math.max(0, bounds2.height - 40) };
104
+ if (this.isPointInBounds({ x: event.x, y: event.y }, inner2)) {
105
+ this.startBoxSelect(event);
106
+ return;
107
+ }
108
+ }
109
+ }
110
+ // Старт перетаскивания как если бы кликнули по объекту
111
+ this.startDrag(selId, event);
112
+ return;
113
+ }
114
+ }
115
+ // Иначе — начинаем рамку выделения
116
+ this.startBoxSelect(event);
117
+ }
118
+ }
119
+
120
+ export function onMouseMove(event) {
121
+ // Проверяем, что инструмент не уничтожен
122
+ if (this.destroyed) {
123
+ return;
124
+ }
125
+
126
+ // Обновляем текущие координаты мыши
127
+ this.currentX = event.x;
128
+ this.currentY = event.y;
129
+
130
+ if (this.isResizing || this.isGroupResizing) {
131
+ this.updateResize(event);
132
+ } else if (this.isRotating || this.isGroupRotating) {
133
+ this.updateRotate(event);
134
+ } else if (this.isDragging || this.isGroupDragging) {
135
+ this.updateDrag(event);
136
+ } else if (this.isBoxSelect) {
137
+ this.updateBoxSelect(event);
138
+ } else {
139
+ // Обновляем курсор в зависимости от того, что под мышью
140
+ this.updateCursor(event);
141
+ }
142
+ }
143
+
144
+ export function onMouseUp(event) {
145
+ if (this.isResizing || this.isGroupResizing) {
146
+ this.endResize();
147
+ } else if (this.isRotating || this.isGroupRotating) {
148
+ this.endRotate();
149
+ } else if (this.isDragging || this.isGroupDragging) {
150
+ this.endDrag();
151
+ } else if (this.isBoxSelect) {
152
+ this.endBoxSelect();
153
+ }
154
+ }
155
+
156
+ export function onDoubleClick(event) {
157
+ const hitResult = this.hitTest(event.x, event.y);
158
+
159
+ if (hitResult.type === 'object') {
160
+ // если это текст или записка — войдём в режим редактирования через ObjectEdit
161
+ const req = { objectId: hitResult.object, pixiObject: null };
162
+ this.emit(Events.Tool.GetObjectPixi, req);
163
+ const pix = req.pixiObject;
164
+
165
+ const isText = !!(pix && pix._mb && pix._mb.type === 'text');
166
+ const isNote = !!(pix && pix._mb && pix._mb.type === 'note');
167
+ const isFile = !!(pix && pix._mb && pix._mb.type === 'file');
168
+
169
+ if (isText) {
170
+ // Получаем позицию объекта для редактирования
171
+ const posData = { objectId: hitResult.object, position: null };
172
+ this.emit(Events.Tool.GetObjectPosition, posData);
173
+
174
+ // Получаем содержимое из properties объекта
175
+ const textContent = pix._mb?.properties?.content || '';
176
+
177
+ this.emit(Events.Tool.ObjectEdit, {
178
+ id: hitResult.object,
179
+ type: 'text',
180
+ position: posData.position,
181
+ properties: { content: textContent },
182
+ caretClick: {
183
+ clientX: event?.originalEvent?.clientX,
184
+ clientY: event?.originalEvent?.clientY
185
+ },
186
+ create: false
187
+ });
188
+ return;
189
+ }
190
+ if (isNote) {
191
+ const noteProps = pix._mb.properties || {};
192
+ // Получаем позицию объекта для редактирования
193
+ const posData = { objectId: hitResult.object, position: null };
194
+ this.emit(Events.Tool.GetObjectPosition, posData);
195
+
196
+ this.emit(Events.Tool.ObjectEdit, {
197
+ id: hitResult.object,
198
+ type: 'note',
199
+ position: posData.position,
200
+ properties: { content: noteProps.content || '' },
201
+ create: false
202
+ });
203
+ return;
204
+ }
205
+
206
+ if (isFile) {
207
+ const fileProps = pix._mb.properties || {};
208
+ // Получаем позицию объекта для редактирования
209
+ const posData = { objectId: hitResult.object, position: null };
210
+ this.emit(Events.Tool.GetObjectPosition, posData);
211
+
212
+ this.emit(Events.Tool.ObjectEdit, {
213
+ id: hitResult.object,
214
+ type: 'file',
215
+ position: posData.position,
216
+ properties: { fileName: fileProps.fileName || 'Untitled' },
217
+ create: false
218
+ });
219
+ return;
220
+ }
221
+ this.editObject(hitResult.object);
222
+ }
223
+ }
224
+
225
+ export function onContextMenu(event) {
226
+ // Определяем, что под курсором
227
+ const hit = this.hitTest(event.x, event.y);
228
+ let context = 'canvas';
229
+ let targetId = null;
230
+ if (hit && hit.type === 'object' && hit.object) {
231
+ targetId = hit.object;
232
+ if (this.selection.has(targetId) && this.selection.size() > 1) {
233
+ context = 'group';
234
+ } else {
235
+ context = 'object';
236
+ }
237
+ } else if (this.selection.size() > 1) {
238
+ context = 'group';
239
+ }
240
+ // Сообщаем ядру/UI, что нужно показать контекстное меню (пока без пунктов)
241
+ this.emit(Events.Tool.ContextMenuShow, { x: event.x, y: event.y, context, targetId });
242
+ }
243
+
244
+ export function onKeyDown(event) {
245
+ // Проверяем, не активен ли текстовый редактор (редактирование названия файла или текста)
246
+ if (this.textEditor && this.textEditor.active) {
247
+ return; // Не обрабатываем клавиши во время редактирования
248
+ }
249
+
250
+ switch (event.key) {
251
+ case 'Delete':
252
+ case 'Backspace':
253
+ this.deleteSelectedObjects();
254
+ break;
255
+
256
+ case 'a':
257
+ if (event.ctrlKey) {
258
+ this.selectAll();
259
+ event.originalEvent.preventDefault();
260
+ }
261
+ break;
262
+
263
+ case 'Escape':
264
+ this.clearSelection();
265
+ break;
266
+ }
267
+ }
@@ -0,0 +1,128 @@
1
+ import { Events } from '../../../core/events/Events.js';
2
+ import { unregisterSelectToolCoreSubscriptions } from './SelectToolSetup.js';
3
+ import { ResizeHandles } from '../../ResizeHandles.js';
4
+ import { SimpleDragController } from './SimpleDragController.js';
5
+ import { ResizeController } from './ResizeController.js';
6
+ import { RotateController } from './RotateController.js';
7
+ import { GroupResizeController } from './GroupResizeController.js';
8
+ import { GroupRotateController } from './GroupRotateController.js';
9
+ import { GroupDragController } from './GroupDragController.js';
10
+ import { BoxSelectController } from './BoxSelectController.js';
11
+
12
+ export function activateSelectTool(app, defaultCursor, superActivate) {
13
+ superActivate();
14
+ // Сохраняем ссылку на PIXI app для оверлеев (рамка выделения)
15
+ this.app = app;
16
+
17
+ // Устанавливаем стандартный курсор для select инструмента
18
+ if (this.app && this.app.view) {
19
+ this.app.view.style.cursor = defaultCursor; // пусто → наследует глобальный CSS
20
+ }
21
+
22
+ // Инициализируем систему ручек изменения размера
23
+ if (!this.resizeHandles && app) {
24
+ this.resizeHandles = new ResizeHandles(app);
25
+ // Полностью отключаем синхронизацию старых PIXI-ручек
26
+ if (this.resizeHandles && typeof this.resizeHandles.hideHandles === 'function') {
27
+ this.resizeHandles.hideHandles();
28
+ }
29
+ this._dragCtrl = new SimpleDragController({
30
+ emit: (event, payload) => this.emit(event, payload)
31
+ });
32
+ this._resizeCtrl = new ResizeController({
33
+ emit: (event, payload) => this.emit(event, payload),
34
+ getRotation: (objectId) => {
35
+ const d = { objectId, rotation: 0 };
36
+ this.emit(Events.Tool.GetObjectRotation, d);
37
+ return d.rotation || 0;
38
+ }
39
+ });
40
+ this._rotateCtrl = new RotateController({
41
+ emit: (event, payload) => this.emit(event, payload)
42
+ });
43
+ this._groupResizeCtrl = new GroupResizeController({
44
+ emit: (event, payload) => this.emit(event, payload),
45
+ selection: this.selection,
46
+ getGroupBounds: () => this.computeGroupBounds(),
47
+ ensureGroupGraphics: (b) => this.ensureGroupBoundsGraphics(b),
48
+ updateGroupGraphics: (b) => this.updateGroupBoundsGraphics(b)
49
+ });
50
+ this._groupRotateCtrl = new GroupRotateController({
51
+ emit: (event, payload) => this.emit(event, payload),
52
+ selection: this.selection,
53
+ getGroupBounds: () => this.computeGroupBounds(),
54
+ ensureGroupGraphics: (b) => this.ensureGroupBoundsGraphics(b),
55
+ updateHandles: () => { if (this.resizeHandles) this.resizeHandles.updateHandles(); }
56
+ });
57
+ this._groupDragCtrl = new GroupDragController({
58
+ emit: (event, payload) => this.emit(event, payload),
59
+ selection: this.selection,
60
+ updateGroupBoundsByTopLeft: (pos) => this.updateGroupBoundsGraphicsByTopLeft(pos)
61
+ });
62
+ this._boxSelect = new BoxSelectController({
63
+ app,
64
+ selection: this.selection,
65
+ emit: (event, payload) => this.emit(event, payload),
66
+ setSelection: (ids) => this.setSelection(ids),
67
+ clearSelection: () => this.clearSelection(),
68
+ rectIntersectsRect: (a, b) => this.rectIntersectsRect(a, b)
69
+ });
70
+ } else if (!app) {
71
+ console.log('❌ PIXI app не передан в activate');
72
+ } else {
73
+ }
74
+ }
75
+
76
+ export function deactivateSelectTool(superDeactivate) {
77
+ superDeactivate();
78
+
79
+ // Закрываем текстовый/файловый редактор если открыт
80
+ if (this.textEditor.active) {
81
+ if (this.textEditor.objectType === 'file') {
82
+ this._closeFileNameEditor(true);
83
+ } else {
84
+ this._closeTextEditor(true);
85
+ }
86
+ }
87
+
88
+ // Очищаем выделение и ручки
89
+ this.clearSelection();
90
+ // Скрываем любые старые PIXI-ручки: используем только HTML-ручки
91
+
92
+ // Сбрасываем курсор на стандартный
93
+ if (this.app && this.app.view) {
94
+ this.app.view.style.cursor = '';
95
+ }
96
+ }
97
+
98
+ export function destroySelectTool(superDestroy) {
99
+ if (this.destroyed) {
100
+ return;
101
+ }
102
+
103
+ this.destroyed = true;
104
+
105
+ unregisterSelectToolCoreSubscriptions(this);
106
+ this.clearSelection();
107
+
108
+ // Уничтожаем ручки изменения размера
109
+ if (this.resizeHandles) {
110
+ this.resizeHandles.destroy();
111
+ this.resizeHandles = null;
112
+ }
113
+
114
+ // Очищаем контроллеры
115
+ this.dragController = null;
116
+ this.resizeController = null;
117
+ this.rotateController = null;
118
+ this.groupDragController = null;
119
+ this.groupResizeController = null;
120
+ this.groupRotateController = null;
121
+ this.boxSelectController = null;
122
+
123
+ // Очищаем модель выделения
124
+ this.selection = null;
125
+
126
+ // Вызываем destroy родительского класса
127
+ superDestroy();
128
+ }
@@ -0,0 +1,134 @@
1
+ import { Events } from '../../../core/events/Events.js';
2
+
3
+ export function initializeSelectToolState(instance) {
4
+ // Флаг состояния объекта
5
+ instance.destroyed = false;
6
+
7
+ // Состояние выделения перенесено в модель
8
+ instance.isMultiSelect = false;
9
+
10
+ // Режим Alt-клонирования при перетаскивании
11
+ // Если Alt зажат при начале drag, создаем копию и перетаскиваем именно её
12
+ instance.isAltCloneMode = false; // активен ли режим Alt-клона
13
+ instance.clonePending = false; // ожидаем подтверждение создания копии
14
+ instance.cloneRequested = false; // запрос на создание копии уже отправлен
15
+ instance.cloneSourceId = null; // исходный объект для копии
16
+ // Групповой Alt-клон
17
+ instance.isAltGroupCloneMode = false;
18
+ instance.groupClonePending = false;
19
+ instance.groupCloneOriginalIds = [];
20
+ instance.groupCloneMap = null; // { originalId: newId }
21
+
22
+ // Состояние перетаскивания
23
+ instance.isDragging = false;
24
+ instance.dragOffset = { x: 0, y: 0 };
25
+ instance.dragTarget = null;
26
+
27
+ // Состояние изменения размера
28
+ instance.isResizing = false;
29
+ instance.resizeHandle = null;
30
+ instance.resizeStartBounds = null;
31
+ instance.resizeStartMousePos = null;
32
+ instance.resizeStartPosition = null;
33
+
34
+ // Система ручек изменения размера
35
+ instance.resizeHandles = null;
36
+ instance.groupSelectionGraphics = null; // визуализация рамок при множественном выделении
37
+ instance.groupBoundsGraphics = null; // невидимая геометрия для ручек группы
38
+ instance.groupId = '__group__';
39
+ instance.isGroupDragging = false;
40
+ instance.isGroupResizing = false;
41
+ instance.isGroupRotating = false;
42
+ instance.groupStartBounds = null;
43
+ instance.groupStartMouse = null;
44
+ instance.groupDragOffset = null;
45
+ instance.groupObjectsInitial = null; // Map id -> { position, size, rotation }
46
+
47
+ // Текущие координаты мыши
48
+ instance.currentX = 0;
49
+ instance.currentY = 0;
50
+
51
+ // Состояние поворота
52
+ instance.isRotating = false;
53
+ instance.rotateCenter = null;
54
+ instance.rotateStartAngle = 0;
55
+ instance.rotateCurrentAngle = 0;
56
+ instance.rotateStartMouseAngle = 0;
57
+
58
+ // Состояние рамки выделения
59
+ instance.isBoxSelect = false;
60
+ instance.selectionBox = null;
61
+ instance.selectionGraphics = null; // PIXI.Graphics для визуализации рамки
62
+ instance.initialSelectionBeforeBox = null; // снимок выделения перед началом box-select
63
+
64
+ instance.textEditor = {
65
+ active: false,
66
+ objectId: null,
67
+ textarea: null,
68
+ wrapper: null,
69
+ world: null,
70
+ position: null, // world top-left
71
+ properties: null, // { fontSize }
72
+ objectType: 'text', // 'text' or 'note'
73
+ isResizing: false,
74
+ };
75
+ }
76
+
77
+ export function registerSelectToolCoreSubscriptions(instance) {
78
+ if (!instance.eventBus) return;
79
+
80
+ const onDuplicateReady = (data) => {
81
+ if (!instance.isAltCloneMode || !instance.clonePending) return;
82
+ if (!data || data.originalId !== instance.cloneSourceId) return;
83
+ instance.onDuplicateReady(data.newId);
84
+ };
85
+
86
+ const onGroupDuplicateReady = (data) => {
87
+ if (!instance.isAltGroupCloneMode || !instance.groupClonePending) return;
88
+ if (!data || !data.map) return;
89
+ instance.onGroupDuplicateReady(data.map);
90
+ };
91
+
92
+ const onObjectEdit = (object) => {
93
+ const objectType = object.type || (object.object && object.object.type) || 'text';
94
+ if (objectType === 'file') {
95
+ instance._openFileNameEditor(object, object.create || false);
96
+ } else {
97
+ if (object.create) {
98
+ instance._openTextEditor(object, true);
99
+ } else {
100
+ instance._openTextEditor(object, false);
101
+ }
102
+ }
103
+ };
104
+
105
+ const onObjectDeleted = (data) => {
106
+ const objectId = data?.objectId || data;
107
+ if (!objectId) return;
108
+ if (instance.selection?.has(objectId)) {
109
+ instance.removeFromSelection(objectId);
110
+ if (instance.selection.size() === 0) {
111
+ instance.emit(Events.Tool.SelectionClear);
112
+ instance.updateResizeHandles();
113
+ }
114
+ } else {
115
+ instance.updateResizeHandles();
116
+ }
117
+ };
118
+
119
+ instance._coreHandlers = { onDuplicateReady, onGroupDuplicateReady, onObjectEdit, onObjectDeleted };
120
+ instance.eventBus.on(Events.Tool.DuplicateReady, onDuplicateReady);
121
+ instance.eventBus.on(Events.Tool.GroupDuplicateReady, onGroupDuplicateReady);
122
+ instance.eventBus.on(Events.Tool.ObjectEdit, onObjectEdit);
123
+ instance.eventBus.on(Events.Object.Deleted, onObjectDeleted);
124
+ }
125
+
126
+ export function unregisterSelectToolCoreSubscriptions(instance) {
127
+ if (!instance.eventBus || !instance._coreHandlers) return;
128
+ const { onDuplicateReady, onGroupDuplicateReady, onObjectEdit, onObjectDeleted } = instance._coreHandlers;
129
+ instance.eventBus.off(Events.Tool.DuplicateReady, onDuplicateReady);
130
+ instance.eventBus.off(Events.Tool.GroupDuplicateReady, onGroupDuplicateReady);
131
+ instance.eventBus.off(Events.Tool.ObjectEdit, onObjectEdit);
132
+ instance.eventBus.off(Events.Object.Deleted, onObjectDeleted);
133
+ instance._coreHandlers = null;
134
+ }
@@ -0,0 +1,81 @@
1
+ import * as PIXI from 'pixi.js';
2
+ import { Events } from '../../../core/events/Events.js';
3
+
4
+ export function drawGroupSelectionGraphics() {
5
+ if (!this.app || !this.app.stage) return;
6
+ const selectedIds = this.selection.toArray();
7
+ if (selectedIds.length <= 1) {
8
+ this.removeGroupSelectionGraphics();
9
+ return;
10
+ }
11
+ // Получаем bounds всех объектов и отрисовываем контур на groupBoundsGraphics (одна рамка с ручками)
12
+ const request = { objects: [] };
13
+ this.emit(Events.Tool.GetAllObjects, request);
14
+ const idToBounds = new Map(request.objects.map(o => [o.id, o.bounds]));
15
+ let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
16
+ for (const id of selectedIds) {
17
+ const b = idToBounds.get(id);
18
+ if (!b) continue;
19
+ minX = Math.min(minX, b.x);
20
+ minY = Math.min(minY, b.y);
21
+ maxX = Math.max(maxX, b.x + b.width);
22
+ maxY = Math.max(maxY, b.y + b.height);
23
+ }
24
+ if (isFinite(minX) && isFinite(minY) && isFinite(maxX) && isFinite(maxY)) {
25
+ const gb = { x: minX, y: minY, width: maxX - minX, height: maxY - minY };
26
+ this.ensureGroupBoundsGraphics(gb);
27
+ this.updateGroupBoundsGraphics(gb);
28
+ }
29
+ }
30
+
31
+ export function removeGroupSelectionGraphics() {
32
+ if (this.groupBoundsGraphics) {
33
+ this.groupBoundsGraphics.clear();
34
+ this.groupBoundsGraphics.rotation = 0;
35
+ }
36
+ }
37
+
38
+ export function computeGroupBounds() {
39
+ const request = { objects: [] };
40
+ this.emit(Events.Tool.GetAllObjects, request);
41
+ const pixiMap = new Map(request.objects.map(o => [o.id, o.pixi]));
42
+ const b = this.selection.computeBounds((id) => pixiMap.get(id));
43
+ if (!b) return { x: 0, y: 0, width: 0, height: 0 };
44
+ return b;
45
+ }
46
+
47
+ export function ensureGroupBoundsGraphics(bounds) {
48
+ if (!this.app || !this.app.stage) return;
49
+ if (!this.groupBoundsGraphics) {
50
+ this.groupBoundsGraphics = new PIXI.Graphics();
51
+ this.groupBoundsGraphics.name = 'group-bounds';
52
+ this.groupBoundsGraphics.zIndex = 1400;
53
+ this.app.stage.addChild(this.groupBoundsGraphics);
54
+ this.app.stage.sortableChildren = true;
55
+ }
56
+ this.updateGroupBoundsGraphics(bounds);
57
+ }
58
+
59
+ export function updateGroupBoundsGraphics(bounds) {
60
+ if (!this.groupBoundsGraphics) return;
61
+ this.groupBoundsGraphics.clear();
62
+ // Прозрачная заливка (alpha ~0), чтобы getBounds() давал корректные размеры и не было артефактов
63
+ this.groupBoundsGraphics.beginFill(0x000000, 0.001);
64
+ this.groupBoundsGraphics.drawRect(0, 0, Math.max(1, bounds.width), Math.max(1, bounds.height));
65
+ this.groupBoundsGraphics.endFill();
66
+ // Размещаем графику в левом-верхнем углу группы
67
+ this.groupBoundsGraphics.position.set(bounds.x, bounds.y);
68
+ // Обновляем ручки, если показаны
69
+ // HTML-ручки обновляются слоем HtmlHandlesLayer
70
+ }
71
+
72
+ export function updateGroupBoundsGraphicsByTopLeft(topLeft) {
73
+ if (!this.groupBoundsGraphics || !this.groupStartBounds) return;
74
+ this.updateGroupBoundsGraphics({ x: topLeft.x, y: topLeft.y, width: this.groupStartBounds.width, height: this.groupStartBounds.height });
75
+ // Рисуем визуальную общую рамку одновременно
76
+ if (this.groupSelectionGraphics) {
77
+ this.groupSelectionGraphics.clear();
78
+ this.groupSelectionGraphics.lineStyle(1, 0x3B82F6, 0.9);
79
+ this.groupSelectionGraphics.drawRect(topLeft.x, topLeft.y, this.groupStartBounds.width, this.groupStartBounds.height);
80
+ }
81
+ }