@sequent-org/moodboard 1.0.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 (123) hide show
  1. package/package.json +44 -0
  2. package/src/assets/icons/README.md +105 -0
  3. package/src/assets/icons/attachments.svg +3 -0
  4. package/src/assets/icons/clear.svg +5 -0
  5. package/src/assets/icons/comments.svg +3 -0
  6. package/src/assets/icons/emoji.svg +6 -0
  7. package/src/assets/icons/frame.svg +3 -0
  8. package/src/assets/icons/image.svg +3 -0
  9. package/src/assets/icons/note.svg +3 -0
  10. package/src/assets/icons/pan.svg +3 -0
  11. package/src/assets/icons/pencil.svg +3 -0
  12. package/src/assets/icons/redo.svg +3 -0
  13. package/src/assets/icons/select.svg +9 -0
  14. package/src/assets/icons/shapes.svg +3 -0
  15. package/src/assets/icons/text-add.svg +3 -0
  16. package/src/assets/icons/topbar/README.md +39 -0
  17. package/src/assets/icons/topbar/grid-cross.svg +6 -0
  18. package/src/assets/icons/topbar/grid-dot.svg +3 -0
  19. package/src/assets/icons/topbar/grid-line.svg +3 -0
  20. package/src/assets/icons/topbar/grid-off.svg +3 -0
  21. package/src/assets/icons/topbar/paint.svg +3 -0
  22. package/src/assets/icons/undo.svg +3 -0
  23. package/src/core/ApiClient.js +309 -0
  24. package/src/core/EventBus.js +42 -0
  25. package/src/core/HistoryManager.js +261 -0
  26. package/src/core/KeyboardManager.js +710 -0
  27. package/src/core/PixiEngine.js +439 -0
  28. package/src/core/SaveManager.js +381 -0
  29. package/src/core/StateManager.js +64 -0
  30. package/src/core/commands/BaseCommand.js +68 -0
  31. package/src/core/commands/CopyObjectCommand.js +44 -0
  32. package/src/core/commands/CreateObjectCommand.js +46 -0
  33. package/src/core/commands/DeleteObjectCommand.js +146 -0
  34. package/src/core/commands/EditFileNameCommand.js +107 -0
  35. package/src/core/commands/GroupMoveCommand.js +47 -0
  36. package/src/core/commands/GroupReorderZCommand.js +74 -0
  37. package/src/core/commands/GroupResizeCommand.js +37 -0
  38. package/src/core/commands/GroupRotateCommand.js +41 -0
  39. package/src/core/commands/MoveObjectCommand.js +89 -0
  40. package/src/core/commands/PasteObjectCommand.js +103 -0
  41. package/src/core/commands/ReorderZCommand.js +45 -0
  42. package/src/core/commands/ResizeObjectCommand.js +135 -0
  43. package/src/core/commands/RotateObjectCommand.js +70 -0
  44. package/src/core/commands/index.js +14 -0
  45. package/src/core/events/Events.js +147 -0
  46. package/src/core/index.js +1632 -0
  47. package/src/core/rendering/GeometryUtils.js +89 -0
  48. package/src/core/rendering/HitTestManager.js +186 -0
  49. package/src/core/rendering/LayerManager.js +137 -0
  50. package/src/core/rendering/ObjectRenderer.js +363 -0
  51. package/src/core/rendering/PixiRenderer.js +140 -0
  52. package/src/core/rendering/index.js +9 -0
  53. package/src/grid/BaseGrid.js +164 -0
  54. package/src/grid/CrossGrid.js +75 -0
  55. package/src/grid/DotGrid.js +148 -0
  56. package/src/grid/GridFactory.js +173 -0
  57. package/src/grid/LineGrid.js +115 -0
  58. package/src/index.js +2 -0
  59. package/src/moodboard/ActionHandler.js +114 -0
  60. package/src/moodboard/DataManager.js +114 -0
  61. package/src/moodboard/MoodBoard.js +359 -0
  62. package/src/moodboard/WorkspaceManager.js +103 -0
  63. package/src/objects/BaseObject.js +1 -0
  64. package/src/objects/CommentObject.js +115 -0
  65. package/src/objects/DrawingObject.js +114 -0
  66. package/src/objects/EmojiObject.js +98 -0
  67. package/src/objects/FileObject.js +318 -0
  68. package/src/objects/FrameObject.js +127 -0
  69. package/src/objects/ImageObject.js +72 -0
  70. package/src/objects/NoteObject.js +227 -0
  71. package/src/objects/ObjectFactory.js +61 -0
  72. package/src/objects/ShapeObject.js +134 -0
  73. package/src/objects/StampObject.js +0 -0
  74. package/src/objects/StickerObject.js +0 -0
  75. package/src/objects/TextObject.js +123 -0
  76. package/src/services/BoardService.js +85 -0
  77. package/src/services/FileUploadService.js +398 -0
  78. package/src/services/FrameService.js +138 -0
  79. package/src/services/ImageUploadService.js +246 -0
  80. package/src/services/ZOrderManager.js +50 -0
  81. package/src/services/ZoomPanController.js +78 -0
  82. package/src/src.7z +0 -0
  83. package/src/src.zip +0 -0
  84. package/src/src2.zip +0 -0
  85. package/src/tools/AlignmentGuides.js +326 -0
  86. package/src/tools/BaseTool.js +257 -0
  87. package/src/tools/ResizeHandles.js +381 -0
  88. package/src/tools/ToolManager.js +580 -0
  89. package/src/tools/board-tools/PanTool.js +43 -0
  90. package/src/tools/board-tools/ZoomTool.js +393 -0
  91. package/src/tools/object-tools/DrawingTool.js +404 -0
  92. package/src/tools/object-tools/PlacementTool.js +1005 -0
  93. package/src/tools/object-tools/SelectTool.js +2183 -0
  94. package/src/tools/object-tools/TextTool.js +416 -0
  95. package/src/tools/object-tools/selection/BoxSelectController.js +105 -0
  96. package/src/tools/object-tools/selection/GeometryUtils.js +101 -0
  97. package/src/tools/object-tools/selection/GroupDragController.js +61 -0
  98. package/src/tools/object-tools/selection/GroupResizeController.js +90 -0
  99. package/src/tools/object-tools/selection/GroupRotateController.js +61 -0
  100. package/src/tools/object-tools/selection/HandlesSync.js +96 -0
  101. package/src/tools/object-tools/selection/ResizeController.js +68 -0
  102. package/src/tools/object-tools/selection/RotateController.js +58 -0
  103. package/src/tools/object-tools/selection/SelectionModel.js +42 -0
  104. package/src/tools/object-tools/selection/SimpleDragController.js +45 -0
  105. package/src/ui/CommentPopover.js +187 -0
  106. package/src/ui/ContextMenu.js +340 -0
  107. package/src/ui/FilePropertiesPanel.js +298 -0
  108. package/src/ui/FramePropertiesPanel.js +462 -0
  109. package/src/ui/HtmlHandlesLayer.js +778 -0
  110. package/src/ui/HtmlTextLayer.js +279 -0
  111. package/src/ui/MapPanel.js +290 -0
  112. package/src/ui/NotePropertiesPanel.js +502 -0
  113. package/src/ui/SaveStatus.js +250 -0
  114. package/src/ui/TextPropertiesPanel.js +911 -0
  115. package/src/ui/Toolbar.js +1118 -0
  116. package/src/ui/Topbar.js +220 -0
  117. package/src/ui/ZoomPanel.js +116 -0
  118. package/src/ui/styles/workspace.css +854 -0
  119. package/src/utils/colors.js +0 -0
  120. package/src/utils/geometry.js +0 -0
  121. package/src/utils/iconLoader.js +270 -0
  122. package/src/utils/objectIdGenerator.js +17 -0
  123. package/src/utils/topbarIconLoader.js +114 -0
