@sequent-org/moodboard 1.2.118 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (122) hide show
  1. package/package.json +11 -1
  2. package/src/assets/icons/rotate-icon.svg +1 -1
  3. package/src/core/HistoryManager.js +16 -16
  4. package/src/core/KeyboardManager.js +48 -539
  5. package/src/core/PixiEngine.js +9 -9
  6. package/src/core/SaveManager.js +56 -31
  7. package/src/core/bootstrap/CoreInitializer.js +65 -0
  8. package/src/core/commands/DeleteObjectCommand.js +8 -0
  9. package/src/core/commands/GroupDeleteCommand.js +75 -0
  10. package/src/core/commands/GroupRotateCommand.js +6 -0
  11. package/src/core/commands/UpdateContentCommand.js +52 -0
  12. package/src/core/commands/UpdateFramePropertiesCommand.js +98 -0
  13. package/src/core/commands/UpdateFrameTypeCommand.js +85 -0
  14. package/src/core/commands/UpdateNoteStyleCommand.js +88 -0
  15. package/src/core/commands/UpdateTextStyleCommand.js +90 -0
  16. package/src/core/commands/index.js +6 -0
  17. package/src/core/events/Events.js +7 -0
  18. package/src/core/flows/ClipboardFlow.js +553 -0
  19. package/src/core/flows/LayerAndViewportFlow.js +283 -0
  20. package/src/core/flows/ObjectLifecycleFlow.js +336 -0
  21. package/src/core/flows/SaveFlow.js +34 -0
  22. package/src/core/flows/TransformFlow.js +277 -0
  23. package/src/core/flows/TransformFlowResizeHelpers.js +83 -0
  24. package/src/core/index.js +41 -1765
  25. package/src/core/keyboard/KeyboardClipboardImagePaste.js +190 -0
  26. package/src/core/keyboard/KeyboardContextGuards.js +35 -0
  27. package/src/core/keyboard/KeyboardEventRouter.js +92 -0
  28. package/src/core/keyboard/KeyboardSelectionActions.js +103 -0
  29. package/src/core/keyboard/KeyboardShortcutMap.js +31 -0
  30. package/src/core/keyboard/KeyboardToolSwitching.js +26 -0
  31. package/src/core/rendering/ObjectRenderer.js +3 -7
  32. package/src/grid/BaseGrid.js +26 -0
  33. package/src/grid/CrossGrid.js +7 -6
  34. package/src/grid/DotGrid.js +89 -33
  35. package/src/grid/DotGridZoomPhases.js +42 -0
  36. package/src/grid/LineGrid.js +22 -21
  37. package/src/moodboard/MoodBoard.js +31 -532
  38. package/src/moodboard/bootstrap/MoodBoardInitializer.js +47 -0
  39. package/src/moodboard/bootstrap/MoodBoardManagersFactory.js +38 -0
  40. package/src/moodboard/bootstrap/MoodBoardUiFactory.js +109 -0
  41. package/src/moodboard/integration/MoodBoardEventBindings.js +65 -0
  42. package/src/moodboard/integration/MoodBoardLoadApi.js +82 -0
  43. package/src/moodboard/integration/MoodBoardScreenshotApi.js +33 -0
  44. package/src/moodboard/integration/MoodBoardScreenshotCanvas.js +98 -0
  45. package/src/moodboard/lifecycle/MoodBoardDestroyer.js +97 -0
  46. package/src/objects/FileObject.js +17 -6
  47. package/src/objects/FrameObject.js +50 -10
  48. package/src/objects/NoteObject.js +5 -4
  49. package/src/services/BoardService.js +42 -2
  50. package/src/services/FrameService.js +83 -42
  51. package/src/services/ResizePolicyService.js +152 -0
  52. package/src/services/SettingsApplier.js +7 -2
  53. package/src/services/ZoomPanController.js +35 -9
  54. package/src/tools/ToolManager.js +30 -537
  55. package/src/tools/board-tools/PanTool.js +5 -11
  56. package/src/tools/manager/ToolActivationController.js +49 -0
  57. package/src/tools/manager/ToolEventRouter.js +396 -0
  58. package/src/tools/manager/ToolManagerGuards.js +33 -0
  59. package/src/tools/manager/ToolManagerLifecycle.js +110 -0
  60. package/src/tools/manager/ToolRegistry.js +33 -0
  61. package/src/tools/object-tools/DrawingTool.js +48 -14
  62. package/src/tools/object-tools/PlacementTool.js +50 -1049
  63. package/src/tools/object-tools/PlacementToolV2.js +88 -0
  64. package/src/tools/object-tools/SelectTool.js +174 -2681
  65. package/src/tools/object-tools/placement/GhostController.js +504 -0
  66. package/src/tools/object-tools/placement/PlacementCoordinateResolver.js +20 -0
  67. package/src/tools/object-tools/placement/PlacementEventsBridge.js +91 -0
  68. package/src/tools/object-tools/placement/PlacementInputRouter.js +267 -0
  69. package/src/tools/object-tools/placement/PlacementPayloadFactory.js +111 -0
  70. package/src/tools/object-tools/placement/PlacementSessionStore.js +18 -0
  71. package/src/tools/object-tools/selection/BoxSelectController.js +0 -5
  72. package/src/tools/object-tools/selection/CloneFlowController.js +71 -0
  73. package/src/tools/object-tools/selection/CoordinateMapper.js +10 -0
  74. package/src/tools/object-tools/selection/CursorController.js +78 -0
  75. package/src/tools/object-tools/selection/FileNameInlineEditorController.js +184 -0
  76. package/src/tools/object-tools/selection/HitTestService.js +102 -0
  77. package/src/tools/object-tools/selection/InlineEditorController.js +24 -0
  78. package/src/tools/object-tools/selection/InlineEditorDomFactory.js +50 -0
  79. package/src/tools/object-tools/selection/InlineEditorListenersRegistry.js +14 -0
  80. package/src/tools/object-tools/selection/InlineEditorPositioningService.js +25 -0
  81. package/src/tools/object-tools/selection/NoteInlineEditorController.js +113 -0
  82. package/src/tools/object-tools/selection/SelectInputRouter.js +267 -0
  83. package/src/tools/object-tools/selection/SelectToolLifecycleController.js +128 -0
  84. package/src/tools/object-tools/selection/SelectToolSetup.js +134 -0
  85. package/src/tools/object-tools/selection/SelectionOverlayService.js +81 -0
  86. package/src/tools/object-tools/selection/SelectionStateController.js +91 -0
  87. package/src/tools/object-tools/selection/TextEditorDomFactory.js +65 -0
  88. package/src/tools/object-tools/selection/TextEditorInteractionController.js +266 -0
  89. package/src/tools/object-tools/selection/TextEditorLifecycleRegistry.js +90 -0
  90. package/src/tools/object-tools/selection/TextEditorPositioningService.js +158 -0
  91. package/src/tools/object-tools/selection/TextEditorSyncService.js +110 -0
  92. package/src/tools/object-tools/selection/TextInlineEditorController.js +457 -0
  93. package/src/tools/object-tools/selection/TransformInteractionController.js +466 -0
  94. package/src/ui/FilePropertiesPanel.js +61 -32
  95. package/src/ui/FramePropertiesPanel.js +176 -101
  96. package/src/ui/HtmlHandlesLayer.js +121 -976
  97. package/src/ui/MapPanel.js +12 -7
  98. package/src/ui/NotePropertiesPanel.js +17 -2
  99. package/src/ui/TextPropertiesPanel.js +124 -738
  100. package/src/ui/Toolbar.js +71 -1180
  101. package/src/ui/Topbar.js +23 -25
  102. package/src/ui/ZoomPanel.js +16 -5
  103. package/src/ui/handles/GroupSelectionHandlesController.js +29 -0
  104. package/src/ui/handles/HandlesDomRenderer.js +278 -0
  105. package/src/ui/handles/HandlesEventBridge.js +102 -0
  106. package/src/ui/handles/HandlesInteractionController.js +772 -0
  107. package/src/ui/handles/HandlesPositioningService.js +206 -0
  108. package/src/ui/handles/SingleSelectionHandlesController.js +22 -0
  109. package/src/ui/styles/toolbar.css +2 -0
  110. package/src/ui/styles/workspace.css +13 -6
  111. package/src/ui/text-properties/TextPropertiesPanelBindings.js +92 -0
  112. package/src/ui/text-properties/TextPropertiesPanelEventBridge.js +77 -0
  113. package/src/ui/text-properties/TextPropertiesPanelMapper.js +173 -0
  114. package/src/ui/text-properties/TextPropertiesPanelRenderer.js +434 -0
  115. package/src/ui/text-properties/TextPropertiesPanelState.js +39 -0
  116. package/src/ui/toolbar/ToolbarActionRouter.js +193 -0
  117. package/src/ui/toolbar/ToolbarDialogsController.js +186 -0
  118. package/src/ui/toolbar/ToolbarPopupsController.js +662 -0
  119. package/src/ui/toolbar/ToolbarRenderer.js +97 -0
  120. package/src/ui/toolbar/ToolbarStateController.js +79 -0
  121. package/src/ui/toolbar/ToolbarTooltipController.js +52 -0
  122. package/src/utils/emojiLoaderNoBundler.js +1 -1
