@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
@@ -0,0 +1,235 @@
1
+ const ROOT_ROLE = 'root';
2
+ const CHILD_ROLE = 'child';
3
+ const LEFT_SIDE = 'left';
4
+ const RIGHT_SIDE = 'right';
5
+ const BOTTOM_SIDE = 'bottom';
6
+ const DEBUG_STORAGE_KEY = 'mb:mindmap:compound:debug';
7
+ const BRANCH_COLOR_HEX = Object.freeze([
8
+ 'EF9A9A', 'CE93D8', '90CAF9',
9
+ '80DEEA', 'A5D6A7', 'E6EE9C',
10
+ 'FFE082', 'BCAAA4', 'B0BEC5',
11
+ ]);
12
+ export const MINDMAP_BRANCH_COLOR_PALETTE = Object.freeze(
13
+ BRANCH_COLOR_HEX.map((hex) => Number.parseInt(hex, 16))
14
+ );
15
+
16
+ function asObject(value) {
17
+ return value && typeof value === 'object' ? value : {};
18
+ }
19
+
20
+ function asNonEmptyString(value) {
21
+ return typeof value === 'string' && value.trim().length > 0 ? value.trim() : null;
22
+ }
23
+
24
+ function asValidRole(value) {
25
+ return value === ROOT_ROLE || value === CHILD_ROLE ? value : null;
26
+ }
27
+
28
+ function asValidSide(value) {
29
+ return value === LEFT_SIDE || value === RIGHT_SIDE || value === BOTTOM_SIDE ? value : null;
30
+ }
31
+
32
+ function asOrder(value) {
33
+ if (!Number.isFinite(value)) return null;
34
+ const normalized = Math.floor(value);
35
+ return normalized >= 0 ? normalized : null;
36
+ }
37
+
38
+ function asBranchOrder(value) {
39
+ if (!Number.isFinite(value)) return null;
40
+ return Number(value);
41
+ }
42
+
43
+ function asOptionalNodeId(value) {
44
+ return asNonEmptyString(value);
45
+ }
46
+
47
+ function asBranchColor(value) {
48
+ if (!Number.isFinite(value)) return null;
49
+ const normalized = Math.floor(Number(value));
50
+ if (normalized < 0 || normalized > 0xFFFFFF) return null;
51
+ return normalized;
52
+ }
53
+
54
+ export function pickRandomMindmapBranchColor(randomFn = Math.random) {
55
+ const rand = typeof randomFn === 'function' ? randomFn : Math.random;
56
+ const palette = MINDMAP_BRANCH_COLOR_PALETTE;
57
+ if (!Array.isArray(palette) || palette.length === 0) return null;
58
+ const raw = Number(rand());
59
+ const safe = Number.isFinite(raw) ? raw : 0;
60
+ const idx = Math.max(0, Math.min(palette.length - 1, Math.floor(safe * palette.length)));
61
+ return palette[idx];
62
+ }
63
+
64
+ export function pickRandomMindmapBranchColorExcluding({
65
+ excludedColors = [],
66
+ randomFn = Math.random,
67
+ } = {}) {
68
+ const excluded = new Set(
69
+ (Array.isArray(excludedColors) ? excludedColors : [])
70
+ .filter((value) => Number.isFinite(value))
71
+ .map((value) => Math.floor(Number(value)))
72
+ );
73
+ const palette = MINDMAP_BRANCH_COLOR_PALETTE.filter((color) => !excluded.has(color));
74
+ if (palette.length === 0) return pickRandomMindmapBranchColor(randomFn);
75
+ const rand = typeof randomFn === 'function' ? randomFn : Math.random;
76
+ const raw = Number(rand());
77
+ const safe = Number.isFinite(raw) ? raw : 0;
78
+ const idx = Math.max(0, Math.min(palette.length - 1, Math.floor(safe * palette.length)));
79
+ return palette[idx];
80
+ }
81
+
82
+ function getMindmapMetadataFromProperties(properties) {
83
+ const props = asObject(properties);
84
+ return asObject(props.mindmap);
85
+ }
86
+
87
+ function getCompoundIdFromObject(objectData) {
88
+ const meta = getMindmapMetadataFromProperties(objectData?.properties);
89
+ return asNonEmptyString(meta.compoundId);
90
+ }
91
+
92
+ function nextChildOrder(existingObjects, compoundId) {
93
+ if (!Array.isArray(existingObjects)) return 0;
94
+ let maxOrder = -1;
95
+ for (const obj of existingObjects) {
96
+ if (!obj || obj.type !== 'mindmap') continue;
97
+ const meta = getMindmapMetadataFromProperties(obj.properties);
98
+ if (asNonEmptyString(meta.compoundId) !== compoundId) continue;
99
+ if (asValidRole(meta.role) !== CHILD_ROLE) continue;
100
+ const order = asOrder(meta.order);
101
+ if (order !== null && order > maxOrder) {
102
+ maxOrder = order;
103
+ }
104
+ }
105
+ return maxOrder + 1;
106
+ }
107
+
108
+ export function createRootMindmapIntentMetadata() {
109
+ return {
110
+ compoundId: null,
111
+ role: ROOT_ROLE,
112
+ parentId: null,
113
+ side: null,
114
+ order: 0,
115
+ branchOrder: 0,
116
+ branchColor: null,
117
+ };
118
+ }
119
+
120
+ export function createChildMindmapIntentMetadata({ sourceObjectId, sourceProperties, side }) {
121
+ const sourceMeta = getMindmapMetadataFromProperties(sourceProperties);
122
+ const sourceId = asNonEmptyString(sourceObjectId);
123
+ const sourceCompoundId = asNonEmptyString(sourceMeta.compoundId);
124
+ const sourceRole = asValidRole(sourceMeta.role);
125
+ const inheritedColor = asBranchColor(sourceMeta.branchColor)
126
+ ?? asBranchColor(sourceProperties?.strokeColor);
127
+ const branchColor = sourceRole === ROOT_ROLE
128
+ ? pickRandomMindmapBranchColor()
129
+ : inheritedColor;
130
+ return {
131
+ compoundId: sourceCompoundId || sourceId,
132
+ role: CHILD_ROLE,
133
+ parentId: sourceId,
134
+ side: asValidSide(side),
135
+ order: null,
136
+ branchOrder: null,
137
+ branchColor,
138
+ };
139
+ }
140
+
141
+ export function normalizeMindmapPropertiesForCreate({
142
+ type,
143
+ objectId,
144
+ properties,
145
+ existingObjects = [],
146
+ }) {
147
+ const props = asObject(properties);
148
+ if (type !== 'mindmap') return props;
149
+
150
+ const meta = getMindmapMetadataFromProperties(props);
151
+ let role = asValidRole(meta.role);
152
+ const parentIdRaw = asNonEmptyString(meta.parentId);
153
+ const branchRootIdRaw = asOptionalNodeId(meta.branchRootId);
154
+ const sideRaw = asValidSide(meta.side);
155
+ let compoundId = asNonEmptyString(meta.compoundId);
156
+ let order = asOrder(meta.order);
157
+ const branchOrder = asBranchOrder(meta.branchOrder);
158
+ let branchColor = asBranchColor(meta.branchColor)
159
+ ?? asBranchColor(props.strokeColor);
160
+
161
+ if (!role) {
162
+ role = parentIdRaw ? CHILD_ROLE : ROOT_ROLE;
163
+ }
164
+
165
+ let parentId = parentIdRaw;
166
+ if (role === ROOT_ROLE) {
167
+ parentId = null;
168
+ } else if (!parentId) {
169
+ // Invalid child payload must degrade to root-safe mode.
170
+ role = ROOT_ROLE;
171
+ parentId = null;
172
+ }
173
+
174
+ if (!compoundId) {
175
+ if (role === CHILD_ROLE && parentId) {
176
+ const parent = Array.isArray(existingObjects)
177
+ ? existingObjects.find((obj) => obj && obj.id === parentId && obj.type === 'mindmap')
178
+ : null;
179
+ compoundId = getCompoundIdFromObject(parent) || parentId;
180
+ } else {
181
+ compoundId = asNonEmptyString(objectId);
182
+ }
183
+ }
184
+
185
+ if (!compoundId) {
186
+ compoundId = asNonEmptyString(objectId);
187
+ }
188
+
189
+ if (order === null) {
190
+ order = role === ROOT_ROLE ? 0 : nextChildOrder(existingObjects, compoundId);
191
+ }
192
+
193
+ if (role === CHILD_ROLE && branchColor === null && parentId) {
194
+ const parent = Array.isArray(existingObjects)
195
+ ? existingObjects.find((obj) => obj && obj.id === parentId && obj.type === 'mindmap')
196
+ : null;
197
+ const parentMeta = getMindmapMetadataFromProperties(parent?.properties);
198
+ branchColor = asBranchColor(parentMeta.branchColor)
199
+ ?? asBranchColor(parent?.properties?.strokeColor);
200
+ if (branchColor === null && asValidRole(parentMeta.role) === ROOT_ROLE) {
201
+ branchColor = pickRandomMindmapBranchColor();
202
+ }
203
+ }
204
+
205
+ return {
206
+ ...props,
207
+ mindmap: {
208
+ compoundId,
209
+ role,
210
+ parentId: role === ROOT_ROLE ? null : parentId,
211
+ side: role === CHILD_ROLE ? sideRaw : null,
212
+ order,
213
+ branchOrder: role === CHILD_ROLE ? branchOrder : 0,
214
+ branchRootId: role === CHILD_ROLE
215
+ ? (branchRootIdRaw || asNonEmptyString(objectId) || null)
216
+ : null,
217
+ branchColor: role === CHILD_ROLE ? branchColor : null,
218
+ },
219
+ };
220
+ }
221
+
222
+ export function isMindmapCompoundDebugEnabled() {
223
+ if (typeof window === 'undefined') return false;
224
+ if (window.__MB_MINDMAP_COMPOUND_DEBUG__ === true) return true;
225
+ try {
226
+ return window.localStorage?.getItem(DEBUG_STORAGE_KEY) === '1';
227
+ } catch (_) {
228
+ return false;
229
+ }
230
+ }
231
+
232
+ export function logMindmapCompoundDebug(eventName, payload) {
233
+ if (!isMindmapCompoundDebugEnabled()) return;
234
+ console.log(`[mindmap:compound] ${eventName}`, payload);
235
+ }
@@ -23,6 +23,7 @@ export class ActionHandler {
23
23
  case 'revit-screenshot-img':
24
24
  case 'comment':
25
25
  case 'file':
26
+ case 'mindmap':
26
27
  // Передаем imageId как extraData для изображений, fileId для файлов
27
28
  const extraData = action.imageId ? { imageId: action.imageId } :
28
29
  action.fileId ? { fileId: action.fileId } : {};
@@ -1,16 +1,51 @@
1
1
  /**
2
2
  * Управляет загрузкой и сохранением данных MoodBoard
3
3
  */
4
+ import { logMindmapCompoundDebug } from '../mindmap/MindmapCompoundContract.js';
4
5
  export class DataManager {
5
6
  constructor(coreMoodboard) {
6
7
  this.coreMoodboard = coreMoodboard;
7
8
  }
9
+
10
+ /**
11
+ * Проверяет, нужно ли блокировать подозрительную пустую загрузку.
12
+ * Блокируем только когда на доске уже есть объекты, а входящий snapshot пуст.
13
+ * Для явного разрешения пустой загрузки используем data.meta.allowEmptyLoad === true.
14
+ */
15
+ _shouldBlockSuspiciousEmptyLoad(data) {
16
+ const incomingObjects = Array.isArray(data?.objects) ? data.objects : [];
17
+ const incomingCount = incomingObjects.length;
18
+ const currentCount = Array.isArray(this.coreMoodboard?.objects) ? this.coreMoodboard.objects.length : 0;
19
+ const allowEmptyLoad = data?.meta?.allowEmptyLoad === true;
20
+
21
+ return incomingCount === 0 && currentCount > 0 && !allowEmptyLoad;
22
+ }
8
23
 
9
24
  /**
10
25
  * Загружает данные в MoodBoard
11
26
  */
12
27
  loadData(data) {
13
28
  if (!data) return;
29
+ if (this._shouldBlockSuspiciousEmptyLoad(data)) {
30
+ console.warn('⚠️ DataManager.loadData: пустой snapshot заблокирован защитой (доска не очищена).');
31
+ return;
32
+ }
33
+ const incomingMindmap = Array.isArray(data?.objects)
34
+ ? data.objects
35
+ .filter((obj) => obj?.type === 'mindmap')
36
+ .map((obj) => ({
37
+ id: obj.id || null,
38
+ compoundId: obj.properties?.mindmap?.compoundId || null,
39
+ role: obj.properties?.mindmap?.role || null,
40
+ parentId: obj.properties?.mindmap?.parentId || null,
41
+ }))
42
+ : [];
43
+ if (incomingMindmap.length > 0) {
44
+ logMindmapCompoundDebug('load:roundtrip:input', {
45
+ totalMindmapNodes: incomingMindmap.length,
46
+ sample: incomingMindmap.slice(0, 5),
47
+ });
48
+ }
14
49
 
15
50
 
16
51
 
@@ -61,6 +96,21 @@ export class DataManager {
61
96
  }
62
97
  });
63
98
  }
