@sequent-org/moodboard 1.3.5 → 1.4.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 (64) hide show
  1. package/package.json +6 -1
  2. package/src/assets/icons/mindmap.svg +3 -0
  3. package/src/core/SaveManager.js +44 -15
  4. package/src/core/commands/MindmapStatePatchCommand.js +85 -0
  5. package/src/core/commands/UpdateContentCommand.js +47 -4
  6. package/src/core/flows/LayerAndViewportFlow.js +87 -14
  7. package/src/core/flows/ObjectLifecycleFlow.js +7 -2
  8. package/src/core/flows/SaveFlow.js +10 -7
  9. package/src/core/flows/TransformFlow.js +2 -2
  10. package/src/core/index.js +81 -11
  11. package/src/core/rendering/ObjectRenderer.js +7 -2
  12. package/src/grid/BaseGrid.js +65 -0
  13. package/src/grid/CrossGrid.js +89 -24
  14. package/src/grid/CrossGridZoomPhases.js +167 -0
  15. package/src/grid/DotGrid.js +117 -34
  16. package/src/grid/DotGridZoomPhases.js +214 -16
  17. package/src/grid/GridDiagnostics.js +80 -0
  18. package/src/grid/GridFactory.js +13 -11
  19. package/src/grid/LineGrid.js +176 -37
  20. package/src/grid/LineGridZoomPhases.js +163 -0
  21. package/src/grid/ScreenGridPhaseMachine.js +51 -0
  22. package/src/mindmap/MindmapCompoundContract.js +235 -0
  23. package/src/moodboard/ActionHandler.js +1 -0
  24. package/src/moodboard/DataManager.js +57 -0
  25. package/src/moodboard/bootstrap/MoodBoardUiFactory.js +21 -0
  26. package/src/moodboard/integration/MoodBoardEventBindings.js +26 -1
  27. package/src/moodboard/lifecycle/MoodBoardDestroyer.js +15 -0
  28. package/src/objects/MindmapObject.js +76 -0
  29. package/src/objects/ObjectFactory.js +3 -1
  30. package/src/services/BoardService.js +127 -31
  31. package/src/services/GridSnapResolver.js +60 -0
  32. package/src/services/MiroZoomLevels.js +39 -0
  33. package/src/services/SettingsApplier.js +0 -4
  34. package/src/services/ZoomPanController.js +51 -32
  35. package/src/tools/object-tools/PlacementTool.js +12 -3
  36. package/src/tools/object-tools/SelectTool.js +11 -1
  37. package/src/tools/object-tools/placement/GhostController.js +100 -1
  38. package/src/tools/object-tools/placement/PlacementEventsBridge.js +2 -0
  39. package/src/tools/object-tools/placement/PlacementInputRouter.js +2 -2
  40. package/src/tools/object-tools/selection/FileNameInlineEditorController.js +2 -2
  41. package/src/tools/object-tools/selection/InlineEditorController.js +15 -0
  42. package/src/tools/object-tools/selection/MindmapInlineEditorController.js +716 -0
  43. package/src/tools/object-tools/selection/SelectInputRouter.js +6 -0
  44. package/src/tools/object-tools/selection/SelectToolSetup.js +2 -0
  45. package/src/tools/object-tools/selection/TextEditorLifecycleRegistry.js +12 -16
  46. package/src/ui/ContextMenu.js +6 -6
  47. package/src/ui/DotGridDebugPanel.js +253 -0
  48. package/src/ui/HtmlTextLayer.js +1 -1
  49. package/src/ui/TextPropertiesPanel.js +2 -2
  50. package/src/ui/handles/GroupSelectionHandlesController.js +4 -1
  51. package/src/ui/handles/HandlesDomRenderer.js +1485 -14
  52. package/src/ui/handles/HandlesEventBridge.js +49 -5
  53. package/src/ui/handles/HandlesInteractionController.js +4 -4
  54. package/src/ui/mindmap/MindmapConnectionLayer.js +239 -0
  55. package/src/ui/mindmap/MindmapHtmlTextLayer.js +285 -0
  56. package/src/ui/mindmap/MindmapLayoutConfig.js +29 -0
  57. package/src/ui/mindmap/MindmapTextOverlayAdapter.js +144 -0
  58. package/src/ui/styles/toolbar.css +1 -0
  59. package/src/ui/styles/workspace.css +100 -0
  60. package/src/ui/toolbar/ToolbarActionRouter.js +35 -0
  61. package/src/ui/toolbar/ToolbarPopupsController.js +6 -6
  62. package/src/ui/toolbar/ToolbarRenderer.js +1 -0
  63. package/src/ui/toolbar/ToolbarStateController.js +1 -0
  64. package/src/utils/iconLoader.js +10 -4
@@ -1,8 +1,792 @@
1
1
  import { Events } from '../../core/events/Events.js';
2
2
  import { createRotatedResizeCursor } from '../../tools/object-tools/selection/CursorController.js';
3
+ import {
4
+ createChildMindmapIntentMetadata,
5
+ logMindmapCompoundDebug,
6
+ pickRandomMindmapBranchColorExcluding,
7
+ } from '../../mindmap/MindmapCompoundContract.js';
8
+ import { MINDMAP_LAYOUT } from '../mindmap/MindmapLayoutConfig.js';
9
+ import { MindmapStatePatchCommand } from '../../core/commands/MindmapStatePatchCommand.js';
3
10
 
4
11
  const HANDLES_ACCENT_COLOR = '#80D8FF';
5
12
  const REVIT_SHOW_IN_MODEL_ICON_SVG = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640" aria-hidden="true" focusable="false"><path d="M384 64C366.3 64 352 78.3 352 96C352 113.7 366.3 128 384 128L466.7 128L265.3 329.4C252.8 341.9 252.8 362.2 265.3 374.7C277.8 387.2 298.1 387.2 310.6 374.7L512 173.3L512 256C512 273.7 526.3 288 544 288C561.7 288 576 273.7 576 256L576 96C576 78.3 561.7 64 544 64L384 64zM144 160C99.8 160 64 195.8 64 240L64 496C64 540.2 99.8 576 144 576L400 576C444.2 576 480 540.2 480 496L480 416C480 398.3 465.7 384 448 384C430.3 384 416 398.3 416 416L416 496C416 504.8 408.8 512 400 512L144 512C135.2 512 128 504.8 128 496L128 240C128 231.2 135.2 224 144 224L224 224C241.7 224 256 209.7 256 192C256 174.3 241.7 160 224 160L144 160z"/></svg>';