@@ -0,0 +1,283 @@
1
+ import { Events } from '../events/Events.js';
2
+ import {
3
+ ReorderZCommand,
4
+ GroupReorderZCommand,
5
+ GroupMoveCommand,
6
+ MoveObjectCommand
7
+ } from '../commands/index.js';
8
+
9
+ export function setupLayerAndViewportFlow(core) {
10
+ const applyZOrderFromState = () => {
11
+ const arr = core.state.state.objects || [];
12
+ core.pixi.app.stage.sortableChildren = true;
13
+ for (let i = 0; i < arr.length; i++) {
14
+ const id = arr[i]?.id;
15
+ const pixi = id ? core.pixi.objects.get(id) : null;
16
+ if (pixi) pixi.zIndex = i;
17
+ }
18
+ };
19
+
20
+ core.eventBus.on(Events.UI.LayerBringToFront, ({ objectId }) => {
21
+ const arr = core.state.state.objects || [];
22
+ const from = arr.findIndex(o => o.id === objectId);
23
+ if (from === -1) return;
24
+ const to = arr.length - 1;
25
+ if (from === to) return;
26
+ const cmd = new ReorderZCommand(core, objectId, from, to);
27
+ cmd.setEventBus(core.eventBus);
28
+ core.history.executeCommand(cmd);
29
+ });
30
+ core.eventBus.on(Events.UI.LayerBringForward, ({ objectId }) => {
31
+ const arr = core.state.state.objects || [];
32
+ const from = arr.findIndex(o => o.id === objectId);
33
+ if (from === -1) return;
34
+ const to = Math.min(from + 1, arr.length - 1);
35
+ if (from === to) return;
36
+ const cmd = new ReorderZCommand(core, objectId, from, to);
37
+ cmd.setEventBus(core.eventBus);
38
+ core.history.executeCommand(cmd);
39
+ });
40
+ core.eventBus.on(Events.UI.LayerSendBackward, ({ objectId }) => {
41
+ const arr = core.state.state.objects || [];
42
+ const from = arr.findIndex(o => o.id === objectId);
43
+ if (from === -1) return;
44
+ const to = Math.max(from - 1, 0);
45
+ if (from === to) return;
46
+ const cmd = new ReorderZCommand(core, objectId, from, to);
47
+ cmd.setEventBus(core.eventBus);
48
+ core.history.executeCommand(cmd);
49
+ });
50
+ core.eventBus.on(Events.UI.LayerSendToBack, ({ objectId }) => {
51
+ const arr = core.state.state.objects || [];
52
+ const from = arr.findIndex(o => o.id === objectId);
53
+ if (from === -1) return;
54
+ const to = 0;
55
+ if (from === to) return;
56
+ const cmd = new ReorderZCommand(core, objectId, from, to);
57
+ cmd.setEventBus(core.eventBus);
58
+ core.history.executeCommand(cmd);
59
+ });
60
+
61
+ const getSelection = () => {
62
+ const ids = core.toolManager.getActiveTool()?.name === 'select'
63
+ ? Array.from(core.toolManager.getActiveTool().selectedObjects || [])
64
+ : [];
65
+ return ids;
66
+ };
67
+
68
+ core.eventBus.on(Events.UI.LayerGroupBringToFront, () => {
69
+ const ids = getSelection();
70
+ if (ids.length === 0) return;
71
+ const cmd = new GroupReorderZCommand(core, ids, 'front');
72
+ cmd.setEventBus(core.eventBus);
73
+ core.history.executeCommand(cmd);
74
+ });
75
+ core.eventBus.on(Events.UI.LayerGroupBringForward, () => {
76
+ const ids = getSelection();
77
+ if (ids.length === 0) return;
78
+ const cmd = new GroupReorderZCommand(core, ids, 'forward');
79
+ cmd.setEventBus(core.eventBus);
80
+ core.history.executeCommand(cmd);
81
+ });
82
+ core.eventBus.on(Events.UI.LayerGroupSendBackward, () => {
83
+ const ids = getSelection();
84
+ if (ids.length === 0) return;
85
+ const cmd = new GroupReorderZCommand(core, ids, 'backward');
86
+ cmd.setEventBus(core.eventBus);
87
+ core.history.executeCommand(cmd);
88
+ });
89
+ core.eventBus.on(Events.UI.LayerGroupSendToBack, () => {
90
+ const ids = getSelection();
91
+ if (ids.length === 0) return;
92
+ const cmd = new GroupReorderZCommand(core, ids, 'back');
93
+ cmd.setEventBus(core.eventBus);
94
+ core.history.executeCommand(cmd);
95
+ });
96
+
97
+ core.eventBus.on(Events.Tool.DragStart, (data) => {
98
+ const pixiObject = core.pixi.objects.get(data.object);
99
+ if (pixiObject) {
100
+ const halfW = (pixiObject.width || 0) / 2;
101
+ const halfH = (pixiObject.height || 0) / 2;
102
+ core.dragStartPosition = { x: pixiObject.x - halfW, y: pixiObject.y - halfH };
103
+ }
104
+ });
105
+
106
+ core.eventBus.on(Events.Tool.PanUpdate, ({ delta }) => {
107
+ if (core.pixi.worldLayer) {
108
+ core.pixi.worldLayer.x += delta.x;
109
+ core.pixi.worldLayer.y += delta.y;
110
+ }
111
+ if (core.pixi.gridLayer) {
112
+ core.pixi.gridLayer.x += delta.x;
113
+ core.pixi.gridLayer.y += delta.y;
114
+ }
115
+ if (!core.pixi.worldLayer) {
116
+ const stage = core.pixi.app.stage;
117
+ stage.x += delta.x;
118
+ stage.y += delta.y;
119
+ }
120
+ try {
121
+ const world = core.pixi.worldLayer || core.pixi.app.stage;
122
+ core.eventBus.emit(Events.Grid.BoardDataChanged, {
123
+ settings: { pan: { x: world.x || 0, y: world.y || 0 } }
124
+ });
125
+ } catch (_) {}
126
+ core.eventBus.emit(Events.Viewport.Changed);
127
+ });
128
+
129
+ core.eventBus.on(Events.UI.ZoomSelection, () => {
130
+ const selected = core.selectTool ? Array.from(core.selectTool.selectedObjects || []) : [];
131
+ if (!selected || selected.length === 0) return;
132
+ const objs = core.state.state.objects || [];
133
+ let minX = Infinity;
134
+ let minY = Infinity;
135
+ let maxX = -Infinity;
136
+ let maxY = -Infinity;
137
+ for (const o of objs) {
138
+ if (!selected.includes(o.id)) continue;
139
+ minX = Math.min(minX, o.position.x);
140
+ minY = Math.min(minY, o.position.y);
141
+ maxX = Math.max(maxX, o.position.x + (o.width || 0));
142
+ maxY = Math.max(maxY, o.position.y + (o.height || 0));
143
+ }
144
+ if (!isFinite(minX)) return;
145
+ const bboxW = Math.max(1, maxX - minX);
146
+ const bboxH = Math.max(1, maxY - minY);
147
+ const viewW = core.pixi.app.view.clientWidth;
148
+ const viewH = core.pixi.app.view.clientHeight;
149
+ const padding = 40;
150
+ const scaleX = (viewW - padding) / bboxW;
151
+ const scaleY = (viewH - padding) / bboxH;
152
+ const newScale = Math.max(0.02, Math.min(5, Math.min(scaleX, scaleY)));
153
+ const world = core.pixi.worldLayer || core.pixi.app.stage;
154
+ const worldCenterX = minX + bboxW / 2;
155
+ const worldCenterY = minY + bboxH / 2;
156
+ world.scale.set(newScale);
157
+ world.x = viewW / 2 - worldCenterX * newScale;
158
+ world.y = viewH / 2 - worldCenterY * newScale;
159
+ core.eventBus.emit(Events.UI.ZoomPercent, { percentage: Math.round(newScale * 100) });
160
+ core.eventBus.emit(Events.Viewport.Changed);
161
+ });
162
+
163
+ core.eventBus.on(Events.UI.MinimapGetData, (data) => {
164
+ const world = core.pixi.worldLayer || core.pixi.app.stage;
165
+ const view = core.pixi.app.view;
166
+ const scale = world?.scale?.x || 1;
167
+
168
+ const objects = (core.state.state.objects || []).map((o) => ({
169
+ id: o.id,
170
+ x: o.position?.x ?? 0,
171
+ y: o.position?.y ?? 0,
172
+ width: o.width ?? 0,
173
+ height: o.height ?? 0,
174
+ rotation: o.rotation ?? (o.transform?.rotation ?? 0)
175
+ }));
176
+
177
+ data.world = { x: world.x || 0, y: world.y || 0, scale };
178
+ data.view = { width: view.clientWidth, height: view.clientHeight };
179
+ data.objects = objects;
180
+ });
181
+
182
+ core.eventBus.on(Events.UI.MinimapCenterOn, ({ worldX, worldY }) => {
183
+ const world = core.pixi.worldLayer || core.pixi.app.stage;
184
+ const view = core.pixi.app.view;
185
+ const s = world?.scale?.x || 1;
186
+ world.x = view.clientWidth / 2 - worldX * s;
187
+ world.y = view.clientHeight / 2 - worldY * s;
188
+ });
189
+
190
+ core.eventBus.on(Events.Tool.GroupDragStart, (data) => {
191
+ core._groupDragStart = new Map();
192
+ for (const id of data.objects) {
193
+ const pixiObject = core.pixi.objects.get(id);
194
+ if (pixiObject) core._groupDragStart.set(id, { x: pixiObject.x, y: pixiObject.y });
195
+ }
196
+ });
197
+
198
+ core.eventBus.on(Events.Tool.GroupDragUpdate, (data) => {
199
+ const { dx, dy } = data.delta;
200
+ for (const id of data.objects) {
201
+ const pixiObject = core.pixi.objects.get(id);
202
+ if (!pixiObject) continue;
203
+ const startCenter = core._groupDragStart.get(id) || { x: pixiObject.x, y: pixiObject.y };
204
+ const newCenter = { x: startCenter.x + dx, y: startCenter.y + dy };
205
+ pixiObject.x = newCenter.x;
206
+ pixiObject.y = newCenter.y;
207
+ const obj = core.state.state.objects.find(o => o.id === id);
208
+ if (obj) {
209
+ const halfW = (pixiObject.width || 0) / 2;
210
+ const halfH = (pixiObject.height || 0) / 2;
211
+ obj.position.x = newCenter.x - halfW;
212
+ obj.position.y = newCenter.y - halfH;
213
+ }
214
+ }
215
+ core.state.markDirty();
216
+ });
217
+
218
+ core.eventBus.on(Events.Tool.GroupDragEnd, (data) => {
219
+ const moves = [];
220
+ for (const id of data.objects) {
221
+ const start = core._groupDragStart?.get(id);
222
+ const pixiObject = core.pixi.objects.get(id);
223
+ if (!start || !pixiObject) continue;
224
+ const finalPosition = { x: pixiObject.x, y: pixiObject.y };
225
+ if (start.x !== finalPosition.x || start.y !== finalPosition.y) {
226
+ moves.push({ id, from: start, to: finalPosition });
227
+ }
228
+ }
229
+ if (moves.length > 0) {
230
+ const cmd = new GroupMoveCommand(core, moves, false);
231
+ cmd.setEventBus(core.eventBus);
232
+ core.history.executeCommand(cmd);
233
+ }
234
+ core._groupDragStart = null;
235
+ });
236
+
237
+ core.eventBus.on(Events.Tool.DragUpdate, (data) => {
238
+ core.updateObjectPositionDirect(data.object, data.position);
239
+ });
240
+
241
+ core.eventBus.on(Events.Tool.DragEnd, (data) => {
242
+ if (core.dragStartPosition) {
243
+ const pixiObject = core.pixi.objects.get(data.object);
244
+ if (pixiObject) {
245
+ const objState = core.state.state.objects.find(o => o.id === data.object);
246
+ const finalPosition = objState && objState.position ? { x: objState.position.x, y: objState.position.y } : { x: 0, y: 0 };
247
+
248
+ if (core.dragStartPosition.x !== finalPosition.x ||
249
+ core.dragStartPosition.y !== finalPosition.y) {
250
+ const moved = core.state.state.objects.find(o => o.id === data.object);
251
+ if (moved && moved.type === 'frame') {
252
+ const attachments = core._getFrameChildren(moved.id);
253
+ const moves = [];
254
+ moves.push({ id: moved.id, from: core.dragStartPosition, to: finalPosition });
255
+ const dx = finalPosition.x - core.dragStartPosition.x;
256
+ const dy = finalPosition.y - core.dragStartPosition.y;
257
+ for (const childId of attachments) {
258
+ const child = core.state.state.objects.find(o => o.id === childId);
259
+ if (!child) continue;
260
+ const start = core._frameDragChildStart?.get(childId);
261
+ const from = start ? { x: start.x, y: start.y } : { x: (child.position.x - dx), y: (child.position.y - dy) };
262
+ const to = { x: child.position.x, y: child.position.y };
263
+ moves.push({ id: childId, from, to });
264
+ }
265
+ const cmd = new GroupMoveCommand(core, moves, true);
266
+ cmd.setEventBus(core.eventBus);
267
+ core.history.executeCommand(cmd);
268
+ } else {
269
+ const command = new MoveObjectCommand(
270
+ core,
271
+ data.object,
272
+ core.dragStartPosition,
273
+ finalPosition
274
+ );
275
+ command.setEventBus(core.eventBus);
276
+ core.history.executeCommand(command);
277
+ }
278
+ }
279
+ }
280
+ core.dragStartPosition = null;
281
+ }
282
+ });
283
+ }
@@ -0,0 +1,336 @@
1
+ import { Events } from '../events/Events.js';
2
+ import {
3
+ EditFileNameCommand,
4
+ GroupDeleteCommand,
5
+ UpdateContentCommand,
6
+ UpdateTextStyleCommand,
7
+ UpdateNoteStyleCommand,
8
+ UpdateFramePropertiesCommand,
9
+ } from '../commands/index.js';
10
+
11
+ const TEXT_STYLE_PROPS = ['fontFamily', 'fontSize', 'color', 'backgroundColor'];
12
+ const TEXT_STYLE_DEFAULTS = {
13
+ fontFamily: 'Roboto, Arial, sans-serif',
14
+ fontSize: 18,
15
+ color: '#000000',
16
+ backgroundColor: 'transparent',
17
+ };
18
+
19
+ const NOTE_STYLE_PROPS = ['fontFamily', 'fontSize', 'textColor', 'backgroundColor'];
20
+ const NOTE_STYLE_DEFAULTS = {
21
+ fontFamily: 'Caveat, Arial, cursive',
22
+ fontSize: 32,
23
+ textColor: 0x1a1a1a,
24
+ backgroundColor: 0xfff9c4,
25
+ };
26
+
27
+ /**
28
+ * Если updates.properties содержит ровно одно свойство стиля записки — создаёт UpdateNoteStyleCommand.
29
+ * @returns {boolean} true, если команда создана и применена
30
+ */
31
+ function tryCreateNoteStyleCommand(core, object, objectId, updates) {
32
+ if (object.type !== 'note') return false;
33
+ if (!updates.properties || Object.keys(updates).length !== 1) return false;
34
+
35
+ const propKeys = Object.keys(updates.properties);
36
+ if (propKeys.length !== 1 || !NOTE_STYLE_PROPS.includes(propKeys[0])) return false;
37
+
38
+ const property = propKeys[0];
39
+ const newValue = updates.properties[property];
40
+
41
+ const oldValue = object.properties?.[property] ?? NOTE_STYLE_DEFAULTS[property];
42
+ if (oldValue === newValue) return false;
43
+
44
+ const command = new UpdateNoteStyleCommand(core, objectId, property, oldValue, newValue);
45
+ core.history.executeCommand(command);
46
+ return true;
47
+ }
48
+
49
+ /**
50
+ * Если updates содержит ровно одно свойство стиля текста — создаёт UpdateTextStyleCommand и выполняет её.
51
+ * @returns {boolean} true, если команда создана и применена (дальнейшая обработка не нужна)
52
+ */
53
+ function tryCreateTextStyleCommand(core, object, objectId, updates) {
54
+ if (object.type !== 'text' && object.type !== 'simple-text') return false;
55
+
56
+ let property = null;
57
+ let newValue = null;
58
+
59
+ if (updates.properties?.fontFamily !== undefined && Object.keys(updates).length === 1) {
60
+ property = 'fontFamily';
61
+ newValue = updates.properties.fontFamily;
62
+ } else if (updates.fontSize !== undefined && !updates.properties && Object.keys(updates).length === 1) {
63
+ property = 'fontSize';
64
+ newValue = typeof updates.fontSize === 'string' ? parseInt(updates.fontSize, 10) : updates.fontSize;
65
+ } else if (updates.color !== undefined && !updates.properties && Object.keys(updates).length === 1) {
66
+ property = 'color';
67
+ newValue = updates.color;
68
+ } else if (updates.backgroundColor !== undefined && !updates.properties && Object.keys(updates).length === 1) {
69
+ property = 'backgroundColor';
70
+ newValue = updates.backgroundColor;
71
+ }
72
+
73
+ if (!property || !TEXT_STYLE_PROPS.includes(property)) return false;
74
+
75
+ const oldValue = property === 'fontFamily'
76
+ ? (object.properties?.fontFamily ?? TEXT_STYLE_DEFAULTS.fontFamily)
77
+ : (object[property] ?? object.properties?.[property] ?? TEXT_STYLE_DEFAULTS[property]);
78
+
79
+ if (oldValue === newValue) return false;
80
+
81
+ const command = new UpdateTextStyleCommand(core, objectId, property, oldValue, newValue);
82
+ core.history.executeCommand(command);
83
+ return true;
84
+ }
85
+
86
+ const FRAME_PROP_KEYS = ['title'];
87
+
88
+ /**
89
+ * Если updates содержит одно свойство фрейма — создаёт UpdateFramePropertiesCommand.
90
+ * Поддерживает: title, backgroundColor, type, lockedAspect.
91
+ * @returns {boolean} true, если команда создана и применена
92
+ */
93
+ function tryCreateFramePropertiesCommand(core, object, objectId, updates) {
94
+ if (object.type !== 'frame') return false;
95
+
96
+ let property = null;
97
+ let oldValue = null;
98
+ let newValue = null;
99
+
100
+ if (updates.backgroundColor !== undefined && !updates.properties) {
101
+ property = 'backgroundColor';
102
+ newValue = updates.backgroundColor;
103
+ oldValue = object.backgroundColor ?? 0xFFFFFF;
104
+ } else if (updates.properties) {
105
+ const propKeys = Object.keys(updates.properties);
106
+ if (propKeys.length === 1 && propKeys[0] === 'title') {
107
+ property = 'title';
108
+ newValue = updates.properties.title;
109
+ oldValue = object.properties?.title ?? '';
110
+ }
111
+ // type и lockedAspect — только через UpdateFrameTypeCommand в панели (один шаг в истории)
112
+ }
113
+
114
+ if (!property) return false;
115
+ const supported = [...FRAME_PROP_KEYS, 'backgroundColor'];
116
+ if (!supported.includes(property)) return false;
117
+ if (oldValue === newValue) return false;
118
+
119
+ const command = new UpdateFramePropertiesCommand(core, objectId, property, oldValue, newValue);
120
+ core.history.executeCommand(command);
121
+ return true;
122
+ }
123
+
124
+ export function setupObjectLifecycleFlow(core) {
125
+ core.eventBus.on(Events.Tool.ObjectsDelete, ({ objects }) => {
126
+ const ids = Array.isArray(objects) ? objects : [];
127
+ if (ids.length === 0) return;
128
+ const command = new GroupDeleteCommand(core, ids);
129
+ core.history.executeCommand(command);
130
+ });
131
+
132
+ core.eventBus.on(Events.Tool.HitTest, (data) => {
133
+ const result = core.pixi.hitTest(data.x, data.y);
134
+ data.result = result;
135
+ });
136
+
137
+ core.eventBus.on(Events.Tool.GetObjectPosition, (data) => {
138
+ const pixiObject = core.pixi.objects.get(data.objectId);
139
+ if (!pixiObject) return;
140
+ const halfW = (pixiObject.width || 0) / 2;
141
+ const halfH = (pixiObject.height || 0) / 2;
142
+ data.position = { x: pixiObject.x - halfW, y: pixiObject.y - halfH };
143
+ });
144
+
145
+ core.eventBus.on(Events.Tool.GetObjectPixi, (data) => {
146
+ const pixiObject = core.pixi.objects.get(data.objectId);
147
+ data.pixiObject = pixiObject || null;
148
+ });
149
+
150
+ core.eventBus.on(Events.Tool.GetAllObjects, (data) => {
151
+ const result = [];
152
+ for (const [objectId, pixiObject] of core.pixi.objects.entries()) {
153
+ const bounds = pixiObject.getBounds();
154
+ result.push({
155
+ id: objectId,
156
+ pixi: pixiObject,
157
+ bounds: { x: bounds.x, y: bounds.y, width: bounds.width, height: bounds.height }
158
+ });
159
+ }
160
+ data.objects = result;
161
+ });
162
+
163
+ core.eventBus.on(Events.Tool.SelectionAll, () => {
164
+ if (core.toolManager?.getActiveTool()?.name !== 'select') return;
165
+ const req = { objects: [] };
166
+ core.eventBus.emit(Events.Tool.GetAllObjects, req);
167
+ const ids = (req.objects || []).map((o) => o.id);
168
+ if (ids.length > 0 && core.selectTool) {
169
+ core.selectTool.setSelection(ids);
170
+ core.selectTool.updateResizeHandles();
171
+ }
172
+ });
173
+
174
+ core.eventBus.on(Events.Tool.GetObjectSize, (data) => {
175
+ const objects = core.state.getObjects();
176
+ const object = objects.find(obj => obj.id === data.objectId);
177
+ if (object) {
178
+ data.size = { width: object.width, height: object.height };
179
+ }
180
+ });
181
+
182
+ core.eventBus.on(Events.Tool.GetObjectRotation, (data) => {
183
+ const pixiObject = core.pixi.objects.get(data.objectId);
184
+ if (pixiObject) {
185
+ data.rotation = pixiObject.rotation * 180 / Math.PI;
186
+ } else {
187
+ data.rotation = 0;
188
+ }
189
+ });
190
+
191
+ core.eventBus.on(Events.Tool.UpdateObjectContent, (data) => {
192
+ const { objectId, content } = data;
193
+ if (objectId && content !== undefined) {
194
+ core.pixi.updateObjectContent(objectId, content);
195
+ }
196
+ });
197
+
198
+ core.eventBus.on(Events.Tool.HideObjectText, (data) => {
199
+ const { objectId } = data;
200
+ if (objectId) {
201
+ core.pixi.hideObjectText(objectId);
202
+ }
203
+ });
204
+
205
+ core.eventBus.on(Events.Tool.ShowObjectText, (data) => {
206
+ const { objectId } = data;
207
+ if (objectId) {
208
+ core.pixi.showObjectText(objectId);
209
+ }
210
+ });
211
+
212
+ core.eventBus.on(Events.Tool.FindObjectByPosition, (data) => {
213
+ const { position, type } = data;
214
+ if (position && type) {
215
+ const foundObject = core.pixi.findObjectByPosition(position, type);
216
+ data.foundObject = foundObject;
217
+ }
218
+ });
219
+
220
+ core.eventBus.on(Events.Object.StateChanged, (data) => {
221
+ const { objectId, updates } = data;
222
+ if (!objectId || !updates || !core.state) return;
223
+
224
+ const objects = core.state.getObjects();
225
+ const object = objects.find(obj => obj.id === objectId);
226
+ if (!object) return;
227
+
228
+ const noteStyleChange = tryCreateNoteStyleCommand(core, object, objectId, updates);
229
+ if (noteStyleChange) return;
230
+
231
+ const textStyleChange = tryCreateTextStyleCommand(core, object, objectId, updates);
232
+ if (textStyleChange) return;
233
+
234
+ const framePropsChange = tryCreateFramePropertiesCommand(core, object, objectId, updates);
235
+ if (framePropsChange) return;
236
+
237
+ if (updates.properties && object.properties) {
238
+ Object.assign(object.properties, updates.properties);
239
+ }
240
+
241
+ const topLevelUpdates = { ...updates };
242
+ delete topLevelUpdates.properties;
243
+ Object.assign(object, topLevelUpdates);
244
+
245
+ const pixiObject = core.pixi.objects.get(objectId);
246
+ if (pixiObject && pixiObject._mb && pixiObject._mb.instance) {
247
+ const instance = pixiObject._mb.instance;
248
+
249
+ if (object.type === 'frame' && updates.properties && updates.properties.title !== undefined) {
250
+ if (instance.setTitle) {
251
+ instance.setTitle(updates.properties.title);
252
+ }
253
+ }
254
+
255
+ if (object.type === 'frame' && updates.backgroundColor !== undefined) {
256
+ if (instance.setBackgroundColor) {
257
+ instance.setBackgroundColor(updates.backgroundColor);
258
+ }
259
+ }
260
+
261
+ if (object.type === 'note' && updates.properties) {
262
+ if (instance.setStyle) {
263
+ const styleUpdates = {};
264
+ if (updates.properties.backgroundColor !== undefined) {
265
+ styleUpdates.backgroundColor = updates.properties.backgroundColor;
266
+ }
267
+ if (updates.properties.borderColor !== undefined) {
268
+ styleUpdates.borderColor = updates.properties.borderColor;
269
+ }
270
+ if (updates.properties.textColor !== undefined) {
271
+ styleUpdates.textColor = updates.properties.textColor;
272
+ }
273
+ if (updates.properties.fontSize !== undefined) {
274
+ styleUpdates.fontSize = updates.properties.fontSize;
275
+ }
276
+ if (updates.properties.fontFamily !== undefined) {
277
+ styleUpdates.fontFamily = updates.properties.fontFamily;
278
+ }
279
+
280
+ if (Object.keys(styleUpdates).length > 0) {
281
+ instance.setStyle(styleUpdates);
282
+ }
283
+ }
284
+ }
285
+ }
286
+
287
+ core.state.markDirty();
288
+ });
289
+
290
+ core.eventBus.on(Events.Object.FileNameChange, (data) => {
291
+ const { objectId, oldName, newName } = data;
292
+ if (objectId && oldName !== undefined && newName !== undefined) {
293
+ const command = new EditFileNameCommand(core, objectId, oldName, newName);
294
+ core.history.executeCommand(command);
295
+ }
296
+ });
297
+
298
+ core.eventBus.on(Events.Object.ContentChange, (data) => {
299
+ const { objectId, oldContent, newContent } = data;
300
+ if (objectId && oldContent !== undefined && newContent !== undefined && oldContent !== newContent) {
301
+ const command = new UpdateContentCommand(core, objectId, oldContent, newContent);
302
+ command.setEventBus(core.eventBus);
303
+ core.history.executeCommand(command);
304
+ }
305
+ });
306
+
307
+ core.eventBus.on('file:metadata:updated', (data) => {
308
+ const { objectId, metadata } = data;
309
+ if (objectId && metadata) {
310
+ const objects = core.state.getObjects();
311
+ const objectData = objects.find(obj => obj.id === objectId);
312
+
313
+ if (objectData && objectData.type === 'file') {
314
+ if (!objectData.properties) {
315
+ objectData.properties = {};
316
+ }
317
+
318
+ if (metadata.name && metadata.name !== objectData.properties.fileName) {
319
+ objectData.properties.fileName = metadata.name;
320
+
321
+ const pixiReq = { objectId, pixiObject: null };
322
+ core.eventBus.emit(Events.Tool.GetObjectPixi, pixiReq);
323
+
324
+ if (pixiReq.pixiObject && pixiReq.pixiObject._mb && pixiReq.pixiObject._mb.instance) {
325
+ const fileInstance = pixiReq.pixiObject._mb.instance;
326
+ if (typeof fileInstance.setFileName === 'function') {
327
+ fileInstance.setFileName(metadata.name);
328
+ }
329
+ }
330
+
331
+ core.state.markDirty();
332
+ }
333
+ }
334
+ }
335
+ });
336
+ }
@@ -0,0 +1,34 @@
1
+ import { Events } from '../events/Events.js';
2
+
3
+ export function setupSaveFlow(core) {
4
+ core.eventBus.on(Events.Save.GetBoardData, (requestData) => {
5
+ requestData.data = core.getBoardData();
6
+ });
7
+
8
+ core.eventBus.on(Events.Grid.BoardDataChanged, ({ grid }) => {
9
+ try {
10
+ if (grid) {
11
+ if (!core.state.state.board) core.state.state.board = {};
12
+ core.state.state.board.grid = grid;
13
+ core.state.markDirty();
14
+ }
15
+ } catch (_) {}
16
+ });
17
+
18
+ core.eventBus.on(Events.Save.StatusChanged, () => {
19
+ });
20
+
21
+ core.eventBus.on(Events.Save.Error, (data) => {
22
+ console.error('Save error:', data.error);
23
+ });
24
+
25
+ core.eventBus.on(Events.Save.Success, async () => {
26
+ try {
27
+ const result = await core.cleanupUnusedImages();
28
+ if (result.deletedCount > 0) {
29
+ }
30
+ } catch (error) {
31
+ console.warn('⚠️ Не удалось выполнить автоматическую очистку изображений:', error.message);
32
+ }
33
+ });
34
+ }