@mindexec/cli 0.2.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 (232) hide show
  1. package/README.md +275 -0
  2. package/codex-runtime.js +960 -0
  3. package/launch-bridge.cjs +162 -0
  4. package/package.json +61 -0
  5. package/port-guard.cjs +232 -0
  6. package/scripts/setup-tree-sitter-grammars.mjs +59 -0
  7. package/server.js +8422 -0
  8. package/start-bridge.bat +32 -0
  9. package/start-bridge.sh +81 -0
  10. package/tree-sitter-grammars/README.md +18 -0
  11. package/tree-sitter-grammars/tree-sitter-c_sharp.wasm +0 -0
  12. package/tree-sitter-grammars/tree-sitter-go.wasm +0 -0
  13. package/tree-sitter-grammars/tree-sitter-java.wasm +0 -0
  14. package/tree-sitter-grammars/tree-sitter-javascript.wasm +0 -0
  15. package/tree-sitter-grammars/tree-sitter-python.wasm +0 -0
  16. package/tree-sitter-grammars/tree-sitter-rust.wasm +0 -0
  17. package/tree-sitter-grammars/tree-sitter-tsx.wasm +0 -0
  18. package/tree-sitter-grammars/tree-sitter-typescript.wasm +0 -0
  19. package/wwwroot/MindExecution.Web.styles.css +3 -0
  20. package/wwwroot/_content/MindExecution.Plugins.Admin/css/admin-dashboard.css +546 -0
  21. package/wwwroot/_content/MindExecution.Plugins.Directory/MindExecution.Plugins.Directory.u7utcng611.bundle.scp.css +7 -0
  22. package/wwwroot/_content/MindExecution.Plugins.Directory/background.png +0 -0
  23. package/wwwroot/_content/MindExecution.Plugins.Directory/directory-manager.js +202 -0
  24. package/wwwroot/_content/MindExecution.Plugins.Directory/exampleJsInterop.js +6 -0
  25. package/wwwroot/_content/MindExecution.Plugins.YouTube/css/youtube-search.css +251 -0
  26. package/wwwroot/_content/MindExecution.Shared/MindExecution.Shared.wsano1j4wp.bundle.scp.css +4 -0
  27. package/wwwroot/_content/MindExecution.Shared/css/admin-dashboard.css +559 -0
  28. package/wwwroot/_content/MindExecution.Shared/css/app.css +1 -0
  29. package/wwwroot/_content/MindExecution.Shared/css/mind-map-overrides.css +2936 -0
  30. package/wwwroot/_content/MindExecution.Shared/fonts/NotoSansKR-Bold.ttf +0 -0
  31. package/wwwroot/_content/MindExecution.Shared/fonts/NotoSansKR-Regular.ttf +0 -0
  32. package/wwwroot/_content/MindExecution.Shared/js/agent-visualization.js +359 -0
  33. package/wwwroot/_content/MindExecution.Shared/js/background-themes.js +1721 -0
  34. package/wwwroot/_content/MindExecution.Shared/js/code-master.js +8316 -0
  35. package/wwwroot/_content/MindExecution.Shared/js/file-system-helper.js +639 -0
  36. package/wwwroot/_content/MindExecution.Shared/js/helpers/InfiniteGridHelper.js +109 -0
  37. package/wwwroot/_content/MindExecution.Shared/js/marked.min.js +69 -0
  38. package/wwwroot/_content/MindExecution.Shared/js/mind-map-core.js +7982 -0
  39. package/wwwroot/_content/MindExecution.Shared/js/mind-map-core.js.backup +1059 -0
  40. package/wwwroot/_content/MindExecution.Shared/js/mind-map-css3d-manager.js +15803 -0
  41. package/wwwroot/_content/MindExecution.Shared/js/mind-map-dev-guards.js +325 -0
  42. package/wwwroot/_content/MindExecution.Shared/js/mind-map-dnd.js +1430 -0
  43. package/wwwroot/_content/MindExecution.Shared/js/mind-map-dnd.js.bak +434 -0
  44. package/wwwroot/_content/MindExecution.Shared/js/mind-map-glow-shader.js +260 -0
  45. package/wwwroot/_content/MindExecution.Shared/js/mind-map-interactions.js +7640 -0
  46. package/wwwroot/_content/MindExecution.Shared/js/mind-map-lod-plan-worker.js +160 -0
  47. package/wwwroot/_content/MindExecution.Shared/js/mind-map-lod-renderer.js +9923 -0
  48. package/wwwroot/_content/MindExecution.Shared/js/mind-map-logic-workers.js +977 -0
  49. package/wwwroot/_content/MindExecution.Shared/js/mind-map-menu-manager.js +1431 -0
  50. package/wwwroot/_content/MindExecution.Shared/js/mind-map-multi-select.js +1716 -0
  51. package/wwwroot/_content/MindExecution.Shared/js/mind-map-node-search-worker.js +553 -0
  52. package/wwwroot/_content/MindExecution.Shared/js/mind-map-nodes.js +4541 -0
  53. package/wwwroot/_content/MindExecution.Shared/js/mind-map-object-manager.js +489 -0
  54. package/wwwroot/_content/MindExecution.Shared/js/mind-map-object-manager.js.backup +372 -0
  55. package/wwwroot/_content/MindExecution.Shared/js/mind-map-pipeline.js +2075 -0
  56. package/wwwroot/_content/MindExecution.Shared/js/mind-map-text-lod-system.js +646 -0
  57. package/wwwroot/_content/MindExecution.Shared/js/mind-map-text-overlay-v2.js +4323 -0
  58. package/wwwroot/_content/MindExecution.Shared/js/mind-map-texture-factory.js +2260 -0
  59. package/wwwroot/_content/MindExecution.Shared/js/mind-map-texture-factory.js.backup +1258 -0
  60. package/wwwroot/_content/MindExecution.Shared/js/mind-map-visibility-worker.js +890 -0
  61. package/wwwroot/_content/MindExecution.Shared/js/mindmap-toolbar.js +594 -0
  62. package/wwwroot/_content/MindExecution.Shared/js/native-drop-handler.js +170 -0
  63. package/wwwroot/_content/MindExecution.Shared/js/plan-master.js +788 -0
  64. package/wwwroot/_content/MindExecution.Shared/js/renderers/CSS3DRenderer.js +50 -0
  65. package/wwwroot/_content/MindExecution.Shared/js/texture-worker-manager.js +188 -0
  66. package/wwwroot/_content/MindExecution.Shared/js/texture-worker.js +208 -0
  67. package/wwwroot/_content/MindExecution.Shared/js/three.min.js +6 -0
  68. package/wwwroot/_content/MindExecution.Shared/js/titlebar-handler.js +191 -0
  69. package/wwwroot/_content/MindExecution.Shared/js/token-manager.js +37 -0
  70. package/wwwroot/_content/MindExecution.Shared/js/token-worker.js +28 -0
  71. package/wwwroot/_content/MindExecution.Shared/js/troika-bundle.js +5626 -0
  72. package/wwwroot/_content/MindExecution.Shared/js/troika-bundle.js.map +7 -0
  73. package/wwwroot/_content/MindExecution.Shared/lib/font-awesome/css/all.min.css +9 -0
  74. package/wwwroot/_content/MindExecution.Shared/lib/font-awesome/webfonts/fa-brands-400.ttf +0 -0
  75. package/wwwroot/_content/MindExecution.Shared/lib/font-awesome/webfonts/fa-brands-400.woff2 +0 -0
  76. package/wwwroot/_content/MindExecution.Shared/lib/font-awesome/webfonts/fa-regular-400.ttf +0 -0
  77. package/wwwroot/_content/MindExecution.Shared/lib/font-awesome/webfonts/fa-regular-400.woff2 +0 -0
  78. package/wwwroot/_content/MindExecution.Shared/lib/font-awesome/webfonts/fa-solid-900.ttf +0 -0
  79. package/wwwroot/_content/MindExecution.Shared/lib/font-awesome/webfonts/fa-solid-900.woff2 +0 -0
  80. package/wwwroot/_content/MindExecution.Shared/models/all-MiniLM-L6-v2-quantized.onnx +0 -0
  81. package/wwwroot/_content/MindExecution.Shared/models/vocab.txt +30522 -0
  82. package/wwwroot/_framework/Google.Protobuf.9h59ukbel7.dll +0 -0
  83. package/wwwroot/_framework/Markdig.d1j7v41cl1.dll +0 -0
  84. package/wwwroot/_framework/MessagePack.Annotations.l6qv48kgpt.dll +0 -0
  85. package/wwwroot/_framework/MessagePack.eqoptzx9d5.dll +0 -0
  86. package/wwwroot/_framework/Microsoft.AspNetCore.Authorization.k7dsih5y5g.dll +0 -0
  87. package/wwwroot/_framework/Microsoft.AspNetCore.Components.6nyje9sa0g.dll +0 -0
  88. package/wwwroot/_framework/Microsoft.AspNetCore.Components.Authorization.iycd6unprw.dll +0 -0
  89. package/wwwroot/_framework/Microsoft.AspNetCore.Components.Web.487u3twia4.dll +0 -0
  90. package/wwwroot/_framework/Microsoft.AspNetCore.Components.WebAssembly.d0gcnmlxxz.dll +0 -0
  91. package/wwwroot/_framework/Microsoft.AspNetCore.Metadata.h4yevl9adi.dll +0 -0
  92. package/wwwroot/_framework/Microsoft.CSharp.qrvp77qmhs.dll +0 -0
  93. package/wwwroot/_framework/Microsoft.Data.Sqlite.jdlxgv2jtg.dll +0 -0
  94. package/wwwroot/_framework/Microsoft.EntityFrameworkCore.4gjazp7kjf.dll +0 -0
  95. package/wwwroot/_framework/Microsoft.EntityFrameworkCore.Abstractions.gocudnvz7b.dll +0 -0
  96. package/wwwroot/_framework/Microsoft.EntityFrameworkCore.Relational.lt4rsvinuo.dll +0 -0
  97. package/wwwroot/_framework/Microsoft.EntityFrameworkCore.Sqlite.69luj0fa9r.dll +0 -0
  98. package/wwwroot/_framework/Microsoft.Extensions.Caching.Abstractions.364t4jh3zz.dll +0 -0
  99. package/wwwroot/_framework/Microsoft.Extensions.Caching.Memory.izlxhpzosu.dll +0 -0
  100. package/wwwroot/_framework/Microsoft.Extensions.Configuration.8zq7hh41o7.dll +0 -0
  101. package/wwwroot/_framework/Microsoft.Extensions.Configuration.Abstractions.8if74zs6ea.dll +0 -0
  102. package/wwwroot/_framework/Microsoft.Extensions.Configuration.Json.duvlngw8i0.dll +0 -0
  103. package/wwwroot/_framework/Microsoft.Extensions.DependencyInjection.Abstractions.t2hh9kvx0o.dll +0 -0
  104. package/wwwroot/_framework/Microsoft.Extensions.DependencyInjection.n4tg99oy8l.dll +0 -0
  105. package/wwwroot/_framework/Microsoft.Extensions.DependencyModel.h0d06ixk3e.dll +0 -0
  106. package/wwwroot/_framework/Microsoft.Extensions.Logging.Abstractions.rl32bkx2sd.dll +0 -0
  107. package/wwwroot/_framework/Microsoft.Extensions.Logging.dlht1xei0t.dll +0 -0
  108. package/wwwroot/_framework/Microsoft.Extensions.Options.qeunebioml.dll +0 -0
  109. package/wwwroot/_framework/Microsoft.Extensions.Primitives.18cr6vnuuz.dll +0 -0
  110. package/wwwroot/_framework/Microsoft.IO.RecyclableMemoryStream.r915vovvw4.dll +0 -0
  111. package/wwwroot/_framework/Microsoft.IdentityModel.Abstractions.1ejljk3erv.dll +0 -0
  112. package/wwwroot/_framework/Microsoft.IdentityModel.JsonWebTokens.1596zr8gne.dll +0 -0
  113. package/wwwroot/_framework/Microsoft.IdentityModel.Logging.229uyvpgio.dll +0 -0
  114. package/wwwroot/_framework/Microsoft.IdentityModel.Tokens.9sibtajc9f.dll +0 -0
  115. package/wwwroot/_framework/Microsoft.JSInterop.17lq4j1j7g.dll +0 -0
  116. package/wwwroot/_framework/Microsoft.JSInterop.WebAssembly.ryia5gxiad.dll +0 -0
  117. package/wwwroot/_framework/Microsoft.ML.OnnxRuntime.w9deo1m5ss.dll +0 -0
  118. package/wwwroot/_framework/Microsoft.ML.Tokenizers.cm2vuv2z61.dll +0 -0
  119. package/wwwroot/_framework/Microsoft.NET.StringTools.3qbrf4v2ki.dll +0 -0
  120. package/wwwroot/_framework/MimeMapping.og9ys58ylm.dll +0 -0
  121. package/wwwroot/_framework/MindExecution.Core.1q1trifbuu.dll +0 -0
  122. package/wwwroot/_framework/MindExecution.Kernel.gwwc40sc45.dll +0 -0
  123. package/wwwroot/_framework/MindExecution.Plugins.Admin.0jgrn1sckv.dll +0 -0
  124. package/wwwroot/_framework/MindExecution.Plugins.Business.13mme2qcag.dll +0 -0
  125. package/wwwroot/_framework/MindExecution.Plugins.Concept.dfp2mdt45q.dll +0 -0
  126. package/wwwroot/_framework/MindExecution.Plugins.Directory.3w4t6n3se0.dll +0 -0
  127. package/wwwroot/_framework/MindExecution.Plugins.PlanMaster.s0qpntz420.dll +0 -0
  128. package/wwwroot/_framework/MindExecution.Plugins.YouTube.iu11fq8d16.dll +0 -0
  129. package/wwwroot/_framework/MindExecution.Shared.7j27dcqnrc.dll +0 -0
  130. package/wwwroot/_framework/MindExecution.Web.pq1ty8ov2v.dll +0 -0
  131. package/wwwroot/_framework/Newtonsoft.Json.a56zs13vug.dll +0 -0
  132. package/wwwroot/_framework/SQLitePCLRaw.batteries_v2.rrd1nzawpp.dll +0 -0
  133. package/wwwroot/_framework/SQLitePCLRaw.core.1dxloztpfz.dll +0 -0
  134. package/wwwroot/_framework/SQLitePCLRaw.provider.e_sqlite3.oekyzl53i1.dll +0 -0
  135. package/wwwroot/_framework/Supabase.Core.s1pkj4aj0l.dll +0 -0
  136. package/wwwroot/_framework/Supabase.Functions.qz4nu782sg.dll +0 -0
  137. package/wwwroot/_framework/Supabase.Gotrue.twah27pkik.dll +0 -0
  138. package/wwwroot/_framework/Supabase.Postgrest.gmuuv369ih.dll +0 -0
  139. package/wwwroot/_framework/Supabase.Realtime.ox3kchdy3w.dll +0 -0
  140. package/wwwroot/_framework/Supabase.Storage.fnjnepaowr.dll +0 -0
  141. package/wwwroot/_framework/Supabase.azmaw5pgcz.dll +0 -0
  142. package/wwwroot/_framework/System.Collections.Concurrent.y1zmvuyipi.dll +0 -0
  143. package/wwwroot/_framework/System.Collections.Immutable.ug3j698qms.dll +0 -0
  144. package/wwwroot/_framework/System.Collections.NonGeneric.h66hj3863h.dll +0 -0
  145. package/wwwroot/_framework/System.Collections.Specialized.umr3y27ntj.dll +0 -0
  146. package/wwwroot/_framework/System.Collections.x53e19vfsj.dll +0 -0
  147. package/wwwroot/_framework/System.ComponentModel.Annotations.tz6gnt4ebt.dll +0 -0
  148. package/wwwroot/_framework/System.ComponentModel.Primitives.j7tiphu4rg.dll +0 -0
  149. package/wwwroot/_framework/System.ComponentModel.TypeConverter.ujlztox1gx.dll +0 -0
  150. package/wwwroot/_framework/System.ComponentModel.x9xz0ojfb6.dll +0 -0
  151. package/wwwroot/_framework/System.Console.ijzpqmj7ne.dll +0 -0
  152. package/wwwroot/_framework/System.Data.Common.1r0sqffq1p.dll +0 -0
  153. package/wwwroot/_framework/System.Diagnostics.DiagnosticSource.9upoqwq09o.dll +0 -0
  154. package/wwwroot/_framework/System.Diagnostics.Process.m99azzntjm.dll +0 -0
  155. package/wwwroot/_framework/System.Diagnostics.TraceSource.pl7wv26myr.dll +0 -0
  156. package/wwwroot/_framework/System.Diagnostics.Tracing.crlhfx6tut.dll +0 -0
  157. package/wwwroot/_framework/System.Drawing.Primitives.22e4y9ikq9.dll +0 -0
  158. package/wwwroot/_framework/System.Drawing.mi7d8hwowb.dll +0 -0
  159. package/wwwroot/_framework/System.Formats.Asn1.jx23sjiqnn.dll +0 -0
  160. package/wwwroot/_framework/System.IO.Compression.6fyoii3uej.dll +0 -0
  161. package/wwwroot/_framework/System.IO.Pipelines.vg77t4cd4d.dll +0 -0
  162. package/wwwroot/_framework/System.IdentityModel.Tokens.Jwt.t67es60z5b.dll +0 -0
  163. package/wwwroot/_framework/System.Linq.1bkoxlqgmq.dll +0 -0
  164. package/wwwroot/_framework/System.Linq.Expressions.24xqiypwdt.dll +0 -0
  165. package/wwwroot/_framework/System.Linq.Queryable.hvd01d6rsa.dll +0 -0
  166. package/wwwroot/_framework/System.Memory.8dx3lwgym4.dll +0 -0
  167. package/wwwroot/_framework/System.Net.Http.Json.3mhdm9l1rf.dll +0 -0
  168. package/wwwroot/_framework/System.Net.Http.eitrz660my.dll +0 -0
  169. package/wwwroot/_framework/System.Net.NetworkInformation.3pkuofcv9r.dll +0 -0
  170. package/wwwroot/_framework/System.Net.Ping.8clj5pklrp.dll +0 -0
  171. package/wwwroot/_framework/System.Net.Primitives.qrp4wcjz1p.dll +0 -0
  172. package/wwwroot/_framework/System.Net.WebSockets.Client.2u6pv01g69.dll +0 -0
  173. package/wwwroot/_framework/System.Net.WebSockets.qp6u31zvm5.dll +0 -0
  174. package/wwwroot/_framework/System.Numerics.Tensors.0c7z4mt3on.dll +0 -0
  175. package/wwwroot/_framework/System.Numerics.Vectors.kc7ufp2j4l.dll +0 -0
  176. package/wwwroot/_framework/System.ObjectModel.qv82fot1ib.dll +0 -0
  177. package/wwwroot/_framework/System.Private.CoreLib.rkafq04oma.dll +0 -0
  178. package/wwwroot/_framework/System.Private.Uri.t9542hmr6j.dll +0 -0
  179. package/wwwroot/_framework/System.Private.Xml.Linq.n8n3ptrbwu.dll +0 -0
  180. package/wwwroot/_framework/System.Private.Xml.rxd3tytisn.dll +0 -0
  181. package/wwwroot/_framework/System.Reactive.t3fuon548l.dll +0 -0
  182. package/wwwroot/_framework/System.Reflection.Emit.9tjhp6y0j3.dll +0 -0
  183. package/wwwroot/_framework/System.Reflection.Emit.ILGeneration.stxyk8zoo1.dll +0 -0
  184. package/wwwroot/_framework/System.Reflection.Emit.Lightweight.6xrd5v8vg0.dll +0 -0
  185. package/wwwroot/_framework/System.Reflection.Primitives.wgn8fpwwvv.dll +0 -0
  186. package/wwwroot/_framework/System.Runtime.InteropServices.JavaScript.sliym526xh.dll +0 -0
  187. package/wwwroot/_framework/System.Runtime.InteropServices.RuntimeInformation.oji7zut14z.dll +0 -0
  188. package/wwwroot/_framework/System.Runtime.InteropServices.te07xr2we9.dll +0 -0
  189. package/wwwroot/_framework/System.Runtime.Intrinsics.507y4h8nzq.dll +0 -0
  190. package/wwwroot/_framework/System.Runtime.Loader.v7gk4bse0k.dll +0 -0
  191. package/wwwroot/_framework/System.Runtime.Numerics.eqy5xjv3nd.dll +0 -0
  192. package/wwwroot/_framework/System.Runtime.Serialization.Formatters.zpkrub8lab.dll +0 -0
  193. package/wwwroot/_framework/System.Runtime.Serialization.Primitives.vhkpnbxjip.dll +0 -0
  194. package/wwwroot/_framework/System.Runtime.jn319d5nyg.dll +0 -0
  195. package/wwwroot/_framework/System.Security.Claims.0ztig1q9vo.dll +0 -0
  196. package/wwwroot/_framework/System.Security.Cryptography.vttizqc9ho.dll +0 -0
  197. package/wwwroot/_framework/System.Text.Encoding.Extensions.utdd47ny8f.dll +0 -0
  198. package/wwwroot/_framework/System.Text.Encodings.Web.wah8r1zoe0.dll +0 -0
  199. package/wwwroot/_framework/System.Text.Json.kxlfxj0wrs.dll +0 -0
  200. package/wwwroot/_framework/System.Text.RegularExpressions.dbqn58klox.dll +0 -0
  201. package/wwwroot/_framework/System.Threading.42ao9vi047.dll +0 -0
  202. package/wwwroot/_framework/System.Threading.Channels.hfa7j0uv2w.dll +0 -0
  203. package/wwwroot/_framework/System.Threading.Thread.caul0pdqul.dll +0 -0
  204. package/wwwroot/_framework/System.Transactions.Local.fimi2hamzo.dll +0 -0
  205. package/wwwroot/_framework/System.Web.HttpUtility.gq8yz50p2e.dll +0 -0
  206. package/wwwroot/_framework/System.Xml.Linq.kitin4zjoj.dll +0 -0
  207. package/wwwroot/_framework/System.Xml.ReaderWriter.kzvw3qgxb0.dll +0 -0
  208. package/wwwroot/_framework/System.Xml.XDocument.c539ki6cuq.dll +0 -0
  209. package/wwwroot/_framework/System.m05i39uvk9.dll +0 -0
  210. package/wwwroot/_framework/Websocket.Client.vapounvmnl.dll +0 -0
  211. package/wwwroot/_framework/blazor.boot.json +305 -0
  212. package/wwwroot/_framework/blazor.webassembly.js +1 -0
  213. package/wwwroot/_framework/dotnet.js +4 -0
  214. package/wwwroot/_framework/dotnet.native.vz0adxojrz.wasm +0 -0
  215. package/wwwroot/_framework/dotnet.native.xsn1d6x2kd.js +16 -0
  216. package/wwwroot/_framework/dotnet.runtime.dstopyvqzi.js +4 -0
  217. package/wwwroot/_framework/icudt_CJK.tjcz0u77k5.dat +0 -0
  218. package/wwwroot/_framework/icudt_EFIGS.tptq2av103.dat +0 -0
  219. package/wwwroot/_framework/icudt_no_CJK.lfu7j35m59.dat +0 -0
  220. package/wwwroot/_framework/netstandard.0xet7jg7ky.dll +0 -0
  221. package/wwwroot/_headers +40 -0
  222. package/wwwroot/_redirects +1 -0
  223. package/wwwroot/appsettings.json +71 -0
  224. package/wwwroot/icon-192.png +0 -0
  225. package/wwwroot/icon-512.png +0 -0
  226. package/wwwroot/index.html +710 -0
  227. package/wwwroot/js/marketing-tool.js +180 -0
  228. package/wwwroot/manifest.webmanifest +22 -0
  229. package/wwwroot/robots.txt +4 -0
  230. package/wwwroot/service-worker-assets.js +857 -0
  231. package/wwwroot/service-worker.js +33 -0
  232. package/wwwroot/sitemap.xml +27 -0
