@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.
- package/README.md +275 -0
- package/codex-runtime.js +960 -0
- package/launch-bridge.cjs +162 -0
- package/package.json +61 -0
- package/port-guard.cjs +232 -0
- package/scripts/setup-tree-sitter-grammars.mjs +59 -0
- package/server.js +8422 -0
- package/start-bridge.bat +32 -0
- package/start-bridge.sh +81 -0
- package/tree-sitter-grammars/README.md +18 -0
- package/tree-sitter-grammars/tree-sitter-c_sharp.wasm +0 -0
- package/tree-sitter-grammars/tree-sitter-go.wasm +0 -0
- package/tree-sitter-grammars/tree-sitter-java.wasm +0 -0
- package/tree-sitter-grammars/tree-sitter-javascript.wasm +0 -0
- package/tree-sitter-grammars/tree-sitter-python.wasm +0 -0
- package/tree-sitter-grammars/tree-sitter-rust.wasm +0 -0
- package/tree-sitter-grammars/tree-sitter-tsx.wasm +0 -0
- package/tree-sitter-grammars/tree-sitter-typescript.wasm +0 -0
- package/wwwroot/MindExecution.Web.styles.css +3 -0
- package/wwwroot/_content/MindExecution.Plugins.Admin/css/admin-dashboard.css +546 -0
- package/wwwroot/_content/MindExecution.Plugins.Directory/MindExecution.Plugins.Directory.u7utcng611.bundle.scp.css +7 -0
- package/wwwroot/_content/MindExecution.Plugins.Directory/background.png +0 -0
- package/wwwroot/_content/MindExecution.Plugins.Directory/directory-manager.js +202 -0
- package/wwwroot/_content/MindExecution.Plugins.Directory/exampleJsInterop.js +6 -0
- package/wwwroot/_content/MindExecution.Plugins.YouTube/css/youtube-search.css +251 -0
- package/wwwroot/_content/MindExecution.Shared/MindExecution.Shared.wsano1j4wp.bundle.scp.css +4 -0
- package/wwwroot/_content/MindExecution.Shared/css/admin-dashboard.css +559 -0
- package/wwwroot/_content/MindExecution.Shared/css/app.css +1 -0
- package/wwwroot/_content/MindExecution.Shared/css/mind-map-overrides.css +2936 -0
- package/wwwroot/_content/MindExecution.Shared/fonts/NotoSansKR-Bold.ttf +0 -0
- package/wwwroot/_content/MindExecution.Shared/fonts/NotoSansKR-Regular.ttf +0 -0
- package/wwwroot/_content/MindExecution.Shared/js/agent-visualization.js +359 -0
- package/wwwroot/_content/MindExecution.Shared/js/background-themes.js +1721 -0
- package/wwwroot/_content/MindExecution.Shared/js/code-master.js +8316 -0
- package/wwwroot/_content/MindExecution.Shared/js/file-system-helper.js +639 -0
- package/wwwroot/_content/MindExecution.Shared/js/helpers/InfiniteGridHelper.js +109 -0
- package/wwwroot/_content/MindExecution.Shared/js/marked.min.js +69 -0
- package/wwwroot/_content/MindExecution.Shared/js/mind-map-core.js +7982 -0
- package/wwwroot/_content/MindExecution.Shared/js/mind-map-core.js.backup +1059 -0
- package/wwwroot/_content/MindExecution.Shared/js/mind-map-css3d-manager.js +15803 -0
- package/wwwroot/_content/MindExecution.Shared/js/mind-map-dev-guards.js +325 -0
- package/wwwroot/_content/MindExecution.Shared/js/mind-map-dnd.js +1430 -0
- package/wwwroot/_content/MindExecution.Shared/js/mind-map-dnd.js.bak +434 -0
- package/wwwroot/_content/MindExecution.Shared/js/mind-map-glow-shader.js +260 -0
- package/wwwroot/_content/MindExecution.Shared/js/mind-map-interactions.js +7640 -0
- package/wwwroot/_content/MindExecution.Shared/js/mind-map-lod-plan-worker.js +160 -0
- package/wwwroot/_content/MindExecution.Shared/js/mind-map-lod-renderer.js +9923 -0
- package/wwwroot/_content/MindExecution.Shared/js/mind-map-logic-workers.js +977 -0
- package/wwwroot/_content/MindExecution.Shared/js/mind-map-menu-manager.js +1431 -0
- package/wwwroot/_content/MindExecution.Shared/js/mind-map-multi-select.js +1716 -0
- package/wwwroot/_content/MindExecution.Shared/js/mind-map-node-search-worker.js +553 -0
- package/wwwroot/_content/MindExecution.Shared/js/mind-map-nodes.js +4541 -0
- package/wwwroot/_content/MindExecution.Shared/js/mind-map-object-manager.js +489 -0
- package/wwwroot/_content/MindExecution.Shared/js/mind-map-object-manager.js.backup +372 -0
- package/wwwroot/_content/MindExecution.Shared/js/mind-map-pipeline.js +2075 -0
- package/wwwroot/_content/MindExecution.Shared/js/mind-map-text-lod-system.js +646 -0
- package/wwwroot/_content/MindExecution.Shared/js/mind-map-text-overlay-v2.js +4323 -0
- package/wwwroot/_content/MindExecution.Shared/js/mind-map-texture-factory.js +2260 -0
- package/wwwroot/_content/MindExecution.Shared/js/mind-map-texture-factory.js.backup +1258 -0
- package/wwwroot/_content/MindExecution.Shared/js/mind-map-visibility-worker.js +890 -0
- package/wwwroot/_content/MindExecution.Shared/js/mindmap-toolbar.js +594 -0
- package/wwwroot/_content/MindExecution.Shared/js/native-drop-handler.js +170 -0
- package/wwwroot/_content/MindExecution.Shared/js/plan-master.js +788 -0
- package/wwwroot/_content/MindExecution.Shared/js/renderers/CSS3DRenderer.js +50 -0
- package/wwwroot/_content/MindExecution.Shared/js/texture-worker-manager.js +188 -0
- package/wwwroot/_content/MindExecution.Shared/js/texture-worker.js +208 -0
- package/wwwroot/_content/MindExecution.Shared/js/three.min.js +6 -0
- package/wwwroot/_content/MindExecution.Shared/js/titlebar-handler.js +191 -0
- package/wwwroot/_content/MindExecution.Shared/js/token-manager.js +37 -0
- package/wwwroot/_content/MindExecution.Shared/js/token-worker.js +28 -0
- package/wwwroot/_content/MindExecution.Shared/js/troika-bundle.js +5626 -0
- package/wwwroot/_content/MindExecution.Shared/js/troika-bundle.js.map +7 -0
- package/wwwroot/_content/MindExecution.Shared/lib/font-awesome/css/all.min.css +9 -0
- package/wwwroot/_content/MindExecution.Shared/lib/font-awesome/webfonts/fa-brands-400.ttf +0 -0
- package/wwwroot/_content/MindExecution.Shared/lib/font-awesome/webfonts/fa-brands-400.woff2 +0 -0
- package/wwwroot/_content/MindExecution.Shared/lib/font-awesome/webfonts/fa-regular-400.ttf +0 -0
- package/wwwroot/_content/MindExecution.Shared/lib/font-awesome/webfonts/fa-regular-400.woff2 +0 -0
- package/wwwroot/_content/MindExecution.Shared/lib/font-awesome/webfonts/fa-solid-900.ttf +0 -0
- package/wwwroot/_content/MindExecution.Shared/lib/font-awesome/webfonts/fa-solid-900.woff2 +0 -0
- package/wwwroot/_content/MindExecution.Shared/models/all-MiniLM-L6-v2-quantized.onnx +0 -0
- package/wwwroot/_content/MindExecution.Shared/models/vocab.txt +30522 -0
- package/wwwroot/_framework/Google.Protobuf.9h59ukbel7.dll +0 -0
- package/wwwroot/_framework/Markdig.d1j7v41cl1.dll +0 -0
- package/wwwroot/_framework/MessagePack.Annotations.l6qv48kgpt.dll +0 -0
- package/wwwroot/_framework/MessagePack.eqoptzx9d5.dll +0 -0
- package/wwwroot/_framework/Microsoft.AspNetCore.Authorization.k7dsih5y5g.dll +0 -0
- package/wwwroot/_framework/Microsoft.AspNetCore.Components.6nyje9sa0g.dll +0 -0
- package/wwwroot/_framework/Microsoft.AspNetCore.Components.Authorization.iycd6unprw.dll +0 -0
- package/wwwroot/_framework/Microsoft.AspNetCore.Components.Web.487u3twia4.dll +0 -0
- package/wwwroot/_framework/Microsoft.AspNetCore.Components.WebAssembly.d0gcnmlxxz.dll +0 -0
- package/wwwroot/_framework/Microsoft.AspNetCore.Metadata.h4yevl9adi.dll +0 -0
- package/wwwroot/_framework/Microsoft.CSharp.qrvp77qmhs.dll +0 -0
- package/wwwroot/_framework/Microsoft.Data.Sqlite.jdlxgv2jtg.dll +0 -0
- package/wwwroot/_framework/Microsoft.EntityFrameworkCore.4gjazp7kjf.dll +0 -0
- package/wwwroot/_framework/Microsoft.EntityFrameworkCore.Abstractions.gocudnvz7b.dll +0 -0
- package/wwwroot/_framework/Microsoft.EntityFrameworkCore.Relational.lt4rsvinuo.dll +0 -0
- package/wwwroot/_framework/Microsoft.EntityFrameworkCore.Sqlite.69luj0fa9r.dll +0 -0
- package/wwwroot/_framework/Microsoft.Extensions.Caching.Abstractions.364t4jh3zz.dll +0 -0
- package/wwwroot/_framework/Microsoft.Extensions.Caching.Memory.izlxhpzosu.dll +0 -0
- package/wwwroot/_framework/Microsoft.Extensions.Configuration.8zq7hh41o7.dll +0 -0
- package/wwwroot/_framework/Microsoft.Extensions.Configuration.Abstractions.8if74zs6ea.dll +0 -0
- package/wwwroot/_framework/Microsoft.Extensions.Configuration.Json.duvlngw8i0.dll +0 -0
- package/wwwroot/_framework/Microsoft.Extensions.DependencyInjection.Abstractions.t2hh9kvx0o.dll +0 -0
- package/wwwroot/_framework/Microsoft.Extensions.DependencyInjection.n4tg99oy8l.dll +0 -0
- package/wwwroot/_framework/Microsoft.Extensions.DependencyModel.h0d06ixk3e.dll +0 -0
- package/wwwroot/_framework/Microsoft.Extensions.Logging.Abstractions.rl32bkx2sd.dll +0 -0
- package/wwwroot/_framework/Microsoft.Extensions.Logging.dlht1xei0t.dll +0 -0
- package/wwwroot/_framework/Microsoft.Extensions.Options.qeunebioml.dll +0 -0
- package/wwwroot/_framework/Microsoft.Extensions.Primitives.18cr6vnuuz.dll +0 -0
- package/wwwroot/_framework/Microsoft.IO.RecyclableMemoryStream.r915vovvw4.dll +0 -0
- package/wwwroot/_framework/Microsoft.IdentityModel.Abstractions.1ejljk3erv.dll +0 -0
- package/wwwroot/_framework/Microsoft.IdentityModel.JsonWebTokens.1596zr8gne.dll +0 -0
- package/wwwroot/_framework/Microsoft.IdentityModel.Logging.229uyvpgio.dll +0 -0
- package/wwwroot/_framework/Microsoft.IdentityModel.Tokens.9sibtajc9f.dll +0 -0
- package/wwwroot/_framework/Microsoft.JSInterop.17lq4j1j7g.dll +0 -0
- package/wwwroot/_framework/Microsoft.JSInterop.WebAssembly.ryia5gxiad.dll +0 -0
- package/wwwroot/_framework/Microsoft.ML.OnnxRuntime.w9deo1m5ss.dll +0 -0
- package/wwwroot/_framework/Microsoft.ML.Tokenizers.cm2vuv2z61.dll +0 -0
- package/wwwroot/_framework/Microsoft.NET.StringTools.3qbrf4v2ki.dll +0 -0
- package/wwwroot/_framework/MimeMapping.og9ys58ylm.dll +0 -0
- package/wwwroot/_framework/MindExecution.Core.1q1trifbuu.dll +0 -0
- package/wwwroot/_framework/MindExecution.Kernel.gwwc40sc45.dll +0 -0
- package/wwwroot/_framework/MindExecution.Plugins.Admin.0jgrn1sckv.dll +0 -0
- package/wwwroot/_framework/MindExecution.Plugins.Business.13mme2qcag.dll +0 -0
- package/wwwroot/_framework/MindExecution.Plugins.Concept.dfp2mdt45q.dll +0 -0
- package/wwwroot/_framework/MindExecution.Plugins.Directory.3w4t6n3se0.dll +0 -0
- package/wwwroot/_framework/MindExecution.Plugins.PlanMaster.s0qpntz420.dll +0 -0
- package/wwwroot/_framework/MindExecution.Plugins.YouTube.iu11fq8d16.dll +0 -0
- package/wwwroot/_framework/MindExecution.Shared.7j27dcqnrc.dll +0 -0
- package/wwwroot/_framework/MindExecution.Web.pq1ty8ov2v.dll +0 -0
- package/wwwroot/_framework/Newtonsoft.Json.a56zs13vug.dll +0 -0
- package/wwwroot/_framework/SQLitePCLRaw.batteries_v2.rrd1nzawpp.dll +0 -0
- package/wwwroot/_framework/SQLitePCLRaw.core.1dxloztpfz.dll +0 -0
- package/wwwroot/_framework/SQLitePCLRaw.provider.e_sqlite3.oekyzl53i1.dll +0 -0
- package/wwwroot/_framework/Supabase.Core.s1pkj4aj0l.dll +0 -0
- package/wwwroot/_framework/Supabase.Functions.qz4nu782sg.dll +0 -0
- package/wwwroot/_framework/Supabase.Gotrue.twah27pkik.dll +0 -0
- package/wwwroot/_framework/Supabase.Postgrest.gmuuv369ih.dll +0 -0
- package/wwwroot/_framework/Supabase.Realtime.ox3kchdy3w.dll +0 -0
- package/wwwroot/_framework/Supabase.Storage.fnjnepaowr.dll +0 -0
- package/wwwroot/_framework/Supabase.azmaw5pgcz.dll +0 -0
- package/wwwroot/_framework/System.Collections.Concurrent.y1zmvuyipi.dll +0 -0
- package/wwwroot/_framework/System.Collections.Immutable.ug3j698qms.dll +0 -0
- package/wwwroot/_framework/System.Collections.NonGeneric.h66hj3863h.dll +0 -0
- package/wwwroot/_framework/System.Collections.Specialized.umr3y27ntj.dll +0 -0
- package/wwwroot/_framework/System.Collections.x53e19vfsj.dll +0 -0
- package/wwwroot/_framework/System.ComponentModel.Annotations.tz6gnt4ebt.dll +0 -0
- package/wwwroot/_framework/System.ComponentModel.Primitives.j7tiphu4rg.dll +0 -0
- package/wwwroot/_framework/System.ComponentModel.TypeConverter.ujlztox1gx.dll +0 -0
- package/wwwroot/_framework/System.ComponentModel.x9xz0ojfb6.dll +0 -0
- package/wwwroot/_framework/System.Console.ijzpqmj7ne.dll +0 -0
- package/wwwroot/_framework/System.Data.Common.1r0sqffq1p.dll +0 -0
- package/wwwroot/_framework/System.Diagnostics.DiagnosticSource.9upoqwq09o.dll +0 -0
- package/wwwroot/_framework/System.Diagnostics.Process.m99azzntjm.dll +0 -0
- package/wwwroot/_framework/System.Diagnostics.TraceSource.pl7wv26myr.dll +0 -0
- package/wwwroot/_framework/System.Diagnostics.Tracing.crlhfx6tut.dll +0 -0
- package/wwwroot/_framework/System.Drawing.Primitives.22e4y9ikq9.dll +0 -0
- package/wwwroot/_framework/System.Drawing.mi7d8hwowb.dll +0 -0
- package/wwwroot/_framework/System.Formats.Asn1.jx23sjiqnn.dll +0 -0
- package/wwwroot/_framework/System.IO.Compression.6fyoii3uej.dll +0 -0
- package/wwwroot/_framework/System.IO.Pipelines.vg77t4cd4d.dll +0 -0
- package/wwwroot/_framework/System.IdentityModel.Tokens.Jwt.t67es60z5b.dll +0 -0
- package/wwwroot/_framework/System.Linq.1bkoxlqgmq.dll +0 -0
- package/wwwroot/_framework/System.Linq.Expressions.24xqiypwdt.dll +0 -0
- package/wwwroot/_framework/System.Linq.Queryable.hvd01d6rsa.dll +0 -0
- package/wwwroot/_framework/System.Memory.8dx3lwgym4.dll +0 -0
- package/wwwroot/_framework/System.Net.Http.Json.3mhdm9l1rf.dll +0 -0
- package/wwwroot/_framework/System.Net.Http.eitrz660my.dll +0 -0
- package/wwwroot/_framework/System.Net.NetworkInformation.3pkuofcv9r.dll +0 -0
- package/wwwroot/_framework/System.Net.Ping.8clj5pklrp.dll +0 -0
- package/wwwroot/_framework/System.Net.Primitives.qrp4wcjz1p.dll +0 -0
- package/wwwroot/_framework/System.Net.WebSockets.Client.2u6pv01g69.dll +0 -0
- package/wwwroot/_framework/System.Net.WebSockets.qp6u31zvm5.dll +0 -0
- package/wwwroot/_framework/System.Numerics.Tensors.0c7z4mt3on.dll +0 -0
- package/wwwroot/_framework/System.Numerics.Vectors.kc7ufp2j4l.dll +0 -0
- package/wwwroot/_framework/System.ObjectModel.qv82fot1ib.dll +0 -0
- package/wwwroot/_framework/System.Private.CoreLib.rkafq04oma.dll +0 -0
- package/wwwroot/_framework/System.Private.Uri.t9542hmr6j.dll +0 -0
- package/wwwroot/_framework/System.Private.Xml.Linq.n8n3ptrbwu.dll +0 -0
- package/wwwroot/_framework/System.Private.Xml.rxd3tytisn.dll +0 -0
- package/wwwroot/_framework/System.Reactive.t3fuon548l.dll +0 -0
- package/wwwroot/_framework/System.Reflection.Emit.9tjhp6y0j3.dll +0 -0
- package/wwwroot/_framework/System.Reflection.Emit.ILGeneration.stxyk8zoo1.dll +0 -0
- package/wwwroot/_framework/System.Reflection.Emit.Lightweight.6xrd5v8vg0.dll +0 -0
- package/wwwroot/_framework/System.Reflection.Primitives.wgn8fpwwvv.dll +0 -0
- package/wwwroot/_framework/System.Runtime.InteropServices.JavaScript.sliym526xh.dll +0 -0
- package/wwwroot/_framework/System.Runtime.InteropServices.RuntimeInformation.oji7zut14z.dll +0 -0
- package/wwwroot/_framework/System.Runtime.InteropServices.te07xr2we9.dll +0 -0
- package/wwwroot/_framework/System.Runtime.Intrinsics.507y4h8nzq.dll +0 -0
- package/wwwroot/_framework/System.Runtime.Loader.v7gk4bse0k.dll +0 -0
- package/wwwroot/_framework/System.Runtime.Numerics.eqy5xjv3nd.dll +0 -0
- package/wwwroot/_framework/System.Runtime.Serialization.Formatters.zpkrub8lab.dll +0 -0
- package/wwwroot/_framework/System.Runtime.Serialization.Primitives.vhkpnbxjip.dll +0 -0
- package/wwwroot/_framework/System.Runtime.jn319d5nyg.dll +0 -0
- package/wwwroot/_framework/System.Security.Claims.0ztig1q9vo.dll +0 -0
- package/wwwroot/_framework/System.Security.Cryptography.vttizqc9ho.dll +0 -0
- package/wwwroot/_framework/System.Text.Encoding.Extensions.utdd47ny8f.dll +0 -0
- package/wwwroot/_framework/System.Text.Encodings.Web.wah8r1zoe0.dll +0 -0
- package/wwwroot/_framework/System.Text.Json.kxlfxj0wrs.dll +0 -0
- package/wwwroot/_framework/System.Text.RegularExpressions.dbqn58klox.dll +0 -0
- package/wwwroot/_framework/System.Threading.42ao9vi047.dll +0 -0
- package/wwwroot/_framework/System.Threading.Channels.hfa7j0uv2w.dll +0 -0
- package/wwwroot/_framework/System.Threading.Thread.caul0pdqul.dll +0 -0
- package/wwwroot/_framework/System.Transactions.Local.fimi2hamzo.dll +0 -0
- package/wwwroot/_framework/System.Web.HttpUtility.gq8yz50p2e.dll +0 -0
- package/wwwroot/_framework/System.Xml.Linq.kitin4zjoj.dll +0 -0
- package/wwwroot/_framework/System.Xml.ReaderWriter.kzvw3qgxb0.dll +0 -0
- package/wwwroot/_framework/System.Xml.XDocument.c539ki6cuq.dll +0 -0
- package/wwwroot/_framework/System.m05i39uvk9.dll +0 -0
- package/wwwroot/_framework/Websocket.Client.vapounvmnl.dll +0 -0
- package/wwwroot/_framework/blazor.boot.json +305 -0
- package/wwwroot/_framework/blazor.webassembly.js +1 -0
- package/wwwroot/_framework/dotnet.js +4 -0
- package/wwwroot/_framework/dotnet.native.vz0adxojrz.wasm +0 -0
- package/wwwroot/_framework/dotnet.native.xsn1d6x2kd.js +16 -0
- package/wwwroot/_framework/dotnet.runtime.dstopyvqzi.js +4 -0
- package/wwwroot/_framework/icudt_CJK.tjcz0u77k5.dat +0 -0
- package/wwwroot/_framework/icudt_EFIGS.tptq2av103.dat +0 -0
- package/wwwroot/_framework/icudt_no_CJK.lfu7j35m59.dat +0 -0
- package/wwwroot/_framework/netstandard.0xet7jg7ky.dll +0 -0
- package/wwwroot/_headers +40 -0
- package/wwwroot/_redirects +1 -0
- package/wwwroot/appsettings.json +71 -0
- package/wwwroot/icon-192.png +0 -0
- package/wwwroot/icon-512.png +0 -0
- package/wwwroot/index.html +710 -0
- package/wwwroot/js/marketing-tool.js +180 -0
- package/wwwroot/manifest.webmanifest +22 -0
- package/wwwroot/robots.txt +4 -0
- package/wwwroot/service-worker-assets.js +857 -0
- package/wwwroot/service-worker.js +33 -0
- 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
|
+
})();
|