@sequent-org/moodboard 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +44 -0
- package/src/assets/icons/README.md +105 -0
- package/src/assets/icons/attachments.svg +3 -0
- package/src/assets/icons/clear.svg +5 -0
- package/src/assets/icons/comments.svg +3 -0
- package/src/assets/icons/emoji.svg +6 -0
- package/src/assets/icons/frame.svg +3 -0
- package/src/assets/icons/image.svg +3 -0
- package/src/assets/icons/note.svg +3 -0
- package/src/assets/icons/pan.svg +3 -0
- package/src/assets/icons/pencil.svg +3 -0
- package/src/assets/icons/redo.svg +3 -0
- package/src/assets/icons/select.svg +9 -0
- package/src/assets/icons/shapes.svg +3 -0
- package/src/assets/icons/text-add.svg +3 -0
- package/src/assets/icons/topbar/README.md +39 -0
- package/src/assets/icons/topbar/grid-cross.svg +6 -0
- package/src/assets/icons/topbar/grid-dot.svg +3 -0
- package/src/assets/icons/topbar/grid-line.svg +3 -0
- package/src/assets/icons/topbar/grid-off.svg +3 -0
- package/src/assets/icons/topbar/paint.svg +3 -0
- package/src/assets/icons/undo.svg +3 -0
- package/src/core/ApiClient.js +309 -0
- package/src/core/EventBus.js +42 -0
- package/src/core/HistoryManager.js +261 -0
- package/src/core/KeyboardManager.js +710 -0
- package/src/core/PixiEngine.js +439 -0
- package/src/core/SaveManager.js +381 -0
- package/src/core/StateManager.js +64 -0
- package/src/core/commands/BaseCommand.js +68 -0
- package/src/core/commands/CopyObjectCommand.js +44 -0
- package/src/core/commands/CreateObjectCommand.js +46 -0
- package/src/core/commands/DeleteObjectCommand.js +146 -0
- package/src/core/commands/EditFileNameCommand.js +107 -0
- package/src/core/commands/GroupMoveCommand.js +47 -0
- package/src/core/commands/GroupReorderZCommand.js +74 -0
- package/src/core/commands/GroupResizeCommand.js +37 -0
- package/src/core/commands/GroupRotateCommand.js +41 -0
- package/src/core/commands/MoveObjectCommand.js +89 -0
- package/src/core/commands/PasteObjectCommand.js +103 -0
- package/src/core/commands/ReorderZCommand.js +45 -0
- package/src/core/commands/ResizeObjectCommand.js +135 -0
- package/src/core/commands/RotateObjectCommand.js +70 -0
- package/src/core/commands/index.js +14 -0
- package/src/core/events/Events.js +147 -0
- package/src/core/index.js +1632 -0
- package/src/core/rendering/GeometryUtils.js +89 -0
- package/src/core/rendering/HitTestManager.js +186 -0
- package/src/core/rendering/LayerManager.js +137 -0
- package/src/core/rendering/ObjectRenderer.js +363 -0
- package/src/core/rendering/PixiRenderer.js +140 -0
- package/src/core/rendering/index.js +9 -0
- package/src/grid/BaseGrid.js +164 -0
- package/src/grid/CrossGrid.js +75 -0
- package/src/grid/DotGrid.js +148 -0
- package/src/grid/GridFactory.js +173 -0
- package/src/grid/LineGrid.js +115 -0
- package/src/index.js +2 -0
- package/src/moodboard/ActionHandler.js +114 -0
- package/src/moodboard/DataManager.js +114 -0
- package/src/moodboard/MoodBoard.js +359 -0
- package/src/moodboard/WorkspaceManager.js +103 -0
- package/src/objects/BaseObject.js +1 -0
- package/src/objects/CommentObject.js +115 -0
- package/src/objects/DrawingObject.js +114 -0
- package/src/objects/EmojiObject.js +98 -0
- package/src/objects/FileObject.js +318 -0
- package/src/objects/FrameObject.js +127 -0
- package/src/objects/ImageObject.js +72 -0
- package/src/objects/NoteObject.js +227 -0
- package/src/objects/ObjectFactory.js +61 -0
- package/src/objects/ShapeObject.js +134 -0
- package/src/objects/StampObject.js +0 -0
- package/src/objects/StickerObject.js +0 -0
- package/src/objects/TextObject.js +123 -0
- package/src/services/BoardService.js +85 -0
- package/src/services/FileUploadService.js +398 -0
- package/src/services/FrameService.js +138 -0
- package/src/services/ImageUploadService.js +246 -0
- package/src/services/ZOrderManager.js +50 -0
- package/src/services/ZoomPanController.js +78 -0
- package/src/src.7z +0 -0
- package/src/src.zip +0 -0
- package/src/src2.zip +0 -0
- package/src/tools/AlignmentGuides.js +326 -0
- package/src/tools/BaseTool.js +257 -0
- package/src/tools/ResizeHandles.js +381 -0
- package/src/tools/ToolManager.js +580 -0
- package/src/tools/board-tools/PanTool.js +43 -0
- package/src/tools/board-tools/ZoomTool.js +393 -0
- package/src/tools/object-tools/DrawingTool.js +404 -0
- package/src/tools/object-tools/PlacementTool.js +1005 -0
- package/src/tools/object-tools/SelectTool.js +2183 -0
- package/src/tools/object-tools/TextTool.js +416 -0
- package/src/tools/object-tools/selection/BoxSelectController.js +105 -0
- package/src/tools/object-tools/selection/GeometryUtils.js +101 -0
- package/src/tools/object-tools/selection/GroupDragController.js +61 -0
- package/src/tools/object-tools/selection/GroupResizeController.js +90 -0
- package/src/tools/object-tools/selection/GroupRotateController.js +61 -0
- package/src/tools/object-tools/selection/HandlesSync.js +96 -0
- package/src/tools/object-tools/selection/ResizeController.js +68 -0
- package/src/tools/object-tools/selection/RotateController.js +58 -0
- package/src/tools/object-tools/selection/SelectionModel.js +42 -0
- package/src/tools/object-tools/selection/SimpleDragController.js +45 -0
- package/src/ui/CommentPopover.js +187 -0
- package/src/ui/ContextMenu.js +340 -0
- package/src/ui/FilePropertiesPanel.js +298 -0
- package/src/ui/FramePropertiesPanel.js +462 -0
- package/src/ui/HtmlHandlesLayer.js +778 -0
- package/src/ui/HtmlTextLayer.js +279 -0
- package/src/ui/MapPanel.js +290 -0
- package/src/ui/NotePropertiesPanel.js +502 -0
- package/src/ui/SaveStatus.js +250 -0
- package/src/ui/TextPropertiesPanel.js +911 -0
- package/src/ui/Toolbar.js +1118 -0
- package/src/ui/Topbar.js +220 -0
- package/src/ui/ZoomPanel.js +116 -0
- package/src/ui/styles/workspace.css +854 -0
- package/src/utils/colors.js +0 -0
- package/src/utils/geometry.js +0 -0
- package/src/utils/iconLoader.js +270 -0
- package/src/utils/objectIdGenerator.js +17 -0
- package/src/utils/topbarIconLoader.js +114 -0
|
@@ -0,0 +1,778 @@
|
|
|
1
|
+
import { Events } from '../core/events/Events.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* HtmlHandlesLayer — HTML-ручки и рамка для выделенных объектов.
|
|
5
|
+
*
|
|
6
|
+
* ✅ АКТИВНО ИСПОЛЬЗУЕТСЯ ✅
|
|
7
|
+
* Это основная система ручек ресайза в приложении.
|
|
8
|
+
* Показывает ручки для одного объекта или группы, синхронизирует с worldLayer.
|
|
9
|
+
* Эмитит те же события, что и Pixi ResizeHandles через EventBus.
|
|
10
|
+
*
|
|
11
|
+
* Альтернатива: ResizeHandles.js (PIXI-ручки, в данный момент не используются)
|
|
12
|
+
*/
|
|
13
|
+
export class HtmlHandlesLayer {
|
|
14
|
+
constructor(container, eventBus, core) {
|
|
15
|
+
this.container = container;
|
|
16
|
+
this.eventBus = eventBus;
|
|
17
|
+
this.core = core;
|
|
18
|
+
this.layer = null;
|
|
19
|
+
this.visible = false;
|
|
20
|
+
this.target = { type: 'none', id: null, bounds: null };
|
|
21
|
+
this.handles = {};
|
|
22
|
+
this._drag = null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
attach() {
|
|
26
|
+
this.layer = document.createElement('div');
|
|
27
|
+
this.layer.className = 'moodboard-html-handles';
|
|
28
|
+
Object.assign(this.layer.style, {
|
|
29
|
+
position: 'absolute', inset: '0', pointerEvents: 'none', zIndex: 12,
|
|
30
|
+
});
|
|
31
|
+
this.container.appendChild(this.layer);
|
|
32
|
+
|
|
33
|
+
// Подписки: обновлять при изменениях выбора и трансформациях
|
|
34
|
+
this.eventBus.on(Events.Tool.SelectionAdd, () => this.update());
|
|
35
|
+
this.eventBus.on(Events.Tool.SelectionRemove, () => this.update());
|
|
36
|
+
this.eventBus.on(Events.Tool.SelectionClear, () => this.hide());
|
|
37
|
+
this.eventBus.on(Events.Tool.DragUpdate, () => this.update());
|
|
38
|
+
this.eventBus.on(Events.Tool.ResizeUpdate, () => this.update());
|
|
39
|
+
this.eventBus.on(Events.Tool.RotateUpdate, () => this.update());
|
|
40
|
+
this.eventBus.on(Events.Tool.GroupDragUpdate, () => this.update());
|
|
41
|
+
this.eventBus.on(Events.Tool.GroupResizeUpdate, () => this.update());
|
|
42
|
+
this.eventBus.on(Events.Tool.GroupRotateUpdate, () => this.update());
|
|
43
|
+
this.eventBus.on(Events.UI.ZoomPercent, () => this.update());
|
|
44
|
+
this.eventBus.on(Events.Tool.PanUpdate, () => this.update());
|
|
45
|
+
|
|
46
|
+
this.update();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
destroy() {
|
|
50
|
+
if (this.layer) this.layer.remove();
|
|
51
|
+
this.layer = null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
update() {
|
|
55
|
+
if (!this.core) return;
|
|
56
|
+
const selectTool = this.core?.selectTool;
|
|
57
|
+
const ids = selectTool ? Array.from(selectTool.selectedObjects || []) : [];
|
|
58
|
+
if (!ids || ids.length === 0) { this.hide(); return; }
|
|
59
|
+
if (ids.length === 1) {
|
|
60
|
+
const id = ids[0];
|
|
61
|
+
const pixi = this.core.pixi.objects.get(id);
|
|
62
|
+
if (!pixi) { this.hide(); return; }
|
|
63
|
+
// Не показываем рамку/ручки для комментариев
|
|
64
|
+
const mb = pixi._mb || {};
|
|
65
|
+
if (mb.type === 'comment') { this.hide(); return; }
|
|
66
|
+
|
|
67
|
+
// Получаем данные объекта через события (избегаем проблем с глобальными границами)
|
|
68
|
+
const positionData = { objectId: id, position: null };
|
|
69
|
+
const sizeData = { objectId: id, size: null };
|
|
70
|
+
this.eventBus.emit(Events.Tool.GetObjectPosition, positionData);
|
|
71
|
+
this.eventBus.emit(Events.Tool.GetObjectSize, sizeData);
|
|
72
|
+
|
|
73
|
+
if (positionData.position && sizeData.size) {
|
|
74
|
+
// Используем данные из состояния вместо getBounds() для избежания масштабирования
|
|
75
|
+
this._showBounds({
|
|
76
|
+
x: positionData.position.x,
|
|
77
|
+
y: positionData.position.y,
|
|
78
|
+
width: sizeData.size.width,
|
|
79
|
+
height: sizeData.size.height
|
|
80
|
+
}, id);
|
|
81
|
+
} else {
|
|
82
|
+
// Fallback к getBounds() если события не сработали
|
|
83
|
+
const b = pixi.getBounds();
|
|
84
|
+
this._showBounds({ x: b.x, y: b.y, width: b.width, height: b.height }, id);
|
|
85
|
+
}
|
|
86
|
+
} else {
|
|
87
|
+
// Группа: вычислим общий bbox по PIXI
|
|
88
|
+
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
|
89
|
+
ids.forEach(id => {
|
|
90
|
+
const p = this.core.pixi.objects.get(id);
|
|
91
|
+
if (!p) return;
|
|
92
|
+
const b = p.getBounds();
|
|
93
|
+
minX = Math.min(minX, b.x);
|
|
94
|
+
minY = Math.min(minY, b.y);
|
|
95
|
+
maxX = Math.max(maxX, b.x + b.width);
|
|
96
|
+
maxY = Math.max(maxY, b.y + b.height);
|
|
97
|
+
});
|
|
98
|
+
if (!isFinite(minX)) { this.hide(); return; }
|
|
99
|
+
this._showBounds({ x: minX, y: minY, width: maxX - minX, height: maxY - minY }, '__group__');
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
hide() {
|
|
104
|
+
if (!this.layer) return;
|
|
105
|
+
this.layer.innerHTML = '';
|
|
106
|
+
this.visible = false;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
_showBounds(worldBounds, id) {
|
|
110
|
+
if (!this.layer) return;
|
|
111
|
+
// Преобразуем world координаты в CSS-пиксели
|
|
112
|
+
const res = (this.core.pixi.app.renderer?.resolution) || 1;
|
|
113
|
+
const view = this.core.pixi.app.view;
|
|
114
|
+
const containerRect = this.container.getBoundingClientRect();
|
|
115
|
+
const viewRect = view.getBoundingClientRect();
|
|
116
|
+
const offsetLeft = viewRect.left - containerRect.left;
|
|
117
|
+
const offsetTop = viewRect.top - containerRect.top;
|
|
118
|
+
|
|
119
|
+
// Получаем масштаб world layer для правильного преобразования
|
|
120
|
+
const world = this.core.pixi.worldLayer || this.core.pixi.app.stage;
|
|
121
|
+
const worldScale = world?.scale?.x || 1;
|
|
122
|
+
const worldX = world?.x || 0;
|
|
123
|
+
const worldY = world?.y || 0;
|
|
124
|
+
|
|
125
|
+
// Вычисляем позицию и размер в CSS координатах
|
|
126
|
+
const cssX = offsetLeft + (worldX + worldBounds.x * worldScale) / res;
|
|
127
|
+
const cssY = offsetTop + (worldY + worldBounds.y * worldScale) / res;
|
|
128
|
+
const cssWidth = Math.max(1, (worldBounds.width * worldScale) / res);
|
|
129
|
+
const cssHeight = Math.max(1, (worldBounds.height * worldScale) / res);
|
|
130
|
+
|
|
131
|
+
const left = cssX;
|
|
132
|
+
const top = cssY;
|
|
133
|
+
const width = cssWidth;
|
|
134
|
+
const height = cssHeight;
|
|
135
|
+
|
|
136
|
+
this.layer.innerHTML = '';
|
|
137
|
+
const box = document.createElement('div');
|
|
138
|
+
box.className = 'mb-handles-box';
|
|
139
|
+
|
|
140
|
+
// Получаем угол поворота объекта для поворота рамки
|
|
141
|
+
let rotation = 0;
|
|
142
|
+
if (id !== '__group__') {
|
|
143
|
+
const rotationData = { objectId: id, rotation: 0 };
|
|
144
|
+
this.eventBus.emit(Events.Tool.GetObjectRotation, rotationData);
|
|
145
|
+
rotation = rotationData.rotation || 0; // В градусах
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
Object.assign(box.style, {
|
|
149
|
+
position: 'absolute', left: `${left}px`, top: `${top}px`,
|
|
150
|
+
width: `${width}px`, height: `${height}px`,
|
|
151
|
+
border: '1px solid #1DE9B6', boxSizing: 'border-box', pointerEvents: 'none',
|
|
152
|
+
transformOrigin: 'center center', // Поворот вокруг центра
|
|
153
|
+
transform: `rotate(${rotation}deg)` // Применяем поворот
|
|
154
|
+
});
|
|
155
|
+
this.layer.appendChild(box);
|
|
156
|
+
|
|
157
|
+
// Угловые ручки для ресайза - круглые с мятно-зелёным цветом и белой серединой
|
|
158
|
+
const mkCorner = (dir, x, y, cursor) => {
|
|
159
|
+
const h = document.createElement('div');
|
|
160
|
+
h.dataset.dir = dir; h.dataset.id = id;
|
|
161
|
+
Object.assign(h.style, {
|
|
162
|
+
position: 'absolute', width: '12px', height: '12px',
|
|
163
|
+
background: '#1DE9B6',
|
|
164
|
+
border: '2px solid #1DE9B6',
|
|
165
|
+
borderRadius: '50%', // Делаем круглыми
|
|
166
|
+
boxSizing: 'border-box',
|
|
167
|
+
pointerEvents: 'auto',
|
|
168
|
+
zIndex: 10, // Увеличиваем z-index
|
|
169
|
+
cursor: cursor
|
|
170
|
+
});
|
|
171
|
+
h.style.left = `${x - 6}px`;
|
|
172
|
+
h.style.top = `${y - 6}px`;
|
|
173
|
+
|
|
174
|
+
// Создаем внутренний белый круг
|
|
175
|
+
const inner = document.createElement('div');
|
|
176
|
+
Object.assign(inner.style, {
|
|
177
|
+
position: 'absolute',
|
|
178
|
+
top: '1px', left: '1px',
|
|
179
|
+
width: '6px', height: '6px',
|
|
180
|
+
background: '#fff',
|
|
181
|
+
borderRadius: '50%',
|
|
182
|
+
pointerEvents: 'none', // Важно: не блокируем события
|
|
183
|
+
zIndex: 1
|
|
184
|
+
});
|
|
185
|
+
h.appendChild(inner);
|
|
186
|
+
|
|
187
|
+
// Эффект при наведении
|
|
188
|
+
h.addEventListener('mouseenter', () => {
|
|
189
|
+
h.style.background = '#17C29A';
|
|
190
|
+
h.style.borderColor = '#17C29A';
|
|
191
|
+
h.style.cursor = cursor; // Принудительно устанавливаем курсор
|
|
192
|
+
});
|
|
193
|
+
h.addEventListener('mouseleave', () => {
|
|
194
|
+
h.style.background = '#1DE9B6';
|
|
195
|
+
h.style.borderColor = '#1DE9B6';
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
h.addEventListener('mousedown', (e) => this._onHandleDown(e, box));
|
|
199
|
+
|
|
200
|
+
box.appendChild(h);
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
const x0 = 0, y0 = 0, x1 = width, y1 = height, cx = width / 2, cy = height / 2;
|
|
204
|
+
mkCorner('nw', x0, y0, 'nwse-resize');
|
|
205
|
+
mkCorner('ne', x1, y0, 'nesw-resize');
|
|
206
|
+
mkCorner('se', x1, y1, 'nwse-resize');
|
|
207
|
+
mkCorner('sw', x0, y1, 'nesw-resize');
|
|
208
|
+
|
|
209
|
+
// Боковые ручки (видимые круглые ручки на серединах сторон)
|
|
210
|
+
mkCorner('n', cx, y0, 'ns-resize'); // верхняя
|
|
211
|
+
mkCorner('e', x1, cy, 'ew-resize'); // правая
|
|
212
|
+
mkCorner('s', cx, y1, 'ns-resize'); // нижняя
|
|
213
|
+
mkCorner('w', x0, cy, 'ew-resize'); // левая
|
|
214
|
+
|
|
215
|
+
// Кликабельные грани для ресайза (невидимые области для лучшего UX)
|
|
216
|
+
// Уменьшаем их, чтобы не перекрывать угловые ручки
|
|
217
|
+
const edgeSize = 10; // уменьшаем размер
|
|
218
|
+
const makeEdge = (name, style, cursor) => {
|
|
219
|
+
const e = document.createElement('div');
|
|
220
|
+
e.dataset.edge = name; e.dataset.id = id;
|
|
221
|
+
Object.assign(e.style, style, {
|
|
222
|
+
position: 'absolute', pointerEvents: 'auto', cursor,
|
|
223
|
+
zIndex: 5, // Меньше чем у ручек (10)
|
|
224
|
+
background: 'transparent' // невидимые области
|
|
225
|
+
});
|
|
226
|
+
e.addEventListener('mousedown', (evt) => this._onEdgeResizeDown(evt));
|
|
227
|
+
box.appendChild(e);
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
// Создаем грани с отступами от углов, чтобы не мешать угловым ручкам
|
|
231
|
+
const cornerGap = 20; // отступ от углов
|
|
232
|
+
|
|
233
|
+
// top - с отступами от углов
|
|
234
|
+
makeEdge('top', {
|
|
235
|
+
left: `${cornerGap}px`,
|
|
236
|
+
top: `-${edgeSize/2}px`,
|
|
237
|
+
width: `${Math.max(0, width - 2 * cornerGap)}px`,
|
|
238
|
+
height: `${edgeSize}px`
|
|
239
|
+
}, 'ns-resize');
|
|
240
|
+
|
|
241
|
+
// bottom - с отступами от углов
|
|
242
|
+
makeEdge('bottom', {
|
|
243
|
+
left: `${cornerGap}px`,
|
|
244
|
+
top: `${height - edgeSize/2}px`,
|
|
245
|
+
width: `${Math.max(0, width - 2 * cornerGap)}px`,
|
|
246
|
+
height: `${edgeSize}px`
|
|
247
|
+
}, 'ns-resize');
|
|
248
|
+
|
|
249
|
+
// left - с отступами от углов
|
|
250
|
+
makeEdge('left', {
|
|
251
|
+
left: `-${edgeSize/2}px`,
|
|
252
|
+
top: `${cornerGap}px`,
|
|
253
|
+
width: `${edgeSize}px`,
|
|
254
|
+
height: `${Math.max(0, height - 2 * cornerGap)}px`
|
|
255
|
+
}, 'ew-resize');
|
|
256
|
+
|
|
257
|
+
// right - с отступами от углов
|
|
258
|
+
makeEdge('right', {
|
|
259
|
+
left: `${width - edgeSize/2}px`,
|
|
260
|
+
top: `${cornerGap}px`,
|
|
261
|
+
width: `${edgeSize}px`,
|
|
262
|
+
height: `${Math.max(0, height - 2 * cornerGap)}px`
|
|
263
|
+
}, 'ew-resize');
|
|
264
|
+
|
|
265
|
+
// Ручка вращения - зеленый круг с символом ↻ возле левого нижнего угла
|
|
266
|
+
const rotateHandle = document.createElement('div');
|
|
267
|
+
rotateHandle.dataset.handle = 'rotate';
|
|
268
|
+
rotateHandle.dataset.id = id;
|
|
269
|
+
Object.assign(rotateHandle.style, {
|
|
270
|
+
position: 'absolute',
|
|
271
|
+
width: '20px', height: '20px',
|
|
272
|
+
background: '#28A745',
|
|
273
|
+
border: '2px solid #fff',
|
|
274
|
+
borderRadius: '50%',
|
|
275
|
+
boxSizing: 'border-box',
|
|
276
|
+
pointerEvents: 'auto',
|
|
277
|
+
cursor: 'grab',
|
|
278
|
+
zIndex: 15, // Выше ручек ресайза
|
|
279
|
+
display: 'flex',
|
|
280
|
+
alignItems: 'center',
|
|
281
|
+
justifyContent: 'center',
|
|
282
|
+
fontSize: '12px',
|
|
283
|
+
color: '#fff',
|
|
284
|
+
fontWeight: 'bold',
|
|
285
|
+
userSelect: 'none'
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
// Позиционируем возле левого нижнего угла с отступом
|
|
289
|
+
rotateHandle.style.left = `${0 - 10}px`; // центрируем относительно угла
|
|
290
|
+
rotateHandle.style.top = `${height + 25 - 10}px`; // отступ 25px от нижней грани
|
|
291
|
+
|
|
292
|
+
// Добавляем символ вращения
|
|
293
|
+
rotateHandle.innerHTML = '↻';
|
|
294
|
+
|
|
295
|
+
// Эффекты при наведении
|
|
296
|
+
rotateHandle.addEventListener('mouseenter', () => {
|
|
297
|
+
rotateHandle.style.background = '#34CE57';
|
|
298
|
+
rotateHandle.style.cursor = 'grab';
|
|
299
|
+
});
|
|
300
|
+
rotateHandle.addEventListener('mouseleave', () => {
|
|
301
|
+
rotateHandle.style.background = '#28A745';
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
// Обработчик вращения
|
|
305
|
+
rotateHandle.addEventListener('mousedown', (e) => this._onRotateHandleDown(e, box));
|
|
306
|
+
|
|
307
|
+
box.appendChild(rotateHandle);
|
|
308
|
+
|
|
309
|
+
this.visible = true;
|
|
310
|
+
this.target = { type: id === '__group__' ? 'group' : 'single', id, bounds: worldBounds };
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
_toWorldScreenInverse(dx, dy) {
|
|
314
|
+
const world = this.core.pixi.worldLayer || this.core.pixi.app.stage;
|
|
315
|
+
const s = world?.scale?.x || 1;
|
|
316
|
+
const res = (this.core.pixi.app.renderer?.resolution) || 1;
|
|
317
|
+
return { dxWorld: (dx * res) / s, dyWorld: (dy * res) / s };
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
_onHandleDown(e, box) {
|
|
321
|
+
e.preventDefault(); e.stopPropagation();
|
|
322
|
+
const dir = e.currentTarget.dataset.dir;
|
|
323
|
+
const id = e.currentTarget.dataset.id;
|
|
324
|
+
const isGroup = id === '__group__';
|
|
325
|
+
const world = this.core.pixi.worldLayer || this.core.pixi.app.stage;
|
|
326
|
+
const s = world?.scale?.x || 1;
|
|
327
|
+
const tx = world?.x || 0;
|
|
328
|
+
const ty = world?.y || 0;
|
|
329
|
+
const res = (this.core.pixi.app.renderer?.resolution) || 1;
|
|
330
|
+
const view = this.core.pixi.app.view;
|
|
331
|
+
const containerRect = this.container.getBoundingClientRect();
|
|
332
|
+
const viewRect = view.getBoundingClientRect();
|
|
333
|
+
const offsetLeft = viewRect.left - containerRect.left;
|
|
334
|
+
const offsetTop = viewRect.top - containerRect.top;
|
|
335
|
+
|
|
336
|
+
const startCSS = {
|
|
337
|
+
left: parseFloat(box.style.left),
|
|
338
|
+
top: parseFloat(box.style.top),
|
|
339
|
+
width: parseFloat(box.style.width),
|
|
340
|
+
height: parseFloat(box.style.height),
|
|
341
|
+
};
|
|
342
|
+
const startScreen = {
|
|
343
|
+
x: (startCSS.left - offsetLeft) * res,
|
|
344
|
+
y: (startCSS.top - offsetTop) * res,
|
|
345
|
+
w: startCSS.width * res,
|
|
346
|
+
h: startCSS.height * res,
|
|
347
|
+
};
|
|
348
|
+
const startWorld = {
|
|
349
|
+
x: (startScreen.x - tx) / s,
|
|
350
|
+
y: (startScreen.y - ty) / s,
|
|
351
|
+
width: startScreen.w / s,
|
|
352
|
+
height: startScreen.h / s,
|
|
353
|
+
};
|
|
354
|
+
|
|
355
|
+
let objects = [id];
|
|
356
|
+
if (isGroup) {
|
|
357
|
+
const req = { selection: [] };
|
|
358
|
+
this.eventBus.emit(Events.Tool.GetSelection, req);
|
|
359
|
+
objects = req.selection || [];
|
|
360
|
+
// Сообщаем ядру старт группового ресайза
|
|
361
|
+
this.eventBus.emit(Events.Tool.GroupResizeStart, { objects, startBounds: { ...startWorld } });
|
|
362
|
+
} else {
|
|
363
|
+
// Сигнал о старте одиночного ресайза
|
|
364
|
+
this.eventBus.emit(Events.Tool.ResizeStart, { object: id, handle: dir });
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
const startMouse = { x: e.clientX, y: e.clientY };
|
|
368
|
+
const onMove = (ev) => {
|
|
369
|
+
const dx = ev.clientX - startMouse.x;
|
|
370
|
+
const dy = ev.clientY - startMouse.y;
|
|
371
|
+
// Новые CSS-габариты и позиция
|
|
372
|
+
let newLeft = startCSS.left;
|
|
373
|
+
let newTop = startCSS.top;
|
|
374
|
+
let newW = startCSS.width;
|
|
375
|
+
let newH = startCSS.height;
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
if (dir.includes('e')) newW = Math.max(1, startCSS.width + dx);
|
|
380
|
+
if (dir.includes('s')) newH = Math.max(1, startCSS.height + dy);
|
|
381
|
+
if (dir.includes('w')) {
|
|
382
|
+
newW = Math.max(1, startCSS.width - dx);
|
|
383
|
+
newLeft = startCSS.left + dx;
|
|
384
|
+
}
|
|
385
|
+
if (dir.includes('n')) {
|
|
386
|
+
newH = Math.max(1, startCSS.height - dy);
|
|
387
|
+
newTop = startCSS.top + dy;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// Обновим визуально
|
|
391
|
+
box.style.left = `${newLeft}px`;
|
|
392
|
+
box.style.top = `${newTop}px`;
|
|
393
|
+
box.style.width = `${newW}px`;
|
|
394
|
+
box.style.height = `${newH}px`;
|
|
395
|
+
// Переставим ручки без перестроения слоя
|
|
396
|
+
this._repositionBoxChildren(box);
|
|
397
|
+
|
|
398
|
+
// Перевод в мировые координаты
|
|
399
|
+
const screenX = (newLeft - offsetLeft) * res;
|
|
400
|
+
const screenY = (newTop - offsetTop) * res;
|
|
401
|
+
const screenW = newW * res;
|
|
402
|
+
const screenH = newH * res;
|
|
403
|
+
const worldX = (screenX - tx) / s;
|
|
404
|
+
const worldY = (screenY - ty) / s;
|
|
405
|
+
const worldW = screenW / s;
|
|
406
|
+
const worldH = screenH / s;
|
|
407
|
+
|
|
408
|
+
// Определяем, изменилась ли позиция (только для левых/верхних ручек)
|
|
409
|
+
const positionChanged = (newLeft !== startCSS.left) || (newTop !== startCSS.top);
|
|
410
|
+
|
|
411
|
+
if (isGroup) {
|
|
412
|
+
this.eventBus.emit(Events.Tool.GroupResizeUpdate, {
|
|
413
|
+
objects,
|
|
414
|
+
startBounds: { ...startWorld },
|
|
415
|
+
newBounds: { x: worldX, y: worldY, width: worldW, height: worldH }
|
|
416
|
+
});
|
|
417
|
+
} else {
|
|
418
|
+
const resizeData = {
|
|
419
|
+
object: id,
|
|
420
|
+
size: { width: worldW, height: worldH }
|
|
421
|
+
};
|
|
422
|
+
|
|
423
|
+
// Отправляем позицию только если она действительно изменилась
|
|
424
|
+
if (positionChanged) {
|
|
425
|
+
resizeData.position = { x: worldX, y: worldY };
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
this.eventBus.emit(Events.Tool.ResizeUpdate, resizeData);
|
|
429
|
+
}
|
|
430
|
+
};
|
|
431
|
+
const onUp = () => {
|
|
432
|
+
document.removeEventListener('mousemove', onMove);
|
|
433
|
+
document.removeEventListener('mouseup', onUp);
|
|
434
|
+
// Финализация
|
|
435
|
+
const endCSS = {
|
|
436
|
+
left: parseFloat(box.style.left),
|
|
437
|
+
top: parseFloat(box.style.top),
|
|
438
|
+
width: parseFloat(box.style.width),
|
|
439
|
+
height: parseFloat(box.style.height),
|
|
440
|
+
};
|
|
441
|
+
const screenX = (endCSS.left - offsetLeft) * res;
|
|
442
|
+
const screenY = (endCSS.top - offsetTop) * res;
|
|
443
|
+
const screenW = endCSS.width * res;
|
|
444
|
+
const screenH = endCSS.height * res;
|
|
445
|
+
const worldX = (screenX - tx) / s;
|
|
446
|
+
const worldY = (screenY - ty) / s;
|
|
447
|
+
const worldW = screenW / s;
|
|
448
|
+
const worldH = screenH / s;
|
|
449
|
+
|
|
450
|
+
if (isGroup) {
|
|
451
|
+
this.eventBus.emit(Events.Tool.GroupResizeEnd, { objects });
|
|
452
|
+
} else {
|
|
453
|
+
// Определяем, изменилась ли позиция
|
|
454
|
+
const finalPositionChanged = (endCSS.left !== startCSS.left) || (endCSS.top !== startCSS.top);
|
|
455
|
+
|
|
456
|
+
const resizeEndData = {
|
|
457
|
+
object: id,
|
|
458
|
+
oldSize: { width: startWorld.width, height: startWorld.height },
|
|
459
|
+
newSize: { width: worldW, height: worldH }
|
|
460
|
+
};
|
|
461
|
+
|
|
462
|
+
// Добавляем информацию о позиции только если она действительно изменилась
|
|
463
|
+
if (finalPositionChanged) {
|
|
464
|
+
resizeEndData.oldPosition = { x: startWorld.x, y: startWorld.y };
|
|
465
|
+
resizeEndData.newPosition = { x: worldX, y: worldY };
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
this.eventBus.emit(Events.Tool.ResizeEnd, resizeEndData);
|
|
469
|
+
}
|
|
470
|
+
};
|
|
471
|
+
document.addEventListener('mousemove', onMove);
|
|
472
|
+
document.addEventListener('mouseup', onUp);
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
_onEdgeResizeDown(e) {
|
|
476
|
+
e.preventDefault(); e.stopPropagation();
|
|
477
|
+
const id = e.currentTarget.dataset.id;
|
|
478
|
+
const isGroup = id === '__group__';
|
|
479
|
+
const edge = e.currentTarget.dataset.edge;
|
|
480
|
+
const world = this.core.pixi.worldLayer || this.core.pixi.app.stage;
|
|
481
|
+
const s = world?.scale?.x || 1;
|
|
482
|
+
const tx = world?.x || 0;
|
|
483
|
+
const ty = world?.y || 0;
|
|
484
|
+
const res = (this.core.pixi.app.renderer?.resolution) || 1;
|
|
485
|
+
const view = this.core.pixi.app.view;
|
|
486
|
+
const containerRect = this.container.getBoundingClientRect();
|
|
487
|
+
const viewRect = view.getBoundingClientRect();
|
|
488
|
+
const offsetLeft = viewRect.left - containerRect.left;
|
|
489
|
+
const offsetTop = viewRect.top - containerRect.top;
|
|
490
|
+
|
|
491
|
+
const box = e.currentTarget.parentElement;
|
|
492
|
+
const startCSS = {
|
|
493
|
+
left: parseFloat(box.style.left),
|
|
494
|
+
top: parseFloat(box.style.top),
|
|
495
|
+
width: parseFloat(box.style.width),
|
|
496
|
+
height: parseFloat(box.style.height),
|
|
497
|
+
};
|
|
498
|
+
const startScreen = {
|
|
499
|
+
x: (startCSS.left - offsetLeft) * res,
|
|
500
|
+
y: (startCSS.top - offsetTop) * res,
|
|
501
|
+
w: startCSS.width * res,
|
|
502
|
+
h: startCSS.height * res,
|
|
503
|
+
};
|
|
504
|
+
const startWorld = {
|
|
505
|
+
x: (startScreen.x - tx) / s,
|
|
506
|
+
y: (startScreen.y - ty) / s,
|
|
507
|
+
width: startScreen.w / s,
|
|
508
|
+
height: startScreen.h / s,
|
|
509
|
+
};
|
|
510
|
+
|
|
511
|
+
let objects = [id];
|
|
512
|
+
if (isGroup) {
|
|
513
|
+
const req = { selection: [] };
|
|
514
|
+
this.eventBus.emit(Events.Tool.GetSelection, req);
|
|
515
|
+
objects = req.selection || [];
|
|
516
|
+
this.eventBus.emit(Events.Tool.GroupResizeStart, { objects, startBounds: { ...startWorld } });
|
|
517
|
+
} else {
|
|
518
|
+
this.eventBus.emit(Events.Tool.ResizeStart, { object: id, handle: edge === 'top' ? 'n' : edge === 'bottom' ? 's' : edge === 'left' ? 'w' : 'e' });
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
const startMouse = { x: e.clientX, y: e.clientY };
|
|
522
|
+
const onMove = (ev) => {
|
|
523
|
+
const dxCSS = ev.clientX - startMouse.x;
|
|
524
|
+
const dyCSS = ev.clientY - startMouse.y;
|
|
525
|
+
// Новые CSS-габариты и позиция
|
|
526
|
+
let newLeft = startCSS.left;
|
|
527
|
+
let newTop = startCSS.top;
|
|
528
|
+
let newW = startCSS.width;
|
|
529
|
+
let newH = startCSS.height;
|
|
530
|
+
if (edge === 'right') newW = Math.max(1, startCSS.width + dxCSS);
|
|
531
|
+
if (edge === 'bottom') newH = Math.max(1, startCSS.height + dyCSS);
|
|
532
|
+
if (edge === 'left') {
|
|
533
|
+
newW = Math.max(1, startCSS.width - dxCSS);
|
|
534
|
+
newLeft = startCSS.left + dxCSS;
|
|
535
|
+
}
|
|
536
|
+
if (edge === 'top') {
|
|
537
|
+
newH = Math.max(1, startCSS.height - dyCSS);
|
|
538
|
+
newTop = startCSS.top + dyCSS;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
// Обновим визуально
|
|
542
|
+
box.style.left = `${newLeft}px`;
|
|
543
|
+
box.style.top = `${newTop}px`;
|
|
544
|
+
box.style.width = `${newW}px`;
|
|
545
|
+
box.style.height = `${newH}px`;
|
|
546
|
+
// Переставим ручки/грани
|
|
547
|
+
this._repositionBoxChildren(box);
|
|
548
|
+
|
|
549
|
+
// Перевод в мировые координаты
|
|
550
|
+
const screenX = (newLeft - offsetLeft) * res;
|
|
551
|
+
const screenY = (newTop - offsetTop) * res;
|
|
552
|
+
const screenW = newW * res;
|
|
553
|
+
const screenH = newH * res;
|
|
554
|
+
const worldX = (screenX - tx) / s;
|
|
555
|
+
const worldY = (screenY - ty) / s;
|
|
556
|
+
const worldW = screenW / s;
|
|
557
|
+
const worldH = screenH / s;
|
|
558
|
+
|
|
559
|
+
// Определяем, изменилась ли позиция (только для левых/верхних граней)
|
|
560
|
+
const edgePositionChanged = (newLeft !== startCSS.left) || (newTop !== startCSS.top);
|
|
561
|
+
|
|
562
|
+
if (isGroup) {
|
|
563
|
+
this.eventBus.emit(Events.Tool.GroupResizeUpdate, {
|
|
564
|
+
objects,
|
|
565
|
+
startBounds: { ...startWorld },
|
|
566
|
+
newBounds: { x: worldX, y: worldY, width: worldW, height: worldH }
|
|
567
|
+
});
|
|
568
|
+
} else {
|
|
569
|
+
const edgeResizeData = {
|
|
570
|
+
object: id,
|
|
571
|
+
size: { width: worldW, height: worldH }
|
|
572
|
+
};
|
|
573
|
+
|
|
574
|
+
// Отправляем позицию только если она действительно изменилась
|
|
575
|
+
if (edgePositionChanged) {
|
|
576
|
+
edgeResizeData.position = { x: worldX, y: worldY };
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
this.eventBus.emit(Events.Tool.ResizeUpdate, edgeResizeData);
|
|
580
|
+
}
|
|
581
|
+
};
|
|
582
|
+
const onUp = () => {
|
|
583
|
+
document.removeEventListener('mousemove', onMove);
|
|
584
|
+
document.removeEventListener('mouseup', onUp);
|
|
585
|
+
const endCSS = {
|
|
586
|
+
left: parseFloat(box.style.left),
|
|
587
|
+
top: parseFloat(box.style.top),
|
|
588
|
+
width: parseFloat(box.style.width),
|
|
589
|
+
height: parseFloat(box.style.height),
|
|
590
|
+
};
|
|
591
|
+
const screenX = (endCSS.left - offsetLeft) * res;
|
|
592
|
+
const screenY = (endCSS.top - offsetTop) * res;
|
|
593
|
+
const screenW = endCSS.width * res;
|
|
594
|
+
const screenH = endCSS.height * res;
|
|
595
|
+
const worldX = (screenX - tx) / s;
|
|
596
|
+
const worldY = (screenY - ty) / s;
|
|
597
|
+
const worldW = screenW / s;
|
|
598
|
+
const worldH = screenH / s;
|
|
599
|
+
|
|
600
|
+
if (isGroup) {
|
|
601
|
+
this.eventBus.emit(Events.Tool.GroupResizeEnd, { objects });
|
|
602
|
+
} else {
|
|
603
|
+
// Определяем, изменилась ли позиция для краевого ресайза
|
|
604
|
+
const edgeFinalPositionChanged = (endCSS.left !== startCSS.left) || (endCSS.top !== startCSS.top);
|
|
605
|
+
|
|
606
|
+
const edgeResizeEndData = {
|
|
607
|
+
object: id,
|
|
608
|
+
oldSize: { width: startWorld.width, height: startWorld.height },
|
|
609
|
+
newSize: { width: worldW, height: worldH }
|
|
610
|
+
};
|
|
611
|
+
|
|
612
|
+
// Добавляем информацию о позиции только если она действительно изменилась
|
|
613
|
+
if (edgeFinalPositionChanged) {
|
|
614
|
+
edgeResizeEndData.oldPosition = { x: startWorld.x, y: startWorld.y };
|
|
615
|
+
edgeResizeEndData.newPosition = { x: worldX, y: worldY };
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
this.eventBus.emit(Events.Tool.ResizeEnd, edgeResizeEndData);
|
|
619
|
+
}
|
|
620
|
+
};
|
|
621
|
+
document.addEventListener('mousemove', onMove);
|
|
622
|
+
document.addEventListener('mouseup', onUp);
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
_onRotateHandleDown(e, box) {
|
|
626
|
+
e.preventDefault(); e.stopPropagation();
|
|
627
|
+
|
|
628
|
+
const id = e.currentTarget.dataset.id;
|
|
629
|
+
const isGroup = id === '__group__';
|
|
630
|
+
|
|
631
|
+
// Получаем центр объекта в CSS координатах
|
|
632
|
+
const boxLeft = parseFloat(box.style.left);
|
|
633
|
+
const boxTop = parseFloat(box.style.top);
|
|
634
|
+
const boxWidth = parseFloat(box.style.width);
|
|
635
|
+
const boxHeight = parseFloat(box.style.height);
|
|
636
|
+
const centerX = boxLeft + boxWidth / 2;
|
|
637
|
+
const centerY = boxTop + boxHeight / 2;
|
|
638
|
+
|
|
639
|
+
// Начальный угол от центра объекта до курсора
|
|
640
|
+
const startAngle = Math.atan2(e.clientY - centerY, e.clientX - centerX);
|
|
641
|
+
|
|
642
|
+
// Получаем текущий поворот объекта из состояния
|
|
643
|
+
let startRotation = 0;
|
|
644
|
+
if (!isGroup) {
|
|
645
|
+
const rotationData = { objectId: id, rotation: 0 };
|
|
646
|
+
this.eventBus.emit(Events.Tool.GetObjectRotation, rotationData);
|
|
647
|
+
startRotation = (rotationData.rotation || 0) * Math.PI / 180; // Преобразуем градусы в радианы
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
// Изменяем курсор на grabbing
|
|
651
|
+
e.currentTarget.style.cursor = 'grabbing';
|
|
652
|
+
|
|
653
|
+
// Уведомляем о начале поворота
|
|
654
|
+
if (isGroup) {
|
|
655
|
+
const req = { selection: [] };
|
|
656
|
+
this.eventBus.emit(Events.Tool.GetSelection, req);
|
|
657
|
+
const objects = req.selection || [];
|
|
658
|
+
this.eventBus.emit(Events.Tool.GroupRotateStart, { objects });
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
const onRotateMove = (ev) => {
|
|
662
|
+
// Вычисляем текущий угол
|
|
663
|
+
const currentAngle = Math.atan2(ev.clientY - centerY, ev.clientX - centerX);
|
|
664
|
+
const deltaAngle = currentAngle - startAngle;
|
|
665
|
+
const newRotation = startRotation + deltaAngle;
|
|
666
|
+
|
|
667
|
+
if (isGroup) {
|
|
668
|
+
const req = { selection: [] };
|
|
669
|
+
this.eventBus.emit(Events.Tool.GetSelection, req);
|
|
670
|
+
const objects = req.selection || [];
|
|
671
|
+
this.eventBus.emit(Events.Tool.GroupRotateUpdate, {
|
|
672
|
+
objects,
|
|
673
|
+
angle: newRotation * 180 / Math.PI // Преобразуем радианы в градусы
|
|
674
|
+
});
|
|
675
|
+
} else {
|
|
676
|
+
this.eventBus.emit(Events.Tool.RotateUpdate, {
|
|
677
|
+
object: id,
|
|
678
|
+
angle: newRotation * 180 / Math.PI // Преобразуем радианы в градусы
|
|
679
|
+
});
|
|
680
|
+
}
|
|
681
|
+
};
|
|
682
|
+
|
|
683
|
+
const onRotateUp = (ev) => {
|
|
684
|
+
document.removeEventListener('mousemove', onRotateMove);
|
|
685
|
+
document.removeEventListener('mouseup', onRotateUp);
|
|
686
|
+
|
|
687
|
+
// Возвращаем курсор
|
|
688
|
+
e.currentTarget.style.cursor = 'grab';
|
|
689
|
+
|
|
690
|
+
// Вычисляем финальный угол
|
|
691
|
+
const finalAngle = Math.atan2(ev.clientY - centerY, ev.clientX - centerX);
|
|
692
|
+
const finalDeltaAngle = finalAngle - startAngle;
|
|
693
|
+
const finalRotation = startRotation + finalDeltaAngle;
|
|
694
|
+
|
|
695
|
+
if (isGroup) {
|
|
696
|
+
const req = { selection: [] };
|
|
697
|
+
this.eventBus.emit(Events.Tool.GetSelection, req);
|
|
698
|
+
const objects = req.selection || [];
|
|
699
|
+
this.eventBus.emit(Events.Tool.GroupRotateEnd, { objects });
|
|
700
|
+
} else {
|
|
701
|
+
this.eventBus.emit(Events.Tool.RotateEnd, {
|
|
702
|
+
object: id,
|
|
703
|
+
oldAngle: startRotation * 180 / Math.PI, // Преобразуем радианы в градусы
|
|
704
|
+
newAngle: finalRotation * 180 / Math.PI // Преобразуем радианы в градусы
|
|
705
|
+
});
|
|
706
|
+
}
|
|
707
|
+
};
|
|
708
|
+
|
|
709
|
+
document.addEventListener('mousemove', onRotateMove);
|
|
710
|
+
document.addEventListener('mouseup', onRotateUp);
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
_repositionBoxChildren(box) {
|
|
714
|
+
const width = parseFloat(box.style.width);
|
|
715
|
+
const height = parseFloat(box.style.height);
|
|
716
|
+
const cx = width / 2;
|
|
717
|
+
const cy = height / 2;
|
|
718
|
+
|
|
719
|
+
// Позиционируем все ручки (угловые + боковые)
|
|
720
|
+
box.querySelectorAll('[data-dir]').forEach(h => {
|
|
721
|
+
const dir = h.dataset.dir;
|
|
722
|
+
switch (dir) {
|
|
723
|
+
// Угловые ручки
|
|
724
|
+
case 'nw': h.style.left = `${-6}px`; h.style.top = `${-6}px`; break;
|
|
725
|
+
case 'ne': h.style.left = `${Math.max(-6, width - 6)}px`; h.style.top = `${-6}px`; break;
|
|
726
|
+
case 'se': h.style.left = `${Math.max(-6, width - 6)}px`; h.style.top = `${Math.max(-6, height - 6)}px`; break;
|
|
727
|
+
case 'sw': h.style.left = `${-6}px`; h.style.top = `${Math.max(-6, height - 6)}px`; break;
|
|
728
|
+
// Боковые ручки
|
|
729
|
+
case 'n': h.style.left = `${cx - 6}px`; h.style.top = `${-6}px`; break;
|
|
730
|
+
case 'e': h.style.left = `${Math.max(-6, width - 6)}px`; h.style.top = `${cy - 6}px`; break;
|
|
731
|
+
case 's': h.style.left = `${cx - 6}px`; h.style.top = `${Math.max(-6, height - 6)}px`; break;
|
|
732
|
+
case 'w': h.style.left = `${-6}px`; h.style.top = `${cy - 6}px`; break;
|
|
733
|
+
}
|
|
734
|
+
});
|
|
735
|
+
|
|
736
|
+
// Позиционируем невидимые области для захвата с отступами от углов
|
|
737
|
+
const edgeSize = 10;
|
|
738
|
+
const cornerGap = 20;
|
|
739
|
+
const top = box.querySelector('[data-edge="top"]');
|
|
740
|
+
const bottom = box.querySelector('[data-edge="bottom"]');
|
|
741
|
+
const left = box.querySelector('[data-edge="left"]');
|
|
742
|
+
const right = box.querySelector('[data-edge="right"]');
|
|
743
|
+
|
|
744
|
+
if (top) Object.assign(top.style, {
|
|
745
|
+
left: `${cornerGap}px`,
|
|
746
|
+
top: `-${edgeSize/2}px`,
|
|
747
|
+
width: `${Math.max(0, width - 2 * cornerGap)}px`,
|
|
748
|
+
height: `${edgeSize}px`
|
|
749
|
+
});
|
|
750
|
+
if (bottom) Object.assign(bottom.style, {
|
|
751
|
+
left: `${cornerGap}px`,
|
|
752
|
+
top: `${height - edgeSize/2}px`,
|
|
753
|
+
width: `${Math.max(0, width - 2 * cornerGap)}px`,
|
|
754
|
+
height: `${edgeSize}px`
|
|
755
|
+
});
|
|
756
|
+
if (left) Object.assign(left.style, {
|
|
757
|
+
left: `-${edgeSize/2}px`,
|
|
758
|
+
top: `${cornerGap}px`,
|
|
759
|
+
width: `${edgeSize}px`,
|
|
760
|
+
height: `${Math.max(0, height - 2 * cornerGap)}px`
|
|
761
|
+
});
|
|
762
|
+
if (right) Object.assign(right.style, {
|
|
763
|
+
left: `${width - edgeSize/2}px`,
|
|
764
|
+
top: `${cornerGap}px`,
|
|
765
|
+
width: `${edgeSize}px`,
|
|
766
|
+
height: `${Math.max(0, height - 2 * cornerGap)}px`
|
|
767
|
+
});
|
|
768
|
+
|
|
769
|
+
// Позиционируем ручку вращения
|
|
770
|
+
const rotateHandle = box.querySelector('[data-handle="rotate"]');
|
|
771
|
+
if (rotateHandle) {
|
|
772
|
+
rotateHandle.style.left = `${0 - 10}px`; // центрируем относительно левого нижнего угла
|
|
773
|
+
rotateHandle.style.top = `${height + 25 - 10}px`; // отступ 25px от нижней грани
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
|