99
+ if (incomingMindmap.length > 0) {
100
+ const stateObjects = this.coreMoodboard?.state?.state?.objects || [];
101
+ const loadedMindmap = stateObjects
102
+ .filter((obj) => obj?.type === 'mindmap')
103
+ .map((obj) => ({
104
+ id: obj.id || null,
105
+ compoundId: obj.properties?.mindmap?.compoundId || null,
106
+ role: obj.properties?.mindmap?.role || null,
107
+ parentId: obj.properties?.mindmap?.parentId || null,
108
+ }));
109
+ logMindmapCompoundDebug('load:roundtrip:state-after-load', {
110
+ totalMindmapNodes: loadedMindmap.length,
111
+ sample: loadedMindmap.slice(0, 5),
112
+ });
113
+ }
64
114
 
65
115
  // Загружаем viewport
66
116
  if (data.viewport) {
@@ -76,6 +126,13 @@ export class DataManager {
76
126
  window.moodboardHtmlTextLayer.rebuildFromState();
77
127
  window.moodboardHtmlTextLayer.updateAll();
78
128
  }
129
+ if (window.moodboardMindmapHtmlTextLayer) {
130
+ window.moodboardMindmapHtmlTextLayer.rebuildFromState();
131
+ window.moodboardMindmapHtmlTextLayer.updateAll();
132
+ }
133
+ if (window.moodboardMindmapConnectionLayer) {
134
+ window.moodboardMindmapConnectionLayer.updateAll();
135
+ }
79
136
  }, 100);
