@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.
- package/package.json +11 -1
- package/src/assets/icons/rotate-icon.svg +1 -1
- package/src/core/HistoryManager.js +16 -16
- package/src/core/KeyboardManager.js +48 -539
- package/src/core/PixiEngine.js +9 -9
- package/src/core/SaveManager.js +56 -31
- package/src/core/bootstrap/CoreInitializer.js +65 -0
- package/src/core/commands/DeleteObjectCommand.js +8 -0
- package/src/core/commands/GroupDeleteCommand.js +75 -0
- package/src/core/commands/GroupRotateCommand.js +6 -0
- package/src/core/commands/UpdateContentCommand.js +52 -0
- package/src/core/commands/UpdateFramePropertiesCommand.js +98 -0
- package/src/core/commands/UpdateFrameTypeCommand.js +85 -0
- package/src/core/commands/UpdateNoteStyleCommand.js +88 -0
- package/src/core/commands/UpdateTextStyleCommand.js +90 -0
- package/src/core/commands/index.js +6 -0
- package/src/core/events/Events.js +6 -0
- package/src/core/flows/ClipboardFlow.js +553 -0
- package/src/core/flows/LayerAndViewportFlow.js +283 -0
- package/src/core/flows/ObjectLifecycleFlow.js +336 -0
- package/src/core/flows/SaveFlow.js +34 -0
- package/src/core/flows/TransformFlow.js +277 -0
- package/src/core/flows/TransformFlowResizeHelpers.js +83 -0
- package/src/core/index.js +41 -1773
- package/src/core/keyboard/KeyboardClipboardImagePaste.js +190 -0
- package/src/core/keyboard/KeyboardContextGuards.js +35 -0
- package/src/core/keyboard/KeyboardEventRouter.js +92 -0
- package/src/core/keyboard/KeyboardSelectionActions.js +103 -0
- package/src/core/keyboard/KeyboardShortcutMap.js +31 -0
- package/src/core/keyboard/KeyboardToolSwitching.js +26 -0
- package/src/core/rendering/ObjectRenderer.js +3 -7
- package/src/grid/BaseGrid.js +26 -0
- package/src/grid/CrossGrid.js +7 -6
- package/src/grid/DotGrid.js +89 -33
- package/src/grid/DotGridZoomPhases.js +42 -0
- package/src/grid/LineGrid.js +22 -21
- package/src/moodboard/MoodBoard.js +31 -532
- package/src/moodboard/bootstrap/MoodBoardInitializer.js +47 -0
- package/src/moodboard/bootstrap/MoodBoardManagersFactory.js +38 -0
- package/src/moodboard/bootstrap/MoodBoardUiFactory.js +109 -0
- package/src/moodboard/integration/MoodBoardEventBindings.js +65 -0
- package/src/moodboard/integration/MoodBoardLoadApi.js +82 -0
- package/src/moodboard/integration/MoodBoardScreenshotApi.js +33 -0
- package/src/moodboard/integration/MoodBoardScreenshotCanvas.js +98 -0
- package/src/moodboard/lifecycle/MoodBoardDestroyer.js +97 -0
- package/src/objects/FileObject.js +17 -6
- package/src/objects/FrameObject.js +50 -10
- package/src/objects/NoteObject.js +5 -4
- package/src/services/BoardService.js +42 -2
- package/src/services/FrameService.js +83 -42
- package/src/services/ResizePolicyService.js +152 -0
- package/src/services/SettingsApplier.js +7 -2
- package/src/services/ZoomPanController.js +35 -9
- package/src/tools/ToolManager.js +30 -537
- package/src/tools/board-tools/PanTool.js +5 -11
- package/src/tools/manager/ToolActivationController.js +49 -0
- package/src/tools/manager/ToolEventRouter.js +396 -0
- package/src/tools/manager/ToolManagerGuards.js +33 -0
- package/src/tools/manager/ToolManagerLifecycle.js +110 -0
- package/src/tools/manager/ToolRegistry.js +33 -0
- package/src/tools/object-tools/DrawingTool.js +48 -14
- package/src/tools/object-tools/PlacementTool.js +50 -1049
- package/src/tools/object-tools/PlacementToolV2.js +88 -0
- package/src/tools/object-tools/SelectTool.js +174 -2681
- package/src/tools/object-tools/placement/GhostController.js +504 -0
- package/src/tools/object-tools/placement/PlacementCoordinateResolver.js +20 -0
- package/src/tools/object-tools/placement/PlacementEventsBridge.js +91 -0
- package/src/tools/object-tools/placement/PlacementInputRouter.js +267 -0
- package/src/tools/object-tools/placement/PlacementPayloadFactory.js +111 -0
- package/src/tools/object-tools/placement/PlacementSessionStore.js +18 -0
- package/src/tools/object-tools/selection/BoxSelectController.js +0 -5
- package/src/tools/object-tools/selection/CloneFlowController.js +71 -0
- package/src/tools/object-tools/selection/CoordinateMapper.js +10 -0
- package/src/tools/object-tools/selection/CursorController.js +78 -0
- package/src/tools/object-tools/selection/FileNameInlineEditorController.js +184 -0
- package/src/tools/object-tools/selection/HitTestService.js +102 -0
- package/src/tools/object-tools/selection/InlineEditorController.js +24 -0
- package/src/tools/object-tools/selection/InlineEditorDomFactory.js +50 -0
- package/src/tools/object-tools/selection/InlineEditorListenersRegistry.js +14 -0
- package/src/tools/object-tools/selection/InlineEditorPositioningService.js +25 -0
- package/src/tools/object-tools/selection/NoteInlineEditorController.js +113 -0
- package/src/tools/object-tools/selection/SelectInputRouter.js +267 -0
- package/src/tools/object-tools/selection/SelectToolLifecycleController.js +128 -0
- package/src/tools/object-tools/selection/SelectToolSetup.js +134 -0
- package/src/tools/object-tools/selection/SelectionOverlayService.js +81 -0
- package/src/tools/object-tools/selection/SelectionStateController.js +91 -0
- package/src/tools/object-tools/selection/TextEditorDomFactory.js +65 -0
- package/src/tools/object-tools/selection/TextEditorInteractionController.js +266 -0
- package/src/tools/object-tools/selection/TextEditorLifecycleRegistry.js +90 -0
- package/src/tools/object-tools/selection/TextEditorPositioningService.js +158 -0
- package/src/tools/object-tools/selection/TextEditorSyncService.js +110 -0
- package/src/tools/object-tools/selection/TextInlineEditorController.js +457 -0
- package/src/tools/object-tools/selection/TransformInteractionController.js +466 -0
- package/src/ui/FilePropertiesPanel.js +61 -32
- package/src/ui/FramePropertiesPanel.js +176 -101
- package/src/ui/HtmlHandlesLayer.js +121 -999
- package/src/ui/MapPanel.js +12 -7
- package/src/ui/NotePropertiesPanel.js +17 -2
- package/src/ui/TextPropertiesPanel.js +124 -738
- package/src/ui/Toolbar.js +71 -1180
- package/src/ui/Topbar.js +23 -25
- package/src/ui/ZoomPanel.js +16 -5
- package/src/ui/handles/GroupSelectionHandlesController.js +29 -0
- package/src/ui/handles/HandlesDomRenderer.js +278 -0
- package/src/ui/handles/HandlesEventBridge.js +102 -0
- package/src/ui/handles/HandlesInteractionController.js +772 -0
- package/src/ui/handles/HandlesPositioningService.js +206 -0
- package/src/ui/handles/SingleSelectionHandlesController.js +22 -0
- package/src/ui/styles/toolbar.css +2 -0
- package/src/ui/styles/workspace.css +13 -6
- package/src/ui/text-properties/TextPropertiesPanelBindings.js +92 -0
- package/src/ui/text-properties/TextPropertiesPanelEventBridge.js +77 -0
- package/src/ui/text-properties/TextPropertiesPanelMapper.js +173 -0
- package/src/ui/text-properties/TextPropertiesPanelRenderer.js +434 -0
- package/src/ui/text-properties/TextPropertiesPanelState.js +39 -0
- package/src/ui/toolbar/ToolbarActionRouter.js +193 -0
- package/src/ui/toolbar/ToolbarDialogsController.js +186 -0
- package/src/ui/toolbar/ToolbarPopupsController.js +662 -0
- package/src/ui/toolbar/ToolbarRenderer.js +97 -0
- package/src/ui/toolbar/ToolbarStateController.js +79 -0
- package/src/ui/toolbar/ToolbarTooltipController.js +52 -0
- package/src/utils/emojiLoaderNoBundler.js +1 -1
package/src/tools/ToolManager.js
CHANGED
|
@@ -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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
100
|
+
return this.registry.getAll();
|
|
130
101
|
}
|
|
131
102
|
|
|
132
103
|
/**
|
|
133
104
|
* Проверяет, зарегистрирован ли инструмент
|
|
134
105
|
*/
|
|
135
106
|
hasActiveTool(toolName) {
|
|
136
|
-
return this.
|
|
107
|
+
return this.registry.has(toolName);
|
|
137
108
|
}
|
|
138
109
|
|
|
139
110
|
/**
|
|
140
111
|
* Инициализирует обработчики событий DOM
|
|
141
112
|
*/
|
|
142
113
|
initEventListeners() {
|
|
143
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
139
|
+
return ToolManagerGuards.isCursorLockedToActiveTool(this);
|
|
321
140
|
}
|
|
322
141
|
|
|
323
142
|
getPixiCursorStyles() {
|
|
324
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|