@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
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
import { Events } from '../../../core/events/Events.js';
|
|
2
|
+
|
|
3
|
+
export function onMouseDown(event) {
|
|
4
|
+
// Если активен текстовый редактор, закрываем его при клике вне
|
|
5
|
+
if (this.textEditor.active) {
|
|
6
|
+
if (this.textEditor.objectType === 'file') {
|
|
7
|
+
this._closeFileNameEditor(true);
|
|
8
|
+
} else {
|
|
9
|
+
this._closeTextEditor(true);
|
|
10
|
+
}
|
|
11
|
+
return; // Прерываем выполнение, чтобы не обрабатывать клик дальше
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
this.isMultiSelect = event.originalEvent.ctrlKey || event.originalEvent.metaKey || event.originalEvent.shiftKey;
|
|
15
|
+
|
|
16
|
+
// Проверяем, что под курсором
|
|
17
|
+
const hitResult = this.hitTest(event.x, event.y);
|
|
18
|
+
|
|
19
|
+
if (hitResult.type === 'resize-handle') {
|
|
20
|
+
this.startResize(hitResult.handle, hitResult.object);
|
|
21
|
+
} else if (hitResult.type === 'rotate-handle') {
|
|
22
|
+
this.startRotate(hitResult.object);
|
|
23
|
+
} else if (this.selection.size() > 1) {
|
|
24
|
+
// Особая логика для группового выделения: клики внутри общей рамки не снимают выделение
|
|
25
|
+
const gb = this.computeGroupBounds();
|
|
26
|
+
const insideGroup = this.isPointInBounds({ x: event.x, y: event.y }, { x: gb.x, y: gb.y, width: gb.width, height: gb.height });
|
|
27
|
+
if (insideGroup) {
|
|
28
|
+
// Если клик внутри группы (по объекту или пустому месту), сохраняем выделение и начинаем перетаскивание группы
|
|
29
|
+
this.startGroupDrag(event);
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
// Вне группы — обычная логика
|
|
33
|
+
if (hitResult.type === 'object') {
|
|
34
|
+
this.handleObjectSelect(hitResult.object, event);
|
|
35
|
+
} else {
|
|
36
|
+
this.startBoxSelect(event);
|
|
37
|
+
}
|
|
38
|
+
} else if (hitResult.type === 'object') {
|
|
39
|
+
// Особая логика для фреймов: если у фрейма есть дети и клик внутри внутренней области (без 20px рамки),
|
|
40
|
+
// то не начинаем drag фрейма, а запускаем box-select для выбора объектов внутри
|
|
41
|
+
const req = { objectId: hitResult.object, pixiObject: null };
|
|
42
|
+
this.emit(Events.Tool.GetObjectPixi, req);
|
|
43
|
+
const mbType = req.pixiObject && req.pixiObject._mb && req.pixiObject._mb.type;
|
|
44
|
+
if (mbType === 'frame') {
|
|
45
|
+
// Получаем данные фрейма и его экранные границы
|
|
46
|
+
const objects = this.core?.state?.getObjects ? this.core.state.getObjects() : [];
|
|
47
|
+
const frameObj = objects.find(o => o.id === hitResult.object);
|
|
48
|
+
const hasChildren = !!(objects && objects.some(o => o.properties && o.properties.frameId === hitResult.object));
|
|
49
|
+
if (req.pixiObject && hasChildren && frameObj) {
|
|
50
|
+
const bounds = req.pixiObject.getBounds(); // экранные координаты
|
|
51
|
+
const inner = { x: bounds.x + 20, y: bounds.y + 20, width: Math.max(0, bounds.width - 40), height: Math.max(0, bounds.height - 40) };
|
|
52
|
+
const insideInner = this.isPointInBounds({ x: event.x, y: event.y }, inner);
|
|
53
|
+
// Если клик внутри внутренней области — запускаем box-select и выходим
|
|
54
|
+
if (insideInner) {
|
|
55
|
+
// Запускаем рамку выделения вместо drag фрейма
|
|
56
|
+
this.startBoxSelect(event);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
// Если клик на 20px рамке — позволяем перетягивать фрейм. Но запрещаем box-select от рамки.
|
|
60
|
+
// Здесь ничего не делаем: ниже пойдёт обычная логика handleObjectSelect
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
// Обычная логика: начинаем drag выбранного объекта
|
|
64
|
+
this.handleObjectSelect(hitResult.object, event);
|
|
65
|
+
} else {
|
|
66
|
+
// Клик по пустому месту — если есть одиночное выделение, разрешаем drag за пределами объекта в пределах рамки
|
|
67
|
+
if (this.selection.size() === 1) {
|
|
68
|
+
const selId = this.selection.toArray()[0];
|
|
69
|
+
// Если выбран фрейм с детьми и клик внутри внутренней области — не начинаем drag, а box-select
|
|
70
|
+
const req = { objectId: selId, pixiObject: null };
|
|
71
|
+
this.emit(Events.Tool.GetObjectPixi, req);
|
|
72
|
+
const isFrame = !!(req.pixiObject && req.pixiObject._mb && req.pixiObject._mb.type === 'frame');
|
|
73
|
+
if (isFrame) {
|
|
74
|
+
const objects = this.core?.state?.getObjects ? this.core.state.getObjects() : [];
|
|
75
|
+
const frameObj = objects.find(o => o.id === selId);
|
|
76
|
+
const hasChildren = !!(objects && objects.some(o => o.properties && o.properties.frameId === selId));
|
|
77
|
+
if (frameObj && hasChildren) {
|
|
78
|
+
const b = { x: frameObj.position.x, y: frameObj.position.y, width: frameObj.width || 0, height: frameObj.height || 0 };
|
|
79
|
+
const inner = { x: b.x + 20, y: b.y + 20, width: Math.max(0, b.width - 40), height: Math.max(0, b.height - 40) };
|
|
80
|
+
const insideInner = this.isPointInBounds({ x: event.x, y: event.y }, inner);
|
|
81
|
+
if (insideInner) {
|
|
82
|
+
this.startBoxSelect(event);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
// Обычная логика: если клик внутри рамки выбранного — начинаем drag
|
|
88
|
+
const boundsReq = { objects: [] };
|
|
89
|
+
this.emit(Events.Tool.GetAllObjects, boundsReq);
|
|
90
|
+
const map = new Map(boundsReq.objects.map(o => [o.id, o.bounds]));
|
|
91
|
+
const b = map.get(selId);
|
|
92
|
+
if (b && this.isPointInBounds({ x: event.x, y: event.y }, b)) {
|
|
93
|
+
// Для фрейма c детьми: отфильтруем клики внутри внутренней области (box-select)
|
|
94
|
+
const req2 = { objectId: selId, pixiObject: null };
|
|
95
|
+
this.emit(Events.Tool.GetObjectPixi, req2);
|
|
96
|
+
const isFrame2 = !!(req2.pixiObject && req2.pixiObject._mb && req2.pixiObject._mb.type === 'frame');
|
|
97
|
+
if (isFrame2) {
|
|
98
|
+
const os = this.core?.state?.getObjects ? this.core.state.getObjects() : [];
|
|
99
|
+
const fr = os.find(o => o.id === selId);
|
|
100
|
+
const hasChildren2 = !!(os && os.some(o => o.properties && o.properties.frameId === selId));
|
|
101
|
+
if (req2.pixiObject && fr && hasChildren2) {
|
|
102
|
+
const bounds2 = req2.pixiObject.getBounds();
|
|
103
|
+
const inner2 = { x: bounds2.x + 20, y: bounds2.y + 20, width: Math.max(0, bounds2.width - 40), height: Math.max(0, bounds2.height - 40) };
|
|
104
|
+
if (this.isPointInBounds({ x: event.x, y: event.y }, inner2)) {
|
|
105
|
+
this.startBoxSelect(event);
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
// Старт перетаскивания как если бы кликнули по объекту
|
|
111
|
+
this.startDrag(selId, event);
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
// Иначе — начинаем рамку выделения
|
|
116
|
+
this.startBoxSelect(event);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function onMouseMove(event) {
|
|
121
|
+
// Проверяем, что инструмент не уничтожен
|
|
122
|
+
if (this.destroyed) {
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Обновляем текущие координаты мыши
|
|
127
|
+
this.currentX = event.x;
|
|
128
|
+
this.currentY = event.y;
|
|
129
|
+
|
|
130
|
+
if (this.isResizing || this.isGroupResizing) {
|
|
131
|
+
this.updateResize(event);
|
|
132
|
+
} else if (this.isRotating || this.isGroupRotating) {
|
|
133
|
+
this.updateRotate(event);
|
|
134
|
+
} else if (this.isDragging || this.isGroupDragging) {
|
|
135
|
+
this.updateDrag(event);
|
|
136
|
+
} else if (this.isBoxSelect) {
|
|
137
|
+
this.updateBoxSelect(event);
|
|
138
|
+
} else {
|
|
139
|
+
// Обновляем курсор в зависимости от того, что под мышью
|
|
140
|
+
this.updateCursor(event);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function onMouseUp(event) {
|
|
145
|
+
if (this.isResizing || this.isGroupResizing) {
|
|
146
|
+
this.endResize();
|
|
147
|
+
} else if (this.isRotating || this.isGroupRotating) {
|
|
148
|
+
this.endRotate();
|
|
149
|
+
} else if (this.isDragging || this.isGroupDragging) {
|
|
150
|
+
this.endDrag();
|
|
151
|
+
} else if (this.isBoxSelect) {
|
|
152
|
+
this.endBoxSelect();
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export function onDoubleClick(event) {
|
|
157
|
+
const hitResult = this.hitTest(event.x, event.y);
|
|
158
|
+
|
|
159
|
+
if (hitResult.type === 'object') {
|
|
160
|
+
// если это текст или записка — войдём в режим редактирования через ObjectEdit
|
|
161
|
+
const req = { objectId: hitResult.object, pixiObject: null };
|
|
162
|
+
this.emit(Events.Tool.GetObjectPixi, req);
|
|
163
|
+
const pix = req.pixiObject;
|
|
164
|
+
|
|
165
|
+
const isText = !!(pix && pix._mb && pix._mb.type === 'text');
|
|
166
|
+
const isNote = !!(pix && pix._mb && pix._mb.type === 'note');
|
|
167
|
+
const isFile = !!(pix && pix._mb && pix._mb.type === 'file');
|
|
168
|
+
|
|
169
|
+
if (isText) {
|
|
170
|
+
// Получаем позицию объекта для редактирования
|
|
171
|
+
const posData = { objectId: hitResult.object, position: null };
|
|
172
|
+
this.emit(Events.Tool.GetObjectPosition, posData);
|
|
173
|
+
|
|
174
|
+
// Получаем содержимое из properties объекта
|
|
175
|
+
const textContent = pix._mb?.properties?.content || '';
|
|
176
|
+
|
|
177
|
+
this.emit(Events.Tool.ObjectEdit, {
|
|
178
|
+
id: hitResult.object,
|
|
179
|
+
type: 'text',
|
|
180
|
+
position: posData.position,
|
|
181
|
+
properties: { content: textContent },
|
|
182
|
+
caretClick: {
|
|
183
|
+
clientX: event?.originalEvent?.clientX,
|
|
184
|
+
clientY: event?.originalEvent?.clientY
|
|
185
|
+
},
|
|
186
|
+
create: false
|
|
187
|
+
});
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
if (isNote) {
|
|
191
|
+
const noteProps = pix._mb.properties || {};
|
|
192
|
+
// Получаем позицию объекта для редактирования
|
|
193
|
+
const posData = { objectId: hitResult.object, position: null };
|
|
194
|
+
this.emit(Events.Tool.GetObjectPosition, posData);
|
|
195
|
+
|
|
196
|
+
this.emit(Events.Tool.ObjectEdit, {
|
|
197
|
+
id: hitResult.object,
|
|
198
|
+
type: 'note',
|
|
199
|
+
position: posData.position,
|
|
200
|
+
properties: { content: noteProps.content || '' },
|
|
201
|
+
create: false
|
|
202
|
+
});
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (isFile) {
|
|
207
|
+
const fileProps = pix._mb.properties || {};
|
|
208
|
+
// Получаем позицию объекта для редактирования
|
|
209
|
+
const posData = { objectId: hitResult.object, position: null };
|
|
210
|
+
this.emit(Events.Tool.GetObjectPosition, posData);
|
|
211
|
+
|
|
212
|
+
this.emit(Events.Tool.ObjectEdit, {
|
|
213
|
+
id: hitResult.object,
|
|
214
|
+
type: 'file',
|
|
215
|
+
position: posData.position,
|
|
216
|
+
properties: { fileName: fileProps.fileName || 'Untitled' },
|
|
217
|
+
create: false
|
|
218
|
+
});
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
this.editObject(hitResult.object);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export function onContextMenu(event) {
|
|
226
|
+
// Определяем, что под курсором
|
|
227
|
+
const hit = this.hitTest(event.x, event.y);
|
|
228
|
+
let context = 'canvas';
|
|
229
|
+
let targetId = null;
|
|
230
|
+
if (hit && hit.type === 'object' && hit.object) {
|
|
231
|
+
targetId = hit.object;
|
|
232
|
+
if (this.selection.has(targetId) && this.selection.size() > 1) {
|
|
233
|
+
context = 'group';
|
|
234
|
+
} else {
|
|
235
|
+
context = 'object';
|
|
236
|
+
}
|
|
237
|
+
} else if (this.selection.size() > 1) {
|
|
238
|
+
context = 'group';
|
|
239
|
+
}
|
|
240
|
+
// Сообщаем ядру/UI, что нужно показать контекстное меню (пока без пунктов)
|
|
241
|
+
this.emit(Events.Tool.ContextMenuShow, { x: event.x, y: event.y, context, targetId });
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
export function onKeyDown(event) {
|
|
245
|
+
// Проверяем, не активен ли текстовый редактор (редактирование названия файла или текста)
|
|
246
|
+
if (this.textEditor && this.textEditor.active) {
|
|
247
|
+
return; // Не обрабатываем клавиши во время редактирования
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
switch (event.key) {
|
|
251
|
+
case 'Delete':
|
|
252
|
+
case 'Backspace':
|
|
253
|
+
this.deleteSelectedObjects();
|
|
254
|
+
break;
|
|
255
|
+
|
|
256
|
+
case 'a':
|
|
257
|
+
if (event.ctrlKey) {
|
|
258
|
+
this.selectAll();
|
|
259
|
+
event.originalEvent.preventDefault();
|
|
260
|
+
}
|
|
261
|
+
break;
|
|
262
|
+
|
|
263
|
+
case 'Escape':
|
|
264
|
+
this.clearSelection();
|
|
265
|
+
break;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { Events } from '../../../core/events/Events.js';
|
|
2
|
+
import { unregisterSelectToolCoreSubscriptions } from './SelectToolSetup.js';
|
|
3
|
+
import { ResizeHandles } from '../../ResizeHandles.js';
|
|
4
|
+
import { SimpleDragController } from './SimpleDragController.js';
|
|
5
|
+
import { ResizeController } from './ResizeController.js';
|
|
6
|
+
import { RotateController } from './RotateController.js';
|
|
7
|
+
import { GroupResizeController } from './GroupResizeController.js';
|
|
8
|
+
import { GroupRotateController } from './GroupRotateController.js';
|
|
9
|
+
import { GroupDragController } from './GroupDragController.js';
|
|
10
|
+
import { BoxSelectController } from './BoxSelectController.js';
|
|
11
|
+
|
|
12
|
+
export function activateSelectTool(app, defaultCursor, superActivate) {
|
|
13
|
+
superActivate();
|
|
14
|
+
// Сохраняем ссылку на PIXI app для оверлеев (рамка выделения)
|
|
15
|
+
this.app = app;
|
|
16
|
+
|
|
17
|
+
// Устанавливаем стандартный курсор для select инструмента
|
|
18
|
+
if (this.app && this.app.view) {
|
|
19
|
+
this.app.view.style.cursor = defaultCursor; // пусто → наследует глобальный CSS
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Инициализируем систему ручек изменения размера
|
|
23
|
+
if (!this.resizeHandles && app) {
|
|
24
|
+
this.resizeHandles = new ResizeHandles(app);
|
|
25
|
+
// Полностью отключаем синхронизацию старых PIXI-ручек
|
|
26
|
+
if (this.resizeHandles && typeof this.resizeHandles.hideHandles === 'function') {
|
|
27
|
+
this.resizeHandles.hideHandles();
|
|
28
|
+
}
|
|
29
|
+
this._dragCtrl = new SimpleDragController({
|
|
30
|
+
emit: (event, payload) => this.emit(event, payload)
|
|
31
|
+
});
|
|
32
|
+
this._resizeCtrl = new ResizeController({
|
|
33
|
+
emit: (event, payload) => this.emit(event, payload),
|
|
34
|
+
getRotation: (objectId) => {
|
|
35
|
+
const d = { objectId, rotation: 0 };
|
|
36
|
+
this.emit(Events.Tool.GetObjectRotation, d);
|
|
37
|
+
return d.rotation || 0;
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
this._rotateCtrl = new RotateController({
|
|
41
|
+
emit: (event, payload) => this.emit(event, payload)
|
|
42
|
+
});
|
|
43
|
+
this._groupResizeCtrl = new GroupResizeController({
|
|
44
|
+
emit: (event, payload) => this.emit(event, payload),
|
|
45
|
+
selection: this.selection,
|
|
46
|
+
getGroupBounds: () => this.computeGroupBounds(),
|
|
47
|
+
ensureGroupGraphics: (b) => this.ensureGroupBoundsGraphics(b),
|
|
48
|
+
updateGroupGraphics: (b) => this.updateGroupBoundsGraphics(b)
|
|
49
|
+
});
|
|
50
|
+
this._groupRotateCtrl = new GroupRotateController({
|
|
51
|
+
emit: (event, payload) => this.emit(event, payload),
|
|
52
|
+
selection: this.selection,
|
|
53
|
+
getGroupBounds: () => this.computeGroupBounds(),
|
|
54
|
+
ensureGroupGraphics: (b) => this.ensureGroupBoundsGraphics(b),
|
|
55
|
+
updateHandles: () => { if (this.resizeHandles) this.resizeHandles.updateHandles(); }
|
|
56
|
+
});
|
|
57
|
+
this._groupDragCtrl = new GroupDragController({
|
|
58
|
+
emit: (event, payload) => this.emit(event, payload),
|
|
59
|
+
selection: this.selection,
|
|
60
|
+
updateGroupBoundsByTopLeft: (pos) => this.updateGroupBoundsGraphicsByTopLeft(pos)
|
|
61
|
+
});
|
|
62
|
+
this._boxSelect = new BoxSelectController({
|
|
63
|
+
app,
|
|
64
|
+
selection: this.selection,
|
|
65
|
+
emit: (event, payload) => this.emit(event, payload),
|
|
66
|
+
setSelection: (ids) => this.setSelection(ids),
|
|
67
|
+
clearSelection: () => this.clearSelection(),
|
|
68
|
+
rectIntersectsRect: (a, b) => this.rectIntersectsRect(a, b)
|
|
69
|
+
});
|
|
70
|
+
} else if (!app) {
|
|
71
|
+
console.log('❌ PIXI app не передан в activate');
|
|
72
|
+
} else {
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function deactivateSelectTool(superDeactivate) {
|
|
77
|
+
superDeactivate();
|
|
78
|
+
|
|
79
|
+
// Закрываем текстовый/файловый редактор если открыт
|
|
80
|
+
if (this.textEditor.active) {
|
|
81
|
+
if (this.textEditor.objectType === 'file') {
|
|
82
|
+
this._closeFileNameEditor(true);
|
|
83
|
+
} else {
|
|
84
|
+
this._closeTextEditor(true);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Очищаем выделение и ручки
|
|
89
|
+
this.clearSelection();
|
|
90
|
+
// Скрываем любые старые PIXI-ручки: используем только HTML-ручки
|
|
91
|
+
|
|
92
|
+
// Сбрасываем курсор на стандартный
|
|
93
|
+
if (this.app && this.app.view) {
|
|
94
|
+
this.app.view.style.cursor = '';
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function destroySelectTool(superDestroy) {
|
|
99
|
+
if (this.destroyed) {
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
this.destroyed = true;
|
|
104
|
+
|
|
105
|
+
unregisterSelectToolCoreSubscriptions(this);
|
|
106
|
+
this.clearSelection();
|
|
107
|
+
|
|
108
|
+
// Уничтожаем ручки изменения размера
|
|
109
|
+
if (this.resizeHandles) {
|
|
110
|
+
this.resizeHandles.destroy();
|
|
111
|
+
this.resizeHandles = null;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Очищаем контроллеры
|
|
115
|
+
this.dragController = null;
|
|
116
|
+
this.resizeController = null;
|
|
117
|
+
this.rotateController = null;
|
|
118
|
+
this.groupDragController = null;
|
|
119
|
+
this.groupResizeController = null;
|
|
120
|
+
this.groupRotateController = null;
|
|
121
|
+
this.boxSelectController = null;
|
|
122
|
+
|
|
123
|
+
// Очищаем модель выделения
|
|
124
|
+
this.selection = null;
|
|
125
|
+
|
|
126
|
+
// Вызываем destroy родительского класса
|
|
127
|
+
superDestroy();
|
|
128
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { Events } from '../../../core/events/Events.js';
|
|
2
|
+
|
|
3
|
+
export function initializeSelectToolState(instance) {
|
|
4
|
+
// Флаг состояния объекта
|
|
5
|
+
instance.destroyed = false;
|
|
6
|
+
|
|
7
|
+
// Состояние выделения перенесено в модель
|
|
8
|
+
instance.isMultiSelect = false;
|
|
9
|
+
|
|
10
|
+
// Режим Alt-клонирования при перетаскивании
|
|
11
|
+
// Если Alt зажат при начале drag, создаем копию и перетаскиваем именно её
|
|
12
|
+
instance.isAltCloneMode = false; // активен ли режим Alt-клона
|
|
13
|
+
instance.clonePending = false; // ожидаем подтверждение создания копии
|
|
14
|
+
instance.cloneRequested = false; // запрос на создание копии уже отправлен
|
|
15
|
+
instance.cloneSourceId = null; // исходный объект для копии
|
|
16
|
+
// Групповой Alt-клон
|
|
17
|
+
instance.isAltGroupCloneMode = false;
|
|
18
|
+
instance.groupClonePending = false;
|
|
19
|
+
instance.groupCloneOriginalIds = [];
|
|
20
|
+
instance.groupCloneMap = null; // { originalId: newId }
|
|
21
|
+
|
|
22
|
+
// Состояние перетаскивания
|
|
23
|
+
instance.isDragging = false;
|
|
24
|
+
instance.dragOffset = { x: 0, y: 0 };
|
|
25
|
+
instance.dragTarget = null;
|
|
26
|
+
|
|
27
|
+
// Состояние изменения размера
|
|
28
|
+
instance.isResizing = false;
|
|
29
|
+
instance.resizeHandle = null;
|
|
30
|
+
instance.resizeStartBounds = null;
|
|
31
|
+
instance.resizeStartMousePos = null;
|
|
32
|
+
instance.resizeStartPosition = null;
|
|
33
|
+
|
|
34
|
+
// Система ручек изменения размера
|
|
35
|
+
instance.resizeHandles = null;
|
|
36
|
+
instance.groupSelectionGraphics = null; // визуализация рамок при множественном выделении
|
|
37
|
+
instance.groupBoundsGraphics = null; // невидимая геометрия для ручек группы
|
|
38
|
+
instance.groupId = '__group__';
|
|
39
|
+
instance.isGroupDragging = false;
|
|
40
|
+
instance.isGroupResizing = false;
|
|
41
|
+
instance.isGroupRotating = false;
|
|
42
|
+
instance.groupStartBounds = null;
|
|
43
|
+
instance.groupStartMouse = null;
|
|
44
|
+
instance.groupDragOffset = null;
|
|
45
|
+
instance.groupObjectsInitial = null; // Map id -> { position, size, rotation }
|
|
46
|
+
|
|
47
|
+
// Текущие координаты мыши
|
|
48
|
+
instance.currentX = 0;
|
|
49
|
+
instance.currentY = 0;
|
|
50
|
+
|
|
51
|
+
// Состояние поворота
|
|
52
|
+
instance.isRotating = false;
|
|
53
|
+
instance.rotateCenter = null;
|
|
54
|
+
instance.rotateStartAngle = 0;
|
|
55
|
+
instance.rotateCurrentAngle = 0;
|
|
56
|
+
instance.rotateStartMouseAngle = 0;
|
|
57
|
+
|
|
58
|
+
// Состояние рамки выделения
|
|
59
|
+
instance.isBoxSelect = false;
|
|
60
|
+
instance.selectionBox = null;
|
|
61
|
+
instance.selectionGraphics = null; // PIXI.Graphics для визуализации рамки
|
|
62
|
+
instance.initialSelectionBeforeBox = null; // снимок выделения перед началом box-select
|
|
63
|
+
|
|
64
|
+
instance.textEditor = {
|
|
65
|
+
active: false,
|
|
66
|
+
objectId: null,
|
|
67
|
+
textarea: null,
|
|
68
|
+
wrapper: null,
|
|
69
|
+
world: null,
|
|
70
|
+
position: null, // world top-left
|
|
71
|
+
properties: null, // { fontSize }
|
|
72
|
+
objectType: 'text', // 'text' or 'note'
|
|
73
|
+
isResizing: false,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function registerSelectToolCoreSubscriptions(instance) {
|
|
78
|
+
if (!instance.eventBus) return;
|
|
79
|
+
|
|
80
|
+
const onDuplicateReady = (data) => {
|
|
81
|
+
if (!instance.isAltCloneMode || !instance.clonePending) return;
|
|
82
|
+
if (!data || data.originalId !== instance.cloneSourceId) return;
|
|
83
|
+
instance.onDuplicateReady(data.newId);
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const onGroupDuplicateReady = (data) => {
|
|
87
|
+
if (!instance.isAltGroupCloneMode || !instance.groupClonePending) return;
|
|
88
|
+
if (!data || !data.map) return;
|
|
89
|
+
instance.onGroupDuplicateReady(data.map);
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const onObjectEdit = (object) => {
|
|
93
|
+
const objectType = object.type || (object.object && object.object.type) || 'text';
|
|
94
|
+
if (objectType === 'file') {
|
|
95
|
+
instance._openFileNameEditor(object, object.create || false);
|
|
96
|
+
} else {
|
|
97
|
+
if (object.create) {
|
|
98
|
+
instance._openTextEditor(object, true);
|
|
99
|
+
} else {
|
|
100
|
+
instance._openTextEditor(object, false);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
const onObjectDeleted = (data) => {
|
|
106
|
+
const objectId = data?.objectId || data;
|
|
107
|
+
if (!objectId) return;
|
|
108
|
+
if (instance.selection?.has(objectId)) {
|
|
109
|
+
instance.removeFromSelection(objectId);
|
|
110
|
+
if (instance.selection.size() === 0) {
|
|
111
|
+
instance.emit(Events.Tool.SelectionClear);
|
|
112
|
+
instance.updateResizeHandles();
|
|
113
|
+
}
|
|
114
|
+
} else {
|
|
115
|
+
instance.updateResizeHandles();
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
instance._coreHandlers = { onDuplicateReady, onGroupDuplicateReady, onObjectEdit, onObjectDeleted };
|
|
120
|
+
instance.eventBus.on(Events.Tool.DuplicateReady, onDuplicateReady);
|
|
121
|
+
instance.eventBus.on(Events.Tool.GroupDuplicateReady, onGroupDuplicateReady);
|
|
122
|
+
instance.eventBus.on(Events.Tool.ObjectEdit, onObjectEdit);
|
|
123
|
+
instance.eventBus.on(Events.Object.Deleted, onObjectDeleted);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function unregisterSelectToolCoreSubscriptions(instance) {
|
|
127
|
+
if (!instance.eventBus || !instance._coreHandlers) return;
|
|
128
|
+
const { onDuplicateReady, onGroupDuplicateReady, onObjectEdit, onObjectDeleted } = instance._coreHandlers;
|
|
129
|
+
instance.eventBus.off(Events.Tool.DuplicateReady, onDuplicateReady);
|
|
130
|
+
instance.eventBus.off(Events.Tool.GroupDuplicateReady, onGroupDuplicateReady);
|
|
131
|
+
instance.eventBus.off(Events.Tool.ObjectEdit, onObjectEdit);
|
|
132
|
+
instance.eventBus.off(Events.Object.Deleted, onObjectDeleted);
|
|
133
|
+
instance._coreHandlers = null;
|
|
134
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import * as PIXI from 'pixi.js';
|
|
2
|
+
import { Events } from '../../../core/events/Events.js';
|
|
3
|
+
|
|
4
|
+
export function drawGroupSelectionGraphics() {
|
|
5
|
+
if (!this.app || !this.app.stage) return;
|
|
6
|
+
const selectedIds = this.selection.toArray();
|
|
7
|
+
if (selectedIds.length <= 1) {
|
|
8
|
+
this.removeGroupSelectionGraphics();
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
// Получаем bounds всех объектов и отрисовываем контур на groupBoundsGraphics (одна рамка с ручками)
|
|
12
|
+
const request = { objects: [] };
|
|
13
|
+
this.emit(Events.Tool.GetAllObjects, request);
|
|
14
|
+
const idToBounds = new Map(request.objects.map(o => [o.id, o.bounds]));
|
|
15
|
+
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
|
16
|
+
for (const id of selectedIds) {
|
|
17
|
+
const b = idToBounds.get(id);
|
|
18
|
+
if (!b) continue;
|
|
19
|
+
minX = Math.min(minX, b.x);
|
|
20
|
+
minY = Math.min(minY, b.y);
|
|
21
|
+
maxX = Math.max(maxX, b.x + b.width);
|
|
22
|
+
maxY = Math.max(maxY, b.y + b.height);
|
|
23
|
+
}
|
|
24
|
+
if (isFinite(minX) && isFinite(minY) && isFinite(maxX) && isFinite(maxY)) {
|
|
25
|
+
const gb = { x: minX, y: minY, width: maxX - minX, height: maxY - minY };
|
|
26
|
+
this.ensureGroupBoundsGraphics(gb);
|
|
27
|
+
this.updateGroupBoundsGraphics(gb);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function removeGroupSelectionGraphics() {
|
|
32
|
+
if (this.groupBoundsGraphics) {
|
|
33
|
+
this.groupBoundsGraphics.clear();
|
|
34
|
+
this.groupBoundsGraphics.rotation = 0;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function computeGroupBounds() {
|
|
39
|
+
const request = { objects: [] };
|
|
40
|
+
this.emit(Events.Tool.GetAllObjects, request);
|
|
41
|
+
const pixiMap = new Map(request.objects.map(o => [o.id, o.pixi]));
|
|
42
|
+
const b = this.selection.computeBounds((id) => pixiMap.get(id));
|
|
43
|
+
if (!b) return { x: 0, y: 0, width: 0, height: 0 };
|
|
44
|
+
return b;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function ensureGroupBoundsGraphics(bounds) {
|
|
48
|
+
if (!this.app || !this.app.stage) return;
|
|
49
|
+
if (!this.groupBoundsGraphics) {
|
|
50
|
+
this.groupBoundsGraphics = new PIXI.Graphics();
|
|
51
|
+
this.groupBoundsGraphics.name = 'group-bounds';
|
|
52
|
+
this.groupBoundsGraphics.zIndex = 1400;
|
|
53
|
+
this.app.stage.addChild(this.groupBoundsGraphics);
|
|
54
|
+
this.app.stage.sortableChildren = true;
|
|
55
|
+
}
|
|
56
|
+
this.updateGroupBoundsGraphics(bounds);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function updateGroupBoundsGraphics(bounds) {
|
|
60
|
+
if (!this.groupBoundsGraphics) return;
|
|
61
|
+
this.groupBoundsGraphics.clear();
|
|
62
|
+
// Прозрачная заливка (alpha ~0), чтобы getBounds() давал корректные размеры и не было артефактов
|
|
63
|
+
this.groupBoundsGraphics.beginFill(0x000000, 0.001);
|
|
64
|
+
this.groupBoundsGraphics.drawRect(0, 0, Math.max(1, bounds.width), Math.max(1, bounds.height));
|
|
65
|
+
this.groupBoundsGraphics.endFill();
|
|
66
|
+
// Размещаем графику в левом-верхнем углу группы
|
|
67
|
+
this.groupBoundsGraphics.position.set(bounds.x, bounds.y);
|
|
68
|
+
// Обновляем ручки, если показаны
|
|
69
|
+
// HTML-ручки обновляются слоем HtmlHandlesLayer
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function updateGroupBoundsGraphicsByTopLeft(topLeft) {
|
|
73
|
+
if (!this.groupBoundsGraphics || !this.groupStartBounds) return;
|
|
74
|
+
this.updateGroupBoundsGraphics({ x: topLeft.x, y: topLeft.y, width: this.groupStartBounds.width, height: this.groupStartBounds.height });
|
|
75
|
+
// Рисуем визуальную общую рамку одновременно
|
|
76
|
+
if (this.groupSelectionGraphics) {
|
|
77
|
+
this.groupSelectionGraphics.clear();
|
|
78
|
+
this.groupSelectionGraphics.lineStyle(1, 0x3B82F6, 0.9);
|
|
79
|
+
this.groupSelectionGraphics.drawRect(topLeft.x, topLeft.y, this.groupStartBounds.width, this.groupStartBounds.height);
|
|
80
|
+
}
|
|
81
|
+
}
|