@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,580 @@
1
+ import { Events } from '../core/events/Events.js';
2
+
3
+ /**
4
+ * Менеджер инструментов - управляет активными инструментами и переключением между ними
5
+ */
6
+ export class ToolManager {
7
+ constructor(eventBus, container, pixiApp = null, core = null) {
8
+ this.eventBus = eventBus;
9
+ this.container = container; // DOM элемент для обработки событий
10
+ this.pixiApp = pixiApp; // PIXI Application для передачи в инструменты
11
+ this.core = core; // Ссылка на core для доступа к imageUploadService
12
+ this.tools = new Map();
13
+ this.activeTool = null;
14
+ this.defaultTool = null;
15
+
16
+ // Состояние для временных инструментов
17
+ this.temporaryTool = null;
18
+ this.previousTool = null;
19
+ this.spacePressed = false;
20
+ this.isMouseDown = false;
21
+ // Последняя позиция курсора относительно контейнера (CSS-пиксели)
22
+ this.lastMousePos = null;
23
+ this.isMouseOverContainer = false;
24
+
25
+ this.initEventListeners();
26
+ }
27
+
28
+ /**
29
+ * Регистрирует инструмент
30
+ */
31
+ registerTool(tool) {
32
+ this.tools.set(tool.name, tool);
33
+
34
+ // Устанавливаем первый инструмент как по умолчанию
35
+ if (!this.defaultTool) {
36
+ this.defaultTool = tool.name;
37
+ }
38
+ }
39
+
40
+ /**
41
+ * Активирует инструмент
42
+ */
43
+ activateTool(toolName) {
44
+ const tool = this.tools.get(toolName);
45
+ if (!tool) {
46
+ console.warn(`Tool "${toolName}" not found`);
47
+ return false;
48
+ }
49
+
50
+ // Деактивируем текущий инструмент
51
+ if (this.activeTool) {
52
+ this.activeTool.deactivate();
53
+ }
54
+
55
+ // Активируем новый инструмент
56
+ this.activeTool = tool;
57
+
58
+ // Передаем PIXI app в метод activate, если он поддерживается
59
+ if (typeof this.activeTool.activate === 'function') {
60
+ this.activeTool.activate(this.pixiApp);
61
+ }
62
+
63
+ return true;
64
+ }
65
+
66
+ /**
67
+ * Временно активирует инструмент (с возвратом к предыдущему)
68
+ */
69
+ activateTemporaryTool(toolName) {
70
+ if (this.activeTool) {
71
+ this.previousTool = this.activeTool.name;
72
+ }
73
+
74
+ this.activateTool(toolName);
75
+ this.temporaryTool = toolName;
76
+ }
77
+
78
+ /**
79
+ * Возвращается к предыдущему инструменту
80
+ */
81
+ returnToPreviousTool() {
82
+ if (this.temporaryTool && this.previousTool) {
83
+ this.activateTool(this.previousTool);
84
+ this.temporaryTool = null;
85
+ this.previousTool = null;
86
+ }
87
+ }
88
+
89
+ /**
90
+ * Возвращается к инструменту по умолчанию
91
+ */
92
+ activateDefaultTool() {
93
+ if (this.defaultTool) {
94
+ this.activateTool(this.defaultTool);
95
+ }
96
+ }
97
+
98
+ /**
99
+ * Получает активный инструмент
100
+ */
101
+ getActiveTool() {
102
+ return this.activeTool;
103
+ }
104
+
105
+ /**
106
+ * Получает список всех инструментов
107
+ */
108
+ getAllTools() {
109
+ return Array.from(this.tools.values());
110
+ }
111
+
112
+ /**
113
+ * Проверяет, зарегистрирован ли инструмент
114
+ */
115
+ hasActiveTool(toolName) {
116
+ return this.tools.has(toolName);
117
+ }
118
+
119
+ /**
120
+ * Инициализирует обработчики событий DOM
121
+ */
122
+ initEventListeners() {
123
+ if (!this.container) return;
124
+
125
+ // События мыши на контейнере
126
+ this.container.addEventListener('mousedown', (e) => this.handleMouseDown(e));
127
+ this.container.addEventListener('mousemove', (e) => this.handleMouseMove(e));
128
+ this.container.addEventListener('mouseup', (e) => this.handleMouseUp(e));
129
+ this.container.addEventListener('mouseenter', () => { this.isMouseOverContainer = true; });
130
+ this.container.addEventListener('mouseleave', () => { this.isMouseOverContainer = false; });
131
+ // Убираем отдельные слушатели aux-pan на контейнере, чтобы не дублировать mousedown/mouseup
132
+
133
+ // Drag & Drop — поддержка перетаскивания изображений на холст
134
+ this.container.addEventListener('dragenter', (e) => {
135
+ e.preventDefault();
136
+ });
137
+ this.container.addEventListener('dragover', (e) => {
138
+ e.preventDefault();
139
+ if (e.dataTransfer) e.dataTransfer.dropEffect = 'copy';
140
+ });
141
+ this.container.addEventListener('dragleave', (e) => {
142
+ // можно снимать подсветку, если добавим в будущем
143
+ });
144
+ this.container.addEventListener('drop', (e) => this.handleDrop(e));
145
+
146
+ // Глобальные события мыши — чтобы корректно завершать drag/resize при отпускании за пределами холста
147
+ document.addEventListener('mousemove', (e) => this.handleMouseMove(e));
148
+ document.addEventListener('mouseup', (e) => {
149
+ this.handleMouseUp(e);
150
+ // Гарантированно завершаем временный pan, даже если кнопка отпущена вне холста
151
+ if (this.temporaryTool === 'pan') {
152
+ this.handleAuxPanEnd(e);
153
+ }
154
+ });
155
+ this.container.addEventListener('dblclick', (e) => this.handleDoubleClick(e));
156
+ this.container.addEventListener('wheel', (e) => this.handleMouseWheel(e));
157
+
158
+ // События клавиатуры (на document)
159
+ document.addEventListener('keydown', (e) => this.handleKeyDown(e));
160
+ document.addEventListener('keyup', (e) => this.handleKeyUp(e));
161
+
162
+ // Контекстное меню: предотвращаем дефолт и пересылаем событие активному инструменту
163
+ this.container.addEventListener('contextmenu', (e) => {
164
+ e.preventDefault();
165
+ if (!this.activeTool) return;
166
+ const rect = this.container.getBoundingClientRect();
167
+ const event = {
168
+ x: e.clientX - rect.left,
169
+ y: e.clientY - rect.top,
170
+ originalEvent: e
171
+ };
172
+ if (typeof this.activeTool.onContextMenu === 'function') {
173
+ this.activeTool.onContextMenu(event);
174
+ }
175
+ });
176
+ }
177
+
178
+ /**
179
+ * Обработчики DOM событий
180
+ */
181
+
182
+ handleMouseDown(e) {
183
+ if (!this.activeTool) return;
184
+ this.isMouseDown = true;
185
+
186
+ // Если удерживается пробел + левая кнопка — сразу запускаем pan и не дергаем активный инструмент
187
+ if (this.spacePressed && e.button === 0) {
188
+ this.handleAuxPanStart(e);
189
+ return;
190
+ }
191
+ // Средняя кнопка — тоже панорамирование без дергания активного инструмента
192
+ if (e.button === 1) {
193
+ this.handleAuxPanStart(e);
194
+ return;
195
+ }
196
+
197
+ const rect = this.container.getBoundingClientRect();
198
+ const event = {
199
+ x: e.clientX - rect.left,
200
+ y: e.clientY - rect.top,
201
+ button: e.button,
202
+ target: e.target,
203
+ originalEvent: e
204
+ };
205
+
206
+ this.lastMousePos = { x: event.x, y: event.y };
207
+ this.eventBus.emit(Events.UI.CursorMove, { x: event.x, y: event.y });
208
+
209
+ this.activeTool.onMouseDown(event);
210
+ }
211
+
212
+ // Поддержка панорамирования средней кнопкой мыши без переключения инструмента
213
+ handleAuxPanStart(e) {
214
+ // Средняя кнопка (button === 1) или пробел зажат и левая кнопка
215
+ const isMiddle = e.button === 1;
216
+ const isSpaceLeft = e.button === 0 && this.spacePressed;
217
+ if (!isMiddle && !isSpaceLeft) return;
218
+
219
+ // Временная активация pan-инструмента
220
+ if (this.hasActiveTool('pan')) {
221
+ this.previousTool = this.activeTool?.name || null;
222
+ this.activateTemporaryTool('pan');
223
+ // Синтетический mousedown для запуска pan
224
+ const rect = this.container.getBoundingClientRect();
225
+ const event = {
226
+ x: e.clientX - rect.left,
227
+ y: e.clientY - rect.top,
228
+ button: 0,
229
+ target: e.target,
230
+ originalEvent: e
231
+ };
232
+ this.lastMousePos = { x: event.x, y: event.y };
233
+ this.eventBus.emit(Events.UI.CursorMove, { x: event.x, y: event.y });
234
+ this.activeTool.onMouseDown(event);
235
+ }
236
+ }
237
+
238
+ handleAuxPanEnd(e) {
239
+ // Завершаем временное панорамирование при отпускании средней/левой (с пробелом)
240
+ if (this.temporaryTool === 'pan') {
241
+ const rect = this.container.getBoundingClientRect();
242
+ const event = {
243
+ x: e.clientX - rect.left,
244
+ y: e.clientY - rect.top,
245
+ button: 0,
246
+ target: e.target,
247
+ originalEvent: e
248
+ };
249
+ this.lastMousePos = { x: event.x, y: event.y };
250
+ this.eventBus.emit(Events.UI.CursorMove, { x: event.x, y: event.y });
251
+ this.activeTool.onMouseUp(event);
252
+ this.returnToPreviousTool();
253
+ return;
254
+ }
255
+ }
256
+
257
+ handleMouseMove(e) {
258
+ if (!this.activeTool) return;
259
+
260
+ const rect = this.container.getBoundingClientRect();
261
+ const event = {
262
+ x: e.clientX - rect.left,
263
+ y: e.clientY - rect.top,
264
+ target: e.target,
265
+ originalEvent: e
266
+ };
267
+
268
+ // Запоминаем и рассылаем позицию курсора для использования другими подсистемами
269
+ this.lastMousePos = { x: event.x, y: event.y };
270
+ this.eventBus.emit(Events.UI.CursorMove, { x: event.x, y: event.y });
271
+
272
+ // Если временно активирован pan, проксируем движение именно ему
273
+ if (this.temporaryTool === 'pan' && this.activeTool?.name === 'pan') {
274
+ this.activeTool.onMouseMove(event);
275
+ return;
276
+ }
277
+ this.activeTool.onMouseMove(event);
278
+ }
279
+
280
+ handleMouseUp(e) {
281
+ if (!this.activeTool) return;
282
+ this.isMouseDown = false;
283
+
284
+ const rect = this.container.getBoundingClientRect();
285
+ const event = {
286
+ x: e.clientX - rect.left,
287
+ y: e.clientY - rect.top,
288
+ button: e.button,
289
+ target: e.target,
290
+ originalEvent: e
291
+ };
292
+ this.lastMousePos = { x: event.x, y: event.y };
293
+ this.eventBus.emit(Events.UI.CursorMove, { x: event.x, y: event.y });
294
+ if (this.temporaryTool === 'pan') {
295
+ this.handleAuxPanEnd(e);
296
+ return;
297
+ }
298
+ this.activeTool.onMouseUp(event);
299
+ }
300
+
301
+ handleDoubleClick(e) {
302
+ if (!this.activeTool) return;
303
+
304
+ const rect = this.container.getBoundingClientRect();
305
+ const event = {
306
+ x: e.clientX - rect.left,
307
+ y: e.clientY - rect.top,
308
+ target: e.target,
309
+ originalEvent: e
310
+ };
311
+ this.lastMousePos = { x: event.x, y: event.y };
312
+ this.eventBus.emit(Events.UI.CursorMove, { x: event.x, y: event.y });
313
+
314
+ console.log('🔧 ToolManager: Double click event, active tool:', this.activeTool.constructor.name);
315
+ this.activeTool.onDoubleClick(event);
316
+ }
317
+
318
+ handleMouseWheel(e) {
319
+ if (!this.activeTool) return;
320
+
321
+ const rect = this.container.getBoundingClientRect();
322
+ const event = {
323
+ x: e.clientX - rect.left,
324
+ y: e.clientY - rect.top,
325
+ delta: e.deltaY,
326
+ ctrlKey: e.ctrlKey,
327
+ shiftKey: e.shiftKey,
328
+ originalEvent: e
329
+ };
330
+ this.lastMousePos = { x: event.x, y: event.y };
331
+ this.eventBus.emit(Events.UI.CursorMove, { x: event.x, y: event.y });
332
+
333
+ // Глобальный зум колесиком (без Ctrl) — предотвращаем дефолтный скролл страницы
334
+ this.eventBus.emit(Events.Tool.WheelZoom, { x: event.x, y: event.y, delta: e.deltaY });
335
+ e.preventDefault();
336
+
337
+ // Предотвращаем скроллинг страницы при зуме
338
+ if (e.ctrlKey) {
339
+ e.preventDefault();
340
+ }
341
+ }
342
+
343
+ async handleDrop(e) {
344
+ e.preventDefault();
345
+ const rect = this.container.getBoundingClientRect();
346
+ const x = e.clientX - rect.left;
347
+ const y = e.clientY - rect.top;
348
+ this.lastMousePos = { x, y };
349
+ this.eventBus.emit(Events.UI.CursorMove, { x, y });
350
+
351
+ const dt = e.dataTransfer;
352
+ if (!dt) return;
353
+
354
+ const emitAt = (src, name, imageId = null, offsetIndex = 0) => {
355
+ const offset = 25 * offsetIndex;
356
+ this.eventBus.emit(Events.UI.PasteImageAt, {
357
+ x: x + offset,
358
+ y: y + offset,
359
+ src,
360
+ name,
361
+ imageId
362
+ });
363
+ };
364
+
365
+ // 1) Файлы с рабочего стола
366
+ const files = dt.files ? Array.from(dt.files) : [];
367
+ const imageFiles = files.filter(f => f.type && f.type.startsWith('image/'));
368
+ if (imageFiles.length > 0) {
369
+ let index = 0;
370
+ for (const file of imageFiles) {
371
+ try {
372
+ // Пытаемся загрузить изображение на сервер
373
+ if (this.core && this.core.imageUploadService) {
374
+ const uploadResult = await this.core.imageUploadService.uploadImage(file, file.name || 'image');
375
+ emitAt(uploadResult.url, uploadResult.name, uploadResult.id, index++);
376
+ } else {
377
+ // Fallback к старому способу (base64)
378
+ await new Promise((resolve) => {
379
+ const reader = new FileReader();
380
+ reader.onload = () => {
381
+ emitAt(reader.result, file.name || 'image', null, index++);
382
+ resolve();
383
+ };
384
+ reader.readAsDataURL(file);
385
+ });
386
+ }
387
+ } catch (error) {
388
+ console.warn('Ошибка загрузки изображения через drag-and-drop:', error);
389
+ // Fallback к base64 при ошибке
390
+ await new Promise((resolve) => {
391
+ const reader = new FileReader();
392
+ reader.onload = () => {
393
+ emitAt(reader.result, file.name || 'image', null, index++);
394
+ resolve();
395
+ };
396
+ reader.readAsDataURL(file);
397
+ });
398
+ }
399
+ }
400
+ return;
401
+ }
402
+
403
+ // 2) Перетаскивание с другой вкладки: HTML/URI/PLAIN
404
+ const html = dt.getData('text/html');
405
+ if (html && html.includes('<img')) {
406
+ const m = html.match(/<img[^>]*src\s*=\s*"([^"]+)"/i);
407
+ if (m && m[1]) {
408
+ const url = m[1];
409
+ if (/^data:image\//i.test(url)) { emitAt(url, 'clipboard-image.png'); return; }
410
+ if (/^https?:\/\//i.test(url)) {
411
+ try {
412
+ const resp = await fetch(url, { mode: 'cors' });
413
+ const blob = await resp.blob();
414
+ const dataUrl = await new Promise((res) => { const r = new FileReader(); r.onload = () => res(r.result); r.readAsDataURL(blob); });
415
+ emitAt(dataUrl, url.split('/').pop() || 'image');
416
+ } catch (_) {
417
+ emitAt(url, url.split('/').pop() || 'image');
418
+ }
419
+ return;
420
+ }
421
+ }
422
+ }
423
+
424
+ const uriList = dt.getData('text/uri-list') || '';
425
+ if (uriList) {
426
+ const lines = uriList.split('\n').filter(l => !!l && !l.startsWith('#'));
427
+ const urls = lines.filter(l => /^https?:\/\//i.test(l));
428
+ let index = 0;
429
+ for (const url of urls) {
430
+ const isImage = /(png|jpe?g|gif|webp|bmp|svg)(\?.*)?$/i.test(url);
431
+ if (!isImage) continue;
432
+ try {
433
+ const resp = await fetch(url, { mode: 'cors' });
434
+ const blob = await resp.blob();
435
+ const dataUrl = await new Promise((res) => { const r = new FileReader(); r.onload = () => res(r.result); r.readAsDataURL(blob); });
436
+ emitAt(dataUrl, url.split('/').pop() || 'image', index++);
437
+ } catch (_) {
438
+ emitAt(url, url.split('/').pop() || 'image', index++);
439
+ }
440
+ }
441
+ if (index > 0) return;
442
+ }
443
+
444
+ const text = dt.getData('text/plain') || '';
445
+ if (text) {
446
+ const trimmed = text.trim();
447
+ const isDataUrl = /^data:image\//i.test(trimmed);
448
+ const isHttpUrl = /^https?:\/\//i.test(trimmed);
449
+ const looksLikeImage = /(png|jpe?g|gif|webp|bmp|svg)(\?.*)?$/i.test(trimmed);
450
+ if (isDataUrl) { emitAt(trimmed, 'clipboard-image.png'); return; }
451
+ if (isHttpUrl && looksLikeImage) {
452
+ try {
453
+ const resp = await fetch(trimmed, { mode: 'cors' });
454
+ const blob = await resp.blob();
455
+ const dataUrl = await new Promise((res) => { const r = new FileReader(); r.onload = () => res(r.result); r.readAsDataURL(blob); });
456
+ emitAt(dataUrl, trimmed.split('/').pop() || 'image');
457
+ } catch (_) {
458
+ emitAt(trimmed, trimmed.split('/').pop() || 'image');
459
+ }
460
+ return;
461
+ }
462
+ }
463
+ }
464
+
465
+ handleKeyDown(e) {
466
+ // Обработка горячих клавиш для переключения инструментов
467
+ this.handleHotkeys(e);
468
+
469
+ if (!this.activeTool) return;
470
+
471
+ const event = {
472
+ key: e.key,
473
+ code: e.code,
474
+ ctrlKey: e.ctrlKey,
475
+ shiftKey: e.shiftKey,
476
+ altKey: e.altKey,
477
+ originalEvent: e
478
+ };
479
+
480
+ this.activeTool.onKeyDown(event);
481
+
482
+ // Тоггл пробела для временного pan
483
+ if (e.key === ' ' && !e.repeat) {
484
+ this.spacePressed = true;
485
+ }
486
+ }
487
+
488
+ handleKeyUp(e) {
489
+ if (!this.activeTool) return;
490
+
491
+ const event = {
492
+ key: e.key,
493
+ code: e.code,
494
+ originalEvent: e
495
+ };
496
+
497
+ this.activeTool.onKeyUp(event);
498
+
499
+ if (e.key === ' ') {
500
+ this.spacePressed = false;
501
+ // Если удерживали pan временно, вернуть инструмент
502
+ if (this.temporaryTool === 'pan') {
503
+ // Корректно завершим pan, если мышь ещё зажата
504
+ if (this.activeTool?.name === 'pan' && this.isMouseDown) {
505
+ this.activeTool.onMouseUp({ x: 0, y: 0, button: 0, target: this.container, originalEvent: e });
506
+ }
507
+ this.returnToPreviousTool();
508
+ return;
509
+ }
510
+ }
511
+ }
512
+
513
+ /**
514
+ * Обработка горячих клавиш
515
+ */
516
+ handleHotkeys(e) {
517
+ // Игнорируем горячие клавиши если фокус в input/textarea
518
+ if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') {
519
+ return;
520
+ }
521
+
522
+ // Ищем инструмент с соответствующей горячей клавишей
523
+ for (const tool of this.tools.values()) {
524
+ if (tool.hotkey === e.key.toLowerCase()) {
525
+ this.activateTool(tool.name);
526
+ e.preventDefault();
527
+ break;
528
+ }
529
+ }
530
+
531
+ // Специальные горячие клавиши
532
+ switch (e.key) {
533
+ case 'Escape': // Escape - возврат к default tool
534
+ this.activateDefaultTool();
535
+ e.preventDefault();
536
+ break;
537
+ }
538
+ }
539
+
540
+ /**
541
+ * Обработка отпускания пробела
542
+ */
543
+ handleSpaceUp(e) {
544
+ if (e.key === ' ' && this.temporaryTool === 'pan') {
545
+ this.returnToPreviousTool();
546
+ }
547
+ }
548
+
549
+ /**
550
+ * Очистка ресурсов
551
+ */
552
+ destroy() {
553
+ // Деактивируем все инструменты
554
+ for (const tool of this.tools.values()) {
555
+ tool.destroy();
556
+ }
557
+
558
+ this.tools.clear();
559
+ this.activeTool = null;
560
+
561
+ // Удаляем обработчики событий
562
+ if (this.container) {
563
+ this.container.removeEventListener('mousedown', this.handleMouseDown);
564
+ this.container.removeEventListener('mousemove', this.handleMouseMove);
565
+ this.container.removeEventListener('mouseup', this.handleMouseUp);
566
+ this.container.removeEventListener('dblclick', this.handleDoubleClick);
567
+ this.container.removeEventListener('wheel', this.handleMouseWheel);
568
+ this.container.removeEventListener('contextmenu', (e) => e.preventDefault());
569
+ this.container.removeEventListener('dragenter', (e) => e.preventDefault());
570
+ this.container.removeEventListener('dragover', (e) => e.preventDefault());
571
+ this.container.removeEventListener('dragleave', () => {});
572
+ this.container.removeEventListener('drop', this.handleDrop);
573
+ }
574
+ document.removeEventListener('mousemove', this.handleMouseMove);
575
+ document.removeEventListener('mouseup', this.handleMouseUp);
576
+
577
+ document.removeEventListener('keydown', this.handleKeyDown);
578
+ document.removeEventListener('keyup', this.handleKeyUp);
579
+ }
580
+ }
@@ -0,0 +1,43 @@
1
+ import { BaseTool } from '../BaseTool.js';
2
+
3
+ // Новый упрощенный PanTool: только drag-логика, без инерции и колесика
4
+ export class PanTool extends BaseTool {
5
+ constructor(eventBus) {
6
+ super('pan', eventBus);
7
+ this.cursor = 'grab';
8
+ this.isDragging = false;
9
+ this.last = { x: 0, y: 0 };
10
+ }
11
+
12
+ onMouseDown(event) {
13
+ // ЛКМ или средняя кнопка
14
+ if (event.button === 0 || event.button === 1) {
15
+ this.isDragging = true;
16
+ this.last = { x: event.x, y: event.y };
17
+ this.cursor = 'grabbing';
18
+ this.setCursor();
19
+ }
20
+ }
21
+
22
+ onMouseMove(event) {
23
+ if (!this.isDragging) return;
24
+ const dx = event.x - this.last.x;
25
+ const dy = event.y - this.last.y;
26
+ this.last = { x: event.x, y: event.y };
27
+ this.emit('pan:update', { delta: { x: dx, y: dy } });
28
+ }
29
+
30
+ onMouseUp(event) {
31
+ if (this.isDragging) {
32
+ this.isDragging = false;
33
+ this.cursor = 'grab';
34
+ this.setCursor();
35
+ }
36
+ }
37
+
38
+ onDeactivate() {
39
+ this.isDragging = false;
40
+ this.cursor = 'default';
41
+ this.setCursor();
42
+ }
43
+ }