@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,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GroupResizeController — изменение размера группы объектов
|
|
3
|
+
*/
|
|
4
|
+
export class GroupResizeController {
|
|
5
|
+
constructor({ emit, selection, getGroupBounds, ensureGroupGraphics, updateGroupGraphics }) {
|
|
6
|
+
this.emit = emit;
|
|
7
|
+
this.selection = selection;
|
|
8
|
+
this.getGroupBounds = getGroupBounds;
|
|
9
|
+
this.ensureGroupGraphics = ensureGroupGraphics;
|
|
10
|
+
this.updateGroupGraphics = updateGroupGraphics;
|
|
11
|
+
|
|
12
|
+
this.isActive = false;
|
|
13
|
+
this.handle = null;
|
|
14
|
+
this.groupStartBounds = null;
|
|
15
|
+
this.groupStartMouse = null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
start(handle, currentMouse) {
|
|
19
|
+
this.isActive = true;
|
|
20
|
+
this.handle = handle;
|
|
21
|
+
this.groupStartBounds = this.getGroupBounds();
|
|
22
|
+
this.groupStartMouse = { x: currentMouse.x, y: currentMouse.y };
|
|
23
|
+
const ids = this.selection.toArray();
|
|
24
|
+
this.emit('group:resize:start', { objects: ids, startBounds: this.groupStartBounds, handle });
|
|
25
|
+
this.ensureGroupGraphics(this.groupStartBounds);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
update(event) {
|
|
29
|
+
if (!this.isActive || !this.groupStartBounds || !this.groupStartMouse) return;
|
|
30
|
+
const start = this.groupStartBounds;
|
|
31
|
+
const deltaX = event.x - this.groupStartMouse.x;
|
|
32
|
+
const deltaY = event.y - this.groupStartMouse.y;
|
|
33
|
+
const minW = 20, minH = 20;
|
|
34
|
+
let x = start.x, y = start.y, w = start.width, h = start.height;
|
|
35
|
+
|
|
36
|
+
const maintainAspectRatio = !!(event.originalEvent && event.originalEvent.shiftKey);
|
|
37
|
+
switch (this.handle) {
|
|
38
|
+
case 'e': w = start.width + deltaX; break;
|
|
39
|
+
case 'w': w = start.width - deltaX; x = start.x + deltaX; break;
|
|
40
|
+
case 's': h = start.height + deltaY; break;
|
|
41
|
+
case 'n': h = start.height - deltaY; y = start.y + deltaY; break;
|
|
42
|
+
case 'se': w = start.width + deltaX; h = start.height + deltaY; break;
|
|
43
|
+
case 'ne': w = start.width + deltaX; h = start.height - deltaY; y = start.y + deltaY; break;
|
|
44
|
+
case 'sw': w = start.width - deltaX; x = start.x + deltaX; h = start.height + deltaY; break;
|
|
45
|
+
case 'nw': w = start.width - deltaX; x = start.x + deltaX; h = start.height - deltaY; y = start.y + deltaY; break;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (maintainAspectRatio && start.height !== 0) {
|
|
49
|
+
const ar = start.width / start.height;
|
|
50
|
+
if (['nw','ne','sw','se'].includes(this.handle)) {
|
|
51
|
+
const widthChange = Math.abs(w - start.width);
|
|
52
|
+
const heightChange = Math.abs(h - start.height);
|
|
53
|
+
if (widthChange > heightChange) {
|
|
54
|
+
h = w / ar;
|
|
55
|
+
if (['n','ne','nw'].includes(this.handle)) y = start.y + (start.height - h); else y = start.y;
|
|
56
|
+
} else {
|
|
57
|
+
w = h * ar;
|
|
58
|
+
if (['w','sw','nw'].includes(this.handle)) x = start.x + (start.width - w); else x = start.x;
|
|
59
|
+
}
|
|
60
|
+
} else if (['e','w'].includes(this.handle)) {
|
|
61
|
+
h = w / ar;
|
|
62
|
+
if (this.handle === 'w') x = start.x + (start.width - w);
|
|
63
|
+
} else if (['n','s'].includes(this.handle)) {
|
|
64
|
+
w = h * ar;
|
|
65
|
+
if (this.handle === 'n') y = start.y + (start.height - h);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (w < minW) { if (['w','sw','nw'].includes(this.handle)) x += (w - minW); w = minW; }
|
|
70
|
+
if (h < minH) { if (['n','ne','nw'].includes(this.handle)) y += (h - minH); h = minH; }
|
|
71
|
+
|
|
72
|
+
const scale = { x: w / start.width, y: h / start.height };
|
|
73
|
+
const ids = this.selection.toArray();
|
|
74
|
+
const newBounds = { x, y, width: w, height: h };
|
|
75
|
+
this.emit('group:resize:update', { objects: ids, startBounds: start, newBounds, scale });
|
|
76
|
+
this.updateGroupGraphics(newBounds);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
end() {
|
|
80
|
+
if (!this.isActive) return;
|
|
81
|
+
const ids = this.selection.toArray();
|
|
82
|
+
this.emit('group:resize:end', { objects: ids });
|
|
83
|
+
this.isActive = false;
|
|
84
|
+
this.handle = null;
|
|
85
|
+
this.groupStartBounds = null;
|
|
86
|
+
this.groupStartMouse = null;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GroupRotateController — вращение группы объектов
|
|
3
|
+
*/
|
|
4
|
+
export class GroupRotateController {
|
|
5
|
+
constructor({ emit, selection, getGroupBounds, ensureGroupGraphics, updateHandles }) {
|
|
6
|
+
this.emit = emit;
|
|
7
|
+
this.selection = selection;
|
|
8
|
+
this.getGroupBounds = getGroupBounds;
|
|
9
|
+
this.ensureGroupGraphics = ensureGroupGraphics;
|
|
10
|
+
this.updateHandles = updateHandles; // функция для обновления ResizeHandles
|
|
11
|
+
|
|
12
|
+
this.isActive = false;
|
|
13
|
+
this.center = null;
|
|
14
|
+
this.groupRotateBounds = null;
|
|
15
|
+
this.rotateStartMouseAngle = 0;
|
|
16
|
+
this.rotateCurrentAngle = 0;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
start(currentMouse) {
|
|
20
|
+
this.isActive = true;
|
|
21
|
+
const gb = this.getGroupBounds();
|
|
22
|
+
this.groupRotateBounds = gb;
|
|
23
|
+
this.center = { x: gb.x + gb.width / 2, y: gb.y + gb.height / 2 };
|
|
24
|
+
this.rotateStartMouseAngle = Math.atan2(currentMouse.y - this.center.y, currentMouse.x - this.center.x);
|
|
25
|
+
// Настроим pivot и позицию group-bounds
|
|
26
|
+
this.ensureGroupGraphics(gb);
|
|
27
|
+
this.emit('group:rotate:start', { objects: this.selection.toArray(), center: this.center });
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
update(event) {
|
|
31
|
+
if (!this.isActive || !this.center) return;
|
|
32
|
+
const currentMouseAngle = Math.atan2(event.y - this.center.y, event.x - this.center.x);
|
|
33
|
+
let delta = currentMouseAngle - this.rotateStartMouseAngle;
|
|
34
|
+
while (delta > Math.PI) delta -= 2 * Math.PI;
|
|
35
|
+
while (delta < -Math.PI) delta += 2 * Math.PI;
|
|
36
|
+
let deltaDeg = delta * 180 / Math.PI;
|
|
37
|
+
if (event.originalEvent && event.originalEvent.shiftKey) {
|
|
38
|
+
deltaDeg = Math.round(deltaDeg / 15) * 15;
|
|
39
|
+
}
|
|
40
|
+
this.rotateCurrentAngle = deltaDeg;
|
|
41
|
+
this.emit('group:rotate:update', { objects: this.selection.toArray(), center: this.center, angle: this.rotateCurrentAngle });
|
|
42
|
+
// Вращение рамки группы вокруг центра — для согласованности ручек
|
|
43
|
+
const angleRad = this.rotateCurrentAngle * Math.PI / 180;
|
|
44
|
+
if (this.ensureGroupGraphics && this.groupRotateBounds) {
|
|
45
|
+
// обновление ручек через внешний колбек
|
|
46
|
+
if (typeof this.updateHandles === 'function') this.updateHandles();
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
end() {
|
|
51
|
+
if (!this.isActive) return;
|
|
52
|
+
this.emit('group:rotate:end', { objects: this.selection.toArray(), angle: this.rotateCurrentAngle });
|
|
53
|
+
this.isActive = false;
|
|
54
|
+
this.center = null;
|
|
55
|
+
this.groupRotateBounds = null;
|
|
56
|
+
this.rotateStartMouseAngle = 0;
|
|
57
|
+
this.rotateCurrentAngle = 0;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import * as PIXI from 'pixi.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* HandlesSync — отвечает за синхронизацию ResizeHandles и групповой рамки с текущим выделением
|
|
5
|
+
* Используется SelectTool, но не знает деталей SelectTool кроме предоставленного API
|
|
6
|
+
*/
|
|
7
|
+
export class HandlesSync {
|
|
8
|
+
constructor({ app, resizeHandles, selection, emit }) {
|
|
9
|
+
this.app = app;
|
|
10
|
+
this.resizeHandles = resizeHandles;
|
|
11
|
+
this.selection = selection; // SelectionModel
|
|
12
|
+
this.emit = emit; // функция для EventBus.emit
|
|
13
|
+
this.groupBoundsGraphics = null;
|
|
14
|
+
this.groupId = '__group__';
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
update() {
|
|
18
|
+
if (!this.resizeHandles) return;
|
|
19
|
+
const count = this.selection.size();
|
|
20
|
+
if (count === 0) {
|
|
21
|
+
this.resizeHandles.hideHandles();
|
|
22
|
+
this._removeGroupGraphics();
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
if (count === 1) {
|
|
26
|
+
this._removeGroupGraphics();
|
|
27
|
+
const objectId = this.selection.toArray()[0];
|
|
28
|
+
const req = { objectId, pixiObject: null };
|
|
29
|
+
this.emit('get:object:pixi', req);
|
|
30
|
+
if (req.pixiObject) {
|
|
31
|
+
// Проверяем тип объекта - для записок не показываем ручки
|
|
32
|
+
const meta = req.pixiObject._mb || {};
|
|
33
|
+
if (meta.type === 'note') {
|
|
34
|
+
console.log(`📝 Скрываем ручки для записки ${objectId} - записки не должны иметь ручек`);
|
|
35
|
+
this.resizeHandles.hideHandles();
|
|
36
|
+
} else {
|
|
37
|
+
this.resizeHandles.showHandles(req.pixiObject, objectId);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
// Группа: считаем границы и показываем ручки на невидимом прямоугольнике
|
|
43
|
+
const gb = this._computeGroupBounds();
|
|
44
|
+
if (!gb || gb.width <= 0 || gb.height <= 0) {
|
|
45
|
+
this.resizeHandles.hideHandles();
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
this._ensureGroupGraphics(gb);
|
|
49
|
+
this.resizeHandles.showHandles(this.groupBoundsGraphics, this.groupId);
|
|
50
|
+
this._drawGroupOutline(gb);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
_computeGroupBounds() {
|
|
54
|
+
const req = { objects: [] };
|
|
55
|
+
this.emit('get:all:objects', req);
|
|
56
|
+
const pixiMap = new Map(req.objects.map(o => [o.id, o.pixi]));
|
|
57
|
+
const b = this.selection.computeBounds((id) => pixiMap.get(id));
|
|
58
|
+
return b;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
_ensureGroupGraphics(bounds) {
|
|
62
|
+
if (!this.app || !this.app.stage) return;
|
|
63
|
+
if (!this.groupBoundsGraphics) {
|
|
64
|
+
this.groupBoundsGraphics = new PIXI.Graphics();
|
|
65
|
+
this.groupBoundsGraphics.name = 'group-bounds';
|
|
66
|
+
this.groupBoundsGraphics.zIndex = 1400;
|
|
67
|
+
this.app.stage.addChild(this.groupBoundsGraphics);
|
|
68
|
+
this.app.stage.sortableChildren = true;
|
|
69
|
+
}
|
|
70
|
+
this._updateGroupGraphics(bounds);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
_updateGroupGraphics(bounds) {
|
|
74
|
+
if (!this.groupBoundsGraphics) return;
|
|
75
|
+
this.groupBoundsGraphics.clear();
|
|
76
|
+
this.groupBoundsGraphics.beginFill(0x000000, 0.001);
|
|
77
|
+
this.groupBoundsGraphics.drawRect(0, 0, Math.max(1, bounds.width), Math.max(1, bounds.height));
|
|
78
|
+
this.groupBoundsGraphics.endFill();
|
|
79
|
+
this.groupBoundsGraphics.position.set(bounds.x, bounds.y);
|
|
80
|
+
if (this.resizeHandles) this.resizeHandles.updateHandles();
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
_drawGroupOutline(bounds) {
|
|
84
|
+
// Визуальная рамка (опционально) — можно реализовать тут, сейчас делегируем через update() SelectTool
|
|
85
|
+
// Оставлено как задел
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
_removeGroupGraphics() {
|
|
89
|
+
if (this.groupBoundsGraphics) {
|
|
90
|
+
this.groupBoundsGraphics.clear();
|
|
91
|
+
this.groupBoundsGraphics.rotation = 0;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ResizeController — изменение размера одного объекта (не группы)
|
|
3
|
+
*/
|
|
4
|
+
export class ResizeController {
|
|
5
|
+
constructor({ emit, getRotation }) {
|
|
6
|
+
this.emit = emit;
|
|
7
|
+
this.getRotation = getRotation;
|
|
8
|
+
this.isResizing = false;
|
|
9
|
+
this.resizeHandle = null;
|
|
10
|
+
this.dragTarget = null;
|
|
11
|
+
this.resizeStartBounds = null;
|
|
12
|
+
this.resizeStartMousePos = null;
|
|
13
|
+
this.resizeStartPosition = null;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
start(handle, objectId, currentMouse) {
|
|
17
|
+
this.isResizing = true;
|
|
18
|
+
this.resizeHandle = handle;
|
|
19
|
+
this.dragTarget = objectId;
|
|
20
|
+
const sizeData = { objectId, size: null };
|
|
21
|
+
this.emit('get:object:size', sizeData);
|
|
22
|
+
const positionData = { objectId, position: null };
|
|
23
|
+
this.emit('get:object:position', positionData);
|
|
24
|
+
this.resizeStartBounds = sizeData.size || { width: 100, height: 100 };
|
|
25
|
+
this.resizeStartMousePos = { x: currentMouse.x, y: currentMouse.y };
|
|
26
|
+
this.resizeStartPosition = positionData.position || { x: 0, y: 0 };
|
|
27
|
+
this.emit('resize:start', { object: objectId, handle });
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
update(event, helpers) {
|
|
31
|
+
if (!this.isResizing || !this.resizeStartBounds || !this.resizeStartMousePos) return;
|
|
32
|
+
const { calculateNewSize, calculatePositionOffset } = helpers;
|
|
33
|
+
const deltaX = event.x - this.resizeStartMousePos.x;
|
|
34
|
+
const deltaY = event.y - this.resizeStartMousePos.y;
|
|
35
|
+
const maintainAspectRatio = !!(event.originalEvent && event.originalEvent.shiftKey);
|
|
36
|
+
const newSize = calculateNewSize(this.resizeHandle, this.resizeStartBounds, deltaX, deltaY, maintainAspectRatio);
|
|
37
|
+
newSize.width = Math.max(20, newSize.width);
|
|
38
|
+
newSize.height = Math.max(20, newSize.height);
|
|
39
|
+
const rotation = this.getRotation ? (this.getRotation(this.dragTarget) || 0) : 0;
|
|
40
|
+
const positionOffset = calculatePositionOffset(this.resizeHandle, this.resizeStartBounds, newSize, rotation);
|
|
41
|
+
const newPosition = { x: this.resizeStartPosition.x + positionOffset.x, y: this.resizeStartPosition.y + positionOffset.y };
|
|
42
|
+
this.emit('resize:update', { object: this.dragTarget, handle: this.resizeHandle, size: newSize, position: newPosition });
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
end() {
|
|
46
|
+
if (this.isResizing && this.dragTarget) {
|
|
47
|
+
const finalSizeData = { objectId: this.dragTarget, size: null };
|
|
48
|
+
this.emit('get:object:size', finalSizeData);
|
|
49
|
+
const finalPositionData = { objectId: this.dragTarget, position: null };
|
|
50
|
+
this.emit('get:object:position', finalPositionData);
|
|
51
|
+
this.emit('resize:end', {
|
|
52
|
+
object: this.dragTarget,
|
|
53
|
+
oldSize: this.resizeStartBounds,
|
|
54
|
+
newSize: finalSizeData.size || this.resizeStartBounds,
|
|
55
|
+
oldPosition: this.resizeStartPosition,
|
|
56
|
+
newPosition: finalPositionData.position || this.resizeStartPosition
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
this.isResizing = false;
|
|
60
|
+
this.resizeHandle = null;
|
|
61
|
+
this.dragTarget = null;
|
|
62
|
+
this.resizeStartBounds = null;
|
|
63
|
+
this.resizeStartMousePos = null;
|
|
64
|
+
this.resizeStartPosition = null;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RotateController — вращение одного объекта (не группы)
|
|
3
|
+
*/
|
|
4
|
+
export class RotateController {
|
|
5
|
+
constructor({ emit }) {
|
|
6
|
+
this.emit = emit;
|
|
7
|
+
this.isRotating = false;
|
|
8
|
+
this.dragTarget = null;
|
|
9
|
+
this.rotateCenter = null;
|
|
10
|
+
this.rotateStartAngle = 0; // начальный угол объекта
|
|
11
|
+
this.rotateStartMouseAngle = 0; // начальный угол мыши от центра
|
|
12
|
+
this.rotateCurrentAngle = 0;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
start(objectId, currentMouse, center) {
|
|
16
|
+
this.isRotating = true;
|
|
17
|
+
this.dragTarget = objectId;
|
|
18
|
+
// Текущий угол объекта
|
|
19
|
+
const rotationData = { objectId, rotation: 0 };
|
|
20
|
+
this.emit('get:object:rotation', rotationData);
|
|
21
|
+
this.rotateStartAngle = rotationData.rotation || 0;
|
|
22
|
+
this.rotateCurrentAngle = this.rotateStartAngle;
|
|
23
|
+
// Центр вращения приходит снаружи (рассчитан по pos+size)
|
|
24
|
+
this.rotateCenter = center;
|
|
25
|
+
this.rotateStartMouseAngle = Math.atan2(currentMouse.y - center.y, currentMouse.x - center.x);
|
|
26
|
+
this.emit('rotate:start', { object: objectId });
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
update(event) {
|
|
30
|
+
if (!this.isRotating || !this.rotateCenter) return;
|
|
31
|
+
const currentMouseAngle = Math.atan2(event.y - this.rotateCenter.y, event.x - this.rotateCenter.x);
|
|
32
|
+
let delta = currentMouseAngle - this.rotateStartMouseAngle;
|
|
33
|
+
while (delta > Math.PI) delta -= 2 * Math.PI;
|
|
34
|
+
while (delta < -Math.PI) delta += 2 * Math.PI;
|
|
35
|
+
let deltaDeg = delta * 180 / Math.PI;
|
|
36
|
+
if (event.originalEvent && event.originalEvent.shiftKey) {
|
|
37
|
+
deltaDeg = Math.round(deltaDeg / 15) * 15;
|
|
38
|
+
}
|
|
39
|
+
this.rotateCurrentAngle = this.rotateStartAngle + deltaDeg;
|
|
40
|
+
while (this.rotateCurrentAngle < 0) this.rotateCurrentAngle += 360;
|
|
41
|
+
while (this.rotateCurrentAngle >= 360) this.rotateCurrentAngle -= 360;
|
|
42
|
+
this.emit('rotate:update', { object: this.dragTarget, angle: this.rotateCurrentAngle });
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
end() {
|
|
46
|
+
if (this.isRotating && this.dragTarget) {
|
|
47
|
+
this.emit('rotate:end', { object: this.dragTarget, oldAngle: this.rotateStartAngle, newAngle: this.rotateCurrentAngle });
|
|
48
|
+
}
|
|
49
|
+
this.isRotating = false;
|
|
50
|
+
this.dragTarget = null;
|
|
51
|
+
this.rotateCenter = null;
|
|
52
|
+
this.rotateStartAngle = 0;
|
|
53
|
+
this.rotateStartMouseAngle = 0;
|
|
54
|
+
this.rotateCurrentAngle = 0;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SelectionModel — хранит и управляет текущим набором выбранных объектов
|
|
3
|
+
*/
|
|
4
|
+
export class SelectionModel {
|
|
5
|
+
constructor() {
|
|
6
|
+
this._ids = new Set();
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
clear() { this._ids.clear(); }
|
|
10
|
+
add(id) { if (id) this._ids.add(id); }
|
|
11
|
+
addMany(ids = []) { ids.forEach((id) => this.add(id)); }
|
|
12
|
+
remove(id) { this._ids.delete(id); }
|
|
13
|
+
toggle(id) { if (this._ids.has(id)) this._ids.delete(id); else this._ids.add(id); }
|
|
14
|
+
has(id) { return this._ids.has(id); }
|
|
15
|
+
size() { return this._ids.size; }
|
|
16
|
+
toArray() { return Array.from(this._ids); }
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Вычисляет групповые границы по getBounds() каждого PIXI-объекта
|
|
20
|
+
* @param {(id:string)=>PIXI.DisplayObject|null} getPixiById
|
|
21
|
+
* @returns {{x:number,y:number,width:number,height:number}|null}
|
|
22
|
+
*/
|
|
23
|
+
computeBounds(getPixiById) {
|
|
24
|
+
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
|
25
|
+
let any = false;
|
|
26
|
+
for (const id of this._ids) {
|
|
27
|
+
const pixi = getPixiById ? getPixiById(id) : null;
|
|
28
|
+
if (!pixi || !pixi.getBounds) continue;
|
|
29
|
+
const b = pixi.getBounds();
|
|
30
|
+
if (!b) continue;
|
|
31
|
+
any = true;
|
|
32
|
+
if (b.x < minX) minX = b.x;
|
|
33
|
+
if (b.y < minY) minY = b.y;
|
|
34
|
+
if (b.x + b.width > maxX) maxX = b.x + b.width;
|
|
35
|
+
if (b.y + b.height > maxY) maxY = b.y + b.height;
|
|
36
|
+
}
|
|
37
|
+
if (!any) return null;
|
|
38
|
+
return { x: minX, y: minY, width: Math.max(0, maxX - minX), height: Math.max(0, maxY - minY) };
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SimpleDragController — управляет перетаскиванием одного объекта
|
|
3
|
+
* Делегирует в ядро события drag:start/update/end
|
|
4
|
+
*/
|
|
5
|
+
export class SimpleDragController {
|
|
6
|
+
constructor({ emit }) {
|
|
7
|
+
this.emit = emit;
|
|
8
|
+
this.active = false;
|
|
9
|
+
this.dragTarget = null;
|
|
10
|
+
this.offset = { x: 0, y: 0 };
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
start(objectId, event) {
|
|
14
|
+
this.active = true;
|
|
15
|
+
this.dragTarget = objectId;
|
|
16
|
+
// Получаем текущую позицию объекта
|
|
17
|
+
const objectData = { objectId, position: null };
|
|
18
|
+
this.emit('get:object:position', objectData);
|
|
19
|
+
if (objectData.position) {
|
|
20
|
+
this.offset = { x: event.x - objectData.position.x, y: event.y - objectData.position.y };
|
|
21
|
+
} else {
|
|
22
|
+
this.offset = { x: 0, y: 0 };
|
|
23
|
+
}
|
|
24
|
+
// Позиция и координаты — уже в мировых координатах (SelectTool нормализует)
|
|
25
|
+
this.emit('drag:start', { object: objectId, position: { x: event.x, y: event.y } });
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
update(event) {
|
|
29
|
+
if (!this.active || !this.dragTarget) return;
|
|
30
|
+
const newX = event.x - this.offset.x;
|
|
31
|
+
const newY = event.y - this.offset.y;
|
|
32
|
+
this.emit('drag:update', { object: this.dragTarget, position: { x: newX, y: newY } });
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
end() {
|
|
36
|
+
if (!this.active) return;
|
|
37
|
+
if (this.dragTarget) {
|
|
38
|
+
this.emit('drag:end', { object: this.dragTarget });
|
|
39
|
+
}
|
|
40
|
+
this.active = false;
|
|
41
|
+
this.dragTarget = null;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { Events } from '../core/events/Events.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* CommentPopover — всплывающее окно для объектов типа comment
|
|
5
|
+
*/
|
|
6
|
+
export class CommentPopover {
|
|
7
|
+
constructor(container, eventBus, core) {
|
|
8
|
+
this.container = container;
|
|
9
|
+
this.eventBus = eventBus;
|
|
10
|
+
this.core = core;
|
|
11
|
+
this.layer = null;
|
|
12
|
+
this.popover = null;
|
|
13
|
+
this.header = null;
|
|
14
|
+
this.body = null;
|
|
15
|
+
this.footer = null;
|
|
16
|
+
this.input = null;
|
|
17
|
+
this.button = null;
|
|
18
|
+
this.currentId = null;
|
|
19
|
+
// Память комментариев: { [objectId]: string[] }
|
|
20
|
+
this.commentsById = new Map();
|
|
21
|
+
this._onDocMouseDown = this._onDocMouseDown.bind(this);
|
|
22
|
+
this._onSubmit = this._onSubmit.bind(this);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
attach() {
|
|
26
|
+
this.layer = document.createElement('div');
|
|
27
|
+
this.layer.className = 'comment-popover-layer';
|
|
28
|
+
Object.assign(this.layer.style, {
|
|
29
|
+
position: 'absolute', inset: '0', pointerEvents: 'none', zIndex: 25
|
|
30
|
+
});
|
|
31
|
+
this.container.appendChild(this.layer);
|
|
32
|
+
|
|
33
|
+
// Подписки
|
|
34
|
+
this.eventBus.on(Events.Tool.SelectionAdd, () => this.updateFromSelection());
|
|
35
|
+
this.eventBus.on(Events.Tool.SelectionRemove, () => this.updateFromSelection());
|
|
36
|
+
this.eventBus.on(Events.Tool.SelectionClear, () => this.hide());
|
|
37
|
+
this.eventBus.on(Events.Tool.DragUpdate, () => this.reposition());
|
|
38
|
+
this.eventBus.on(Events.Tool.GroupDragUpdate, () => this.reposition());
|
|
39
|
+
this.eventBus.on(Events.Tool.ResizeUpdate, () => this.reposition());
|
|
40
|
+
this.eventBus.on(Events.Tool.RotateUpdate, () => this.reposition());
|
|
41
|
+
this.eventBus.on(Events.UI.ZoomPercent, () => this.reposition());
|
|
42
|
+
this.eventBus.on(Events.Tool.PanUpdate, () => this.reposition());
|
|
43
|
+
this.eventBus.on(Events.Object.Deleted, ({ objectId }) => {
|
|
44
|
+
if (this.currentId && objectId === this.currentId) this.hide();
|
|
45
|
+
// По желанию можно очистить: this.commentsById.delete(objectId);
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
destroy() {
|
|
50
|
+
this.hide();
|
|
51
|
+
if (this.layer) this.layer.remove();
|
|
52
|
+
this.layer = null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
updateFromSelection() {
|
|
56
|
+
// Показываем только для одиночного выделения комментария
|
|
57
|
+
const ids = this.core?.selectTool ? Array.from(this.core.selectTool.selectedObjects || []) : [];
|
|
58
|
+
if (!ids || ids.length !== 1) { this.hide(); return; }
|
|
59
|
+
const id = ids[0];
|
|
60
|
+
const pixi = this.core?.pixi?.objects?.get ? this.core.pixi.objects.get(id) : null;
|
|
61
|
+
if (!pixi) { this.hide(); return; }
|
|
62
|
+
const mb = pixi._mb || {};
|
|
63
|
+
if (mb.type !== 'comment') { this.hide(); return; }
|
|
64
|
+
this.currentId = id;
|
|
65
|
+
this.showFor(id);
|
|
66
|
+
this._renderBodyFor(id);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
showFor(id) {
|
|
70
|
+
if (!this.layer) return;
|
|
71
|
+
if (!this.popover) {
|
|
72
|
+
this.popover = this._createPopover();
|
|
73
|
+
this.layer.appendChild(this.popover);
|
|
74
|
+
document.addEventListener('mousedown', this._onDocMouseDown, true);
|
|
75
|
+
}
|
|
76
|
+
this.popover.style.display = 'flex';
|
|
77
|
+
this.reposition();
|
|
78
|
+
// автофокус в input
|
|
79
|
+
if (this.input) this.input.focus();
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
hide() {
|
|
83
|
+
this.currentId = null;
|
|
84
|
+
if (this.popover) this.popover.style.display = 'none';
|
|
85
|
+
document.removeEventListener('mousedown', this._onDocMouseDown, true);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
_createPopover() {
|
|
89
|
+
const el = document.createElement('div');
|
|
90
|
+
el.className = 'comment-popover';
|
|
91
|
+
Object.assign(el.style, { position: 'absolute', pointerEvents: 'auto', display: 'flex', flexDirection: 'column' });
|
|
92
|
+
// Header
|
|
93
|
+
this.header = document.createElement('div');
|
|
94
|
+
this.header.className = 'comment-popover__header';
|
|
95
|
+
const title = document.createElement('div');
|
|
96
|
+
title.className = 'comment-popover__title';
|
|
97
|
+
title.textContent = 'Комментарий';
|
|
98
|
+
const close = document.createElement('button');
|
|
99
|
+
close.type = 'button';
|
|
100
|
+
close.className = 'comment-popover__close';
|
|
101
|
+
close.textContent = '✕';
|
|
102
|
+
close.addEventListener('click', () => this.hide());
|
|
103
|
+
this.header.appendChild(title);
|
|
104
|
+
this.header.appendChild(close);
|
|
105
|
+
// Body
|
|
106
|
+
this.body = document.createElement('div');
|
|
107
|
+
this.body.className = 'comment-popover__body';
|
|
108
|
+
Object.assign(this.body.style, { overflowY: 'auto', maxHeight: '240px' });
|
|
109
|
+
// Footer
|
|
110
|
+
this.footer = document.createElement('div');
|
|
111
|
+
this.footer.className = 'comment-popover__footer';
|
|
112
|
+
const form = document.createElement('form');
|
|
113
|
+
form.className = 'comment-popover__form';
|
|
114
|
+
form.addEventListener('submit', this._onSubmit);
|
|
115
|
+
this.input = document.createElement('input');
|
|
116
|
+
this.input.type = 'text';
|
|
117
|
+
this.input.placeholder = 'Напишите комментарий';
|
|
118
|
+
this.input.className = 'comment-popover__input';
|
|
119
|
+
this.button = document.createElement('button');
|
|
120
|
+
this.button.type = 'submit';
|
|
121
|
+
this.button.textContent = 'Добавить';
|
|
122
|
+
this.button.className = 'comment-popover__button';
|
|
123
|
+
form.appendChild(this.input);
|
|
124
|
+
form.appendChild(this.button);
|
|
125
|
+
this.footer.appendChild(form);
|
|
126
|
+
|
|
127
|
+
el.appendChild(this.header);
|
|
128
|
+
el.appendChild(this.body);
|
|
129
|
+
el.appendChild(this.footer);
|
|
130
|
+
return el;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
_onDocMouseDown(e) {
|
|
134
|
+
if (!this.popover || this.popover.style.display === 'none') return;
|
|
135
|
+
if (this.popover.contains(e.target)) return; // клик внутри окна — не закрываем
|
|
136
|
+
this.hide();
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
_onSubmit(e) {
|
|
140
|
+
e.preventDefault();
|
|
141
|
+
if (!this.input || !this.currentId) return;
|
|
142
|
+
const text = this.input.value.trim();
|
|
143
|
+
if (!text) return;
|
|
144
|
+
// Записываем в список комментов текущего объекта
|
|
145
|
+
const list = this.commentsById.get(this.currentId) || [];
|
|
146
|
+
list.push(text);
|
|
147
|
+
this.commentsById.set(this.currentId, list);
|
|
148
|
+
// Перерисовываем body
|
|
149
|
+
this._renderBodyFor(this.currentId);
|
|
150
|
+
this.input.value = '';
|
|
151
|
+
this.input.focus();
|
|
152
|
+
// В дальнейшем можно сохранить в объекте/сервере
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
_renderBodyFor(id) {
|
|
156
|
+
if (!this.body) return;
|
|
157
|
+
this.body.innerHTML = '';
|
|
158
|
+
const list = this.commentsById.get(id) || [];
|
|
159
|
+
list.forEach((text) => {
|
|
160
|
+
const row = document.createElement('div');
|
|
161
|
+
row.className = 'comment-popover__row';
|
|
162
|
+
row.textContent = text;
|
|
163
|
+
this.body.appendChild(row);
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
reposition() {
|
|
168
|
+
if (!this.popover || !this.currentId) return;
|
|
169
|
+
const pixi = this.core?.pixi?.objects?.get ? this.core.pixi.objects.get(this.currentId) : null;
|
|
170
|
+
if (!pixi) { this.hide(); return; }
|
|
171
|
+
|
|
172
|
+
const b = pixi.getBounds(); // глобальные координаты PIXI
|
|
173
|
+
const res = (this.core.pixi.app.renderer?.resolution) || 1;
|
|
174
|
+
const view = this.core.pixi.app.view;
|
|
175
|
+
const containerRect = this.container.getBoundingClientRect();
|
|
176
|
+
const viewRect = view.getBoundingClientRect();
|
|
177
|
+
const offsetLeft = viewRect.left - containerRect.left;
|
|
178
|
+
const offsetTop = viewRect.top - containerRect.top;
|
|
179
|
+
|
|
180
|
+
const left = offsetLeft + (b.x + b.width) / res + 12; // справа от объекта + зазор
|
|
181
|
+
const top = offsetTop + b.y / res; // по верхнему краю
|
|
182
|
+
|
|
183
|
+
this.popover.style.left = `${Math.round(left)}px`;
|
|
184
|
+
this.popover.style.top = `${Math.round(top)}px`;
|
|
185
|
+
this.popover.style.minHeight = '180px';
|
|
186
|
+
}
|
|
187
|
+
}
|