@sequent-org/moodboard 1.2.118 → 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 +7 -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 -1765
  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 -976
  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
@@ -4,8 +4,7 @@ import { BaseTool } from '../BaseTool.js';
4
4
  export class PanTool extends BaseTool {
5
5
  constructor(eventBus) {
6
6
  super('pan', eventBus);
7
- // По умолчанию курсор для панорамирования системный move
8
- this.cursor = 'move';
7
+ this.cursor = 'grab'; // курсор-рука (открытая) когда инструмент выбран
9
8
  this.isDragging = false;
10
9
  this.last = { x: 0, y: 0 };
11
10
  this.app = null;
@@ -18,19 +17,16 @@ export class PanTool extends BaseTool {
18
17
  activate(app) {
19
18
  super.activate();
20
19
  this.app = app || this.app;
21
- // При активации сразу показываем курсор move
22
- this.cursor = 'move';
20
+ this.cursor = 'grab';
23
21
  this.setCursor();
24
22
  }
25
23
 
26
24
  onMouseDown(event) {
27
- // ЛКМ или средняя кнопка
25
+ // ЛКМ или средняя кнопка (или пробел+ЛКМ — aux pan)
28
26
  if (event.button === 0 || event.button === 1) {
29
27
  this.isDragging = true;
30
28
  this.last = { x: event.x, y: event.y };
31
- // Во время активного drag оставляем курсор move,
32
- // чтобы пользователь всегда видел иконку перемещения
33
- this.cursor = 'move';
29
+ this.cursor = 'grabbing'; // сжатая рука (хватает) во время drag
34
30
  this.setCursor();
35
31
  }
36
32
  }
@@ -46,15 +42,13 @@ export class PanTool extends BaseTool {
46
42
  onMouseUp(event) {
47
43
  if (this.isDragging) {
48
44
  this.isDragging = false;
49
- // После завершения drag возвращаем курсор move
50
- this.cursor = 'move';
45
+ this.cursor = 'grab';
51
46
  this.setCursor();
52
47
  }
53
48
  }
54
49
 
55
50
  onDeactivate() {
56
51
  this.isDragging = false;
57
- // Сбрасываем курсор на стандартный для canvas
58
52
  this.cursor = '';
59
53
  this.setCursor();
60
54
  super.onDeactivate();
@@ -0,0 +1,49 @@
1
+ export class ToolActivationController {
2
+ constructor(manager) {
3
+ this.manager = manager;
4
+ }
5
+
6
+ activateTool(toolName) {
7
+ const tool = this.manager.registry.get(toolName);
8
+ if (!tool) {
9
+ console.warn(`Tool "${toolName}" not found`);
10
+ return false;
11
+ }
12
+
13
+ if (this.manager.activeTool) {
14
+ this.manager.activeTool.deactivate();
15
+ }
16
+
17
+ this.manager.activeTool = tool;
18
+
19
+ if (typeof this.manager.activeTool.activate === 'function') {
20
+ this.manager.activeTool.activate(this.manager.pixiApp);
21
+ }
22
+ this.manager.syncActiveToolCursor();
23
+
24
+ return true;
25
+ }
26
+
27
+ activateTemporaryTool(toolName) {
28
+ if (this.manager.activeTool) {
29
+ this.manager.previousTool = this.manager.activeTool.name;
30
+ }
31
+
32
+ this.activateTool(toolName);
33
+ this.manager.temporaryTool = toolName;
34
+ }
35
+
36
+ returnToPreviousTool() {
37
+ if (this.manager.temporaryTool && this.manager.previousTool) {
38
+ this.activateTool(this.manager.previousTool);
39
+ this.manager.temporaryTool = null;
40
+ this.manager.previousTool = null;
41
+ }
42
+ }
43
+
44
+ activateDefaultTool() {
45
+ if (this.manager.defaultTool) {
46
+ this.activateTool(this.manager.defaultTool);
47
+ }
48
+ }
49
+ }
@@ -0,0 +1,396 @@
1
+ import { Events } from '../../core/events/Events.js';
2
+ import { ToolManagerGuards } from './ToolManagerGuards.js';
3
+
4
+ function createPointerEvent(manager, event, extras = {}) {
5
+ const rect = manager.container.getBoundingClientRect();
6
+
7
+ return {
8
+ x: event.clientX - rect.left,
9
+ y: event.clientY - rect.top,
10
+ originalEvent: event,
11
+ ...extras
12
+ };
13
+ }
14
+
15
+ function rememberCursorPosition(manager, event) {
16
+ manager.lastMousePos = { x: event.x, y: event.y };
17
+ manager.eventBus.emit(Events.UI.CursorMove, { x: event.x, y: event.y });
18
+ }
19
+
20
+ export class ToolEventRouter {
21
+ static handleMouseDown(manager, event) {
22
+ if (!manager.activeTool) return;
23
+ manager.isMouseDown = true;
24
+
25
+ if (manager.spacePressed && event.button === 0) {
26
+ this.handleAuxPanStart(manager, event);
27
+ return;
28
+ }
29
+ if (event.button === 1) {
30
+ this.handleAuxPanStart(manager, event);
31
+ return;
32
+ }
33
+
34
+ const toolEvent = createPointerEvent(manager, event, {
35
+ button: event.button,
36
+ target: event.target
37
+ });
38
+
39
+ rememberCursorPosition(manager, toolEvent);
40
+ manager.activeTool.onMouseDown(toolEvent);
41
+ }
42
+
43
+ static handleAuxPanStart(manager, event) {
44
+ if (!ToolManagerGuards.isAuxPanStart(manager, event)) return;
45
+
46
+ if (manager.hasActiveTool('pan')) {
47
+ manager.previousTool = manager.activeTool?.name || null;
48
+ manager.activateTemporaryTool('pan');
49
+
50
+ const toolEvent = createPointerEvent(manager, event, {
51
+ button: 0,
52
+ target: event.target
53
+ });
54
+
55
+ rememberCursorPosition(manager, toolEvent);
56
+ manager.activeTool.onMouseDown(toolEvent);
57
+ }
58
+ }
59
+
60
+ static handleAuxPanEnd(manager, event) {
61
+ if (ToolManagerGuards.isTemporaryPanActive(manager)) {
62
+ const toolEvent = createPointerEvent(manager, event, {
63
+ button: 0,
64
+ target: event.target
65
+ });
66
+
67
+ rememberCursorPosition(manager, toolEvent);
68
+ manager.activeTool.onMouseUp(toolEvent);
69
+ manager.returnToPreviousTool();
70
+ }
71
+ }
72
+
73
+ static handleMouseMove(manager, event) {
74
+ if (!manager.activeTool) return;
75
+
76
+ const toolEvent = createPointerEvent(manager, event, {
77
+ target: event.target
78
+ });
79
+
80
+ rememberCursorPosition(manager, toolEvent);
81
+
82
+ if (ToolManagerGuards.isTemporaryPanActive(manager) && manager.activeTool?.name === 'pan') {
83
+ manager.activeTool.onMouseMove(toolEvent);
84
+ manager.syncActiveToolCursor();
85
+ return;
86
+ }
87
+
88
+ manager.activeTool.onMouseMove(toolEvent);
89
+ manager.syncActiveToolCursor();
90
+ }
91
+
92
+ static handleMouseUp(manager, event) {
93
+ if (!manager.activeTool) return;
94
+ manager.isMouseDown = false;
95
+
96
+ const toolEvent = createPointerEvent(manager, event, {
97
+ button: event.button,
98
+ target: event.target
99
+ });
100
+
101
+ rememberCursorPosition(manager, toolEvent);
102
+ if (ToolManagerGuards.isTemporaryPanActive(manager)) {
103
+ manager.handleAuxPanEnd(event);
104
+ return;
105
+ }
106
+
107
+ manager.activeTool.onMouseUp(toolEvent);
108
+ manager.syncActiveToolCursor();
109
+ }
110
+
111
+ static handleDoubleClick(manager, event) {
112
+ if (!manager.activeTool) return;
113
+
114
+ const toolEvent = createPointerEvent(manager, event, {
115
+ target: event.target
116
+ });
117
+
118
+ rememberCursorPosition(manager, toolEvent);
119
+
120
+ console.log('🔧 ToolManager: Double click event, active tool:', manager.activeTool.constructor.name);
121
+ manager.activeTool.onDoubleClick(toolEvent);
122
+ }
123
+
124
+ static handleMouseWheel(manager, event) {
125
+ if (!manager.activeTool) return;
126
+
127
+ const toolEvent = createPointerEvent(manager, event, {
128
+ delta: event.deltaY,
129
+ ctrlKey: event.ctrlKey,
130
+ shiftKey: event.shiftKey
131
+ });
132
+
133
+ rememberCursorPosition(manager, toolEvent);
134
+
135
+ manager.eventBus.emit(Events.Tool.WheelZoom, { x: toolEvent.x, y: toolEvent.y, delta: event.deltaY });
136
+ event.preventDefault();
137
+
138
+ if (event.ctrlKey) {
139
+ event.preventDefault();
140
+ }
141
+ }
142
+
143
+ static async handleDrop(manager, event) {
144
+ event.preventDefault();
145
+
146
+ const rect = manager.container.getBoundingClientRect();
147
+ const x = event.clientX - rect.left;
148
+ const y = event.clientY - rect.top;
149
+ manager.lastMousePos = { x, y };
150
+ manager.eventBus.emit(Events.UI.CursorMove, { x, y });
151
+
152
+ const dt = event.dataTransfer;
153
+ if (!dt) return;
154
+
155
+ const emitAt = (src, name, imageId = null, offsetIndex = 0) => {
156
+ const offset = 25 * offsetIndex;
157
+ manager.eventBus.emit(Events.UI.PasteImageAt, {
158
+ x: x + offset,
159
+ y: y + offset,
160
+ src,
161
+ name,
162
+ imageId
163
+ });
164
+ };
165
+
166
+ const files = dt.files ? Array.from(dt.files) : [];
167
+ const imageFiles = files.filter((file) => file.type && file.type.startsWith('image/'));
168
+ if (imageFiles.length > 0) {
169
+ let index = 0;
170
+ for (const file of imageFiles) {
171
+ try {
172
+ if (manager.core && manager.core.imageUploadService) {
173
+ const uploadResult = await manager.core.imageUploadService.uploadImage(file, file.name || 'image');
174
+ emitAt(uploadResult.url, uploadResult.name, uploadResult.imageId || uploadResult.id, index++);
175
+ } else {
176
+ await new Promise((resolve) => {
177
+ const reader = new FileReader();
178
+ reader.onload = () => {
179
+ emitAt(reader.result, file.name || 'image', null, index++);
180
+ resolve();
181
+ };
182
+ reader.readAsDataURL(file);
183
+ });
184
+ }
185
+ } catch (error) {
186
+ console.warn('Ошибка загрузки изображения через drag-and-drop:', error);
187
+ await new Promise((resolve) => {
188
+ const reader = new FileReader();
189
+ reader.onload = () => {
190
+ emitAt(reader.result, file.name || 'image', null, index++);
191
+ resolve();
192
+ };
193
+ reader.readAsDataURL(file);
194
+ });
195
+ }
196
+ }
197
+ return;
198
+ }
199
+
200
+ const nonImageFiles = files.filter((file) => !file.type || !file.type.startsWith('image/'));
201
+ if (nonImageFiles.length > 0) {
202
+ let index = 0;
203
+ for (const file of nonImageFiles) {
204
+ const offset = 25 * index++;
205
+ const position = { x: x + offset, y: y + offset };
206
+ const fallbackProps = {
207
+ fileName: file.name || 'file',
208
+ fileSize: file.size || 0,
209
+ mimeType: file.type || 'application/octet-stream',
210
+ formattedSize: null,
211
+ url: null,
212
+ width: 120,
213
+ height: 140
214
+ };
215
+ try {
216
+ if (manager.core && manager.core.fileUploadService) {
217
+ const uploadResult = await manager.core.fileUploadService.uploadFile(file, file.name || 'file');
218
+ manager.eventBus.emit(Events.UI.ToolbarAction, {
219
+ type: 'file',
220
+ id: 'file',
221
+ position,
222
+ properties: {
223
+ fileName: uploadResult.name,
224
+ fileSize: uploadResult.size,
225
+ mimeType: uploadResult.mimeType,
226
+ formattedSize: uploadResult.formattedSize,
227
+ url: uploadResult.url,
228
+ width: 120,
229
+ height: 140
230
+ },
231
+ fileId: uploadResult.fileId || uploadResult.id || null
232
+ });
233
+ } else {
234
+ manager.eventBus.emit(Events.UI.ToolbarAction, {
235
+ type: 'file',
236
+ id: 'file',
237
+ position,
238
+ properties: fallbackProps
239
+ });
240
+ }
241
+ } catch (error) {
242
+ console.warn('Ошибка загрузки файла через drag-and-drop:', error);
243
+ manager.eventBus.emit(Events.UI.ToolbarAction, {
244
+ type: 'file',
245
+ id: 'file',
246
+ position,
247
+ properties: fallbackProps
248
+ });
249
+ }
250
+ }
251
+ return;
252
+ }
253
+
254
+ const html = dt.getData('text/html');
255
+ if (html && html.includes('<img')) {
256
+ const match = html.match(/<img[^>]*src\s*=\s*"([^"]+)"/i);
257
+ if (match && match[1]) {
258
+ const url = match[1];
259
+ if (/^data:image\//i.test(url)) {
260
+ emitAt(url, 'clipboard-image.png');
261
+ return;
262
+ }
263
+ if (/^https?:\/\//i.test(url)) {
264
+ try {
265
+ const response = await fetch(url, { mode: 'cors' });
266
+ const blob = await response.blob();
267
+ const dataUrl = await new Promise((resolve) => {
268
+ const reader = new FileReader();
269
+ reader.onload = () => resolve(reader.result);
270
+ reader.readAsDataURL(blob);
271
+ });
272
+ emitAt(dataUrl, url.split('/').pop() || 'image');
273
+ } catch (_) {
274
+ emitAt(url, url.split('/').pop() || 'image');
275
+ }
276
+ return;
277
+ }
278
+ }
279
+ }
280
+
281
+ const uriList = dt.getData('text/uri-list') || '';
282
+ if (uriList) {
283
+ const lines = uriList.split('\n').filter((line) => !!line && !line.startsWith('#'));
284
+ const urls = lines.filter((line) => /^https?:\/\//i.test(line));
285
+ let index = 0;
286
+ for (const url of urls) {
287
+ const isImage = /(png|jpe?g|gif|webp|bmp|svg)(\?.*)?$/i.test(url);
288
+ if (!isImage) continue;
289
+ try {
290
+ const response = await fetch(url, { mode: 'cors' });
291
+ const blob = await response.blob();
292
+ const dataUrl = await new Promise((resolve) => {
293
+ const reader = new FileReader();
294
+ reader.onload = () => resolve(reader.result);
295
+ reader.readAsDataURL(blob);
296
+ });
297
+ emitAt(dataUrl, url.split('/').pop() || 'image', index++);
298
+ } catch (_) {
299
+ emitAt(url, url.split('/').pop() || 'image', index++);
300
+ }
301
+ }
302
+ if (index > 0) return;
303
+ }
304
+
305
+ const text = dt.getData('text/plain') || '';
306
+ if (text) {
307
+ const trimmed = text.trim();
308
+ const isDataUrl = /^data:image\//i.test(trimmed);
309
+ const isHttpUrl = /^https?:\/\//i.test(trimmed);
310
+ const looksLikeImage = /(png|jpe?g|gif|webp|bmp|svg)(\?.*)?$/i.test(trimmed);
311
+ if (isDataUrl) {
312
+ emitAt(trimmed, 'clipboard-image.png');
313
+ return;
314
+ }
315
+ if (isHttpUrl && looksLikeImage) {
316
+ try {
317
+ const response = await fetch(trimmed, { mode: 'cors' });
318
+ const blob = await response.blob();
319
+ const dataUrl = await new Promise((resolve) => {
320
+ const reader = new FileReader();
321
+ reader.onload = () => resolve(reader.result);
322
+ reader.readAsDataURL(blob);
323
+ });
324
+ emitAt(dataUrl, trimmed.split('/').pop() || 'image');
325
+ } catch (_) {
326
+ emitAt(trimmed, trimmed.split('/').pop() || 'image');
327
+ }
328
+ }
329
+ }
330
+ }
331
+
332
+ static handleKeyDown(manager, event) {
333
+ this.handleHotkeys(manager, event);
334
+
335
+ if (!manager.activeTool) return;
336
+
337
+ const toolEvent = {
338
+ key: event.key,
339
+ code: event.code,
340
+ ctrlKey: event.ctrlKey,
341
+ shiftKey: event.shiftKey,
342
+ altKey: event.altKey,
343
+ originalEvent: event
344
+ };
345
+
346
+ manager.activeTool.onKeyDown(toolEvent);
347
+
348
+ if (event.key === ' ' && !event.repeat) {
349
+ manager.spacePressed = true;
350
+ }
351
+ }
352
+
353
+ static handleKeyUp(manager, event) {
354
+ if (!manager.activeTool) return;
355
+
356
+ const toolEvent = {
357
+ key: event.key,
358
+ code: event.code,
359
+ originalEvent: event
360
+ };
361
+
362
+ manager.activeTool.onKeyUp(toolEvent);
363
+
364
+ if (event.key === ' ') {
365
+ manager.spacePressed = false;
366
+ if (ToolManagerGuards.isTemporaryPanActive(manager)) {
367
+ if (manager.activeTool?.name === 'pan' && manager.isMouseDown) {
368
+ manager.activeTool.onMouseUp({ x: 0, y: 0, button: 0, target: manager.container, originalEvent: event });
369
+ }
370
+ manager.returnToPreviousTool();
371
+ }
372
+ }
373
+ }
374
+
375
+ static handleHotkeys(manager, event) {
376
+ if (ToolManagerGuards.shouldIgnoreHotkeys(event)) {
377
+ return;
378
+ }
379
+
380
+ const tools = manager.registry ? manager.registry.values() : manager.tools.values();
381
+ for (const tool of tools) {
382
+ if (tool.hotkey === event.key.toLowerCase()) {
383
+ manager.activateTool(tool.name);
384
+ event.preventDefault();
385
+ break;
386
+ }
387
+ }
388
+
389
+ switch (event.key) {
390
+ case 'Escape':
391
+ manager.activateDefaultTool();
392
+ event.preventDefault();
393
+ break;
394
+ }
395
+ }
396
+ }
@@ -0,0 +1,33 @@
1
+ export class ToolManagerGuards {
2
+ static isCursorLockedToActiveTool(manager) {
3
+ return !!manager.activeTool && manager.activeTool.name !== 'select';
4
+ }
5
+
6
+ static getPixiCursorStyles(manager) {
7
+ const renderer = manager.pixiApp && manager.pixiApp.renderer;
8
+ if (!renderer) return null;
9
+
10
+ const events = renderer.events || (renderer.plugins && renderer.plugins.interaction);
11
+ return events && events.cursorStyles ? events.cursorStyles : null;
12
+ }
13
+
14
+ static getActiveToolCursor(manager, defaultCursor = '') {
15
+ const cursor = manager.activeTool && manager.activeTool.cursor;
16
+ if (typeof cursor === 'string' && cursor.length > 0) return cursor;
17
+ return defaultCursor;
18
+ }
19
+
20
+ static shouldIgnoreHotkeys(event) {
21
+ return event.target.tagName === 'INPUT' || event.target.tagName === 'TEXTAREA';
22
+ }
23
+
24
+ static isAuxPanStart(manager, event) {
25
+ const isMiddle = event.button === 1;
26
+ const isSpaceLeft = event.button === 0 && manager.spacePressed;
27
+ return isMiddle || isSpaceLeft;
28
+ }
29
+
30
+ static isTemporaryPanActive(manager) {
31
+ return manager.temporaryTool === 'pan';
32
+ }
33
+ }
@@ -0,0 +1,110 @@
1
+ const PASSIVE_FALSE = { passive: false };
2
+
3
+ export class ToolManagerLifecycle {
4
+ static initEventListeners(manager, defaultCursor) {
5
+ if (!manager.container) return;
6
+
7
+ manager.container.addEventListener('mousedown', (event) => manager.handleMouseDown(event));
8
+ manager.container.addEventListener('mousemove', (event) => manager.handleMouseMove(event));
9
+ manager.container.addEventListener('mouseup', (event) => manager.handleMouseUp(event));
10
+ manager.container.addEventListener('mouseenter', () => {
11
+ manager.isMouseOverContainer = true;
12
+ if (!manager.activeTool) {
13
+ manager.container.style.cursor = defaultCursor;
14
+ return;
15
+ }
16
+ manager.syncActiveToolCursor();
17
+ });
18
+ manager.container.addEventListener('mouseleave', () => {
19
+ manager.isMouseOverContainer = false;
20
+ });
21
+
22
+ manager.container.addEventListener('dragenter', (event) => {
23
+ event.preventDefault();
24
+ });
25
+ manager.container.addEventListener('dragover', (event) => {
26
+ event.preventDefault();
27
+ if (event.dataTransfer) event.dataTransfer.dropEffect = 'copy';
28
+ });
29
+ manager.container.addEventListener('dragleave', () => {
30
+ // можно снимать подсветку, если добавим в будущем
31
+ });
32
+ manager.container.addEventListener('drop', (event) => manager.handleDrop(event));
33
+
34
+ document.addEventListener('mousemove', (event) => manager.handleMouseMove(event));
35
+ document.addEventListener('mouseup', (event) => {
36
+ manager.handleMouseUp(event);
37
+ if (manager.temporaryTool === 'pan') {
38
+ manager.handleAuxPanEnd(event);
39
+ }
40
+ });
41
+ manager.container.addEventListener('dblclick', (event) => manager.handleDoubleClick(event));
42
+ manager.container.addEventListener('wheel', (event) => manager.handleMouseWheel(event), PASSIVE_FALSE);
43
+
44
+ manager._onWindowWheel = (event) => {
45
+ try {
46
+ if (event && event.ctrlKey && manager.isMouseOverContainer) {
47
+ event.preventDefault();
48
+ }
49
+ } catch (_) {}
50
+ };
51
+ window.addEventListener('wheel', manager._onWindowWheel, PASSIVE_FALSE);
52
+
53
+ document.addEventListener('keydown', (event) => manager.handleKeyDown(event));
54
+ document.addEventListener('keyup', (event) => manager.handleKeyUp(event));
55
+
56
+ manager.container.addEventListener('contextmenu', (event) => {
57
+ event.preventDefault();
58
+ if (!manager.activeTool) return;
59
+ const rect = manager.container.getBoundingClientRect();
60
+ const toolEvent = {
61
+ x: event.clientX - rect.left,
62
+ y: event.clientY - rect.top,
63
+ originalEvent: event
64
+ };
65
+ if (typeof manager.activeTool.onContextMenu === 'function') {
66
+ manager.activeTool.onContextMenu(toolEvent);
67
+ }
68
+ });
69
+ }
70
+
71
+ static destroy(manager) {
72
+ for (const tool of manager.registry.values()) {
73
+ tool.destroy();
74
+ }
75
+
76
+ manager.registry.clear();
77
+ manager.activeTool = null;
78
+
79
+ if (manager.container) {
80
+ manager.container.removeEventListener('mousedown', manager.handleMouseDown);
81
+ manager.container.removeEventListener('mousemove', manager.handleMouseMove);
82
+ manager.container.removeEventListener('mouseup', manager.handleMouseUp);
83
+ manager.container.removeEventListener('dblclick', manager.handleDoubleClick);
84
+ manager.container.removeEventListener('wheel', manager.handleMouseWheel);
85
+ manager.container.removeEventListener('contextmenu', (event) => event.preventDefault());
86
+ manager.container.removeEventListener('dragenter', (event) => event.preventDefault());
87
+ manager.container.removeEventListener('dragover', (event) => event.preventDefault());
88
+ manager.container.removeEventListener('dragleave', () => {});
89
+ manager.container.removeEventListener('drop', manager.handleDrop);
90
+ }
91
+ document.removeEventListener('mousemove', manager.handleMouseMove);
92
+ document.removeEventListener('mouseup', manager.handleMouseUp);
93
+
94
+ document.removeEventListener('keydown', manager.handleKeyDown);
95
+ document.removeEventListener('keyup', manager.handleKeyUp);
96
+ if (manager._onWindowWheel) {
97
+ try {
98
+ window.removeEventListener('wheel', manager._onWindowWheel);
99
+ } catch (_) {}
100
+ manager._onWindowWheel = null;
101
+ }
102
+
103
+ const cursorStyles = manager.getPixiCursorStyles();
104
+ if (cursorStyles && manager._originalPixiCursorStyles) {
105
+ cursorStyles.pointer = manager._originalPixiCursorStyles.pointer;
106
+ cursorStyles.default = manager._originalPixiCursorStyles.default;
107
+ }
108
+ manager._originalPixiCursorStyles = null;
109
+ }
110
+ }
@@ -0,0 +1,33 @@
1
+ export class ToolRegistry {
2
+ constructor(manager) {
3
+ this.manager = manager;
4
+ }
5
+
6
+ register(tool) {
7
+ this.manager.tools.set(tool.name, tool);
8
+
9
+ if (!this.manager.defaultTool) {
10
+ this.manager.defaultTool = tool.name;
11
+ }
12
+ }
13
+
14
+ get(toolName) {
15
+ return this.manager.tools.get(toolName);
16
+ }
17
+
18
+ has(toolName) {
19
+ return this.manager.tools.has(toolName);
20
+ }
21
+
22
+ getAll() {
23
+ return Array.from(this.manager.tools.values());
24
+ }
25
+
26
+ values() {
27
+ return this.manager.tools.values();
28
+ }
29
+
30
+ clear() {
31
+ this.manager.tools.clear();
32
+ }
33
+ }