13
+ const MINDMAP_CHILD_WIDTH_FACTOR = 0.9;
14
+ const MINDMAP_CHILD_HEIGHT_FACTOR = 0.8;
15
+ const MINDMAP_CHILD_PADDING_FACTOR = 0.5;
16
+ const MINDMAP_CHILD_STROKE_COLOR = 0x16A34A;
17
+ const MINDMAP_CHILD_FILL_ALPHA = 0.25;
18
+ const MINDMAP_CHILD_GAP_MULTIPLIER = 10;
19
+ const MINDMAP_CHILD_VERTICAL_GAP_MULTIPLIER = 1;
20
+
21
+ function asBranchColor(value) {
22
+ if (!Number.isFinite(value)) return null;
23
+ const normalized = Math.floor(Number(value));
24
+ if (normalized < 0 || normalized > 0xFFFFFF) return null;
25
+ return normalized;
26
+ }
27
+
28
+ function resolveBottomSiblingParentId(sourceObjectId, sourceMeta) {
29
+ if (typeof sourceMeta?.parentId === 'string' && sourceMeta.parentId.length > 0) {
30
+ return sourceMeta.parentId;
31
+ }
32
+ if (typeof sourceMeta?.branchRootId === 'string' && sourceMeta.branchRootId.length > 0) {
33
+ return sourceMeta.branchRootId;
34
+ }
35
+ if (typeof sourceObjectId === 'string' && sourceObjectId.length > 0) {
36
+ return sourceObjectId;
37
+ }
38
+ return sourceMeta?.parentId || null;
39
+ }
40
+
41
+ function relayoutMindmapBranchLevel({ core, eventBus, parentId, side }) {
42
+ if (!core || !eventBus || !parentId || (side !== 'left' && side !== 'right')) return;
43
+ const objects = core?.state?.state?.objects || [];
44
+ const mindmaps = Array.isArray(objects) ? objects.filter((obj) => obj?.type === 'mindmap') : [];
45
+ const byId = new Map(mindmaps.map((obj) => [obj.id, obj]));
46
+ const parent = byId.get(parentId);
47
+ if (!parent) return;
48
+
49
+ const getBranchOrder = (obj) => {
50
+ const raw = obj?.properties?.mindmap?.branchOrder;
51
+ return Number.isFinite(raw) ? Number(raw) : null;
52
+ };
53
+ const siblings = mindmaps
54
+ .filter((obj) => {
55
+ const meta = obj?.properties?.mindmap || {};
56
+ return meta?.role === 'child' && meta?.parentId === parentId && meta?.side === side;
57
+ })
58
+ .sort((a, b) => {
59
+ const ao = getBranchOrder(a);
60
+ const bo = getBranchOrder(b);
61
+ if (ao !== null && bo !== null && ao !== bo) return ao - bo;
62
+ if (ao !== null && bo === null) return -1;
63
+ if (ao === null && bo !== null) return 1;
64
+ const ay = a?.position?.y || 0;
65
+ const by = b?.position?.y || 0;
66
+ if (ay !== by) return ay - by;
67
+ return String(a?.id || '').localeCompare(String(b?.id || ''));
68
+ });
69
+ if (siblings.length === 0) return;
70
+
71
+ const app = core?.pixi?.app;
72
+ const worldLayer = core?.pixi?.worldLayer || app?.stage;
73
+ const rendererRes = app?.renderer?.resolution || 1;
74
+ const worldScale = worldLayer?.scale?.x || 1;
75
+ const baseGapWorld = Math.max(1, Math.round((10 * rendererRes) / worldScale));
76
+ const gapWorld = Math.max(1, Math.round(baseGapWorld * MINDMAP_CHILD_GAP_MULTIPLIER));
77
+ const verticalGapWorld = Math.max(1, Math.round(baseGapWorld * MINDMAP_CHILD_VERTICAL_GAP_MULTIPLIER));
78
+
79
+ const byParentBySide = new Map();
80
+ const childrenByParent = new Map();
81
+ mindmaps.forEach((obj) => {
82
+ const meta = obj?.properties?.mindmap || {};
83
+ if (meta?.role !== 'child') return;
84
+ const key = `${meta.parentId || ''}::${meta.side || ''}`;
85
+ if (!byParentBySide.has(key)) byParentBySide.set(key, []);
86
+ byParentBySide.get(key).push(obj.id);
87
+ if (!childrenByParent.has(meta.parentId)) childrenByParent.set(meta.parentId, []);
88
+ childrenByParent.get(meta.parentId).push(obj.id);
89
+ });
90
+
91
+ const subtreeSpanCache = new Map();
92
+ const getNodeHeight = (node) => Math.max(1, Math.round(node?.height || node?.properties?.height || 1));
93
+ const getGroupSpan = (ownerId, branchSide, visiting) => {
94
+ const key = `${ownerId}::${branchSide}`;
95
+ const children = byParentBySide.get(key) || [];
96
+ if (!children.length) return 0;
97
+ let total = 0;
98
+ children.forEach((childId, index) => {
99
+ const childNode = byId.get(childId);
100
+ if (!childNode) return;
101
+ total += getSubtreeSpan(childNode, visiting);
102
+ if (index > 0) total += verticalGapWorld;
103
+ });
104
+ return Math.max(0, Math.round(total));
105
+ };
106
+ const getSubtreeSpan = (node, visiting = new Set()) => {
107
+ if (!node?.id) return 1;
108
+ if (subtreeSpanCache.has(node.id)) return subtreeSpanCache.get(node.id);
109
+ if (visiting.has(node.id)) return getNodeHeight(node);
110
+ visiting.add(node.id);
111
+ const ownHeight = getNodeHeight(node);
112
+ const leftSpan = getGroupSpan(node.id, 'left', visiting);
113
+ const rightSpan = getGroupSpan(node.id, 'right', visiting);
114
+ const span = Math.max(ownHeight, leftSpan, rightSpan);
115
+ subtreeSpanCache.set(node.id, span);
116
+ visiting.delete(node.id);
117
+ return span;
118
+ };
119
+
120
+ const parentX = Math.round(parent?.position?.x || 0);
121
+ const parentWidth = Math.max(1, Math.round(parent?.width || parent?.properties?.width || 1));
122
+ const anchorLeftX = side === 'right'
123
+ ? Math.round(parentX + parentWidth + gapWorld)
124
+ : null;
125
+ const anchorRightX = side === 'left'
126
+ ? Math.round(parentX - gapWorld)
127
+ : null;
128
+
129
+ const siblingHeights = siblings.map((node) => getNodeHeight(node));
130
+ const siblingSpans = siblings.map((node) => getSubtreeSpan(node));
131
+ const avgHeight = Math.max(
132
+ 1,
133
+ Math.round(
134
+ siblingHeights.reduce((acc, h) => acc + h, 0) / siblings.length
135
+ )
136
+ );
137
+ const verticalStep = Math.max(1, avgHeight + verticalGapWorld);
138
+ const parentHeight = Math.max(1, Math.round(parent?.height || parent?.properties?.height || 1));
139
+ const parentCenterY = Math.round(parent?.position?.y || 0) + Math.round(parentHeight / 2);
140
+ const totalStackHeight = siblingSpans.reduce((acc, h) => acc + h, 0)
141
+ + Math.max(0, siblings.length - 1) * verticalGapWorld;
142
+ const startY = Math.round(parentCenterY - totalStackHeight / 2);
143
+ logMindmapCompoundDebug('layout:branch-level-start', {
144
+ parentId,
145
+ side,
146
+ siblings: siblings.map((node, index) => ({
147
+ id: node?.id || null,
148
+ index,
149
+ y: Math.round(node?.position?.y || 0),
150
+ height: siblingHeights[index] || null,
151
+ span: siblingSpans[index] || null,
152
+ })),
153
+ parentCenterY,
154
+ totalStackHeight,
155
+ verticalGapWorld,
156
+ });
157
+ const movedPositions = new Map();
158
+ const getCurrentPos = (nodeId) => {
159
+ if (movedPositions.has(nodeId)) return movedPositions.get(nodeId);
160
+ const node = byId.get(nodeId);
161
+ return {
162
+ x: Math.round(node?.position?.x || 0),
163
+ y: Math.round(node?.position?.y || 0),
164
+ };
165
+ };
166
+ const getDescendants = (rootId) => {
167
+ const result = [];
168
+ const queue = [...(childrenByParent.get(rootId) || [])];
169
+ const seen = new Set();
170
+ while (queue.length > 0) {
171
+ const nextId = queue.shift();
172
+ if (!nextId || seen.has(nextId)) continue;
173
+ seen.add(nextId);
174
+ result.push(nextId);
175
+ const nested = childrenByParent.get(nextId) || [];
176
+ nested.forEach((nestedId) => {
177
+ if (!seen.has(nestedId)) queue.push(nestedId);
178
+ });
179
+ }
180
+ return result;
181
+ };
182
+
183
+ let movedCount = 0;
184
+ let cursorY = startY;
185
+ siblings.forEach((node, index) => {
186
+ const currentPos = getCurrentPos(node.id);
187
+ const currentX = currentPos.x;
188
+ const currentY = currentPos.y;
189
+ const nodeWidth = Math.max(1, Math.round(node?.width || node?.properties?.width || 1));
190
+ const nodeHeight = siblingHeights[index] || Math.max(1, Math.round(node?.height || node?.properties?.height || 1));
191
+ const nodeSpan = siblingSpans[index] || nodeHeight;
192
+ const targetX = side === 'right'
193
+ ? anchorLeftX
194
+ : Math.round((anchorRightX || 0) - nodeWidth);
195
+ const targetY = Math.round(cursorY + Math.max(0, nodeSpan - nodeHeight) / 2);
196
+ logMindmapCompoundDebug('layout:branch-level-node-target', {
197
+ parentId,
198
+ side,
199
+ nodeId: node?.id || null,
200
+ index,
201
+ currentX,
202
+ currentY,
203
+ targetX,
204
+ targetY,
205
+ nodeHeight,
206
+ nodeSpan,
207
+ });
208
+
209
+ if (!(targetX === currentX && targetY === currentY)) {
210
+ core.updateObjectPositionDirect(node.id, { x: targetX, y: targetY }, { snap: false });
211
+ eventBus.emit(Events.Object.TransformUpdated, { objectId: node.id });
212
+ eventBus.emit(Events.Tool.DragUpdate, { object: node.id });
213
+ movedPositions.set(node.id, { x: targetX, y: targetY });
214
+
215
+ const dx = Math.round(targetX - currentX);
216
+ const dy = Math.round(targetY - currentY);
217
+ if (dx !== 0 || dy !== 0) {
218
+ const descendants = getDescendants(node.id);
219
+ logMindmapCompoundDebug('layout:branch-level-node-shift', {
220
+ parentId,
221
+ side,
222
+ nodeId: node?.id || null,
223
+ index,
224
+ dx,
225
+ dy,
226
+ descendantsCount: descendants.length,
227
+ });
228
+ descendants.forEach((descId) => {
229
+ const pos = getCurrentPos(descId);
230
+ const nextPos = {
231
+ x: Math.round(pos.x + dx),
232
+ y: Math.round(pos.y + dy),
233
+ };
234
+ core.updateObjectPositionDirect(descId, nextPos, { snap: false });
235
+ eventBus.emit(Events.Object.TransformUpdated, { objectId: descId });
236
+ eventBus.emit(Events.Tool.DragUpdate, { object: descId });
237
+ movedPositions.set(descId, nextPos);
238
+ });
239
+ }
240
+ movedCount += 1;
241
+ }
242
+ cursorY += nodeSpan + verticalGapWorld;
243
+ });
244
+
245
+ logMindmapCompoundDebug('layout:branch-level-align', {
246
+ parentId,
247
+ side,
248
+ siblingsCount: siblings.length,
249
+ movedCount,
250
+ verticalStep,
251
+ parentCenterY,
252
+ totalStackHeight,
253
+ });
254
+ }
255
+
256
+ function relayoutMindmapBranchCascade({ core, eventBus, startParentId, startSide }) {
257
+ if (!core || !eventBus || !startParentId || (startSide !== 'left' && startSide !== 'right')) return;
258
+ const objects = core?.state?.state?.objects || [];
259
+ const mindmaps = Array.isArray(objects) ? objects.filter((obj) => obj?.type === 'mindmap') : [];
260
+ const byId = new Map(mindmaps.map((obj) => [obj.id, obj]));
261
+
262
+ let parentId = startParentId;
263
+ let side = startSide;
264
+ const visited = new Set();
265
+
266
+ while (parentId && !visited.has(`${parentId}:${side}`)) {
267
+ visited.add(`${parentId}:${side}`);
268
+ relayoutMindmapBranchLevel({ core, eventBus, parentId, side });
269
+ const parentNode = byId.get(parentId);
270
+ const parentMeta = parentNode?.properties?.mindmap || {};
271
+ if (parentMeta?.role !== 'child') break;
272
+ parentId = parentMeta.parentId || null;
273
+ side = (parentMeta.side === 'left' || parentMeta.side === 'right') ? parentMeta.side : side;
274
+ }
275
+ }
276
+
277
+ function relayoutMindmapSubtreeLevels({ core, eventBus, rootIds = [] }) {
278
+ if (!core || !eventBus) return;
279
+ const queue = Array.isArray(rootIds) ? [...rootIds] : [];
280
+ const visited = new Set();
281
+ while (queue.length > 0) {
282
+ const nodeId = queue.shift();
283
+ if (!nodeId || visited.has(nodeId)) continue;
284
+ visited.add(nodeId);
285
+
286
+ relayoutMindmapBranchLevel({ core, eventBus, parentId: nodeId, side: 'left' });
287
+ relayoutMindmapBranchLevel({ core, eventBus, parentId: nodeId, side: 'right' });
288
+
289
+ const objects = core?.state?.state?.objects || [];
290
+ const children = (Array.isArray(objects) ? objects : [])
291
+ .filter((obj) => {
292
+ if (!obj || obj.type !== 'mindmap') return false;
293
+ const meta = obj?.properties?.mindmap || {};
294
+ return meta?.role === 'child' && meta?.parentId === nodeId;
295
+ })
296
+ .map((obj) => obj.id);
297
+ children.forEach((id) => {
298
+ if (!visited.has(id)) queue.push(id);
299
+ });
300
+ }
301
+ }
302
+
303
+ function collectMindmapChildrenByParent(mindmaps) {
304
+ const childrenByParent = new Map();
305
+ (Array.isArray(mindmaps) ? mindmaps : []).forEach((obj) => {
306
+ if (!obj || obj.type !== 'mindmap') return;
307
+ const meta = obj?.properties?.mindmap || {};
308
+ if (meta?.role !== 'child' || !meta?.parentId) return;
309
+ if (!childrenByParent.has(meta.parentId)) childrenByParent.set(meta.parentId, []);
310
+ childrenByParent.get(meta.parentId).push(obj.id);
311
+ });
312
+ return childrenByParent;
313
+ }
314
+
315
+ function collectMindmapDescendants(childrenByParent, rootId) {
316
+ const result = [];
317
+ const queue = [...(childrenByParent.get(rootId) || [])];
318
+ const seen = new Set();
319
+ while (queue.length > 0) {
320
+ const nextId = queue.shift();
321
+ if (!nextId || seen.has(nextId)) continue;
322
+ seen.add(nextId);
323
+ result.push(nextId);
324
+ const nested = childrenByParent.get(nextId) || [];
325
+ nested.forEach((id) => {
326
+ if (!seen.has(id)) queue.push(id);
327
+ });
328
+ }
329
+ return result;
330
+ }
331
+
332
+ function getMindmapRect(obj) {
333
+ const x = Math.round(obj?.position?.x || 0);
334
+ const y = Math.round(obj?.position?.y || 0);
335
+ const width = Math.max(1, Math.round(obj?.width || obj?.properties?.width || 1));
336
+ const height = Math.max(1, Math.round(obj?.height || obj?.properties?.height || 1));
337
+ return { x, y, width, height };
338
+ }
339
+
340
+ function isPointInsideRect(point, rect) {
341
+ if (!point || !rect) return false;
342
+ return point.x >= rect.x
343
+ && point.x <= rect.x + rect.width
344
+ && point.y >= rect.y
345
+ && point.y <= rect.y + rect.height;
346
+ }
347
+
348
+ function normalizeBranchOrderByCurrentY({ core, parentId, side }) {
349
+ if (!core || !parentId || (side !== 'left' && side !== 'right')) return { changed: 0, count: 0 };
350
+ const objects = core?.state?.state?.objects || [];
351
+ const siblings = (Array.isArray(objects) ? objects : [])
352
+ .filter((obj) => {
353
+ if (!obj || obj.type !== 'mindmap') return false;
354
+ const meta = obj?.properties?.mindmap || {};
355
+ return meta?.role === 'child' && meta?.parentId === parentId && meta?.side === side;
356
+ })
357
+ .sort((a, b) => {
358
+ const ay = Math.round(a?.position?.y || 0);
359
+ const by = Math.round(b?.position?.y || 0);
360
+ if (ay !== by) return ay - by;
361
+ return String(a?.id || '').localeCompare(String(b?.id || ''));
362
+ });
363
+ let changed = 0;
364
+ siblings.forEach((obj, index) => {
365
+ if (!obj.properties) obj.properties = {};
366
+ const meta = obj.properties.mindmap || {};
367
+ const prev = Number.isFinite(meta.branchOrder) ? Number(meta.branchOrder) : null;
368
+ if (prev === index) return;
369
+ obj.properties.mindmap = {
370
+ ...meta,
371
+ branchOrder: index,
372
+ };
373
+ changed += 1;
374
+ });
375
+ if (changed > 0) core?.state?.markDirty?.();
376
+ return { changed, count: siblings.length };
377
+ }
378
+
379
+ function syncMindmapVisualFromState({ core, eventBus, objectId }) {
380
+ if (!core || !objectId) return;
381
+ const stateObjects = core?.state?.state?.objects || [];
382
+ const objectData = (Array.isArray(stateObjects) ? stateObjects : []).find((obj) => obj?.id === objectId && obj?.type === 'mindmap');
383
+ if (!objectData) return;
384
+
385
+ const pixiObject = core?.pixi?.objects?.get?.(objectId);
386
+ const instance = pixiObject?._mb?.instance;
387
+ const props = objectData?.properties || {};
388
+ if (!instance) return;
389
+
390
+ if (Number.isFinite(props.strokeColor)) instance.strokeColor = Math.floor(Number(props.strokeColor));
391
+ if (Number.isFinite(props.fillColor)) instance.fillColor = Math.floor(Number(props.fillColor));
392
+ if (Number.isFinite(props.fillAlpha)) instance.fillAlpha = Number(props.fillAlpha);
393
+ if (Number.isFinite(props.strokeWidth)) instance.strokeWidth = Math.max(1, Math.round(Number(props.strokeWidth)));
394
+
395
+ if (pixiObject?._mb) {
396
+ pixiObject._mb.properties = props;
397
+ }
398
+ if (typeof instance._redrawPreserveTransform === 'function') {
399
+ instance._redrawPreserveTransform();
400
+ } else if (typeof instance._draw === 'function') {
401
+ instance._draw();
402
+ }
403
+ eventBus?.emit?.(Events.Object.Updated, { objectId });
404
+ }
405
+
406
+ function collectMindmapStateEntries(core) {
407
+ const objects = core?.state?.state?.objects || [];
408
+ const entries = {};
409
+ (Array.isArray(objects) ? objects : []).forEach((obj) => {
410
+ if (!obj || obj.type !== 'mindmap' || !obj.id) return;
411
+ entries[obj.id] = {
412
+ id: obj.id,
413
+ position: {
414
+ x: Math.round(obj?.position?.x || 0),
415
+ y: Math.round(obj?.position?.y || 0),
416
+ },
417
+ size: {
418
+ width: Math.max(1, Math.round(obj?.width || obj?.properties?.width || 1)),
419
+ height: Math.max(1, Math.round(obj?.height || obj?.properties?.height || 1)),
420
+ },
421
+ properties: JSON.parse(JSON.stringify(obj?.properties || {})),
422
+ };
423
+ });
424
+ return entries;
425
+ }
426
+
427
+ function collectMindmapStateEntriesFromSnapshot(snapshot) {
428
+ const entries = {};
429
+ const src = snapshot && typeof snapshot === 'object' ? snapshot : {};
430
+ Object.keys(src).forEach((id) => {
431
+ if (id === '__meta__') return;
432
+ const item = src[id];
433
+ if (!item || typeof item !== 'object') return;
434
+ const state = item.state;
435
+ if (!state || typeof state !== 'object') return;
436
+ entries[id] = JSON.parse(JSON.stringify(state));
437
+ });
438
+ return entries;
439
+ }
440
+
441
+ function areMindmapStateEntriesEqual(beforeEntries, afterEntries) {
442
+ const before = beforeEntries && typeof beforeEntries === 'object' ? beforeEntries : {};
443
+ const after = afterEntries && typeof afterEntries === 'object' ? afterEntries : {};
444
+ const beforeIds = Object.keys(before).sort();
445
+ const afterIds = Object.keys(after).sort();
446
+ if (beforeIds.length !== afterIds.length) return false;
447
+ for (let i = 0; i < beforeIds.length; i += 1) {
448
+ if (beforeIds[i] !== afterIds[i]) return false;
449
+ const id = beforeIds[i];
450
+ if (JSON.stringify(before[id]) !== JSON.stringify(after[id])) return false;
451
+ }
452
+ return true;
453
+ }
454
+
455
+ function replaceLastMoveHistoryWithMindmapSnapshot({ core, eventBus, beforeSnapshot }) {
456
+ if (!core?.history || !beforeSnapshot) return;
457
+ const history = core.history;
458
+ const idx = Number(history.currentIndex);
459
+ if (!Number.isFinite(idx) || idx < 0 || !Array.isArray(history.history) || idx >= history.history.length) return;
460
+ const last = history.history[idx];
461
+ if (!last || (last.type !== 'move_object' && last.type !== 'group-move')) return;
462
+
463
+ const beforeEntries = collectMindmapStateEntriesFromSnapshot(beforeSnapshot);
464
+ const afterEntries = collectMindmapStateEntries(core);
465
+ if (areMindmapStateEntriesEqual(beforeEntries, afterEntries)) return;
466
+
467
+ const command = new MindmapStatePatchCommand(core, beforeEntries, afterEntries, 'Изменить состояние mindmap после drag');
468
+ command.setEventBus(eventBus);
469
+ command.timestamp = last.timestamp;
470
+ history.history[idx] = command;
471
+ }
472
+
473
+ function tryReparentMindmapOnDrop({ core, draggedId, draggedIds = [] }) {
474
+ if (!core || !draggedId) return null;
475
+ const objects = core?.state?.state?.objects || [];
476
+ const mindmaps = Array.isArray(objects) ? objects.filter((obj) => obj?.type === 'mindmap') : [];
477
+ const byId = new Map(mindmaps.map((obj) => [obj.id, obj]));
478
+ const dragged = byId.get(draggedId);
479
+ if (!dragged) return null;
480
+
481
+ const normalizedDraggedIds = Array.from(new Set(
482
+ [draggedId, ...(Array.isArray(draggedIds) ? draggedIds : [])]
483
+ .filter((id) => typeof id === 'string' && id.length > 0)
484
+ .filter((id) => byId.has(id))
485
+ ));
486
+ const selectedSet = new Set(normalizedDraggedIds);
487
+ const topLevelDraggedIds = normalizedDraggedIds.filter((id) => {
488
+ const node = byId.get(id);
489
+ const meta = node?.properties?.mindmap || {};
490
+ const parentId = meta?.parentId || null;
491
+ return !parentId || !selectedSet.has(parentId);
492
+ });
493
+
494
+ const childrenByParent = collectMindmapChildrenByParent(mindmaps);
495
+ const descendantIds = collectMindmapDescendants(childrenByParent, draggedId);
496
+ const blockedTargetIds = new Set([draggedId, ...descendantIds]);
497
+ const draggedRect = getMindmapRect(dragged);
498
+ const draggedCenter = {
499
+ x: draggedRect.x + Math.round(draggedRect.width / 2),
500
+ y: draggedRect.y + Math.round(draggedRect.height / 2),
501
+ };
502
+ const candidates = mindmaps.filter((obj) => !blockedTargetIds.has(obj?.id));
503
+ if (!candidates.length) return null;
504
+
505
+ let best = null;
506
+ candidates.forEach((target) => {
507
+ const targetRect = getMindmapRect(target);
508
+ if (!isPointInsideRect(draggedCenter, targetRect)) return;
509
+ const targetCenter = {
510
+ x: targetRect.x + Math.round(targetRect.width / 2),
511
+ y: targetRect.y + Math.round(targetRect.height / 2),
512
+ };
513
+ const dx = draggedCenter.x - targetCenter.x;
514
+ const dy = draggedCenter.y - targetCenter.y;
515
+ const dist2 = dx * dx + dy * dy;
516
+ if (!best || dist2 < best.dist2) {
517
+ best = { target, dist2 };
518
+ }
519
+ });
520
+ if (!best?.target?.id) return null;
521
+
522
+ const target = best.target;
523
+ const targetMeta = target?.properties?.mindmap || {};
524
+ const draggedMeta = dragged?.properties?.mindmap || {};
525
+ const targetRect = getMindmapRect(target);
526
+ const draggedCenterX = draggedRect.x + Math.round(draggedRect.width / 2);
527
+ const targetCenterX = targetRect.x + Math.round(targetRect.width / 2);
528
+ const nextParentId = target.id;
529
+ const nextCompoundId = (typeof targetMeta?.compoundId === 'string' && targetMeta.compoundId.length > 0)
530
+ ? targetMeta.compoundId
531
+ : target.id;
532
+ const compoundRoot = mindmaps.find((obj) => {
533
+ if (!obj || obj.type !== 'mindmap') return false;
534
+ const meta = obj?.properties?.mindmap || {};
535
+ return meta?.role === 'root' && meta?.compoundId === nextCompoundId && !meta?.parentId;
536
+ }) || null;
537
+ const rootRect = compoundRoot ? getMindmapRect(compoundRoot) : null;
538
+ const rootCenterX = rootRect
539
+ ? Math.round(rootRect.x + Math.round(rootRect.width / 2))
540
+ : targetCenterX;
541
+ const nextSide = draggedCenterX < rootCenterX ? 'left' : 'right';
542
+ const nextBranchColor = asBranchColor(targetMeta?.branchColor)
543
+ ?? asBranchColor(target?.properties?.strokeColor)
544
+ ?? asBranchColor(draggedMeta?.branchColor)
545
+ ?? asBranchColor(dragged?.properties?.strokeColor);
546
+
547
+ const reparentedIds = [];
548
+ const movedIdsSet = new Set();
549
+ const prevScopes = [];
550
+ let firstChanged = null;
551
+ let changed = false;
552
+
553
+ const applyReparentForNode = (nodeId) => {
554
+ const node = byId.get(nodeId);
555
+ if (!node) return;
556
+ const nodeMeta = node?.properties?.mindmap || {};
557
+ const nodeDescendantIds = collectMindmapDescendants(childrenByParent, nodeId);
558
+ const blockedForNode = new Set([nodeId, ...nodeDescendantIds]);
559
+ if (blockedForNode.has(nextParentId)) return;
560
+
561
+ const prevParentId = nodeMeta?.parentId || null;
562
+ const prevSide = nodeMeta?.side || null;
563
+ const prevCompoundId = nodeMeta?.compoundId || null;
564
+ const unchanged = prevParentId === nextParentId
565
+ && prevSide === nextSide
566
+ && prevCompoundId === nextCompoundId
567
+ && nodeMeta?.role === 'child';
568
+ if (unchanged) return;
569
+
570
+ const shouldMirrorOrientation = (nodeMeta?.side === 'left' || nodeMeta?.side === 'right')
571
+ && nodeMeta.side !== nextSide;
572
+ const mirrorSide = (value) => (value === 'left' ? 'right' : (value === 'right' ? 'left' : value));
573
+
574
+ if (!node.properties) node.properties = {};
575
+ node.properties.mindmap = {
576
+ ...nodeMeta,
577
+ role: 'child',
578
+ parentId: nextParentId,
579
+ side: nextSide,
580
+ compoundId: nextCompoundId,
581
+ branchRootId: nextParentId,
582
+ branchOrder: null,
583
+ branchColor: nextBranchColor,
584
+ };
585
+ if (Number.isFinite(nextBranchColor)) {
586
+ node.properties.strokeColor = nextBranchColor;
587
+ node.properties.fillColor = nextBranchColor;
588
+ if (!Number.isFinite(node.properties.fillAlpha)) {
589
+ node.properties.fillAlpha = MINDMAP_CHILD_FILL_ALPHA;
590
+ }
591
+ }
592
+ syncMindmapVisualFromState({ core, eventBus: core?.eventBus, objectId: nodeId });
593
+
594
+ movedIdsSet.add(nodeId);
595
+ nodeDescendantIds.forEach((id) => {
596
+ const descendant = byId.get(id);
597
+ if (!descendant) return;
598
+ if (!descendant.properties) descendant.properties = {};
599
+ const meta = descendant.properties.mindmap || {};
600
+ descendant.properties.mindmap = {
601
+ ...meta,
602
+ compoundId: nextCompoundId,
603
+ side: shouldMirrorOrientation ? mirrorSide(meta?.side) : meta?.side,
604
+ branchColor: Number.isFinite(nextBranchColor)
605
+ ? nextBranchColor
606
+ : (asBranchColor(meta?.branchColor) ?? null),
607
+ };
608
+ if (Number.isFinite(nextBranchColor)) {
609
+ descendant.properties.strokeColor = nextBranchColor;
610
+ descendant.properties.fillColor = nextBranchColor;
611
+ if (!Number.isFinite(descendant.properties.fillAlpha)) {
612
+ descendant.properties.fillAlpha = MINDMAP_CHILD_FILL_ALPHA;
613
+ }
614
+ }
615
+ syncMindmapVisualFromState({ core, eventBus: core?.eventBus, objectId: id });
616
+ movedIdsSet.add(id);
617
+ });
618
+
619
+ reparentedIds.push(nodeId);
620
+ prevScopes.push({
621
+ parentId: prevParentId,
622
+ side: prevSide,
623
+ compoundId: prevCompoundId,
624
+ });
625
+ if (!firstChanged) {
626
+ firstChanged = { prevParentId, prevSide, prevCompoundId };
627
+ }
628
+ changed = true;
629
+ };
630
+
631
+ applyReparentForNode(draggedId);
632
+ topLevelDraggedIds.forEach((id) => {
633
+ if (id === draggedId) return;
634
+ applyReparentForNode(id);
635
+ });
636
+
637
+ if (!changed) return null;
638
+ core?.state?.markDirty?.();
639
+
640
+ normalizeBranchOrderByCurrentY({ core, parentId: nextParentId, side: nextSide });
641
+ const prevScopeKeys = new Set();
642
+ prevScopes.forEach((scope) => {
643
+ const parentId = scope?.parentId || null;
644
+ const side = scope?.side || null;
645
+ if (!parentId || (side !== 'left' && side !== 'right')) return;
646
+ const key = `${parentId}::${side}`;
647
+ if (prevScopeKeys.has(key)) return;
648
+ prevScopeKeys.add(key);
649
+ normalizeBranchOrderByCurrentY({ core, parentId, side });
650
+ });
651
+ return {
652
+ changed: true,
653
+ draggedId,
654
+ reparentedIds,
655
+ movedIds: Array.from(movedIdsSet),
656
+ prevParentId: firstChanged?.prevParentId || null,
657
+ prevSide: firstChanged?.prevSide || null,
658
+ prevCompoundId: firstChanged?.prevCompoundId || null,
659
+ prevScopes,
660
+ nextParentId,
661
+ nextSide,
662
+ nextCompoundId,
663
+ };
664
+ }
665
+
666
+ function tryReorderMindmapBranchByDraggedNode({ core, draggedId }) {
667
+ if (!core || !draggedId) return null;
668
+ const objects = core?.state?.state?.objects || [];
669
+ const mindmaps = Array.isArray(objects) ? objects.filter((obj) => obj?.type === 'mindmap') : [];
670
+ const byId = new Map(mindmaps.map((obj) => [obj.id, obj]));
671
+ const dragged = byId.get(draggedId);
672
+ if (!dragged) return null;
673
+
674
+ const draggedMeta = dragged?.properties?.mindmap || {};
675
+ const parentId = draggedMeta?.parentId || null;
676
+ const side = draggedMeta?.side || null;
677
+ if (draggedMeta?.role !== 'child' || !parentId || (side !== 'left' && side !== 'right')) return null;
678
+
679
+ const branchNodes = mindmaps
680
+ .filter((obj) => {
681
+ const meta = obj?.properties?.mindmap || {};
682
+ return meta?.role === 'child' && meta?.parentId === parentId && meta?.side === side;
683
+ });
684
+ if (branchNodes.length <= 1) return null;
685
+
686
+ const getOrder = (obj) => {
687
+ const raw = obj?.properties?.mindmap?.branchOrder;
688
+ return Number.isFinite(raw) ? Number(raw) : null;
689
+ };
690
+ const sortByOrderThenY = (a, b) => {
691
+ const ao = getOrder(a);
692
+ const bo = getOrder(b);
693
+ if (ao !== null && bo !== null && ao !== bo) return ao - bo;
694
+ if (ao !== null && bo === null) return -1;
695
+ if (ao === null && bo !== null) return 1;
696
+ const ay = Math.round(a?.position?.y || 0);
697
+ const by = Math.round(b?.position?.y || 0);
698
+ if (ay !== by) return ay - by;
699
+ return String(a?.id || '').localeCompare(String(b?.id || ''));
700
+ };
701
+
702
+ const orderedCurrent = [...branchNodes].sort(sortByOrderThenY);
703
+ const currentIndex = orderedCurrent.findIndex((obj) => obj?.id === draggedId);
704
+ if (currentIndex < 0) return null;
705
+ const orderedBeforeIds = orderedCurrent.map((obj) => obj?.id).filter(Boolean);
706
+
707
+ const siblings = orderedCurrent.filter((obj) => obj?.id !== draggedId);
708
+ const draggedCenterY = Math.round(dragged?.position?.y || 0) + Math.round((dragged?.height || dragged?.properties?.height || 0) / 2);
709
+ const insertionIndex = (() => {
710
+ let idx = 0;
711
+ while (idx < siblings.length) {
712
+ const node = siblings[idx];
713
+ const centerY = Math.round(node?.position?.y || 0) + Math.round((node?.height || node?.properties?.height || 0) / 2);
714
+ if (draggedCenterY < centerY) break;
715
+ idx += 1;
716
+ }
717
+ return idx;
718
+ })();
719
+ logMindmapCompoundDebug('layout:branch-reorder-eval', {
720
+ draggedId,
721
+ parentId,
722
+ side,
723
+ branchSize: branchNodes.length,
724
+ draggedCenterY,
725
+ fromIndex: currentIndex,
726
+ toIndex: insertionIndex,
727
+ orderedBeforeIds,
728
+ });
729
+ if (insertionIndex === currentIndex) {
730
+ return { changed: false, parentId, side, fromIndex: currentIndex, toIndex: insertionIndex };
731
+ }
732
+
733
+ const nextOrder = [...siblings];
734
+ nextOrder.splice(insertionIndex, 0, dragged);
735
+ let changedCount = 0;
736
+ nextOrder.forEach((node, index) => {
737
+ if (!node?.properties) node.properties = {};
738
+ const prevMeta = node.properties.mindmap || {};
739
+ const prevOrder = Number.isFinite(prevMeta.branchOrder) ? Number(prevMeta.branchOrder) : null;
740
+ if (prevOrder === index) return;
741
+ node.properties.mindmap = {
742
+ ...prevMeta,
743
+ branchOrder: index,
744
+ };
745
+ changedCount += 1;
746
+ });
747
+ if (changedCount > 0) {
748
+ core?.state?.markDirty?.();
749
+ }
750
+ const orderedAfterIds = nextOrder.map((obj) => obj?.id).filter(Boolean);
751
+ logMindmapCompoundDebug('layout:branch-reorder-apply', {
752
+ draggedId,
753
+ parentId,
754
+ side,
755
+ fromIndex: currentIndex,
756
+ toIndex: insertionIndex,
757
+ changedCount,
758
+ orderedAfterIds,
759
+ });
760
+ return {
761
+ changed: changedCount > 0,
762
+ changedCount,
763
+ parentId,
764
+ side,
765
+ fromIndex: currentIndex,
766
+ toIndex: insertionIndex,
767
+ };
768
+ }
769
+
770
+ function resolvePrimaryDraggedMindmapId({ byId, draggedMindmapIds = [] }) {
771
+ const ids = Array.isArray(draggedMindmapIds) ? draggedMindmapIds.filter((id) => typeof id === 'string' && id.length > 0) : [];
772
+ if (!ids.length) return null;
773
+ if (ids.length === 1) return ids[0];
774
+ const idSet = new Set(ids);
775
+ const topLevelCandidates = ids.filter((id) => {
776
+ const node = byId.get(id);
777
+ const meta = node?.properties?.mindmap || {};
778
+ const parentId = meta?.parentId || null;
779
+ return !parentId || !idSet.has(parentId);
780
+ });
781
+ if (topLevelCandidates.length === 1) return topLevelCandidates[0];
782
+ const childCandidate = topLevelCandidates.find((id) => {
783
+ const node = byId.get(id);
784
+ const role = node?.properties?.mindmap?.role || null;
785
+ return role === 'child';
786
+ });
787
+ if (childCandidate) return childCandidate;
788
+ return topLevelCandidates[0] || ids[0];
789
+ }
6
790
 
