@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.
- 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 +7 -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 -1765
- 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 -976
- 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
|
@@ -1,6 +1,10 @@
|
|
|
1
|
-
import { Events } from '../core/events/Events.js';
|
|
2
|
-
import * as PIXI from 'pixi.js';
|
|
3
1
|
import rotateIconSvg from '../assets/icons/rotate-icon.svg?raw';
|
|
2
|
+
import { HandlesDomRenderer } from './handles/HandlesDomRenderer.js';
|
|
3
|
+
import { HandlesPositioningService } from './handles/HandlesPositioningService.js';
|
|
4
|
+
import { HandlesEventBridge } from './handles/HandlesEventBridge.js';
|
|
5
|
+
import { SingleSelectionHandlesController } from './handles/SingleSelectionHandlesController.js';
|
|
6
|
+
import { GroupSelectionHandlesController } from './handles/GroupSelectionHandlesController.js';
|
|
7
|
+
import { HandlesInteractionController } from './handles/HandlesInteractionController.js';
|
|
4
8
|
|
|
5
9
|
/**
|
|
6
10
|
* HtmlHandlesLayer — HTML-ручки и рамка для выделенных объектов.
|
|
@@ -23,11 +27,18 @@ export class HtmlHandlesLayer {
|
|
|
23
27
|
this.handles = {};
|
|
24
28
|
this._drag = null;
|
|
25
29
|
this._handlesSuppressed = false; // скрывать ручки во время перетаскивания/трансформаций
|
|
30
|
+
this._groupRotationPreview = null;
|
|
26
31
|
|
|
27
32
|
// Ссылки на обработчики, чтобы корректно отписаться при destroy()
|
|
28
33
|
this._onWindowResize = null;
|
|
29
34
|
this._onDprChange = null;
|
|
30
35
|
this._dprMediaQuery = null;
|
|
36
|
+
this.positioningService = new HandlesPositioningService(this);
|
|
37
|
+
this.domRenderer = new HandlesDomRenderer(this, rotateIconSvg);
|
|
38
|
+
this.eventBridge = new HandlesEventBridge(this);
|
|
39
|
+
this.singleSelectionController = new SingleSelectionHandlesController(this);
|
|
40
|
+
this.groupSelectionController = new GroupSelectionHandlesController(this);
|
|
41
|
+
this.interactionController = new HandlesInteractionController(this);
|
|
31
42
|
}
|
|
32
43
|
|
|
33
44
|
attach() {
|
|
@@ -53,58 +64,7 @@ export class HtmlHandlesLayer {
|
|
|
53
64
|
} catch (_) {}
|
|
54
65
|
}
|
|
55
66
|
|
|
56
|
-
|
|
57
|
-
this.eventBus.on(Events.Tool.SelectionAdd, () => this.update());
|
|
58
|
-
this.eventBus.on(Events.Tool.SelectionRemove, () => this.update());
|
|
59
|
-
this.eventBus.on(Events.Tool.SelectionClear, () => this.hide());
|
|
60
|
-
this.eventBus.on(Events.Tool.DragUpdate, () => this.update());
|
|
61
|
-
|
|
62
|
-
// ИСПРАВЛЕНИЕ: Обработка удаления объектов
|
|
63
|
-
this.eventBus.on(Events.Object.Deleted, (data) => {
|
|
64
|
-
const objectId = data?.objectId || data;
|
|
65
|
-
console.log('🗑️ HtmlHandlesLayer: получено событие удаления:', data, 'objectId:', objectId);
|
|
66
|
-
|
|
67
|
-
// Принудительно скрываем и очищаем все ручки
|
|
68
|
-
this.hide();
|
|
69
|
-
|
|
70
|
-
// Очищаем DOM от старых ручек
|
|
71
|
-
this.layer.innerHTML = '';
|
|
72
|
-
|
|
73
|
-
// Обновляем для актуального состояния
|
|
74
|
-
setTimeout(() => {
|
|
75
|
-
this.update();
|
|
76
|
-
}, 10); // Небольшая задержка для полной очистки
|
|
77
|
-
});
|
|
78
|
-
this.eventBus.on(Events.Tool.DragStart, () => { this._handlesSuppressed = true; this._setHandlesVisibility(false); });
|
|
79
|
-
this.eventBus.on(Events.Tool.DragEnd, () => { this._handlesSuppressed = false; this._setHandlesVisibility(true); });
|
|
80
|
-
this.eventBus.on(Events.Tool.ResizeUpdate, () => this.update());
|
|
81
|
-
this.eventBus.on(Events.Tool.ResizeStart, () => { this._handlesSuppressed = true; this._setHandlesVisibility(false); });
|
|
82
|
-
this.eventBus.on(Events.Tool.ResizeEnd, () => { this._handlesSuppressed = false; this._setHandlesVisibility(true); });
|
|
83
|
-
this.eventBus.on(Events.Tool.RotateUpdate, () => this.update());
|
|
84
|
-
this.eventBus.on(Events.Tool.RotateStart, () => { this._handlesSuppressed = true; this._setHandlesVisibility(false); });
|
|
85
|
-
this.eventBus.on(Events.Tool.RotateEnd, () => { this._handlesSuppressed = false; this._setHandlesVisibility(true); });
|
|
86
|
-
this.eventBus.on(Events.Tool.GroupDragUpdate, () => this.update());
|
|
87
|
-
this.eventBus.on(Events.Tool.GroupDragStart, () => { this._handlesSuppressed = true; this._setHandlesVisibility(false); });
|
|
88
|
-
this.eventBus.on(Events.Tool.GroupDragEnd, () => { this._handlesSuppressed = false; this._setHandlesVisibility(true); });
|
|
89
|
-
this.eventBus.on(Events.Tool.GroupResizeUpdate, () => this.update());
|
|
90
|
-
this.eventBus.on(Events.Tool.GroupResizeStart, () => { this._handlesSuppressed = true; this._setHandlesVisibility(false); });
|
|
91
|
-
this.eventBus.on(Events.Tool.GroupResizeEnd, () => { this._handlesSuppressed = false; this._setHandlesVisibility(true); });
|
|
92
|
-
this.eventBus.on(Events.Tool.GroupRotateUpdate, () => this.update());
|
|
93
|
-
this.eventBus.on(Events.Tool.GroupRotateStart, () => { this._handlesSuppressed = true; this._setHandlesVisibility(false); });
|
|
94
|
-
this.eventBus.on(Events.Tool.GroupRotateEnd, () => { this._handlesSuppressed = false; this._setHandlesVisibility(true); });
|
|
95
|
-
this.eventBus.on(Events.UI.ZoomPercent, () => this.update());
|
|
96
|
-
this.eventBus.on(Events.Tool.PanUpdate, () => this.update());
|
|
97
|
-
|
|
98
|
-
// Обновление рамки при undo/redo команд трансформации (перемещение, ресайз, поворот)
|
|
99
|
-
this.eventBus.on(Events.Object.TransformUpdated, (data) => {
|
|
100
|
-
// Проверяем, что обновленный объект выделен, и обновляем ручки
|
|
101
|
-
if (this.core?.selectTool && data.objectId) {
|
|
102
|
-
const isSelected = this.core.selectTool.selectedObjects.has(data.objectId);
|
|
103
|
-
if (isSelected) {
|
|
104
|
-
this.update();
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
});
|
|
67
|
+
this.eventBridge.attach();
|
|
108
68
|
|
|
109
69
|
this.update();
|
|
110
70
|
}
|
|
@@ -127,6 +87,7 @@ export class HtmlHandlesLayer {
|
|
|
127
87
|
this._dprMediaQuery = null;
|
|
128
88
|
this._onDprChange = null;
|
|
129
89
|
}
|
|
90
|
+
this.eventBridge.detach();
|
|
130
91
|
|
|
131
92
|
if (this.layer) {
|
|
132
93
|
this.layer.remove();
|
|
@@ -142,61 +103,9 @@ export class HtmlHandlesLayer {
|
|
|
142
103
|
const ids = selectTool ? Array.from(selectTool.selectedObjects || []) : [];
|
|
143
104
|
if (!ids || ids.length === 0) { this.hide(); return; }
|
|
144
105
|
if (ids.length === 1) {
|
|
145
|
-
|
|
146
|
-
const pixi = this.core.pixi.objects.get(id);
|
|
147
|
-
if (!pixi) { this.hide(); return; }
|
|
148
|
-
// Не показываем рамку/ручки для комментариев
|
|
149
|
-
const mb = pixi._mb || {};
|
|
150
|
-
if (mb.type === 'comment') { this.hide(); return; }
|
|
151
|
-
|
|
152
|
-
// Получаем данные объекта через события (избегаем проблем с глобальными границами)
|
|
153
|
-
const positionData = { objectId: id, position: null };
|
|
154
|
-
const sizeData = { objectId: id, size: null };
|
|
155
|
-
this.eventBus.emit(Events.Tool.GetObjectPosition, positionData);
|
|
156
|
-
this.eventBus.emit(Events.Tool.GetObjectSize, sizeData);
|
|
157
|
-
|
|
158
|
-
if (positionData.position && sizeData.size) {
|
|
159
|
-
// Используем данные из состояния вместо getBounds() для избежания масштабирования
|
|
160
|
-
this._showBounds({
|
|
161
|
-
x: positionData.position.x,
|
|
162
|
-
y: positionData.position.y,
|
|
163
|
-
width: sizeData.size.width,
|
|
164
|
-
height: sizeData.size.height
|
|
165
|
-
}, id);
|
|
166
|
-
} else {
|
|
167
|
-
// Fallback к getBounds() если события не сработали — конвертируем в мировые координаты (без зума)
|
|
168
|
-
const world = this.core.pixi.worldLayer || this.core.pixi.app.stage;
|
|
169
|
-
const b = pixi.getBounds();
|
|
170
|
-
const tl = world.toLocal(new PIXI.Point(b.x, b.y));
|
|
171
|
-
const br = world.toLocal(new PIXI.Point(b.x + b.width, b.y + b.height));
|
|
172
|
-
const wx = Math.min(tl.x, br.x);
|
|
173
|
-
const wy = Math.min(tl.y, br.y);
|
|
174
|
-
const ww = Math.max(1, Math.abs(br.x - tl.x));
|
|
175
|
-
const wh = Math.max(1, Math.abs(br.y - tl.y));
|
|
176
|
-
this._showBounds({ x: wx, y: wy, width: ww, height: wh }, id);
|
|
177
|
-
}
|
|
106
|
+
this.singleSelectionController.renderForSelection(ids[0]);
|
|
178
107
|
} else {
|
|
179
|
-
|
|
180
|
-
const world = this.core.pixi.worldLayer || this.core.pixi.app.stage;
|
|
181
|
-
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
|
182
|
-
ids.forEach(id => {
|
|
183
|
-
const p = this.core.pixi.objects.get(id);
|
|
184
|
-
if (!p) return;
|
|
185
|
-
const b = p.getBounds();
|
|
186
|
-
// Конвертируем углы прямоугольника из экранных в мировые координаты
|
|
187
|
-
const tl = world.toLocal(new PIXI.Point(b.x, b.y));
|
|
188
|
-
const br = world.toLocal(new PIXI.Point(b.x + b.width, b.y + b.height));
|
|
189
|
-
const x0 = Math.min(tl.x, br.x);
|
|
190
|
-
const y0 = Math.min(tl.y, br.y);
|
|
191
|
-
const x1 = Math.max(tl.x, br.x);
|
|
192
|
-
const y1 = Math.max(tl.y, br.y);
|
|
193
|
-
minX = Math.min(minX, x0);
|
|
194
|
-
minY = Math.min(minY, y0);
|
|
195
|
-
maxX = Math.max(maxX, x1);
|
|
196
|
-
maxY = Math.max(maxY, y1);
|
|
197
|
-
});
|
|
198
|
-
if (!isFinite(minX)) { this.hide(); return; }
|
|
199
|
-
this._showBounds({ x: minX, y: minY, width: Math.max(1, maxX - minX), height: Math.max(1, maxY - minY) }, '__group__');
|
|
108
|
+
this.groupSelectionController.renderForSelection(ids);
|
|
200
109
|
}
|
|
201
110
|
}
|
|
202
111
|
|
|
@@ -207,903 +116,139 @@ export class HtmlHandlesLayer {
|
|
|
207
116
|
}
|
|
208
117
|
|
|
209
118
|
_setHandlesVisibility(show) {
|
|
210
|
-
|
|
211
|
-
const box = this.layer.querySelector('.mb-handles-box');
|
|
212
|
-
if (!box) return;
|
|
213
|
-
// Уголки
|
|
214
|
-
box.querySelectorAll('[data-dir]').forEach(el => {
|
|
215
|
-
el.style.display = show ? '' : 'none';
|
|
216
|
-
});
|
|
217
|
-
// Рёбра
|
|
218
|
-
box.querySelectorAll('[data-edge]').forEach(el => {
|
|
219
|
-
el.style.display = show ? '' : 'none';
|
|
220
|
-
});
|
|
221
|
-
// Ручка вращения
|
|
222
|
-
const rot = box.querySelector('[data-handle="rotate"]');
|
|
223
|
-
if (rot) rot.style.display = show ? '' : 'none';
|
|
224
|
-
// Если нужно показать, но ручек нет (мы их не создавали в suppressed-режиме) — перерисуем
|
|
225
|
-
if (show && !box.querySelector('[data-dir]')) {
|
|
226
|
-
this.update();
|
|
227
|
-
}
|
|
119
|
+
this.domRenderer.setHandlesVisibility(show);
|
|
228
120
|
}
|
|
229
121
|
|
|
230
|
-
_showBounds(worldBounds, id) {
|
|
231
|
-
|
|
232
|
-
// Преобразуем world координаты в CSS-пиксели
|
|
233
|
-
const view = this.core.pixi.app.view;
|
|
234
|
-
const rendererRes = (this.core.pixi.app.renderer?.resolution) || 1;
|
|
235
|
-
const containerRect = this.container.getBoundingClientRect();
|
|
236
|
-
const viewRect = view.getBoundingClientRect();
|
|
237
|
-
const offsetLeft = viewRect.left - containerRect.left;
|
|
238
|
-
const offsetTop = viewRect.top - containerRect.top;
|
|
239
|
-
|
|
240
|
-
// Получаем масштаб world layer для правильного преобразования
|
|
241
|
-
const world = this.core.pixi.worldLayer || this.core.pixi.app.stage;
|
|
242
|
-
const worldScale = world?.scale?.x || 1;
|
|
243
|
-
const worldX = world?.x || 0;
|
|
244
|
-
const worldY = world?.y || 0;
|
|
245
|
-
|
|
246
|
-
// Узнаём тип объекта (нужно, чтобы для file/frame отключать определённые элементы)
|
|
247
|
-
let isFileTarget = false;
|
|
248
|
-
let isFrameTarget = false;
|
|
249
|
-
if (id !== '__group__') {
|
|
250
|
-
const req = { objectId: id, pixiObject: null };
|
|
251
|
-
this.eventBus.emit(Events.Tool.GetObjectPixi, req);
|
|
252
|
-
const mbType = req.pixiObject && req.pixiObject._mb && req.pixiObject._mb.type;
|
|
253
|
-
isFileTarget = mbType === 'file';
|
|
254
|
-
isFrameTarget = mbType === 'frame';
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
// Вычисляем позицию и размер через математику сцены (toGlobal) и переводим в CSS px.
|
|
258
|
-
// Важно: toGlobal() уже возвращает координаты в логических экранных пикселях
|
|
259
|
-
// (учитывая позицию/масштаб world), поэтому ДЕЛИТЬ их на renderer.resolution не нужно.
|
|
260
|
-
// Деление приводило к эффекту 1 / resolution (например, при res = 0.8 рамка
|
|
261
|
-
// становилась больше и съезжала относительно объекта).
|
|
262
|
-
const tl = world.toGlobal(new PIXI.Point(worldBounds.x, worldBounds.y));
|
|
263
|
-
const br = world.toGlobal(new PIXI.Point(worldBounds.x + worldBounds.width, worldBounds.y + worldBounds.height));
|
|
264
|
-
const cssX = offsetLeft + tl.x;
|
|
265
|
-
const cssY = offsetTop + tl.y;
|
|
266
|
-
const cssWidth = Math.max(1, (br.x - tl.x));
|
|
267
|
-
const cssHeight = Math.max(1, (br.y - tl.y));
|
|
268
|
-
|
|
269
|
-
const left = Math.round(cssX);
|
|
270
|
-
const top = Math.round(cssY);
|
|
271
|
-
const width = Math.round(cssWidth);
|
|
272
|
-
const height = Math.round(cssHeight);
|
|
273
|
-
|
|
274
|
-
this.layer.innerHTML = '';
|
|
275
|
-
const box = document.createElement('div');
|
|
276
|
-
box.className = 'mb-handles-box';
|
|
277
|
-
|
|
278
|
-
// Получаем угол поворота объекта для поворота рамки
|
|
279
|
-
let rotation = 0;
|
|
280
|
-
if (id !== '__group__') {
|
|
281
|
-
const rotationData = { objectId: id, rotation: 0 };
|
|
282
|
-
this.eventBus.emit(Events.Tool.GetObjectRotation, rotationData);
|
|
283
|
-
rotation = rotationData.rotation || 0; // В градусах
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
Object.assign(box.style, {
|
|
287
|
-
position: 'absolute', left: `${left}px`, top: `${top}px`,
|
|
288
|
-
width: `${width}px`, height: `${height}px`,
|
|
289
|
-
border: '1px solid #1DE9B6', borderRadius: '3px', boxSizing: 'content-box', pointerEvents: 'none',
|
|
290
|
-
transformOrigin: 'center center', // Поворот вокруг центра
|
|
291
|
-
transform: `rotate(${rotation}deg)` // Применяем поворот
|
|
292
|
-
});
|
|
293
|
-
this.layer.appendChild(box);
|
|
294
|
-
// Если сейчас подавление ручек активно — не создавать ручки вовсе, оставляем только рамку
|
|
295
|
-
if (this._handlesSuppressed) {
|
|
296
|
-
this.visible = true;
|
|
297
|
-
return;
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
// Угловые ручки для ресайза - круглые с мятно-зелёным цветом и белой серединой
|
|
301
|
-
const mkCorner = (dir, x, y, cursor) => {
|
|
302
|
-
const h = document.createElement('div');
|
|
303
|
-
h.dataset.dir = dir; h.dataset.id = id;
|
|
304
|
-
h.className = 'mb-handle';
|
|
305
|
-
h.style.pointerEvents = isFileTarget ? 'none' : 'auto';
|
|
306
|
-
h.style.cursor = cursor;
|
|
307
|
-
h.style.left = `${x - 6}px`;
|
|
308
|
-
h.style.top = `${y - 6}px`;
|
|
309
|
-
// Для файла скрываем ручки, для остальных показываем
|
|
310
|
-
h.style.display = isFileTarget ? 'none' : 'block';
|
|
311
|
-
|
|
312
|
-
// Создаем внутренний белый круг
|
|
313
|
-
const inner = document.createElement('div');
|
|
314
|
-
inner.className = 'mb-handle-inner';
|
|
315
|
-
h.appendChild(inner);
|
|
316
|
-
|
|
317
|
-
// Эффект при наведении
|
|
318
|
-
h.addEventListener('mouseenter', () => {
|
|
319
|
-
h.style.background = '#17C29A';
|
|
320
|
-
h.style.borderColor = '#17C29A';
|
|
321
|
-
h.style.cursor = cursor; // Принудительно устанавливаем курсор
|
|
322
|
-
});
|
|
323
|
-
h.addEventListener('mouseleave', () => {
|
|
324
|
-
h.style.background = '#1DE9B6';
|
|
325
|
-
h.style.borderColor = '#1DE9B6';
|
|
326
|
-
});
|
|
327
|
-
|
|
328
|
-
if (!isFileTarget) {
|
|
329
|
-
h.addEventListener('mousedown', (e) => this._onHandleDown(e, box));
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
box.appendChild(h);
|
|
333
|
-
};
|
|
334
|
-
|
|
335
|
-
const x0 = 0, y0 = 0, x1 = width, y1 = height, cx = Math.round(width / 2), cy = Math.round(height / 2);
|
|
336
|
-
mkCorner('nw', x0, y0, 'nwse-resize');
|
|
337
|
-
mkCorner('ne', x1, y0, 'nesw-resize');
|
|
338
|
-
mkCorner('se', x1, y1, 'nwse-resize');
|
|
339
|
-
mkCorner('sw', x0, y1, 'nesw-resize');
|
|
340
|
-
|
|
341
|
-
// Видимые ручки на серединах сторон отключены (масштабирование по рёбрам работает через невидимые зоны)
|
|
342
|
-
|
|
343
|
-
// Кликабельные грани для ресайза (невидимые области для лучшего UX)
|
|
344
|
-
// Уменьшаем их, чтобы не перекрывать угловые ручки
|
|
345
|
-
const edgeSize = 10; // уменьшаем размер
|
|
346
|
-
const makeEdge = (name, style, cursor) => {
|
|
347
|
-
const e = document.createElement('div');
|
|
348
|
-
e.dataset.edge = name; e.dataset.id = id;
|
|
349
|
-
e.className = 'mb-edge';
|
|
350
|
-
Object.assign(e.style, style, {
|
|
351
|
-
pointerEvents: isFileTarget ? 'none' : 'auto', cursor,
|
|
352
|
-
display: isFileTarget ? 'none' : 'block'
|
|
353
|
-
});
|
|
354
|
-
if (!isFileTarget) {
|
|
355
|
-
e.addEventListener('mousedown', (evt) => this._onEdgeResizeDown(evt));
|
|
356
|
-
}
|
|
357
|
-
box.appendChild(e);
|
|
358
|
-
};
|
|
359
|
-
|
|
360
|
-
// Создаем грани с отступами от углов, чтобы не мешать угловым ручкам
|
|
361
|
-
const cornerGap = 20; // отступ от углов
|
|
362
|
-
|
|
363
|
-
// top - с отступами от углов
|
|
364
|
-
makeEdge('top', {
|
|
365
|
-
left: `${cornerGap}px`,
|
|
366
|
-
top: `-${edgeSize/2}px`,
|
|
367
|
-
width: `${Math.max(0, width - 2 * cornerGap)}px`,
|
|
368
|
-
height: `${edgeSize}px`
|
|
369
|
-
}, 'ns-resize');
|
|
370
|
-
|
|
371
|
-
// bottom - с отступами от углов
|
|
372
|
-
makeEdge('bottom', {
|
|
373
|
-
left: `${cornerGap}px`,
|
|
374
|
-
top: `${height - edgeSize/2}px`,
|
|
375
|
-
width: `${Math.max(0, width - 2 * cornerGap)}px`,
|
|
376
|
-
height: `${edgeSize}px`
|
|
377
|
-
}, 'ns-resize');
|
|
378
|
-
|
|
379
|
-
// left - с отступами от углов
|
|
380
|
-
makeEdge('left', {
|
|
381
|
-
left: `-${edgeSize/2}px`,
|
|
382
|
-
top: `${cornerGap}px`,
|
|
383
|
-
width: `${edgeSize}px`,
|
|
384
|
-
height: `${Math.max(0, height - 2 * cornerGap)}px`
|
|
385
|
-
}, 'ew-resize');
|
|
386
|
-
|
|
387
|
-
// right - с отступами от углов
|
|
388
|
-
makeEdge('right', {
|
|
389
|
-
left: `${width - edgeSize/2}px`,
|
|
390
|
-
top: `${cornerGap}px`,
|
|
391
|
-
width: `${edgeSize}px`,
|
|
392
|
-
height: `${Math.max(0, height - 2 * cornerGap)}px`
|
|
393
|
-
}, 'ew-resize');
|
|
394
|
-
|
|
395
|
-
// Ручка вращения: SVG-иконка, показываем для всех, кроме файла
|
|
396
|
-
const rotateHandle = document.createElement('div');
|
|
397
|
-
rotateHandle.dataset.handle = 'rotate';
|
|
398
|
-
rotateHandle.dataset.id = id;
|
|
399
|
-
if (isFileTarget || isFrameTarget) {
|
|
400
|
-
Object.assign(rotateHandle.style, { display: 'none', pointerEvents: 'none' });
|
|
401
|
-
} else {
|
|
402
|
-
rotateHandle.className = 'mb-rotate-handle';
|
|
403
|
-
// Фиксированная дистанция 20px по диагонали (top-right → bottom-left) от угла (0, h)
|
|
404
|
-
const d = 38;
|
|
405
|
-
const L = Math.max(1, Math.hypot(width, height));
|
|
406
|
-
const centerX = -(width / L) * d; // влево от левого нижнего угла
|
|
407
|
-
const centerY = height + (height / L) * d; // ниже нижней грани
|
|
408
|
-
rotateHandle.style.left = `${Math.round(centerX - 0)}px`;
|
|
409
|
-
rotateHandle.style.top = `${Math.round(centerY - 10)}px`;
|
|
410
|
-
rotateHandle.innerHTML = rotateIconSvg;
|
|
411
|
-
const svgEl = rotateHandle.querySelector('svg');
|
|
412
|
-
if (svgEl) {
|
|
413
|
-
svgEl.style.width = '100%';
|
|
414
|
-
svgEl.style.height = '100%';
|
|
415
|
-
svgEl.style.display = 'block';
|
|
416
|
-
}
|
|
417
|
-
rotateHandle.addEventListener('mousedown', (e) => this._onRotateHandleDown(e, box));
|
|
418
|
-
}
|
|
419
|
-
box.appendChild(rotateHandle);
|
|
420
|
-
|
|
421
|
-
this.visible = true;
|
|
422
|
-
this.target = { type: id === '__group__' ? 'group' : 'single', id, bounds: worldBounds };
|
|
122
|
+
_showBounds(worldBounds, id, options = {}) {
|
|
123
|
+
this.domRenderer.showBounds(worldBounds, id, options);
|
|
423
124
|
}
|
|
424
125
|
|
|
425
126
|
_toWorldScreenInverse(dx, dy) {
|
|
426
|
-
|
|
427
|
-
const s = world?.scale?.x || 1;
|
|
428
|
-
// dx, dy приходят в CSS-пикселях; для world делим только на масштаб
|
|
429
|
-
return { dxWorld: dx / s, dyWorld: dy / s };
|
|
127
|
+
return this.positioningService.toWorldScreenInverse(dx, dy);
|
|
430
128
|
}
|
|
431
129
|
|
|
432
130
|
_onHandleDown(e, box) {
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
const id = e.currentTarget.dataset.id;
|
|
436
|
-
const isGroup = id === '__group__';
|
|
437
|
-
const world = this.core.pixi.worldLayer || this.core.pixi.app.stage;
|
|
438
|
-
const s = world?.scale?.x || 1;
|
|
439
|
-
const tx = world?.x || 0;
|
|
440
|
-
const ty = world?.y || 0;
|
|
441
|
-
const rendererRes = (this.core.pixi.app.renderer?.resolution) || 1;
|
|
442
|
-
const containerRect = this.container.getBoundingClientRect();
|
|
443
|
-
const view = this.core.pixi.app.view;
|
|
444
|
-
const viewRect = view.getBoundingClientRect();
|
|
445
|
-
const offsetLeft = viewRect.left - containerRect.left;
|
|
446
|
-
const offsetTop = viewRect.top - containerRect.top;
|
|
447
|
-
|
|
448
|
-
const startCSS = {
|
|
449
|
-
left: parseFloat(box.style.left),
|
|
450
|
-
top: parseFloat(box.style.top),
|
|
451
|
-
width: parseFloat(box.style.width),
|
|
452
|
-
height: parseFloat(box.style.height),
|
|
453
|
-
};
|
|
454
|
-
const startScreen = {
|
|
455
|
-
x: (startCSS.left - offsetLeft),
|
|
456
|
-
y: (startCSS.top - offsetTop),
|
|
457
|
-
w: startCSS.width,
|
|
458
|
-
h: startCSS.height,
|
|
459
|
-
};
|
|
460
|
-
// Экранные координаты (CSS) → device-пиксели → world
|
|
461
|
-
const startWorld = {
|
|
462
|
-
x: ((startScreen.x * rendererRes) - tx) / s,
|
|
463
|
-
y: ((startScreen.y * rendererRes) - ty) / s,
|
|
464
|
-
width: (startScreen.w * rendererRes) / s,
|
|
465
|
-
height: (startScreen.h * rendererRes) / s,
|
|
466
|
-
};
|
|
467
|
-
|
|
468
|
-
let objects = [id];
|
|
469
|
-
if (isGroup) {
|
|
470
|
-
const req = { selection: [] };
|
|
471
|
-
this.eventBus.emit(Events.Tool.GetSelection, req);
|
|
472
|
-
objects = req.selection || [];
|
|
473
|
-
// Сообщаем ядру старт группового ресайза
|
|
474
|
-
this.eventBus.emit(Events.Tool.GroupResizeStart, { objects, startBounds: { ...startWorld } });
|
|
475
|
-
} else {
|
|
476
|
-
// Сигнал о старте одиночного ресайза
|
|
477
|
-
this.eventBus.emit(Events.Tool.ResizeStart, { object: id, handle: dir });
|
|
478
|
-
}
|
|
479
|
-
|
|
480
|
-
const startMouse = { x: e.clientX, y: e.clientY };
|
|
481
|
-
// Определяем тип объекта (нужно, чтобы для текста автоподгонять высоту)
|
|
482
|
-
let isTextTarget = false;
|
|
483
|
-
let isNoteTarget = false;
|
|
484
|
-
{
|
|
485
|
-
const req = { objectId: id, pixiObject: null };
|
|
486
|
-
this.eventBus.emit(Events.Tool.GetObjectPixi, req);
|
|
487
|
-
const mbType = req.pixiObject && req.pixiObject._mb && req.pixiObject._mb.type;
|
|
488
|
-
isTextTarget = (mbType === 'text' || mbType === 'simple-text');
|
|
489
|
-
isNoteTarget = (mbType === 'note');
|
|
490
|
-
}
|
|
491
|
-
|
|
492
|
-
const onMove = (ev) => {
|
|
493
|
-
const dx = ev.clientX - startMouse.x;
|
|
494
|
-
const dy = ev.clientY - startMouse.y;
|
|
495
|
-
// Новые CSS-габариты и позиция
|
|
496
|
-
let newLeft = startCSS.left;
|
|
497
|
-
let newTop = startCSS.top;
|
|
498
|
-
let newW = startCSS.width;
|
|
499
|
-
let newH = startCSS.height;
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
if (dir.includes('e')) newW = Math.max(1, startCSS.width + dx);
|
|
504
|
-
if (dir.includes('s')) newH = Math.max(1, startCSS.height + dy);
|
|
505
|
-
if (dir.includes('w')) {
|
|
506
|
-
newW = Math.max(1, startCSS.width - dx);
|
|
507
|
-
newLeft = startCSS.left + dx;
|
|
508
|
-
}
|
|
509
|
-
if (dir.includes('n')) {
|
|
510
|
-
newH = Math.max(1, startCSS.height - dy);
|
|
511
|
-
newTop = startCSS.top + dy;
|
|
512
|
-
}
|
|
513
|
-
|
|
514
|
-
// Для записки удерживаем квадрат и компенсируем позицию в зависимости от ручки
|
|
515
|
-
if (isNoteTarget) {
|
|
516
|
-
const s = Math.max(newW, newH);
|
|
517
|
-
// базовая фиксация размера
|
|
518
|
-
newW = s; newH = s;
|
|
519
|
-
// корректируем привязку противоположной стороны
|
|
520
|
-
if (dir.includes('w')) { newLeft = startCSS.left + (startCSS.width - s); }
|
|
521
|
-
if (dir.includes('n')) { newTop = startCSS.top + (startCSS.height - s); }
|
|
522
|
-
}
|
|
523
|
-
|
|
524
|
-
// Минимальная ширина = ширина трёх символов текущего шрифта
|
|
525
|
-
if (isTextTarget) {
|
|
526
|
-
try {
|
|
527
|
-
const textLayer = (typeof window !== 'undefined') ? window.moodboardHtmlTextLayer : null;
|
|
528
|
-
const el = textLayer && textLayer.idToEl ? textLayer.idToEl.get && textLayer.idToEl.get(id) : null;
|
|
529
|
-
if (el && typeof window.getComputedStyle === 'function') {
|
|
530
|
-
const cs = window.getComputedStyle(el);
|
|
531
|
-
const meas = document.createElement('span');
|
|
532
|
-
meas.style.position = 'absolute';
|
|
533
|
-
meas.style.visibility = 'hidden';
|
|
534
|
-
meas.style.whiteSpace = 'pre';
|
|
535
|
-
meas.style.fontFamily = cs.fontFamily;
|
|
536
|
-
meas.style.fontSize = cs.fontSize;
|
|
537
|
-
meas.style.fontWeight = cs.fontWeight;
|
|
538
|
-
meas.style.fontStyle = cs.fontStyle;
|
|
539
|
-
meas.style.letterSpacing = cs.letterSpacing || 'normal';
|
|
540
|
-
meas.textContent = 'WWW';
|
|
541
|
-
document.body.appendChild(meas);
|
|
542
|
-
const minWidthPx = Math.max(1, Math.ceil(meas.getBoundingClientRect().width));
|
|
543
|
-
meas.remove();
|
|
544
|
-
if (newW < minWidthPx) {
|
|
545
|
-
if (dir.includes('w')) {
|
|
546
|
-
newLeft = startCSS.left + (startCSS.width - minWidthPx);
|
|
547
|
-
}
|
|
548
|
-
newW = minWidthPx;
|
|
549
|
-
}
|
|
550
|
-
}
|
|
551
|
-
} catch (_) {}
|
|
552
|
-
}
|
|
553
|
-
|
|
554
|
-
// Для текстовых объектов подгоняем высоту под контент при изменении ширины
|
|
555
|
-
if (isTextTarget) {
|
|
556
|
-
try {
|
|
557
|
-
const textLayer = (typeof window !== 'undefined') ? window.moodboardHtmlTextLayer : null;
|
|
558
|
-
const el = textLayer && textLayer.idToEl ? textLayer.idToEl.get && textLayer.idToEl.get(id) : null;
|
|
559
|
-
if (el) {
|
|
560
|
-
// Минимальная ширина в 3 символа
|
|
561
|
-
let minWidthPx = 0;
|
|
562
|
-
try {
|
|
563
|
-
const cs = window.getComputedStyle(el);
|
|
564
|
-
const meas = document.createElement('span');
|
|
565
|
-
meas.style.position = 'absolute';
|
|
566
|
-
meas.style.visibility = 'hidden';
|
|
567
|
-
meas.style.whiteSpace = 'pre';
|
|
568
|
-
meas.style.fontFamily = cs.fontFamily;
|
|
569
|
-
meas.style.fontSize = cs.fontSize;
|
|
570
|
-
meas.style.fontWeight = cs.fontWeight;
|
|
571
|
-
meas.style.fontStyle = cs.fontStyle;
|
|
572
|
-
meas.style.letterSpacing = cs.letterSpacing || 'normal';
|
|
573
|
-
meas.textContent = 'WWW';
|
|
574
|
-
document.body.appendChild(meas);
|
|
575
|
-
minWidthPx = Math.max(1, Math.ceil(meas.getBoundingClientRect().width));
|
|
576
|
-
meas.remove();
|
|
577
|
-
} catch (_) {}
|
|
578
|
-
|
|
579
|
-
if (minWidthPx > 0 && newW < minWidthPx) {
|
|
580
|
-
if (dir.includes('w')) {
|
|
581
|
-
newLeft = startCSS.left + (startCSS.width - minWidthPx);
|
|
582
|
-
}
|
|
583
|
-
newW = minWidthPx;
|
|
584
|
-
}
|
|
585
|
-
el.style.width = `${Math.max(1, Math.round(newW))}px`;
|
|
586
|
-
el.style.height = 'auto';
|
|
587
|
-
const measured = Math.max(1, Math.round(el.scrollHeight));
|
|
588
|
-
newH = measured;
|
|
589
|
-
}
|
|
590
|
-
} catch (_) {}
|
|
591
|
-
}
|
|
131
|
+
this.interactionController.onHandleDown(e, box);
|
|
132
|
+
}
|
|
592
133
|
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
box.style.width = `${Math.round(newW)}px`;
|
|
597
|
-
box.style.height = `${Math.round(newH)}px`;
|
|
598
|
-
// Переставим ручки без перестроения слоя
|
|
599
|
-
this._repositionBoxChildren(box);
|
|
134
|
+
_onEdgeResizeDown(e) {
|
|
135
|
+
this.interactionController.onEdgeResizeDown(e);
|
|
136
|
+
}
|
|
600
137
|
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
const screenW = newW;
|
|
605
|
-
const screenH = newH;
|
|
606
|
-
const worldX = ((screenX * rendererRes) - tx) / s;
|
|
607
|
-
const worldY = ((screenY * rendererRes) - ty) / s;
|
|
608
|
-
const worldW = (screenW * rendererRes) / s;
|
|
609
|
-
const worldH = (screenH * rendererRes) / s;
|
|
138
|
+
_onRotateHandleDown(e, box) {
|
|
139
|
+
this.interactionController.onRotateHandleDown(e, box);
|
|
140
|
+
}
|
|
610
141
|
|
|
611
|
-
|
|
612
|
-
|
|
142
|
+
_repositionBoxChildren(box) {
|
|
143
|
+
this.domRenderer.repositionBoxChildren(box);
|
|
144
|
+
}
|
|
613
145
|
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
146
|
+
_startGroupRotationPreview(payload = {}) {
|
|
147
|
+
const selectTool = this.core?.selectTool;
|
|
148
|
+
const ids = Array.from(selectTool?.selectedObjects || []);
|
|
149
|
+
if (ids.length <= 1) {
|
|
150
|
+
this._groupRotationPreview = null;
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
const prevPreview = this._groupRotationPreview;
|
|
154
|
+
const hasSameSelection = Boolean(
|
|
155
|
+
prevPreview &&
|
|
156
|
+
Array.isArray(prevPreview.ids) &&
|
|
157
|
+
prevPreview.ids.length === ids.length &&
|
|
158
|
+
ids.every((id) => prevPreview.ids.includes(id))
|
|
159
|
+
);
|
|
160
|
+
const measuredBounds = this.positioningService.getGroupSelectionWorldBounds(ids);
|
|
161
|
+
const startBounds = hasSameSelection
|
|
162
|
+
? prevPreview.startBounds
|
|
163
|
+
: measuredBounds;
|
|
164
|
+
if (!startBounds) {
|
|
165
|
+
this._groupRotationPreview = null;
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
const baseAngle = hasSameSelection ? (prevPreview.angle || 0) : 0;
|
|
169
|
+
const previewCenter = payload.center
|
|
170
|
+
? { ...payload.center }
|
|
171
|
+
: hasSameSelection && prevPreview.center
|
|
172
|
+
? { ...prevPreview.center }
|
|
173
|
+
: {
|
|
174
|
+
x: startBounds.x + startBounds.width / 2,
|
|
175
|
+
y: startBounds.y + startBounds.height / 2,
|
|
635
176
|
};
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
177
|
+
this._groupRotationPreview = {
|
|
178
|
+
ids,
|
|
179
|
+
center: previewCenter,
|
|
180
|
+
startBounds,
|
|
181
|
+
angle: baseAngle,
|
|
182
|
+
baseAngle,
|
|
183
|
+
isActive: true,
|
|
184
|
+
lastMeasuredCenter: {
|
|
185
|
+
x: measuredBounds ? measuredBounds.x + measuredBounds.width / 2 : previewCenter.x,
|
|
186
|
+
y: measuredBounds ? measuredBounds.y + measuredBounds.height / 2 : previewCenter.y,
|
|
187
|
+
},
|
|
639
188
|
};
|
|
640
|
-
|
|
641
|
-
document.removeEventListener('mousemove', onMove);
|
|
642
|
-
document.removeEventListener('mouseup', onUp);
|
|
643
|
-
// Финализация
|
|
644
|
-
const endCSS = {
|
|
645
|
-
left: parseFloat(box.style.left),
|
|
646
|
-
top: parseFloat(box.style.top),
|
|
647
|
-
width: parseFloat(box.style.width),
|
|
648
|
-
height: parseFloat(box.style.height),
|
|
649
|
-
};
|
|
650
|
-
const screenX = (endCSS.left - offsetLeft);
|
|
651
|
-
const screenY = (endCSS.top - offsetTop);
|
|
652
|
-
const screenW = endCSS.width;
|
|
653
|
-
const screenH = endCSS.height;
|
|
654
|
-
const worldX = ((screenX * rendererRes) - tx) / s;
|
|
655
|
-
const worldY = ((screenY * rendererRes) - ty) / s;
|
|
656
|
-
const worldW = (screenW * rendererRes) / s;
|
|
657
|
-
const worldH = (screenH * rendererRes) / s;
|
|
189
|
+
}
|
|
658
190
|
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
const finalPositionChanged = (endCSS.left !== startCSS.left) || (endCSS.top !== startCSS.top);
|
|
664
|
-
|
|
665
|
-
const isEdgeLeftOrTop = dir.includes('w') || dir.includes('n');
|
|
666
|
-
let isFrameTarget = false;
|
|
667
|
-
{
|
|
668
|
-
const req = { objectId: id, pixiObject: null };
|
|
669
|
-
this.eventBus.emit(Events.Tool.GetObjectPixi, req);
|
|
670
|
-
const mbType = req.pixiObject && req.pixiObject._mb && req.pixiObject._mb.type;
|
|
671
|
-
isFrameTarget = mbType === 'frame';
|
|
672
|
-
}
|
|
673
|
-
const resizeEndData = {
|
|
674
|
-
object: id,
|
|
675
|
-
oldSize: { width: startWorld.width, height: startWorld.height },
|
|
676
|
-
newSize: { width: worldW, height: worldH },
|
|
677
|
-
oldPosition: { x: startWorld.x, y: startWorld.y },
|
|
678
|
-
newPosition: isFrameTarget ? null : (isEdgeLeftOrTop ? { x: worldX, y: worldY } : { x: startWorld.x, y: startWorld.y })
|
|
679
|
-
};
|
|
191
|
+
_updateGroupRotationPreview(payload = {}) {
|
|
192
|
+
if (!this._groupRotationPreview) return;
|
|
193
|
+
this._groupRotationPreview.angle = (this._groupRotationPreview.baseAngle || 0) + (payload.angle || 0);
|
|
194
|
+
}
|
|
680
195
|
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
const el = textLayer && textLayer.idToEl ? textLayer.idToEl.get && textLayer.idToEl.get(id) : null;
|
|
690
|
-
if (el) {
|
|
691
|
-
el.style.width = `${Math.max(1, Math.round(endCSS.width))}px`;
|
|
692
|
-
el.style.height = 'auto';
|
|
693
|
-
const measured = Math.max(1, Math.round(el.scrollHeight));
|
|
694
|
-
const worldH2 = (measured * res) / s;
|
|
695
|
-
const fixData = {
|
|
696
|
-
object: id,
|
|
697
|
-
size: { width: worldW, height: worldH2 },
|
|
698
|
-
position: isFrameTarget ? null : (isEdgeLeftOrTop ? { x: worldX, y: worldY } : { x: startWorld.x, y: startWorld.y })
|
|
699
|
-
};
|
|
700
|
-
this.eventBus.emit(Events.Tool.ResizeUpdate, fixData);
|
|
701
|
-
}
|
|
702
|
-
}
|
|
703
|
-
} catch (_) {}
|
|
704
|
-
}
|
|
196
|
+
_finishGroupRotationPreview() {
|
|
197
|
+
if (!this._groupRotationPreview) return;
|
|
198
|
+
this._groupRotationPreview.isActive = false;
|
|
199
|
+
const liveBounds = this.positioningService.getGroupSelectionWorldBounds(this._groupRotationPreview.ids);
|
|
200
|
+
if (!liveBounds) return;
|
|
201
|
+
this._groupRotationPreview.lastMeasuredCenter = {
|
|
202
|
+
x: liveBounds.x + liveBounds.width / 2,
|
|
203
|
+
y: liveBounds.y + liveBounds.height / 2,
|
|
705
204
|
};
|
|
706
|
-
document.addEventListener('mousemove', onMove);
|
|
707
|
-
document.addEventListener('mouseup', onUp);
|
|
708
205
|
}
|
|
709
206
|
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
const
|
|
713
|
-
|
|
714
|
-
const
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
const tx = world?.x || 0;
|
|
718
|
-
const ty = world?.y || 0;
|
|
719
|
-
const rendererRes = (this.core.pixi.app.renderer?.resolution) || 1;
|
|
720
|
-
const containerRect = this.container.getBoundingClientRect();
|
|
721
|
-
const view = this.core.pixi.app.view;
|
|
722
|
-
const viewRect = view.getBoundingClientRect();
|
|
723
|
-
const offsetLeft = viewRect.left - containerRect.left;
|
|
724
|
-
const offsetTop = viewRect.top - containerRect.top;
|
|
725
|
-
|
|
726
|
-
const box = e.currentTarget.parentElement;
|
|
727
|
-
const startCSS = {
|
|
728
|
-
left: parseFloat(box.style.left),
|
|
729
|
-
top: parseFloat(box.style.top),
|
|
730
|
-
width: parseFloat(box.style.width),
|
|
731
|
-
height: parseFloat(box.style.height),
|
|
732
|
-
};
|
|
733
|
-
const startScreen = {
|
|
734
|
-
x: (startCSS.left - offsetLeft),
|
|
735
|
-
y: (startCSS.top - offsetTop),
|
|
736
|
-
w: startCSS.width,
|
|
737
|
-
h: startCSS.height,
|
|
738
|
-
};
|
|
739
|
-
const startWorld = {
|
|
740
|
-
x: ((startScreen.x * rendererRes) - tx) / s,
|
|
741
|
-
y: ((startScreen.y * rendererRes) - ty) / s,
|
|
742
|
-
width: (startScreen.w * rendererRes) / s,
|
|
743
|
-
height: (startScreen.h * rendererRes) / s,
|
|
207
|
+
_syncGroupRotationPreviewTranslation() {
|
|
208
|
+
if (!this._groupRotationPreview || this._groupRotationPreview.isActive) return;
|
|
209
|
+
const liveBounds = this.positioningService.getGroupSelectionWorldBounds(this._groupRotationPreview.ids);
|
|
210
|
+
if (!liveBounds) return;
|
|
211
|
+
const liveCenter = {
|
|
212
|
+
x: liveBounds.x + liveBounds.width / 2,
|
|
213
|
+
y: liveBounds.y + liveBounds.height / 2,
|
|
744
214
|
};
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
this.eventBus.emit(Events.Tool.GetSelection, req);
|
|
750
|
-
objects = req.selection || [];
|
|
751
|
-
this.eventBus.emit(Events.Tool.GroupResizeStart, { objects, startBounds: { ...startWorld } });
|
|
752
|
-
} else {
|
|
753
|
-
this.eventBus.emit(Events.Tool.ResizeStart, { object: id, handle: edge === 'top' ? 'n' : edge === 'bottom' ? 's' : edge === 'left' ? 'w' : 'e' });
|
|
215
|
+
const prevCenter = this._groupRotationPreview.lastMeasuredCenter;
|
|
216
|
+
if (prevCenter) {
|
|
217
|
+
this._groupRotationPreview.center.x += liveCenter.x - prevCenter.x;
|
|
218
|
+
this._groupRotationPreview.center.y += liveCenter.y - prevCenter.y;
|
|
754
219
|
}
|
|
220
|
+
this._groupRotationPreview.lastMeasuredCenter = liveCenter;
|
|
221
|
+
}
|
|
755
222
|
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
const mbType = req.pixiObject && req.pixiObject._mb && req.pixiObject._mb.type;
|
|
764
|
-
isTextTarget = (mbType === 'text' || mbType === 'simple-text');
|
|
765
|
-
isNoteTarget = (mbType === 'note');
|
|
766
|
-
}
|
|
767
|
-
const onMove = (ev) => {
|
|
768
|
-
const dxCSS = ev.clientX - startMouse.x;
|
|
769
|
-
const dyCSS = ev.clientY - startMouse.y;
|
|
770
|
-
// Новые CSS-габариты и позиция
|
|
771
|
-
let newLeft = startCSS.left;
|
|
772
|
-
let newTop = startCSS.top;
|
|
773
|
-
let newW = startCSS.width;
|
|
774
|
-
let newH = startCSS.height;
|
|
775
|
-
if (edge === 'right') newW = Math.max(1, startCSS.width + dxCSS);
|
|
776
|
-
if (edge === 'bottom') newH = Math.max(1, startCSS.height + dyCSS);
|
|
777
|
-
if (edge === 'left') {
|
|
778
|
-
newW = Math.max(1, startCSS.width - dxCSS);
|
|
779
|
-
newLeft = startCSS.left + dxCSS;
|
|
780
|
-
}
|
|
781
|
-
if (edge === 'top') {
|
|
782
|
-
newH = Math.max(1, startCSS.height - dyCSS);
|
|
783
|
-
newTop = startCSS.top + dyCSS;
|
|
784
|
-
}
|
|
785
|
-
|
|
786
|
-
// Для записки удерживаем квадрат и компенсируем противоположные стороны
|
|
787
|
-
if (isNoteTarget) {
|
|
788
|
-
const s = Math.max(newW, newH);
|
|
789
|
-
switch (edge) {
|
|
790
|
-
case 'right':
|
|
791
|
-
newW = s; newH = s;
|
|
792
|
-
newTop = startCSS.top + Math.round((startCSS.height - s) / 2);
|
|
793
|
-
break;
|
|
794
|
-
case 'left':
|
|
795
|
-
newW = s; newH = s;
|
|
796
|
-
newLeft = startCSS.left + (startCSS.width - s);
|
|
797
|
-
newTop = startCSS.top + Math.round((startCSS.height - s) / 2);
|
|
798
|
-
break;
|
|
799
|
-
case 'bottom':
|
|
800
|
-
newW = s; newH = s;
|
|
801
|
-
newLeft = startCSS.left + Math.round((startCSS.width - s) / 2);
|
|
802
|
-
break;
|
|
803
|
-
case 'top':
|
|
804
|
-
newW = s; newH = s;
|
|
805
|
-
newTop = startCSS.top + (startCSS.height - s);
|
|
806
|
-
newLeft = startCSS.left + Math.round((startCSS.width - s) / 2);
|
|
807
|
-
break;
|
|
808
|
-
}
|
|
809
|
-
}
|
|
810
|
-
|
|
811
|
-
// Минимальная ширина = ширина трёх символов текущего шрифта
|
|
812
|
-
if (isTextTarget) {
|
|
813
|
-
try {
|
|
814
|
-
const textLayer = (typeof window !== 'undefined') ? window.moodboardHtmlTextLayer : null;
|
|
815
|
-
const el = textLayer && textLayer.idToEl ? textLayer.idToEl.get && textLayer.idToEl.get(id) : null;
|
|
816
|
-
if (el && typeof window.getComputedStyle === 'function') {
|
|
817
|
-
const cs = window.getComputedStyle(el);
|
|
818
|
-
const meas = document.createElement('span');
|
|
819
|
-
meas.style.position = 'absolute';
|
|
820
|
-
meas.style.visibility = 'hidden';
|
|
821
|
-
meas.style.whiteSpace = 'pre';
|
|
822
|
-
meas.style.fontFamily = cs.fontFamily;
|
|
823
|
-
meas.style.fontSize = cs.fontSize;
|
|
824
|
-
meas.style.fontWeight = cs.fontWeight;
|
|
825
|
-
meas.style.fontStyle = cs.fontStyle;
|
|
826
|
-
meas.style.letterSpacing = cs.letterSpacing || 'normal';
|
|
827
|
-
meas.textContent = 'WWW';
|
|
828
|
-
document.body.appendChild(meas);
|
|
829
|
-
const minWidthPx = Math.max(1, Math.ceil(meas.getBoundingClientRect().width));
|
|
830
|
-
meas.remove();
|
|
831
|
-
if (newW < minWidthPx) {
|
|
832
|
-
if (edge === 'left') {
|
|
833
|
-
newLeft = startCSS.left + (startCSS.width - minWidthPx);
|
|
834
|
-
}
|
|
835
|
-
newW = minWidthPx;
|
|
836
|
-
}
|
|
837
|
-
}
|
|
838
|
-
} catch (_) {}
|
|
839
|
-
}
|
|
840
|
-
|
|
841
|
-
// Для текстовых объектов при изменении ширины вычисляем высоту по контенту
|
|
842
|
-
const widthChanged = (edge === 'left' || edge === 'right');
|
|
843
|
-
if (isTextTarget && widthChanged) {
|
|
844
|
-
try {
|
|
845
|
-
const textLayer = (typeof window !== 'undefined') ? window.moodboardHtmlTextLayer : null;
|
|
846
|
-
const el = textLayer && textLayer.idToEl ? textLayer.idToEl.get && textLayer.idToEl.get(id) : null;
|
|
847
|
-
if (el) {
|
|
848
|
-
el.style.width = `${Math.max(1, Math.round(newW))}px`;
|
|
849
|
-
el.style.height = 'auto';
|
|
850
|
-
const measured = Math.max(1, Math.round(el.scrollHeight));
|
|
851
|
-
newH = measured;
|
|
852
|
-
}
|
|
853
|
-
} catch (_) {}
|
|
854
|
-
}
|
|
855
|
-
|
|
856
|
-
// Обновим визуально
|
|
857
|
-
box.style.left = `${newLeft}px`;
|
|
858
|
-
box.style.top = `${newTop}px`;
|
|
859
|
-
box.style.width = `${newW}px`;
|
|
860
|
-
box.style.height = `${newH}px`;
|
|
861
|
-
// Переставим ручки/грани
|
|
862
|
-
this._repositionBoxChildren(box);
|
|
863
|
-
|
|
864
|
-
// Перевод в мировые координаты
|
|
865
|
-
const screenX = (newLeft - offsetLeft);
|
|
866
|
-
const screenY = (newTop - offsetTop);
|
|
867
|
-
const screenW = newW;
|
|
868
|
-
const screenH = newH;
|
|
869
|
-
const worldX = ((screenX * rendererRes) - tx) / s;
|
|
870
|
-
const worldY = ((screenY * rendererRes) - ty) / s;
|
|
871
|
-
const worldW = (screenW * rendererRes) / s;
|
|
872
|
-
const worldH = (screenH * rendererRes) / s;
|
|
873
|
-
|
|
874
|
-
// Определяем, изменилась ли позиция (только для левых/верхних граней)
|
|
875
|
-
const edgePositionChanged = (newLeft !== startCSS.left) || (newTop !== startCSS.top);
|
|
876
|
-
|
|
877
|
-
if (isGroup) {
|
|
878
|
-
this.eventBus.emit(Events.Tool.GroupResizeUpdate, {
|
|
879
|
-
objects,
|
|
880
|
-
startBounds: { ...startWorld },
|
|
881
|
-
newBounds: { x: worldX, y: worldY, width: worldW, height: worldH }
|
|
882
|
-
});
|
|
883
|
-
} else {
|
|
884
|
-
const edgeResizeData = {
|
|
885
|
-
object: id,
|
|
886
|
-
size: { width: worldW, height: worldH },
|
|
887
|
-
position: edgePositionChanged ? { x: worldX, y: worldY } : { x: startWorld.x, y: startWorld.y }
|
|
888
|
-
};
|
|
889
|
-
|
|
890
|
-
this.eventBus.emit(Events.Tool.ResizeUpdate, edgeResizeData);
|
|
891
|
-
}
|
|
892
|
-
};
|
|
893
|
-
const onUp = () => {
|
|
894
|
-
document.removeEventListener('mousemove', onMove);
|
|
895
|
-
document.removeEventListener('mouseup', onUp);
|
|
896
|
-
const endCSS = {
|
|
897
|
-
left: parseFloat(box.style.left),
|
|
898
|
-
top: parseFloat(box.style.top),
|
|
899
|
-
width: parseFloat(box.style.width),
|
|
900
|
-
height: parseFloat(box.style.height),
|
|
223
|
+
_startGroupResizePreview(payload = {}) {
|
|
224
|
+
if (!this._groupRotationPreview) return;
|
|
225
|
+
if (payload.startBounds) {
|
|
226
|
+
this._groupRotationPreview.startBounds = { ...payload.startBounds };
|
|
227
|
+
this._groupRotationPreview.center = {
|
|
228
|
+
x: payload.startBounds.x + payload.startBounds.width / 2,
|
|
229
|
+
y: payload.startBounds.y + payload.startBounds.height / 2,
|
|
901
230
|
};
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
const screenH = endCSS.height;
|
|
906
|
-
const worldX = ((screenX * rendererRes) - tx) / s;
|
|
907
|
-
const worldY = ((screenY * rendererRes) - ty) / s;
|
|
908
|
-
const worldW = (screenW * rendererRes) / s;
|
|
909
|
-
const worldH = (screenH * rendererRes) / s;
|
|
910
|
-
|
|
911
|
-
if (isGroup) {
|
|
912
|
-
this.eventBus.emit(Events.Tool.GroupResizeEnd, { objects });
|
|
913
|
-
} else {
|
|
914
|
-
// Определяем, изменилась ли позиция для краевого ресайза
|
|
915
|
-
const edgeFinalPositionChanged = (endCSS.left !== startCSS.left) || (endCSS.top !== startCSS.top);
|
|
916
|
-
|
|
917
|
-
// Финальная коррекция высоты для текстовых объектов
|
|
918
|
-
let finalWorldH = worldH;
|
|
919
|
-
if (isTextTarget && (edge === 'left' || edge === 'right')) {
|
|
920
|
-
try {
|
|
921
|
-
const textLayer = (typeof window !== 'undefined') ? window.moodboardHtmlTextLayer : null;
|
|
922
|
-
const el = textLayer && textLayer.idToEl ? textLayer.idToEl.get && textLayer.idToEl.get(id) : null;
|
|
923
|
-
if (el) {
|
|
924
|
-
el.style.width = `${Math.max(1, Math.round(endCSS.width))}px`;
|
|
925
|
-
el.style.height = 'auto';
|
|
926
|
-
const measured = Math.max(1, Math.round(el.scrollHeight));
|
|
927
|
-
finalWorldH = (measured * rendererRes) / s;
|
|
928
|
-
}
|
|
929
|
-
} catch (_) {}
|
|
930
|
-
}
|
|
931
|
-
|
|
932
|
-
const edgeResizeEndData = {
|
|
933
|
-
object: id,
|
|
934
|
-
oldSize: { width: startWorld.width, height: startWorld.height },
|
|
935
|
-
newSize: { width: worldW, height: finalWorldH },
|
|
936
|
-
oldPosition: { x: startWorld.x, y: startWorld.y },
|
|
937
|
-
newPosition: edgeFinalPositionChanged ? { x: worldX, y: worldY } : { x: startWorld.x, y: startWorld.y }
|
|
938
|
-
};
|
|
231
|
+
this._groupRotationPreview.lastMeasuredCenter = { ...this._groupRotationPreview.center };
|
|
232
|
+
}
|
|
233
|
+
}
|
|
939
234
|
|
|
940
|
-
|
|
941
|
-
|
|
235
|
+
_updateGroupResizePreview(payload = {}) {
|
|
236
|
+
if (!this._groupRotationPreview || !payload.newBounds) return;
|
|
237
|
+
this._groupRotationPreview.startBounds = { ...payload.newBounds };
|
|
238
|
+
this._groupRotationPreview.center = {
|
|
239
|
+
x: payload.newBounds.x + payload.newBounds.width / 2,
|
|
240
|
+
y: payload.newBounds.y + payload.newBounds.height / 2,
|
|
942
241
|
};
|
|
943
|
-
|
|
944
|
-
document.addEventListener('mouseup', onUp);
|
|
242
|
+
this._groupRotationPreview.lastMeasuredCenter = { ...this._groupRotationPreview.center };
|
|
945
243
|
}
|
|
946
244
|
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
const handleElement = e.currentTarget;
|
|
951
|
-
const id = handleElement?.dataset?.id;
|
|
952
|
-
if (!id) return;
|
|
953
|
-
const isGroup = id === '__group__';
|
|
954
|
-
|
|
955
|
-
// Получаем центр объекта в CSS координатах
|
|
956
|
-
const boxLeft = parseFloat(box.style.left);
|
|
957
|
-
const boxTop = parseFloat(box.style.top);
|
|
958
|
-
const boxWidth = parseFloat(box.style.width);
|
|
959
|
-
const boxHeight = parseFloat(box.style.height);
|
|
960
|
-
const centerX = boxLeft + boxWidth / 2;
|
|
961
|
-
const centerY = boxTop + boxHeight / 2;
|
|
962
|
-
|
|
963
|
-
// Начальный угол от центра объекта до курсора
|
|
964
|
-
const startAngle = Math.atan2(e.clientY - centerY, e.clientX - centerX);
|
|
965
|
-
|
|
966
|
-
// Получаем текущий поворот объекта из состояния
|
|
967
|
-
let startRotation = 0;
|
|
968
|
-
if (!isGroup) {
|
|
969
|
-
const rotationData = { objectId: id, rotation: 0 };
|
|
970
|
-
this.eventBus.emit(Events.Tool.GetObjectRotation, rotationData);
|
|
971
|
-
startRotation = (rotationData.rotation || 0) * Math.PI / 180; // Преобразуем градусы в радианы
|
|
972
|
-
}
|
|
973
|
-
|
|
974
|
-
// Изменяем курсор на grabbing
|
|
975
|
-
if (handleElement) {
|
|
976
|
-
handleElement.style.cursor = 'grabbing';
|
|
977
|
-
}
|
|
978
|
-
|
|
979
|
-
// Уведомляем о начале поворота
|
|
980
|
-
if (isGroup) {
|
|
981
|
-
const req = { selection: [] };
|
|
982
|
-
this.eventBus.emit(Events.Tool.GetSelection, req);
|
|
983
|
-
const objects = req.selection || [];
|
|
984
|
-
this.eventBus.emit(Events.Tool.GroupRotateStart, { objects });
|
|
985
|
-
}
|
|
986
|
-
|
|
987
|
-
const onRotateMove = (ev) => {
|
|
988
|
-
// Вычисляем текущий угол
|
|
989
|
-
const currentAngle = Math.atan2(ev.clientY - centerY, ev.clientX - centerX);
|
|
990
|
-
const deltaAngle = currentAngle - startAngle;
|
|
991
|
-
const newRotation = startRotation + deltaAngle;
|
|
992
|
-
|
|
993
|
-
if (isGroup) {
|
|
994
|
-
const req = { selection: [] };
|
|
995
|
-
this.eventBus.emit(Events.Tool.GetSelection, req);
|
|
996
|
-
const objects = req.selection || [];
|
|
997
|
-
this.eventBus.emit(Events.Tool.GroupRotateUpdate, {
|
|
998
|
-
objects,
|
|
999
|
-
angle: newRotation * 180 / Math.PI // Преобразуем радианы в градусы
|
|
1000
|
-
});
|
|
1001
|
-
} else {
|
|
1002
|
-
this.eventBus.emit(Events.Tool.RotateUpdate, {
|
|
1003
|
-
object: id,
|
|
1004
|
-
angle: newRotation * 180 / Math.PI // Преобразуем радианы в градусы
|
|
1005
|
-
});
|
|
1006
|
-
}
|
|
1007
|
-
};
|
|
1008
|
-
|
|
1009
|
-
const onRotateUp = (ev) => {
|
|
1010
|
-
document.removeEventListener('mousemove', onRotateMove);
|
|
1011
|
-
document.removeEventListener('mouseup', onRotateUp);
|
|
1012
|
-
|
|
1013
|
-
// Возвращаем курсор ручки, если она всё ещё доступна
|
|
1014
|
-
if (handleElement) {
|
|
1015
|
-
handleElement.style.cursor = 'grab';
|
|
1016
|
-
}
|
|
1017
|
-
|
|
1018
|
-
// Вычисляем финальный угол
|
|
1019
|
-
const finalAngle = Math.atan2(ev.clientY - centerY, ev.clientX - centerX);
|
|
1020
|
-
const finalDeltaAngle = finalAngle - startAngle;
|
|
1021
|
-
const finalRotation = startRotation + finalDeltaAngle;
|
|
1022
|
-
|
|
1023
|
-
if (isGroup) {
|
|
1024
|
-
const req = { selection: [] };
|
|
1025
|
-
this.eventBus.emit(Events.Tool.GetSelection, req);
|
|
1026
|
-
const objects = req.selection || [];
|
|
1027
|
-
this.eventBus.emit(Events.Tool.GroupRotateEnd, { objects });
|
|
1028
|
-
} else {
|
|
1029
|
-
this.eventBus.emit(Events.Tool.RotateEnd, {
|
|
1030
|
-
object: id,
|
|
1031
|
-
oldAngle: startRotation * 180 / Math.PI, // Преобразуем радианы в градусы
|
|
1032
|
-
newAngle: finalRotation * 180 / Math.PI // Преобразуем радианы в градусы
|
|
1033
|
-
});
|
|
1034
|
-
}
|
|
1035
|
-
};
|
|
1036
|
-
|
|
1037
|
-
document.addEventListener('mousemove', onRotateMove);
|
|
1038
|
-
document.addEventListener('mouseup', onRotateUp);
|
|
245
|
+
_finishGroupResizePreview() {
|
|
246
|
+
if (!this._groupRotationPreview) return;
|
|
247
|
+
this._groupRotationPreview.lastMeasuredCenter = { ...this._groupRotationPreview.center };
|
|
1039
248
|
}
|
|
1040
249
|
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
const height = parseFloat(box.style.height);
|
|
1044
|
-
const cx = width / 2;
|
|
1045
|
-
const cy = height / 2;
|
|
1046
|
-
|
|
1047
|
-
// Позиционируем все ручки (угловые + боковые)
|
|
1048
|
-
box.querySelectorAll('[data-dir]').forEach(h => {
|
|
1049
|
-
const dir = h.dataset.dir;
|
|
1050
|
-
switch (dir) {
|
|
1051
|
-
// Угловые ручки
|
|
1052
|
-
case 'nw': h.style.left = `${-6}px`; h.style.top = `${-6}px`; break;
|
|
1053
|
-
case 'ne': h.style.left = `${Math.max(-6, width - 6)}px`; h.style.top = `${-6}px`; break;
|
|
1054
|
-
case 'se': h.style.left = `${Math.max(-6, width - 6)}px`; h.style.top = `${Math.max(-6, height - 6)}px`; break;
|
|
1055
|
-
case 'sw': h.style.left = `${-6}px`; h.style.top = `${Math.max(-6, height - 6)}px`; break;
|
|
1056
|
-
// Боковые ручки
|
|
1057
|
-
case 'n': h.style.left = `${cx - 6}px`; h.style.top = `${-6}px`; break;
|
|
1058
|
-
case 'e': h.style.left = `${Math.max(-6, width - 6)}px`; h.style.top = `${cy - 6}px`; break;
|
|
1059
|
-
case 's': h.style.left = `${cx - 6}px`; h.style.top = `${Math.max(-6, height - 6)}px`; break;
|
|
1060
|
-
case 'w': h.style.left = `${-6}px`; h.style.top = `${cy - 6}px`; break;
|
|
1061
|
-
}
|
|
1062
|
-
});
|
|
1063
|
-
|
|
1064
|
-
// Позиционируем невидимые области для захвата с отступами от углов
|
|
1065
|
-
const edgeSize = 10;
|
|
1066
|
-
const cornerGap = 20;
|
|
1067
|
-
const top = box.querySelector('[data-edge="top"]');
|
|
1068
|
-
const bottom = box.querySelector('[data-edge="bottom"]');
|
|
1069
|
-
const left = box.querySelector('[data-edge="left"]');
|
|
1070
|
-
const right = box.querySelector('[data-edge="right"]');
|
|
1071
|
-
|
|
1072
|
-
if (top) Object.assign(top.style, {
|
|
1073
|
-
left: `${cornerGap}px`,
|
|
1074
|
-
top: `-${edgeSize/2}px`,
|
|
1075
|
-
width: `${Math.max(0, width - 2 * cornerGap)}px`,
|
|
1076
|
-
height: `${edgeSize}px`
|
|
1077
|
-
});
|
|
1078
|
-
if (bottom) Object.assign(bottom.style, {
|
|
1079
|
-
left: `${cornerGap}px`,
|
|
1080
|
-
top: `${height - edgeSize/2}px`,
|
|
1081
|
-
width: `${Math.max(0, width - 2 * cornerGap)}px`,
|
|
1082
|
-
height: `${edgeSize}px`
|
|
1083
|
-
});
|
|
1084
|
-
if (left) Object.assign(left.style, {
|
|
1085
|
-
left: `-${edgeSize/2}px`,
|
|
1086
|
-
top: `${cornerGap}px`,
|
|
1087
|
-
width: `${edgeSize}px`,
|
|
1088
|
-
height: `${Math.max(0, height - 2 * cornerGap)}px`
|
|
1089
|
-
});
|
|
1090
|
-
if (right) Object.assign(right.style, {
|
|
1091
|
-
left: `${width - edgeSize/2}px`,
|
|
1092
|
-
top: `${cornerGap}px`,
|
|
1093
|
-
width: `${edgeSize}px`,
|
|
1094
|
-
height: `${Math.max(0, height - 2 * cornerGap)}px`
|
|
1095
|
-
});
|
|
1096
|
-
|
|
1097
|
-
// Позиционируем ручку вращения
|
|
1098
|
-
const rotateHandle = box.querySelector('[data-handle="rotate"]');
|
|
1099
|
-
if (rotateHandle) {
|
|
1100
|
-
const d = 20;
|
|
1101
|
-
const L = Math.max(1, Math.hypot(width, height));
|
|
1102
|
-
const centerX = -(width / L) * d;
|
|
1103
|
-
const centerY = height + (height / L) * d;
|
|
1104
|
-
rotateHandle.style.left = `${Math.round(centerX - 10)}px`;
|
|
1105
|
-
rotateHandle.style.top = `${Math.round(centerY - 10)}px`;
|
|
1106
|
-
}
|
|
250
|
+
_endGroupRotationPreview() {
|
|
251
|
+
this._groupRotationPreview = null;
|
|
1107
252
|
}
|
|
1108
253
|
}
|
|
1109
254
|
|