@mindexec/cli 0.2.476 → 0.2.478

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 (145) hide show
  1. package/README.md +323 -323
  2. package/codex-model-catalog.js +202 -202
  3. package/codex-runtime.js +1608 -1608
  4. package/desktop-workspace-state.cjs +120 -120
  5. package/electron/main.cjs +517 -517
  6. package/electron/preload.cjs +17 -17
  7. package/electron/recurring-update-owner-smoke.mjs +55 -55
  8. package/electron/recurring-update-owner.cjs +97 -97
  9. package/electron/source-smoke.mjs +108 -108
  10. package/electron/update-feed-resolver.cjs +79 -79
  11. package/electron/update-feed.json +10 -10
  12. package/electron/update-manager-smoke.mjs +146 -146
  13. package/electron/update-manager.cjs +297 -297
  14. package/electron/windows-package-smoke.mjs +156 -156
  15. package/launch-bridge.cjs +236 -236
  16. package/lzo1x.js +110 -110
  17. package/mode4-atlas-worker.js +440 -440
  18. package/mode4-atlas.js +130 -130
  19. package/package.json +1 -1
  20. package/port-guard.cjs +285 -285
  21. package/remote-fast/osx-arm64/MacScreenCaptureKitH264.swift +339 -339
  22. package/remote-fast/osx-arm64/MacSystemAudioCapture.swift +214 -214
  23. package/remote-fast/osx-arm64/UnixRawCaptureHelper.mjs +80 -80
  24. package/remote-fast/osx-arm64/livedesk-raw-capture-resampler.mjs +73 -73
  25. package/remote-fast/osx-arm64/mindexec-remote-fast.dll +0 -0
  26. package/remote-fast/osx-x64/MacScreenCaptureKitH264.swift +339 -339
  27. package/remote-fast/osx-x64/MacSystemAudioCapture.swift +214 -214
  28. package/remote-fast/osx-x64/UnixRawCaptureHelper.mjs +80 -80
  29. package/remote-fast/osx-x64/livedesk-raw-capture-resampler.mjs +73 -73
  30. package/remote-fast/osx-x64/mindexec-remote-fast.dll +0 -0
  31. package/remote-fast/win-x64/mindexec-remote-fast.dll +0 -0
  32. package/remote-hub.js +3698 -3698
  33. package/scripts/auth-session-smoke.mjs +262 -262
  34. package/scripts/board-render-ownership-smoke.mjs +85 -85
  35. package/scripts/codex-model-catalog-smoke.mjs +150 -150
  36. package/scripts/codex-runtime-home-smoke.mjs +58 -58
  37. package/scripts/codex-sdk-image-live-smoke.mjs +43 -43
  38. package/scripts/codex-sdk-image-runtime-smoke.mjs +122 -122
  39. package/scripts/codex-sdk-runtime-smoke.mjs +89 -89
  40. package/scripts/desktop-update-publisher-smoke.mjs +89 -89
  41. package/scripts/desktop-update-worker-smoke.mjs +72 -72
  42. package/scripts/desktop-workspace-state-smoke.mjs +81 -81
  43. package/scripts/drive-installed-update-smoke.mjs +67 -67
  44. package/scripts/npm-fresh-install-smoke.mjs +216 -216
  45. package/scripts/publish-mindexec-desktop-updates.mjs +224 -224
  46. package/scripts/remote-agent-managed-smoke.mjs +318 -318
  47. package/scripts/remote-agent-package-smoke.mjs +64 -64
  48. package/scripts/remote-agent-ws-smoke.mjs +269 -269
  49. package/scripts/remote-fast-live-rate-smoke.mjs +343 -343
  50. package/scripts/remote-fast-mdm-browser-smoke.mjs +453 -453
  51. package/scripts/remote-fleet-render-smoke.mjs +1350 -1350
  52. package/scripts/remote-frame-ws-smoke.mjs +234 -234
  53. package/scripts/remote-http-smoke.mjs +592 -592
  54. package/scripts/remote-hub-identity-smoke.mjs +146 -146
  55. package/scripts/remote-hub-scale-smoke.mjs +124 -124
  56. package/scripts/remote-hub-smoke.mjs +699 -699
  57. package/scripts/remote-input-ws-smoke.mjs +263 -263
  58. package/scripts/remote-registry-follower-smoke.mjs +815 -815
  59. package/scripts/setup-tree-sitter-grammars.mjs +59 -59
  60. package/server.js +7690 -7690
  61. package/start-bridge.bat +32 -32
  62. package/start-bridge.sh +81 -81
  63. package/wwwroot/_content/MindExecution.Plugins.Admin/css/admin-dashboard.css +546 -546
  64. package/wwwroot/_content/MindExecution.Plugins.YouTube/css/youtube-search.css +251 -251
  65. package/wwwroot/_content/MindExecution.Shared/css/app.css +1 -1
  66. package/wwwroot/_content/MindExecution.Shared/css/mind-map-overrides.css +3789 -3789
  67. package/wwwroot/_content/MindExecution.Shared/js/agent-visualization.js +27 -27
  68. package/wwwroot/_content/MindExecution.Shared/js/background-themes.js +1449 -1449
  69. package/wwwroot/_content/MindExecution.Shared/js/code-master.js +8316 -8316
  70. package/wwwroot/_content/MindExecution.Shared/js/file-system-helper.js +338 -338
  71. package/wwwroot/_content/MindExecution.Shared/js/marked.min.js +69 -69
  72. package/wwwroot/_content/MindExecution.Shared/js/mind-map-animated-image-preview.js +270 -270
  73. package/wwwroot/_content/MindExecution.Shared/js/mind-map-automation-edge-motion.js +170 -170
  74. package/wwwroot/_content/MindExecution.Shared/js/mind-map-board-render-ownership.js +81 -81
  75. package/wwwroot/_content/MindExecution.Shared/js/mind-map-core.js +10506 -10517
  76. package/wwwroot/_content/MindExecution.Shared/js/mind-map-css3d-hotpath.js +902 -902
  77. package/wwwroot/_content/MindExecution.Shared/js/mind-map-css3d-manager.js +24637 -24637
  78. package/wwwroot/_content/MindExecution.Shared/js/mind-map-dev-guards.js +325 -325
  79. package/wwwroot/_content/MindExecution.Shared/js/mind-map-dnd.js +1491 -1491
  80. package/wwwroot/_content/MindExecution.Shared/js/mind-map-glow-shader.js +70 -70
  81. package/wwwroot/_content/MindExecution.Shared/js/mind-map-image-policy.js +218 -218
  82. package/wwwroot/_content/MindExecution.Shared/js/mind-map-interactions.js +7065 -7065
  83. package/wwwroot/_content/MindExecution.Shared/js/mind-map-lod-plan-worker.js +160 -160
  84. package/wwwroot/_content/MindExecution.Shared/js/mind-map-lod-renderer.js +9956 -9956
  85. package/wwwroot/_content/MindExecution.Shared/js/mind-map-lod-video-placeholder.js +107 -107
  86. package/wwwroot/_content/MindExecution.Shared/js/mind-map-logic-workers.js +1004 -1004
  87. package/wwwroot/_content/MindExecution.Shared/js/mind-map-menu-manager.js +1351 -1351
  88. package/wwwroot/_content/MindExecution.Shared/js/mind-map-multi-select.js +865 -865
  89. package/wwwroot/_content/MindExecution.Shared/js/mind-map-node-search-worker.js +554 -554
  90. package/wwwroot/_content/MindExecution.Shared/js/mind-map-nodes.js +4744 -4744
  91. package/wwwroot/_content/MindExecution.Shared/js/mind-map-normal-boundary-optimizer.js +624 -624
  92. package/wwwroot/_content/MindExecution.Shared/js/mind-map-object-manager.js +241 -241
  93. package/wwwroot/_content/MindExecution.Shared/js/mind-map-pipeline.js +2290 -2290
  94. package/wwwroot/_content/MindExecution.Shared/js/mind-map-remote-codecs.js +202 -202
  95. package/wwwroot/_content/MindExecution.Shared/js/mind-map-render-plan.js +1971 -1971
  96. package/wwwroot/_content/MindExecution.Shared/js/mind-map-text-lod-system.js +93 -93
  97. package/wwwroot/_content/MindExecution.Shared/js/mind-map-texture-factory.js +2391 -2391
  98. package/wwwroot/_content/MindExecution.Shared/js/mind-map-video-playback-controls.js +95 -95
  99. package/wwwroot/_content/MindExecution.Shared/js/mind-map-visibility-worker.js +895 -895
  100. package/wwwroot/_content/MindExecution.Shared/js/mindmap-toolbar.js +636 -636
  101. package/wwwroot/_content/MindExecution.Shared/js/native-drop-handler.js +45 -45
  102. package/wwwroot/_content/MindExecution.Shared/js/plan-master.js +117 -117
  103. package/wwwroot/_content/MindExecution.Shared/js/renderers/CSS3DRenderer.js +114 -114
  104. package/wwwroot/_content/MindExecution.Shared/js/texture-worker-manager.js +10 -10
  105. package/wwwroot/_content/MindExecution.Shared/js/three.min.js +6 -6
  106. package/wwwroot/_content/MindExecution.Shared/js/token-manager.js +7 -7
  107. package/wwwroot/_content/MindExecution.Shared/js/troika-bundle.js +6 -6
  108. package/wwwroot/_content/MindExecution.Shared/js/troika-bundle.js.map +2 -2
  109. package/wwwroot/_content/MindExecution.Shared/lib/font-awesome/css/all.min.css +8 -8
  110. package/wwwroot/_content/MindExecution.Shared/models/vocab.txt +30522 -30522
  111. package/wwwroot/_framework/{Google.Protobuf.9h59ukbel7.dll → Google.Protobuf.33g2gb5is0.dll} +0 -0
  112. package/wwwroot/_framework/{MindExecution.Core.cx2pre7qz2.dll → MindExecution.Core.z100xo4mg0.dll} +0 -0
  113. package/wwwroot/_framework/{MindExecution.Kernel.engfih12kr.dll → MindExecution.Kernel.i9cldrx8yc.dll} +0 -0
  114. package/wwwroot/_framework/{MindExecution.Plugins.Admin.xfunemn9zi.dll → MindExecution.Plugins.Admin.48lzqddb0a.dll} +0 -0
  115. package/wwwroot/_framework/{MindExecution.Plugins.Business.ja94a5ze3i.dll → MindExecution.Plugins.Business.7m8u1q142p.dll} +0 -0
  116. package/wwwroot/_framework/{MindExecution.Plugins.Concept.3q05vzviqb.dll → MindExecution.Plugins.Concept.cl5wmr53od.dll} +0 -0
  117. package/wwwroot/_framework/{MindExecution.Plugins.Directory.re4jjs91d1.dll → MindExecution.Plugins.Directory.slipi39041.dll} +0 -0
  118. package/wwwroot/_framework/{MindExecution.Plugins.PlanMaster.89etwfquz1.dll → MindExecution.Plugins.PlanMaster.qfge05l671.dll} +0 -0
  119. package/wwwroot/_framework/{MindExecution.Plugins.YouTube.aqf4vztjji.dll → MindExecution.Plugins.YouTube.tn7v29ut77.dll} +0 -0
  120. package/wwwroot/_framework/{MindExecution.Shared.9b1rucdxvv.dll → MindExecution.Shared.rjqlf0umjn.dll} +0 -0
  121. package/wwwroot/_framework/{MindExecution.Web.ssp0n0xf1e.dll → MindExecution.Web.rwawrxxk38.dll} +0 -0
  122. package/wwwroot/_framework/blazor.boot.json +23 -23
  123. package/wwwroot/_headers +40 -40
  124. package/wwwroot/_redirects +1 -1
  125. package/wwwroot/index.html +772 -772
  126. package/wwwroot/js/marketing-tool.js +180 -180
  127. package/wwwroot/manifest.webmanifest +23 -23
  128. package/wwwroot/service-worker-assets.js +78 -106
  129. package/wwwroot/service-worker.js +33 -33
  130. package/tree-sitter-grammars/README.md +0 -18
  131. package/tree-sitter-grammars/tree-sitter-c_sharp.wasm +0 -0
  132. package/tree-sitter-grammars/tree-sitter-go.wasm +0 -0
  133. package/tree-sitter-grammars/tree-sitter-java.wasm +0 -0
  134. package/tree-sitter-grammars/tree-sitter-javascript.wasm +0 -0
  135. package/tree-sitter-grammars/tree-sitter-python.wasm +0 -0
  136. package/tree-sitter-grammars/tree-sitter-rust.wasm +0 -0
  137. package/tree-sitter-grammars/tree-sitter-tsx.wasm +0 -0
  138. package/tree-sitter-grammars/tree-sitter-typescript.wasm +0 -0
  139. package/wwwroot/_content/MindExecution.Plugins.Directory/background.png +0 -0
  140. package/wwwroot/_content/MindExecution.Shared/fonts/NotoSansKR-Bold.ttf +0 -0
  141. package/wwwroot/_content/MindExecution.Shared/fonts/NotoSansKR-Regular.ttf +0 -0
  142. package/wwwroot/_content/MindExecution.Shared/js/mind-map-core.js.backup +0 -1059
  143. package/wwwroot/_content/MindExecution.Shared/js/mind-map-dnd.js.bak +0 -434
  144. package/wwwroot/_content/MindExecution.Shared/js/mind-map-object-manager.js.backup +0 -372
  145. package/wwwroot/_content/MindExecution.Shared/js/mind-map-texture-factory.js.backup +0 -1258
