@sequent-org/moodboard 1.2.119 → 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 +6 -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 -1773
  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 -999
  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,553 @@
1
+ import { Events } from '../events/Events.js';
2
+ import { PasteObjectCommand } from '../commands/index.js';
3
+
4
+ export function setupClipboardFlow(core) {
5
+ core.eventBus.on(Events.UI.CopyObject, ({ objectId }) => {
6
+ if (!objectId) return;
7
+ core.copyObject(objectId);
8
+ });
9
+
10
+ core.eventBus.on(Events.UI.CopyGroup, () => {
11
+ if (core.toolManager.getActiveTool()?.name !== 'select') return;
12
+ const selected = Array.from(core.toolManager.getActiveTool().selectedObjects || []);
13
+ if (selected.length <= 1) return;
14
+ const objects = core.state.state.objects || [];
15
+ const groupData = selected
16
+ .map(id => objects.find(o => o.id === id))
17
+ .filter(Boolean)
18
+ .map(o => JSON.parse(JSON.stringify(o)));
19
+ if (groupData.length === 0) return;
20
+ core.clipboard = {
21
+ type: 'group',
22
+ data: groupData,
23
+ meta: { pasteCount: 0 }
24
+ };
25
+ });
26
+
27
+ core.eventBus.on(Events.UI.PasteAt, ({ x, y }) => {
28
+ if (!core.clipboard) return;
29
+ if (core.clipboard.type === 'object') {
30
+ core.pasteObject({ x, y });
31
+ } else if (core.clipboard.type === 'group') {
32
+ const group = core.clipboard;
33
+ const data = Array.isArray(group.data) ? group.data : [];
34
+ if (data.length === 0) return;
35
+
36
+ if (group.meta && group.meta.frameBundle) {
37
+ let minX = Infinity;
38
+ let minY = Infinity;
39
+ data.forEach(o => {
40
+ if (!o || !o.position) return;
41
+ minX = Math.min(minX, o.position.x);
42
+ minY = Math.min(minY, o.position.y);
43
+ });
44
+ if (!isFinite(minX) || !isFinite(minY)) return;
45
+ const baseX = minX;
46
+ const baseY = minY;
47
+
48
+ const frames = data.filter(o => o && o.type === 'frame');
49
+ if (frames.length !== 1) {
50
+ const newIds = [];
51
+ let pending = data.length;
52
+ const onPasted = (payload) => {
53
+ if (!payload || !payload.newId) return;
54
+ newIds.push(payload.newId);
55
+ pending -= 1;
56
+ if (pending === 0) {
57
+ core.eventBus.off(Events.Object.Pasted, onPasted);
58
+ requestAnimationFrame(() => {
59
+ if (core.selectTool && newIds.length > 0) {
60
+ core.selectTool.setSelection(newIds);
61
+ core.selectTool.updateResizeHandles();
62
+ }
63
+ });
64
+ }
65
+ };
66
+ core.eventBus.on(Events.Object.Pasted, onPasted);
67
+ data.forEach(orig => {
68
+ const cloned = JSON.parse(JSON.stringify(orig));
69
+ const targetPos = {
70
+ x: x + (cloned.position.x - baseX),
71
+ y: y + (cloned.position.y - baseY)
72
+ };
73
+ core.clipboard = { type: 'object', data: cloned };
74
+ const cmd = new PasteObjectCommand(core, targetPos);
75
+ cmd.setEventBus(core.eventBus);
76
+ core.history.executeCommand(cmd);
77
+ });
78
+ core.clipboard = group;
79
+ return;
80
+ }
81
+
82
+ const frameOriginal = frames[0];
83
+ const children = data.filter(o => o && o.id !== frameOriginal.id);
84
+ const totalToPaste = 1 + children.length;
85
+ const newIds = [];
86
+ let pastedCount = 0;
87
+ let newFrameId = null;
88
+
89
+ const onPasted = (payload) => {
90
+ if (!payload || !payload.newId) return;
91
+ newIds.push(payload.newId);
92
+ pastedCount += 1;
93
+ if (!newFrameId && payload.originalId === frameOriginal.id) {
94
+ newFrameId = payload.newId;
95
+ for (const child of children) {
96
+ const clonedChild = JSON.parse(JSON.stringify(child));
97
+ clonedChild.properties = clonedChild.properties || {};
98
+ clonedChild.properties.frameId = newFrameId;
99
+ const targetPos = {
100
+ x: x + (clonedChild.position.x - baseX),
101
+ y: y + (clonedChild.position.y - baseY)
102
+ };
103
+ core.clipboard = { type: 'object', data: clonedChild };
104
+ const cmdChild = new PasteObjectCommand(core, targetPos);
105
+ cmdChild.setEventBus(core.eventBus);
106
+ core.history.executeCommand(cmdChild);
107
+ }
108
+ }
109
+ if (pastedCount === totalToPaste) {
110
+ core.eventBus.off(Events.Object.Pasted, onPasted);
111
+ requestAnimationFrame(() => {
112
+ if (core.selectTool && newIds.length > 0) {
113
+ core.selectTool.setSelection(newIds);
114
+ core.selectTool.updateResizeHandles();
115
+ }
116
+ });
117
+ }
118
+ };
119
+ core.eventBus.on(Events.Object.Pasted, onPasted);
120
+
121
+ const frameClone = JSON.parse(JSON.stringify(frameOriginal));
122
+ core.clipboard = { type: 'object', data: frameClone };
123
+ const targetPosFrame = {
124
+ x: x + (frameClone.position.x - baseX),
125
+ y: y + (frameClone.position.y - baseY)
126
+ };
127
+ const cmdFrame = new PasteObjectCommand(core, targetPosFrame);
128
+ cmdFrame.setEventBus(core.eventBus);
129
+ core.history.executeCommand(cmdFrame);
130
+ core.clipboard = group;
131
+ return;
132
+ }
133
+
134
+ const newIds = [];
135
+ let pending = data.length;
136
+ const onPasted = (payload) => {
137
+ if (!payload || !payload.newId) return;
138
+ newIds.push(payload.newId);
139
+ pending -= 1;
140
+ if (pending === 0) {
141
+ core.eventBus.off(Events.Object.Pasted, onPasted);
142
+ requestAnimationFrame(() => {
143
+ if (core.selectTool && newIds.length > 0) {
144
+ core.selectTool.setSelection(newIds);
145
+ core.selectTool.updateResizeHandles();
146
+ }
147
+ });
148
+ }
149
+ };
150
+ core.eventBus.on(Events.Object.Pasted, onPasted);
151
+ data.forEach(orig => {
152
+ const cloned = JSON.parse(JSON.stringify(orig));
153
+ const targetPos = {
154
+ x: x + (cloned.position.x - minX),
155
+ y: y + (cloned.position.y - minY)
156
+ };
157
+ core.clipboard = { type: 'object', data: cloned };
158
+ const cmd = new PasteObjectCommand(core, targetPos);
159
+ cmd.setEventBus(core.eventBus);
160
+ core.history.executeCommand(cmd);
161
+ });
162
+ core.clipboard = group;
163
+ }
164
+ });
165
+
166
+ core._cursor = { x: null, y: null };
167
+ core.eventBus.on(Events.UI.CursorMove, ({ x, y }) => {
168
+ core._cursor.x = x;
169
+ core._cursor.y = y;
170
+ });
171
+
172
+ core.eventBus.on(Events.UI.PasteImage, ({ src, name, imageId }) => {
173
+ if (!src) return;
174
+ const view = core.pixi.app.view;
175
+ const world = core.pixi.worldLayer || core.pixi.app.stage;
176
+ const s = world?.scale?.x || 1;
177
+ const hasCursor = Number.isFinite(core._cursor.x) && Number.isFinite(core._cursor.y);
178
+
179
+ let screenX;
180
+ let screenY;
181
+ if (hasCursor) {
182
+ screenX = core._cursor.x;
183
+ screenY = core._cursor.y;
184
+ } else {
185
+ screenX = view.clientWidth / 2;
186
+ screenY = view.clientHeight / 2;
187
+ }
188
+
189
+ const worldX = (screenX - (world?.x || 0)) / s;
190
+ const worldY = (screenY - (world?.y || 0)) / s;
191
+
192
+ const placeWithAspect = (natW, natH) => {
193
+ let w = 300;
194
+ let h = 200;
195
+ if (natW > 0 && natH > 0) {
196
+ const ar = natW / natH;
197
+ w = 300;
198
+ h = Math.max(1, Math.round(w / ar));
199
+ }
200
+ const properties = { src, name, width: w, height: h };
201
+ const extraData = imageId ? { imageId } : {};
202
+ core.createObject('image', { x: Math.round(worldX - Math.round(w / 2)), y: Math.round(worldY - Math.round(h / 2)) }, properties, extraData);
203
+ };
204
+
205
+ try {
206
+ const img = new Image();
207
+ img.decoding = 'async';
208
+ img.onload = () => placeWithAspect(img.naturalWidth || 0, img.naturalHeight || 0);
209
+ img.onerror = () => placeWithAspect(0, 0);
210
+ img.src = src;
211
+ } catch (_) {
212
+ placeWithAspect(0, 0);
213
+ }
214
+ });
215
+
216
+ core.eventBus.on(Events.UI.PasteImageAt, ({ x, y, src, name, imageId }) => {
217
+ if (!src) return;
218
+ const world = core.pixi.worldLayer || core.pixi.app.stage;
219
+ const s = world?.scale?.x || 1;
220
+ const worldX = (x - (world?.x || 0)) / s;
221
+ const worldY = (y - (world?.y || 0)) / s;
222
+
223
+ const placeWithAspect = (natW, natH) => {
224
+ let w = 300;
225
+ let h = 200;
226
+ if (natW > 0 && natH > 0) {
227
+ const ar = natW / natH;
228
+ w = 300;
229
+ h = Math.max(1, Math.round(w / ar));
230
+ }
231
+ const properties = { src, name, width: w, height: h };
232
+ const extraData = imageId ? { imageId } : {};
233
+ core.createObject('image', { x: Math.round(worldX - Math.round(w / 2)), y: Math.round(worldY - Math.round(h / 2)) }, properties, extraData);
234
+ };
235
+
236
+ try {
237
+ const img = new Image();
238
+ img.decoding = 'async';
239
+ img.onload = () => placeWithAspect(img.naturalWidth || 0, img.naturalHeight || 0);
240
+ img.onerror = () => placeWithAspect(0, 0);
241
+ img.src = src;
242
+ } catch (_) {
243
+ placeWithAspect(0, 0);
244
+ }
245
+ });
246
+
247
+ core.eventBus.on(Events.Tool.DuplicateRequest, (data) => {
248
+ const { originalId, position } = data || {};
249
+ if (!originalId) return;
250
+ const objects = core.state.state.objects;
251
+ const original = objects.find(obj => obj.id === originalId);
252
+ if (!original) return;
253
+
254
+ if (original.type === 'frame') {
255
+ const frame = JSON.parse(JSON.stringify(original));
256
+ const dx = (position?.x ?? frame.position.x) - frame.position.x;
257
+ const dy = (position?.y ?? frame.position.y) - frame.position.y;
258
+ const children = (core.state.state.objects || []).filter(o => o && o.properties && o.properties.frameId === originalId);
259
+
260
+ const onFramePasted = (payload) => {
261
+ if (!payload || payload.originalId !== originalId) return;
262
+ const newFrameId = payload.newId;
263
+ core.eventBus.off(Events.Object.Pasted, onFramePasted);
264
+ for (const child of children) {
265
+ const clonedChild = JSON.parse(JSON.stringify(child));
266
+ clonedChild.properties = clonedChild.properties || {};
267
+ clonedChild.properties.frameId = newFrameId;
268
+ const targetPos = {
269
+ x: (child.position?.x || 0) + dx,
270
+ y: (child.position?.y || 0) + dy
271
+ };
272
+ core.clipboard = { type: 'object', data: clonedChild };
273
+ const cmdChild = new PasteObjectCommand(core, targetPos);
274
+ cmdChild.setEventBus(core.eventBus);
275
+ core.history.executeCommand(cmdChild);
276
+ }
277
+ };
278
+ core.eventBus.on(Events.Object.Pasted, onFramePasted);
279
+
280
+ const frameClone = JSON.parse(JSON.stringify(frame));
281
+ try {
282
+ const arr = core.state.state.objects || [];
283
+ let maxNum = 0;
284
+ for (const o of arr) {
285
+ if (!o || o.type !== 'frame') continue;
286
+ const t = o?.properties?.title || '';
287
+ const m = t.match(/^\s*Фрейм\s+(\d+)\s*$/i);
288
+ if (m) {
289
+ const n = parseInt(m[1], 10);
290
+ if (Number.isFinite(n)) maxNum = Math.max(maxNum, n);
291
+ }
292
+ }
293
+ const next = maxNum + 1;
294
+ frameClone.properties = frameClone.properties || {};
295
+ frameClone.properties.title = `Фрейм ${next}`;
296
+ } catch (_) {}
297
+ core.clipboard = { type: 'object', data: frameClone };
298
+ const cmdFrame = new PasteObjectCommand(core, { x: frame.position.x + dx, y: frame.position.y + dy });
299
+ cmdFrame.setEventBus(core.eventBus);
300
+ core.history.executeCommand(cmdFrame);
301
+ return;
302
+ }
303
+
304
+ core.clipboard = {
305
+ type: 'object',
306
+ data: JSON.parse(JSON.stringify(original))
307
+ };
308
+ try {
309
+ if (original.type === 'frame') {
310
+ core._dupTitleMap = core._dupTitleMap || new Map();
311
+ const prevTitle = (original.properties && typeof original.properties.title !== 'undefined') ? original.properties.title : undefined;
312
+ core._dupTitleMap.set(originalId, prevTitle);
313
+ }
314
+ } catch (_) {}
315
+ try {
316
+ if (core.clipboard.data && core.clipboard.data.type === 'frame') {
317
+ const arr = core.state.state.objects || [];
318
+ let maxNum = 0;
319
+ for (const o of arr) {
320
+ if (!o || o.type !== 'frame') continue;
321
+ const t = o?.properties?.title || '';
322
+ const m = t.match(/^\s*Фрейм\s+(\d+)\s*$/i);
323
+ if (m) {
324
+ const n = parseInt(m[1], 10);
325
+ if (Number.isFinite(n)) maxNum = Math.max(maxNum, n);
326
+ }
327
+ }
328
+ const next = maxNum + 1;
329
+ core.clipboard.data.properties = core.clipboard.data.properties || {};
330
+ core.clipboard.data.properties.title = `Фрейм ${next}`;
331
+ }
332
+ } catch (_) {}
333
+
334
+ core.pasteObject(position);
335
+ });
336
+
337
+ core.eventBus.on(Events.Tool.GroupDuplicateRequest, (data) => {
338
+ const originals = (data.objects || []).filter((id) => core.state.state.objects.some(o => o.id === id));
339
+ const total = originals.length;
340
+ if (total === 0) {
341
+ core.eventBus.emit(Events.Tool.GroupDuplicateReady, { map: {} });
342
+ return;
343
+ }
344
+ const idMap = {};
345
+ let remaining = total;
346
+ const tempHandlers = new Map();
347
+ const onPasted = (originalId) => (payload) => {
348
+ if (payload.originalId !== originalId) return;
349
+ idMap[originalId] = payload.newId;
350
+ const h = tempHandlers.get(originalId);
351
+ if (h) core.eventBus.off(Events.Object.Pasted, h);
352
+ remaining -= 1;
353
+ if (remaining === 0) {
354
+ core.eventBus.emit(Events.Tool.GroupDuplicateReady, { map: idMap });
355
+ }
356
+ };
357
+ for (const originalId of originals) {
358
+ const obj = core.state.state.objects.find(o => o.id === originalId);
359
+ if (!obj) continue;
360
+ const handler = onPasted(originalId);
361
+ tempHandlers.set(originalId, handler);
362
+ core.eventBus.on(Events.Object.Pasted, handler);
363
+ core.clipboard = { type: 'object', data: JSON.parse(JSON.stringify(obj)) };
364
+ try {
365
+ if (obj.type === 'frame') {
366
+ core._dupTitleMap = core._dupTitleMap || new Map();
367
+ const prevTitle = (obj.properties && typeof obj.properties.title !== 'undefined') ? obj.properties.title : undefined;
368
+ core._dupTitleMap.set(obj.id, prevTitle);
369
+ }
370
+ } catch (_) {}
371
+ try {
372
+ if (core.clipboard.data && core.clipboard.data.type === 'frame') {
373
+ const arr = core.state.state.objects || [];
374
+ let maxNum = 0;
375
+ for (const o2 of arr) {
376
+ if (!o2 || o2.type !== 'frame') continue;
377
+ const t2 = o2?.properties?.title || '';
378
+ const m2 = t2.match(/^\s*Фрейм\s+(\d+)\s*$/i);
379
+ if (m2) {
380
+ const n2 = parseInt(m2[1], 10);
381
+ if (Number.isFinite(n2)) maxNum = Math.max(maxNum, n2);
382
+ }
383
+ }
384
+ const next2 = maxNum + 1;
385
+ core.clipboard.data.properties = core.clipboard.data.properties || {};
386
+ core.clipboard.data.properties.title = `Фрейм ${next2}`;
387
+ }
388
+ } catch (_) {}
389
+ const cmd = new PasteObjectCommand(core, { x: obj.position.x, y: obj.position.y });
390
+ cmd.setEventBus(core.eventBus);
391
+ core.history.executeCommand(cmd);
392
+ }
393
+ });
394
+
395
+ core.eventBus.on(Events.Object.Pasted, ({ originalId, newId }) => {
396
+ try {
397
+ const arr = core.state.state.objects || [];
398
+ const newObj = arr.find(o => o.id === newId);
399
+ const origObj = arr.find(o => o.id === originalId);
400
+ if (newObj && newObj.type === 'frame') {
401
+ let maxNum = 0;
402
+ for (const o of arr) {
403
+ if (!o || o.id === newId || o.type !== 'frame') continue;
404
+ const t = o?.properties?.title || '';
405
+ const m = t.match(/^\s*Фрейм\s+(\d+)\s*$/i);
406
+ if (m) {
407
+ const n = parseInt(m[1], 10);
408
+ if (Number.isFinite(n)) maxNum = Math.max(maxNum, n);
409
+ }
410
+ }
411
+ const next = maxNum + 1;
412
+ newObj.properties = newObj.properties || {};
413
+ newObj.properties.title = `Фрейм ${next}`;
414
+ const pixNew = core.pixi.objects.get(newId);
415
+ if (pixNew && pixNew._mb?.instance?.setTitle) pixNew._mb.instance.setTitle(newObj.properties.title);
416
+ if (core._dupTitleMap && core._dupTitleMap.has(originalId) && origObj && origObj.type === 'frame') {
417
+ const prev = core._dupTitleMap.get(originalId);
418
+ origObj.properties = origObj.properties || {};
419
+ origObj.properties.title = prev;
420
+ const pixOrig = core.pixi.objects.get(originalId);
421
+ if (pixOrig && pixOrig._mb?.instance?.setTitle) pixOrig._mb.instance.setTitle(prev);
422
+ core._dupTitleMap.delete(originalId);
423
+ }
424
+ core.state.markDirty();
425
+ }
426
+ } catch (_) {}
427
+ core.eventBus.emit(Events.Tool.DuplicateReady, { originalId, newId });
428
+ });
429
+ }
430
+
431
+ export function setupClipboardKeyboardFlow(core) {
432
+ core.eventBus.on(Events.Keyboard.Copy, () => {
433
+ if (core.toolManager.getActiveTool()?.name !== 'select') return;
434
+ const selected = Array.from(core.toolManager.getActiveTool().selectedObjects || []);
435
+ if (selected.length === 0) return;
436
+ if (selected.length === 1) {
437
+ core.copyObject(selected[0]);
438
+ return;
439
+ }
440
+ const objects = core.state.state.objects || [];
441
+ const groupData = selected
442
+ .map(id => objects.find(o => o.id === id))
443
+ .filter(Boolean)
444
+ .map(o => JSON.parse(JSON.stringify(o)));
445
+ if (groupData.length === 0) return;
446
+ core.clipboard = {
447
+ type: 'group',
448
+ data: groupData,
449
+ meta: { pasteCount: 0 }
450
+ };
451
+ });
452
+
453
+ core.eventBus.on(Events.Keyboard.Paste, () => {
454
+ if (!core.clipboard) return;
455
+ if (core.clipboard.type === 'object') {
456
+ core.pasteObject();
457
+ return;
458
+ }
459
+ if (core.clipboard.type === 'group') {
460
+ const group = core.clipboard;
461
+ const data = Array.isArray(group.data) ? group.data : [];
462
+ if (data.length === 0) return;
463
+ const offsetStep = 25;
464
+ group.meta = group.meta || { pasteCount: 0 };
465
+ group.meta.pasteCount = (group.meta.pasteCount || 0) + 1;
466
+ const dx = offsetStep * group.meta.pasteCount;
467
+ const dy = offsetStep * group.meta.pasteCount;
468
+
469
+ if (group.meta && group.meta.frameBundle) {
470
+ const frames = data.filter(o => o && o.type === 'frame');
471
+ if (frames.length === 1) {
472
+ const frameOriginal = frames[0];
473
+ const children = data.filter(o => o && o.id !== frameOriginal.id);
474
+ const totalToPaste = 1 + children.length;
475
+ let pastedCount = 0;
476
+ const newIds = [];
477
+ let newFrameId = null;
478
+
479
+ const onPasted = (payload) => {
480
+ if (!payload || !payload.newId) return;
481
+ newIds.push(payload.newId);
482
+ pastedCount += 1;
483
+ if (!newFrameId && payload.originalId === frameOriginal.id) {
484
+ newFrameId = payload.newId;
485
+ for (const child of children) {
486
+ const clonedChild = JSON.parse(JSON.stringify(child));
487
+ clonedChild.properties = clonedChild.properties || {};
488
+ clonedChild.properties.frameId = newFrameId;
489
+ const targetPos = {
490
+ x: (clonedChild.position?.x || 0) + dx,
491
+ y: (clonedChild.position?.y || 0) + dy
492
+ };
493
+ core.clipboard = { type: 'object', data: clonedChild };
494
+ const cmdChild = new PasteObjectCommand(core, targetPos);
495
+ cmdChild.setEventBus(core.eventBus);
496
+ core.history.executeCommand(cmdChild);
497
+ }
498
+ }
499
+ if (pastedCount === totalToPaste) {
500
+ core.eventBus.off(Events.Object.Pasted, onPasted);
501
+ if (core.selectTool && newIds.length > 0) {
502
+ requestAnimationFrame(() => {
503
+ core.selectTool.setSelection(newIds);
504
+ core.selectTool.updateResizeHandles();
505
+ });
506
+ }
507
+ }
508
+ };
509
+ core.eventBus.on(Events.Object.Pasted, onPasted);
510
+
511
+ const frameClone = JSON.parse(JSON.stringify(frameOriginal));
512
+ core.clipboard = { type: 'object', data: frameClone };
513
+ const cmdFrame = new PasteObjectCommand(core, { x: (frameClone.position?.x || 0) + dx, y: (frameClone.position?.y || 0) + dy });
514
+ cmdFrame.setEventBus(core.eventBus);
515
+ core.history.executeCommand(cmdFrame);
516
+ core.clipboard = group;
517
+ return;
518
+ }
519
+ }
520
+
521
+ let pending = data.length;
522
+ const newIds = [];
523
+ const onPasted = (payload) => {
524
+ if (!payload || !payload.newId) return;
525
+ newIds.push(payload.newId);
526
+ pending -= 1;
527
+ if (pending === 0) {
528
+ core.eventBus.off(Events.Object.Pasted, onPasted);
529
+ if (core.selectTool && newIds.length > 0) {
530
+ requestAnimationFrame(() => {
531
+ core.selectTool.setSelection(newIds);
532
+ core.selectTool.updateResizeHandles();
533
+ });
534
+ }
535
+ }
536
+ };
537
+ core.eventBus.on(Events.Object.Pasted, onPasted);
538
+
539
+ for (const original of data) {
540
+ const cloned = JSON.parse(JSON.stringify(original));
541
+ const targetPos = {
542
+ x: (cloned.position?.x || 0) + dx,
543
+ y: (cloned.position?.y || 0) + dy
544
+ };
545
+ core.clipboard = { type: 'object', data: cloned };
546
+ const cmd = new PasteObjectCommand(core, targetPos);
547
+ cmd.setEventBus(core.eventBus);
548
+ core.history.executeCommand(cmd);
549
+ }
550
+ core.clipboard = group;
551
+ }
552
+ });
553
+ }