@sequent-org/moodboard 1.3.4 → 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 +1486 -15
  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
@@ -5,6 +5,7 @@ export class HandlesEventBridge {
5
5
  this.host = host;
6
6
  this.subscriptions = [];
7
7
  this.isAttached = false;
8
+ this._mindmapDragSnapshot = null;
8
9
  }
9
10
 
10
11
  attach() {
@@ -18,6 +19,7 @@ export class HandlesEventBridge {
18
19
  this.host._endGroupRotationPreview();
19
20
  this.host.hide();
20
21
  }],
22
+ [Events.Object.Created, () => this.host.update()],
21
23
  [Events.Tool.DragUpdate, () => this.host.update()],
22
24
  [Events.Object.Deleted, (data) => {
23
25
  const objectId = data?.objectId || data;
@@ -28,9 +30,27 @@ export class HandlesEventBridge {
28
30
  this.host.update();
29
31
  }, 10);
30
32
  }],
31
- [Events.Tool.DragStart, () => { this.host._handlesSuppressed = true; this.host._setHandlesVisibility(false); }],
32
- [Events.Tool.DragEnd, () => { this.host._handlesSuppressed = false; this.host._setHandlesVisibility(true); }],
33
- [Events.Tool.ResizeUpdate, () => this.host.update()],
33
+ [Events.Tool.DragStart, () => {
34
+ this._mindmapDragSnapshot = this.host.domRenderer?.captureMindmapSnapshot?.() || null;
35
+ this.host._handlesSuppressed = true;
36
+ this.host._setHandlesVisibility(false);
37
+ }],
38
+ [Events.Tool.DragEnd, (data) => {
39
+ this.host.domRenderer?.enforceMindmapAutoLayoutAfterDragEnd?.({
40
+ draggedIds: [data?.object].filter(Boolean),
41
+ snapshot: this._mindmapDragSnapshot,
42
+ });
43
+ this._mindmapDragSnapshot = null;
44
+ this.host._handlesSuppressed = false;
45
+ this.host._setHandlesVisibility(true);
46
+ this.host.update();
47
+ }],
48
+ [Events.Tool.ResizeUpdate, (data) => {
49
+ this.host.domRenderer?.relayoutMindmapAfterResize?.({
50
+ objectId: data?.object || null,
51
+ });
52
+ this.host.update();
53
+ }],
34
54
  [Events.Tool.ResizeStart, () => { this.host._handlesSuppressed = true; this.host._setHandlesVisibility(false); }],
35
55
  [Events.Tool.ResizeEnd, () => { this.host._handlesSuppressed = false; this.host._setHandlesVisibility(true); }],
36
56
  [Events.Tool.RotateUpdate, () => this.host.update()],
@@ -40,9 +60,29 @@ export class HandlesEventBridge {
40
60
  this.host._syncGroupRotationPreviewTranslation();
41
61
  this.host.update();
42
62
  }],
43
- [Events.Tool.GroupDragStart, () => { this.host._handlesSuppressed = true; this.host._setHandlesVisibility(false); }],
44
- [Events.Tool.GroupDragEnd, () => { this.host._handlesSuppressed = false; this.host._setHandlesVisibility(true); }],
63
+ [Events.Tool.GroupDragStart, () => {
64
+ this._mindmapDragSnapshot = this.host.domRenderer?.captureMindmapSnapshot?.() || null;
65
+ this.host._handlesSuppressed = true;
66
+ this.host._setHandlesVisibility(false);
67
+ }],
68
+ [Events.Tool.GroupDragEnd, (data) => {
69
+ const draggedIds = Array.isArray(data?.objects) ? data.objects : [];
70
+ this.host.domRenderer?.enforceMindmapAutoLayoutAfterDragEnd?.({
71
+ draggedIds,
72
+ snapshot: this._mindmapDragSnapshot,
73
+ });
74
+ this._mindmapDragSnapshot = null;
75
+ this.host._handlesSuppressed = false;
76
+ this.host._setHandlesVisibility(true);
77
+ this.host.update();
78
+ }],
45
79
  [Events.Tool.GroupResizeUpdate, (data) => {
80
+ const resizedIds = Array.isArray(data?.objects) ? data.objects : [];
81
+ resizedIds.forEach((id) => {
82
+ this.host.domRenderer?.relayoutMindmapAfterResize?.({
83
+ objectId: id,
84
+ });
85
+ });
46
86
  this.host._updateGroupResizePreview(data);
47
87
  this.host.update();
48
88
  }],