@@ -4,55 +4,55 @@
4
4
  'use strict';
5
5
 
6
6
  let _module = null;
7
- let multiSelectOverlay = null;
8
- let multiSelectMenu = null;
9
- let singleImageOverlay = null;
10
- let currentBounds = null;
7
+ let multiSelectOverlay = null;
8
+ let multiSelectMenu = null;
9
+ let singleImageOverlay = null;
10
+ let currentBounds = null;
11
11
  let isResizing = false;
12
12
  let resizeHandle = null;
13
13
  let resizeStartBounds = null;
14
14
  let resizeStartMouse = null;
15
15
  let initialNodeSizes = new Map(); // Store initial sizes for proportional scaling
16
16
 
17
- const SINGLE_IMAGE_HANDLE_SIZE = 10;
18
- const MULTI_SELECT_SCREEN_PADDING_PX = 10;
19
-
20
- // ▼▼▼ [Perf] Throttling and caching for drag performance ▼▼▼
21
- const UPDATE_THROTTLE_MS = 32; // ~30fps for overlay updates during drag
22
- let lastUpdateTime = 0;
23
- let cachedImageNodeIds = null; // Cached list of selected image node IDs
24
- let isDragging = false; // Derived from module.isDraggingMultipleNodes
25
- // ▲▲▲ [Perf] ▲▲▲
26
-
27
- function setBrowserSelectionLock(active, reason) {
28
- if (window.MindMapInteractions?.setBrowserSelectionLock) {
29
- window.MindMapInteractions.setBrowserSelectionLock(active, reason || 'multi-select-resize');
30
- return;
31
- }
32
-
33
- document?.body?.classList?.toggle?.('mindcanvas-browser-selection-locked', !!active);
34
- document?.documentElement?.classList?.toggle?.('mindcanvas-browser-selection-locked', !!active);
35
- }
36
-
37
- function init(module) {
38
- _module = module;
39
- createMultiSelectOverlay();
40
- createSingleImageOverlay();
41
- }
42
-
43
- function createMultiSelectOverlay() {
44
- if (multiSelectOverlay?.isConnected) return;
45
-
46
- // Create overlay container (absolutely positioned in 3D space via CSS3DObject)
47
- multiSelectOverlay = document.createElement('div');
48
- multiSelectOverlay.className = 'multi-select-overlay';
49
- multiSelectOverlay.style.cssText = `
50
- position: fixed;
51
- pointer-events: none;
52
- border: 2px dashed #2563eb;
53
- background: rgba(37, 99, 235, 0.05);
54
- border-radius: 8px;
55
- display: none;
17
+ const SINGLE_IMAGE_HANDLE_SIZE = 10;
18
+ const MULTI_SELECT_SCREEN_PADDING_PX = 10;
19
+
20
+ // ▼▼▼ [Perf] Throttling and caching for drag performance ▼▼▼
21
+ const UPDATE_THROTTLE_MS = 32; // ~30fps for overlay updates during drag
22
+ let lastUpdateTime = 0;
23
+ let cachedImageNodeIds = null; // Cached list of selected image node IDs
24
+ let isDragging = false; // Derived from module.isDraggingMultipleNodes
25
+ // ▲▲▲ [Perf] ▲▲▲
26
+
27
+ function setBrowserSelectionLock(active, reason) {
28
+ if (window.MindMapInteractions?.setBrowserSelectionLock) {
29
+ window.MindMapInteractions.setBrowserSelectionLock(active, reason || 'multi-select-resize');
30
+ return;
31
+ }
32
+
33
+ document?.body?.classList?.toggle?.('mindcanvas-browser-selection-locked', !!active);
34
+ document?.documentElement?.classList?.toggle?.('mindcanvas-browser-selection-locked', !!active);
35
+ }
36
+
37
+ function init(module) {
38
+ _module = module;
39
+ createMultiSelectOverlay();
40
+ createSingleImageOverlay();
41
+ }
42
+
43
+ function createMultiSelectOverlay() {
44
+ if (multiSelectOverlay?.isConnected) return;
45
+
46
+ // Create overlay container (absolutely positioned in 3D space via CSS3DObject)
47
+ multiSelectOverlay = document.createElement('div');
48
+ multiSelectOverlay.className = 'multi-select-overlay';
49
+ multiSelectOverlay.style.cssText = `
50
+ position: fixed;
51
+ pointer-events: none;
52
+ border: 2px dashed #2563eb;
53
+ background: rgba(37, 99, 235, 0.05);
54
+ border-radius: 8px;
55
+ display: none;
56
56
  z-index: 1000;
57
57
  `;
58
58
 
@@ -105,59 +105,59 @@
105
105
  // Initial empty menu - will be updated by updateMenuButtons()
106
106
 
107
107
  multiSelectOverlay.appendChild(multiSelectMenu);
108
-
109
- // Append to body (will be positioned in screen space)
110
- document.body.appendChild(multiSelectOverlay);
111
- }
112
-
113
- function createSingleImageOverlay() {
114
- if (singleImageOverlay?.isConnected) return;
115
-
116
- singleImageOverlay = document.createElement('div');
117
- singleImageOverlay.className = 'single-image-selection-overlay';
118
- singleImageOverlay.style.cssText = `
119
- position: fixed;
120
- pointer-events: none;
121
- background: transparent;
122
- box-sizing: border-box;
123
- outline: 1px dashed #2563eb;
124
- outline-offset: 1px;
125
- display: none;
126
- visibility: hidden;
127
- z-index: 999;
128
- `;
129
-
130
- const corners = ['nw', 'ne', 'sw', 'se'];
131
- const handleOffset = SINGLE_IMAGE_HANDLE_SIZE / 2;
132
- corners.forEach(corner => {
133
- const handle = document.createElement('div');
134
- handle.className = `single-image-selection-handle-${corner}`;
135
- handle.style.cssText = `
136
- position: absolute;
137
- width: ${SINGLE_IMAGE_HANDLE_SIZE}px;
138
- height: ${SINGLE_IMAGE_HANDLE_SIZE}px;
139
- background: #2563eb;
140
- pointer-events: none;
141
- `;
142
-
143
- if (corner.includes('n')) handle.style.top = `${-handleOffset}px`;
144
- if (corner.includes('s')) handle.style.bottom = `${-handleOffset}px`;
145
- if (corner.includes('w')) handle.style.left = `${-handleOffset}px`;
146
- if (corner.includes('e')) handle.style.right = `${-handleOffset}px`;
147
-
148
- singleImageOverlay.appendChild(handle);
149
- });
150
-
151
- document.body.appendChild(singleImageOverlay);
152
- }
108
+
109
+ // Append to body (will be positioned in screen space)
110
+ document.body.appendChild(multiSelectOverlay);
111
+ }
112
+
113
+ function createSingleImageOverlay() {
114
+ if (singleImageOverlay?.isConnected) return;
115
+
116
+ singleImageOverlay = document.createElement('div');
117
+ singleImageOverlay.className = 'single-image-selection-overlay';
118
+ singleImageOverlay.style.cssText = `
119
+ position: fixed;
120
+ pointer-events: none;
121
+ background: transparent;
122
+ box-sizing: border-box;
123
+ outline: 1px dashed #2563eb;
124
+ outline-offset: 1px;
125
+ display: none;
126
+ visibility: hidden;
127
+ z-index: 999;
128
+ `;
129
+
130
+ const corners = ['nw', 'ne', 'sw', 'se'];
131
+ const handleOffset = SINGLE_IMAGE_HANDLE_SIZE / 2;
132
+ corners.forEach(corner => {
133
+ const handle = document.createElement('div');
134
+ handle.className = `single-image-selection-handle-${corner}`;
135
+ handle.style.cssText = `
136
+ position: absolute;
137
+ width: ${SINGLE_IMAGE_HANDLE_SIZE}px;
138
+ height: ${SINGLE_IMAGE_HANDLE_SIZE}px;
139
+ background: #2563eb;
140
+ pointer-events: none;
141
+ `;
142
+
143
+ if (corner.includes('n')) handle.style.top = `${-handleOffset}px`;
144
+ if (corner.includes('s')) handle.style.bottom = `${-handleOffset}px`;
145
+ if (corner.includes('w')) handle.style.left = `${-handleOffset}px`;
146
+ if (corner.includes('e')) handle.style.right = `${-handleOffset}px`;
147
+
148
+ singleImageOverlay.appendChild(handle);
149
+ });
150
+
151
+ document.body.appendChild(singleImageOverlay);
152
+ }
153
153
 
154
154
  // Resize move/end are handled by MindMapInteractions (centralized)
155
155
 
156
156
  function showMultiSelectBounds(selectedImageNodeIds) {
157
157
  if (!_module || selectedImageNodeIds.length < 2) {
158
- hide();
159
- return;
160
- }
158
+ hide();
159
+ return;
160
+ }
161
161
 
162
162
  // Calculate bounding box of selected nodes
163
163
  let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
@@ -182,12 +182,12 @@
182
182
  });
183
183
 
184
184
  if (minX === Infinity) {
185
- hide();
186
- return;
187
- }
185
+ hide();
186
+ return;
187
+ }
188
188
 
189
189
  // Add padding
190
- const padding = 10;
190
+ const padding = 10;
191
191
  minX -= padding;
192
192
  minY -= padding;
193
193
  maxX += padding;
@@ -212,86 +212,86 @@
212
212
  }
213
213
  });
214
214
 
215
- // Convert node bounds to screen coordinates
216
- updateOverlayPosition(selectedImageNodeIds);
217
- multiSelectOverlay.style.display = 'block';
218
- multiSelectOverlay.style.visibility = 'visible';
219
- }
220
-
221
- function getNodeScreenRect(nodeEntry) {
222
- if (!nodeEntry || !_module) return null;
223
-
224
- if (nodeEntry.currentType === 'CSS') {
225
- const element = nodeEntry.cssObject?.element;
226
- if (element?.isConnected) {
227
- const rect = element.getBoundingClientRect();
228
- if (Number.isFinite(rect.width) && Number.isFinite(rect.height) && rect.width > 0 && rect.height > 0) {
229
- return {
230
- left: rect.left,
231
- top: rect.top,
232
- right: rect.right,
233
- bottom: rect.bottom
234
- };
235
- }
236
- }
237
- }
238
-
239
- const obj = nodeEntry.glObject || nodeEntry.cssObject;
240
- if (!obj || !_module.camera || !_module.container) return null;
241
-
242
- const width = Math.max(1, Number(obj.userData?.worldWidth || nodeEntry.model?.width || 0) || 1);
243
- const height = Math.max(1, Number(obj.userData?.worldHeight || nodeEntry.model?.height || 0) || 1);
244
- const topLeft = worldToScreen(obj.position.x, obj.position.y, _module.camera, _module.container);
245
- const bottomRight = worldToScreen(obj.position.x + width, obj.position.y - height, _module.camera, _module.container);
246
- const left = Math.min(topLeft.x, bottomRight.x);
247
- const top = Math.min(topLeft.y, bottomRight.y);
248
- const right = Math.max(topLeft.x, bottomRight.x);
249
- const bottom = Math.max(topLeft.y, bottomRight.y);
250
-
251
- if (![left, top, right, bottom].every(Number.isFinite)) return null;
252
-
253
- return { left, top, right, bottom };
254
- }
255
-
256
- function applyOverlayRect(left, top, right, bottom) {
257
- multiSelectOverlay.style.left = `${left}px`;
258
- multiSelectOverlay.style.top = `${top}px`;
259
- multiSelectOverlay.style.width = `${Math.max(0, right - left)}px`;
260
- multiSelectOverlay.style.height = `${Math.max(0, bottom - top)}px`;
261
- }
262
-
263
- function updateOverlayPosition(selectedNodeIds = null, forceWorldProjection = false) {
264
- if (!currentBounds || !_module || !_module.camera || !_module.container) return;
265
-
266
- if (!forceWorldProjection && Array.isArray(selectedNodeIds) && selectedNodeIds.length > 0) {
267
- let minLeft = Infinity;
268
- let minTop = Infinity;
269
- let maxRight = -Infinity;
270
- let maxBottom = -Infinity;
271
-
272
- selectedNodeIds.forEach(nodeId => {
273
- const rect = getNodeScreenRect(_module.nodeObjectsById.get(nodeId));
274
- if (!rect) return;
275
-
276
- minLeft = Math.min(minLeft, rect.left);
277
- minTop = Math.min(minTop, rect.top);
278
- maxRight = Math.max(maxRight, rect.right);
279
- maxBottom = Math.max(maxBottom, rect.bottom);
280
- });
281
-
282
- if (minLeft !== Infinity) {
283
- applyOverlayRect(
284
- minLeft - MULTI_SELECT_SCREEN_PADDING_PX,
285
- minTop - MULTI_SELECT_SCREEN_PADDING_PX,
286
- maxRight + MULTI_SELECT_SCREEN_PADDING_PX,
287
- maxBottom + MULTI_SELECT_SCREEN_PADDING_PX
288
- );
289
- return;
290
- }
291
- }
292
-
293
- const camera = _module.camera;
294
- const container = _module.container;
215
+ // Convert node bounds to screen coordinates
216
+ updateOverlayPosition(selectedImageNodeIds);
217
+ multiSelectOverlay.style.display = 'block';
218
+ multiSelectOverlay.style.visibility = 'visible';
219
+ }
220
+
221
+ function getNodeScreenRect(nodeEntry) {
222
+ if (!nodeEntry || !_module) return null;
223
+
224
+ if (nodeEntry.currentType === 'CSS') {
225
+ const element = nodeEntry.cssObject?.element;
226
+ if (element?.isConnected) {
227
+ const rect = element.getBoundingClientRect();
228
+ if (Number.isFinite(rect.width) && Number.isFinite(rect.height) && rect.width > 0 && rect.height > 0) {
229
+ return {
230
+ left: rect.left,
231
+ top: rect.top,
232
+ right: rect.right,
233
+ bottom: rect.bottom
234
+ };
235
+ }
236
+ }
237
+ }
238
+
239
+ const obj = nodeEntry.glObject || nodeEntry.cssObject;
240
+ if (!obj || !_module.camera || !_module.container) return null;
241
+
242
+ const width = Math.max(1, Number(obj.userData?.worldWidth || nodeEntry.model?.width || 0) || 1);
243
+ const height = Math.max(1, Number(obj.userData?.worldHeight || nodeEntry.model?.height || 0) || 1);
244
+ const topLeft = worldToScreen(obj.position.x, obj.position.y, _module.camera, _module.container);
245
+ const bottomRight = worldToScreen(obj.position.x + width, obj.position.y - height, _module.camera, _module.container);
246
+ const left = Math.min(topLeft.x, bottomRight.x);
247
+ const top = Math.min(topLeft.y, bottomRight.y);
248
+ const right = Math.max(topLeft.x, bottomRight.x);
249
+ const bottom = Math.max(topLeft.y, bottomRight.y);
250
+
251
+ if (![left, top, right, bottom].every(Number.isFinite)) return null;
252
+
253
+ return { left, top, right, bottom };
254
+ }
255
+
256
+ function applyOverlayRect(left, top, right, bottom) {
257
+ multiSelectOverlay.style.left = `${left}px`;
258
+ multiSelectOverlay.style.top = `${top}px`;
259
+ multiSelectOverlay.style.width = `${Math.max(0, right - left)}px`;
260
+ multiSelectOverlay.style.height = `${Math.max(0, bottom - top)}px`;
261
+ }
262
+
263
+ function updateOverlayPosition(selectedNodeIds = null, forceWorldProjection = false) {
264
+ if (!currentBounds || !_module || !_module.camera || !_module.container) return;
265
+
266
+ if (!forceWorldProjection && Array.isArray(selectedNodeIds) && selectedNodeIds.length > 0) {
267
+ let minLeft = Infinity;
268
+ let minTop = Infinity;
269
+ let maxRight = -Infinity;
270
+ let maxBottom = -Infinity;
271
+
272
+ selectedNodeIds.forEach(nodeId => {
273
+ const rect = getNodeScreenRect(_module.nodeObjectsById.get(nodeId));
274
+ if (!rect) return;
275
+
276
+ minLeft = Math.min(minLeft, rect.left);
277
+ minTop = Math.min(minTop, rect.top);
278
+ maxRight = Math.max(maxRight, rect.right);
279
+ maxBottom = Math.max(maxBottom, rect.bottom);
280
+ });
281
+
282
+ if (minLeft !== Infinity) {
283
+ applyOverlayRect(
284
+ minLeft - MULTI_SELECT_SCREEN_PADDING_PX,
285
+ minTop - MULTI_SELECT_SCREEN_PADDING_PX,
286
+ maxRight + MULTI_SELECT_SCREEN_PADDING_PX,
287
+ maxBottom + MULTI_SELECT_SCREEN_PADDING_PX
288
+ );
289
+ return;
290
+ }
291
+ }
292
+
293
+ const camera = _module.camera;
294
+ const container = _module.container;
295
295
 
296
296
  // Convert world corners to screen coordinates
297
297
  const topLeft = worldToScreen(currentBounds.minX, currentBounds.maxY, camera, container);
@@ -302,8 +302,8 @@
302
302
  const width = Math.abs(bottomRight.x - topLeft.x);
303
303
  const height = Math.abs(bottomRight.y - topLeft.y);
304
304
 
305
- applyOverlayRect(left, top, left + width, top + height);
306
- }
305
+ applyOverlayRect(left, top, left + width, top + height);
306
+ }
307
307
 
308
308
  function worldToScreen(worldX, worldY, camera, container) {
309
309
  const THREE = globalThis.THREE;
@@ -319,108 +319,108 @@
319
319
  };
320
320
  }
321
321
 
322
- function hideSingleImageOverlay() {
323
- if (singleImageOverlay) {
324
- singleImageOverlay.style.display = 'none';
325
- singleImageOverlay.style.visibility = 'hidden';
326
- }
327
- }
328
-
329
- function getActiveSelectedNodeIds() {
330
- if (!_module) return [];
331
- if (_module.multiSelectedNodeIds?.size > 0) {
332
- return Array.from(_module.multiSelectedNodeIds);
333
- }
334
- return _module.selectedNodeIdJs ? [_module.selectedNodeIdJs] : [];
335
- }
336
-
337
- function getSingleSelectedImageNodeId() {
338
- const selectedIds = getActiveSelectedNodeIds();
339
- if (selectedIds.length !== 1) return null;
340
-
341
- const nodeId = selectedIds[0];
342
- const nodeEntry = _module?.nodeObjectsById?.get(nodeId);
343
- return nodeEntry?.model?.contentType === 'image' ? nodeId : null;
344
- }
345
-
346
- function updateSingleImageOverlay(forceWorldProjection = false) {
347
- if (!_module || !singleImageOverlay || !_module.camera || !_module.container) return;
348
-
349
- const nodeId = getSingleSelectedImageNodeId();
350
- if (!nodeId) {
351
- hideSingleImageOverlay();
352
- return;
353
- }
354
-
355
- const nodeEntry = _module.nodeObjectsById.get(nodeId);
356
- const obj = nodeEntry?.glObject || nodeEntry?.cssObject;
357
- const model = nodeEntry?.model;
358
- if (!obj || !model) {
359
- hideSingleImageOverlay();
360
- return;
361
- }
362
-
363
- // Camera-only motion must stay on the projection fast path. Reading the
364
- // CSS node rect here can force layout and may still reflect the previous
365
- // CSS3D camera transform when this overlay is updated in the same frame.
366
- const screenRect = forceWorldProjection ? null : getNodeScreenRect(nodeEntry);
367
- if (screenRect) {
368
- const screenWidth = Math.max(0, screenRect.right - screenRect.left);
369
- const screenHeight = Math.max(0, screenRect.bottom - screenRect.top);
370
- if (screenWidth > 0 && screenHeight > 0) {
371
- singleImageOverlay.style.left = `${screenRect.left}px`;
372
- singleImageOverlay.style.top = `${screenRect.top}px`;
373
- singleImageOverlay.style.width = `${screenWidth}px`;
374
- singleImageOverlay.style.height = `${screenHeight}px`;
375
- singleImageOverlay.style.display = 'block';
376
- singleImageOverlay.style.visibility = 'visible';
377
- return;
378
- }
379
- }
380
-
381
- const x = obj.position.x;
382
- const y = obj.position.y;
383
- const width = Math.max(1, Number(model.width || obj.userData?.worldWidth || 0) || 1);
384
- const height = Math.max(1, Number(model.height || obj.userData?.worldHeight || 0) || 1);
385
-
386
- const topLeft = worldToScreen(x, y, _module.camera, _module.container);
387
- const bottomRight = worldToScreen(x + width, y - height, _module.camera, _module.container);
388
-
389
- const left = Math.min(topLeft.x, bottomRight.x);
390
- const top = Math.min(topLeft.y, bottomRight.y);
391
- const screenWidth = Math.abs(bottomRight.x - topLeft.x);
392
- const screenHeight = Math.abs(bottomRight.y - topLeft.y);
393
-
394
- if (![left, top, screenWidth, screenHeight].every(Number.isFinite) || screenWidth <= 0 || screenHeight <= 0) {
395
- hideSingleImageOverlay();
396
- return;
397
- }
398
-
399
- singleImageOverlay.style.left = `${left}px`;
400
- singleImageOverlay.style.top = `${top}px`;
401
- singleImageOverlay.style.width = `${screenWidth}px`;
402
- singleImageOverlay.style.height = `${screenHeight}px`;
403
- singleImageOverlay.style.display = 'block';
404
- singleImageOverlay.style.visibility = 'visible';
405
- }
406
-
407
- function hide() {
408
- if (multiSelectOverlay) {
409
- multiSelectOverlay.style.display = 'none';
410
- multiSelectOverlay.style.visibility = 'hidden';
411
- }
412
- hideSingleImageOverlay();
413
- currentBounds = null;
414
- initialNodeSizes.clear();
415
- }
322
+ function hideSingleImageOverlay() {
323
+ if (singleImageOverlay) {
324
+ singleImageOverlay.style.display = 'none';
325
+ singleImageOverlay.style.visibility = 'hidden';
326
+ }
327
+ }
328
+
329
+ function getActiveSelectedNodeIds() {
330
+ if (!_module) return [];
331
+ if (_module.multiSelectedNodeIds?.size > 0) {
332
+ return Array.from(_module.multiSelectedNodeIds);
333
+ }
334
+ return _module.selectedNodeIdJs ? [_module.selectedNodeIdJs] : [];
335
+ }
336
+
337
+ function getSingleSelectedImageNodeId() {
338
+ const selectedIds = getActiveSelectedNodeIds();
339
+ if (selectedIds.length !== 1) return null;
340
+
341
+ const nodeId = selectedIds[0];
342
+ const nodeEntry = _module?.nodeObjectsById?.get(nodeId);
343
+ return nodeEntry?.model?.contentType === 'image' ? nodeId : null;
344
+ }
345
+
346
+ function updateSingleImageOverlay(forceWorldProjection = false) {
347
+ if (!_module || !singleImageOverlay || !_module.camera || !_module.container) return;
348
+
349
+ const nodeId = getSingleSelectedImageNodeId();
350
+ if (!nodeId) {
351
+ hideSingleImageOverlay();
352
+ return;
353
+ }
354
+
355
+ const nodeEntry = _module.nodeObjectsById.get(nodeId);
356
+ const obj = nodeEntry?.glObject || nodeEntry?.cssObject;
357
+ const model = nodeEntry?.model;
358
+ if (!obj || !model) {
359
+ hideSingleImageOverlay();
360
+ return;
361
+ }
362
+
363
+ // Camera-only motion must stay on the projection fast path. Reading the
364
+ // CSS node rect here can force layout and may still reflect the previous
365
+ // CSS3D camera transform when this overlay is updated in the same frame.
366
+ const screenRect = forceWorldProjection ? null : getNodeScreenRect(nodeEntry);
367
+ if (screenRect) {
368
+ const screenWidth = Math.max(0, screenRect.right - screenRect.left);
369
+ const screenHeight = Math.max(0, screenRect.bottom - screenRect.top);
370
+ if (screenWidth > 0 && screenHeight > 0) {
371
+ singleImageOverlay.style.left = `${screenRect.left}px`;
372
+ singleImageOverlay.style.top = `${screenRect.top}px`;
373
+ singleImageOverlay.style.width = `${screenWidth}px`;
374
+ singleImageOverlay.style.height = `${screenHeight}px`;
375
+ singleImageOverlay.style.display = 'block';
376
+ singleImageOverlay.style.visibility = 'visible';
377
+ return;
378
+ }
379
+ }
380
+
381
+ const x = obj.position.x;
382
+ const y = obj.position.y;
383
+ const width = Math.max(1, Number(model.width || obj.userData?.worldWidth || 0) || 1);
384
+ const height = Math.max(1, Number(model.height || obj.userData?.worldHeight || 0) || 1);
385
+
386
+ const topLeft = worldToScreen(x, y, _module.camera, _module.container);
387
+ const bottomRight = worldToScreen(x + width, y - height, _module.camera, _module.container);
388
+
389
+ const left = Math.min(topLeft.x, bottomRight.x);
390
+ const top = Math.min(topLeft.y, bottomRight.y);
391
+ const screenWidth = Math.abs(bottomRight.x - topLeft.x);
392
+ const screenHeight = Math.abs(bottomRight.y - topLeft.y);
393
+
394
+ if (![left, top, screenWidth, screenHeight].every(Number.isFinite) || screenWidth <= 0 || screenHeight <= 0) {
395
+ hideSingleImageOverlay();
396
+ return;
397
+ }
398
+
399
+ singleImageOverlay.style.left = `${left}px`;
400
+ singleImageOverlay.style.top = `${top}px`;
401
+ singleImageOverlay.style.width = `${screenWidth}px`;
402
+ singleImageOverlay.style.height = `${screenHeight}px`;
403
+ singleImageOverlay.style.display = 'block';
404
+ singleImageOverlay.style.visibility = 'visible';
405
+ }
406
+
407
+ function hide() {
408
+ if (multiSelectOverlay) {
409
+ multiSelectOverlay.style.display = 'none';
410
+ multiSelectOverlay.style.visibility = 'hidden';
411
+ }
412
+ hideSingleImageOverlay();
413
+ currentBounds = null;
414
+ initialNodeSizes.clear();
415
+ }
416
416
 
417
417
  function onResizeStart(e) {
418
418
  e.stopPropagation();
419
419
  e.preventDefault();
420
420
 
421
- isResizing = true;
422
- setBrowserSelectionLock(true, 'resize');
423
- resizeHandle = e.target.dataset.corner;
421
+ isResizing = true;
422
+ setBrowserSelectionLock(true, 'resize');
423
+ resizeHandle = e.target.dataset.corner;
424
424
  resizeStartBounds = { ...currentBounds };
425
425
  resizeStartMouse = { x: e.clientX, y: e.clientY };
426
426
 
@@ -464,8 +464,8 @@
464
464
  return;
465
465
  }
466
466
 
467
- currentBounds = newBounds;
468
- updateOverlayPosition(null, true);
467
+ currentBounds = newBounds;
468
+ updateOverlayPosition(null, true);
469
469
 
470
470
  // Scale and reposition selected nodes proportionally
471
471
  scaleSelectedNodes();
@@ -474,9 +474,9 @@
474
474
  function onResizeEnd(e) {
475
475
  if (!isResizing) return;
476
476
 
477
- isResizing = false;
478
- setBrowserSelectionLock(false, 'resize');
479
- resizeHandle = null;
477
+ isResizing = false;
478
+ setBrowserSelectionLock(false, 'resize');
479
+ resizeHandle = null;
480
480
  resizeStartBounds = null;
481
481
  resizeStartMouse = null;
482
482
  multiSelectOverlay.style.cursor = '';
@@ -584,9 +584,9 @@
584
584
  case 'stack':
585
585
  await mergeSelectedImagesIntoStack();
586
586
  break;
587
- case 'expand':
588
- await fitTextNodesToContentAndReflowY(selectedIds);
589
- break;
587
+ case 'expand':
588
+ await fitTextNodesToContentAndReflowY(selectedIds);
589
+ break;
590
590
  case 'delete':
591
591
  deleteSelectedNodes(selectedIds);
592
592
  break;
@@ -594,9 +594,9 @@
594
594
  }
595
595
 
596
596
  // ▼▼▼ [New] Supported node types for multi-select ▼▼▼
597
- const SUPPORTED_TYPES = ['image', 'text', 'markdown', 'note', 'code'];
598
- const TEXT_TYPES = ['text', 'markdown', 'note', 'code'];
599
- const TEXT_FIT_MAX_HEIGHT = 12000;
597
+ const SUPPORTED_TYPES = ['image', 'text', 'markdown', 'note', 'code'];
598
+ const TEXT_TYPES = ['text', 'markdown', 'note', 'code'];
599
+ const TEXT_FIT_MAX_HEIGHT = 12000;
600
600
  // ▲▲▲ [New] ▲▲▲
601
601
 
602
602
  function getSelectedNodeIds() {
@@ -766,12 +766,12 @@
766
766
  .catch(err => console.warn('[MultiSelect] Failed to refresh node after tidy:', item.nodeId, err));
767
767
  });
768
768
 
769
- if (_module.markLODPositionsDirty) {
770
- _module.markLODPositionsDirty(selectedIds);
771
- }
772
- if (resized.length > 0 && _module.markLODDirty) {
773
- _module.markLODDirty('update-node', resized.map(item => item.nodeId));
774
- }
769
+ if (_module.markLODPositionsDirty) {
770
+ _module.markLODPositionsDirty(selectedIds);
771
+ }
772
+ if (resized.length > 0 && _module.markLODDirty) {
773
+ _module.markLODDirty('update-node', resized.map(item => item.nodeId));
774
+ }
775
775
  if (_module.updateVisibility && _module.camera) {
776
776
  _module.updateVisibility(_module.camera);
777
777
  }
@@ -824,17 +824,17 @@
824
824
  if (!result) return;
825
825
 
826
826
  const removedIds = result.removedNodeIds || result.RemovedNodeIds || [];
827
- removedIds.forEach(nodeId => {
828
- if (!nodeId) return;
829
- if (_module.multiSelectedNodeIds?.delete) {
830
- _module.multiSelectedNodeIds.delete(nodeId);
831
- }
832
- });
833
- if (window.MindMapNodes?.removeNodes) {
834
- window.MindMapNodes.removeNodes(_module, removedIds);
835
- } else {
836
- removedIds.forEach(nodeId => window.MindMapNodes?.removeNode?.(_module, nodeId));
837
- }
827
+ removedIds.forEach(nodeId => {
828
+ if (!nodeId) return;
829
+ if (_module.multiSelectedNodeIds?.delete) {
830
+ _module.multiSelectedNodeIds.delete(nodeId);
831
+ }
832
+ });
833
+ if (window.MindMapNodes?.removeNodes) {
834
+ window.MindMapNodes.removeNodes(_module, removedIds);
835
+ } else {
836
+ removedIds.forEach(nodeId => window.MindMapNodes?.removeNode?.(_module, nodeId));
837
+ }
838
838
 
839
839
  const primaryNodeId = result.primaryNodeId || result.PrimaryNodeId || null;
840
840
  if (primaryNodeId) {
@@ -858,33 +858,33 @@
858
858
 
859
859
  function copySelectedNodes(selectedIds) {
860
860
  // Copy nodes to clipboard (as internal format)
861
- const copiedNodes = [];
862
- selectedIds.forEach(nodeId => {
863
- const nodeEntry = _module.nodeObjectsById.get(nodeId);
864
- if (nodeEntry?.model) {
865
- const model = nodeEntry.model;
866
- copiedNodes.push({
867
- contentType: model.contentType,
868
- prompt: model.prompt,
869
- response: model.response,
870
- width: model.width,
871
- height: model.height,
872
- positionX: model.positionX,
873
- positionY: model.positionY,
874
- positionZ: model.positionZ,
875
- isAiEnabled: model.isAiEnabled !== false,
876
- isWordWrap: !!model.isWordWrap,
877
- isChunkedText: model.isChunkedText,
878
- totalLineCount: model.totalLineCount,
879
- sourceFilePath: model.sourceFilePath,
880
- scrollOffset: model.scrollOffset,
881
- maxScroll: model.maxScroll,
882
- isExpanded: !!model.isExpanded,
883
- originalHeight: model.originalHeight,
884
- metadata: model.metadata
885
- });
886
- }
887
- });
861
+ const copiedNodes = [];
862
+ selectedIds.forEach(nodeId => {
863
+ const nodeEntry = _module.nodeObjectsById.get(nodeId);
864
+ if (nodeEntry?.model) {
865
+ const model = nodeEntry.model;
866
+ copiedNodes.push({
867
+ contentType: model.contentType,
868
+ prompt: model.prompt,
869
+ response: model.response,
870
+ width: model.width,
871
+ height: model.height,
872
+ positionX: model.positionX,
873
+ positionY: model.positionY,
874
+ positionZ: model.positionZ,
875
+ isAiEnabled: model.isAiEnabled !== false,
876
+ isWordWrap: !!model.isWordWrap,
877
+ isChunkedText: model.isChunkedText,
878
+ totalLineCount: model.totalLineCount,
879
+ sourceFilePath: model.sourceFilePath,
880
+ scrollOffset: model.scrollOffset,
881
+ maxScroll: model.maxScroll,
882
+ isExpanded: !!model.isExpanded,
883
+ originalHeight: model.originalHeight,
884
+ metadata: model.metadata
885
+ });
886
+ }
887
+ });
888
888
 
889
889
  _module.copiedNodes = copiedNodes;
890
890
  _module.copiedNodesTimestamp = Date.now();
@@ -902,73 +902,73 @@
902
902
  console.log(`[MultiSelect] Copied ${copiedNodes.length} nodes`);
903
903
  }
904
904
 
905
- function readCssPixelValue(value) {
906
- const parsed = Number.parseFloat(String(value || '0'));
907
- return Number.isFinite(parsed) ? parsed : 0;
908
- }
909
-
910
- function getVerticalBoxExtras(element) {
911
- if (!element || typeof window === 'undefined' || !window.getComputedStyle) return 0;
912
-
913
- const computed = window.getComputedStyle(element);
914
- return readCssPixelValue(computed.paddingTop)
915
- + readCssPixelValue(computed.paddingBottom)
916
- + readCssPixelValue(computed.borderTopWidth)
917
- + readCssPixelValue(computed.borderBottomWidth);
918
- }
919
-
920
- function getVerticalMargins(element) {
921
- if (!element || typeof window === 'undefined' || !window.getComputedStyle) return 0;
922
-
923
- const computed = window.getComputedStyle(element);
924
- return readCssPixelValue(computed.marginTop) + readCssPixelValue(computed.marginBottom);
925
- }
926
-
927
- function forceNaturalBlockHeight(element, preserveScrollMetrics = false) {
928
- if (!element?.style) return;
929
-
930
- element.style.setProperty('height', 'auto', 'important');
931
- element.style.setProperty('min-height', '0', 'important');
932
- element.style.setProperty('max-height', 'none', 'important');
933
- element.style.setProperty('flex', '0 0 auto', 'important');
934
-
935
- if (preserveScrollMetrics) {
936
- // Keep scrollbar-gutter and right padding active so wrapping matches the real CSS3D node.
937
- element.style.setProperty('overflow-x', 'hidden', 'important');
938
- element.style.setProperty('overflow-y', 'auto', 'important');
939
- } else {
940
- element.style.setProperty('overflow', 'visible', 'important');
941
- element.style.setProperty('overflow-y', 'visible', 'important');
942
- }
943
- }
944
-
945
- function measureOuterHeight(element, preferScrollHeight = false) {
946
- if (!element) return 0;
947
-
948
- let height = 0;
949
- const push = (value) => {
950
- const numeric = Number(value);
951
- if (Number.isFinite(numeric) && numeric > height) {
952
- height = numeric;
953
- }
954
- };
955
-
956
- if (typeof element.getBoundingClientRect === 'function') {
957
- push(element.getBoundingClientRect().height);
958
- }
959
- push(element.offsetHeight);
960
- push(element.clientHeight);
961
- if (preferScrollHeight) {
962
- push(element.scrollHeight);
963
- }
964
-
965
- return Math.ceil(height + getVerticalMargins(element));
966
- }
967
-
968
- function measureExpandedHeightFromCssDom(nodeEntry, extraBuffer = 0) {
969
- const wrapper = nodeEntry?.cssObject?.element;
970
- const source = wrapper?.firstElementChild;
971
- if (!source || typeof document === 'undefined') return null;
905
+ function readCssPixelValue(value) {
906
+ const parsed = Number.parseFloat(String(value || '0'));
907
+ return Number.isFinite(parsed) ? parsed : 0;
908
+ }
909
+
910
+ function getVerticalBoxExtras(element) {
911
+ if (!element || typeof window === 'undefined' || !window.getComputedStyle) return 0;
912
+
913
+ const computed = window.getComputedStyle(element);
914
+ return readCssPixelValue(computed.paddingTop)
915
+ + readCssPixelValue(computed.paddingBottom)
916
+ + readCssPixelValue(computed.borderTopWidth)
917
+ + readCssPixelValue(computed.borderBottomWidth);
918
+ }
919
+
920
+ function getVerticalMargins(element) {
921
+ if (!element || typeof window === 'undefined' || !window.getComputedStyle) return 0;
922
+
923
+ const computed = window.getComputedStyle(element);
924
+ return readCssPixelValue(computed.marginTop) + readCssPixelValue(computed.marginBottom);
925
+ }
926
+
927
+ function forceNaturalBlockHeight(element, preserveScrollMetrics = false) {
928
+ if (!element?.style) return;
929
+
930
+ element.style.setProperty('height', 'auto', 'important');
931
+ element.style.setProperty('min-height', '0', 'important');
932
+ element.style.setProperty('max-height', 'none', 'important');
933
+ element.style.setProperty('flex', '0 0 auto', 'important');
934
+
935
+ if (preserveScrollMetrics) {
936
+ // Keep scrollbar-gutter and right padding active so wrapping matches the real CSS3D node.
937
+ element.style.setProperty('overflow-x', 'hidden', 'important');
938
+ element.style.setProperty('overflow-y', 'auto', 'important');
939
+ } else {
940
+ element.style.setProperty('overflow', 'visible', 'important');
941
+ element.style.setProperty('overflow-y', 'visible', 'important');
942
+ }
943
+ }
944
+
945
+ function measureOuterHeight(element, preferScrollHeight = false) {
946
+ if (!element) return 0;
947
+
948
+ let height = 0;
949
+ const push = (value) => {
950
+ const numeric = Number(value);
951
+ if (Number.isFinite(numeric) && numeric > height) {
952
+ height = numeric;
953
+ }
954
+ };
955
+
956
+ if (typeof element.getBoundingClientRect === 'function') {
957
+ push(element.getBoundingClientRect().height);
958
+ }
959
+ push(element.offsetHeight);
960
+ push(element.clientHeight);
961
+ if (preferScrollHeight) {
962
+ push(element.scrollHeight);
963
+ }
964
+
965
+ return Math.ceil(height + getVerticalMargins(element));
966
+ }
967
+
968
+ function measureExpandedHeightFromCssDom(nodeEntry, extraBuffer = 0) {
969
+ const wrapper = nodeEntry?.cssObject?.element;
970
+ const source = wrapper?.firstElementChild;
971
+ if (!source || typeof document === 'undefined') return null;
972
972
 
973
973
  let probe = null;
974
974
  try {
@@ -997,56 +997,56 @@
997
997
  boxSizing: 'border-box'
998
998
  });
999
999
 
1000
- const contentWrapper = probe.querySelector('.node-content-wrapper');
1001
- if (contentWrapper) {
1002
- forceNaturalBlockHeight(contentWrapper);
1003
- contentWrapper.style.display = 'block';
1004
- }
1005
-
1006
- const contentSelectors = [
1007
- '[id^="node-response-"]',
1008
- '.node-response',
1009
- '.note-content',
1010
- '.markdown-body',
1011
- '.code-body',
1012
- '.code-body-content',
1013
- '.note-textarea',
1014
- 'textarea'
1015
- ].join(', ');
1016
- const contentElements = Array.from(probe.querySelectorAll(contentSelectors));
1017
- contentElements.forEach(element => {
1018
- if (element.style) {
1019
- element.style.display = element.tagName === 'TEXTAREA' ? 'block' : (element.style.display || 'block');
1020
- }
1021
- forceNaturalBlockHeight(element, true);
1022
- });
1023
-
1024
- document.body.appendChild(probe);
1025
-
1026
- const measuredWhole = Math.max(
1027
- measureOuterHeight(probe, true),
1028
- Math.ceil(probe.scrollHeight || 0)
1029
- );
1030
-
1031
- let measuredByChildren = 0;
1032
- if (contentWrapper) {
1033
- measuredByChildren += getVerticalBoxExtras(probe);
1034
- measuredByChildren += getVerticalBoxExtras(contentWrapper);
1035
-
1036
- Array.from(contentWrapper.children || []).forEach(child => {
1037
- if (child instanceof HTMLElement && window.getComputedStyle(child).display === 'none') {
1038
- return;
1039
- }
1040
-
1041
- const isContent = child.matches?.(contentSelectors);
1042
- measuredByChildren += measureOuterHeight(child, !!isContent);
1043
- });
1044
- }
1045
-
1046
- const measured = Math.ceil(Math.max(measuredWhole, measuredByChildren));
1047
- if (!Number.isFinite(measured) || measured <= 0) return null;
1048
-
1049
- return measured + Math.max(0, extraBuffer);
1000
+ const contentWrapper = probe.querySelector('.node-content-wrapper');
1001
+ if (contentWrapper) {
1002
+ forceNaturalBlockHeight(contentWrapper);
1003
+ contentWrapper.style.display = 'block';
1004
+ }
1005
+
1006
+ const contentSelectors = [
1007
+ '[id^="node-response-"]',
1008
+ '.node-response',
1009
+ '.note-content',
1010
+ '.markdown-body',
1011
+ '.code-body',
1012
+ '.code-body-content',
1013
+ '.note-textarea',
1014
+ 'textarea'
1015
+ ].join(', ');
1016
+ const contentElements = Array.from(probe.querySelectorAll(contentSelectors));
1017
+ contentElements.forEach(element => {
1018
+ if (element.style) {
1019
+ element.style.display = element.tagName === 'TEXTAREA' ? 'block' : (element.style.display || 'block');
1020
+ }
1021
+ forceNaturalBlockHeight(element, true);
1022
+ });
1023
+
1024
+ document.body.appendChild(probe);
1025
+
1026
+ const measuredWhole = Math.max(
1027
+ measureOuterHeight(probe, true),
1028
+ Math.ceil(probe.scrollHeight || 0)
1029
+ );
1030
+
1031
+ let measuredByChildren = 0;
1032
+ if (contentWrapper) {
1033
+ measuredByChildren += getVerticalBoxExtras(probe);
1034
+ measuredByChildren += getVerticalBoxExtras(contentWrapper);
1035
+
1036
+ Array.from(contentWrapper.children || []).forEach(child => {
1037
+ if (child instanceof HTMLElement && window.getComputedStyle(child).display === 'none') {
1038
+ return;
1039
+ }
1040
+
1041
+ const isContent = child.matches?.(contentSelectors);
1042
+ measuredByChildren += measureOuterHeight(child, !!isContent);
1043
+ });
1044
+ }
1045
+
1046
+ const measured = Math.ceil(Math.max(measuredWhole, measuredByChildren));
1047
+ if (!Number.isFinite(measured) || measured <= 0) return null;
1048
+
1049
+ return measured + Math.max(0, extraBuffer);
1050
1050
  } catch {
1051
1051
  return null;
1052
1052
  } finally {
@@ -1056,334 +1056,334 @@
1056
1056
  }
1057
1057
  }
1058
1058
 
1059
- function getTextFitMinHeight(model) {
1060
- const contentType = String(model?.contentType || '').toLowerCase();
1061
- if (contentType === 'note') return 96;
1062
- if (contentType === 'code') return 120;
1063
- return 88;
1064
- }
1065
-
1066
- async function measureTextFitHeight(nodeEntry) {
1067
- const model = nodeEntry?.model;
1068
- if (!model) return null;
1069
-
1070
- const FIT_BUFFER = 18;
1071
- const minHeight = getTextFitMinHeight(model);
1072
-
1073
- const domHeight = measureExpandedHeightFromCssDom(nodeEntry, FIT_BUFFER);
1074
- let factoryHeight = null;
1075
-
1076
- if (window.MindMapTextureFactory?.measureHtmlHeightAsync) {
1077
- const zoomLevel = 2;
1078
- const measureModel = {
1079
- ...model,
1080
- response: model.response ?? model.Response ?? model.content ?? ''
1081
- };
1082
- const { height: measuredHeight } = await window.MindMapTextureFactory.measureHtmlHeightAsync(
1083
- measureModel,
1084
- zoomLevel,
1085
- (model.width || 400) * zoomLevel,
1086
- null,
1087
- TEXT_FIT_MAX_HEIGHT
1088
- );
1089
- factoryHeight = measuredHeight + FIT_BUFFER;
1090
- }
1091
-
1092
- const requiredHeight = Math.max(
1093
- Number.isFinite(domHeight) ? domHeight : 0,
1094
- Number.isFinite(factoryHeight) ? factoryHeight : 0
1095
- );
1096
-
1097
- if (!Number.isFinite(requiredHeight) || requiredHeight <= 0) {
1098
- return null;
1099
- }
1100
-
1101
- return Math.min(TEXT_FIT_MAX_HEIGHT, Math.max(minHeight, Math.ceil(requiredHeight)));
1102
- }
1103
-
1104
- function readRenderedTextOverflowAdjustment(item) {
1105
- const entry = item?.entry;
1106
- const root = entry?.cssObject?.element?.firstElementChild || entry?.cssObject?.element;
1107
- if (!root) return 0;
1108
-
1109
- const contentEl = root.querySelector?.('[id^="node-response-"]')
1110
- || root.querySelector?.('.node-response')
1111
- || root.querySelector?.('.note-textarea')
1112
- || root.querySelector?.('textarea');
1113
- if (!contentEl) return 0;
1114
-
1115
- const scrollHeight = Math.ceil(Number(contentEl.scrollHeight || 0));
1116
- const clientHeight = Math.ceil(Number(contentEl.clientHeight || 0));
1117
- const overflow = scrollHeight - clientHeight;
1118
- if (!Number.isFinite(overflow) || overflow <= 1) return 0;
1119
-
1120
- return overflow + 12;
1121
- }
1122
-
1123
- function refineTextFitItemsFromRenderedDom(items) {
1124
- if (!Array.isArray(items) || items.length === 0) return 0;
1125
-
1126
- let adjusted = 0;
1127
- for (const item of items) {
1128
- const correction = readRenderedTextOverflowAdjustment(item);
1129
- if (correction <= 0) continue;
1130
-
1131
- item.newHeight = Math.min(TEXT_FIT_MAX_HEIGHT, Math.ceil(item.newHeight + correction));
1132
- adjusted++;
1133
- }
1134
-
1135
- return adjusted;
1136
- }
1137
-
1138
- function getNodeFrame(nodeId) {
1139
- const entry = _module?.nodeObjectsById?.get?.(nodeId);
1140
- const model = entry?.model;
1141
- if (!entry || !model) return null;
1142
-
1143
- const x = Number(model.positionX ?? model.x ?? entry.glObject?.position?.x ?? entry.cssObject?.position?.x ?? 0);
1144
- const y = Number(model.positionY ?? model.y ?? entry.glObject?.position?.y ?? entry.cssObject?.position?.y ?? 0);
1145
- const width = Math.max(1, Math.round(Number(model.width || 400)));
1146
- const height = Math.max(1, Math.round(Number(model.height || 200)));
1147
-
1148
- return {
1149
- nodeId,
1150
- entry,
1151
- model,
1152
- x,
1153
- y,
1154
- width,
1155
- height,
1156
- left: x,
1157
- right: x + width,
1158
- centerX: x + width / 2
1159
- };
1160
- }
1161
-
1162
- function horizontalOverlapRatio(a, b) {
1163
- const overlap = Math.min(a.right, b.right) - Math.max(a.left, b.left);
1164
- if (overlap <= 0) return 0;
1165
- return overlap / Math.max(1, Math.min(a.width, b.width));
1166
- }
1167
-
1168
- function groupTextFramesByColumn(items) {
1169
- const groups = [];
1170
- const ordered = [...items].sort((a, b) => a.left - b.left || b.y - a.y);
1171
-
1172
- for (const item of ordered) {
1173
- let target = null;
1174
- let bestScore = 0;
1175
-
1176
- for (const group of groups) {
1177
- const groupFrame = {
1178
- left: group.left,
1179
- right: group.right,
1180
- width: Math.max(1, group.right - group.left)
1181
- };
1182
- const overlapScore = horizontalOverlapRatio(item, groupFrame);
1183
- const centerDistance = Math.abs(item.centerX - group.centerX);
1184
- const centerThreshold = Math.max(80, Math.min(item.width, groupFrame.width) * 0.35);
1185
- const score = overlapScore > 0 ? overlapScore : (centerDistance <= centerThreshold ? 0.2 : 0);
1186
-
1187
- if (score > bestScore) {
1188
- bestScore = score;
1189
- target = group;
1190
- }
1191
- }
1192
-
1193
- if (!target || bestScore < 0.15) {
1194
- target = {
1195
- items: [],
1196
- left: item.left,
1197
- right: item.right,
1198
- centerX: item.centerX
1199
- };
1200
- groups.push(target);
1201
- }
1202
-
1203
- target.items.push(item);
1204
- target.left = Math.min(target.left, item.left);
1205
- target.right = Math.max(target.right, item.right);
1206
- target.centerX = target.items.reduce((sum, n) => sum + n.centerX, 0) / target.items.length;
1207
- }
1208
-
1209
- return groups;
1210
- }
1211
-
1212
- function buildTextFitLayoutUpdates(items) {
1213
- const DEFAULT_GAP = 24;
1214
- const MIN_GAP = 12;
1215
- const groups = groupTextFramesByColumn(items);
1216
- const updates = [];
1217
-
1218
- for (const group of groups) {
1219
- const ordered = [...group.items].sort((a, b) => b.y - a.y || a.x - b.x);
1220
-
1221
- let previous = null;
1222
- for (const item of ordered) {
1223
- let y = item.y;
1224
-
1225
- if (previous) {
1226
- const originalGap = previous.y - previous.height - item.y;
1227
- const gap = Number.isFinite(originalGap) && originalGap >= 0
1228
- ? Math.max(MIN_GAP, originalGap)
1229
- : DEFAULT_GAP;
1230
- y = previous.newY - previous.newHeight - gap;
1231
- }
1232
-
1233
- item.newY = y;
1234
- previous = item;
1235
-
1236
- updates.push({
1237
- nodeId: item.nodeId,
1238
- x: item.x,
1239
- y,
1240
- width: item.width,
1241
- height: item.newHeight
1242
- });
1243
- }
1244
- }
1245
-
1246
- return updates;
1247
- }
1248
-
1249
- async function applyTextFitLayoutUpdates(updates) {
1250
- if (!_module || !Array.isArray(updates) || updates.length === 0) return;
1251
-
1252
- const renderPromises = [];
1253
-
1254
- for (const update of updates) {
1255
- const entry = _module.nodeObjectsById.get(update.nodeId);
1256
- if (!entry || !entry.model) continue;
1257
-
1258
- const width = Math.max(1, Math.round(Number(update.width || entry.model.width || 400)));
1259
- const height = Math.max(1, Math.round(Number(update.height || entry.model.height || 200)));
1260
- const x = Number(update.x ?? entry.model.positionX ?? entry.model.x ?? 0);
1261
- const y = Number(update.y ?? entry.model.positionY ?? entry.model.y ?? 0);
1262
-
1263
- entry.model.positionX = x;
1264
- entry.model.positionY = y;
1265
- entry.model.x = x;
1266
- entry.model.y = y;
1267
- entry.model.width = width;
1268
- entry.model.height = height;
1269
- entry.model.originalHeight = height;
1270
- entry.model.isExpanded = false;
1271
- entry.model.scrollOffset = 0;
1272
- entry.model.maxScroll = 0;
1273
-
1274
- if (entry.glObject) {
1275
- entry.glObject.position.x = x;
1276
- entry.glObject.position.y = y;
1277
- if (entry.glObject.userData) {
1278
- entry.glObject.userData.worldWidth = width;
1279
- entry.glObject.userData.worldHeight = height;
1280
- }
1281
- }
1282
- if (entry.cssObject) {
1283
- entry.cssObject.position.x = x;
1284
- entry.cssObject.position.y = y;
1285
- if (entry.cssObject.userData) {
1286
- entry.cssObject.userData.worldWidth = width;
1287
- entry.cssObject.userData.worldHeight = height;
1288
- }
1289
- window.MindMapCss3DManager?.syncCss3dContent?.(entry.model, entry.cssObject);
1290
- entry.cssObject.updateMatrixWorld?.(true);
1291
- }
1292
-
1293
- window.MindMapNodes?.updateNodeSpatialGrid?.(_module, update.nodeId);
1294
- window.MindMapTextLOD?.invalidateNodeCache?.(update.nodeId, false);
1295
- window.MindMapTextOverlayV2?.invalidateNode?.(_module, update.nodeId, 'layout');
1296
-
1297
- if (window.MindMapNodes?.pendingNodeUpdates && window.MindMapPipeline?.processRenderQueue) {
1298
- window.MindMapNodes.pendingNodeUpdates.set(update.nodeId, {
1299
- content: entry.model.response ?? entry.model.Response ?? entry.model.content ?? '',
1300
- width,
1301
- height,
1302
- skipMeasure: true,
1303
- skipPersistDimensions: true
1304
- });
1305
- renderPromises.push(window.MindMapPipeline.processRenderQueue(_module, update.nodeId));
1306
- }
1307
- }
1308
-
1309
- const RENDER_CHUNK_SIZE = 10;
1310
- for (let i = 0; i < renderPromises.length; i += RENDER_CHUNK_SIZE) {
1311
- await Promise.all(renderPromises.slice(i, i + RENDER_CHUNK_SIZE));
1312
- if (i + RENDER_CHUNK_SIZE < renderPromises.length) {
1313
- await new Promise(resolve => setTimeout(resolve, 0));
1314
- }
1315
- }
1316
-
1317
- for (const update of updates) {
1318
- window.MindMapTextOverlayV2?.updateNodePlacement?.(_module, update.nodeId);
1319
- }
1320
-
1321
- _module.markLODPositionsDirty?.();
1322
- _module.markLODDirty?.();
1323
- _module.updateVisibility?.(_module.camera);
1324
- }
1325
-
1326
- // Fit selected text nodes to their CSS3D content and reflow Y inside each column.
1327
- async function fitTextNodesToContentAndReflowY(selectedIds) {
1328
- if (!_module) return;
1329
-
1330
- const textNodeIds = selectedIds.filter(nodeId => {
1331
- const entry = _module.nodeObjectsById.get(nodeId);
1332
- return entry && TEXT_TYPES.includes(entry.model?.contentType);
1333
- });
1334
-
1335
- if (textNodeIds.length === 0) {
1336
- console.log('[MultiSelect] No text nodes to fit');
1337
- return;
1338
- }
1339
-
1340
- console.log(`[MultiSelect] Fitting ${textNodeIds.length} text nodes to content...`);
1341
- const startTime = performance.now();
1342
-
1343
- const measureStart = performance.now();
1344
- const measurePromises = textNodeIds.map(async (nodeId) => {
1345
- const frame = getNodeFrame(nodeId);
1346
- if (!frame) return null;
1347
-
1348
- try {
1349
- const newHeight = await measureTextFitHeight(frame.entry);
1350
- if (!Number.isFinite(newHeight) || newHeight <= 0) return null;
1351
- return { ...frame, newHeight };
1352
- } catch (err) {
1353
- console.error(`[MultiSelect] Failed to measure node ${nodeId}:`, err);
1354
- return null;
1355
- }
1356
- });
1357
-
1358
- const measureResults = await Promise.all(measurePromises);
1359
- const fitItems = measureResults.filter(r => r !== null);
1360
-
1361
- const measureTime = (performance.now() - measureStart).toFixed(0);
1362
- console.log(`[MultiSelect] Measured ${textNodeIds.length} nodes in ${measureTime}ms`);
1363
-
1364
- if (fitItems.length > 0) {
1365
- let layoutUpdates = buildTextFitLayoutUpdates(fitItems);
1366
- await applyTextFitLayoutUpdates(layoutUpdates);
1367
-
1368
- const refinedCount = refineTextFitItemsFromRenderedDom(fitItems);
1369
- if (refinedCount > 0) {
1370
- layoutUpdates = buildTextFitLayoutUpdates(fitItems);
1371
- await applyTextFitLayoutUpdates(layoutUpdates);
1372
- console.log(`[MultiSelect] Refined ${refinedCount} text node heights from rendered DOM`);
1373
- }
1374
-
1375
- if (_module.dotNetHelper && layoutUpdates.length > 0) {
1376
- await _module.dotNetHelper.invokeMethodAsync('UpdateMultipleNodesPositionFromJs', layoutUpdates);
1377
- _module.dotNetHelper.invokeMethodAsync('TriggerBoardSave');
1378
- }
1379
-
1380
- const elapsed = (performance.now() - startTime).toFixed(0);
1381
- console.log(`[MultiSelect] ✅ Fit ${layoutUpdates.length} text nodes in ${elapsed}ms`);
1382
- }
1383
-
1384
- // Update bounds after fitting/reflow.
1385
- showMultiSelectBounds(getSelectedNodeIds());
1386
- }
1059
+ function getTextFitMinHeight(model) {
1060
+ const contentType = String(model?.contentType || '').toLowerCase();
1061
+ if (contentType === 'note') return 96;
1062
+ if (contentType === 'code') return 120;
1063
+ return 88;
1064
+ }
1065
+
1066
+ async function measureTextFitHeight(nodeEntry) {
1067
+ const model = nodeEntry?.model;
1068
+ if (!model) return null;
1069
+
1070
+ const FIT_BUFFER = 18;
1071
+ const minHeight = getTextFitMinHeight(model);
1072
+
1073
+ const domHeight = measureExpandedHeightFromCssDom(nodeEntry, FIT_BUFFER);
1074
+ let factoryHeight = null;
1075
+
1076
+ if (window.MindMapTextureFactory?.measureHtmlHeightAsync) {
1077
+ const zoomLevel = 2;
1078
+ const measureModel = {
1079
+ ...model,
1080
+ response: model.response ?? model.Response ?? model.content ?? ''
1081
+ };
1082
+ const { height: measuredHeight } = await window.MindMapTextureFactory.measureHtmlHeightAsync(
1083
+ measureModel,
1084
+ zoomLevel,
1085
+ (model.width || 400) * zoomLevel,
1086
+ null,
1087
+ TEXT_FIT_MAX_HEIGHT
1088
+ );
1089
+ factoryHeight = measuredHeight + FIT_BUFFER;
1090
+ }
1091
+
1092
+ const requiredHeight = Math.max(
1093
+ Number.isFinite(domHeight) ? domHeight : 0,
1094
+ Number.isFinite(factoryHeight) ? factoryHeight : 0
1095
+ );
1096
+
1097
+ if (!Number.isFinite(requiredHeight) || requiredHeight <= 0) {
1098
+ return null;
1099
+ }
1100
+
1101
+ return Math.min(TEXT_FIT_MAX_HEIGHT, Math.max(minHeight, Math.ceil(requiredHeight)));
1102
+ }
1103
+
1104
+ function readRenderedTextOverflowAdjustment(item) {
1105
+ const entry = item?.entry;
1106
+ const root = entry?.cssObject?.element?.firstElementChild || entry?.cssObject?.element;
1107
+ if (!root) return 0;
1108
+
1109
+ const contentEl = root.querySelector?.('[id^="node-response-"]')
1110
+ || root.querySelector?.('.node-response')
1111
+ || root.querySelector?.('.note-textarea')
1112
+ || root.querySelector?.('textarea');
1113
+ if (!contentEl) return 0;
1114
+
1115
+ const scrollHeight = Math.ceil(Number(contentEl.scrollHeight || 0));
1116
+ const clientHeight = Math.ceil(Number(contentEl.clientHeight || 0));
1117
+ const overflow = scrollHeight - clientHeight;
1118
+ if (!Number.isFinite(overflow) || overflow <= 1) return 0;
1119
+
1120
+ return overflow + 12;
1121
+ }
1122
+
1123
+ function refineTextFitItemsFromRenderedDom(items) {
1124
+ if (!Array.isArray(items) || items.length === 0) return 0;
1125
+
1126
+ let adjusted = 0;
1127
+ for (const item of items) {
1128
+ const correction = readRenderedTextOverflowAdjustment(item);
1129
+ if (correction <= 0) continue;
1130
+
1131
+ item.newHeight = Math.min(TEXT_FIT_MAX_HEIGHT, Math.ceil(item.newHeight + correction));
1132
+ adjusted++;
1133
+ }
1134
+
1135
+ return adjusted;
1136
+ }
1137
+
1138
+ function getNodeFrame(nodeId) {
1139
+ const entry = _module?.nodeObjectsById?.get?.(nodeId);
1140
+ const model = entry?.model;
1141
+ if (!entry || !model) return null;
1142
+
1143
+ const x = Number(model.positionX ?? model.x ?? entry.glObject?.position?.x ?? entry.cssObject?.position?.x ?? 0);
1144
+ const y = Number(model.positionY ?? model.y ?? entry.glObject?.position?.y ?? entry.cssObject?.position?.y ?? 0);
1145
+ const width = Math.max(1, Math.round(Number(model.width || 400)));
1146
+ const height = Math.max(1, Math.round(Number(model.height || 200)));
1147
+
1148
+ return {
1149
+ nodeId,
1150
+ entry,
1151
+ model,
1152
+ x,
1153
+ y,
1154
+ width,
1155
+ height,
1156
+ left: x,
1157
+ right: x + width,
1158
+ centerX: x + width / 2
1159
+ };
1160
+ }
1161
+
1162
+ function horizontalOverlapRatio(a, b) {
1163
+ const overlap = Math.min(a.right, b.right) - Math.max(a.left, b.left);
1164
+ if (overlap <= 0) return 0;
1165
+ return overlap / Math.max(1, Math.min(a.width, b.width));
1166
+ }
1167
+
1168
+ function groupTextFramesByColumn(items) {
1169
+ const groups = [];
1170
+ const ordered = [...items].sort((a, b) => a.left - b.left || b.y - a.y);
1171
+
1172
+ for (const item of ordered) {
1173
+ let target = null;
1174
+ let bestScore = 0;
1175
+
1176
+ for (const group of groups) {
1177
+ const groupFrame = {
1178
+ left: group.left,
1179
+ right: group.right,
1180
+ width: Math.max(1, group.right - group.left)
1181
+ };
1182
+ const overlapScore = horizontalOverlapRatio(item, groupFrame);
1183
+ const centerDistance = Math.abs(item.centerX - group.centerX);
1184
+ const centerThreshold = Math.max(80, Math.min(item.width, groupFrame.width) * 0.35);
1185
+ const score = overlapScore > 0 ? overlapScore : (centerDistance <= centerThreshold ? 0.2 : 0);
1186
+
1187
+ if (score > bestScore) {
1188
+ bestScore = score;
1189
+ target = group;
1190
+ }
1191
+ }
1192
+
1193
+ if (!target || bestScore < 0.15) {
1194
+ target = {
1195
+ items: [],
1196
+ left: item.left,
1197
+ right: item.right,
1198
+ centerX: item.centerX
1199
+ };
1200
+ groups.push(target);
1201
+ }
1202
+
1203
+ target.items.push(item);
1204
+ target.left = Math.min(target.left, item.left);
1205
+ target.right = Math.max(target.right, item.right);
1206
+ target.centerX = target.items.reduce((sum, n) => sum + n.centerX, 0) / target.items.length;
1207
+ }
1208
+
1209
+ return groups;
1210
+ }
1211
+
1212
+ function buildTextFitLayoutUpdates(items) {
1213
+ const DEFAULT_GAP = 24;
1214
+ const MIN_GAP = 12;
1215
+ const groups = groupTextFramesByColumn(items);
1216
+ const updates = [];
1217
+
1218
+ for (const group of groups) {
1219
+ const ordered = [...group.items].sort((a, b) => b.y - a.y || a.x - b.x);
1220
+
1221
+ let previous = null;
1222
+ for (const item of ordered) {
1223
+ let y = item.y;
1224
+
1225
+ if (previous) {
1226
+ const originalGap = previous.y - previous.height - item.y;
1227
+ const gap = Number.isFinite(originalGap) && originalGap >= 0
1228
+ ? Math.max(MIN_GAP, originalGap)
1229
+ : DEFAULT_GAP;
1230
+ y = previous.newY - previous.newHeight - gap;
1231
+ }
1232
+
1233
+ item.newY = y;
1234
+ previous = item;
1235
+
1236
+ updates.push({
1237
+ nodeId: item.nodeId,
1238
+ x: item.x,
1239
+ y,
1240
+ width: item.width,
1241
+ height: item.newHeight
1242
+ });
1243
+ }
1244
+ }
1245
+
1246
+ return updates;
1247
+ }
1248
+
1249
+ async function applyTextFitLayoutUpdates(updates) {
1250
+ if (!_module || !Array.isArray(updates) || updates.length === 0) return;
1251
+
1252
+ const renderPromises = [];
1253
+
1254
+ for (const update of updates) {
1255
+ const entry = _module.nodeObjectsById.get(update.nodeId);
1256
+ if (!entry || !entry.model) continue;
1257
+
1258
+ const width = Math.max(1, Math.round(Number(update.width || entry.model.width || 400)));
1259
+ const height = Math.max(1, Math.round(Number(update.height || entry.model.height || 200)));
1260
+ const x = Number(update.x ?? entry.model.positionX ?? entry.model.x ?? 0);
1261
+ const y = Number(update.y ?? entry.model.positionY ?? entry.model.y ?? 0);
1262
+
1263
+ entry.model.positionX = x;
1264
+ entry.model.positionY = y;
1265
+ entry.model.x = x;
1266
+ entry.model.y = y;
1267
+ entry.model.width = width;
1268
+ entry.model.height = height;
1269
+ entry.model.originalHeight = height;
1270
+ entry.model.isExpanded = false;
1271
+ entry.model.scrollOffset = 0;
1272
+ entry.model.maxScroll = 0;
1273
+
1274
+ if (entry.glObject) {
1275
+ entry.glObject.position.x = x;
1276
+ entry.glObject.position.y = y;
1277
+ if (entry.glObject.userData) {
1278
+ entry.glObject.userData.worldWidth = width;
1279
+ entry.glObject.userData.worldHeight = height;
1280
+ }
1281
+ }
1282
+ if (entry.cssObject) {
1283
+ entry.cssObject.position.x = x;
1284
+ entry.cssObject.position.y = y;
1285
+ if (entry.cssObject.userData) {
1286
+ entry.cssObject.userData.worldWidth = width;
1287
+ entry.cssObject.userData.worldHeight = height;
1288
+ }
1289
+ window.MindMapCss3DManager?.syncCss3dContent?.(entry.model, entry.cssObject);
1290
+ entry.cssObject.updateMatrixWorld?.(true);
1291
+ }
1292
+
1293
+ window.MindMapNodes?.updateNodeSpatialGrid?.(_module, update.nodeId);
1294
+ window.MindMapTextLOD?.invalidateNodeCache?.(update.nodeId, false);
1295
+ window.MindMapTextOverlayV2?.invalidateNode?.(_module, update.nodeId, 'layout');
1296
+
1297
+ if (window.MindMapNodes?.pendingNodeUpdates && window.MindMapPipeline?.processRenderQueue) {
1298
+ window.MindMapNodes.pendingNodeUpdates.set(update.nodeId, {
1299
+ content: entry.model.response ?? entry.model.Response ?? entry.model.content ?? '',
1300
+ width,
1301
+ height,
1302
+ skipMeasure: true,
1303
+ skipPersistDimensions: true
1304
+ });
1305
+ renderPromises.push(window.MindMapPipeline.processRenderQueue(_module, update.nodeId));
1306
+ }
1307
+ }
1308
+
1309
+ const RENDER_CHUNK_SIZE = 10;
1310
+ for (let i = 0; i < renderPromises.length; i += RENDER_CHUNK_SIZE) {
1311
+ await Promise.all(renderPromises.slice(i, i + RENDER_CHUNK_SIZE));
1312
+ if (i + RENDER_CHUNK_SIZE < renderPromises.length) {
1313
+ await new Promise(resolve => setTimeout(resolve, 0));
1314
+ }
1315
+ }
1316
+
1317
+ for (const update of updates) {
1318
+ window.MindMapTextOverlayV2?.updateNodePlacement?.(_module, update.nodeId);
1319
+ }
1320
+
1321
+ _module.markLODPositionsDirty?.();
1322
+ _module.markLODDirty?.();
1323
+ _module.updateVisibility?.(_module.camera);
1324
+ }
1325
+
1326
+ // Fit selected text nodes to their CSS3D content and reflow Y inside each column.
1327
+ async function fitTextNodesToContentAndReflowY(selectedIds) {
1328
+ if (!_module) return;
1329
+
1330
+ const textNodeIds = selectedIds.filter(nodeId => {
1331
+ const entry = _module.nodeObjectsById.get(nodeId);
1332
+ return entry && TEXT_TYPES.includes(entry.model?.contentType);
1333
+ });
1334
+
1335
+ if (textNodeIds.length === 0) {
1336
+ console.log('[MultiSelect] No text nodes to fit');
1337
+ return;
1338
+ }
1339
+
1340
+ console.log(`[MultiSelect] Fitting ${textNodeIds.length} text nodes to content...`);
1341
+ const startTime = performance.now();
1342
+
1343
+ const measureStart = performance.now();
1344
+ const measurePromises = textNodeIds.map(async (nodeId) => {
1345
+ const frame = getNodeFrame(nodeId);
1346
+ if (!frame) return null;
1347
+
1348
+ try {
1349
+ const newHeight = await measureTextFitHeight(frame.entry);
1350
+ if (!Number.isFinite(newHeight) || newHeight <= 0) return null;
1351
+ return { ...frame, newHeight };
1352
+ } catch (err) {
1353
+ console.error(`[MultiSelect] Failed to measure node ${nodeId}:`, err);
1354
+ return null;
1355
+ }
1356
+ });
1357
+
1358
+ const measureResults = await Promise.all(measurePromises);
1359
+ const fitItems = measureResults.filter(r => r !== null);
1360
+
1361
+ const measureTime = (performance.now() - measureStart).toFixed(0);
1362
+ console.log(`[MultiSelect] Measured ${textNodeIds.length} nodes in ${measureTime}ms`);
1363
+
1364
+ if (fitItems.length > 0) {
1365
+ let layoutUpdates = buildTextFitLayoutUpdates(fitItems);
1366
+ await applyTextFitLayoutUpdates(layoutUpdates);
1367
+
1368
+ const refinedCount = refineTextFitItemsFromRenderedDom(fitItems);
1369
+ if (refinedCount > 0) {
1370
+ layoutUpdates = buildTextFitLayoutUpdates(fitItems);
1371
+ await applyTextFitLayoutUpdates(layoutUpdates);
1372
+ console.log(`[MultiSelect] Refined ${refinedCount} text node heights from rendered DOM`);
1373
+ }
1374
+
1375
+ if (_module.dotNetHelper && layoutUpdates.length > 0) {
1376
+ await _module.dotNetHelper.invokeMethodAsync('UpdateMultipleNodesPositionFromJs', layoutUpdates);
1377
+ _module.dotNetHelper.invokeMethodAsync('TriggerBoardSave');
1378
+ }
1379
+
1380
+ const elapsed = (performance.now() - startTime).toFixed(0);
1381
+ console.log(`[MultiSelect] ✅ Fit ${layoutUpdates.length} text nodes in ${elapsed}ms`);
1382
+ }
1383
+
1384
+ // Update bounds after fitting/reflow.
1385
+ showMultiSelectBounds(getSelectedNodeIds());
1386
+ }
1387
1387
 
1388
1388
  function applyMasonryLayout(selectedIds) {
1389
1389
  if (!currentBounds || selectedIds.length === 0) return;
@@ -1511,27 +1511,27 @@
1511
1511
  if (multiSelectOverlay) {
1512
1512
  multiSelectOverlay.style.display = 'none';
1513
1513
  multiSelectOverlay.style.visibility = 'hidden';
1514
- }
1515
-
1516
- // Delete via C# (batch delete - much faster than individual calls)
1517
- const snapshots = selectedIds
1518
- .map(nodeId => window.MindMapNodes?.captureDeleteSnapshot?.(_module, nodeId))
1519
- .filter(Boolean);
1520
- if (snapshots.length > 0) {
1521
- window.MindMapNodes?.removeNodes?.(_module, snapshots.map(snapshot => snapshot.nodeId));
1522
- }
1523
- _module.dotNetHelper.invokeMethodAsync('DeleteMultipleNodeSnapshotsFromJs', snapshots);
1524
-
1525
- console.log(`[MultiSelect] Deleted ${selectedIds.length} nodes`);
1526
- }
1514
+ }
1515
+
1516
+ // Delete via C# (batch delete - much faster than individual calls)
1517
+ const snapshots = selectedIds
1518
+ .map(nodeId => window.MindMapNodes?.captureDeleteSnapshot?.(_module, nodeId))
1519
+ .filter(Boolean);
1520
+ if (snapshots.length > 0) {
1521
+ window.MindMapNodes?.removeNodes?.(_module, snapshots.map(snapshot => snapshot.nodeId));
1522
+ }
1523
+ _module.dotNetHelper.invokeMethodAsync('DeleteMultipleNodeSnapshotsFromJs', snapshots);
1524
+
1525
+ console.log(`[MultiSelect] Deleted ${selectedIds.length} nodes`);
1526
+ }
1527
1527
 
1528
1528
  // Update overlay position on camera move or node drag (called from animate loop)
1529
1529
  // ▼▼▼ [Perf] Throttled and optimized for drag performance ▼▼▼
1530
- function updateMultiSelectOverlay() {
1531
- if (!_module || !multiSelectOverlay || multiSelectOverlay.style.display === 'none') return;
1532
-
1533
- // Centralized drag-state sync from interactions module
1534
- const draggingNow = !!_module.isDraggingMultipleNodes;
1530
+ function updateMultiSelectOverlay() {
1531
+ if (!_module || !multiSelectOverlay || multiSelectOverlay.style.display === 'none') return;
1532
+
1533
+ // Centralized drag-state sync from interactions module
1534
+ const draggingNow = !!_module.isDraggingMultipleNodes;
1535
1535
  if (draggingNow !== isDragging) {
1536
1536
  isDragging = draggingNow;
1537
1537
  lastUpdateTime = 0;
@@ -1581,45 +1581,45 @@
1581
1581
  }
1582
1582
 
1583
1583
  // Add padding
1584
- const padding = 10;
1584
+ const padding = 10;
1585
1585
  currentBounds = {
1586
1586
  minX: minX - padding,
1587
1587
  minY: minY - padding,
1588
1588
  maxX: maxX + padding,
1589
1589
  maxY: maxY + padding
1590
1590
  };
1591
-
1592
- updateOverlayPosition(selectedIds);
1593
- }
1594
-
1595
- function update() {
1596
- if (!_module) return;
1597
- updateMultiSelectOverlay();
1598
- updateSingleImageOverlay();
1599
- }
1600
-
1601
- // Camera-only fast path: currentBounds and node world frames are already
1602
- // canonical, so reproject only the fixed overlays. This deliberately avoids
1603
- // selected-node scans, DOM layout reads, menu work, and renderer rebuilds.
1604
- function updateForCameraMotion() {
1605
- if (!_module || !_module.camera || !_module.container) return false;
1606
-
1607
- let updated = false;
1608
- if (multiSelectOverlay &&
1609
- multiSelectOverlay.style.display !== 'none' &&
1610
- currentBounds) {
1611
- updateOverlayPosition(null, true);
1612
- updated = true;
1613
- }
1614
-
1615
- if (getSingleSelectedImageNodeId()) {
1616
- updateSingleImageOverlay(true);
1617
- updated = true;
1618
- }
1619
-
1620
- return updated;
1621
- }
1622
- // ▲▲▲ [Perf] ▲▲▲
1591
+
1592
+ updateOverlayPosition(selectedIds);
1593
+ }
1594
+
1595
+ function update() {
1596
+ if (!_module) return;
1597
+ updateMultiSelectOverlay();
1598
+ updateSingleImageOverlay();
1599
+ }
1600
+
1601
+ // Camera-only fast path: currentBounds and node world frames are already
1602
+ // canonical, so reproject only the fixed overlays. This deliberately avoids
1603
+ // selected-node scans, DOM layout reads, menu work, and renderer rebuilds.
1604
+ function updateForCameraMotion() {
1605
+ if (!_module || !_module.camera || !_module.container) return false;
1606
+
1607
+ let updated = false;
1608
+ if (multiSelectOverlay &&
1609
+ multiSelectOverlay.style.display !== 'none' &&
1610
+ currentBounds) {
1611
+ updateOverlayPosition(null, true);
1612
+ updated = true;
1613
+ }
1614
+
1615
+ if (getSingleSelectedImageNodeId()) {
1616
+ updateSingleImageOverlay(true);
1617
+ updated = true;
1618
+ }
1619
+
1620
+ return updated;
1621
+ }
1622
+ // ▲▲▲ [Perf] ▲▲▲
1623
1623
 
1624
1624
  // ▼▼▼ [New] Dynamic menu button creation based on selection type ▼▼▼
1625
1625
  function updateMenuButtons() {
@@ -1639,24 +1639,24 @@
1639
1639
  // Copy is always available
1640
1640
  buttons.push({ icon: 'fa-regular fa-copy', title: 'Copy', action: 'copy', class: 'js-btn-multi-copy' });
1641
1641
 
1642
- const isMixedImageTextSelection = hasImages && hasText;
1643
-
1644
- if (hasImages && !isMixedImageTextSelection) {
1645
- // Image layout organization actions
1646
- buttons.push({ icon: 'fa-solid fa-table-cells-large', title: 'Tidy masonry', action: 'masonry', class: 'js-btn-multi-masonry' });
1647
- buttons.push({ icon: 'fa-solid fa-border-all', title: 'Tidy grid', action: 'grid', class: 'js-btn-multi-grid' });
1648
- buttons.push({ icon: 'fa-solid fa-grip-lines', title: 'Tidy strip', action: 'stripHorizontal', class: 'js-btn-multi-strip' });
1642
+ const isMixedImageTextSelection = hasImages && hasText;
1643
+
1644
+ if (hasImages && !isMixedImageTextSelection) {
1645
+ // Image layout organization actions
1646
+ buttons.push({ icon: 'fa-solid fa-table-cells-large', title: 'Tidy masonry', action: 'masonry', class: 'js-btn-multi-masonry' });
1647
+ buttons.push({ icon: 'fa-solid fa-border-all', title: 'Tidy grid', action: 'grid', class: 'js-btn-multi-grid' });
1648
+ buttons.push({ icon: 'fa-solid fa-grip-lines', title: 'Tidy strip', action: 'stripHorizontal', class: 'js-btn-multi-strip' });
1649
1649
 
1650
1650
  // Stack is only for image-only selection
1651
1651
  if (!hasText) {
1652
1652
  buttons.push({ icon: 'fa-solid fa-layer-group', title: 'Stack into pile', action: 'stack', class: 'js-btn-multi-stack' });
1653
1653
  }
1654
1654
  }
1655
-
1656
- // Text-specific actions
1657
- if (hasText && !isMixedImageTextSelection) {
1658
- buttons.push({ icon: 'fa-solid fa-arrows-up-down', title: 'Fit text height', action: 'expand', class: 'js-btn-multi-expand' });
1659
- }
1655
+
1656
+ // Text-specific actions
1657
+ if (hasText && !isMixedImageTextSelection) {
1658
+ buttons.push({ icon: 'fa-solid fa-arrows-up-down', title: 'Fit text height', action: 'expand', class: 'js-btn-multi-expand' });
1659
+ }
1660
1660
 
1661
1661
  buttons.push({ separator: true });
1662
1662
  buttons.push({ icon: 'fa-solid fa-trash', title: 'Delete all', action: 'delete', class: 'js-btn-multi-delete delete-btn' });
@@ -1703,37 +1703,37 @@
1703
1703
  function checkAndUpdateDisplay() {
1704
1704
  if (!_module) return;
1705
1705
 
1706
- const nodeIds = getSelectedNodeIds();
1707
-
1708
- if (nodeIds.length >= 2) {
1709
- updateMenuButtons(); // Update menu based on selection type
1710
- showMultiSelectBounds(nodeIds);
1711
- hideSingleImageOverlay();
1712
- // Hide single-node menu
1713
- if (window.MindMapMenuManager) {
1714
- window.MindMapMenuManager.hideMenu();
1715
- }
1716
- } else {
1717
- if (multiSelectOverlay) {
1718
- multiSelectOverlay.style.display = 'none';
1719
- multiSelectOverlay.style.visibility = 'hidden';
1720
- }
1721
- currentBounds = null;
1722
- initialNodeSizes.clear();
1723
- updateSingleImageOverlay();
1724
- }
1725
- }
1706
+ const nodeIds = getSelectedNodeIds();
1707
+
1708
+ if (nodeIds.length >= 2) {
1709
+ updateMenuButtons(); // Update menu based on selection type
1710
+ showMultiSelectBounds(nodeIds);
1711
+ hideSingleImageOverlay();
1712
+ // Hide single-node menu
1713
+ if (window.MindMapMenuManager) {
1714
+ window.MindMapMenuManager.hideMenu();
1715
+ }
1716
+ } else {
1717
+ if (multiSelectOverlay) {
1718
+ multiSelectOverlay.style.display = 'none';
1719
+ multiSelectOverlay.style.visibility = 'hidden';
1720
+ }
1721
+ currentBounds = null;
1722
+ initialNodeSizes.clear();
1723
+ updateSingleImageOverlay();
1724
+ }
1725
+ }
1726
1726
 
1727
1727
  window.MindMapMultiSelect = {
1728
1728
  init,
1729
- showMultiSelectBounds,
1730
- hide,
1731
- update,
1732
- updateForCameraMotion,
1733
- checkAndUpdateDisplay,
1734
- getSelectedImageNodeIds,
1735
- fitTextNodesToContentAndReflowY,
1736
- isResizing: () => isResizing,
1729
+ showMultiSelectBounds,
1730
+ hide,
1731
+ update,
1732
+ updateForCameraMotion,
1733
+ checkAndUpdateDisplay,
1734
+ getSelectedImageNodeIds,
1735
+ fitTextNodesToContentAndReflowY,
1736
+ isResizing: () => isResizing,
1737
1737
  handleResizeMove: onResizeMove,
1738
1738
  handleResizeEnd: onResizeEnd
1739
1739
  };