80
137
 
81
138
  }
@@ -3,8 +3,11 @@ import { SaveStatus } from '../../ui/SaveStatus.js';
3
3
  import { Topbar } from '../../ui/Topbar.js';
4
4
  import { ZoomPanel } from '../../ui/ZoomPanel.js';
5
5
  import { MapPanel } from '../../ui/MapPanel.js';
6
+ import { DotGridDebugPanel } from '../../ui/DotGridDebugPanel.js';
6
7
  import { ContextMenu } from '../../ui/ContextMenu.js';
7
8
  import { HtmlTextLayer } from '../../ui/HtmlTextLayer.js';
9
+ import { MindmapHtmlTextLayer } from '../../ui/mindmap/MindmapHtmlTextLayer.js';
10
+ import { MindmapConnectionLayer } from '../../ui/mindmap/MindmapConnectionLayer.js';
8
11
  import { HtmlHandlesLayer } from '../../ui/HtmlHandlesLayer.js';
9
12
  import { CommentPopover } from '../../ui/CommentPopover.js';
10
13
  import { TextPropertiesPanel } from '../../ui/TextPropertiesPanel.js';
@@ -69,6 +72,13 @@ function initMapbar(board) {
69
72
  );
70
73
  }
71
74
 
75
+ function initDotGridDebugPanel(board) {
76
+ board.dotGridDebugPanel = new DotGridDebugPanel(
77
+ board.workspaceElement,
78
+ board.coreMoodboard
79
+ );
80
+ }
81
+
72
82
  function initContextMenu(board) {
73
83
  board.contextMenu = new ContextMenu(
74
84
  board.canvasContainer,
@@ -79,12 +89,18 @@ function initContextMenu(board) {
79
89
  function initHtmlLayersAndPanels(board) {
80
90
  board.htmlTextLayer = new HtmlTextLayer(board.canvasContainer, board.coreMoodboard.eventBus, board.coreMoodboard);
81
91
  board.htmlTextLayer.attach();
92
+ board.mindmapHtmlTextLayer = new MindmapHtmlTextLayer(board.canvasContainer, board.coreMoodboard.eventBus, board.coreMoodboard);
93
+ board.mindmapHtmlTextLayer.attach();
94
+ board.mindmapConnectionLayer = new MindmapConnectionLayer(board.coreMoodboard.eventBus, board.coreMoodboard);
95
+ board.mindmapConnectionLayer.attach();
82
96
 
83
97
  board.htmlHandlesLayer = new HtmlHandlesLayer(board.canvasContainer, board.coreMoodboard.eventBus, board.coreMoodboard);
84
98
  board.htmlHandlesLayer.attach();
85
99
 
86
100
  if (typeof window !== 'undefined') {
87
101
  window.moodboardHtmlTextLayer = board.htmlTextLayer;
102
+ window.moodboardMindmapHtmlTextLayer = board.mindmapHtmlTextLayer;
103
+ window.moodboardMindmapConnectionLayer = board.mindmapConnectionLayer;
88
104
  window.moodboardHtmlHandlesLayer = board.htmlHandlesLayer;
89
105
  }
90
106
 
@@ -104,6 +120,11 @@ export function createMoodBoardUi(board) {
104
120
  initTopbar(board);
105
121
  initZoombar(board);
106
122
  initMapbar(board);
123
+ // Debug-панель сетки оставляем в проекте, но не показываем по умолчанию.
124
+ // Для включения: передать showGridDebugPanel: true в options MoodBoard.
125
+ if (board?.options?.showGridDebugPanel === true) {
126
+ initDotGridDebugPanel(board);
127
+ }
107
128
  initContextMenu(board);
108
129
  initHtmlLayersAndPanels(board);
109
130
  }
@@ -1,8 +1,33 @@
1
1
  import { Events } from '../../core/events/Events.js';
2
+ import { logMindmapCompoundDebug } from '../../mindmap/MindmapCompoundContract.js';
2
3
 
3
4
  export function bindToolbarEvents(board) {
4
5
  board.coreMoodboard.eventBus.on(Events.UI.ToolbarAction, (action) => {
5
- board.actionHandler.handleToolbarAction(action);
6
+ if (action?.type === 'mindmap') {
7
+ logMindmapCompoundDebug('toolbar:action', {
8
+ type: action.type,
9
+ position: action.position || null,
10
+ mindmap: action.properties?.mindmap || null,
11
+ });
12
+ }
13
+ const createdObject = board.actionHandler.handleToolbarAction(action);
14
+ if (action?.type === 'mindmap' && createdObject?.id) {
15
+ const content = String(createdObject?.properties?.content || '');
16
+ if (content.trim().length === 0) {
17
+ setTimeout(() => {
18
+ board.coreMoodboard.eventBus.emit(Events.Keyboard.ToolSelect, { tool: 'select' });
19
+ board.coreMoodboard.eventBus.emit(Events.Tool.ObjectEdit, {
20
+ object: {
21
+ id: createdObject.id,
22
+ type: 'mindmap',
23
+ position: createdObject.position || null,
24
+ properties: createdObject.properties || {},
25
+ },
26
+ create: true,
27
+ });
28
+ }, 20);
29
+ }
30
+ }
6
31
  });
7
32
  }
8
33
 
@@ -49,6 +49,12 @@ export function destroyMoodBoard(board) {
49
49
  safeDestroy(board.htmlTextLayer, 'htmlTextLayer');
50
50
  board.htmlTextLayer = null;
51
51
 
52
+ safeDestroy(board.mindmapHtmlTextLayer, 'mindmapHtmlTextLayer');
53
+ board.mindmapHtmlTextLayer = null;
54
+
55
+ safeDestroy(board.mindmapConnectionLayer, 'mindmapConnectionLayer');
56
+ board.mindmapConnectionLayer = null;
57
+
52
58
  safeDestroy(board.htmlHandlesLayer, 'htmlHandlesLayer');
53
59
  board.htmlHandlesLayer = null;
54
60
 
@@ -64,6 +70,9 @@ export function destroyMoodBoard(board) {
64
70
  safeDestroy(board.mapbar, 'mapbar');
65
71
  board.mapbar = null;
66
72
 
73
+ safeDestroy(board.dotGridDebugPanel, 'dotGridDebugPanel');
74
+ board.dotGridDebugPanel = null;
75
+
67
76
  safeDestroy(board.coreMoodboard, 'coreMoodboard');
68
77
  board.coreMoodboard = null;
69
78
 
@@ -82,6 +91,12 @@ export function destroyMoodBoard(board) {
82
91
  if (window.moodboardHtmlTextLayer === board.htmlTextLayer) {
83
92
  window.moodboardHtmlTextLayer = null;
84
93
  }
94
+ if (window.moodboardMindmapHtmlTextLayer === board.mindmapHtmlTextLayer) {
95
+ window.moodboardMindmapHtmlTextLayer = null;
96
+ }
97
+ if (window.moodboardMindmapConnectionLayer === board.mindmapConnectionLayer) {
98
+ window.moodboardMindmapConnectionLayer = null;
99
+ }
85
100
  if (window.moodboardHtmlHandlesLayer === board.htmlHandlesLayer) {
86
101
  window.moodboardHtmlHandlesLayer = null;
87
102
  }
@@ -0,0 +1,76 @@
1
+ import * as PIXI from 'pixi.js';
2
+ import { MINDMAP_LAYOUT } from '../ui/mindmap/MindmapLayoutConfig.js';
3
+
4
+ /**
5
+ * Простой объект mindmap: прямоугольник с синей обводкой и полупрозрачной синей заливкой.
6
+ */
7
+ export class MindmapObject {
8
+ constructor(objectData = {}) {
9
+ this.objectData = objectData;
10
+ this.width = objectData.width || objectData.properties?.width || MINDMAP_LAYOUT.width;
11
+ this.height = objectData.height || objectData.properties?.height || MINDMAP_LAYOUT.height;
12
+ const props = objectData.properties || {};
13
+ this.strokeColor = (typeof props.strokeColor === 'number') ? props.strokeColor : 0x2563EB;
14
+ this.fillColor = (typeof props.fillColor === 'number') ? props.fillColor : 0x3B82F6;
15
+ this.fillAlpha = (typeof props.fillAlpha === 'number') ? props.fillAlpha : 0.25;
16
+ this.strokeWidth = (typeof props.strokeWidth === 'number') ? props.strokeWidth : 1;
17
+ this.capsuleBaseHeight = (typeof props.capsuleBaseHeight === 'number')
18
+ ? Math.max(1, Math.round(props.capsuleBaseHeight))
19
+ : Math.max(1, Math.round(Math.min(this.height, MINDMAP_LAYOUT.height)));
20
+
21
+ this.graphics = new PIXI.Graphics();
22
+ this.graphics.roundPixels = true;
23
+ this._draw();
24
+ }
25
+
26
+ getPixi() {
27
+ return this.graphics;
28
+ }
29
+
30
+ updateSize(size) {
31
+ if (!size) return;
32
+ this.width = Math.max(1, size.width || this.width);
33
+ this.height = Math.max(1, size.height || this.height);
34
+ this._redrawPreserveTransform();
35
+ }
36
+
37
+ _redrawPreserveTransform() {
38
+ const g = this.graphics;
39
+ const centerX = g.x;
40
+ const centerY = g.y;
41
+ const rot = g.rotation || 0;
42
+
43
+ this._draw();
44
+ g.pivot.set(Math.floor(this.width / 2), Math.floor(this.height / 2));
45
+ g.x = Math.round(centerX);
46
+ g.y = Math.round(centerY);
47
+ g.rotation = rot;
48
+ }
49
+
50
+ _draw() {
51
+ const g = this.graphics;
52
+ g.clear();
53
+ const dynamicRadius = Math.max(0, Math.floor(Math.min(this.width, this.height) / 2));
54
+ const fixedBaseRadius = Math.max(0, Math.floor(this.capsuleBaseHeight / 2));
55
+ const capsuleRadius = Math.min(dynamicRadius, fixedBaseRadius);
56
+
57
+ g.beginFill(this.fillColor, this.fillAlpha);
58
+ g.drawRoundedRect(0, 0, this.width, this.height, capsuleRadius);
59
+ g.endFill();
60
+
61
+ try {
62
+ g.lineStyle({
63
+ width: this.strokeWidth,
64
+ color: this.strokeColor,
65
+ alpha: 1,
66
+ alignment: 0,
67
+ cap: 'round',
68
+ join: 'round',
69
+ miterLimit: 2,
70
+ });
71
+ } catch (_) {
72
+ g.lineStyle(this.strokeWidth, this.strokeColor, 1, 0);
73
+ }
74
+ g.drawRoundedRect(0, 0, this.width, this.height, capsuleRadius);
75
+ }
76
+ }
@@ -8,6 +8,7 @@ import { RevitScreenshotImageObject } from './RevitScreenshotImageObject.js';
8
8
  import { CommentObject } from './CommentObject.js';
9
9
  import { NoteObject } from './NoteObject.js';
10
10
  import { FileObject } from './FileObject.js';
11
+ import { MindmapObject } from './MindmapObject.js';
11
12
 
12
13
  /**
13
14
  * Фабрика объектов холста
@@ -25,7 +26,8 @@ export class ObjectFactory {
25
26
  ['revit-screenshot-img', RevitScreenshotImageObject],
26
27
  ['comment', CommentObject],
27
28
  ['note', NoteObject],
28
- ['file', FileObject]
29
+ ['file', FileObject],
30
+ ['mindmap', MindmapObject]
29
31
  ]);
30
32
 
31
33
  /**