7
791
  export class HandlesDomRenderer {
8
792
  constructor(host, rotateIconSvg) {
@@ -10,25 +794,351 @@ export class HandlesDomRenderer {
10
794
  this.rotateIconSvg = rotateIconSvg;
11
795
  }
12
796
 
797
+ captureMindmapSnapshot() {
798
+ const objects = this.host.core?.state?.state?.objects || [];
799
+ const snapshot = {};
800
+ (Array.isArray(objects) ? objects : []).forEach((obj) => {
801
+ if (!obj || obj.type !== 'mindmap' || !obj.id) return;
802
+ snapshot[obj.id] = {
803
+ x: Math.round(obj?.position?.x || 0),
804
+ y: Math.round(obj?.position?.y || 0),
805
+ state: {
806
+ id: obj.id,
807
+ position: {
808
+ x: Math.round(obj?.position?.x || 0),
809
+ y: Math.round(obj?.position?.y || 0),
810
+ },
811
+ size: {
812
+ width: Math.max(1, Math.round(obj?.width || obj?.properties?.width || 1)),
813
+ height: Math.max(1, Math.round(obj?.height || obj?.properties?.height || 1)),
814
+ },
815
+ properties: JSON.parse(JSON.stringify(obj?.properties || {})),
816
+ },
817
+ };
818
+ });
819
+ return snapshot;
820
+ }
821
+
822
+ enforceMindmapAutoLayoutAfterDragEnd({ draggedIds = [], snapshot = null } = {}) {
823
+ const ids = Array.isArray(draggedIds) ? draggedIds.filter((id) => typeof id === 'string' && id.length > 0) : [];
824
+ if (!ids.length) return;
825
+ const core = this.host.core;
826
+ const eventBus = this.host.eventBus;
827
+ if (!core || !eventBus) return;
828
+ const finalizeMindmapHistory = () => {
829
+ replaceLastMoveHistoryWithMindmapSnapshot({
830
+ core,
831
+ eventBus,
832
+ beforeSnapshot: snapshot,
833
+ });
834
+ };
835
+
836
+ const objects = core?.state?.state?.objects || [];
837
+ const mindmaps = (Array.isArray(objects) ? objects : []).filter((obj) => obj?.type === 'mindmap');
838
+ if (!mindmaps.length) return;
839
+ const byId = new Map(mindmaps.map((obj) => [obj.id, obj]));
840
+ const draggedMindmapIds = ids.filter((id) => byId.has(id));
841
+ if (!draggedMindmapIds.length) return;
842
+ const primaryDraggedId = resolvePrimaryDraggedMindmapId({ byId, draggedMindmapIds });
843
+ if (draggedMindmapIds.length > 1) {
844
+ const dragMeta = draggedMindmapIds.map((id) => {
845
+ const node = byId.get(id);
846
+ const meta = node?.properties?.mindmap || {};
847
+ return {
848
+ id,
849
+ role: meta.role || null,
850
+ parentId: meta.parentId || null,
851
+ side: meta.side || null,
852
+ branchOrder: Number.isFinite(meta.branchOrder) ? Number(meta.branchOrder) : null,
853
+ };
854
+ });
855
+ logMindmapCompoundDebug('layout:drag-end-skip-reorder-multi', {
856
+ draggedIds: draggedMindmapIds,
857
+ dragMeta,
858
+ primaryDraggedId: primaryDraggedId || null,
859
+ });
860
+ }
861
+ const reparentResult = primaryDraggedId
862
+ ? tryReparentMindmapOnDrop({ core, draggedId: primaryDraggedId, draggedIds: draggedMindmapIds })
863
+ : null;
864
+ if (reparentResult?.changed) {
865
+ logMindmapCompoundDebug('layout:drag-end-reparent', {
866
+ draggedId: reparentResult.draggedId,
867
+ reparentedIds: Array.isArray(reparentResult.reparentedIds) ? reparentResult.reparentedIds : [],
868
+ prevParentId: reparentResult.prevParentId || null,
869
+ prevSide: reparentResult.prevSide || null,
870
+ nextParentId: reparentResult.nextParentId || null,
871
+ nextSide: reparentResult.nextSide || null,
872
+ prevCompoundId: reparentResult.prevCompoundId || null,
873
+ nextCompoundId: reparentResult.nextCompoundId || null,
874
+ });
875
+ relayoutMindmapBranchCascade({
876
+ core,
877
+ eventBus,
878
+ startParentId: reparentResult.nextParentId,
879
+ startSide: reparentResult.nextSide,
880
+ });
881
+ const prevScopes = Array.isArray(reparentResult.prevScopes)
882
+ ? reparentResult.prevScopes
883
+ : [{
884
+ parentId: reparentResult.prevParentId || null,
885
+ side: reparentResult.prevSide || null,
886
+ }];
887
+ const emittedPrev = new Set();
888
+ prevScopes.forEach((scope) => {
889
+ const parentId = scope?.parentId || null;
890
+ const side = scope?.side || null;
891
+ if (!parentId || (side !== 'left' && side !== 'right')) return;
892
+ const key = `${parentId}::${side}`;
893
+ if (emittedPrev.has(key)) return;
894
+ emittedPrev.add(key);
895
+ relayoutMindmapBranchCascade({
896
+ core,
897
+ eventBus,
898
+ startParentId: parentId,
899
+ startSide: side,
900
+ });
901
+ });
902
+ relayoutMindmapSubtreeLevels({
903
+ core,
904
+ eventBus,
905
+ rootIds: Array.isArray(reparentResult.reparentedIds)
906
+ ? reparentResult.reparentedIds
907
+ : [reparentResult.draggedId].filter(Boolean),
908
+ });
909
+ finalizeMindmapHistory();
910
+ return;
911
+ }
912
+ const reorderResult = primaryDraggedId
913
+ ? tryReorderMindmapBranchByDraggedNode({ core, draggedId: primaryDraggedId })
914
+ : null;
915
+ if (reorderResult?.changed) {
916
+ logMindmapCompoundDebug('layout:drag-end-branch-reorder', {
917
+ draggedId: primaryDraggedId,
918
+ parentId: reorderResult.parentId || null,
919
+ side: reorderResult.side || null,
920
+ fromIndex: reorderResult.fromIndex,
921
+ toIndex: reorderResult.toIndex,
922
+ changedCount: reorderResult.changedCount || 0,
923
+ });
924
+ }
925
+
926
+ const compoundToAllIds = new Map();
927
+ const compoundOf = (obj) => {
928
+ const c = obj?.properties?.mindmap?.compoundId;
929
+ return (typeof c === 'string' && c.length > 0) ? c : obj?.id;
930
+ };
931
+ mindmaps.forEach((obj) => {
932
+ const key = compoundOf(obj);
933
+ if (!compoundToAllIds.has(key)) compoundToAllIds.set(key, new Set());
934
+ compoundToAllIds.get(key).add(obj.id);
935
+ });
936
+ const touchedCompounds = new Set(draggedMindmapIds.map((id) => compoundOf(byId.get(id))));
937
+ const childrenByParent = collectMindmapChildrenByParent(mindmaps);
938
+ const translatedScopeByCompound = new Map();
939
+
940
+ if (!reorderResult?.changed && primaryDraggedId && snapshot) {
941
+ const primaryNode = byId.get(primaryDraggedId);
942
+ const primarySnap = snapshot[primaryDraggedId];
943
+ const primaryMeta = primaryNode?.properties?.mindmap || {};
944
+ const parentNode = primaryMeta?.parentId ? byId.get(primaryMeta.parentId) : null;
945
+ const parentRole = parentNode?.properties?.mindmap?.role || null;
946
+ const allowPersistentTranslate = primaryMeta?.role === 'root'
947
+ || (primaryMeta?.role === 'child' && parentRole === 'root');
948
+ const currentX = Math.round(primaryNode?.position?.x || 0);
949
+ const currentY = Math.round(primaryNode?.position?.y || 0);
950
+ const dx = Number.isFinite(primarySnap?.x) ? Math.round(currentX - Number(primarySnap.x)) : 0;
951
+ const dy = Number.isFinite(primarySnap?.y) ? Math.round(currentY - Number(primarySnap.y)) : 0;
952
+ if (primaryNode && allowPersistentTranslate && (dx !== 0 || dy !== 0)) {
953
+ const compoundId = compoundOf(primaryNode);
954
+ const allIds = compoundToAllIds.get(compoundId) || new Set();
955
+ const moveScopeIds = (() => {
956
+ if (primaryMeta?.role === 'root' || !primaryMeta?.parentId) {
957
+ return new Set(allIds);
958
+ }
959
+ const descendants = collectMindmapDescendants(childrenByParent, primaryDraggedId);
960
+ return new Set([primaryDraggedId, ...descendants]);
961
+ })();
962
+ moveScopeIds.forEach((nodeId) => {
963
+ const snap = snapshot[nodeId];
964
+ if (!snap || !Number.isFinite(snap.x) || !Number.isFinite(snap.y)) return;
965
+ const nextPos = { x: Math.round(Number(snap.x) + dx), y: Math.round(Number(snap.y) + dy) };
966
+ core.updateObjectPositionDirect(nodeId, nextPos, { snap: false });
967
+ eventBus.emit(Events.Object.TransformUpdated, { objectId: nodeId });
968
+ eventBus.emit(Events.Tool.DragUpdate, { object: nodeId });
969
+ });
970
+ translatedScopeByCompound.set(compoundId, moveScopeIds);
971
+ logMindmapCompoundDebug('layout:drag-end-translate-scope', {
972
+ primaryDraggedId,
973
+ compoundId,
974
+ role: primaryMeta?.role || null,
975
+ parentId: primaryMeta?.parentId || null,
976
+ dx,
977
+ dy,
978
+ movedIds: Array.from(moveScopeIds),
979
+ });
980
+ }
981
+ }
982
+
983
+ touchedCompounds.forEach((compoundId) => {
984
+ const allIds = compoundToAllIds.get(compoundId) || new Set();
985
+ const selectedIds = draggedMindmapIds.filter((id) => allIds.has(id));
986
+ const isWholeTreeMove = selectedIds.length > 0 && selectedIds.length === allIds.size;
987
+ if (isWholeTreeMove) return;
988
+ const translatedIds = translatedScopeByCompound.get(compoundId) || new Set();
989
+
990
+ allIds.forEach((nodeId) => {
991
+ if (translatedIds.has(nodeId)) return;
992
+ const snap = snapshot && snapshot[nodeId];
993
+ if (!snap || !Number.isFinite(snap.x) || !Number.isFinite(snap.y)) return;
994
+ const nextPos = { x: Math.round(snap.x), y: Math.round(snap.y) };
995
+ core.updateObjectPositionDirect(nodeId, nextPos, { snap: false });
996
+ eventBus.emit(Events.Object.TransformUpdated, { objectId: nodeId });
997
+ eventBus.emit(Events.Tool.DragUpdate, { object: nodeId });
998
+ });
999
+ logMindmapCompoundDebug('layout:drag-end-restore-compound', {
1000
+ compoundId,
1001
+ selectedCount: selectedIds.length,
1002
+ totalCount: allIds.size,
1003
+ });
1004
+ });
1005
+
1006
+ touchedCompounds.forEach((compoundId) => {
1007
+ if (!reorderResult?.changed && translatedScopeByCompound.has(compoundId)) return;
1008
+ const compoundNodes = mindmaps.filter((obj) => compoundOf(obj) === compoundId);
1009
+ const roots = compoundNodes.filter((obj) => {
1010
+ const meta = obj?.properties?.mindmap || {};
1011
+ if (meta?.role === 'root') return true;
1012
+ return !meta?.parentId;
1013
+ });
1014
+ roots.forEach((root) => {
1015
+ relayoutMindmapBranchCascade({
1016
+ core,
1017
+ eventBus,
1018
+ startParentId: root.id,
1019
+ startSide: 'left',
1020
+ });
1021
+ relayoutMindmapBranchCascade({
1022
+ core,
1023
+ eventBus,
1024
+ startParentId: root.id,
1025
+ startSide: 'right',
1026
+ });
1027
+ });
1028
+ });
1029
+ if (reorderResult?.changed && reorderResult.parentId && (reorderResult.side === 'left' || reorderResult.side === 'right')) {
1030
+ relayoutMindmapBranchCascade({
1031
+ core,
1032
+ eventBus,
1033
+ startParentId: reorderResult.parentId,
1034
+ startSide: reorderResult.side,
1035
+ });
1036
+ }
1037
+ finalizeMindmapHistory();
1038
+ }
1039
+
13
1040
  setHandlesVisibility(show) {
14
1041
  if (!this.host.layer) return;
15
1042
  const box = this.host.layer.querySelector('.mb-handles-box');
16
1043
  if (!box) return;
17
1044
 
1045
+ const applyVisibility = (el) => {
1046
+ if (!el) return;
1047
+ const lockedHidden = el.dataset.lockedHidden === '1';
1048
+ el.style.display = show && !lockedHidden ? '' : 'none';
1049
+ };
1050
+
18
1051
  box.querySelectorAll('[data-dir]').forEach((el) => {
19
- el.style.display = show ? '' : 'none';
1052
+ applyVisibility(el);
20
1053
  });
21
1054
  box.querySelectorAll('[data-edge]').forEach((el) => {
22
- el.style.display = show ? '' : 'none';
1055
+ applyVisibility(el);
23
1056
  });
24
1057
 
25
1058
  const rot = box.querySelector('[data-handle="rotate"]');
26
- if (rot) rot.style.display = show ? '' : 'none';
1059
+ if (rot) applyVisibility(rot);
1060
+ this.host.layer.querySelectorAll('.mb-mindmap-side-btn').forEach((btn) => {
1061
+ btn.style.display = show ? '' : 'none';
1062
+ });
27
1063
  if (show && !box.querySelector('[data-dir]')) {
28
1064
  this.host.update();
29
1065
  }
30
1066
  }
31
1067
 
1068
+ relayoutMindmapAfterResize({ objectId } = {}) {
1069
+ if (!objectId) return;
1070
+ const core = this.host.core;
1071
+ const eventBus = this.host.eventBus;
1072
+ if (!core || !eventBus) return;
1073
+ const objects = core?.state?.state?.objects || [];
1074
+ const node = (Array.isArray(objects) ? objects : []).find((obj) => obj?.id === objectId && obj?.type === 'mindmap');
1075
+ if (!node) return;
1076
+ const meta = node?.properties?.mindmap || {};
1077
+ const role = meta?.role || null;
1078
+ const parentId = meta?.parentId || null;
1079
+ const side = meta?.side || null;
1080
+
1081
+ if (role === 'root' || !parentId) {
1082
+ relayoutMindmapBranchLevel({ core, eventBus, parentId: objectId, side: 'left' });
1083
+ relayoutMindmapBranchLevel({ core, eventBus, parentId: objectId, side: 'right' });
1084
+ return;
1085
+ }
1086
+
1087
+ if (side === 'left' || side === 'right') {
1088
+ relayoutMindmapBranchCascade({
1089
+ core,
1090
+ eventBus,
1091
+ startParentId: parentId,
1092
+ startSide: side,
1093
+ });
1094
+ }
1095
+ relayoutMindmapSubtreeLevels({
1096
+ core,
1097
+ eventBus,
1098
+ rootIds: [objectId],
1099
+ });
1100
+ }
1101
+
1102
+ relayoutAllMindmapCompounds() {
1103
+ const core = this.host.core;
1104
+ const eventBus = this.host.eventBus;
1105
+ if (!core || !eventBus) return;
1106
+ const objects = core?.state?.state?.objects || [];
1107
+ const mindmaps = (Array.isArray(objects) ? objects : []).filter((obj) => obj?.type === 'mindmap');
1108
+ if (!mindmaps.length) return;
1109
+
1110
+ const rootIds = mindmaps
1111
+ .filter((obj) => {
1112
+ const meta = obj?.properties?.mindmap || {};
1113
+ if (meta?.role === 'root') return true;
1114
+ return !meta?.parentId;
1115
+ })
1116
+ .map((obj) => obj.id)
1117
+ .filter((id) => typeof id === 'string' && id.length > 0);
1118
+ const uniqueRootIds = Array.from(new Set(rootIds));
1119
+ if (!uniqueRootIds.length) return;
1120
+
1121
+ relayoutMindmapSubtreeLevels({
1122
+ core,
1123
+ eventBus,
1124
+ rootIds: uniqueRootIds,
1125
+ });
1126
+ uniqueRootIds.forEach((rootId) => {
1127
+ relayoutMindmapBranchCascade({
1128
+ core,
1129
+ eventBus,
1130
+ startParentId: rootId,
1131
+ startSide: 'left',
1132
+ });
1133
+ relayoutMindmapBranchCascade({
1134
+ core,
1135
+ eventBus,
1136
+ startParentId: rootId,
1137
+ startSide: 'right',
1138
+ });
1139
+ });
1140
+ }
1141
+
32
1142
  showBounds(worldBounds, id, options = {}) {
33
1143
  if (!this.host.layer) return;
34
1144
 
@@ -36,22 +1146,52 @@ export class HandlesDomRenderer {
36
1146
 
37
1147
  let isFileTarget = false;
38
1148
  let isFrameTarget = false;
1149
+ let isMindmapTarget = false;
1150
+ let isMindmapOnlyGroupTarget = false;
39
1151
  let isRevitScreenshotTarget = false;
40
1152
  let revitViewPayload = null;
1153
+ let sourceMindmapProperties = null;
1154
+ const occupiedOutgoingSides = new Set();
1155
+ const hiddenIncomingSide = { value: null };
41
1156
  if (id !== '__group__') {
42
1157
  const req = { objectId: id, pixiObject: null };
43
1158
  this.host.eventBus.emit(Events.Tool.GetObjectPixi, req);
44
1159
  const mbType = req.pixiObject && req.pixiObject._mb && req.pixiObject._mb.type;
45
1160
  isFileTarget = mbType === 'file';
46
1161
  isFrameTarget = mbType === 'frame';
1162
+ isMindmapTarget = mbType === 'mindmap';
47
1163
  isRevitScreenshotTarget = mbType === 'revit-screenshot-img';
48
1164
  revitViewPayload = req.pixiObject?._mb?.properties?.view || null;
1165
+ if (isMindmapTarget) {
1166
+ sourceMindmapProperties = req.pixiObject?._mb?.properties || null;
1167
+ const allObjects = this.host.core?.state?.state?.objects || [];
1168
+ allObjects.forEach((obj) => {
1169
+ if (!obj || obj.type !== 'mindmap') return;
1170
+ const meta = obj.properties?.mindmap || {};
1171
+ const isOutgoingFromCurrent = meta?.role === 'child'
1172
+ && typeof meta?.side === 'string'
1173
+ && meta?.parentId === id;
1174
+ if (isOutgoingFromCurrent) {
1175
+ occupiedOutgoingSides.add(meta.side);
1176
+ }
1177
+ });
1178
+ const incoming = sourceMindmapProperties?.mindmap?.side || null;
1179
+ if (incoming === 'left') hiddenIncomingSide.value = 'right';
1180
+ else if (incoming === 'right') hiddenIncomingSide.value = 'left';
1181
+ }
1182
+ } else {
1183
+ const selectionIds = Array.isArray(options.selectionIds) ? options.selectionIds : [];
1184
+ if (selectionIds.length > 0) {
1185
+ const byId = new Map((this.host.core?.state?.state?.objects || []).map((obj) => [obj?.id, obj]));
1186
+ isMindmapOnlyGroupTarget = selectionIds.every((selectedId) => byId.get(selectedId)?.type === 'mindmap');
1187
+ }
49
1188
  }
1189
+ const isNonResizableTarget = isFileTarget || isMindmapTarget || isMindmapOnlyGroupTarget;
50
1190
 
51
- const left = cssRect.left;
52
- const top = cssRect.top;
53
- const width = cssRect.width;
54
- const height = cssRect.height;
1191
+ const left = Math.round(cssRect.left);
1192
+ const top = Math.round(cssRect.top);
1193
+ const width = Math.max(1, Math.round(cssRect.width));
1194
+ const height = Math.max(1, Math.round(cssRect.height));
55
1195
 
56
1196
  this.host.layer.innerHTML = '';
57
1197
  const box = document.createElement('div');
@@ -83,11 +1223,12 @@ export class HandlesDomRenderer {
83
1223
  h.dataset.dir = dir;
84
1224
  h.dataset.id = id;
85
1225
  h.className = 'mb-handle';
86
- h.style.pointerEvents = isFileTarget ? 'none' : 'auto';
1226
+ h.style.pointerEvents = isNonResizableTarget ? 'none' : 'auto';
87
1227
  h.style.cursor = cursor;
88
1228
  h.style.left = `${x - 6}px`;
89
1229
  h.style.top = `${y - 6}px`;
90
- h.style.display = isFileTarget ? 'none' : 'block';
1230
+ h.style.display = isNonResizableTarget ? 'none' : 'block';
1231
+ if (isNonResizableTarget) h.dataset.lockedHidden = '1';
91
1232
 
92
1233
  const inner = document.createElement('div');
93
1234
  inner.className = 'mb-handle-inner';
@@ -103,7 +1244,7 @@ export class HandlesDomRenderer {
103
1244
  h.style.borderColor = HANDLES_ACCENT_COLOR;
104
1245
  });
105
1246
 
106
- if (!isFileTarget) {
1247
+ if (!isNonResizableTarget) {
107
1248
  h.addEventListener('mousedown', (e) => this.host._onHandleDown(e, box));
108
1249
  }
109
1250
 
@@ -127,11 +1268,12 @@ export class HandlesDomRenderer {
127
1268
  e.dataset.id = id;
128
1269
  e.className = 'mb-edge';
129
1270
  Object.assign(e.style, style, {
130
- pointerEvents: isFileTarget ? 'none' : 'auto',
1271
+ pointerEvents: isNonResizableTarget ? 'none' : 'auto',
131
1272
  cursor,
132
- display: isFileTarget ? 'none' : 'block',
1273
+ display: isNonResizableTarget ? 'none' : 'block',
133
1274
  });
134
- if (!isFileTarget) {
1275
+ if (isNonResizableTarget) e.dataset.lockedHidden = '1';
1276
+ if (!isNonResizableTarget) {
135
1277
  e.addEventListener('mousedown', (evt) => this.host._onEdgeResizeDown(evt));
136
1278
  }
137
1279
  box.appendChild(e);
@@ -169,7 +1311,8 @@ export class HandlesDomRenderer {
169
1311
  const rotateHandle = document.createElement('div');
170
1312
  rotateHandle.dataset.handle = 'rotate';
171
1313
  rotateHandle.dataset.id = id;
172
- if (isFileTarget || isFrameTarget) {
1314
+ if (isFileTarget || isFrameTarget || isMindmapTarget || isMindmapOnlyGroupTarget) {
1315
+ rotateHandle.dataset.lockedHidden = '1';
173
1316
  Object.assign(rotateHandle.style, { display: 'none', pointerEvents: 'none' });
174
1317
  } else {
175
1318
  rotateHandle.className = 'mb-rotate-handle';
@@ -190,6 +1333,334 @@ export class HandlesDomRenderer {
190
1333
  }
191
1334
  box.appendChild(rotateHandle);
192
1335
 
1336
+ if (isMindmapTarget) {
1337
+ const emitChildMindmapFromSource = (direction) => {
1338
+ const app = this.host.core?.pixi?.app;
1339
+ const worldLayer = this.host.core?.pixi?.worldLayer || app?.stage;
1340
+ const rendererRes = app?.renderer?.resolution || 1;
1341
+ const worldScale = worldLayer?.scale?.x || 1;
1342
+ const baseGapWorld = Math.max(1, Math.round((10 * rendererRes) / worldScale));
1343
+ const gapWorld = Math.max(1, Math.round(baseGapWorld * MINDMAP_CHILD_GAP_MULTIPLIER));
1344
+ const childWidth = Math.max(1, Math.round(MINDMAP_LAYOUT.width * MINDMAP_CHILD_WIDTH_FACTOR));
1345
+ const childHeight = Math.max(1, Math.round(MINDMAP_LAYOUT.height * MINDMAP_CHILD_HEIGHT_FACTOR));
1346
+ const childPaddingX = Math.max(1, Math.round(MINDMAP_LAYOUT.paddingX * MINDMAP_CHILD_PADDING_FACTOR));
1347
+ const childPaddingY = Math.max(1, Math.round(MINDMAP_LAYOUT.paddingY * MINDMAP_CHILD_PADDING_FACTOR));
1348
+ const sourceMeta = sourceMindmapProperties?.mindmap || {};
1349
+ const isBottomSiblingClone = direction === 'bottom' && sourceMeta?.role === 'child';
1350
+ const metaParentId = isBottomSiblingClone
1351
+ ? resolveBottomSiblingParentId(id, sourceMeta)
1352
+ : id;
1353
+ const metaSide = isBottomSiblingClone
1354
+ ? (sourceMeta?.side || 'right')
1355
+ : direction;
1356
+ const normalizeBranchOrderForBottomInsert = () => {
1357
+ if (!isBottomSiblingClone || !metaParentId || !metaSide) {
1358
+ return { sourceIndex: -1, siblings: [] };
1359
+ }
1360
+ const objects = this.host.core?.state?.state?.objects || [];
1361
+ const siblings = (Array.isArray(objects) ? objects : [])
1362
+ .filter((obj) => {
1363
+ if (!obj || obj.type !== 'mindmap') return false;
1364
+ const meta = obj.properties?.mindmap || {};
1365
+ return meta?.role === 'child' && meta?.parentId === metaParentId && meta?.side === metaSide;
1366
+ })
1367
+ .sort((a, b) => (a?.position?.y || 0) - (b?.position?.y || 0));
1368
+ siblings.forEach((obj, index) => {
1369
+ if (!obj.properties) obj.properties = {};
1370
+ const meta = obj.properties.mindmap || {};
1371
+ if (!Number.isFinite(meta.branchOrder) || Number(meta.branchOrder) !== index) {
1372
+ obj.properties.mindmap = {
1373
+ ...meta,
1374
+ branchOrder: index,
1375
+ };
1376
+ }
1377
+ });
1378
+ const sourceIndex = siblings.findIndex((obj) => obj?.id === id);
1379
+ if (sourceIndex >= 0) {
1380
+ for (let idx = sourceIndex + 1; idx < siblings.length; idx += 1) {
1381
+ const node = siblings[idx];
1382
+ if (!node?.properties) continue;
1383
+ const meta = node.properties.mindmap || {};
1384
+ const nextOrder = idx + 1;
1385
+ if (!Number.isFinite(meta.branchOrder) || Number(meta.branchOrder) !== nextOrder) {
1386
+ node.properties.mindmap = {
1387
+ ...meta,
1388
+ branchOrder: nextOrder,
1389
+ };
1390
+ }
1391
+ }
1392
+ }
1393
+ this.host.core?.state?.markDirty?.();
1394
+ return { sourceIndex, siblings };
1395
+ };
1396
+ const normalizeBranchOrderForSideCenterInsert = () => {
1397
+ if (isBottomSiblingClone || !metaParentId || (metaSide !== 'left' && metaSide !== 'right')) {
1398
+ return { insertIndex: null, siblings: [] };
1399
+ }
1400
+ const objects = this.host.core?.state?.state?.objects || [];
1401
+ const siblings = (Array.isArray(objects) ? objects : [])
1402
+ .filter((obj) => {
1403
+ if (!obj || obj.type !== 'mindmap') return false;
1404
+ const meta = obj.properties?.mindmap || {};
1405
+ return meta?.role === 'child' && meta?.parentId === metaParentId && meta?.side === metaSide;
1406
+ })
1407
+ .sort((a, b) => {
1408
+ const ao = Number.isFinite(a?.properties?.mindmap?.branchOrder)
1409
+ ? Number(a.properties.mindmap.branchOrder)
1410
+ : null;
1411
+ const bo = Number.isFinite(b?.properties?.mindmap?.branchOrder)
1412
+ ? Number(b.properties.mindmap.branchOrder)
1413
+ : null;
1414
+ if (ao !== null && bo !== null && ao !== bo) return ao - bo;
1415
+ if (ao !== null && bo === null) return -1;
1416
+ if (ao === null && bo !== null) return 1;
1417
+ return (a?.position?.y || 0) - (b?.position?.y || 0);
1418
+ });
1419
+ siblings.forEach((obj, index) => {
1420
+ if (!obj.properties) obj.properties = {};
1421
+ const meta = obj.properties.mindmap || {};
1422
+ if (!Number.isFinite(meta.branchOrder) || Number(meta.branchOrder) !== index) {
1423
+ obj.properties.mindmap = {
1424
+ ...meta,
1425
+ branchOrder: index,
1426
+ };
1427
+ }
1428
+ });
1429
+ const insertIndex = Math.floor(siblings.length / 2);
1430
+ for (let idx = insertIndex; idx < siblings.length; idx += 1) {
1431
+ const node = siblings[idx];
1432
+ if (!node?.properties) continue;
1433
+ const meta = node.properties.mindmap || {};
1434
+ const nextOrder = idx + 1;
1435
+ if (!Number.isFinite(meta.branchOrder) || Number(meta.branchOrder) !== nextOrder) {
1436
+ node.properties.mindmap = {
1437
+ ...meta,
1438
+ branchOrder: nextOrder,
1439
+ };
1440
+ }
1441
+ }
1442
+ this.host.core?.state?.markDirty?.();
1443
+ return { insertIndex, siblings };
1444
+ };
1445
+ const bottomInsertData = normalizeBranchOrderForBottomInsert();
1446
+ const sideCenterInsertData = normalizeBranchOrderForSideCenterInsert();
1447
+ const resolveBottomInsertY = () => {
1448
+ if (!isBottomSiblingClone || !metaParentId || !metaSide) {
1449
+ return Math.round(worldBounds.y + worldBounds.height + gapWorld);
1450
+ }
1451
+ const siblings = bottomInsertData.siblings;
1452
+ const sourceIndex = bottomInsertData.sourceIndex;
1453
+ if (sourceIndex < 0) {
1454
+ return Math.round(worldBounds.y + worldBounds.height + gapWorld);
1455
+ }
1456
+ const sourceY = Math.round(siblings[sourceIndex]?.position?.y || worldBounds.y);
1457
+ const nextSibling = siblings[sourceIndex + 1] || null;
1458
+ if (!nextSibling) {
1459
+ return Math.round(sourceY + Math.max(1, childHeight + 1));
1460
+ }
1461
+ const nextY = Math.round(nextSibling?.position?.y || sourceY + childHeight + 1);
1462
+ if (nextY <= sourceY) return Math.round(sourceY + 1);
1463
+ const midY = Math.round((sourceY + nextY) / 2);
1464
+ if (midY <= sourceY) return Math.round(sourceY + 1);
1465
+ if (midY >= nextY) return Math.round(nextY - 1);
1466
+ return midY;
1467
+ };
1468
+ const resolveSideInsertY = () => {
1469
+ if (isBottomSiblingClone || !metaParentId || (metaSide !== 'left' && metaSide !== 'right')) {
1470
+ return Math.round(worldBounds.y);
1471
+ }
1472
+ const siblings = sideCenterInsertData.siblings || [];
1473
+ const insertIndex = Number.isFinite(sideCenterInsertData.insertIndex)
1474
+ ? Number(sideCenterInsertData.insertIndex)
1475
+ : 0;
1476
+ if (siblings.length === 0) return Math.round(worldBounds.y);
1477
+ if (insertIndex <= 0) return Math.round(siblings[0]?.position?.y || worldBounds.y);
1478
+ if (insertIndex >= siblings.length) {
1479
+ const last = siblings[siblings.length - 1];
1480
+ return Math.round((last?.position?.y || worldBounds.y) + Math.max(1, childHeight + 1));
1481
+ }
1482
+ const prevY = Math.round(siblings[insertIndex - 1]?.position?.y || worldBounds.y);
1483
+ const nextY = Math.round(siblings[insertIndex]?.position?.y || prevY + childHeight + 1);
1484
+ return Math.round((prevY + nextY) / 2);
1485
+ };
1486
+ const nextPosition = direction === 'left'
1487
+ ? { x: Math.round(worldBounds.x - childWidth - gapWorld), y: resolveSideInsertY() }
1488
+ : direction === 'right'
1489
+ ? { x: Math.round(worldBounds.x + worldBounds.width + gapWorld), y: resolveSideInsertY() }
1490
+ : { x: Math.round(worldBounds.x), y: resolveBottomInsertY() };
1491
+ if (isBottomSiblingClone) {
1492
+ logMindmapCompoundDebug('handles:bottom-branch-parent-resolve', {
1493
+ sourceId: id,
1494
+ sourceParentId: sourceMeta?.parentId || null,
1495
+ sourceBranchRootId: sourceMeta?.branchRootId || null,
1496
+ resolvedParentId: metaParentId || null,
1497
+ resolvedSide: metaSide || null,
1498
+ });
1499
+ }
1500
+ logMindmapCompoundDebug('handles:child-create-intent', {
1501
+ sourceId: id,
1502
+ direction,
1503
+ sourceRole: sourceMeta?.role || null,
1504
+ sourceParentId: sourceMeta?.parentId || null,
1505
+ sourceSide: sourceMeta?.side || null,
1506
+ isBottomSiblingClone,
1507
+ metaParentId: metaParentId || null,
1508
+ metaSide: metaSide || null,
1509
+ sourceBounds: {
1510
+ x: Math.round(worldBounds.x || 0),
1511
+ y: Math.round(worldBounds.y || 0),
1512
+ width: Math.round(worldBounds.width || 0),
1513
+ height: Math.round(worldBounds.height || 0),
1514
+ },
1515
+ nextPosition,
1516
+ });
1517
+ const childMindmapMeta = createChildMindmapIntentMetadata({
1518
+ sourceObjectId: metaParentId,
1519
+ sourceProperties: sourceMindmapProperties || {},
1520
+ side: metaSide,
1521
+ });
1522
+ const allObjects = this.host.core?.state?.state?.objects || [];
1523
+ const parentNode = (Array.isArray(allObjects) ? allObjects : [])
1524
+ .find((obj) => obj?.id === metaParentId && obj?.type === 'mindmap');
1525
+ const parentMeta = parentNode?.properties?.mindmap || {};
1526
+ const isDirectChildOfMainRoot = parentMeta?.role === 'root'
1527
+ && !parentMeta?.parentId;
1528
+ const siblingBranchColors = isDirectChildOfMainRoot
1529
+ ? (Array.isArray(allObjects) ? allObjects : [])
1530
+ .filter((obj) => obj?.type === 'mindmap')
1531
+ .filter((obj) => {
1532
+ const meta = obj?.properties?.mindmap || {};
1533
+ return meta?.role === 'child' && meta?.parentId === metaParentId;
1534
+ })
1535
+ .map((obj) => {
1536
+ const meta = obj?.properties?.mindmap || {};
1537
+ return asBranchColor(meta?.branchColor)
1538
+ ?? asBranchColor(obj?.properties?.strokeColor);
1539
+ })
1540
+ .filter((value) => Number.isFinite(value))
1541
+ : [];
1542
+ const branchColor = isDirectChildOfMainRoot
1543
+ ? pickRandomMindmapBranchColorExcluding({
1544
+ excludedColors: siblingBranchColors,
1545
+ })
1546
+ : (
1547
+ asBranchColor(childMindmapMeta?.branchColor)
1548
+ ?? asBranchColor(sourceMeta?.branchColor)
1549
+ ?? asBranchColor(sourceMindmapProperties?.strokeColor)
1550
+ ?? MINDMAP_CHILD_STROKE_COLOR
1551
+ );
1552
+ childMindmapMeta.branchColor = branchColor;
1553
+ if (!isBottomSiblingClone && Number.isFinite(sideCenterInsertData.insertIndex)) {
1554
+ childMindmapMeta.branchOrder = Number(sideCenterInsertData.insertIndex);
1555
+ }
1556
+ if (isBottomSiblingClone && metaParentId) {
1557
+ childMindmapMeta.branchRootId = metaParentId;
1558
+ if (bottomInsertData.sourceIndex >= 0) {
1559
+ childMindmapMeta.branchOrder = bottomInsertData.sourceIndex + 1;
1560
+ }
1561
+ }
1562
+ this.host.eventBus.emit(Events.UI.ToolbarAction, {
1563
+ type: 'mindmap',
1564
+ id: 'mindmap',
1565
+ position: nextPosition,
1566
+ properties: {
1567
+ ...(sourceMindmapProperties || {}),
1568
+ mindmap: childMindmapMeta,
1569
+ content: '',
1570
+ width: childWidth,
1571
+ height: childHeight,
1572
+ capsuleBaseWidth: childWidth,
1573
+ capsuleBaseHeight: childHeight,
1574
+ paddingX: childPaddingX,
1575
+ paddingY: childPaddingY,
1576
+ strokeColor: branchColor,
1577
+ fillColor: branchColor,
1578
+ fillAlpha: MINDMAP_CHILD_FILL_ALPHA,
1579
+ strokeWidth: 1,
1580
+ },
1581
+ });
1582
+ setTimeout(() => {
1583
+ relayoutMindmapBranchCascade({
1584
+ core: this.host.core,
1585
+ eventBus: this.host.eventBus,
1586
+ startParentId: metaParentId,
1587
+ startSide: metaSide,
1588
+ });
1589
+ }, 0);
1590
+ setTimeout(() => {
1591
+ relayoutMindmapBranchCascade({
1592
+ core: this.host.core,
1593
+ eventBus: this.host.eventBus,
1594
+ startParentId: metaParentId,
1595
+ startSide: metaSide,
1596
+ });
1597
+ }, 24);
1598
+ };
1599
+ const createMindmapSideButton = (side) => {
1600
+ const btn = document.createElement('button');
1601
+ btn.type = 'button';
1602
+ btn.className = 'mb-mindmap-side-btn';
1603
+ btn.dataset.side = side;
1604
+ btn.dataset.id = id;
1605
+ btn.textContent = '';
1606
+ btn.setAttribute('aria-label', 'Добавить узел mindmap');
1607
+ const centerY = top + Math.round(height / 2);
1608
+ const edgeGap = 10;
1609
+ const buttonRadius = 12;
1610
+ const centerOffset = edgeGap + buttonRadius;
1611
+ if (side === 'left') {
1612
+ btn.style.left = `${Math.round(left - centerOffset)}px`;
1613
+ } else {
1614
+ btn.style.left = `${Math.round(left + width + centerOffset)}px`;
1615
+ }
1616
+ btn.style.top = `${centerY}px`;
1617
+ btn.addEventListener('mousedown', (evt) => {
1618
+ evt.preventDefault();
1619
+ evt.stopPropagation();
1620
+ });
1621
+ btn.addEventListener('click', (evt) => {
1622
+ evt.preventDefault();
1623
+ evt.stopPropagation();
1624
+ emitChildMindmapFromSource(side);
1625
+ });
1626
+ return btn;
1627
+ };
1628
+ const createMindmapBottomButton = () => {
1629
+ const btn = document.createElement('button');
1630
+ btn.type = 'button';
1631
+ btn.className = 'mb-mindmap-side-btn mb-mindmap-side-btn--down';
1632
+ btn.dataset.side = 'bottom';
1633
+ btn.dataset.id = id;
1634
+ btn.textContent = '';
1635
+ btn.setAttribute('aria-label', 'Добавить дочерний узел вниз');
1636
+ const centerX = left + Math.round(width / 2);
1637
+ const edgeGap = 10;
1638
+ const buttonRadius = 12;
1639
+ const centerOffset = edgeGap + buttonRadius;
1640
+ btn.style.left = `${centerX}px`;
1641
+ btn.style.top = `${Math.round(top + height + centerOffset)}px`;
1642
+ btn.addEventListener('mousedown', (evt) => {
1643
+ evt.preventDefault();
1644
+ evt.stopPropagation();
1645
+ });
1646
+ btn.addEventListener('click', (evt) => {
1647
+ evt.preventDefault();
1648
+ evt.stopPropagation();
1649
+ emitChildMindmapFromSource('bottom');
1650
+ });
1651
+ return btn;
1652
+ };
1653
+ const canShowLeft = hiddenIncomingSide.value !== 'left';
1654
+ const canShowRight = hiddenIncomingSide.value !== 'right';
1655
+ if (canShowLeft) this.host.layer.appendChild(createMindmapSideButton('left'));
1656
+ if (canShowRight) this.host.layer.appendChild(createMindmapSideButton('right'));
1657
+ const role = sourceMindmapProperties?.mindmap?.role || null;
1658
+ const canShowBottom = !occupiedOutgoingSides.has('bottom');
1659
+ if (role === 'child' && canShowBottom) {
1660
+ this.host.layer.appendChild(createMindmapBottomButton());
1661
+ }
1662
+ }
1663
+
193
1664
  if (isRevitScreenshotTarget && typeof revitViewPayload === 'string' && revitViewPayload.length > 0) {
194
1665
  const showInModelButton = document.createElement('button');
195
1666
  showInModelButton.type = 'button';