@@ -0,0 +1,1716 @@
1
+ // File: wwwroot/js/mind-map-multi-select.js
2
+ // Multi-image selection bounding box and layout management
3
+ (function () {
4
+ 'use strict';
5
+
6
+ let _module = null;
7
+ let multiSelectOverlay = null;
8
+ let multiSelectMenu = null;
9
+ let singleImageOverlay = null;
10
+ let currentBounds = null;
11
+ let isResizing = false;
12
+ let resizeHandle = null;
13
+ let resizeStartBounds = null;
14
+ let resizeStartMouse = null;
15
+ let initialNodeSizes = new Map(); // Store initial sizes for proportional scaling
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;
56
+ z-index: 1000;
57
+ `;
58
+
59
+ // Create resize handles (corners)
60
+ const corners = ['nw', 'ne', 'sw', 'se'];
61
+ corners.forEach(corner => {
62
+ const handle = document.createElement('div');
63
+ handle.className = `resize-handle-${corner}`;
64
+ handle.dataset.corner = corner;
65
+ handle.style.cssText = `
66
+ position: absolute;
67
+ width: 12px;
68
+ height: 12px;
69
+ background: #2563eb;
70
+ border: 2px solid white;
71
+ border-radius: 3px;
72
+ cursor: ${corner}-resize;
73
+ pointer-events: auto;
74
+ `;
75
+ // Position based on corner
76
+ if (corner.includes('n')) handle.style.top = '-6px';
77
+ if (corner.includes('s')) handle.style.bottom = '-6px';
78
+ if (corner.includes('w')) handle.style.left = '-6px';
79
+ if (corner.includes('e')) handle.style.right = '-6px';
80
+
81
+ handle.addEventListener('mousedown', onResizeStart);
82
+ multiSelectOverlay.appendChild(handle);
83
+ });
84
+
85
+ // Create multi-select menu
86
+ multiSelectMenu = document.createElement('div');
87
+ multiSelectMenu.className = 'multi-select-menu';
88
+ multiSelectMenu.style.cssText = `
89
+ position: absolute;
90
+ top: -35px;
91
+ left: 50%;
92
+ transform: translateX(-50%);
93
+ display: flex;
94
+ align-items: center;
95
+ gap: 2px;
96
+ background: rgba(255, 255, 255, 0.95);
97
+ backdrop-filter: blur(4px);
98
+ padding: 4px 6px;
99
+ border-radius: 8px;
100
+ box-shadow: 0 4px 20px rgba(0,0,0,0.15), 0 0 0 1px rgba(0,0,0,0.05);
101
+ pointer-events: auto;
102
+ `;
103
+
104
+ // Menu buttons will be populated dynamically based on selection
105
+ // Initial empty menu - will be updated by updateMenuButtons()
106
+
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
+ }
153
+
154
+ // Resize move/end are handled by MindMapInteractions (centralized)
155
+
156
+ function showMultiSelectBounds(selectedImageNodeIds) {
157
+ if (!_module || selectedImageNodeIds.length < 2) {
158
+ hide();
159
+ return;
160
+ }
161
+
162
+ // Calculate bounding box of selected nodes
163
+ let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
164
+
165
+ selectedImageNodeIds.forEach(nodeId => {
166
+ const nodeEntry = _module.nodeObjectsById.get(nodeId);
167
+ if (!nodeEntry) return;
168
+
169
+ const obj = nodeEntry.glObject || nodeEntry.cssObject;
170
+ if (!obj) return;
171
+
172
+ const model = nodeEntry.model;
173
+ const x = obj.position.x;
174
+ const y = obj.position.y;
175
+ const w = model.width || 200;
176
+ const h = model.height || 200;
177
+
178
+ minX = Math.min(minX, x);
179
+ maxX = Math.max(maxX, x + w);
180
+ minY = Math.min(minY, y - h); // Note: y is top, y-h is bottom in our coord system
181
+ maxY = Math.max(maxY, y);
182
+ });
183
+
184
+ if (minX === Infinity) {
185
+ hide();
186
+ return;
187
+ }
188
+
189
+ // Add padding
190
+ const padding = 10;
191
+ minX -= padding;
192
+ minY -= padding;
193
+ maxX += padding;
194
+ maxY += padding;
195
+
196
+ currentBounds = { minX, minY, maxX, maxY };
197
+
198
+ // Store initial node sizes for proportional scaling
199
+ initialNodeSizes.clear();
200
+ selectedImageNodeIds.forEach(nodeId => {
201
+ const nodeEntry = _module.nodeObjectsById.get(nodeId);
202
+ if (nodeEntry) {
203
+ const obj = nodeEntry.glObject || nodeEntry.cssObject;
204
+ if (obj) {
205
+ initialNodeSizes.set(nodeId, {
206
+ x: obj.position.x,
207
+ y: obj.position.y,
208
+ width: nodeEntry.model.width || 200,
209
+ height: nodeEntry.model.height || 200
210
+ });
211
+ }
212
+ }
213
+ });
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;
295
+
296
+ // Convert world corners to screen coordinates
297
+ const topLeft = worldToScreen(currentBounds.minX, currentBounds.maxY, camera, container);
298
+ const bottomRight = worldToScreen(currentBounds.maxX, currentBounds.minY, camera, container);
299
+
300
+ const left = Math.min(topLeft.x, bottomRight.x);
301
+ const top = Math.min(topLeft.y, bottomRight.y);
302
+ const width = Math.abs(bottomRight.x - topLeft.x);
303
+ const height = Math.abs(bottomRight.y - topLeft.y);
304
+
305
+ applyOverlayRect(left, top, left + width, top + height);
306
+ }
307
+
308
+ function worldToScreen(worldX, worldY, camera, container) {
309
+ const THREE = globalThis.THREE;
310
+ const vector = new THREE.Vector3(worldX, worldY, 0);
311
+ vector.project(camera);
312
+
313
+ const halfWidth = container.clientWidth / 2;
314
+ const halfHeight = container.clientHeight / 2;
315
+
316
+ return {
317
+ x: (vector.x * halfWidth) + halfWidth,
318
+ y: -(vector.y * halfHeight) + halfHeight
319
+ };
320
+ }
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() {
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
+ const screenRect = getNodeScreenRect(nodeEntry);
364
+ if (screenRect) {
365
+ const screenWidth = Math.max(0, screenRect.right - screenRect.left);
366
+ const screenHeight = Math.max(0, screenRect.bottom - screenRect.top);
367
+ if (screenWidth > 0 && screenHeight > 0) {
368
+ singleImageOverlay.style.left = `${screenRect.left}px`;
369
+ singleImageOverlay.style.top = `${screenRect.top}px`;
370
+ singleImageOverlay.style.width = `${screenWidth}px`;
371
+ singleImageOverlay.style.height = `${screenHeight}px`;
372
+ singleImageOverlay.style.display = 'block';
373
+ singleImageOverlay.style.visibility = 'visible';
374
+ return;
375
+ }
376
+ }
377
+
378
+ const x = obj.position.x;
379
+ const y = obj.position.y;
380
+ const width = Math.max(1, Number(model.width || obj.userData?.worldWidth || 0) || 1);
381
+ const height = Math.max(1, Number(model.height || obj.userData?.worldHeight || 0) || 1);
382
+
383
+ const topLeft = worldToScreen(x, y, _module.camera, _module.container);
384
+ const bottomRight = worldToScreen(x + width, y - height, _module.camera, _module.container);
385
+
386
+ const left = Math.min(topLeft.x, bottomRight.x);
387
+ const top = Math.min(topLeft.y, bottomRight.y);
388
+ const screenWidth = Math.abs(bottomRight.x - topLeft.x);
389
+ const screenHeight = Math.abs(bottomRight.y - topLeft.y);
390
+
391
+ if (![left, top, screenWidth, screenHeight].every(Number.isFinite) || screenWidth <= 0 || screenHeight <= 0) {
392
+ hideSingleImageOverlay();
393
+ return;
394
+ }
395
+
396
+ singleImageOverlay.style.left = `${left}px`;
397
+ singleImageOverlay.style.top = `${top}px`;
398
+ singleImageOverlay.style.width = `${screenWidth}px`;
399
+ singleImageOverlay.style.height = `${screenHeight}px`;
400
+ singleImageOverlay.style.display = 'block';
401
+ singleImageOverlay.style.visibility = 'visible';
402
+ }
403
+
404
+ function hide() {
405
+ if (multiSelectOverlay) {
406
+ multiSelectOverlay.style.display = 'none';
407
+ multiSelectOverlay.style.visibility = 'hidden';
408
+ }
409
+ hideSingleImageOverlay();
410
+ currentBounds = null;
411
+ initialNodeSizes.clear();
412
+ }
413
+
414
+ function onResizeStart(e) {
415
+ e.stopPropagation();
416
+ e.preventDefault();
417
+
418
+ isResizing = true;
419
+ setBrowserSelectionLock(true, 'resize');
420
+ resizeHandle = e.target.dataset.corner;
421
+ resizeStartBounds = { ...currentBounds };
422
+ resizeStartMouse = { x: e.clientX, y: e.clientY };
423
+
424
+ multiSelectOverlay.style.cursor = `${resizeHandle}-resize`;
425
+ }
426
+
427
+ function onResizeMove(e) {
428
+ if (!isResizing || !resizeHandle || !_module) return;
429
+
430
+ const deltaX = e.clientX - resizeStartMouse.x;
431
+ const deltaY = e.clientY - resizeStartMouse.y;
432
+
433
+ // Convert screen delta to world delta (approximate)
434
+ const camera = _module.camera;
435
+ const container = _module.container;
436
+ const scaleX = (camera.position.z / 1000) * 1.5; // Rough conversion factor
437
+ const scaleY = scaleX;
438
+
439
+ const worldDeltaX = deltaX * scaleX;
440
+ const worldDeltaY = -deltaY * scaleY; // Screen Y is inverted
441
+
442
+ // Update bounds based on which corner is being dragged
443
+ const newBounds = { ...resizeStartBounds };
444
+
445
+ if (resizeHandle.includes('e')) {
446
+ newBounds.maxX = resizeStartBounds.maxX + worldDeltaX;
447
+ }
448
+ if (resizeHandle.includes('w')) {
449
+ newBounds.minX = resizeStartBounds.minX + worldDeltaX;
450
+ }
451
+ if (resizeHandle.includes('s')) {
452
+ newBounds.minY = resizeStartBounds.minY + worldDeltaY;
453
+ }
454
+ if (resizeHandle.includes('n')) {
455
+ newBounds.maxY = resizeStartBounds.maxY + worldDeltaY;
456
+ }
457
+
458
+ // Ensure minimum size
459
+ const minSize = 100;
460
+ if (newBounds.maxX - newBounds.minX < minSize || newBounds.maxY - newBounds.minY < minSize) {
461
+ return;
462
+ }
463
+
464
+ currentBounds = newBounds;
465
+ updateOverlayPosition(null, true);
466
+
467
+ // Scale and reposition selected nodes proportionally
468
+ scaleSelectedNodes();
469
+ }
470
+
471
+ function onResizeEnd(e) {
472
+ if (!isResizing) return;
473
+
474
+ isResizing = false;
475
+ setBrowserSelectionLock(false, 'resize');
476
+ resizeHandle = null;
477
+ resizeStartBounds = null;
478
+ resizeStartMouse = null;
479
+ multiSelectOverlay.style.cursor = '';
480
+
481
+ // Re-apply masonry layout to fit the new bounds
482
+ const selectedIds = getSelectedImageNodeIds();
483
+ if (selectedIds.length > 0) {
484
+ applyTidyLayout('masonry', selectedIds);
485
+ }
486
+ }
487
+
488
+ function scaleSelectedNodes() {
489
+ if (!_module || !resizeStartBounds || !currentBounds || !resizeHandle) return;
490
+
491
+ const oldWidth = resizeStartBounds.maxX - resizeStartBounds.minX;
492
+ const oldHeight = resizeStartBounds.maxY - resizeStartBounds.minY;
493
+ const newWidth = currentBounds.maxX - currentBounds.minX;
494
+ const newHeight = currentBounds.maxY - currentBounds.minY;
495
+
496
+ const scaleX = newWidth / oldWidth;
497
+ const scaleY = newHeight / oldHeight;
498
+
499
+ // ▼▼▼ [Fix] Determine anchor point based on which handle is being dragged ▼▼▼
500
+ // The anchor is the OPPOSITE corner of the handle being dragged
501
+ let anchorX, anchorY, oldAnchorX, oldAnchorY;
502
+
503
+ if (resizeHandle.includes('e')) {
504
+ // Dragging east side: anchor is west (minX)
505
+ anchorX = currentBounds.minX;
506
+ oldAnchorX = resizeStartBounds.minX;
507
+ } else {
508
+ // Dragging west side: anchor is east (maxX)
509
+ anchorX = currentBounds.maxX;
510
+ oldAnchorX = resizeStartBounds.maxX;
511
+ }
512
+
513
+ if (resizeHandle.includes('s')) {
514
+ // Dragging south side: anchor is north (maxY)
515
+ anchorY = currentBounds.maxY;
516
+ oldAnchorY = resizeStartBounds.maxY;
517
+ } else {
518
+ // Dragging north side: anchor is south (minY)
519
+ anchorY = currentBounds.minY;
520
+ oldAnchorY = resizeStartBounds.minY;
521
+ }
522
+ // ▲▲▲ [Fix] ▲▲▲
523
+
524
+ initialNodeSizes.forEach((initialData, nodeId) => {
525
+ const nodeEntry = _module.nodeObjectsById.get(nodeId);
526
+ if (!nodeEntry) return;
527
+
528
+ const obj = nodeEntry.glObject || nodeEntry.cssObject;
529
+ if (!obj) return;
530
+
531
+ // ▼▼▼ [Fix] Calculate relative position from anchor point ▼▼▼
532
+ const relX = (initialData.x - oldAnchorX) / oldWidth;
533
+ const relY = (initialData.y - oldAnchorY) / oldHeight;
534
+
535
+ // New position relative to new anchor
536
+ const newX = anchorX + relX * newWidth;
537
+ const newY = anchorY + relY * newHeight;
538
+ // ▲▲▲ [Fix] ▲▲▲
539
+
540
+ // ▼▼▼ [Fix] Use independent X/Y scaling for free movement ▼▼▼
541
+ const newNodeWidth = Math.max(50, Math.round(initialData.width * scaleX));
542
+ const newNodeHeight = Math.max(50, Math.round(initialData.height * scaleY));
543
+ // ▲▲▲ [Fix] ▲▲▲
544
+
545
+ // Update position
546
+ obj.position.x = newX;
547
+ obj.position.y = newY;
548
+
549
+ // Update model
550
+ nodeEntry.model.x = newX;
551
+ nodeEntry.model.y = newY;
552
+ nodeEntry.model.width = newNodeWidth;
553
+ nodeEntry.model.height = newNodeHeight;
554
+
555
+ // Trigger texture/geometry update
556
+ if (window.MindMapNodes && window.MindMapNodes.updateNodeContent) {
557
+ // Queue texture regeneration (we'll do this after resize ends to avoid lag)
558
+ }
559
+ });
560
+ }
561
+
562
+ async function handleMenuAction(action) {
563
+ if (!_module) return;
564
+
565
+ const selectedIds = getSelectedNodeIds();
566
+ if (selectedIds.length === 0) return;
567
+
568
+ switch (action) {
569
+ case 'copy':
570
+ copySelectedNodes(selectedIds);
571
+ break;
572
+ case 'masonry':
573
+ await applyTidyLayout('masonry');
574
+ break;
575
+ case 'grid':
576
+ await applyTidyLayout('grid');
577
+ break;
578
+ case 'stripHorizontal':
579
+ await applyTidyLayout('stripHorizontal');
580
+ break;
581
+ case 'stack':
582
+ await mergeSelectedImagesIntoStack();
583
+ break;
584
+ case 'expand':
585
+ await fitTextNodesToContentAndReflowY(selectedIds);
586
+ break;
587
+ case 'delete':
588
+ deleteSelectedNodes(selectedIds);
589
+ break;
590
+ }
591
+ }
592
+
593
+ // ▼▼▼ [New] Supported node types for multi-select ▼▼▼
594
+ const SUPPORTED_TYPES = ['image', 'text', 'markdown', 'note', 'code'];
595
+ const TEXT_TYPES = ['text', 'markdown', 'note', 'code'];
596
+ const TEXT_FIT_MAX_HEIGHT = 12000;
597
+ // ▲▲▲ [New] ▲▲▲
598
+
599
+ function getSelectedNodeIds() {
600
+ if (!_module) return [];
601
+
602
+ const nodeIds = [];
603
+ _module.multiSelectedNodeIds.forEach(nodeId => {
604
+ const nodeEntry = _module.nodeObjectsById.get(nodeId);
605
+ if (nodeEntry && nodeEntry.model && SUPPORTED_TYPES.includes(nodeEntry.model.contentType)) {
606
+ nodeIds.push(nodeId);
607
+ }
608
+ });
609
+ return nodeIds;
610
+ }
611
+
612
+ function getSelectedImageNodeIds() {
613
+ if (!_module) return [];
614
+
615
+ const imageNodeIds = [];
616
+ _module.multiSelectedNodeIds.forEach(nodeId => {
617
+ const nodeEntry = _module.nodeObjectsById.get(nodeId);
618
+ if (nodeEntry && nodeEntry.model && nodeEntry.model.contentType === 'image') {
619
+ imageNodeIds.push(nodeId);
620
+ }
621
+ });
622
+ return imageNodeIds;
623
+ }
624
+
625
+ function getSelectedTextNodeIds() {
626
+ if (!_module) return [];
627
+
628
+ const textNodeIds = [];
629
+ _module.multiSelectedNodeIds.forEach(nodeId => {
630
+ const nodeEntry = _module.nodeObjectsById.get(nodeId);
631
+ if (nodeEntry && nodeEntry.model && TEXT_TYPES.includes(nodeEntry.model.contentType)) {
632
+ textNodeIds.push(nodeId);
633
+ }
634
+ });
635
+ return textNodeIds;
636
+ }
637
+
638
+ function quantile(values, q) {
639
+ if (!Array.isArray(values) || values.length === 0) return 0;
640
+
641
+ const sorted = [...values].sort((a, b) => a - b);
642
+ const index = (sorted.length - 1) * q;
643
+ const i0 = Math.floor(index);
644
+ const i1 = Math.min(sorted.length - 1, i0 + 1);
645
+ const t = index - i0;
646
+ return sorted[i0] * (1 - t) + sorted[i1] * t;
647
+ }
648
+
649
+ function getImageNodeRects(nodeIds) {
650
+ return nodeIds.map(nodeId => {
651
+ const entry = _module.nodeObjectsById.get(nodeId);
652
+ if (!entry) return null;
653
+
654
+ const model = entry.model || {};
655
+ return {
656
+ nodeId,
657
+ width: Math.max(1, Number(model.width || 0) || 1),
658
+ height: Math.max(1, Number(model.height || 0) || 1)
659
+ };
660
+ }).filter(Boolean);
661
+ }
662
+
663
+ function buildTidyOptions(mode, nodeIds) {
664
+ const rects = getImageNodeRects(nodeIds);
665
+ const widths = rects.map(r => r.width);
666
+ const heights = rects.map(r => r.height);
667
+
668
+ const q75w = Math.max(120, Math.round(quantile(widths, 0.75) || 280));
669
+ const q75h = Math.max(120, Math.round(quantile(heights, 0.75) || 220));
670
+
671
+ const selectionWidth = currentBounds
672
+ ? Math.max(120, (currentBounds.maxX - currentBounds.minX) - 40)
673
+ : Math.max(120, widths.reduce((sum, w) => sum + w, 0));
674
+
675
+ const options = {
676
+ mode,
677
+ gapX: 24,
678
+ gapY: 24,
679
+ quantile: 0.75,
680
+ order: 'spatialReading',
681
+ anchor: 'selectionBoundsTopLeft',
682
+ maxColumns: 12,
683
+ sizeMode: 'preserve'
684
+ };
685
+
686
+ switch (mode) {
687
+ case 'masonry':
688
+ options.sizeMode = 'uniformWidth';
689
+ options.targetWidth = q75w;
690
+ break;
691
+ case 'grid':
692
+ options.sizeMode = 'fitCellDownOnly';
693
+ options.cellWidth = q75w;
694
+ options.cellHeight = q75h;
695
+ break;
696
+ case 'stripHorizontal':
697
+ options.sizeMode = 'uniformHeight';
698
+ options.targetHeight = q75h;
699
+ break;
700
+ case 'stripVertical':
701
+ options.sizeMode = 'uniformWidth';
702
+ options.targetWidth = q75w;
703
+ break;
704
+ default:
705
+ options.sizeMode = 'preserve';
706
+ break;
707
+ }
708
+
709
+ if (mode === 'grid' || mode === 'masonry') {
710
+ options.columns = 2;
711
+ }
712
+
713
+ return options;
714
+ }
715
+
716
+ function applyLayoutUpdatesToScene(updates) {
717
+ if (!_module || !Array.isArray(updates) || updates.length === 0) return;
718
+
719
+ const resized = [];
720
+
721
+ updates.forEach(update => {
722
+ const nodeId = update.nodeId ?? update.NodeId;
723
+ if (!nodeId) return;
724
+
725
+ const entry = _module.nodeObjectsById.get(nodeId);
726
+ if (!entry || !entry.model) return;
727
+
728
+ const x = Number(update.x ?? update.X ?? entry.model.positionX ?? entry.model.x ?? 0);
729
+ const y = Number(update.y ?? update.Y ?? entry.model.positionY ?? entry.model.y ?? 0);
730
+ const width = Math.max(1, Math.round(Number(update.width ?? update.Width ?? entry.model.width ?? 1)));
731
+ const height = Math.max(1, Math.round(Number(update.height ?? update.Height ?? entry.model.height ?? 1)));
732
+
733
+ const oldWidth = Math.round(Number(entry.model.width || 0));
734
+ const oldHeight = Math.round(Number(entry.model.height || 0));
735
+
736
+ entry.model.positionX = x;
737
+ entry.model.positionY = y;
738
+ entry.model.x = x;
739
+ entry.model.y = y;
740
+ entry.model.width = width;
741
+ entry.model.height = height;
742
+
743
+ if (entry.glObject) {
744
+ entry.glObject.position.x = x;
745
+ entry.glObject.position.y = y;
746
+ }
747
+ if (entry.cssObject) {
748
+ entry.cssObject.position.x = x;
749
+ entry.cssObject.position.y = y;
750
+ }
751
+
752
+ if (window.MindMapNodes?.updateNodeSpatialGrid) {
753
+ window.MindMapNodes.updateNodeSpatialGrid(_module, nodeId);
754
+ }
755
+
756
+ if (oldWidth !== width || oldHeight !== height) {
757
+ resized.push({ nodeId, width, height });
758
+ }
759
+ });
760
+
761
+ resized.forEach(item => {
762
+ MindMapNodes.updateNodeContent(_module, item.nodeId, null, item.width, item.height)
763
+ .catch(err => console.warn('[MultiSelect] Failed to refresh node after tidy:', item.nodeId, err));
764
+ });
765
+
766
+ if (_module.markLODPositionsDirty) {
767
+ _module.markLODPositionsDirty(selectedIds);
768
+ }
769
+ if (resized.length > 0 && _module.markLODDirty) {
770
+ _module.markLODDirty('update-node', resized.map(item => item.nodeId));
771
+ }
772
+ if (_module.updateVisibility && _module.camera) {
773
+ _module.updateVisibility(_module.camera);
774
+ }
775
+ }
776
+
777
+ async function applyTidyLayout(mode, explicitNodeIds = null) {
778
+ if (!_module) return;
779
+
780
+ const selectedIds = (explicitNodeIds && explicitNodeIds.length > 0)
781
+ ? explicitNodeIds
782
+ : getSelectedImageNodeIds();
783
+
784
+ if (selectedIds.length === 0) return;
785
+
786
+ if (!_module.dotNetHelper) {
787
+ if (mode === 'masonry') {
788
+ applyMasonryLayout(selectedIds);
789
+ }
790
+ return;
791
+ }
792
+
793
+ const request = {
794
+ nodeIds: selectedIds,
795
+ options: buildTidyOptions(mode, selectedIds)
796
+ };
797
+
798
+ try {
799
+ const updates = await _module.dotNetHelper.invokeMethodAsync('ApplyTidyLayoutFromJs', request);
800
+ applyLayoutUpdatesToScene(updates);
801
+ showMultiSelectBounds(getSelectedNodeIds());
802
+ } catch (error) {
803
+ console.warn(`[MultiSelect] Tidy layout failed (${mode}), using fallback:`, error);
804
+ if (mode === 'masonry') {
805
+ applyMasonryLayout(selectedIds);
806
+ }
807
+ }
808
+ }
809
+
810
+ async function mergeSelectedImagesIntoStack() {
811
+ if (!_module || !_module.dotNetHelper) return;
812
+
813
+ const selectedIds = getSelectedImageNodeIds();
814
+ if (selectedIds.length < 2) return;
815
+
816
+ try {
817
+ const result = await _module.dotNetHelper.invokeMethodAsync('MergeImageStacksFromJs', {
818
+ nodeIds: selectedIds
819
+ });
820
+
821
+ if (!result) return;
822
+
823
+ const removedIds = result.removedNodeIds || result.RemovedNodeIds || [];
824
+ removedIds.forEach(nodeId => {
825
+ if (!nodeId) return;
826
+ if (_module.multiSelectedNodeIds?.delete) {
827
+ _module.multiSelectedNodeIds.delete(nodeId);
828
+ }
829
+ });
830
+ if (window.MindMapNodes?.removeNodes) {
831
+ window.MindMapNodes.removeNodes(_module, removedIds);
832
+ } else {
833
+ removedIds.forEach(nodeId => window.MindMapNodes?.removeNode?.(_module, nodeId));
834
+ }
835
+
836
+ const primaryNodeId = result.primaryNodeId || result.PrimaryNodeId || null;
837
+ if (primaryNodeId) {
838
+ _module.multiSelectedNodeIds?.clear?.();
839
+ _module.multiSelectedNodeIds?.add?.(primaryNodeId);
840
+ _module.selectedNodeIdJs = primaryNodeId;
841
+ window.MindMapNodes?.clearMultiSelection?.(_module, primaryNodeId);
842
+ window.MindMapNodes?.updateNodeSelectionStyle?.(_module, primaryNodeId, true, true);
843
+ }
844
+
845
+ const remainingSelection = getSelectedNodeIds();
846
+ if (remainingSelection.length >= 2) {
847
+ showMultiSelectBounds(remainingSelection);
848
+ } else {
849
+ hide();
850
+ }
851
+ } catch (error) {
852
+ console.warn('[MultiSelect] Failed to merge image stacks:', error);
853
+ }
854
+ }
855
+
856
+ function copySelectedNodes(selectedIds) {
857
+ // Copy nodes to clipboard (as internal format)
858
+ const copiedNodes = [];
859
+ selectedIds.forEach(nodeId => {
860
+ const nodeEntry = _module.nodeObjectsById.get(nodeId);
861
+ if (nodeEntry?.model) {
862
+ const model = nodeEntry.model;
863
+ copiedNodes.push({
864
+ contentType: model.contentType,
865
+ prompt: model.prompt,
866
+ response: model.response,
867
+ width: model.width,
868
+ height: model.height,
869
+ positionX: model.positionX,
870
+ positionY: model.positionY,
871
+ positionZ: model.positionZ,
872
+ isAiEnabled: model.isAiEnabled !== false,
873
+ isWordWrap: !!model.isWordWrap,
874
+ isChunkedText: model.isChunkedText,
875
+ totalLineCount: model.totalLineCount,
876
+ sourceFilePath: model.sourceFilePath,
877
+ scrollOffset: model.scrollOffset,
878
+ maxScroll: model.maxScroll,
879
+ isExpanded: !!model.isExpanded,
880
+ originalHeight: model.originalHeight,
881
+ metadata: model.metadata
882
+ });
883
+ }
884
+ });
885
+
886
+ _module.copiedNodes = copiedNodes;
887
+ _module.copiedNodesTimestamp = Date.now();
888
+
889
+ // ▼▼▼ [Cross-Tab] Write full node data as JSON to system clipboard ▼▼▼
890
+ try {
891
+ const clipboardPayload = JSON.stringify({
892
+ __type: '__MINDMAP_NODES__',
893
+ nodes: copiedNodes
894
+ });
895
+ navigator.clipboard.writeText(clipboardPayload);
896
+ } catch { }
897
+ // ▲▲▲ [Cross-Tab] ▲▲▲
898
+
899
+ console.log(`[MultiSelect] Copied ${copiedNodes.length} nodes`);
900
+ }
901
+
902
+ function readCssPixelValue(value) {
903
+ const parsed = Number.parseFloat(String(value || '0'));
904
+ return Number.isFinite(parsed) ? parsed : 0;
905
+ }
906
+
907
+ function getVerticalBoxExtras(element) {
908
+ if (!element || typeof window === 'undefined' || !window.getComputedStyle) return 0;
909
+
910
+ const computed = window.getComputedStyle(element);
911
+ return readCssPixelValue(computed.paddingTop)
912
+ + readCssPixelValue(computed.paddingBottom)
913
+ + readCssPixelValue(computed.borderTopWidth)
914
+ + readCssPixelValue(computed.borderBottomWidth);
915
+ }
916
+
917
+ function getVerticalMargins(element) {
918
+ if (!element || typeof window === 'undefined' || !window.getComputedStyle) return 0;
919
+
920
+ const computed = window.getComputedStyle(element);
921
+ return readCssPixelValue(computed.marginTop) + readCssPixelValue(computed.marginBottom);
922
+ }
923
+
924
+ function forceNaturalBlockHeight(element, preserveScrollMetrics = false) {
925
+ if (!element?.style) return;
926
+
927
+ element.style.setProperty('height', 'auto', 'important');
928
+ element.style.setProperty('min-height', '0', 'important');
929
+ element.style.setProperty('max-height', 'none', 'important');
930
+ element.style.setProperty('flex', '0 0 auto', 'important');
931
+
932
+ if (preserveScrollMetrics) {
933
+ // Keep scrollbar-gutter and right padding active so wrapping matches the real CSS3D node.
934
+ element.style.setProperty('overflow-x', 'hidden', 'important');
935
+ element.style.setProperty('overflow-y', 'auto', 'important');
936
+ } else {
937
+ element.style.setProperty('overflow', 'visible', 'important');
938
+ element.style.setProperty('overflow-y', 'visible', 'important');
939
+ }
940
+ }
941
+
942
+ function measureOuterHeight(element, preferScrollHeight = false) {
943
+ if (!element) return 0;
944
+
945
+ let height = 0;
946
+ const push = (value) => {
947
+ const numeric = Number(value);
948
+ if (Number.isFinite(numeric) && numeric > height) {
949
+ height = numeric;
950
+ }
951
+ };
952
+
953
+ if (typeof element.getBoundingClientRect === 'function') {
954
+ push(element.getBoundingClientRect().height);
955
+ }
956
+ push(element.offsetHeight);
957
+ push(element.clientHeight);
958
+ if (preferScrollHeight) {
959
+ push(element.scrollHeight);
960
+ }
961
+
962
+ return Math.ceil(height + getVerticalMargins(element));
963
+ }
964
+
965
+ function measureExpandedHeightFromCssDom(nodeEntry, extraBuffer = 0) {
966
+ const wrapper = nodeEntry?.cssObject?.element;
967
+ const source = wrapper?.firstElementChild;
968
+ if (!source || typeof document === 'undefined') return null;
969
+
970
+ let probe = null;
971
+ try {
972
+ const modelWidth = Math.max(1, Number(nodeEntry.model?.width || 400));
973
+ probe = source.cloneNode(true);
974
+
975
+ if (probe.removeAttribute) probe.removeAttribute('id');
976
+ const idElements = probe.querySelectorAll?.('[id]');
977
+ if (idElements?.length) {
978
+ idElements.forEach(el => el.removeAttribute('id'));
979
+ }
980
+
981
+ Object.assign(probe.style, {
982
+ position: 'absolute',
983
+ left: '-100000px',
984
+ top: '0',
985
+ width: `${modelWidth}px`,
986
+ height: 'auto',
987
+ minHeight: '0',
988
+ maxHeight: 'none',
989
+ overflow: 'visible',
990
+ display: 'block',
991
+ visibility: 'hidden',
992
+ pointerEvents: 'none',
993
+ transform: 'none',
994
+ boxSizing: 'border-box'
995
+ });
996
+
997
+ const contentWrapper = probe.querySelector('.node-content-wrapper');
998
+ if (contentWrapper) {
999
+ forceNaturalBlockHeight(contentWrapper);
1000
+ contentWrapper.style.display = 'block';
1001
+ }
1002
+
1003
+ const contentSelectors = [
1004
+ '[id^="node-response-"]',
1005
+ '.node-response',
1006
+ '.note-content',
1007
+ '.markdown-body',
1008
+ '.code-body',
1009
+ '.code-body-content',
1010
+ '.note-textarea',
1011
+ 'textarea'
1012
+ ].join(', ');
1013
+ const contentElements = Array.from(probe.querySelectorAll(contentSelectors));
1014
+ contentElements.forEach(element => {
1015
+ if (element.style) {
1016
+ element.style.display = element.tagName === 'TEXTAREA' ? 'block' : (element.style.display || 'block');
1017
+ }
1018
+ forceNaturalBlockHeight(element, true);
1019
+ });
1020
+
1021
+ document.body.appendChild(probe);
1022
+
1023
+ const measuredWhole = Math.max(
1024
+ measureOuterHeight(probe, true),
1025
+ Math.ceil(probe.scrollHeight || 0)
1026
+ );
1027
+
1028
+ let measuredByChildren = 0;
1029
+ if (contentWrapper) {
1030
+ measuredByChildren += getVerticalBoxExtras(probe);
1031
+ measuredByChildren += getVerticalBoxExtras(contentWrapper);
1032
+
1033
+ Array.from(contentWrapper.children || []).forEach(child => {
1034
+ if (child instanceof HTMLElement && window.getComputedStyle(child).display === 'none') {
1035
+ return;
1036
+ }
1037
+
1038
+ const isContent = child.matches?.(contentSelectors);
1039
+ measuredByChildren += measureOuterHeight(child, !!isContent);
1040
+ });
1041
+ }
1042
+
1043
+ const measured = Math.ceil(Math.max(measuredWhole, measuredByChildren));
1044
+ if (!Number.isFinite(measured) || measured <= 0) return null;
1045
+
1046
+ return measured + Math.max(0, extraBuffer);
1047
+ } catch {
1048
+ return null;
1049
+ } finally {
1050
+ if (probe && probe.parentNode) {
1051
+ probe.parentNode.removeChild(probe);
1052
+ }
1053
+ }
1054
+ }
1055
+
1056
+ function getTextFitMinHeight(model) {
1057
+ const contentType = String(model?.contentType || '').toLowerCase();
1058
+ if (contentType === 'note') return 96;
1059
+ if (contentType === 'code') return 120;
1060
+ return 88;
1061
+ }
1062
+
1063
+ async function measureTextFitHeight(nodeEntry) {
1064
+ const model = nodeEntry?.model;
1065
+ if (!model) return null;
1066
+
1067
+ const FIT_BUFFER = 18;
1068
+ const minHeight = getTextFitMinHeight(model);
1069
+
1070
+ const domHeight = measureExpandedHeightFromCssDom(nodeEntry, FIT_BUFFER);
1071
+ let factoryHeight = null;
1072
+
1073
+ if (window.MindMapTextureFactory?.measureHtmlHeightAsync) {
1074
+ const zoomLevel = 2;
1075
+ const measureModel = {
1076
+ ...model,
1077
+ response: model.response ?? model.Response ?? model.content ?? ''
1078
+ };
1079
+ const { height: measuredHeight } = await window.MindMapTextureFactory.measureHtmlHeightAsync(
1080
+ measureModel,
1081
+ zoomLevel,
1082
+ (model.width || 400) * zoomLevel,
1083
+ null,
1084
+ TEXT_FIT_MAX_HEIGHT
1085
+ );
1086
+ factoryHeight = measuredHeight + FIT_BUFFER;
1087
+ }
1088
+
1089
+ const requiredHeight = Math.max(
1090
+ Number.isFinite(domHeight) ? domHeight : 0,
1091
+ Number.isFinite(factoryHeight) ? factoryHeight : 0
1092
+ );
1093
+
1094
+ if (!Number.isFinite(requiredHeight) || requiredHeight <= 0) {
1095
+ return null;
1096
+ }
1097
+
1098
+ return Math.min(TEXT_FIT_MAX_HEIGHT, Math.max(minHeight, Math.ceil(requiredHeight)));
1099
+ }
1100
+
1101
+ function readRenderedTextOverflowAdjustment(item) {
1102
+ const entry = item?.entry;
1103
+ const root = entry?.cssObject?.element?.firstElementChild || entry?.cssObject?.element;
1104
+ if (!root) return 0;
1105
+
1106
+ const contentEl = root.querySelector?.('[id^="node-response-"]')
1107
+ || root.querySelector?.('.node-response')
1108
+ || root.querySelector?.('.note-textarea')
1109
+ || root.querySelector?.('textarea');
1110
+ if (!contentEl) return 0;
1111
+
1112
+ const scrollHeight = Math.ceil(Number(contentEl.scrollHeight || 0));
1113
+ const clientHeight = Math.ceil(Number(contentEl.clientHeight || 0));
1114
+ const overflow = scrollHeight - clientHeight;
1115
+ if (!Number.isFinite(overflow) || overflow <= 1) return 0;
1116
+
1117
+ return overflow + 12;
1118
+ }
1119
+
1120
+ function refineTextFitItemsFromRenderedDom(items) {
1121
+ if (!Array.isArray(items) || items.length === 0) return 0;
1122
+
1123
+ let adjusted = 0;
1124
+ for (const item of items) {
1125
+ const correction = readRenderedTextOverflowAdjustment(item);
1126
+ if (correction <= 0) continue;
1127
+
1128
+ item.newHeight = Math.min(TEXT_FIT_MAX_HEIGHT, Math.ceil(item.newHeight + correction));
1129
+ adjusted++;
1130
+ }
1131
+
1132
+ return adjusted;
1133
+ }
1134
+
1135
+ function getNodeFrame(nodeId) {
1136
+ const entry = _module?.nodeObjectsById?.get?.(nodeId);
1137
+ const model = entry?.model;
1138
+ if (!entry || !model) return null;
1139
+
1140
+ const x = Number(model.positionX ?? model.x ?? entry.glObject?.position?.x ?? entry.cssObject?.position?.x ?? 0);
1141
+ const y = Number(model.positionY ?? model.y ?? entry.glObject?.position?.y ?? entry.cssObject?.position?.y ?? 0);
1142
+ const width = Math.max(1, Math.round(Number(model.width || 400)));
1143
+ const height = Math.max(1, Math.round(Number(model.height || 200)));
1144
+
1145
+ return {
1146
+ nodeId,
1147
+ entry,
1148
+ model,
1149
+ x,
1150
+ y,
1151
+ width,
1152
+ height,
1153
+ left: x,
1154
+ right: x + width,
1155
+ centerX: x + width / 2
1156
+ };
1157
+ }
1158
+
1159
+ function horizontalOverlapRatio(a, b) {
1160
+ const overlap = Math.min(a.right, b.right) - Math.max(a.left, b.left);
1161
+ if (overlap <= 0) return 0;
1162
+ return overlap / Math.max(1, Math.min(a.width, b.width));
1163
+ }
1164
+
1165
+ function groupTextFramesByColumn(items) {
1166
+ const groups = [];
1167
+ const ordered = [...items].sort((a, b) => a.left - b.left || b.y - a.y);
1168
+
1169
+ for (const item of ordered) {
1170
+ let target = null;
1171
+ let bestScore = 0;
1172
+
1173
+ for (const group of groups) {
1174
+ const groupFrame = {
1175
+ left: group.left,
1176
+ right: group.right,
1177
+ width: Math.max(1, group.right - group.left)
1178
+ };
1179
+ const overlapScore = horizontalOverlapRatio(item, groupFrame);
1180
+ const centerDistance = Math.abs(item.centerX - group.centerX);
1181
+ const centerThreshold = Math.max(80, Math.min(item.width, groupFrame.width) * 0.35);
1182
+ const score = overlapScore > 0 ? overlapScore : (centerDistance <= centerThreshold ? 0.2 : 0);
1183
+
1184
+ if (score > bestScore) {
1185
+ bestScore = score;
1186
+ target = group;
1187
+ }
1188
+ }
1189
+
1190
+ if (!target || bestScore < 0.15) {
1191
+ target = {
1192
+ items: [],
1193
+ left: item.left,
1194
+ right: item.right,
1195
+ centerX: item.centerX
1196
+ };
1197
+ groups.push(target);
1198
+ }
1199
+
1200
+ target.items.push(item);
1201
+ target.left = Math.min(target.left, item.left);
1202
+ target.right = Math.max(target.right, item.right);
1203
+ target.centerX = target.items.reduce((sum, n) => sum + n.centerX, 0) / target.items.length;
1204
+ }
1205
+
1206
+ return groups;
1207
+ }
1208
+
1209
+ function buildTextFitLayoutUpdates(items) {
1210
+ const DEFAULT_GAP = 24;
1211
+ const MIN_GAP = 12;
1212
+ const groups = groupTextFramesByColumn(items);
1213
+ const updates = [];
1214
+
1215
+ for (const group of groups) {
1216
+ const ordered = [...group.items].sort((a, b) => b.y - a.y || a.x - b.x);
1217
+
1218
+ let previous = null;
1219
+ for (const item of ordered) {
1220
+ let y = item.y;
1221
+
1222
+ if (previous) {
1223
+ const originalGap = previous.y - previous.height - item.y;
1224
+ const gap = Number.isFinite(originalGap) && originalGap >= 0
1225
+ ? Math.max(MIN_GAP, originalGap)
1226
+ : DEFAULT_GAP;
1227
+ y = previous.newY - previous.newHeight - gap;
1228
+ }
1229
+
1230
+ item.newY = y;
1231
+ previous = item;
1232
+
1233
+ updates.push({
1234
+ nodeId: item.nodeId,
1235
+ x: item.x,
1236
+ y,
1237
+ width: item.width,
1238
+ height: item.newHeight
1239
+ });
1240
+ }
1241
+ }
1242
+
1243
+ return updates;
1244
+ }
1245
+
1246
+ async function applyTextFitLayoutUpdates(updates) {
1247
+ if (!_module || !Array.isArray(updates) || updates.length === 0) return;
1248
+
1249
+ const renderPromises = [];
1250
+
1251
+ for (const update of updates) {
1252
+ const entry = _module.nodeObjectsById.get(update.nodeId);
1253
+ if (!entry || !entry.model) continue;
1254
+
1255
+ const width = Math.max(1, Math.round(Number(update.width || entry.model.width || 400)));
1256
+ const height = Math.max(1, Math.round(Number(update.height || entry.model.height || 200)));
1257
+ const x = Number(update.x ?? entry.model.positionX ?? entry.model.x ?? 0);
1258
+ const y = Number(update.y ?? entry.model.positionY ?? entry.model.y ?? 0);
1259
+
1260
+ entry.model.positionX = x;
1261
+ entry.model.positionY = y;
1262
+ entry.model.x = x;
1263
+ entry.model.y = y;
1264
+ entry.model.width = width;
1265
+ entry.model.height = height;
1266
+ entry.model.originalHeight = height;
1267
+ entry.model.isExpanded = false;
1268
+ entry.model.scrollOffset = 0;
1269
+ entry.model.maxScroll = 0;
1270
+
1271
+ if (entry.glObject) {
1272
+ entry.glObject.position.x = x;
1273
+ entry.glObject.position.y = y;
1274
+ if (entry.glObject.userData) {
1275
+ entry.glObject.userData.worldWidth = width;
1276
+ entry.glObject.userData.worldHeight = height;
1277
+ }
1278
+ }
1279
+ if (entry.cssObject) {
1280
+ entry.cssObject.position.x = x;
1281
+ entry.cssObject.position.y = y;
1282
+ if (entry.cssObject.userData) {
1283
+ entry.cssObject.userData.worldWidth = width;
1284
+ entry.cssObject.userData.worldHeight = height;
1285
+ }
1286
+ window.MindMapCss3DManager?.syncCss3dContent?.(entry.model, entry.cssObject);
1287
+ entry.cssObject.updateMatrixWorld?.(true);
1288
+ }
1289
+
1290
+ window.MindMapNodes?.updateNodeSpatialGrid?.(_module, update.nodeId);
1291
+ window.MindMapTextLOD?.invalidateNodeCache?.(update.nodeId, false);
1292
+ window.MindMapTextOverlayV2?.invalidateNode?.(_module, update.nodeId, 'layout');
1293
+
1294
+ if (window.MindMapNodes?.pendingNodeUpdates && window.MindMapPipeline?.processRenderQueue) {
1295
+ window.MindMapNodes.pendingNodeUpdates.set(update.nodeId, {
1296
+ content: entry.model.response ?? entry.model.Response ?? entry.model.content ?? '',
1297
+ width,
1298
+ height,
1299
+ skipMeasure: true,
1300
+ skipPersistDimensions: true
1301
+ });
1302
+ renderPromises.push(window.MindMapPipeline.processRenderQueue(_module, update.nodeId));
1303
+ }
1304
+ }
1305
+
1306
+ const RENDER_CHUNK_SIZE = 10;
1307
+ for (let i = 0; i < renderPromises.length; i += RENDER_CHUNK_SIZE) {
1308
+ await Promise.all(renderPromises.slice(i, i + RENDER_CHUNK_SIZE));
1309
+ if (i + RENDER_CHUNK_SIZE < renderPromises.length) {
1310
+ await new Promise(resolve => setTimeout(resolve, 0));
1311
+ }
1312
+ }
1313
+
1314
+ for (const update of updates) {
1315
+ window.MindMapTextOverlayV2?.updateNodePlacement?.(_module, update.nodeId);
1316
+ }
1317
+
1318
+ _module.markLODPositionsDirty?.();
1319
+ _module.markLODDirty?.();
1320
+ _module.updateVisibility?.(_module.camera);
1321
+ }
1322
+
1323
+ // Fit selected text nodes to their CSS3D content and reflow Y inside each column.
1324
+ async function fitTextNodesToContentAndReflowY(selectedIds) {
1325
+ if (!_module) return;
1326
+
1327
+ const textNodeIds = selectedIds.filter(nodeId => {
1328
+ const entry = _module.nodeObjectsById.get(nodeId);
1329
+ return entry && TEXT_TYPES.includes(entry.model?.contentType);
1330
+ });
1331
+
1332
+ if (textNodeIds.length === 0) {
1333
+ console.log('[MultiSelect] No text nodes to fit');
1334
+ return;
1335
+ }
1336
+
1337
+ console.log(`[MultiSelect] Fitting ${textNodeIds.length} text nodes to content...`);
1338
+ const startTime = performance.now();
1339
+
1340
+ const measureStart = performance.now();
1341
+ const measurePromises = textNodeIds.map(async (nodeId) => {
1342
+ const frame = getNodeFrame(nodeId);
1343
+ if (!frame) return null;
1344
+
1345
+ try {
1346
+ const newHeight = await measureTextFitHeight(frame.entry);
1347
+ if (!Number.isFinite(newHeight) || newHeight <= 0) return null;
1348
+ return { ...frame, newHeight };
1349
+ } catch (err) {
1350
+ console.error(`[MultiSelect] Failed to measure node ${nodeId}:`, err);
1351
+ return null;
1352
+ }
1353
+ });
1354
+
1355
+ const measureResults = await Promise.all(measurePromises);
1356
+ const fitItems = measureResults.filter(r => r !== null);
1357
+
1358
+ const measureTime = (performance.now() - measureStart).toFixed(0);
1359
+ console.log(`[MultiSelect] Measured ${textNodeIds.length} nodes in ${measureTime}ms`);
1360
+
1361
+ if (fitItems.length > 0) {
1362
+ let layoutUpdates = buildTextFitLayoutUpdates(fitItems);
1363
+ await applyTextFitLayoutUpdates(layoutUpdates);
1364
+
1365
+ const refinedCount = refineTextFitItemsFromRenderedDom(fitItems);
1366
+ if (refinedCount > 0) {
1367
+ layoutUpdates = buildTextFitLayoutUpdates(fitItems);
1368
+ await applyTextFitLayoutUpdates(layoutUpdates);
1369
+ console.log(`[MultiSelect] Refined ${refinedCount} text node heights from rendered DOM`);
1370
+ }
1371
+
1372
+ if (_module.dotNetHelper && layoutUpdates.length > 0) {
1373
+ await _module.dotNetHelper.invokeMethodAsync('UpdateMultipleNodesPositionFromJs', layoutUpdates);
1374
+ _module.dotNetHelper.invokeMethodAsync('TriggerBoardSave');
1375
+ }
1376
+
1377
+ const elapsed = (performance.now() - startTime).toFixed(0);
1378
+ console.log(`[MultiSelect] ✅ Fit ${layoutUpdates.length} text nodes in ${elapsed}ms`);
1379
+ }
1380
+
1381
+ // Update bounds after fitting/reflow.
1382
+ showMultiSelectBounds(getSelectedNodeIds());
1383
+ }
1384
+
1385
+ function applyMasonryLayout(selectedIds) {
1386
+ if (!currentBounds || selectedIds.length === 0) return;
1387
+
1388
+ const areaWidth = currentBounds.maxX - currentBounds.minX - 40; // Subtract padding
1389
+ const areaHeight = currentBounds.maxY - currentBounds.minY - 40;
1390
+ const startX = currentBounds.minX + 20;
1391
+ const startY = currentBounds.maxY - 20; // Top of area (remember Y is inverted)
1392
+
1393
+ // Get all nodes with their dimensions
1394
+ const nodes = selectedIds.map(nodeId => {
1395
+ const entry = _module.nodeObjectsById.get(nodeId);
1396
+ return {
1397
+ nodeId,
1398
+ entry,
1399
+ width: entry?.model?.width || 200,
1400
+ height: entry?.model?.height || 200
1401
+ };
1402
+ });
1403
+
1404
+ // Simple masonry: pack into columns
1405
+ const gap = 10;
1406
+ const numColumns = Math.max(1, Math.floor(areaWidth / 500)); // ~500px per column for larger nodes
1407
+ const columnWidth = (areaWidth - gap * (numColumns - 1)) / numColumns;
1408
+ const columnHeights = new Array(numColumns).fill(0);
1409
+
1410
+ // ▼▼▼ [Fix] Add minimum size limits to prevent images from becoming too small ▼▼▼
1411
+ const MIN_IMAGE_WIDTH = 150;
1412
+ const MIN_IMAGE_HEIGHT = 150;
1413
+ const MIN_SCALE_FACTOR = 0.5; // Don't shrink below 50% of original size
1414
+
1415
+ // Calculate scale factor with limits
1416
+ const maxNodeWidth = Math.max(...nodes.map(n => n.width));
1417
+ const rawScaleFactor = columnWidth / maxNodeWidth;
1418
+ const scaleFactor = Math.max(MIN_SCALE_FACTOR, rawScaleFactor);
1419
+ // ▲▲▲ [Fix] ▲▲▲
1420
+
1421
+ nodes.forEach(node => {
1422
+ // Find shortest column
1423
+ const shortestCol = columnHeights.indexOf(Math.min(...columnHeights));
1424
+
1425
+ // Scale with minimum size enforcement
1426
+ let scaledWidth = Math.round(node.width * scaleFactor);
1427
+ let scaledHeight = Math.round(node.height * scaleFactor);
1428
+
1429
+ // ▼▼▼ [Fix] Enforce minimum dimensions ▼▼▼
1430
+ if (scaledWidth < MIN_IMAGE_WIDTH) {
1431
+ const adjustScale = MIN_IMAGE_WIDTH / node.width;
1432
+ scaledWidth = MIN_IMAGE_WIDTH;
1433
+ scaledHeight = Math.round(node.height * adjustScale);
1434
+ }
1435
+ if (scaledHeight < MIN_IMAGE_HEIGHT) {
1436
+ const adjustScale = MIN_IMAGE_HEIGHT / node.height;
1437
+ scaledHeight = MIN_IMAGE_HEIGHT;
1438
+ scaledWidth = Math.round(node.width * adjustScale);
1439
+ }
1440
+ // ▲▲▲ [Fix] ▲▲▲
1441
+
1442
+ const x = startX + shortestCol * (columnWidth + gap);
1443
+ const y = startY - columnHeights[shortestCol];
1444
+
1445
+ // Update node position and size
1446
+ if (node.entry) {
1447
+ const obj = node.entry.glObject || node.entry.cssObject;
1448
+ if (obj) {
1449
+ obj.position.x = x;
1450
+ obj.position.y = y;
1451
+ node.entry.model.positionX = x;
1452
+ node.entry.model.positionY = y;
1453
+ node.entry.model.x = x;
1454
+ node.entry.model.y = y;
1455
+ node.entry.model.width = scaledWidth;
1456
+ node.entry.model.height = scaledHeight;
1457
+ }
1458
+ }
1459
+
1460
+ columnHeights[shortestCol] += scaledHeight + gap;
1461
+ });
1462
+
1463
+ // Notify C# and update textures
1464
+ notifyMasonryChanges(selectedIds);
1465
+
1466
+ // Update bounds display
1467
+ showMultiSelectBounds(selectedIds);
1468
+
1469
+ console.log(`[MultiSelect] Applied masonry layout to ${selectedIds.length} nodes`);
1470
+ }
1471
+
1472
+ function notifyMasonryChanges(selectedIds) {
1473
+ const updates = selectedIds.map(nodeId => {
1474
+ const entry = _module.nodeObjectsById.get(nodeId);
1475
+ return {
1476
+ nodeId,
1477
+ x: entry?.model?.positionX ?? entry?.model?.x ?? 0,
1478
+ y: entry?.model?.positionY ?? entry?.model?.y ?? 0,
1479
+ width: entry?.model?.width || 200,
1480
+ height: entry?.model?.height || 200
1481
+ };
1482
+ });
1483
+
1484
+ if (_module.dotNetHelper && updates.length > 0) {
1485
+ _module.dotNetHelper.invokeMethodAsync('UpdateMultipleNodesPositionFromJs', updates);
1486
+
1487
+ // Trigger board save after masonry layout
1488
+ _module.dotNetHelper.invokeMethodAsync('TriggerBoardSave');
1489
+ }
1490
+
1491
+ // Update textures for resized nodes
1492
+ updates.forEach(u => {
1493
+ MindMapNodes.updateNodeContent(_module, u.nodeId, null, u.width, u.height);
1494
+ });
1495
+ }
1496
+
1497
+ function deleteSelectedNodes(selectedIds) {
1498
+ if (!_module || !_module.dotNetHelper) return;
1499
+
1500
+ // Clear selection state FIRST to prevent UI from reappearing
1501
+ _module.multiSelectedNodeIds.clear();
1502
+ _module.selectedNodeIdJs = null;
1503
+
1504
+ // Hide overlay immediately
1505
+ hide();
1506
+
1507
+ // Also force hide in case update() is called
1508
+ if (multiSelectOverlay) {
1509
+ multiSelectOverlay.style.display = 'none';
1510
+ multiSelectOverlay.style.visibility = 'hidden';
1511
+ }
1512
+
1513
+ // Delete via C# (batch delete - much faster than individual calls)
1514
+ const snapshots = selectedIds
1515
+ .map(nodeId => window.MindMapNodes?.captureDeleteSnapshot?.(_module, nodeId))
1516
+ .filter(Boolean);
1517
+ if (snapshots.length > 0) {
1518
+ window.MindMapNodes?.removeNodes?.(_module, snapshots.map(snapshot => snapshot.nodeId));
1519
+ }
1520
+ _module.dotNetHelper.invokeMethodAsync('DeleteMultipleNodeSnapshotsFromJs', snapshots);
1521
+
1522
+ console.log(`[MultiSelect] Deleted ${selectedIds.length} nodes`);
1523
+ }
1524
+
1525
+ // Update overlay position on camera move or node drag (called from animate loop)
1526
+ // ▼▼▼ [Perf] Throttled and optimized for drag performance ▼▼▼
1527
+ function updateMultiSelectOverlay() {
1528
+ if (!_module || !multiSelectOverlay || multiSelectOverlay.style.display === 'none') return;
1529
+
1530
+ // Centralized drag-state sync from interactions module
1531
+ const draggingNow = !!_module.isDraggingMultipleNodes;
1532
+ if (draggingNow !== isDragging) {
1533
+ isDragging = draggingNow;
1534
+ lastUpdateTime = 0;
1535
+ cachedImageNodeIds = isDragging ? getSelectedNodeIds() : null;
1536
+ }
1537
+
1538
+ // Throttle updates during drag
1539
+ const now = performance.now();
1540
+ if (isDragging && now - lastUpdateTime < UPDATE_THROTTLE_MS) {
1541
+ return;
1542
+ }
1543
+ lastUpdateTime = now;
1544
+
1545
+ // Use cached IDs during drag, recalculate only when not dragging
1546
+ const selectedIds = isDragging && cachedImageNodeIds ? cachedImageNodeIds : getSelectedNodeIds();
1547
+ if (selectedIds.length < 2) {
1548
+ hide();
1549
+ return;
1550
+ }
1551
+
1552
+ let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
1553
+
1554
+ // Use for loop instead of forEach for performance
1555
+ for (let i = 0; i < selectedIds.length; i++) {
1556
+ const nodeId = selectedIds[i];
1557
+ const nodeEntry = _module.nodeObjectsById.get(nodeId);
1558
+ if (!nodeEntry) continue;
1559
+
1560
+ const obj = nodeEntry.glObject || nodeEntry.cssObject;
1561
+ if (!obj) continue;
1562
+
1563
+ const model = nodeEntry.model;
1564
+ const x = obj.position.x;
1565
+ const y = obj.position.y;
1566
+ const w = model.width || 200;
1567
+ const h = model.height || 200;
1568
+
1569
+ if (x < minX) minX = x;
1570
+ if (x + w > maxX) maxX = x + w;
1571
+ if (y - h < minY) minY = y - h;
1572
+ if (y > maxY) maxY = y;
1573
+ }
1574
+
1575
+ if (minX === Infinity) {
1576
+ hide();
1577
+ return;
1578
+ }
1579
+
1580
+ // Add padding
1581
+ const padding = 10;
1582
+ currentBounds = {
1583
+ minX: minX - padding,
1584
+ minY: minY - padding,
1585
+ maxX: maxX + padding,
1586
+ maxY: maxY + padding
1587
+ };
1588
+
1589
+ updateOverlayPosition(selectedIds);
1590
+ }
1591
+
1592
+ function update() {
1593
+ if (!_module) return;
1594
+ updateMultiSelectOverlay();
1595
+ updateSingleImageOverlay();
1596
+ }
1597
+ // ▲▲▲ [Perf] ▲▲▲
1598
+
1599
+ // ▼▼▼ [New] Dynamic menu button creation based on selection type ▼▼▼
1600
+ function updateMenuButtons() {
1601
+ if (!multiSelectMenu) return;
1602
+
1603
+ // Clear existing buttons
1604
+ multiSelectMenu.innerHTML = '';
1605
+
1606
+ const imageNodeIds = getSelectedImageNodeIds();
1607
+ const textNodeIds = getSelectedTextNodeIds();
1608
+ const hasImages = imageNodeIds.length > 0;
1609
+ const hasText = textNodeIds.length > 0;
1610
+
1611
+ // Determine which buttons to show
1612
+ const buttons = [];
1613
+
1614
+ // Copy is always available
1615
+ buttons.push({ icon: 'fa-regular fa-copy', title: 'Copy', action: 'copy', class: 'js-btn-multi-copy' });
1616
+
1617
+ const isMixedImageTextSelection = hasImages && hasText;
1618
+
1619
+ if (hasImages && !isMixedImageTextSelection) {
1620
+ // Image layout organization actions
1621
+ buttons.push({ icon: 'fa-solid fa-table-cells-large', title: 'Tidy masonry', action: 'masonry', class: 'js-btn-multi-masonry' });
1622
+ buttons.push({ icon: 'fa-solid fa-border-all', title: 'Tidy grid', action: 'grid', class: 'js-btn-multi-grid' });
1623
+ buttons.push({ icon: 'fa-solid fa-grip-lines', title: 'Tidy strip', action: 'stripHorizontal', class: 'js-btn-multi-strip' });
1624
+
1625
+ // Stack is only for image-only selection
1626
+ if (!hasText) {
1627
+ buttons.push({ icon: 'fa-solid fa-layer-group', title: 'Stack into pile', action: 'stack', class: 'js-btn-multi-stack' });
1628
+ }
1629
+ }
1630
+
1631
+ // Text-specific actions
1632
+ if (hasText && !isMixedImageTextSelection) {
1633
+ buttons.push({ icon: 'fa-solid fa-arrows-up-down', title: 'Fit text height', action: 'expand', class: 'js-btn-multi-expand' });
1634
+ }
1635
+
1636
+ buttons.push({ separator: true });
1637
+ buttons.push({ icon: 'fa-solid fa-trash', title: 'Delete all', action: 'delete', class: 'js-btn-multi-delete delete-btn' });
1638
+
1639
+ // Create buttons
1640
+ buttons.forEach(btn => {
1641
+ if (btn.separator) {
1642
+ const sep = document.createElement('div');
1643
+ sep.className = 'menu-separator';
1644
+ sep.style.cssText = 'width: 1px; height: 20px; background-color: #e5e7eb; margin: 0 4px; align-self: center;';
1645
+ multiSelectMenu.appendChild(sep);
1646
+ } else {
1647
+ const button = document.createElement('button');
1648
+ button.className = `menu-btn ${btn.class}`;
1649
+ button.title = btn.title;
1650
+ button.innerHTML = `<i class="${btn.icon}"></i>`;
1651
+ button.style.cssText = `
1652
+ width: 32px;
1653
+ height: 32px;
1654
+ border-radius: 6px;
1655
+ border: none;
1656
+ background: transparent;
1657
+ color: #4b5563;
1658
+ display: flex;
1659
+ align-items: center;
1660
+ justify-content: center;
1661
+ cursor: pointer;
1662
+ transition: all 0.15s;
1663
+ font-size: 14px;
1664
+ line-height: 1;
1665
+ `;
1666
+ button.addEventListener('click', (e) => {
1667
+ e.stopPropagation();
1668
+ handleMenuAction(btn.action);
1669
+ });
1670
+ button.addEventListener('mousedown', e => e.stopPropagation());
1671
+ multiSelectMenu.appendChild(button);
1672
+ }
1673
+ });
1674
+ }
1675
+ // ▲▲▲ [New] ▲▲▲
1676
+
1677
+ // Check if multiple supported nodes are selected and show/hide accordingly
1678
+ function checkAndUpdateDisplay() {
1679
+ if (!_module) return;
1680
+
1681
+ const nodeIds = getSelectedNodeIds();
1682
+
1683
+ if (nodeIds.length >= 2) {
1684
+ updateMenuButtons(); // Update menu based on selection type
1685
+ showMultiSelectBounds(nodeIds);
1686
+ hideSingleImageOverlay();
1687
+ // Hide single-node menu
1688
+ if (window.MindMapMenuManager) {
1689
+ window.MindMapMenuManager.hideMenu();
1690
+ }
1691
+ } else {
1692
+ if (multiSelectOverlay) {
1693
+ multiSelectOverlay.style.display = 'none';
1694
+ multiSelectOverlay.style.visibility = 'hidden';
1695
+ }
1696
+ currentBounds = null;
1697
+ initialNodeSizes.clear();
1698
+ updateSingleImageOverlay();
1699
+ }
1700
+ }
1701
+
1702
+ window.MindMapMultiSelect = {
1703
+ init,
1704
+ showMultiSelectBounds,
1705
+ hide,
1706
+ update,
1707
+ checkAndUpdateDisplay,
1708
+ getSelectedImageNodeIds,
1709
+ fitTextNodesToContentAndReflowY,
1710
+ isResizing: () => isResizing,
1711
+ handleResizeMove: onResizeMove,
1712
+ handleResizeEnd: onResizeEnd
1713
+ };
1714
+
1715
+ console.log('✅ mind-map-multi-select.js loaded');
1716
+ })();