@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.
Files changed (123) hide show
  1. package/package.json +44 -0
  2. package/src/assets/icons/README.md +105 -0
  3. package/src/assets/icons/attachments.svg +3 -0
  4. package/src/assets/icons/clear.svg +5 -0
  5. package/src/assets/icons/comments.svg +3 -0
  6. package/src/assets/icons/emoji.svg +6 -0
  7. package/src/assets/icons/frame.svg +3 -0
  8. package/src/assets/icons/image.svg +3 -0
  9. package/src/assets/icons/note.svg +3 -0
  10. package/src/assets/icons/pan.svg +3 -0
  11. package/src/assets/icons/pencil.svg +3 -0
  12. package/src/assets/icons/redo.svg +3 -0
  13. package/src/assets/icons/select.svg +9 -0
  14. package/src/assets/icons/shapes.svg +3 -0
  15. package/src/assets/icons/text-add.svg +3 -0
  16. package/src/assets/icons/topbar/README.md +39 -0
  17. package/src/assets/icons/topbar/grid-cross.svg +6 -0
  18. package/src/assets/icons/topbar/grid-dot.svg +3 -0
  19. package/src/assets/icons/topbar/grid-line.svg +3 -0
  20. package/src/assets/icons/topbar/grid-off.svg +3 -0
  21. package/src/assets/icons/topbar/paint.svg +3 -0
  22. package/src/assets/icons/undo.svg +3 -0
  23. package/src/core/ApiClient.js +309 -0
  24. package/src/core/EventBus.js +42 -0
  25. package/src/core/HistoryManager.js +261 -0
  26. package/src/core/KeyboardManager.js +710 -0
  27. package/src/core/PixiEngine.js +439 -0
  28. package/src/core/SaveManager.js +381 -0
  29. package/src/core/StateManager.js +64 -0
  30. package/src/core/commands/BaseCommand.js +68 -0
  31. package/src/core/commands/CopyObjectCommand.js +44 -0
  32. package/src/core/commands/CreateObjectCommand.js +46 -0
  33. package/src/core/commands/DeleteObjectCommand.js +146 -0
  34. package/src/core/commands/EditFileNameCommand.js +107 -0
  35. package/src/core/commands/GroupMoveCommand.js +47 -0
  36. package/src/core/commands/GroupReorderZCommand.js +74 -0
  37. package/src/core/commands/GroupResizeCommand.js +37 -0
  38. package/src/core/commands/GroupRotateCommand.js +41 -0
  39. package/src/core/commands/MoveObjectCommand.js +89 -0
  40. package/src/core/commands/PasteObjectCommand.js +103 -0
  41. package/src/core/commands/ReorderZCommand.js +45 -0
  42. package/src/core/commands/ResizeObjectCommand.js +135 -0
  43. package/src/core/commands/RotateObjectCommand.js +70 -0
  44. package/src/core/commands/index.js +14 -0
  45. package/src/core/events/Events.js +147 -0
  46. package/src/core/index.js +1632 -0
  47. package/src/core/rendering/GeometryUtils.js +89 -0
  48. package/src/core/rendering/HitTestManager.js +186 -0
  49. package/src/core/rendering/LayerManager.js +137 -0
  50. package/src/core/rendering/ObjectRenderer.js +363 -0
  51. package/src/core/rendering/PixiRenderer.js +140 -0
  52. package/src/core/rendering/index.js +9 -0
  53. package/src/grid/BaseGrid.js +164 -0
  54. package/src/grid/CrossGrid.js +75 -0
  55. package/src/grid/DotGrid.js +148 -0
  56. package/src/grid/GridFactory.js +173 -0
  57. package/src/grid/LineGrid.js +115 -0
  58. package/src/index.js +2 -0
  59. package/src/moodboard/ActionHandler.js +114 -0
  60. package/src/moodboard/DataManager.js +114 -0
  61. package/src/moodboard/MoodBoard.js +359 -0
  62. package/src/moodboard/WorkspaceManager.js +103 -0
  63. package/src/objects/BaseObject.js +1 -0
  64. package/src/objects/CommentObject.js +115 -0
  65. package/src/objects/DrawingObject.js +114 -0
  66. package/src/objects/EmojiObject.js +98 -0
  67. package/src/objects/FileObject.js +318 -0
  68. package/src/objects/FrameObject.js +127 -0
  69. package/src/objects/ImageObject.js +72 -0
  70. package/src/objects/NoteObject.js +227 -0
  71. package/src/objects/ObjectFactory.js +61 -0
  72. package/src/objects/ShapeObject.js +134 -0
  73. package/src/objects/StampObject.js +0 -0
  74. package/src/objects/StickerObject.js +0 -0
  75. package/src/objects/TextObject.js +123 -0
  76. package/src/services/BoardService.js +85 -0
  77. package/src/services/FileUploadService.js +398 -0
  78. package/src/services/FrameService.js +138 -0
  79. package/src/services/ImageUploadService.js +246 -0
  80. package/src/services/ZOrderManager.js +50 -0
  81. package/src/services/ZoomPanController.js +78 -0
  82. package/src/src.7z +0 -0
  83. package/src/src.zip +0 -0
  84. package/src/src2.zip +0 -0
  85. package/src/tools/AlignmentGuides.js +326 -0
  86. package/src/tools/BaseTool.js +257 -0
  87. package/src/tools/ResizeHandles.js +381 -0
  88. package/src/tools/ToolManager.js +580 -0
  89. package/src/tools/board-tools/PanTool.js +43 -0
  90. package/src/tools/board-tools/ZoomTool.js +393 -0
  91. package/src/tools/object-tools/DrawingTool.js +404 -0
  92. package/src/tools/object-tools/PlacementTool.js +1005 -0
  93. package/src/tools/object-tools/SelectTool.js +2183 -0
  94. package/src/tools/object-tools/TextTool.js +416 -0
  95. package/src/tools/object-tools/selection/BoxSelectController.js +105 -0
  96. package/src/tools/object-tools/selection/GeometryUtils.js +101 -0
  97. package/src/tools/object-tools/selection/GroupDragController.js +61 -0
  98. package/src/tools/object-tools/selection/GroupResizeController.js +90 -0
  99. package/src/tools/object-tools/selection/GroupRotateController.js +61 -0
  100. package/src/tools/object-tools/selection/HandlesSync.js +96 -0
  101. package/src/tools/object-tools/selection/ResizeController.js +68 -0
  102. package/src/tools/object-tools/selection/RotateController.js +58 -0
  103. package/src/tools/object-tools/selection/SelectionModel.js +42 -0
  104. package/src/tools/object-tools/selection/SimpleDragController.js +45 -0
  105. package/src/ui/CommentPopover.js +187 -0
  106. package/src/ui/ContextMenu.js +340 -0
  107. package/src/ui/FilePropertiesPanel.js +298 -0
  108. package/src/ui/FramePropertiesPanel.js +462 -0
  109. package/src/ui/HtmlHandlesLayer.js +778 -0
  110. package/src/ui/HtmlTextLayer.js +279 -0
  111. package/src/ui/MapPanel.js +290 -0
  112. package/src/ui/NotePropertiesPanel.js +502 -0
  113. package/src/ui/SaveStatus.js +250 -0
  114. package/src/ui/TextPropertiesPanel.js +911 -0
  115. package/src/ui/Toolbar.js +1118 -0
  116. package/src/ui/Topbar.js +220 -0
  117. package/src/ui/ZoomPanel.js +116 -0
  118. package/src/ui/styles/workspace.css +854 -0
  119. package/src/utils/colors.js +0 -0
  120. package/src/utils/geometry.js +0 -0
  121. package/src/utils/iconLoader.js +270 -0
  122. package/src/utils/objectIdGenerator.js +17 -0
  123. 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
+ }