@@ -0,0 +1,2183 @@
1
+ import { calculateNewSize, calculatePositionOffset } from './selection/GeometryUtils.js';
2
+ import { BaseTool } from '../BaseTool.js';
3
+ import { ResizeHandles } from '../ResizeHandles.js';
4
+ import * as PIXI from 'pixi.js';
5
+ import { Events } from '../../core/events/Events.js';
6
+ import { SelectionModel } from './selection/SelectionModel.js';
7
+ import { HandlesSync } from './selection/HandlesSync.js';
8
+ import { SimpleDragController } from './selection/SimpleDragController.js';
9
+ import { ResizeController } from './selection/ResizeController.js';
10
+ import { RotateController } from './selection/RotateController.js';
11
+ import { GroupResizeController } from './selection/GroupResizeController.js';
12
+ import { GroupRotateController } from './selection/GroupRotateController.js';
13
+ import { GroupDragController } from './selection/GroupDragController.js';
14
+ import { BoxSelectController } from './selection/BoxSelectController.js';
15
+
16
+ /**
17
+ * Инструмент выделения и работы с объектами
18
+ * Основной инструмент для выделения, перемещения, изменения размера и поворота объектов
19
+ */
20
+ export class SelectTool extends BaseTool {
21
+ constructor(eventBus) {
22
+ super('select', eventBus);
23
+ this.cursor = 'default';
24
+ this.hotkey = 'v';
25
+
26
+ // Состояние выделения перенесено в модель
27
+ this.selection = new SelectionModel();
28
+ this.isMultiSelect = false;
29
+
30
+ // Режим Alt-клонирования при перетаскивании
31
+ // Если Alt зажат при начале drag, создаем копию и перетаскиваем именно её
32
+ this.isAltCloneMode = false; // активен ли режим Alt-клона
33
+ this.clonePending = false; // ожидаем подтверждение создания копии
34
+ this.cloneRequested = false; // запрос на создание копии уже отправлен
35
+ this.cloneSourceId = null; // исходный объект для копии
36
+ // Групповой Alt-клон
37
+ this.isAltGroupCloneMode = false;
38
+ this.groupClonePending = false;
39
+ this.groupCloneOriginalIds = [];
40
+ this.groupCloneMap = null; // { originalId: newId }
41
+
42
+ // Состояние перетаскивания
43
+ this.isDragging = false;
44
+ this.dragOffset = { x: 0, y: 0 };
45
+ this.dragTarget = null;
46
+
47
+ // Состояние изменения размера
48
+ this.isResizing = false;
49
+ this.resizeHandle = null;
50
+ this.resizeStartBounds = null;
51
+ this.resizeStartMousePos = null;
52
+ this.resizeStartPosition = null;
53
+
54
+ // Система ручек изменения размера
55
+ this.resizeHandles = null;
56
+ this.groupSelectionGraphics = null; // визуализация рамок при множественном выделении
57
+ this.groupBoundsGraphics = null; // невидимая геометрия для ручек группы
58
+ this.groupId = '__group__';
59
+ this.isGroupDragging = false;
60
+ this.isGroupResizing = false;
61
+ this.isGroupRotating = false;
62
+ this.groupStartBounds = null;
63
+ this.groupStartMouse = null;
64
+ this.groupDragOffset = null;
65
+ this.groupObjectsInitial = null; // Map id -> { position, size, rotation }
66
+
67
+ // Текущие координаты мыши
68
+ this.currentX = 0;
69
+ this.currentY = 0;
70
+
71
+ // Состояние поворота
72
+ this.isRotating = false;
73
+ this.rotateCenter = null;
74
+ this.rotateStartAngle = 0;
75
+ this.rotateCurrentAngle = 0;
76
+ this.rotateStartMouseAngle = 0;
77
+
78
+ // Состояние рамки выделения
79
+ this.isBoxSelect = false;
80
+ this.selectionBox = null;
81
+ this.selectionGraphics = null; // PIXI.Graphics для визуализации рамки
82
+ this.initialSelectionBeforeBox = null; // снимок выделения перед началом box-select
83
+
84
+ // Подписка на событие готовности дубликата (от Core)
85
+ // Когда PasteObjectCommand завершится, ядро сообщит newId
86
+ if (this.eventBus) {
87
+ this.eventBus.on(Events.Tool.DuplicateReady, (data) => {
88
+ // data: { originalId, newId }
89
+ if (!this.isAltCloneMode || !this.clonePending) return;
90
+ if (!data || data.originalId !== this.cloneSourceId) return;
91
+ this.onDuplicateReady(data.newId);
92
+ });
93
+ // Групповой клон готов
94
+ this.eventBus.on(Events.Tool.GroupDuplicateReady, (data) => {
95
+ // data: { map: { [originalId]: newId } }
96
+ if (!this.isAltGroupCloneMode || !this.groupClonePending) return;
97
+ if (!data || !data.map) return;
98
+ this.onGroupDuplicateReady(data.map);
99
+ });
100
+ this.eventBus.on(Events.Tool.ObjectEdit, (object) => {
101
+ // Определяем тип редактируемого объекта
102
+ const objectType = object.type || (object.object && object.object.type) || 'text';
103
+
104
+ if (objectType === 'file') {
105
+ // Для файлов используем специальный редактор названия
106
+ this._openFileNameEditor(object, object.create || false);
107
+ } else {
108
+ // Для текста и записок используем обычный редактор
109
+ if (object.create) {
110
+ // Создание нового объекта с редактированием
111
+ this._openTextEditor(object, true);
112
+ } else {
113
+ // Редактирование существующего объекта
114
+ this._openTextEditor(object, false);
115
+ }
116
+ }
117
+ });
118
+ }
119
+ this.textEditor = {
120
+ active: false,
121
+ objectId: null,
122
+ textarea: null,
123
+ wrapper: null,
124
+ world: null,
125
+ position: null, // world top-left
126
+ properties: null, // { fontSize }
127
+ objectType: 'text', // 'text' or 'note'
128
+ isResizing: false,
129
+ };
130
+ }
131
+
132
+ /**
133
+ * Активация инструмента
134
+ */
135
+ activate(app) {
136
+ super.activate();
137
+ console.log('🔧 SelectTool активирован, app:', !!app);
138
+ // Сохраняем ссылку на PIXI app для оверлеев (рамка выделения)
139
+ this.app = app;
140
+
141
+ // Устанавливаем стандартный курсор для select инструмента
142
+ if (this.app && this.app.view) {
143
+ this.app.view.style.cursor = 'default';
144
+ }
145
+
146
+ // Инициализируем систему ручек изменения размера
147
+ if (!this.resizeHandles && app) {
148
+ this.resizeHandles = new ResizeHandles(app);
149
+ this._handlesSync = new HandlesSync({
150
+ app,
151
+ resizeHandles: this.resizeHandles,
152
+ selection: this.selection,
153
+ emit: (event, payload) => this.emit(event, payload)
154
+ });
155
+ this._dragCtrl = new SimpleDragController({
156
+ emit: (event, payload) => this.emit(event, payload)
157
+ });
158
+ this._resizeCtrl = new ResizeController({
159
+ emit: (event, payload) => this.emit(event, payload),
160
+ getRotation: (objectId) => {
161
+ const d = { objectId, rotation: 0 };
162
+ this.emit(Events.Tool.GetObjectRotation, d);
163
+ return d.rotation || 0;
164
+ }
165
+ });
166
+ this._rotateCtrl = new RotateController({
167
+ emit: (event, payload) => this.emit(event, payload)
168
+ });
169
+ this._groupResizeCtrl = new GroupResizeController({
170
+ emit: (event, payload) => this.emit(event, payload),
171
+ selection: this.selection,
172
+ getGroupBounds: () => this.computeGroupBounds(),
173
+ ensureGroupGraphics: (b) => this.ensureGroupBoundsGraphics(b),
174
+ updateGroupGraphics: (b) => this.updateGroupBoundsGraphics(b)
175
+ });
176
+ this._groupRotateCtrl = new GroupRotateController({
177
+ emit: (event, payload) => this.emit(event, payload),
178
+ selection: this.selection,
179
+ getGroupBounds: () => this.computeGroupBounds(),
180
+ ensureGroupGraphics: (b) => this.ensureGroupBoundsGraphics(b),
181
+ updateHandles: () => { if (this.resizeHandles) this.resizeHandles.updateHandles(); }
182
+ });
183
+ this._groupDragCtrl = new GroupDragController({
184
+ emit: (event, payload) => this.emit(event, payload),
185
+ selection: this.selection,
186
+ updateGroupBoundsByTopLeft: (pos) => this.updateGroupBoundsGraphicsByTopLeft(pos)
187
+ });
188
+ this._boxSelect = new BoxSelectController({
189
+ app,
190
+ selection: this.selection,
191
+ emit: (event, payload) => this.emit(event, payload),
192
+ setSelection: (ids) => this.setSelection(ids),
193
+ clearSelection: () => this.clearSelection(),
194
+ rectIntersectsRect: (a, b) => this.rectIntersectsRect(a, b)
195
+ });
196
+ } else if (!app) {
197
+ console.log('❌ PIXI app не передан в activate');
198
+ } else {
199
+ console.log('ℹ️ ResizeHandles уже созданы');
200
+ }
201
+ }
202
+
203
+ // Удобные врапперы вокруг SelectionModel (для минимальных правок ниже)
204
+ _has(id) { return this.selection.has(id); }
205
+ _size() { return this.selection.size(); }
206
+ _ids() { return this.selection.toArray(); }
207
+ _clear() { this.selection.clear(); }
208
+ _add(id) { this.selection.add(id); }
209
+ _addMany(ids) { this.selection.addMany(ids); }
210
+ _remove(id) { this.selection.remove(id); }
211
+ _toggle(id) { this.selection.toggle(id); }
212
+ _computeGroupBounds(getPixiById) { return this.selection.computeBounds(getPixiById); }
213
+
214
+ /**
215
+ * Деактивация инструмента
216
+ */
217
+ deactivate() {
218
+ super.deactivate();
219
+
220
+ // Закрываем текстовый/файловый редактор если открыт
221
+ if (this.textEditor.active) {
222
+ if (this.textEditor.objectType === 'file') {
223
+ this._closeFileNameEditor(true);
224
+ } else {
225
+ this._closeTextEditor(true);
226
+ }
227
+ }
228
+
229
+ // Очищаем выделение и ручки
230
+ this.clearSelection();
231
+ if (this.resizeHandles) {
232
+ this.resizeHandles.hideHandles();
233
+ }
234
+
235
+ // Сбрасываем курсор на стандартный
236
+ if (this.app && this.app.view) {
237
+ this.app.view.style.cursor = '';
238
+ }
239
+ }
240
+
241
+ /**
242
+ * Нажатие кнопки мыши
243
+ */
244
+ onMouseDown(event) {
245
+ super.onMouseDown(event);
246
+
247
+ // Если активен текстовый редактор, закрываем его при клике вне
248
+ if (this.textEditor.active) {
249
+ console.log('🔧 SelectTool: closing text editor on mouse down, objectType:', this.textEditor.objectType, 'objectId:', this.textEditor.objectId);
250
+ if (this.textEditor.objectType === 'file') {
251
+ this._closeFileNameEditor(true);
252
+ } else {
253
+ this._closeTextEditor(true);
254
+ }
255
+ return; // Прерываем выполнение, чтобы не обрабатывать клик дальше
256
+ }
257
+
258
+ this.isMultiSelect = event.originalEvent.ctrlKey || event.originalEvent.metaKey;
259
+
260
+ // Проверяем, что под курсором
261
+ const hitResult = this.hitTest(event.x, event.y);
262
+
263
+ if (hitResult.type === 'resize-handle') {
264
+ this.startResize(hitResult.handle, hitResult.object);
265
+ } else if (hitResult.type === 'rotate-handle') {
266
+ this.startRotate(hitResult.object);
267
+ } else if (this.selection.size() > 1) {
268
+ // Особая логика для группового выделения: клики внутри общей рамки не снимают выделение
269
+ const gb = this.computeGroupBounds();
270
+ const insideGroup = this.isPointInBounds({ x: event.x, y: event.y }, { x: gb.x, y: gb.y, width: gb.width, height: gb.height });
271
+ if (insideGroup) {
272
+ // Если клик внутри группы (по объекту или пустому месту), сохраняем выделение и начинаем перетаскивание группы
273
+ this.startGroupDrag(event);
274
+ return;
275
+ }
276
+ // Вне группы — обычная логика
277
+ if (hitResult.type === 'object') {
278
+ this.handleObjectSelect(hitResult.object, event);
279
+ } else {
280
+ this.startBoxSelect(event);
281
+ }
282
+ } else if (hitResult.type === 'object') {
283
+ // Начинаем обычный drag исходника; Alt-режим включим на лету при движении
284
+ this.handleObjectSelect(hitResult.object, event);
285
+ } else {
286
+ // Клик по пустому месту — если есть одиночное выделение, разрешаем drag за пределами объекта в пределах рамки
287
+ if (this.selection.size() === 1) {
288
+ const selId = this.selection.toArray()[0];
289
+ const boundsReq = { objects: [] };
290
+ this.emit(Events.Tool.GetAllObjects, boundsReq);
291
+ const map = new Map(boundsReq.objects.map(o => [o.id, o.bounds]));
292
+ const b = map.get(selId);
293
+ if (b && this.isPointInBounds({ x: event.x, y: event.y }, b)) {
294
+ // Старт перетаскивания как если бы кликнули по объекту
295
+ this.startDrag(selId, event);
296
+ return;
297
+ }
298
+ }
299
+ // Иначе — начинаем рамку выделения
300
+ this.startBoxSelect(event);
301
+ }
302
+ }
303
+
304
+ /**
305
+ * Перемещение мыши
306
+ */
307
+ onMouseMove(event) {
308
+ super.onMouseMove(event);
309
+
310
+ // Обновляем текущие координаты мыши
311
+ this.currentX = event.x;
312
+ this.currentY = event.y;
313
+
314
+ if (this.isResizing || this.isGroupResizing) {
315
+ this.updateResize(event);
316
+ } else if (this.isRotating || this.isGroupRotating) {
317
+ this.updateRotate(event);
318
+ } else if (this.isDragging || this.isGroupDragging) {
319
+ this.updateDrag(event);
320
+ } else if (this.isBoxSelect) {
321
+ this.updateBoxSelect(event);
322
+ } else {
323
+ // Обновляем курсор в зависимости от того, что под мышью
324
+ this.updateCursor(event);
325
+ }
326
+ }
327
+
328
+ /**
329
+ * Отпускание кнопки мыши
330
+ */
331
+ onMouseUp(event) {
332
+ if (this.isResizing || this.isGroupResizing) {
333
+ this.endResize();
334
+ } else if (this.isRotating || this.isGroupRotating) {
335
+ this.endRotate();
336
+ } else if (this.isDragging || this.isGroupDragging) {
337
+ this.endDrag();
338
+ } else if (this.isBoxSelect) {
339
+ this.endBoxSelect();
340
+ }
341
+
342
+ super.onMouseUp(event);
343
+ }
344
+
345
+ /**
346
+ * Двойной клик - переход в режим редактирования
347
+ */
348
+ onDoubleClick(event) {
349
+ const hitResult = this.hitTest(event.x, event.y);
350
+
351
+ if (hitResult.type === 'object') {
352
+ // если это текст или записка — войдём в режим редактирования через ObjectEdit
353
+ const req = { objectId: hitResult.object, pixiObject: null };
354
+ this.emit(Events.Tool.GetObjectPixi, req);
355
+ const pix = req.pixiObject;
356
+
357
+ const isText = !!(pix && pix._mb && pix._mb.type === 'text');
358
+ const isNote = !!(pix && pix._mb && pix._mb.type === 'note');
359
+ const isFile = !!(pix && pix._mb && pix._mb.type === 'file');
360
+
361
+ if (isText) {
362
+ // Получаем позицию объекта для редактирования
363
+ const posData = { objectId: hitResult.object, position: null };
364
+ this.emit(Events.Tool.GetObjectPosition, posData);
365
+
366
+ // Получаем содержимое из properties объекта
367
+ const textContent = pix._mb?.properties?.content || '';
368
+
369
+ this.emit(Events.Tool.ObjectEdit, {
370
+ id: hitResult.object,
371
+ type: 'text',
372
+ position: posData.position,
373
+ properties: { content: textContent },
374
+ create: false
375
+ });
376
+ return;
377
+ }
378
+ if (isNote) {
379
+ const noteProps = pix._mb.properties || {};
380
+ // Получаем позицию объекта для редактирования
381
+ const posData = { objectId: hitResult.object, position: null };
382
+ this.emit(Events.Tool.GetObjectPosition, posData);
383
+
384
+ this.emit(Events.Tool.ObjectEdit, {
385
+ id: hitResult.object,
386
+ type: 'note',
387
+ position: posData.position,
388
+ properties: { content: noteProps.content || '' },
389
+ create: false
390
+ });
391
+ return;
392
+ }
393
+
394
+ if (isFile) {
395
+ const fileProps = pix._mb.properties || {};
396
+ // Получаем позицию объекта для редактирования
397
+ const posData = { objectId: hitResult.object, position: null };
398
+ this.emit(Events.Tool.GetObjectPosition, posData);
399
+
400
+ this.emit(Events.Tool.ObjectEdit, {
401
+ id: hitResult.object,
402
+ type: 'file',
403
+ position: posData.position,
404
+ properties: { fileName: fileProps.fileName || 'Untitled' },
405
+ create: false
406
+ });
407
+ return;
408
+ }
409
+ this.editObject(hitResult.object);
410
+ }
411
+ }
412
+
413
+ /**
414
+ * Контекстное меню (правая кнопка) — пока пустое, только определяем контекст
415
+ */
416
+ onContextMenu(event) {
417
+ // Определяем, что под курсором
418
+ const hit = this.hitTest(event.x, event.y);
419
+ let context = 'canvas';
420
+ let targetId = null;
421
+ if (hit && hit.type === 'object' && hit.object) {
422
+ targetId = hit.object;
423
+ if (this.selection.has(targetId) && this.selection.size() > 1) {
424
+ context = 'group';
425
+ } else {
426
+ context = 'object';
427
+ }
428
+ } else if (this.selection.size() > 1) {
429
+ context = 'group';
430
+ }
431
+ // Сообщаем ядру/UI, что нужно показать контекстное меню (пока без пунктов)
432
+ this.emit(Events.Tool.ContextMenuShow, { x: event.x, y: event.y, context, targetId });
433
+ }
434
+
435
+ /**
436
+ * Обработка клавиш
437
+ */
438
+ onKeyDown(event) {
439
+ // Проверяем, не активен ли текстовый редактор (редактирование названия файла или текста)
440
+ if (this.textEditor && this.textEditor.active) {
441
+ console.log('🔒 SelectTool: Текстовый редактор активен, пропускаем обработку клавиш');
442
+ return; // Не обрабатываем клавиши во время редактирования
443
+ }
444
+
445
+ switch (event.key) {
446
+ case 'Delete':
447
+ case 'Backspace':
448
+ this.deleteSelectedObjects();
449
+ break;
450
+
451
+ case 'a':
452
+ if (event.ctrlKey) {
453
+ this.selectAll();
454
+ event.originalEvent.preventDefault();
455
+ }
456
+ break;
457
+
458
+ case 'Escape':
459
+ this.clearSelection();
460
+ break;
461
+ }
462
+ }
463
+
464
+ /**
465
+ * Тестирование попадания курсора
466
+ */
467
+ hitTest(x, y) {
468
+ // Сначала проверяем ручки изменения размера (они имеют приоритет)
469
+ if (this.resizeHandles) {
470
+ const pixiObjectAtPoint = this.getPixiObjectAt(x, y);
471
+
472
+
473
+ const handleInfo = this.resizeHandles.getHandleInfo(pixiObjectAtPoint);
474
+ if (handleInfo) {
475
+
476
+
477
+ // Определяем тип ручки
478
+ const hitType = handleInfo.type === 'rotate' ? 'rotate-handle' : 'resize-handle';
479
+
480
+ return {
481
+ type: hitType,
482
+ handle: handleInfo.type,
483
+ object: handleInfo.targetObjectId,
484
+ pixiObject: handleInfo.handle
485
+ };
486
+ }
487
+ }
488
+
489
+ // Получаем объекты из системы через событие
490
+ const hitTestData = { x, y, result: null };
491
+ this.emit(Events.Tool.HitTest, hitTestData);
492
+
493
+ if (hitTestData.result && hitTestData.result.object) {
494
+ return hitTestData.result;
495
+ }
496
+
497
+ return { type: 'empty' };
498
+ }
499
+
500
+ /**
501
+ * Получить PIXI объект по координатам (для внутреннего использования)
502
+ */
503
+ getPixiObjectAt(x, y) {
504
+ if (!this.resizeHandles || !this.resizeHandles.app) return null;
505
+
506
+ const point = new PIXI.Point(x, y);
507
+
508
+ // Сначала ищем в контейнере ручек (приоритет)
509
+ if (this.resizeHandles.container.visible) {
510
+ for (let i = this.resizeHandles.container.children.length - 1; i >= 0; i--) {
511
+ const child = this.resizeHandles.container.children[i];
512
+
513
+ // Проверяем обычные объекты
514
+ if (child.containsPoint && child.containsPoint(point)) {
515
+
516
+ return child;
517
+ }
518
+
519
+ // Специальная проверка для контейнеров (ручка вращения)
520
+ if (child instanceof PIXI.Container && child.children.length > 0) {
521
+ // Проверяем границы контейнера
522
+ const bounds = child.getBounds();
523
+ if (point.x >= bounds.x && point.x <= bounds.x + bounds.width &&
524
+ point.y >= bounds.y && point.y <= bounds.y + bounds.height) {
525
+
526
+ return child;
527
+ }
528
+ }
529
+ }
530
+ }
531
+
532
+ // Затем ищем в основной сцене
533
+ const stage = this.resizeHandles.app.stage;
534
+ for (let i = stage.children.length - 1; i >= 0; i--) {
535
+ const child = stage.children[i];
536
+ if (child !== this.resizeHandles.container && child.containsPoint && child.containsPoint(point)) {
537
+
538
+ return child;
539
+ }
540
+ }
541
+
542
+ return null;
543
+ }
544
+
545
+ /**
546
+ * Обработка выделения объекта
547
+ */
548
+ handleObjectSelect(objectId, event) {
549
+ if (!this.isMultiSelect) {
550
+ this.clearSelection();
551
+ }
552
+
553
+ if (this.selection.has(objectId)) {
554
+ if (this.isMultiSelect) {
555
+ this.removeFromSelection(objectId);
556
+ } else if (this.selection.size() > 1) {
557
+ // Перетаскивание группы
558
+ this.startGroupDrag(event);
559
+ } else {
560
+ // Начинаем перетаскивание
561
+ this.startDrag(objectId, event);
562
+ }
563
+ } else {
564
+ this.addToSelection(objectId);
565
+ if (this.selection.size() > 1) {
566
+ this.startGroupDrag(event);
567
+ } else {
568
+ this.startDrag(objectId, event);
569
+ }
570
+ }
571
+ }
572
+
573
+ /**
574
+ * Начало перетаскивания
575
+ */
576
+ startDrag(objectId, event) {
577
+ this.isDragging = true;
578
+ this.dragTarget = objectId;
579
+
580
+ // Получаем текущую позицию объекта
581
+ const objectData = { objectId, position: null };
582
+ this.emit(Events.Tool.GetObjectPosition, objectData);
583
+ // Нормализуем координаты в мировые (worldLayer), чтобы убрать влияние зума
584
+ const w = this._toWorld(event.x, event.y);
585
+ const worldEvent = { ...event, x: w.x, y: w.y };
586
+ if (this._dragCtrl) this._dragCtrl.start(objectId, worldEvent);
587
+ }
588
+
589
+ /**
590
+ * Обновление перетаскивания
591
+ */
592
+ updateDrag(event) {
593
+ // Перетаскивание группы
594
+ if (this.isGroupDragging && this._groupDragCtrl) {
595
+ const w = this._toWorld(event.x, event.y);
596
+ this._groupDragCtrl.update({ ...event, x: w.x, y: w.y });
597
+ return;
598
+ }
599
+ // Если во время обычного перетаскивания зажали Alt — включаем режим клонирования на лету
600
+ if (this.isDragging && !this.isAltCloneMode && event.originalEvent && event.originalEvent.altKey) {
601
+ this.isAltCloneMode = true;
602
+ this.cloneSourceId = this.dragTarget;
603
+ this.clonePending = true;
604
+ // Запрашиваем текущую позицию исходного объекта
605
+ const positionData = { objectId: this.cloneSourceId, position: null };
606
+ this.emit(Events.Tool.GetObjectPosition, positionData);
607
+ // Сообщаем ядру о необходимости создать дубликат у позиции исходного объекта
608
+ this.emit(Events.Tool.DuplicateRequest, {
609
+ originalId: this.cloneSourceId,
610
+ position: positionData.position || { x: event.x, y: event.y }
611
+ });
612
+ // Не сбрасываем dragTarget, чтобы исходник продолжал двигаться до появления копии
613
+ // Визуально это ок: копия появится и захватит drag в onDuplicateReady
614
+ }
615
+ // Если ожидаем создание копии — продолжаем двигать текущую цель (исходник)
616
+ if (!this.dragTarget) return;
617
+
618
+ if (this._dragCtrl) {
619
+ const w = this._toWorld(event.x, event.y);
620
+ this._dragCtrl.update({ ...event, x: w.x, y: w.y });
621
+ }
622
+
623
+ // Обновляем ручки во время перетаскивания
624
+ if (this.resizeHandles && this.selection.has(this.dragTarget)) {
625
+ this.resizeHandles.updateHandles();
626
+ }
627
+ }
628
+
629
+ /**
630
+ * Завершение перетаскивания
631
+ */
632
+ endDrag() {
633
+ if (this.isGroupDragging) {
634
+ const ids = this.selection.toArray();
635
+ this.emit(Events.Tool.GroupDragEnd, { objects: ids });
636
+ if (this._groupDragCtrl) this._groupDragCtrl.end();
637
+ this.isAltGroupCloneMode = false;
638
+ this.groupClonePending = false;
639
+ this.groupCloneOriginalIds = [];
640
+ this.groupCloneMap = null;
641
+ } else if (this.dragTarget) {
642
+ if (this._dragCtrl) this._dragCtrl.end();
643
+ }
644
+
645
+ this.isDragging = false;
646
+ this.isGroupDragging = false;
647
+ this.dragTarget = null;
648
+ this.dragOffset = { x: 0, y: 0 };
649
+ // Сбрасываем состояние Alt-клона
650
+ this.isAltCloneMode = false;
651
+ this.clonePending = false;
652
+ this.cloneSourceId = null;
653
+ }
654
+
655
+ /**
656
+ * Начало изменения размера
657
+ */
658
+ startResize(handle, objectId) {
659
+ console.log(`🔧 Начинаем resize: ручка ${handle}, объект ${objectId}`);
660
+ // Групповой resize
661
+ if (objectId === this.groupId && this.selection.size() > 1) {
662
+ this.isGroupResizing = true;
663
+ this.resizeHandle = handle;
664
+ if (this._groupResizeCtrl) this._groupResizeCtrl.start(handle, { x: this.currentX, y: this.currentY });
665
+ this.isResizing = false;
666
+ return;
667
+ }
668
+
669
+ this.isResizing = true;
670
+ this.resizeHandle = handle;
671
+ this.dragTarget = objectId;
672
+ if (this._resizeCtrl) {
673
+ const w = this._toWorld(this.currentX, this.currentY);
674
+ this._resizeCtrl.start(handle, objectId, { x: w.x, y: w.y });
675
+ }
676
+ }
677
+
678
+ /**
679
+ * Обновление изменения размера
680
+ */
681
+ updateResize(event) {
682
+ // Групповой resize
683
+ if (this.isGroupResizing && this._groupResizeCtrl) {
684
+ const w = this._toWorld(event.x, event.y);
685
+ this._groupResizeCtrl.update({ ...event, x: w.x, y: w.y });
686
+ return;
687
+ }
688
+
689
+ if (this._resizeCtrl) {
690
+ const w = this._toWorld(event.x, event.y);
691
+ this._resizeCtrl.update({ ...event, x: w.x, y: w.y }, {
692
+ calculateNewSize: (handleType, startBounds, dx, dy, keepAR) => {
693
+ const rot = (() => { const d = { objectId: this.dragTarget, rotation: 0 }; this.emit(Events.Tool.GetObjectRotation, d); return d.rotation || 0; })();
694
+ return this.calculateNewSize(handleType, startBounds, dx, dy, keepAR, rot);
695
+ },
696
+ calculatePositionOffset: (handleType, startBounds, newSize, objectRotation) => {
697
+ return this.calculatePositionOffset(handleType, startBounds, newSize, objectRotation);
698
+ }
699
+ });
700
+ }
701
+
702
+ // Обновляем ручки в реальном времени во время resize
703
+ if (this.resizeHandles) {
704
+ this.resizeHandles.updateHandles();
705
+ }
706
+ }
707
+
708
+ /**
709
+ * Завершение изменения размера
710
+ */
711
+ endResize() {
712
+ if (this.isGroupResizing) {
713
+ if (this._groupResizeCtrl) this._groupResizeCtrl.end();
714
+ this.isGroupResizing = false;
715
+ this.resizeHandle = null;
716
+ this.groupStartBounds = null;
717
+ this.groupStartMouse = null;
718
+ this.groupObjectsInitial = null;
719
+ // Принудительно синхронизируем ручки и рамку после завершения, чтобы отлипли от курсора
720
+ const gb = this.computeGroupBounds();
721
+ this.ensureGroupBoundsGraphics(gb);
722
+ if (this.groupBoundsGraphics) {
723
+ this.groupBoundsGraphics.rotation = 0;
724
+ this.groupBoundsGraphics.pivot.set(0, 0);
725
+ this.groupBoundsGraphics.position.set(gb.x, gb.y);
726
+ }
727
+ if (this.resizeHandles) {
728
+ this.resizeHandles.showHandles(this.groupBoundsGraphics, this.groupId);
729
+ }
730
+ return;
731
+ }
732
+ if (this._resizeCtrl) this._resizeCtrl.end();
733
+
734
+ // Обновляем позицию ручек после resize
735
+ if (this.resizeHandles) {
736
+ this.resizeHandles.updateHandles(); // Обновляем позицию ручек
737
+ }
738
+
739
+ this.isResizing = false;
740
+ this.resizeHandle = null;
741
+ this.resizeStartBounds = null;
742
+ this.resizeStartMousePos = null;
743
+ this.resizeStartPosition = null;
744
+ }
745
+
746
+ /**
747
+ * Начало поворота
748
+ */
749
+ startRotate(objectId) {
750
+ // Групповой поворот
751
+ if (objectId === this.groupId && this.selection.size() > 1) {
752
+ this.isGroupRotating = true;
753
+ const gb = this.computeGroupBounds();
754
+ this.groupRotateBounds = gb;
755
+ this.rotateCenter = { x: gb.x + gb.width / 2, y: gb.y + gb.height / 2 };
756
+ this.rotateStartAngle = 0;
757
+ this.rotateCurrentAngle = 0;
758
+ this.rotateStartMouseAngle = Math.atan2(
759
+ this.currentY - this.rotateCenter.y,
760
+ this.currentX - this.rotateCenter.x
761
+ );
762
+ // Настраиваем целевой прямоугольник для ручек: центр в pivot для корректного вращения
763
+ this.ensureGroupBoundsGraphics(gb);
764
+ if (this.groupBoundsGraphics) {
765
+ this.groupBoundsGraphics.pivot.set(gb.width / 2, gb.height / 2);
766
+ this.groupBoundsGraphics.position.set(this.rotateCenter.x, this.rotateCenter.y);
767
+ this.groupBoundsGraphics.rotation = 0;
768
+ }
769
+ // Подгоняем визуальную рамку под центр
770
+ if (this.groupSelectionGraphics) {
771
+ this.groupSelectionGraphics.pivot.set(0, 0);
772
+ this.groupSelectionGraphics.position.set(0, 0);
773
+ this.groupSelectionGraphics.clear();
774
+ this.groupSelectionGraphics.lineStyle(1, 0x3B82F6, 1);
775
+ // Нарисуем пока осевую рамку, вращение применим в update
776
+ this.groupSelectionGraphics.drawRect(gb.x, gb.y, gb.width, gb.height);
777
+ }
778
+ const ids = this.selection.toArray();
779
+ this.emit('group:rotate:start', { objects: ids, center: this.rotateCenter });
780
+ return;
781
+ }
782
+
783
+ this.isRotating = true;
784
+ this.dragTarget = objectId; // Используем dragTarget для совместимости
785
+ const posData = { objectId, position: null };
786
+ this.emit('get:object:position', posData);
787
+ const sizeData = { objectId, size: null };
788
+ this.emit('get:object:size', sizeData);
789
+ if (posData.position && sizeData.size && this._rotateCtrl) {
790
+ const center = { x: posData.position.x + sizeData.size.width / 2, y: posData.position.y + sizeData.size.height / 2 };
791
+ const w = this._toWorld(this.currentX, this.currentY);
792
+ this._rotateCtrl.start(objectId, { x: w.x, y: w.y }, center);
793
+ }
794
+ }
795
+
796
+ /**
797
+ * Обновление поворота
798
+ */
799
+ updateRotate(event) {
800
+ // Групповой поворот
801
+ if (this.isGroupRotating && this._groupRotateCtrl) {
802
+ const w = this._toWorld(event.x, event.y);
803
+ this._groupRotateCtrl.update({ ...event, x: w.x, y: w.y });
804
+ return;
805
+ }
806
+ if (!this.isRotating || !this._rotateCtrl) return;
807
+ {
808
+ const w = this._toWorld(event.x, event.y);
809
+ this._rotateCtrl.update({ ...event, x: w.x, y: w.y });
810
+ }
811
+
812
+ // Обновляем ручки в реальном времени во время поворота
813
+ if (this.resizeHandles) {
814
+ this.resizeHandles.updateHandles();
815
+ }
816
+ }
817
+
818
+ /**
819
+ * Завершение поворота
820
+ */
821
+ endRotate() {
822
+ if (this.isGroupRotating) {
823
+ if (this._groupRotateCtrl) this._groupRotateCtrl.end();
824
+ this.isGroupRotating = false;
825
+ // Восстановление рамки
826
+ const gb = this.computeGroupBounds();
827
+ this.ensureGroupBoundsGraphics(gb);
828
+ if (this.groupBoundsGraphics) {
829
+ this.groupBoundsGraphics.rotation = 0;
830
+ this.groupBoundsGraphics.pivot.set(0, 0);
831
+ this.groupBoundsGraphics.position.set(gb.x, gb.y);
832
+ }
833
+ if (this.resizeHandles) this.resizeHandles.showHandles(this.groupBoundsGraphics, this.groupId);
834
+ return;
835
+ }
836
+ if (this._rotateCtrl) this._rotateCtrl.end();
837
+
838
+ // Обновляем позицию ручек после поворота
839
+ if (this.resizeHandles) {
840
+ this.resizeHandles.updateHandles(); // Обновляем позицию ручек
841
+ }
842
+
843
+ this.isRotating = false;
844
+ this.rotateCenter = null;
845
+ this.rotateStartAngle = 0;
846
+ this.rotateCurrentAngle = 0;
847
+ this.rotateStartMouseAngle = 0;
848
+ }
849
+
850
+ /**
851
+ * Начало рамки выделения
852
+ */
853
+ startBoxSelect(event) {
854
+ this.isBoxSelect = true;
855
+ if (this._boxSelect) this._boxSelect.start({ x: event.x, y: event.y }, this.isMultiSelect);
856
+ }
857
+
858
+ /**
859
+ * Обновление рамки выделения
860
+ */
861
+ updateBoxSelect(event) {
862
+ if (this._boxSelect) this._boxSelect.update({ x: event.x, y: event.y });
863
+ }
864
+
865
+ /**
866
+ * Завершение рамки выделения
867
+ */
868
+ endBoxSelect() {
869
+ this.isBoxSelect = false;
870
+ if (this._boxSelect) this._boxSelect.end();
871
+ }
872
+
873
+ /**
874
+ * Пересечение прямоугольников
875
+ */
876
+ rectIntersectsRect(a, b) {
877
+ return !(
878
+ b.x > a.x + a.width ||
879
+ b.x + b.width < a.x ||
880
+ b.y > a.y + a.height ||
881
+ b.y + b.height < a.y
882
+ );
883
+ }
884
+
885
+ /**
886
+ * Установить выделение списком ID за один раз (батч)
887
+ */
888
+ setSelection(objectIds) {
889
+ const prev = this.selection.toArray();
890
+ this.selection.clear();
891
+ this.selection.addMany(objectIds);
892
+ // Эмитим события для совместимости
893
+ if (prev.length > 0) {
894
+ this.emit(Events.Tool.SelectionClear, { objects: prev });
895
+ }
896
+ for (const id of objectIds) {
897
+ this.emit(Events.Tool.SelectionAdd, { object: id });
898
+ }
899
+ this.updateResizeHandles();
900
+ }
901
+
902
+ /**
903
+ * Рисует рамки вокруг всех выбранных объектов (для множественного выделения)
904
+ */
905
+ drawGroupSelectionGraphics() {
906
+ if (!this.app || !this.app.stage) return;
907
+ const selectedIds = this.selection.toArray();
908
+ if (selectedIds.length <= 1) {
909
+ this.removeGroupSelectionGraphics();
910
+ return;
911
+ }
912
+ // Получаем bounds всех объектов и отрисовываем контур на groupBoundsGraphics (одна рамка с ручками)
913
+ const request = { objects: [] };
914
+ this.emit(Events.Tool.GetAllObjects, request);
915
+ const idToBounds = new Map(request.objects.map(o => [o.id, o.bounds]));
916
+ let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
917
+ for (const id of selectedIds) {
918
+ const b = idToBounds.get(id);
919
+ if (!b) continue;
920
+ minX = Math.min(minX, b.x);
921
+ minY = Math.min(minY, b.y);
922
+ maxX = Math.max(maxX, b.x + b.width);
923
+ maxY = Math.max(maxY, b.y + b.height);
924
+ }
925
+ if (isFinite(minX) && isFinite(minY) && isFinite(maxX) && isFinite(maxY)) {
926
+ const gb = { x: minX, y: minY, width: maxX - minX, height: maxY - minY };
927
+ this.ensureGroupBoundsGraphics(gb);
928
+ this.updateGroupBoundsGraphics(gb);
929
+ }
930
+ }
931
+
932
+ /**
933
+ * Удаляет графику множественного выделения
934
+ */
935
+ removeGroupSelectionGraphics() {
936
+ if (this.groupBoundsGraphics) {
937
+ this.groupBoundsGraphics.clear();
938
+ this.groupBoundsGraphics.rotation = 0;
939
+ }
940
+ }
941
+
942
+ /**
943
+ * Вычисляет общие границы текущего множественного выделения
944
+ */
945
+ computeGroupBounds() {
946
+ const request = { objects: [] };
947
+ this.emit(Events.Tool.GetAllObjects, request);
948
+ const pixiMap = new Map(request.objects.map(o => [o.id, o.pixi]));
949
+ const b = this.selection.computeBounds((id) => pixiMap.get(id));
950
+ if (!b) return { x: 0, y: 0, width: 0, height: 0 };
951
+ return b;
952
+ }
953
+
954
+ ensureGroupBoundsGraphics(bounds) {
955
+ if (!this.app || !this.app.stage) return;
956
+ if (!this.groupBoundsGraphics) {
957
+ this.groupBoundsGraphics = new PIXI.Graphics();
958
+ this.groupBoundsGraphics.name = 'group-bounds';
959
+ this.groupBoundsGraphics.zIndex = 1400;
960
+ this.app.stage.addChild(this.groupBoundsGraphics);
961
+ this.app.stage.sortableChildren = true;
962
+ }
963
+ this.updateGroupBoundsGraphics(bounds);
964
+ }
965
+
966
+ updateGroupBoundsGraphics(bounds) {
967
+ if (!this.groupBoundsGraphics) return;
968
+ this.groupBoundsGraphics.clear();
969
+ // Прозрачная заливка (alpha ~0), чтобы getBounds() давал корректные размеры и не было артефактов
970
+ this.groupBoundsGraphics.beginFill(0x000000, 0.001);
971
+ this.groupBoundsGraphics.drawRect(0, 0, Math.max(1, bounds.width), Math.max(1, bounds.height));
972
+ this.groupBoundsGraphics.endFill();
973
+ // Размещаем графику в левом-верхнем углу группы
974
+ this.groupBoundsGraphics.position.set(bounds.x, bounds.y);
975
+ // Обновляем ручки, если показаны
976
+ if (this.resizeHandles) {
977
+ this.resizeHandles.updateHandles();
978
+ }
979
+ }
980
+
981
+ updateGroupBoundsGraphicsByTopLeft(topLeft) {
982
+ if (!this.groupBoundsGraphics || !this.groupStartBounds) return;
983
+ this.updateGroupBoundsGraphics({ x: topLeft.x, y: topLeft.y, width: this.groupStartBounds.width, height: this.groupStartBounds.height });
984
+ // Рисуем визуальную общую рамку одновременно
985
+ if (this.groupSelectionGraphics) {
986
+ this.groupSelectionGraphics.clear();
987
+ this.groupSelectionGraphics.lineStyle(1, 0x3B82F6, 0.9);
988
+ this.groupSelectionGraphics.drawRect(topLeft.x, topLeft.y, this.groupStartBounds.width, this.groupStartBounds.height);
989
+ }
990
+ }
991
+
992
+ // Преобразование экранных координат (canvas/view) в мировые (worldLayer)
993
+ _toWorld(x, y) {
994
+ if (!this.app || !this.app.stage) return { x, y };
995
+ const world = this.app.stage.getChildByName && this.app.stage.getChildByName('worldLayer');
996
+ if (!world || !world.toLocal) return { x, y };
997
+ const p = new PIXI.Point(x, y);
998
+ const local = world.toLocal(p);
999
+ return { x: local.x, y: local.y };
1000
+ }
1001
+
1002
+ startGroupDrag(event) {
1003
+ const gb = this.computeGroupBounds();
1004
+ this.groupStartBounds = gb;
1005
+ this.isGroupDragging = true;
1006
+ this.isDragging = false; // отключаем одиночный drag, если был
1007
+ this.ensureGroupBoundsGraphics(gb);
1008
+ if (this.groupBoundsGraphics && this.resizeHandles) {
1009
+ this.resizeHandles.showHandles(this.groupBoundsGraphics, this.groupId);
1010
+ }
1011
+ if (this._groupDragCtrl) {
1012
+ const w = this._toWorld(event.x, event.y);
1013
+ this._groupDragCtrl.start(gb, { x: w.x, y: w.y });
1014
+ }
1015
+ this.emit(Events.Tool.GroupDragStart, { objects: this.selection.toArray() });
1016
+ }
1017
+
1018
+ /**
1019
+ * Переключение на клон группы после готовности
1020
+ */
1021
+ onGroupDuplicateReady(idMap) {
1022
+ this.groupClonePending = false;
1023
+ this.groupCloneMap = idMap;
1024
+ if (this._groupDragCtrl) this._groupDragCtrl.onGroupDuplicateReady(idMap);
1025
+ // Формируем новое выделение из клонов
1026
+ const newIds = [];
1027
+ for (const orig of this.groupCloneOriginalIds) {
1028
+ const nid = idMap[orig];
1029
+ if (nid) newIds.push(nid);
1030
+ }
1031
+ if (newIds.length > 0) {
1032
+ this.setSelection(newIds);
1033
+ // Пересчитываем стартовые параметры для продолжения drag
1034
+ const gb = this.computeGroupBounds();
1035
+ this.groupStartBounds = gb;
1036
+ this.groupDragOffset = { x: this.currentX - gb.x, y: this.currentY - gb.y };
1037
+ // Сообщаем ядру о старте drag для новых объектов, чтобы зафиксировать начальные позиции
1038
+ this.emit('group:drag:start', { objects: newIds });
1039
+ }
1040
+ }
1041
+
1042
+ /**
1043
+ * Обновление курсора
1044
+ */
1045
+ updateCursor(event) {
1046
+ const hitResult = this.hitTest(event.x, event.y);
1047
+
1048
+ switch (hitResult.type) {
1049
+ case 'resize-handle':
1050
+ this.cursor = this.getResizeCursor(hitResult.handle);
1051
+ break;
1052
+ case 'rotate-handle':
1053
+ this.cursor = 'grab';
1054
+ break;
1055
+ case 'object':
1056
+ this.cursor = 'move';
1057
+ break;
1058
+ default:
1059
+ this.cursor = 'default';
1060
+ }
1061
+
1062
+ this.setCursor();
1063
+ }
1064
+
1065
+ /**
1066
+ * Создает кастомный курсор изменения размера, повернутый на нужный угол
1067
+ */
1068
+ createRotatedResizeCursor(handleType, rotationDegrees) {
1069
+ // Базовые углы для каждого типа ручки (в градусах)
1070
+ const baseAngles = {
1071
+ 'e': 0, // Восток - горизонтальная стрелка →
1072
+ 'se': 45, // Юго-восток - диагональная стрелка ↘
1073
+ 's': 90, // Юг - вертикальная стрелка ↓
1074
+ 'sw': 135, // Юго-запад - диагональная стрелка ↙
1075
+ 'w': 180, // Запад - горизонтальная стрелка ←
1076
+ 'nw': 225, // Северо-запад - диагональная стрелка ↖
1077
+ 'n': 270, // Север - вертикальная стрелка ↑
1078
+ 'ne': 315 // Северо-восток - диагональная стрелка ↗
1079
+ };
1080
+
1081
+ // Вычисляем итоговый угол: базовый угол ручки + поворот объекта
1082
+ const totalAngle = (baseAngles[handleType] + rotationDegrees) % 360;
1083
+
1084
+ // Создаем SVG курсор изменения размера, повернутый на нужный угол (белый, крупнее)
1085
+ const svg = `<svg width="32" height="32" viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg"><g transform="rotate(${totalAngle} 16 16)"><path d="M4 16 L9 11 L9 13 L23 13 L23 11 L28 16 L23 21 L23 19 L9 19 L9 21 Z" fill="white" stroke="black" stroke-width="1"/></g></svg>`;
1086
+
1087
+ // Используем encodeURIComponent вместо btoa для безопасного кодирования
1088
+ const dataUrl = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg)}`;
1089
+
1090
+ // Возвращаем CSS cursor с кастомным изображением (hotspot в центре 16x16)
1091
+ return `url("${dataUrl}") 16 16, auto`;
1092
+ }
1093
+
1094
+ /**
1095
+ * Получение курсора для ресайз-хендла с учетом точного поворота объекта
1096
+ */
1097
+ getResizeCursor(handle) {
1098
+ // Получаем ID выбранного объекта для определения его поворота
1099
+ const selectedObject = Array.from(this.selectedObjects)[0];
1100
+ if (!selectedObject) {
1101
+ return 'default';
1102
+ }
1103
+
1104
+ // Получаем угол поворота объекта
1105
+ const rotationData = { objectId: selectedObject, rotation: 0 };
1106
+ this.emit(Events.Tool.GetObjectRotation, rotationData);
1107
+ const objectRotation = rotationData.rotation || 0;
1108
+
1109
+ // Создаем кастомный курсор, повернутый на точный угол объекта
1110
+ return this.createRotatedResizeCursor(handle, objectRotation);
1111
+ }
1112
+
1113
+ /**
1114
+ * Переопределяем setCursor для установки курсора на canvas
1115
+ */
1116
+ setCursor() {
1117
+ if (this.resizeHandles && this.resizeHandles.app && this.resizeHandles.app.view) {
1118
+ // Устанавливаем курсор на canvas, а не на body
1119
+ this.resizeHandles.app.view.style.cursor = this.cursor;
1120
+ } else {
1121
+ // Fallback на базовую реализацию
1122
+ super.setCursor();
1123
+ }
1124
+ }
1125
+
1126
+ /**
1127
+ * Управление выделением
1128
+ */
1129
+
1130
+ addToSelection(object) {
1131
+ console.log(`➕ Добавляем в выделение: ${object}`);
1132
+ this.selection.add(object);
1133
+ this.emit(Events.Tool.SelectionAdd, { object });
1134
+ this.updateResizeHandles();
1135
+ }
1136
+
1137
+ removeFromSelection(object) {
1138
+ this.selection.remove(object);
1139
+ this.emit(Events.Tool.SelectionRemove, { object });
1140
+ this.updateResizeHandles();
1141
+ }
1142
+
1143
+ clearSelection() {
1144
+ const objects = this.selection.toArray();
1145
+ this.selection.clear();
1146
+ this.emit(Events.Tool.SelectionClear, { objects });
1147
+ this.updateResizeHandles();
1148
+ }
1149
+
1150
+ selectAll() {
1151
+ // TODO: Выделить все объекты на доске
1152
+ this.emit(Events.Tool.SelectionAll);
1153
+ }
1154
+
1155
+ deleteSelectedObjects() {
1156
+ const objects = this.selection.toArray();
1157
+ this.clearSelection();
1158
+ this.emit(Events.Tool.ObjectsDelete, { objects });
1159
+ }
1160
+
1161
+ editObject(object) {
1162
+ this.emit(Events.Tool.ObjectEdit, { object });
1163
+ }
1164
+
1165
+ /**
1166
+ * Получение информации о выделении
1167
+ */
1168
+ getSelection() {
1169
+ return this.selection.toArray();
1170
+ }
1171
+
1172
+ // Совместимость с существующим кодом ядра: возвращаем Set выбранных id
1173
+ get selectedObjects() {
1174
+ return new Set(this.selection.toArray());
1175
+ }
1176
+
1177
+ // Экспонируем выделение через EventBus для внешних слушателей (keyboard)
1178
+ onActivate() {
1179
+ // Подписка безопасна: EventBus простая шина, а вызов синхронный
1180
+ this.eventBus.on(Events.Tool.GetSelection, (data) => {
1181
+ data.selection = this.getSelection();
1182
+ });
1183
+ }
1184
+
1185
+ hasSelection() {
1186
+ return this.selection.size() > 0;
1187
+ }
1188
+
1189
+ /**
1190
+ * Обновление ручек изменения размера
1191
+ */
1192
+ updateResizeHandles() {
1193
+ // Используем HTML-ручки (HtmlHandlesLayer). Прячем Pixi-ручки и групповые графики.
1194
+ try {
1195
+ if (this.resizeHandles && typeof this.resizeHandles.hideHandles === 'function') {
1196
+ this.resizeHandles.hideHandles();
1197
+ }
1198
+ const stage = this.app?.stage;
1199
+ const world = stage?.getChildByName && stage.getChildByName('worldLayer');
1200
+ const rh = world && world.getChildByName && world.getChildByName('resize-handles');
1201
+ if (rh) rh.visible = false;
1202
+ const gb = stage && stage.getChildByName && stage.getChildByName('group-bounds');
1203
+ if (gb) gb.visible = false;
1204
+ } catch (e) {
1205
+ // noop
1206
+ }
1207
+ }
1208
+
1209
+ /**
1210
+ * Подготовка перетаскивания с созданием копии при зажатом Alt
1211
+ */
1212
+ prepareAltCloneDrag(objectId, event) {
1213
+ // Очищаем текущее выделение и выделяем исходный объект
1214
+ this.clearSelection();
1215
+ this.addToSelection(objectId);
1216
+
1217
+ // Включаем режим Alt-клона и запрашиваем дубликат у ядра
1218
+ this.isAltCloneMode = true;
1219
+ this.clonePending = true;
1220
+ this.cloneSourceId = objectId;
1221
+
1222
+ // Сохраняем текущее положение курсора
1223
+ this.currentX = event.x;
1224
+ this.currentY = event.y;
1225
+
1226
+ // Запрашиваем текущую позицию исходного объекта
1227
+ const positionData = { objectId, position: null };
1228
+ this.emit('get:object:position', positionData);
1229
+
1230
+ // Сообщаем ядру о необходимости создать дубликат у позиции исходного объекта
1231
+ this.emit('duplicate:request', {
1232
+ originalId: objectId,
1233
+ position: positionData.position || { x: event.x, y: event.y }
1234
+ });
1235
+
1236
+ // Помечаем, что находимся в состоянии drag, но цели пока нет — ждём newId
1237
+ this.isDragging = true;
1238
+ this.dragTarget = null;
1239
+ }
1240
+
1241
+ /**
1242
+ * Когда ядро сообщило о создании дубликата — переключаем drag на новый объект
1243
+ */
1244
+ onDuplicateReady(newObjectId) {
1245
+ this.clonePending = false;
1246
+
1247
+ // Переключаем выделение на новый объект
1248
+ this.clearSelection();
1249
+ this.addToSelection(newObjectId);
1250
+
1251
+ // Устанавливаем цель перетаскивания — новый объект
1252
+ this.dragTarget = newObjectId;
1253
+
1254
+ // ВАЖНО: не пересчитываем dragOffset — сохраняем исходное смещение курсора
1255
+ // Это гарантирует, что курсор останется в той же точке относительно объекта
1256
+
1257
+ // Сообщаем о старте перетаскивания для истории (Undo/Redo)
1258
+ this.emit('drag:start', { object: newObjectId, position: { x: this.currentX, y: this.currentY } });
1259
+
1260
+ // Мгновенно обновляем позицию под курсор
1261
+ this.updateDrag({ x: this.currentX, y: this.currentY });
1262
+
1263
+ // Обновляем ручки
1264
+ this.updateResizeHandles();
1265
+ }
1266
+
1267
+ /**
1268
+ * Преобразует тип ручки с учетом поворота объекта
1269
+ */
1270
+ transformHandleType(handleType, rotationDegrees) {
1271
+ // Нормализуем угол поворота к диапазону 0-360
1272
+ let angle = rotationDegrees % 360;
1273
+ if (angle < 0) angle += 360;
1274
+
1275
+ // Определяем количество поворотов на 90 градусов
1276
+ const rotations = Math.round(angle / 90) % 4;
1277
+
1278
+ if (rotations === 0) return handleType; // Нет поворота
1279
+
1280
+ // Карта преобразований для каждого поворота на 90°
1281
+ const transformMap = {
1282
+ 'nw': ['ne', 'se', 'sw', 'nw'], // nw -> ne -> se -> sw -> nw
1283
+ 'n': ['e', 's', 'w', 'n'], // n -> e -> s -> w -> n
1284
+ 'ne': ['se', 'sw', 'nw', 'ne'], // ne -> se -> sw -> nw -> ne
1285
+ 'e': ['s', 'w', 'n', 'e'], // e -> s -> w -> n -> e
1286
+ 'se': ['sw', 'nw', 'ne', 'se'], // se -> sw -> nw -> ne -> se
1287
+ 's': ['w', 'n', 'e', 's'], // s -> w -> n -> e -> s
1288
+ 'sw': ['nw', 'ne', 'se', 'sw'], // sw -> nw -> ne -> se -> sw
1289
+ 'w': ['n', 'e', 's', 'w'] // w -> n -> e -> s -> w
1290
+ };
1291
+
1292
+ return transformMap[handleType] ? transformMap[handleType][rotations - 1] : handleType;
1293
+ }
1294
+
1295
+ /**
1296
+ * Вычисляет новые размеры объекта на основе типа ручки и смещения мыши
1297
+ */
1298
+ calculateNewSize(handleType, startBounds, deltaX, deltaY, maintainAspectRatio) {
1299
+ let newWidth = startBounds.width;
1300
+ let newHeight = startBounds.height;
1301
+
1302
+ // Получаем угол поворота объекта
1303
+ const rotationData = { objectId: this.dragTarget, rotation: 0 };
1304
+ this.emit('get:object:rotation', rotationData);
1305
+ const objectRotation = rotationData.rotation || 0;
1306
+
1307
+ // Преобразуем тип ручки с учетом поворота объекта
1308
+ const transformedHandleType = this.transformHandleType(handleType, objectRotation);
1309
+
1310
+ // Вычисляем изменения в зависимости от преобразованного типа ручки
1311
+ switch (transformedHandleType) {
1312
+ case 'nw': // Северо-запад - левый верхний угол
1313
+ newWidth = startBounds.width - deltaX; // влево = меньше ширина
1314
+ newHeight = startBounds.height - deltaY; // вверх = меньше высота
1315
+ break;
1316
+ case 'n': // Север - верхняя сторона
1317
+ newHeight = startBounds.height - deltaY; // вверх = меньше высота
1318
+ break;
1319
+ case 'ne': // Северо-восток - правый верхний угол
1320
+ newWidth = startBounds.width + deltaX; // вправо = больше ширина
1321
+ newHeight = startBounds.height - deltaY; // вверх = меньше высота
1322
+ break;
1323
+ case 'e': // Восток - правая сторона
1324
+ newWidth = startBounds.width + deltaX; // вправо = больше ширина
1325
+ break;
1326
+ case 'se': // Юго-восток - правый нижний угол
1327
+ newWidth = startBounds.width + deltaX; // вправо = больше ширина
1328
+ newHeight = startBounds.height + deltaY; // вниз = больше высота
1329
+ break;
1330
+ case 's': // Юг - нижняя сторона
1331
+ newHeight = startBounds.height + deltaY; // вниз = больше высота
1332
+ break;
1333
+ case 'sw': // Юго-запад - левый нижний угол
1334
+ newWidth = startBounds.width - deltaX; // влево = меньше ширина
1335
+ newHeight = startBounds.height + deltaY; // вниз = больше высота
1336
+ break;
1337
+ case 'w': // Запад - левая сторона
1338
+ newWidth = startBounds.width - deltaX; // влево = меньше ширина
1339
+ break;
1340
+ }
1341
+
1342
+
1343
+
1344
+ // Поддержка пропорционального изменения размера (Shift)
1345
+ if (maintainAspectRatio) {
1346
+ const aspectRatio = startBounds.width / startBounds.height;
1347
+
1348
+ // Определяем, какую сторону использовать как основную
1349
+ if (['nw', 'ne', 'sw', 'se'].includes(handleType)) {
1350
+ // Угловые ручки - используем большее изменение
1351
+ const widthChange = Math.abs(newWidth - startBounds.width);
1352
+ const heightChange = Math.abs(newHeight - startBounds.height);
1353
+
1354
+ if (widthChange > heightChange) {
1355
+ newHeight = newWidth / aspectRatio;
1356
+ } else {
1357
+ newWidth = newHeight * aspectRatio;
1358
+ }
1359
+ } else if (['e', 'w'].includes(handleType)) {
1360
+ // Горизонтальные ручки
1361
+ newHeight = newWidth / aspectRatio;
1362
+ } else if (['n', 's'].includes(handleType)) {
1363
+ // Вертикальные ручки
1364
+ newWidth = newHeight * aspectRatio;
1365
+ }
1366
+ }
1367
+
1368
+ return {
1369
+ width: Math.round(newWidth),
1370
+ height: Math.round(newHeight)
1371
+ };
1372
+ }
1373
+
1374
+ /**
1375
+ * Вычисляет смещение позиции при изменении размера через левые/верхние ручки
1376
+ */
1377
+ calculatePositionOffset(handleType, startBounds, newSize, objectRotation = 0) {
1378
+ // Позиция в состоянии — левый верх. Для правых/нижних ручек топ-лев остается на месте.
1379
+ // Для левых/верхних ручек топ-лев должен смещаться на полную величину изменения размера.
1380
+ // deltaWidth/deltaHeight = изменение размера (может быть отрицательным при уменьшении)
1381
+
1382
+ const deltaWidth = newSize.width - startBounds.width;
1383
+ const deltaHeight = newSize.height - startBounds.height;
1384
+
1385
+ let offsetX = 0;
1386
+ let offsetY = 0;
1387
+
1388
+ switch (handleType) {
1389
+ case 'nw':
1390
+ offsetX = -deltaWidth; // левый край смещается на полную величину изменения ширины
1391
+ offsetY = -deltaHeight; // верхний край смещается на полную величину изменения высоты
1392
+ break;
1393
+ case 'n':
1394
+ offsetY = -deltaHeight; // только верхний край смещается
1395
+ break;
1396
+ case 'ne':
1397
+ offsetY = -deltaHeight; // верх смещается, правый край — нет
1398
+ break;
1399
+ case 'e':
1400
+ // правый край — левый верх не смещается
1401
+ break;
1402
+ case 'se':
1403
+ // правый нижний — левый верх не смещается
1404
+ break;
1405
+ case 's':
1406
+ // нижний — левый верх не смещается
1407
+ break;
1408
+ case 'sw':
1409
+ offsetX = -deltaWidth; // левый край смещается, низ — нет
1410
+ break;
1411
+ case 'w':
1412
+ offsetX = -deltaWidth; // левый край смещается на полную величину
1413
+ break;
1414
+ }
1415
+
1416
+ // Для поворота корректное смещение требует преобразования в локальные координаты объекта
1417
+ // и обратно. В данной итерации оставляем смещение в мировых осях для устойчивости без вращения.
1418
+ return { x: offsetX, y: offsetY };
1419
+ }
1420
+
1421
+ _openTextEditor(object, create = false) {
1422
+
1423
+
1424
+ // Проверяем структуру объекта и извлекаем данные
1425
+ let objectId, objectType, position, properties;
1426
+
1427
+ if (create) {
1428
+ // Для создания нового объекта - данные в object.object
1429
+ const objData = object.object || object;
1430
+ objectId = objData.id || null;
1431
+ objectType = objData.type || 'text';
1432
+ position = objData.position;
1433
+ properties = objData.properties || {};
1434
+ } else {
1435
+ // Для редактирования существующего объекта - данные в корне
1436
+ objectId = object.id;
1437
+ objectType = object.type || 'text';
1438
+ position = object.position;
1439
+ properties = object.properties || {};
1440
+ }
1441
+
1442
+
1443
+ let { fontSize = 18, content = '', initialSize } = properties;
1444
+
1445
+ // Определяем тип объекта
1446
+ const isNote = objectType === 'note';
1447
+
1448
+ // Проверяем, что position существует
1449
+ if (!position) {
1450
+ console.error('❌ SelectTool: position is undefined in _openTextEditor', { object, create });
1451
+ return;
1452
+ }
1453
+
1454
+ // Закрываем предыдущий редактор, если он открыт
1455
+ if (this.textEditor.active) this._closeTextEditor(true);
1456
+
1457
+ // Если это редактирование существующего объекта, получаем его данные
1458
+ if (!create && objectId) {
1459
+ const posData = { objectId, position: null };
1460
+ const sizeData = { objectId, size: null };
1461
+ const pixiReq = { objectId, pixiObject: null };
1462
+ this.eventBus.emit(Events.Tool.GetObjectPosition, posData);
1463
+ this.eventBus.emit(Events.Tool.GetObjectSize, sizeData);
1464
+ this.eventBus.emit(Events.Tool.GetObjectPixi, pixiReq);
1465
+
1466
+ // Обновляем данные из полученной информации
1467
+ if (posData.position) position = posData.position;
1468
+ if (sizeData.size) initialSize = sizeData.size;
1469
+
1470
+ const meta = pixiReq.pixiObject && pixiReq.pixiObject._mb ? pixiReq.pixiObject._mb.properties || {} : {};
1471
+ if (meta.content) properties.content = meta.content;
1472
+ if (meta.fontSize) properties.fontSize = meta.fontSize;
1473
+ }
1474
+
1475
+ // Уведомляем о начале редактирования
1476
+ this.eventBus.emit(Events.UI.TextEditStart, { objectId: objectId || null });
1477
+
1478
+ const app = this.app;
1479
+ const world = app?.stage?.getChildByName && app.stage.getChildByName('worldLayer');
1480
+ this.textEditor.world = world || null;
1481
+ const view = app?.view;
1482
+ if (!view) return;
1483
+ if (this.resizeHandles && typeof this.resizeHandles.hideHandles === 'function') {
1484
+ this.resizeHandles.hideHandles();
1485
+ }
1486
+ // Обертка для рамки + textarea + ручек
1487
+ const wrapper = document.createElement('div');
1488
+ wrapper.className = 'moodboard-text-editor';
1489
+
1490
+ // Убираем рамки и ручки для всех типов объектов в режиме редактирования
1491
+ Object.assign(wrapper.style, {
1492
+ position: 'absolute',
1493
+ left: '0px',
1494
+ top: '0px',
1495
+ transformOrigin: '0 0',
1496
+ boxSizing: 'border-box',
1497
+ border: 'none', // Убираем рамку для всех типов
1498
+ background: 'transparent',
1499
+ zIndex: 10000,
1500
+ });
1501
+
1502
+ const textarea = document.createElement('textarea');
1503
+ textarea.className = 'moodboard-text-input';
1504
+ textarea.value = content || '';
1505
+ textarea.placeholder = 'напишите что-нибудь';
1506
+
1507
+ Object.assign(textarea.style, {
1508
+ position: 'relative',
1509
+ left: '0px',
1510
+ top: '0px',
1511
+ border: 'none',
1512
+ padding: '6px 8px', // Увеличиваем отступы для лучшего отображения
1513
+ fontSize: `${fontSize}px`,
1514
+ fontFamily: 'Arial, sans-serif',
1515
+ lineHeight: '1.2',
1516
+ color: '#111', // Для записок делаем текст черным для лучшей видимости
1517
+ background: 'white',
1518
+ outline: 'none',
1519
+ resize: 'none',
1520
+ minWidth: '240px', // Для заметок уменьшаем минимальную ширину
1521
+ minHeight: '28px', // Для заметок уменьшаем минимальную высоту
1522
+ width: '280px', // Для заметок уменьшаем начальную ширину
1523
+ height: '36px', // Для заметок уменьшаем начальную высоту
1524
+ boxSizing: 'border-box',
1525
+ // Повыше чёткость текста в CSS
1526
+ WebkitFontSmoothing: 'antialiased',
1527
+ MozOsxFontSmoothing: 'grayscale',
1528
+ });
1529
+
1530
+ wrapper.appendChild(textarea);
1531
+
1532
+ // Убираем ручки ресайза для всех типов объектов
1533
+ // let handles = [];
1534
+ // let placeHandles = () => {};
1535
+
1536
+ // if (!isNote) {
1537
+ // // Ручки ресайза (8 штук) только для обычного текста
1538
+ // handles = ['nw','n','ne','e','se','s','sw','w'].map(dir => {
1539
+ // const h = document.createElement('div');
1540
+ // h.dataset.dir = dir;
1541
+ // Object.assign(h.style, {
1542
+ // position: 'absolute', width: '12px', height: '12px', background: '#007ACC',
1543
+ // border: '1px solid #fff', boxSizing: 'border-box', zIndex: 10001,
1544
+ // });
1545
+ // return h;
1546
+ // });
1547
+ //
1548
+ // placeHandles = () => {
1549
+ // const w = wrapper.offsetWidth;
1550
+ // const h = wrapper.offsetHeight;
1551
+ // handles.forEach(hd => {
1552
+ // const dir = hd.dataset.dir;
1553
+ // // default reset
1554
+ // hd.style.left = '0px';
1555
+ // hd.style.top = '0px';
1556
+ // hd.style.right = '';
1557
+ // hd.style.bottom = '';
1558
+ // switch (dir) {
1559
+ // case 'nw':
1560
+ // hd.style.left = `${-6}px`;
1561
+ // hd.style.top = `${-6}px`;
1562
+ // hd.style.cursor = 'nwse-resize';
1563
+ // break;
1564
+ // case 'n':
1565
+ // hd.style.left = `${Math.round(w / 2 - 6)}px`;
1566
+ // hd.style.top = `${-6}px`;
1567
+ // hd.style.cursor = 'n-resize';
1568
+ // break;
1569
+ // case 'ne':
1570
+ // hd.style.left = `${Math.max(-6, w - 6)}px`;
1571
+ // hd.style.top = `${-6}px`;
1572
+ // hd.style.cursor = 'nesw-resize';
1573
+ // break;
1574
+ // case 'e':
1575
+ // hd.style.left = `${Math.max(-6, w - 6)}px`;
1576
+ // hd.style.top = `${Math.round(h / 2 - 6)}px`;
1577
+ // hd.style.cursor = 'e-resize';
1578
+ // break;
1579
+ // case 'se':
1580
+ // hd.style.left = `${Math.max(-6, w - 6)}px`;
1581
+ // hd.style.top = `${Math.max(-6, h - 6)}px`;
1582
+ // hd.style.cursor = 'nwse-resize';
1583
+ // break;
1584
+ // case 's':
1585
+ // hd.style.left = `${Math.round(w / 2 - 6)}px`;
1586
+ // hd.style.top = `${Math.max(-6, h - 6)}px`;
1587
+ // hd.style.cursor = 's-resize';
1588
+ // break;
1589
+ // case 'sw':
1590
+ // hd.style.left = `${-6}px`;
1591
+ // hd.style.top = `${Math.max(-6, h - 6)}px`;
1592
+ // hd.style.cursor = 'nesw-resize';
1593
+ // break;
1594
+ // case 'w':
1595
+ // hd.style.left = `${-6}px`;
1596
+ // hd.style.top = `${Math.round(h / 2 - 6)}px`;
1597
+ // hd.style.cursor = 'w-resize';
1598
+ // break;
1599
+ // }
1600
+ // });
1601
+ // }
1602
+ // }
1603
+
1604
+ // Добавляем в DOM
1605
+ wrapper.appendChild(textarea);
1606
+ view.parentElement.appendChild(wrapper);
1607
+
1608
+ // Автоматически устанавливаем фокус на textarea
1609
+ textarea.focus();
1610
+
1611
+ // Позиция обертки по миру → экран
1612
+ const toScreen = (wx, wy) => {
1613
+ const worldLayer = this.textEditor.world || (this.app?.stage);
1614
+ if (!worldLayer) return { x: wx, y: wy };
1615
+ const global = worldLayer.toGlobal(new PIXI.Point(wx, wy));
1616
+ const viewRes = (this.app?.renderer?.resolution) || (view.width && view.clientWidth ? (view.width / view.clientWidth) : 1);
1617
+ return { x: global.x / viewRes, y: global.y / viewRes };
1618
+ };
1619
+ const screenPos = toScreen(position.x, position.y);
1620
+
1621
+ // Для записок позиционируем редактор внутри записки
1622
+ if (objectType === 'note') {
1623
+ // Получаем актуальные размеры записки
1624
+ let noteWidth = 160;
1625
+ let noteHeight = 100;
1626
+
1627
+ if (initialSize) {
1628
+ noteWidth = initialSize.width;
1629
+ noteHeight = initialSize.height;
1630
+ } else if (objectId) {
1631
+ // Если размер не передан, пытаемся получить его из объекта
1632
+ const sizeData = { objectId, size: null };
1633
+ this.eventBus.emit(Events.Tool.GetObjectSize, sizeData);
1634
+ if (sizeData.size) {
1635
+ noteWidth = sizeData.size.width;
1636
+ noteHeight = sizeData.size.height;
1637
+ }
1638
+ }
1639
+
1640
+ // Позиционируем редактор точно там, где находится текст на заметке
1641
+ // В NoteObject текст позиционируется с topMargin = 20 и центрируется по горизонтали
1642
+ const topMargin = 20; // Отступ от верха (ниже полоски)
1643
+ const horizontalPadding = 8; // Отступы по горизонтали
1644
+ const editorWidth = Math.min(280, noteWidth - (horizontalPadding * 2));
1645
+ const editorHeight = Math.min(36, noteHeight - topMargin - horizontalPadding);
1646
+
1647
+ // Позиционируем редактор точно там, где находится текст
1648
+ // Текст центрирован по горизонтали и имеет отступ topMargin от верха
1649
+ const textCenterX = noteWidth / 2; // центр текста по горизонтали
1650
+ const textTopY = topMargin; // позиция текста по вертикали
1651
+
1652
+ // Позиционируем редактор так, чтобы его центр совпадал с центром текста
1653
+ const editorLeft = textCenterX - (editorWidth / 2);
1654
+ const editorTop = textTopY;
1655
+
1656
+ wrapper.style.left = `${screenPos.x + editorLeft}px`;
1657
+ wrapper.style.top = `${screenPos.y + editorTop}px`;
1658
+
1659
+ // Устанавливаем размеры редактора
1660
+ textarea.style.width = `${editorWidth}px`;
1661
+ textarea.style.height = `${editorHeight}px`;
1662
+ wrapper.style.width = `${editorWidth}px`;
1663
+ wrapper.style.height = `${editorHeight}px`;
1664
+ } else {
1665
+ // Для обычного текста используем стандартное позиционирование
1666
+ wrapper.style.left = `${screenPos.x}px`;
1667
+ wrapper.style.top = `${screenPos.y}px`;
1668
+ }
1669
+ // Минимальные границы (зависят от текущего режима: новый объект или редактирование существующего)
1670
+ const worldLayerRef = this.textEditor.world || (this.app?.stage);
1671
+ const s = worldLayerRef?.scale?.x || 1;
1672
+ const viewRes = (this.app?.renderer?.resolution) || (view.width && view.clientWidth ? (view.width / view.clientWidth) : 1);
1673
+ const initialWpx = initialSize ? Math.max(1, (initialSize.width || 0) * s / viewRes) : null;
1674
+ const initialHpx = initialSize ? Math.max(1, (initialSize.height || 0) * s / viewRes) : null;
1675
+
1676
+ // Определяем минимальные границы для всех типов объектов
1677
+ let minWBound = initialWpx || 240;
1678
+ let minHBound = 28;
1679
+
1680
+ // Для записок размеры уже установлены выше, пропускаем эту логику
1681
+ if (!isNote) {
1682
+ if (initialWpx) {
1683
+ textarea.style.width = `${initialWpx}px`;
1684
+ wrapper.style.width = `${initialWpx}px`;
1685
+ }
1686
+ if (initialHpx) {
1687
+ textarea.style.height = `${initialHpx}px`;
1688
+ wrapper.style.height = `${initialHpx}px`;
1689
+ }
1690
+ }
1691
+ // Автоподгон
1692
+ const autoSize = () => {
1693
+ if (isNote) {
1694
+ // Для заметок используем фиксированные размеры, вычисленные выше
1695
+ // Не вызываем autoSize, чтобы сохранить точное позиционирование
1696
+ return;
1697
+ }
1698
+
1699
+ // Для обычного текста восстанавливаем автоподгон
1700
+ textarea.style.height = '1px';
1701
+ textarea.style.width = '1px';
1702
+ const w = Math.max(minWBound, textarea.scrollWidth + 8);
1703
+ const h = Math.max(minHBound, textarea.scrollHeight + 4);
1704
+ textarea.style.width = `${w}px`;
1705
+ textarea.style.height = `${h}px`;
1706
+ wrapper.style.width = `${w}px`;
1707
+ wrapper.style.height = `${h}px`;
1708
+ // Обновляем ручки только для обычного текста
1709
+ // placeHandles();
1710
+ };
1711
+
1712
+ // Вызываем autoSize только для обычного текста
1713
+ if (!isNote) {
1714
+ autoSize();
1715
+ }
1716
+ textarea.focus();
1717
+ // Локальная CSS-настройка placeholder (меньше базового шрифта)
1718
+ const uid = 'mbti-' + Math.random().toString(36).slice(2);
1719
+ textarea.classList.add(uid);
1720
+ const styleEl = document.createElement('style');
1721
+ const phSize = Math.max(12, Math.round(fontSize * 0.8));
1722
+ const placeholderOpacity = isNote ? '0.4' : '0.6'; // Для записок делаем placeholder менее заметным
1723
+ styleEl.textContent = `.${uid}::placeholder{font-size:${phSize}px;opacity:${placeholderOpacity};}`;
1724
+ document.head.appendChild(styleEl);
1725
+ this.textEditor = { active: true, objectId, textarea, wrapper, world: this.textEditor.world, position, properties: { fontSize }, objectType, _phStyle: styleEl };
1726
+
1727
+ // Скрываем статичный текст во время редактирования для всех типов объектов
1728
+ if (objectId) {
1729
+ // Проверяем, что HTML-элемент существует перед попыткой скрыть текст
1730
+ if (window.moodboard && window.moodboard.htmlTextLayer) {
1731
+ const el = window.moodboard.htmlTextLayer.idToEl.get(objectId);
1732
+ if (el) {
1733
+ this.eventBus.emit(Events.Tool.HideObjectText, { objectId });
1734
+ } else {
1735
+ console.warn(`❌ SelectTool: HTML-элемент для объекта ${objectId} не найден, пропускаем HideObjectText`);
1736
+ }
1737
+ } else {
1738
+ this.eventBus.emit(Events.Tool.HideObjectText, { objectId });
1739
+ }
1740
+ }
1741
+ // Ресайз мышью только для обычного текста
1742
+ if (!isNote) {
1743
+ const onHandleDown = (e) => {
1744
+ e.preventDefault(); e.stopPropagation();
1745
+ const dir = e.target.dataset.dir;
1746
+ if (!dir) return;
1747
+ const start = {
1748
+ x: e.clientX, y: e.clientY,
1749
+ w: wrapper.offsetWidth, h: wrapper.offsetHeight,
1750
+ left: parseFloat(wrapper.style.left), top: parseFloat(wrapper.style.top), dir
1751
+ };
1752
+ const onMove = (ev) => {
1753
+ const dx = ev.clientX - start.x;
1754
+ const dy = ev.clientY - start.y;
1755
+ let newW = start.w, newH = start.h, newLeft = start.left, newTop = start.top;
1756
+ if (dir.includes('e')) newW = Math.max(80, start.w + dx);
1757
+ if (dir.includes('s')) newH = Math.max(24, start.h + dy);
1758
+ if (dir.includes('w')) { newW = Math.max(80, start.w - dx); newLeft = start.left + dx; }
1759
+ if (dir.includes('n')) { newH = Math.max(24, start.h - dy); newTop = start.top + dy; }
1760
+ wrapper.style.width = `${newW}px`;
1761
+ wrapper.style.height = `${newH}px`;
1762
+ wrapper.style.left = `${newLeft}px`;
1763
+ wrapper.style.top = `${newTop}px`;
1764
+ textarea.style.width = `${Math.max(minWBound, newW)}px`;
1765
+ textarea.style.height = `${Math.max(minHBound, newH)}px`;
1766
+ // placeHandles();
1767
+ };
1768
+ const onUp = () => {
1769
+ document.removeEventListener('mousemove', onMove);
1770
+ document.removeEventListener('mouseup', onUp);
1771
+ };
1772
+ document.addEventListener('mousemove', onMove);
1773
+ document.addEventListener('mouseup', onUp);
1774
+ };
1775
+ // handles.forEach(h => h.addEventListener('mousedown', onHandleDown));
1776
+ }
1777
+ // Завершение
1778
+ const finalize = (commit) => {
1779
+ console.log('🔧 SelectTool: finalize called with commit:', commit, 'objectId:', objectId, 'objectType:', this.textEditor.objectType);
1780
+ const value = textarea.value.trim();
1781
+ const commitValue = commit && value.length > 0;
1782
+
1783
+ // Сохраняем objectType ДО сброса this.textEditor
1784
+ const currentObjectType = this.textEditor.objectType;
1785
+ console.log('🔧 SelectTool: finalize - saved objectType:', currentObjectType);
1786
+
1787
+ // Показываем статичный текст после завершения редактирования для всех типов объектов
1788
+ if (objectId) {
1789
+ // Проверяем, что HTML-элемент существует перед попыткой показать текст
1790
+ if (window.moodboard && window.moodboard.htmlTextLayer) {
1791
+ const el = window.moodboard.htmlTextLayer.idToEl.get(objectId);
1792
+ if (el) {
1793
+ this.eventBus.emit(Events.Tool.ShowObjectText, { objectId });
1794
+ } else {
1795
+ console.warn(`❌ SelectTool: HTML-элемент для объекта ${objectId} не найден, пропускаем ShowObjectText`);
1796
+ }
1797
+ } else {
1798
+ this.eventBus.emit(Events.Tool.ShowObjectText, { objectId });
1799
+ }
1800
+ }
1801
+
1802
+ wrapper.remove();
1803
+ this.textEditor = { active: false, objectId: null, textarea: null, wrapper: null, world: null, position: null, properties: null, objectType: 'text' };
1804
+ this.eventBus.emit(Events.UI.TextEditEnd, { objectId: objectId || null });
1805
+ if (!commitValue) {
1806
+ console.log('🔧 SelectTool: finalize - no commit, returning');
1807
+ return;
1808
+ }
1809
+ if (objectId == null) {
1810
+ console.log('🔧 SelectTool: finalize - creating new object');
1811
+ // Создаем объект с правильным типом
1812
+ const objectType = currentObjectType || 'text';
1813
+ this.eventBus.emit(Events.UI.ToolbarAction, {
1814
+ type: objectType,
1815
+ id: objectType,
1816
+ position: { x: position.x, y: position.y },
1817
+ properties: { content: value, fontSize }
1818
+ });
1819
+ } else {
1820
+ // Обновление существующего: используем команду обновления содержимого
1821
+ if (currentObjectType === 'note') {
1822
+ console.log('🔧 SelectTool: updating note content via UpdateObjectContent');
1823
+ // Для записок обновляем содержимое через PixiEngine
1824
+ this.eventBus.emit(Events.Tool.UpdateObjectContent, {
1825
+ objectId: objectId,
1826
+ content: value
1827
+ });
1828
+
1829
+ // Обновляем состояние объекта в StateManager
1830
+ this.eventBus.emit(Events.Object.StateChanged, {
1831
+ objectId: objectId,
1832
+ updates: {
1833
+ content: value
1834
+ }
1835
+ });
1836
+ } else {
1837
+ // Для обычного текста тоже используем обновление содержимого
1838
+ console.log('🔧 SelectTool: finalize - updating text content via UpdateObjectContent');
1839
+ this.eventBus.emit(Events.Tool.UpdateObjectContent, {
1840
+ objectId: objectId,
1841
+ content: value
1842
+ });
1843
+
1844
+ // Обновляем состояние объекта в StateManager
1845
+ this.eventBus.emit(Events.Object.StateChanged, {
1846
+ objectId: objectId,
1847
+ updates: {
1848
+ content: value
1849
+ }
1850
+ });
1851
+ }
1852
+ }
1853
+ };
1854
+ textarea.addEventListener('blur', (e) => {
1855
+ // Не закрываем новый пустой текст по потере фокуса — чтобы поле не исчезало сразу
1856
+ const isNew = objectId == null;
1857
+ const value = (textarea.value || '').trim();
1858
+ if (isNew && value.length === 0) {
1859
+ // Вернём фокус обратно, чтобы пользователь мог ввести текст
1860
+ setTimeout(() => textarea.focus(), 0);
1861
+ return;
1862
+ }
1863
+ finalize(true);
1864
+ });
1865
+ textarea.addEventListener('keydown', (e) => {
1866
+ if (e.key === 'Enter' && !e.shiftKey) {
1867
+ e.preventDefault();
1868
+ finalize(true);
1869
+ } else if (e.key === 'Escape') {
1870
+ e.preventDefault();
1871
+ finalize(false);
1872
+ }
1873
+ });
1874
+ // Автоподгон при вводе только для обычного текста
1875
+ if (!isNote) {
1876
+ textarea.addEventListener('input', autoSize);
1877
+ }
1878
+ }
1879
+
1880
+ /**
1881
+ * Открывает редактор названия файла
1882
+ */
1883
+ _openFileNameEditor(object, create = false) {
1884
+ // Проверяем структуру объекта и извлекаем данные
1885
+ let objectId, position, properties;
1886
+
1887
+ if (create) {
1888
+ // Для создания нового объекта - данные в object.object
1889
+ const objData = object.object || object;
1890
+ objectId = objData.id || null;
1891
+ position = objData.position;
1892
+ properties = objData.properties || {};
1893
+ } else {
1894
+ // Для редактирования существующего объекта - данные в корне
1895
+ objectId = object.id;
1896
+ position = object.position;
1897
+ properties = object.properties || {};
1898
+ }
1899
+
1900
+ const fileName = properties.fileName || 'Untitled';
1901
+
1902
+ // Проверяем, что position существует
1903
+ if (!position) {
1904
+ console.error('❌ SelectTool: position is undefined in _openFileNameEditor', { object, create });
1905
+ return;
1906
+ }
1907
+
1908
+ // Закрываем предыдущий редактор, если он открыт
1909
+ if (this.textEditor.active) {
1910
+ if (this.textEditor.objectType === 'file') {
1911
+ this._closeFileNameEditor(true);
1912
+ } else {
1913
+ this._closeTextEditor(true);
1914
+ }
1915
+ }
1916
+
1917
+ // Если это редактирование существующего объекта, получаем его данные
1918
+ if (!create && objectId) {
1919
+ const posData = { objectId, position: null };
1920
+ const pixiReq = { objectId, pixiObject: null };
1921
+ this.eventBus.emit(Events.Tool.GetObjectPosition, posData);
1922
+ this.eventBus.emit(Events.Tool.GetObjectPixi, pixiReq);
1923
+
1924
+ // Обновляем данные из полученной информации
1925
+ if (posData.position) position = posData.position;
1926
+
1927
+ const meta = pixiReq.pixiObject && pixiReq.pixiObject._mb ? pixiReq.pixiObject._mb.properties || {} : {};
1928
+
1929
+ // Скрываем текст файла на время редактирования
1930
+ if (pixiReq.pixiObject && pixiReq.pixiObject._mb && pixiReq.pixiObject._mb.instance) {
1931
+ const fileInstance = pixiReq.pixiObject._mb.instance;
1932
+ if (typeof fileInstance.hideText === 'function') {
1933
+ fileInstance.hideText();
1934
+ }
1935
+ }
1936
+ }
1937
+
1938
+ // Создаем wrapper для input
1939
+ const wrapper = document.createElement('div');
1940
+ wrapper.className = 'moodboard-file-name-editor';
1941
+ wrapper.style.cssText = `
1942
+ position: absolute;
1943
+ z-index: 1000;
1944
+ background: white;
1945
+ border: 2px solid #2563eb;
1946
+ border-radius: 6px;
1947
+ padding: 6px 8px;
1948
+ box-shadow: 0 4px 12px rgba(0,0,0,0.15);
1949
+ min-width: 140px;
1950
+ max-width: 200px;
1951
+ font-family: system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif;
1952
+ `;
1953
+
1954
+ // Создаем input для редактирования названия
1955
+ const input = document.createElement('input');
1956
+ input.type = 'text';
1957
+ input.value = fileName;
1958
+ input.style.cssText = `
1959
+ border: none;
1960
+ outline: none;
1961
+ background: transparent;
1962
+ font-family: inherit;
1963
+ font-size: 12px;
1964
+ text-align: center;
1965
+ width: 100%;
1966
+ padding: 2px 4px;
1967
+ color: #1f2937;
1968
+ font-weight: 500;
1969
+ `;
1970
+
1971
+ wrapper.appendChild(input);
1972
+ document.body.appendChild(wrapper);
1973
+
1974
+ // Позиционируем редактор (аналогично _openTextEditor)
1975
+ const toScreen = (wx, wy) => {
1976
+ const worldLayer = this.textEditor.world || (this.app?.stage);
1977
+ if (!worldLayer) return { x: wx, y: wy };
1978
+ const global = worldLayer.toGlobal(new PIXI.Point(wx, wy));
1979
+ const view = this.app?.view || document.querySelector('canvas');
1980
+ const viewRes = (this.app?.renderer?.resolution) || (view && view.width && view.clientWidth ? (view.width / view.clientWidth) : 1);
1981
+ return { x: global.x / viewRes, y: global.y / viewRes };
1982
+ };
1983
+ const screenPos = toScreen(position.x, position.y);
1984
+
1985
+ // Получаем размеры файлового объекта для точного позиционирования
1986
+ let fileWidth = 120;
1987
+ let fileHeight = 140;
1988
+
1989
+ if (objectId) {
1990
+ const sizeData = { objectId, size: null };
1991
+ this.eventBus.emit(Events.Tool.GetObjectSize, sizeData);
1992
+ if (sizeData.size) {
1993
+ fileWidth = sizeData.size.width;
1994
+ fileHeight = sizeData.size.height;
1995
+ }
1996
+ }
1997
+
1998
+ // Позиционируем редактор в нижней части файла (где название)
1999
+ // В FileObject название находится в позиции y = height - 40
2000
+ const nameY = fileHeight - 40;
2001
+ const centerX = fileWidth / 2;
2002
+
2003
+ wrapper.style.left = `${screenPos.x + centerX - 60}px`; // Центрируем относительно файла
2004
+ wrapper.style.top = `${screenPos.y + nameY}px`; // Позиционируем на уровне названия
2005
+
2006
+ // Сохраняем состояние редактора
2007
+ this.textEditor = {
2008
+ active: true,
2009
+ objectId: objectId,
2010
+ textarea: input,
2011
+ wrapper: wrapper,
2012
+ position: position,
2013
+ properties: properties,
2014
+ objectType: 'file',
2015
+ isResizing: false
2016
+ };
2017
+
2018
+ // Фокусируем и выделяем весь текст
2019
+ input.focus();
2020
+ input.select();
2021
+
2022
+ // Функция завершения редактирования
2023
+ const finalize = (commit) => {
2024
+ this._closeFileNameEditor(commit);
2025
+ };
2026
+
2027
+ // Обработчики событий
2028
+ input.addEventListener('blur', () => finalize(true));
2029
+ input.addEventListener('keydown', (e) => {
2030
+ if (e.key === 'Enter') {
2031
+ e.preventDefault();
2032
+ finalize(true);
2033
+ } else if (e.key === 'Escape') {
2034
+ e.preventDefault();
2035
+ finalize(false);
2036
+ }
2037
+ });
2038
+ }
2039
+
2040
+ /**
2041
+ * Закрывает редактор названия файла
2042
+ */
2043
+ _closeFileNameEditor(commit) {
2044
+ console.log('🔧 SelectTool: _closeFileNameEditor called with commit:', commit);
2045
+
2046
+ // Проверяем, что редактор существует и не закрыт
2047
+ if (!this.textEditor || !this.textEditor.textarea || this.textEditor.closing) {
2048
+ return;
2049
+ }
2050
+
2051
+ // Устанавливаем флаг закрытия, чтобы избежать повторных вызовов
2052
+ this.textEditor.closing = true;
2053
+
2054
+ const input = this.textEditor.textarea;
2055
+ const value = input.value.trim();
2056
+ const commitValue = commit && value.length > 0;
2057
+ const objectId = this.textEditor.objectId;
2058
+
2059
+ console.log('🔧 SelectTool: _closeFileNameEditor - objectId:', objectId, 'commitValue:', commitValue, 'newName:', value);
2060
+
2061
+ // Убираем wrapper из DOM
2062
+ if (this.textEditor.wrapper && this.textEditor.wrapper.parentNode) {
2063
+ this.textEditor.wrapper.remove();
2064
+ }
2065
+
2066
+ // Показываем обратно текст файла
2067
+ if (objectId) {
2068
+ const pixiReq = { objectId, pixiObject: null };
2069
+ this.eventBus.emit(Events.Tool.GetObjectPixi, pixiReq);
2070
+
2071
+ if (pixiReq.pixiObject && pixiReq.pixiObject._mb && pixiReq.pixiObject._mb.instance) {
2072
+ const fileInstance = pixiReq.pixiObject._mb.instance;
2073
+ if (typeof fileInstance.showText === 'function') {
2074
+ fileInstance.showText();
2075
+ }
2076
+
2077
+ // Применяем изменения если нужно
2078
+ if (commitValue && value !== this.textEditor.properties.fileName) {
2079
+ console.log('🔧 Применяем новое название файла:', value);
2080
+
2081
+ // Создаем команду изменения названия файла
2082
+ const oldName = this.textEditor.properties.fileName || 'Untitled';
2083
+ this.eventBus.emit(Events.Object.FileNameChange, {
2084
+ objectId: objectId,
2085
+ oldName: oldName,
2086
+ newName: value
2087
+ });
2088
+ }
2089
+ }
2090
+ }
2091
+
2092
+ // Сбрасываем состояние редактора
2093
+ this.textEditor = {
2094
+ active: false,
2095
+ objectId: null,
2096
+ textarea: null,
2097
+ wrapper: null,
2098
+ world: null,
2099
+ position: null,
2100
+ properties: null,
2101
+ objectType: 'text',
2102
+ isResizing: false
2103
+ };
2104
+ }
2105
+
2106
+ _closeTextEditor(commit) {
2107
+ console.log('🔧 SelectTool: _closeTextEditor called with commit:', commit);
2108
+ const textarea = this.textEditor.textarea;
2109
+ if (!textarea) return;
2110
+ const value = textarea.value.trim();
2111
+ const commitValue = commit && value.length > 0;
2112
+ const objectType = this.textEditor.objectType || 'text';
2113
+ const objectId = this.textEditor.objectId;
2114
+ const position = this.textEditor.position;
2115
+ const properties = this.textEditor.properties;
2116
+
2117
+ console.log('🔧 SelectTool: _closeTextEditor - objectType:', objectType, 'objectId:', objectId, 'commitValue:', commitValue);
2118
+
2119
+ // Показываем статичный текст после завершения редактирования для всех типов объектов
2120
+ if (objectId) {
2121
+ // Проверяем, что HTML-элемент существует перед попыткой показать текст
2122
+ if (window.moodboard && window.moodboard.htmlTextLayer) {
2123
+ const el = window.moodboard.htmlTextLayer.idToEl.get(objectId);
2124
+ if (el) {
2125
+ this.eventBus.emit(Events.Tool.ShowObjectText, { objectId });
2126
+ } else {
2127
+ console.warn(`❌ SelectTool: HTML-элемент для объекта ${objectId} не найден, пропускаем ShowObjectText`);
2128
+ }
2129
+ } else {
2130
+ this.eventBus.emit(Events.Tool.ShowObjectText, { objectId });
2131
+ }
2132
+ }
2133
+
2134
+ textarea.remove();
2135
+ this.textEditor = { active: false, objectId: null, textarea: null, world: null, objectType: 'text' };
2136
+ if (!commitValue) return;
2137
+ if (objectId == null) {
2138
+ // Создаём новый объект через ToolbarAction
2139
+ console.log('🔧 SelectTool: creating new object via ToolbarAction, type:', objectType);
2140
+ this.eventBus.emit(Events.UI.ToolbarAction, {
2141
+ type: objectType,
2142
+ id: objectType,
2143
+ position: { x: position.x, y: position.y },
2144
+ properties: { content: value, fontSize: properties.fontSize }
2145
+ });
2146
+ } else {
2147
+ // Обновление существующего: используем команду обновления содержимого
2148
+ if (objectType === 'note') {
2149
+ console.log('🔧 SelectTool: updating note content via UpdateObjectContent');
2150
+ // Для записок обновляем содержимое через PixiEngine
2151
+ this.eventBus.emit(Events.Tool.UpdateObjectContent, {
2152
+ objectId: objectId,
2153
+ content: value
2154
+ });
2155
+
2156
+ // Обновляем состояние объекта в StateManager
2157
+ this.eventBus.emit(Events.Object.StateChanged, {
2158
+ objectId: objectId,
2159
+ updates: {
2160
+ content: value
2161
+ }
2162
+ });
2163
+ } else {
2164
+ // Для обычного текста тоже используем обновление содержимого
2165
+ console.log('🔧 SelectTool: updating text content via UpdateObjectContent');
2166
+ this.eventBus.emit(Events.Tool.UpdateObjectContent, {
2167
+ objectId: objectId,
2168
+ content: value
2169
+ });
2170
+
2171
+ // Обновляем состояние объекта в StateManager
2172
+ this.eventBus.emit(Events.Object.StateChanged, {
2173
+ objectId: objectId,
2174
+ updates: {
2175
+ content: value
2176
+ }
2177
+ });
2178
+ }
2179
+ }
2180
+ }
2181
+
2182
+
2183
+ }