@@ -76,6 +116,10 @@ export class HandlesEventBridge {
76
116
  [Events.History.Changed, (data) => {
77
117
  if (data?.lastUndone || data?.lastRedone) {
78
118
  this.host._endGroupRotationPreview();
119
+ this.host.domRenderer?.relayoutAllMindmapCompounds?.();
120
+ setTimeout(() => {
121
+ this.host.domRenderer?.relayoutAllMindmapCompounds?.();
122
+ }, 0);
79
123
  this.host.update();
80
124
  }
81
125
  }],
@@ -318,10 +318,10 @@ export class HandlesInteractionController {
318
318
  } catch (_) {}
319
319
  }
320
320
 
321
- box.style.left = `${Math.round(newLeft)}px`;
322
- box.style.top = `${Math.round(newTop)}px`;
323
- box.style.width = `${Math.round(newW)}px`;
324
- box.style.height = `${Math.round(newH)}px`;
321
+ box.style.left = `${newLeft}px`;
322
+ box.style.top = `${newTop}px`;
323
+ box.style.width = `${newW}px`;
324
+ box.style.height = `${newH}px`;
325
325
  this.host._repositionBoxChildren(box);
326
326
 
327
327
  const screenX = (newLeft - offsetLeft);
@@ -0,0 +1,239 @@
1
+ import * as PIXI from 'pixi.js';
2
+ import { Events } from '../../core/events/Events.js';
3
+
4
+ const MINDMAP_TYPE = 'mindmap';
5
+ const SIDE_LEFT = 'left';
6
+ const SIDE_RIGHT = 'right';
7
+ const SIDE_BOTTOM = 'bottom';
8
+
9
+ function asArray(value) {
10
+ return Array.isArray(value) ? value : [];
11
+ }
12
+
13
+ function asMindmapMeta(obj) {
14
+ return obj?.properties?.mindmap || {};
15
+ }
16
+
17
+ function isMindmap(obj) {
18
+ return obj?.type === MINDMAP_TYPE;
19
+ }
20
+
21
+ function asNumber(value, fallback = 0) {
22
+ return Number.isFinite(value) ? value : fallback;
23
+ }
24
+
25
+ function getNodeRect(node) {
26
+ const x = asNumber(node?.position?.x, 0);
27
+ const y = asNumber(node?.position?.y, 0);
28
+ const width = Math.max(1, Math.round(asNumber(node?.width, asNumber(node?.properties?.width, 1))));
29
+ const height = Math.max(1, Math.round(asNumber(node?.height, asNumber(node?.properties?.height, 1))));
30
+ return { x, y, width, height };
31
+ }
32
+
33
+ function getAnchorPoint(node, side) {
34
+ const rect = getNodeRect(node);
35
+ if (side === SIDE_LEFT) {
36
+ return { x: rect.x, y: rect.y + rect.height / 2 };
37
+ }
38
+ if (side === SIDE_RIGHT) {
39
+ return { x: rect.x + rect.width, y: rect.y + rect.height / 2 };
40
+ }
41
+ if (side === SIDE_BOTTOM) {
42
+ return { x: rect.x + rect.width / 2, y: rect.y + rect.height };
43
+ }
44
+ return { x: rect.x + rect.width / 2, y: rect.y };
45
+ }
46
+
47
+ function nudgeStartOutsideNode(point, side) {
48
+ const px = 1;
49
+ if (side === SIDE_LEFT) return { x: point.x - px, y: point.y };
50
+ if (side === SIDE_RIGHT) return { x: point.x + px, y: point.y };
51
+ if (side === SIDE_BOTTOM) return { x: point.x, y: point.y + px };
52
+ return point;
53
+ }
54
+
55
+ function getChildAttachSide(parentSide) {
56
+ if (parentSide === SIDE_LEFT) return SIDE_RIGHT;
57
+ if (parentSide === SIDE_RIGHT) return SIDE_LEFT;
58
+ if (parentSide === SIDE_BOTTOM) return 'top';
59
+ return 'top';
60
+ }
61
+
62
+ function getBezierControls(start, end, side) {
63
+ if (side === SIDE_BOTTOM) {
64
+ const spanY = Math.max(30, Math.round(Math.abs(end.y - start.y) * 0.35));
65
+ return {
66
+ cp1: { x: start.x, y: start.y + spanY },
67
+ cp2: { x: end.x, y: end.y - spanY },
68
+ };
69
+ }
70
+ const dir = side === SIDE_LEFT ? -1 : 1;
71
+ const spanX = Math.max(30, Math.round(Math.abs(end.x - start.x) * 0.35));
72
+ return {
73
+ cp1: { x: start.x + spanX * dir, y: start.y },
74
+ cp2: { x: end.x - spanX * dir, y: end.y },
75
+ };
76
+ }
77
+
78
+ function resolveLegacyLink(child, byId, rootByCompoundId) {
79
+ const childMeta = asMindmapMeta(child);
80
+ const compoundId = childMeta?.compoundId || null;
81
+ let parentId = childMeta?.parentId || null;
82
+ let side = childMeta?.side || null;
83
+ const childId = child?.id || null;
84
+
85
+ if (!parentId || parentId === childId) {
86
+ parentId = childMeta?.branchRootId || null;
87
+ }
88
+
89
+ let parent = parentId ? byId.get(parentId) : null;
90
+ if (!parent && compoundId) {
91
+ parentId = rootByCompoundId.get(compoundId) || null;
92
+ parent = parentId ? byId.get(parentId) : null;
93
+ }
94
+
95
+ if (![SIDE_LEFT, SIDE_RIGHT, SIDE_BOTTOM].includes(side)) {
96
+ side = SIDE_RIGHT;
97
+ }
98
+ if (parentId === childId && compoundId) {
99
+ const rootId = rootByCompoundId.get(compoundId) || null;
100
+ if (rootId && rootId !== childId) parentId = rootId;
101
+ }
102
+ return { parentId, side };
103
+ }
104
+
105
+ export class MindmapConnectionLayer {
106
+ constructor(eventBus, core) {
107
+ this.eventBus = eventBus;
108
+ this.core = core;
109
+ this.graphics = null;
110
+ this.subscriptions = [];
111
+ this._lastSegments = [];
112
+ }
113
+
114
+ attach() {
115
+ if (!this.core?.pixi) return;
116
+ if (this.graphics) return;
117
+ this.graphics = new PIXI.Graphics();
118
+ this.graphics.name = 'mindmap-connection-layer';
119
+ this.graphics.zIndex = 2;
120
+ const world = this.core.pixi.worldLayer || this.core.pixi.app?.stage;
121
+ world?.addChild?.(this.graphics);
122
+ this._attachEvents();
123
+ this.updateAll();
124
+ }
125
+
126
+ destroy() {
127
+ this._detachEvents();
128
+ if (this.graphics) {
129
+ this.graphics.clear();
130
+ this.graphics.removeFromParent();
131
+ this.graphics.destroy();
132
+ this.graphics = null;
133
+ }
134
+ this._lastSegments = [];
135
+ }
136
+
137
+ _attachEvents() {
138
+ const bindings = [
139
+ [Events.Object.Created, () => this.updateAll()],
140
+ [Events.Object.Deleted, () => this.updateAll()],
141
+ [Events.Object.Updated, () => this.updateAll()],
142
+ [Events.Object.StateChanged, () => this.updateAll()],
143
+ [Events.Tool.DragUpdate, () => this.updateAll()],
144
+ [Events.Tool.DragEnd, () => this.updateAll()],
145
+ [Events.Tool.ResizeUpdate, () => this.updateAll()],
146
+ [Events.Tool.ResizeEnd, () => this.updateAll()],
147
+ [Events.Tool.GroupDragUpdate, () => this.updateAll()],
148
+ [Events.Tool.GroupResizeUpdate, () => this.updateAll()],
149
+ [Events.Tool.RotateUpdate, () => this.updateAll()],
150
+ [Events.Tool.PanUpdate, () => this.updateAll()],
151
+ [Events.UI.ZoomPercent, () => this.updateAll()],
152
+ [Events.History.Changed, () => this.updateAll()],
153
+ [Events.Board.Loaded, () => this.updateAll()],
154
+ ];
155
+ bindings.forEach(([event, handler]) => {
156
+ this.eventBus.on(event, handler);
157
+ this.subscriptions.push([event, handler]);
158
+ });
159
+ }
160
+
161
+ _detachEvents() {
162
+ if (typeof this.eventBus?.off !== 'function') {
163
+ this.subscriptions = [];
164
+ return;
165
+ }
166
+ this.subscriptions.forEach(([event, handler]) => this.eventBus.off(event, handler));
167
+ this.subscriptions = [];
168
+ }
169
+
170
+ updateAll() {
171
+ if (!this.graphics) return;
172
+ const objects = asArray(this.core?.state?.state?.objects);
173
+ const mindmaps = objects.filter(isMindmap);
174
+ const byId = new Map(mindmaps.map((obj) => [obj.id, obj]));
175
+ const rootByCompoundId = new Map();
176
+ mindmaps.forEach((obj) => {
177
+ const meta = asMindmapMeta(obj);
178
+ if (meta?.role === 'root' && typeof meta?.compoundId === 'string' && meta.compoundId.length > 0) {
179
+ rootByCompoundId.set(meta.compoundId, obj.id);
180
+ }
181
+ });
182
+ const children = mindmaps.filter((obj) => {
183
+ const meta = asMindmapMeta(obj);
184
+ return meta.role === 'child' && typeof meta.parentId === 'string' && meta.parentId.length > 0;
185
+ });
186
+
187
+ const g = this.graphics;
188
+ g.clear();
189
+ this._lastSegments = [];
190
+
191
+ children.forEach((child) => {
192
+ const childMeta = asMindmapMeta(child);
193
+ const { parentId, side } = resolveLegacyLink(child, byId, rootByCompoundId);
194
+ const parent = parentId ? byId.get(parentId) : null;
195
+ if (!parent || ![SIDE_LEFT, SIDE_RIGHT, SIDE_BOTTOM].includes(side)) return;
196
+
197
+ const parentMeta = asMindmapMeta(parent);
198
+ if (parentMeta.compoundId && childMeta.compoundId && parentMeta.compoundId !== childMeta.compoundId) {
199
+ return;
200
+ }
201
+
202
+ const startBase = getAnchorPoint(parent, side);
203
+ const start = nudgeStartOutsideNode(startBase, side);
204
+ const end = getAnchorPoint(child, getChildAttachSide(side));
205
+ const { cp1, cp2 } = getBezierControls(start, end, side);
206
+ const color = Number(child?.properties?.strokeColor || parent?.properties?.strokeColor || 0x2563EB);
207
+
208
+ try {
209
+ g.lineStyle({
210
+ width: 1,
211
+ color,
212
+ alpha: 0.95,
213
+ alignment: 0,
214
+ cap: 'round',
215
+ join: 'round',
216
+ miterLimit: 2,
217
+ });
218
+ } catch (_) {
219
+ g.lineStyle(1, color, 0.95, 0);
220
+ }
221
+ g.moveTo(Math.round(start.x), Math.round(start.y));
222
+ g.bezierCurveTo(
223
+ Math.round(cp1.x),
224
+ Math.round(cp1.y),
225
+ Math.round(cp2.x),
226
+ Math.round(cp2.y),
227
+ Math.round(end.x),
228
+ Math.round(end.y)
229
+ );
230
+ this._lastSegments.push({
231
+ parentId: parent.id,
232
+ childId: child.id,
233
+ side,
234
+ start: { x: Math.round(start.x), y: Math.round(start.y) },
235
+ end: { x: Math.round(end.x), y: Math.round(end.y) },
236
+ });
237
+ });
238
+ }
239
+ }
@@ -0,0 +1,285 @@
1
+ import { Events } from '../../core/events/Events.js';
2
+ import * as PIXI from 'pixi.js';
3
+ import { MindmapTextOverlayAdapter } from './MindmapTextOverlayAdapter.js';
4
+ import { MINDMAP_LAYOUT } from './MindmapLayoutConfig.js';
5
+
6
+ const MINDMAP_PLACEHOLDER = 'Напишите что-нибудь';
7
+ const MINDMAP_MAX_LINE_CHARS = MINDMAP_LAYOUT.maxLineChars;
8
+
9
+ function normalizeMindmapLineLength(value, maxLineChars = MINDMAP_MAX_LINE_CHARS) {
10
+ const text = (typeof value === 'string')
11
+ ? value.replace(/\r/g, '').replace(/\n/g, '')
12
+ : '';
13
+ const chunks = [];
14
+ if (text.length === 0) return '';
15
+ for (let i = 0; i < text.length; i += maxLineChars) {
16
+ chunks.push(text.slice(i, i + maxLineChars));
17
+ }
18
+ return chunks.join('\n');
19
+ }
20
+
21
+ /**
22
+ * Отдельный HTML-слой только для текста mindmap-объектов.
23
+ * Изолирован от общего HtmlTextLayer (text/simple-text).
24
+ */
25
+ export class MindmapHtmlTextLayer {
26
+ constructor(container, eventBus, core) {
27
+ this.container = container;
28
+ this.eventBus = eventBus;
29
+ this.core = core;
30
+ this.layer = null;
31
+ this.idToEl = new Map();
32
+ this.idToCleanup = new Map();
33
+ this.idToContentEl = new Map();
34
+ this.overlayAdapter = new MindmapTextOverlayAdapter();
35
+ }
36
+
37
+ attach() {
38
+ this.layer = document.createElement('div');
39
+ this.layer.className = 'moodboard-html-layer moodboard-html-layer--mindmap';
40
+ Object.assign(this.layer.style, {
41
+ position: 'absolute',
42
+ inset: '0',
43
+ overflow: 'hidden',
44
+ pointerEvents: 'none',
45
+ zIndex: 10,
46
+ });
47
+ this.container.appendChild(this.layer);
48
+
49
+ this.eventBus.on(Events.Object.Created, ({ objectId, objectData }) => {
50
+ if (!this.overlayAdapter.supportsObject(objectData)) return;
51
+ this._ensureTextEl(objectId, objectData);
52
+ this.updateOne(objectId);
53
+ });
54
+
55
+ this.eventBus.on(Events.Object.Deleted, ({ objectId }) => {
56
+ this._removeTextEl(objectId);
57
+ });
58
+
59
+ this.eventBus.on(Events.Object.TransformUpdated, ({ objectId }) => {
60
+ this.updateOne(objectId);
61
+ });
62
+
63
+ this.eventBus.on(Events.Tool.HideObjectText, ({ objectId }) => {
64
+ const el = this.idToEl.get(objectId);
65
+ if (el) el.style.visibility = 'hidden';
66
+ });
67
+ this.eventBus.on(Events.Tool.ShowObjectText, ({ objectId }) => {
68
+ const el = this.idToEl.get(objectId);
69
+ if (el) el.style.visibility = '';
70
+ });
71
+
72
+ this.eventBus.on(Events.Tool.UpdateObjectContent, ({ objectId, content }) => {
73
+ const contentEl = this.idToContentEl.get(objectId);
74
+ const containerEl = this.idToEl.get(objectId);
75
+ if (contentEl && containerEl && typeof content === 'string') {
76
+ this._applyContentValue(containerEl, contentEl, content);
77
+ }
78
+ });
79
+
80
+ this.eventBus.on(Events.Object.StateChanged, ({ objectId, updates }) => {
81
+ const el = this.idToEl.get(objectId);
82
+ if (!el || !updates) return;
83
+ const nextFont = updates.fontFamily || updates.properties?.fontFamily;
84
+ if (nextFont) {
85
+ el.style.fontFamily = nextFont;
86
+ }
87
+ if (updates.fontSize) {
88
+ el.dataset.baseFontSize = String(updates.fontSize);
89
+ }
90
+ if (updates.color || updates.properties?.textColor) {
91
+ el.style.color = updates.color || updates.properties?.textColor;
92
+ }
93
+ this.updateOne(objectId);
94
+ });
95
+
96
+ this.eventBus.on(Events.UI.ZoomPercent, () => this.updateAll());
97
+ this.eventBus.on(Events.Tool.PanUpdate, () => this.updateAll());
98
+ this.eventBus.on(Events.Tool.DragUpdate, ({ object }) => this.updateOne(object));
99
+ this.eventBus.on(Events.Tool.ResizeUpdate, ({ object }) => this.updateOne(object));
100
+ this.eventBus.on(Events.Tool.RotateUpdate, ({ object }) => this.updateOne(object));
101
+ this.eventBus.on(Events.Tool.GroupDragUpdate, ({ objects }) => {
102
+ (Array.isArray(objects) ? objects : []).forEach((id) => this.updateOne(id));
103
+ });
104
+ this.eventBus.on(Events.Tool.GroupResizeUpdate, ({ objects }) => {
105
+ (Array.isArray(objects) ? objects : []).forEach((id) => this.updateOne(id));
106
+ });
107
+ this.eventBus.on(Events.Tool.GroupRotateUpdate, ({ objects }) => {
108
+ (Array.isArray(objects) ? objects : []).forEach((id) => this.updateOne(id));
109
+ });
110
+
111
+ this.rebuildFromState();
112
+ this.updateAll();
113
+ }
114
+
115
+ destroy() {
116
+ if (this.layer) this.layer.remove();
117
+ this.layer = null;
118
+ this.idToEl.clear();
119
+ this.idToCleanup.clear();
120
+ this.idToContentEl.clear();
121
+ }
122
+
123
+ rebuildFromState() {
124
+ if (!this.core?.state) return;
125
+ const objects = this.core.state.state.objects || [];
126
+ objects.forEach((objectData) => {
127
+ if (!this.overlayAdapter.supportsObject(objectData)) return;
128
+ this._ensureTextEl(objectData.id, objectData);
129
+ });
130
+ this.updateAll();
131
+ }
132
+
133
+ _ensureTextEl(objectId, objectData) {
134
+ if (!this.layer || !objectId) return;
135
+ if (this.idToEl.has(objectId)) return;
136
+
137
+ const el = document.createElement('div');
138
+ el.className = 'mb-text';
139
+ el.dataset.id = objectId;
140
+ const contentEl = document.createElement('span');
141
+ contentEl.className = 'mb-text--mindmap-content';
142
+
143
+ const fontFamily = this.overlayAdapter.getDefaultFontFamily(objectData);
144
+ const color = objectData.color || objectData.properties?.color || objectData.properties?.textColor || '#212121';
145
+ const baseFontSize = objectData.fontSize || objectData.properties?.fontSize || MINDMAP_LAYOUT.fontSize;
146
+ const baseLineHeight = Math.round(baseFontSize * 1.24);
147
+ const paddingX = Math.max(0, Math.round(objectData.properties?.paddingX ?? MINDMAP_LAYOUT.paddingX));
148
+ const paddingY = Math.max(0, Math.round(objectData.properties?.paddingY ?? MINDMAP_LAYOUT.paddingY));
149
+ const maxLineChars = Math.max(1, Math.round(objectData.properties?.maxLineChars || MINDMAP_LAYOUT.maxLineChars));
150
+
151
+ el.style.color = color;
152
+ el.style.fontFamily = fontFamily;
153
+ el.style.lineHeight = `${baseLineHeight}px`;
154
+ el.style.paddingTop = `${paddingY}px`;
155
+ el.style.paddingBottom = `${paddingY}px`;
156
+ el.style.paddingLeft = `${paddingX}px`;
157
+ el.style.paddingRight = `${paddingX}px`;
158
+ el.style.whiteSpace = 'pre';
159
+ el.style.wordBreak = 'normal';
160
+ el.style.overflowWrap = 'normal';
161
+ el.style.overflow = 'hidden';
162
+ el.style.letterSpacing = '0px';
163
+ el.style.fontKerning = 'normal';
164
+ el.style.textRendering = 'optimizeLegibility';
165
+
166
+ this.overlayAdapter.applyElementStyles(el);
167
+
168
+ const initialContent = objectData.content || objectData.properties?.content || '';
169
+ this._applyContentValue(el, contentEl, initialContent);
170
+ el.dataset.baseFontSize = String(baseFontSize);
171
+ el.dataset.baseW = String(Math.max(1, objectData.width || objectData.properties?.width || MINDMAP_LAYOUT.width));
172
+ el.dataset.baseH = String(Math.max(1, objectData.height || objectData.properties?.height || MINDMAP_LAYOUT.height));
173
+ el.dataset.basePaddingX = String(paddingX);
174
+ el.dataset.basePaddingY = String(paddingY);
175
+ el.dataset.maxLineChars = String(maxLineChars);
176
+
177
+ const cleanup = this.overlayAdapter.attachEditOnClick({
178
+ el,
179
+ targetEl: contentEl,
180
+ objectId,
181
+ objectData,
182
+ eventBus: this.eventBus,
183
+ });
184
+ this.idToCleanup.set(objectId, cleanup);
185
+
186
+ el.appendChild(contentEl);
187
+ this.layer.appendChild(el);
188
+ this.idToEl.set(objectId, el);
189
+ this.idToContentEl.set(objectId, contentEl);
190
+ }
191
+
192
+ _removeTextEl(objectId) {
193
+ const el = this.idToEl.get(objectId);
194
+ const cleanup = this.idToCleanup.get(objectId);
195
+ if (typeof cleanup === 'function') cleanup();
196
+ if (el) el.remove();
197
+ this.idToEl.delete(objectId);
198
+ this.idToCleanup.delete(objectId);
199
+ this.idToContentEl.delete(objectId);
200
+ }
201
+
202
+ updateAll() {
203
+ if (!this.core?.pixi) return;
204
+ for (const id of this.idToEl.keys()) this.updateOne(id);
205
+ }
206
+
207
+ updateOne(objectId) {
208
+ const el = this.idToEl.get(objectId);
209
+ if (!el || !this.core) return;
210
+
211
+ const world = this.core.pixi.worldLayer || this.core.pixi.app.stage;
212
+ const res = (this.core?.pixi?.app?.renderer?.resolution) || 1;
213
+ const objectData = (this.core.state.state.objects || []).find((o) => o.id === objectId);
214
+ if (!objectData || objectData.type !== 'mindmap') return;
215
+
216
+ const x = objectData.position?.x || 0;
217
+ const y = objectData.position?.y || 0;
218
+ const w = objectData.width || 0;
219
+ const h = objectData.height || 0;
220
+
221
+ const pixiObject = this.core?.pixi?.objects?.get ? this.core.pixi.objects.get(objectId) : null;
222
+ const angle = (pixiObject && typeof pixiObject.rotation === 'number')
223
+ ? (pixiObject.rotation * 180 / Math.PI)
224
+ : (objectData.rotation || objectData.transform?.rotation || 0);
225
+
226
+ const baseFS = parseFloat(el.dataset.baseFontSize || `${MINDMAP_LAYOUT.fontSize}`) || MINDMAP_LAYOUT.fontSize;
227
+ const worldScale = world?.scale?.x || 1;
228
+ const sCss = worldScale / res;
229
+ const fontSizePx = Math.max(1, baseFS * sCss);
230
+ el.style.fontSize = `${fontSizePx}px`;
231
+ el.style.lineHeight = `${Math.round(fontSizePx * 1.24)}px`;
232
+ const basePaddingX = Math.max(
233
+ 0,
234
+ Math.round(objectData.properties?.paddingX ?? parseFloat(el.dataset.basePaddingX || `${MINDMAP_LAYOUT.paddingX}`))
235
+ );
236
+ const basePaddingY = Math.max(
237
+ 0,
238
+ Math.round(objectData.properties?.paddingY ?? parseFloat(el.dataset.basePaddingY || `${MINDMAP_LAYOUT.paddingY}`))
239
+ );
240
+ const paddingXCss = Math.max(0, Math.round(basePaddingX * sCss));
241
+ const paddingYCss = Math.max(0, Math.round(basePaddingY * sCss));
242
+ el.style.paddingTop = `${paddingYCss}px`;
243
+ el.style.paddingBottom = `${paddingYCss}px`;
244
+ el.style.paddingLeft = `${paddingXCss}px`;
245
+ el.style.paddingRight = `${paddingXCss}px`;
246
+
247
+ const worldLayer = this.core.pixi.worldLayer || this.core.pixi.app.stage;
248
+ const view = this.core.pixi.app.view;
249
+ if (worldLayer && view && view.parentElement) {
250
+ const containerRect = view.parentElement.getBoundingClientRect();
251
+ const viewRect = view.getBoundingClientRect();
252
+ const offsetLeft = viewRect.left - containerRect.left;
253
+ const offsetTop = viewRect.top - containerRect.top;
254
+ const tl = worldLayer.toGlobal(new PIXI.Point(x, y));
255
+ const br = worldLayer.toGlobal(new PIXI.Point(x + w, y + h));
256
+ el.style.left = `${offsetLeft + tl.x}px`;
257
+ el.style.top = `${offsetTop + tl.y}px`;
258
+ el.style.width = `${Math.max(1, br.x - tl.x)}px`;
259
+ el.style.height = `${Math.max(1, br.y - tl.y)}px`;
260
+ }
261
+
262
+ const content = objectData.content || objectData.properties?.content;
263
+ if (typeof content === 'string') {
264
+ const contentEl = this.idToContentEl.get(objectId);
265
+ if (contentEl) {
266
+ this._applyContentValue(el, contentEl, content);
267
+ }
268
+ }
269
+
270
+ el.style.transformOrigin = 'center center';
271
+ el.style.transform = angle ? `rotate(${angle}deg)` : '';
272
+ }
273
+
274
+ _applyContentValue(containerEl, contentEl, rawContent) {
275
+ const maxLineChars = Math.max(
276
+ 1,
277
+ parseInt(containerEl?.dataset?.maxLineChars || `${MINDMAP_LAYOUT.maxLineChars}`, 10) || MINDMAP_LAYOUT.maxLineChars
278
+ );
279
+ const actual = normalizeMindmapLineLength((typeof rawContent === 'string') ? rawContent : '', maxLineChars);
280
+ const isPlaceholder = actual.trim().length === 0;
281
+ containerEl.dataset.mbContent = actual;
282
+ contentEl.textContent = isPlaceholder ? MINDMAP_PLACEHOLDER : actual;
283
+ contentEl.classList.toggle('is-placeholder', isPlaceholder);
284
+ }
285
+ }
@@ -0,0 +1,29 @@
1
+ const MINDMAP_COMPACT_SCALE = 0.8;
2
+ const MINDMAP_ROOT_SIZE_FACTOR = 0.5;
3
+ const MINDMAP_ROOT_WIDTH_FACTOR = 0.73;
4
+ const MINDMAP_ROOT_FONT_FACTOR = 0.85;
5
+ const MINDMAP_ROOT_PADDING_FACTOR = 0.5;
6
+
7
+ const MINDMAP_BASE_LAYOUT = Object.freeze({
8
+ width: 306,
9
+ height: 100,
10
+ fontSize: 20,
11
+ paddingX: 50,
12
+ paddingY: 50,
13
+ maxLineChars: 50,
14
+ });
15
+
16
+ function scaleInt(value) {
17
+ return Math.max(1, Math.round(value * MINDMAP_COMPACT_SCALE));
18
+ }
19
+
20
+ export const MINDMAP_LAYOUT = Object.freeze({
21
+ scale: MINDMAP_COMPACT_SCALE,
22
+ width: Math.max(1, Math.round(scaleInt(MINDMAP_BASE_LAYOUT.width) * MINDMAP_ROOT_WIDTH_FACTOR)),
23
+ height: Math.max(1, Math.round(scaleInt(MINDMAP_BASE_LAYOUT.height) * MINDMAP_ROOT_SIZE_FACTOR)),
24
+ fontSize: Math.max(1, Math.round(scaleInt(MINDMAP_BASE_LAYOUT.fontSize) * MINDMAP_ROOT_FONT_FACTOR)),
25
+ paddingX: Math.max(1, Math.round(scaleInt(MINDMAP_BASE_LAYOUT.paddingX) * MINDMAP_ROOT_PADDING_FACTOR)),
26
+ paddingY: Math.max(1, Math.round(scaleInt(MINDMAP_BASE_LAYOUT.paddingY) * MINDMAP_ROOT_PADDING_FACTOR)),
27
+ maxLineChars: MINDMAP_BASE_LAYOUT.maxLineChars,
28
+ });
29
+