@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
@@ -1,5 +1,10 @@
1
1
  import { Events } from '../core/events/Events.js';
2
2
  import cursorDefaultSvg from '../assets/icons/cursor-default.svg?raw';
3
+ import { ToolActivationController } from './manager/ToolActivationController.js';
4
+ import { ToolEventRouter } from './manager/ToolEventRouter.js';
5
+ import { ToolManagerGuards } from './manager/ToolManagerGuards.js';
6
+ import { ToolManagerLifecycle } from './manager/ToolManagerLifecycle.js';
7
+ import { ToolRegistry } from './manager/ToolRegistry.js';
3
8
 
4
9
  // Масштабируем курсор в 2 раза меньше
5
10
  const _scaledCursorSvg = (() => {
@@ -23,6 +28,8 @@ export class ToolManager {
23
28
  this.pixiApp = pixiApp; // PIXI Application для передачи в инструменты
24
29
  this.core = core; // Ссылка на core для доступа к imageUploadService
25
30
  this.tools = new Map();
31
+ this.registry = new ToolRegistry(this);
32
+ this.activation = new ToolActivationController(this);
26
33
  this.activeTool = null;
27
34
  this.defaultTool = null;
28
35
 
@@ -48,71 +55,35 @@ export class ToolManager {
48
55
  * Регистрирует инструмент
49
56
  */
50
57
  registerTool(tool) {
51
- this.tools.set(tool.name, tool);
52
-
53
- // Устанавливаем первый инструмент как по умолчанию
54
- if (!this.defaultTool) {
55
- this.defaultTool = tool.name;
56
- }
58
+ this.registry.register(tool);
57
59
  }
58
60
 
59
61
  /**
60
62
  * Активирует инструмент
61
63
  */
62
64
  activateTool(toolName) {
63
- const tool = this.tools.get(toolName);
64
- if (!tool) {
65
- console.warn(`Tool "${toolName}" not found`);
66
- return false;
67
- }
68
-
69
- // Деактивируем текущий инструмент
70
- if (this.activeTool) {
71
- this.activeTool.deactivate();
72
- }
73
-
74
- // Активируем новый инструмент
75
- this.activeTool = tool;
76
-
77
- // Передаем PIXI app в метод activate, если он поддерживается
78
- if (typeof this.activeTool.activate === 'function') {
79
- this.activeTool.activate(this.pixiApp);
80
- }
81
- this.syncActiveToolCursor();
82
-
83
- return true;
65
+ return this.activation.activateTool(toolName);
84
66
  }
85
67
 
86
68
  /**
87
69
  * Временно активирует инструмент (с возвратом к предыдущему)
88
70
  */
89
71
  activateTemporaryTool(toolName) {
90
- if (this.activeTool) {
91
- this.previousTool = this.activeTool.name;
92
- }
93
-
94
- this.activateTool(toolName);
95
- this.temporaryTool = toolName;
72
+ this.activation.activateTemporaryTool(toolName);
96
73
  }
97
74
 
98
75
  /**
99
76
  * Возвращается к предыдущему инструменту
100
77
  */
101
78
  returnToPreviousTool() {
102
- if (this.temporaryTool && this.previousTool) {
103
- this.activateTool(this.previousTool);
104
- this.temporaryTool = null;
105
- this.previousTool = null;
106
- }
79
+ this.activation.returnToPreviousTool();
107
80
  }
108
81
 
109
82
  /**
110
83
  * Возвращается к инструменту по умолчанию
111
84
  */
112
85
  activateDefaultTool() {
113
- if (this.defaultTool) {
114
- this.activateTool(this.defaultTool);
115
- }
86
+ this.activation.activateDefaultTool();
116
87
  }
117
88
 
118
89
  /**
@@ -126,90 +97,21 @@ export class ToolManager {
126
97
  * Получает список всех инструментов
127
98
  */
128
99
  getAllTools() {
129
- return Array.from(this.tools.values());
100
+ return this.registry.getAll();
130
101
  }
131
102
 
132
103
  /**
133
104
  * Проверяет, зарегистрирован ли инструмент
134
105
  */
135
106
  hasActiveTool(toolName) {
136
- return this.tools.has(toolName);
107
+ return this.registry.has(toolName);
137
108
  }
138
109
 
139
110
  /**
140
111
  * Инициализирует обработчики событий DOM
141
112
  */
142
113
  initEventListeners() {
143
- if (!this.container) return;
144
-
145
- // События мыши на контейнере
146
- this.container.addEventListener('mousedown', (e) => this.handleMouseDown(e));
147
- this.container.addEventListener('mousemove', (e) => this.handleMouseMove(e));
148
- this.container.addEventListener('mouseup', (e) => this.handleMouseUp(e));
149
- this.container.addEventListener('mouseenter', () => {
150
- this.isMouseOverContainer = true;
151
- if (!this.activeTool) {
152
- this.container.style.cursor = DEFAULT_CURSOR;
153
- return;
154
- }
155
- this.syncActiveToolCursor();
156
- });
157
- this.container.addEventListener('mouseleave', () => { this.isMouseOverContainer = false; });
158
- // Убираем отдельные слушатели aux-pan на контейнере, чтобы не дублировать mousedown/mouseup
159
-
160
- // Drag & Drop — поддержка перетаскивания изображений на холст
161
- this.container.addEventListener('dragenter', (e) => {
162
- e.preventDefault();
163
- });
164
- this.container.addEventListener('dragover', (e) => {
165
- e.preventDefault();
166
- if (e.dataTransfer) e.dataTransfer.dropEffect = 'copy';
167
- });
168
- this.container.addEventListener('dragleave', (e) => {
169
- // можно снимать подсветку, если добавим в будущем
170
- });
171
- this.container.addEventListener('drop', (e) => this.handleDrop(e));
172
-
173
- // Глобальные события мыши — чтобы корректно завершать drag/resize при отпускании за пределами холста
174
- document.addEventListener('mousemove', (e) => this.handleMouseMove(e));
175
- document.addEventListener('mouseup', (e) => {
176
- this.handleMouseUp(e);
177
- // Гарантированно завершаем временный pan, даже если кнопка отпущена вне холста
178
- if (this.temporaryTool === 'pan') {
179
- this.handleAuxPanEnd(e);
180
- }
181
- });
182
- this.container.addEventListener('dblclick', (e) => this.handleDoubleClick(e));
183
- // wheel должен быть non-passive, чтобы preventDefault работал корректно
184
- this.container.addEventListener('wheel', (e) => this.handleMouseWheel(e), { passive: false });
185
- // Блокируем системный зум браузера (Ctrl + колесо) над рабочей областью
186
- this._onWindowWheel = (e) => {
187
- try {
188
- if (e && e.ctrlKey && this.isMouseOverContainer) {
189
- e.preventDefault();
190
- }
191
- } catch (_) {}
192
- };
193
- window.addEventListener('wheel', this._onWindowWheel, { passive: false });
194
-
195
- // События клавиатуры (на document)
196
- document.addEventListener('keydown', (e) => this.handleKeyDown(e));
197
- document.addEventListener('keyup', (e) => this.handleKeyUp(e));
198
-
199
- // Контекстное меню: предотвращаем дефолт и пересылаем событие активному инструменту
200
- this.container.addEventListener('contextmenu', (e) => {
201
- e.preventDefault();
202
- if (!this.activeTool) return;
203
- const rect = this.container.getBoundingClientRect();
204
- const event = {
205
- x: e.clientX - rect.left,
206
- y: e.clientY - rect.top,
207
- originalEvent: e
208
- };
209
- if (typeof this.activeTool.onContextMenu === 'function') {
210
- this.activeTool.onContextMenu(event);
211
- }
212
- });
114
+ ToolManagerLifecycle.initEventListeners(this, DEFAULT_CURSOR);
213
115
  }
214
116
 
215
117
  /**
@@ -217,120 +119,32 @@ export class ToolManager {
217
119
  */
218
120
 
219
121
  handleMouseDown(e) {
220
- if (!this.activeTool) return;
221
- this.isMouseDown = true;
222
-
223
- // Если удерживается пробел + левая кнопка — сразу запускаем pan и не дергаем активный инструмент
224
- if (this.spacePressed && e.button === 0) {
225
- this.handleAuxPanStart(e);
226
- return;
227
- }
228
- // Средняя кнопка — тоже панорамирование без дергания активного инструмента
229
- if (e.button === 1) {
230
- this.handleAuxPanStart(e);
231
- return;
232
- }
233
-
234
- const rect = this.container.getBoundingClientRect();
235
- const event = {
236
- x: e.clientX - rect.left,
237
- y: e.clientY - rect.top,
238
- button: e.button,
239
- target: e.target,
240
- originalEvent: e
241
- };
242
-
243
- this.lastMousePos = { x: event.x, y: event.y };
244
- this.eventBus.emit(Events.UI.CursorMove, { x: event.x, y: event.y });
245
-
246
- this.activeTool.onMouseDown(event);
122
+ return ToolEventRouter.handleMouseDown(this, e);
247
123
  }
248
124
 
249
125
  // Поддержка панорамирования средней кнопкой мыши без переключения инструмента
250
126
  handleAuxPanStart(e) {
251
- // Средняя кнопка (button === 1) или пробел зажат и левая кнопка
252
- const isMiddle = e.button === 1;
253
- const isSpaceLeft = e.button === 0 && this.spacePressed;
254
- if (!isMiddle && !isSpaceLeft) return;
255
-
256
- // Временная активация pan-инструмента
257
- if (this.hasActiveTool('pan')) {
258
- this.previousTool = this.activeTool?.name || null;
259
- this.activateTemporaryTool('pan');
260
- // Синтетический mousedown для запуска pan
261
- const rect = this.container.getBoundingClientRect();
262
- const event = {
263
- x: e.clientX - rect.left,
264
- y: e.clientY - rect.top,
265
- button: 0,
266
- target: e.target,
267
- originalEvent: e
268
- };
269
- this.lastMousePos = { x: event.x, y: event.y };
270
- this.eventBus.emit(Events.UI.CursorMove, { x: event.x, y: event.y });
271
- this.activeTool.onMouseDown(event);
272
- }
127
+ return ToolEventRouter.handleAuxPanStart(this, e);
273
128
  }
274
129
 
275
130
  handleAuxPanEnd(e) {
276
- // Завершаем временное панорамирование при отпускании средней/левой (с пробелом)
277
- if (this.temporaryTool === 'pan') {
278
- const rect = this.container.getBoundingClientRect();
279
- const event = {
280
- x: e.clientX - rect.left,
281
- y: e.clientY - rect.top,
282
- button: 0,
283
- target: e.target,
284
- originalEvent: e
285
- };
286
- this.lastMousePos = { x: event.x, y: event.y };
287
- this.eventBus.emit(Events.UI.CursorMove, { x: event.x, y: event.y });
288
- this.activeTool.onMouseUp(event);
289
- this.returnToPreviousTool();
290
- return;
291
- }
131
+ return ToolEventRouter.handleAuxPanEnd(this, e);
292
132
  }
293
133
 
294
134
  handleMouseMove(e) {
295
- if (!this.activeTool) return;
296
-
297
- const rect = this.container.getBoundingClientRect();
298
- const event = {
299
- x: e.clientX - rect.left,
300
- y: e.clientY - rect.top,
301
- target: e.target,
302
- originalEvent: e
303
- };
304
-
305
- // Запоминаем и рассылаем позицию курсора для использования другими подсистемами
306
- this.lastMousePos = { x: event.x, y: event.y };
307
- this.eventBus.emit(Events.UI.CursorMove, { x: event.x, y: event.y });
308
-
309
- // Если временно активирован pan, проксируем движение именно ему
310
- if (this.temporaryTool === 'pan' && this.activeTool?.name === 'pan') {
311
- this.activeTool.onMouseMove(event);
312
- this.syncActiveToolCursor();
313
- return;
314
- }
315
- this.activeTool.onMouseMove(event);
316
- this.syncActiveToolCursor();
135
+ return ToolEventRouter.handleMouseMove(this, e);
317
136
  }
318
137
 
319
138
  isCursorLockedToActiveTool() {
320
- return !!this.activeTool && this.activeTool.name !== 'select';
139
+ return ToolManagerGuards.isCursorLockedToActiveTool(this);
321
140
  }
322
141
 
323
142
  getPixiCursorStyles() {
324
- const renderer = this.pixiApp && this.pixiApp.renderer;
325
- if (!renderer) return null;
326
- const events = renderer.events || (renderer.plugins && renderer.plugins.interaction);
327
- return events && events.cursorStyles ? events.cursorStyles : null;
143
+ return ToolManagerGuards.getPixiCursorStyles(this);
328
144
  }
329
145
 
330
146
  getActiveToolCursor() {
331
- const cursor = this.activeTool && this.activeTool.cursor;
332
- if (typeof cursor === 'string' && cursor.length > 0) return cursor;
333
- return DEFAULT_CURSOR;
147
+ return ToolManagerGuards.getActiveToolCursor(this, DEFAULT_CURSOR);
334
148
  }
335
149
 
336
150
  syncActiveToolCursor() {
@@ -361,318 +175,34 @@ export class ToolManager {
361
175
  }
362
176
 
363
177
  handleMouseUp(e) {
364
- if (!this.activeTool) return;
365
- this.isMouseDown = false;
366
-
367
- const rect = this.container.getBoundingClientRect();
368
- const event = {
369
- x: e.clientX - rect.left,
370
- y: e.clientY - rect.top,
371
- button: e.button,
372
- target: e.target,
373
- originalEvent: e
374
- };
375
- this.lastMousePos = { x: event.x, y: event.y };
376
- this.eventBus.emit(Events.UI.CursorMove, { x: event.x, y: event.y });
377
- if (this.temporaryTool === 'pan') {
378
- this.handleAuxPanEnd(e);
379
- return;
380
- }
381
- this.activeTool.onMouseUp(event);
382
- this.syncActiveToolCursor();
178
+ return ToolEventRouter.handleMouseUp(this, e);
383
179
  }
384
180
 
385
181
  handleDoubleClick(e) {
386
- if (!this.activeTool) return;
387
-
388
- const rect = this.container.getBoundingClientRect();
389
- const event = {
390
- x: e.clientX - rect.left,
391
- y: e.clientY - rect.top,
392
- target: e.target,
393
- originalEvent: e
394
- };
395
- this.lastMousePos = { x: event.x, y: event.y };
396
- this.eventBus.emit(Events.UI.CursorMove, { x: event.x, y: event.y });
397
-
398
- console.log('🔧 ToolManager: Double click event, active tool:', this.activeTool.constructor.name);
399
- this.activeTool.onDoubleClick(event);
182
+ return ToolEventRouter.handleDoubleClick(this, e);
400
183
  }
401
184
 
402
185
  handleMouseWheel(e) {
403
- if (!this.activeTool) return;
404
-
405
- const rect = this.container.getBoundingClientRect();
406
- const event = {
407
- x: e.clientX - rect.left,
408
- y: e.clientY - rect.top,
409
- delta: e.deltaY,
410
- ctrlKey: e.ctrlKey,
411
- shiftKey: e.shiftKey,
412
- originalEvent: e
413
- };
414
- this.lastMousePos = { x: event.x, y: event.y };
415
- this.eventBus.emit(Events.UI.CursorMove, { x: event.x, y: event.y });
416
-
417
- // Глобальный зум колесиком (без Ctrl) — предотвращаем дефолтный скролл страницы
418
- this.eventBus.emit(Events.Tool.WheelZoom, { x: event.x, y: event.y, delta: e.deltaY });
419
- e.preventDefault();
420
-
421
- // Предотвращаем скроллинг страницы при зуме
422
- if (e.ctrlKey) {
423
- e.preventDefault();
424
- }
186
+ return ToolEventRouter.handleMouseWheel(this, e);
425
187
  }
426
188
 
427
189
  async handleDrop(e) {
428
- e.preventDefault();
429
- const rect = this.container.getBoundingClientRect();
430
- const x = e.clientX - rect.left;
431
- const y = e.clientY - rect.top;
432
- this.lastMousePos = { x, y };
433
- this.eventBus.emit(Events.UI.CursorMove, { x, y });
434
-
435
- const dt = e.dataTransfer;
436
- if (!dt) return;
437
-
438
- const emitAt = (src, name, imageId = null, offsetIndex = 0) => {
439
- const offset = 25 * offsetIndex;
440
- this.eventBus.emit(Events.UI.PasteImageAt, {
441
- x: x + offset,
442
- y: y + offset,
443
- src,
444
- name,
445
- imageId
446
- });
447
- };
448
-
449
- // 1) Файлы с рабочего стола
450
- const files = dt.files ? Array.from(dt.files) : [];
451
- const imageFiles = files.filter(f => f.type && f.type.startsWith('image/'));
452
- if (imageFiles.length > 0) {
453
- let index = 0;
454
- for (const file of imageFiles) {
455
- try {
456
- // Пытаемся загрузить изображение на сервер
457
- if (this.core && this.core.imageUploadService) {
458
- const uploadResult = await this.core.imageUploadService.uploadImage(file, file.name || 'image');
459
- emitAt(uploadResult.url, uploadResult.name, uploadResult.imageId || uploadResult.id, index++);
460
- } else {
461
- // Fallback к старому способу (base64)
462
- await new Promise((resolve) => {
463
- const reader = new FileReader();
464
- reader.onload = () => {
465
- emitAt(reader.result, file.name || 'image', null, index++);
466
- resolve();
467
- };
468
- reader.readAsDataURL(file);
469
- });
470
- }
471
- } catch (error) {
472
- console.warn('Ошибка загрузки изображения через drag-and-drop:', error);
473
- // Fallback к base64 при ошибке
474
- await new Promise((resolve) => {
475
- const reader = new FileReader();
476
- reader.onload = () => {
477
- emitAt(reader.result, file.name || 'image', null, index++);
478
- resolve();
479
- };
480
- reader.readAsDataURL(file);
481
- });
482
- }
483
- }
484
- return;
485
- }
486
-
487
- const nonImageFiles = files.filter(f => !f.type || !f.type.startsWith('image/'));
488
- if (nonImageFiles.length > 0) {
489
- let index = 0;
490
- for (const file of nonImageFiles) {
491
- const offset = 25 * index++;
492
- const position = { x: x + offset, y: y + offset };
493
- const fallbackProps = {
494
- fileName: file.name || 'file',
495
- fileSize: file.size || 0,
496
- mimeType: file.type || 'application/octet-stream',
497
- formattedSize: null,
498
- url: null,
499
- width: 120,
500
- height: 140
501
- };
502
- try {
503
- if (this.core && this.core.fileUploadService) {
504
- const uploadResult = await this.core.fileUploadService.uploadFile(file, file.name || 'file');
505
- this.eventBus.emit(Events.UI.ToolbarAction, {
506
- type: 'file',
507
- id: 'file',
508
- position,
509
- properties: {
510
- fileName: uploadResult.name,
511
- fileSize: uploadResult.size,
512
- mimeType: uploadResult.mimeType,
513
- formattedSize: uploadResult.formattedSize,
514
- url: uploadResult.url,
515
- width: 120,
516
- height: 140
517
- },
518
- fileId: uploadResult.fileId || uploadResult.id || null
519
- });
520
- } else {
521
- this.eventBus.emit(Events.UI.ToolbarAction, {
522
- type: 'file',
523
- id: 'file',
524
- position,
525
- properties: fallbackProps
526
- });
527
- }
528
- } catch (error) {
529
- console.warn('Ошибка загрузки файла через drag-and-drop:', error);
530
- this.eventBus.emit(Events.UI.ToolbarAction, {
531
- type: 'file',
532
- id: 'file',
533
- position,
534
- properties: fallbackProps
535
- });
536
- }
537
- }
538
- return;
539
- }
540
-
541
- // 2) Перетаскивание с другой вкладки: HTML/URI/PLAIN
542
- const html = dt.getData('text/html');
543
- if (html && html.includes('<img')) {
544
- const m = html.match(/<img[^>]*src\s*=\s*"([^"]+)"/i);
545
- if (m && m[1]) {
546
- const url = m[1];
547
- if (/^data:image\//i.test(url)) { emitAt(url, 'clipboard-image.png'); return; }
548
- if (/^https?:\/\//i.test(url)) {
549
- try {
550
- const resp = await fetch(url, { mode: 'cors' });
551
- const blob = await resp.blob();
552
- const dataUrl = await new Promise((res) => { const r = new FileReader(); r.onload = () => res(r.result); r.readAsDataURL(blob); });
553
- emitAt(dataUrl, url.split('/').pop() || 'image');
554
- } catch (_) {
555
- emitAt(url, url.split('/').pop() || 'image');
556
- }
557
- return;
558
- }
559
- }
560
- }
561
-
562
- const uriList = dt.getData('text/uri-list') || '';
563
- if (uriList) {
564
- const lines = uriList.split('\n').filter(l => !!l && !l.startsWith('#'));
565
- const urls = lines.filter(l => /^https?:\/\//i.test(l));
566
- let index = 0;
567
- for (const url of urls) {
568
- const isImage = /(png|jpe?g|gif|webp|bmp|svg)(\?.*)?$/i.test(url);
569
- if (!isImage) continue;
570
- try {
571
- const resp = await fetch(url, { mode: 'cors' });
572
- const blob = await resp.blob();
573
- const dataUrl = await new Promise((res) => { const r = new FileReader(); r.onload = () => res(r.result); r.readAsDataURL(blob); });
574
- emitAt(dataUrl, url.split('/').pop() || 'image', index++);
575
- } catch (_) {
576
- emitAt(url, url.split('/').pop() || 'image', index++);
577
- }
578
- }
579
- if (index > 0) return;
580
- }
581
-
582
- const text = dt.getData('text/plain') || '';
583
- if (text) {
584
- const trimmed = text.trim();
585
- const isDataUrl = /^data:image\//i.test(trimmed);
586
- const isHttpUrl = /^https?:\/\//i.test(trimmed);
587
- const looksLikeImage = /(png|jpe?g|gif|webp|bmp|svg)(\?.*)?$/i.test(trimmed);
588
- if (isDataUrl) { emitAt(trimmed, 'clipboard-image.png'); return; }
589
- if (isHttpUrl && looksLikeImage) {
590
- try {
591
- const resp = await fetch(trimmed, { mode: 'cors' });
592
- const blob = await resp.blob();
593
- const dataUrl = await new Promise((res) => { const r = new FileReader(); r.onload = () => res(r.result); r.readAsDataURL(blob); });
594
- emitAt(dataUrl, trimmed.split('/').pop() || 'image');
595
- } catch (_) {
596
- emitAt(trimmed, trimmed.split('/').pop() || 'image');
597
- }
598
- return;
599
- }
600
- }
190
+ return ToolEventRouter.handleDrop(this, e);
601
191
  }
602
192
 
603
193
  handleKeyDown(e) {
604
- // Обработка горячих клавиш для переключения инструментов
605
- this.handleHotkeys(e);
606
-
607
- if (!this.activeTool) return;
608
-
609
- const event = {
610
- key: e.key,
611
- code: e.code,
612
- ctrlKey: e.ctrlKey,
613
- shiftKey: e.shiftKey,
614
- altKey: e.altKey,
615
- originalEvent: e
616
- };
617
-
618
- this.activeTool.onKeyDown(event);
619
-
620
- // Тоггл пробела для временного pan
621
- if (e.key === ' ' && !e.repeat) {
622
- this.spacePressed = true;
623
- }
194
+ return ToolEventRouter.handleKeyDown(this, e);
624
195
  }
625
196
 
626
197
  handleKeyUp(e) {
627
- if (!this.activeTool) return;
628
-
629
- const event = {
630
- key: e.key,
631
- code: e.code,
632
- originalEvent: e
633
- };
634
-
635
- this.activeTool.onKeyUp(event);
636
-
637
- if (e.key === ' ') {
638
- this.spacePressed = false;
639
- // Если удерживали pan временно, вернуть инструмент
640
- if (this.temporaryTool === 'pan') {
641
- // Корректно завершим pan, если мышь ещё зажата
642
- if (this.activeTool?.name === 'pan' && this.isMouseDown) {
643
- this.activeTool.onMouseUp({ x: 0, y: 0, button: 0, target: this.container, originalEvent: e });
644
- }
645
- this.returnToPreviousTool();
646
- return;
647
- }
648
- }
198
+ return ToolEventRouter.handleKeyUp(this, e);
649
199
  }
650
200
 
651
201
  /**
652
202
  * Обработка горячих клавиш
653
203
  */
654
204
  handleHotkeys(e) {
655
- // Игнорируем горячие клавиши если фокус в input/textarea
656
- if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') {
657
- return;
658
- }
659
-
660
- // Ищем инструмент с соответствующей горячей клавишей
661
- for (const tool of this.tools.values()) {
662
- if (tool.hotkey === e.key.toLowerCase()) {
663
- this.activateTool(tool.name);
664
- e.preventDefault();
665
- break;
666
- }
667
- }
668
-
669
- // Специальные горячие клавиши
670
- switch (e.key) {
671
- case 'Escape': // Escape - возврат к default tool
672
- this.activateDefaultTool();
673
- e.preventDefault();
674
- break;
675
- }
205
+ return ToolEventRouter.handleHotkeys(this, e);
676
206
  }
677
207
 
678
208
  /**
@@ -688,43 +218,6 @@ export class ToolManager {
688
218
  * Очистка ресурсов
689
219
  */
690
220
  destroy() {
691
- // Деактивируем все инструменты
692
- for (const tool of this.tools.values()) {
693
- tool.destroy();
694
- }
695
-
696
- this.tools.clear();
697
- this.activeTool = null;
698
-
699
- // Удаляем обработчики событий
700
- if (this.container) {
701
- this.container.removeEventListener('mousedown', this.handleMouseDown);
702
- this.container.removeEventListener('mousemove', this.handleMouseMove);
703
- this.container.removeEventListener('mouseup', this.handleMouseUp);
704
- this.container.removeEventListener('dblclick', this.handleDoubleClick);
705
- this.container.removeEventListener('wheel', this.handleMouseWheel);
706
- this.container.removeEventListener('contextmenu', (e) => e.preventDefault());
707
- this.container.removeEventListener('dragenter', (e) => e.preventDefault());
708
- this.container.removeEventListener('dragover', (e) => e.preventDefault());
709
- this.container.removeEventListener('dragleave', () => {});
710
- this.container.removeEventListener('drop', this.handleDrop);
711
- }
712
- document.removeEventListener('mousemove', this.handleMouseMove);
713
- document.removeEventListener('mouseup', this.handleMouseUp);
714
-
715
- document.removeEventListener('keydown', this.handleKeyDown);
716
- document.removeEventListener('keyup', this.handleKeyUp);
717
- // Снимаем глобальный блокировщик Ctrl+колесо
718
- if (this._onWindowWheel) {
719
- try { window.removeEventListener('wheel', this._onWindowWheel); } catch (_) {}
720
- this._onWindowWheel = null;
721
- }
722
-
723
- const cursorStyles = this.getPixiCursorStyles();
724
- if (cursorStyles && this._originalPixiCursorStyles) {
725
- cursorStyles.pointer = this._originalPixiCursorStyles.pointer;
726
- cursorStyles.default = this._originalPixiCursorStyles.default;
727
- }
728
- this._originalPixiCursorStyles = null;
221
+ ToolManagerLifecycle.destroy(this);
729
222
  }
730
223
  }