@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,4541 @@
|
|
|
1
|
+
// File: mind-map-nodes.js
|
|
2
|
+
// [Refactor] Core API + state orchestrator for the MindMap node module
|
|
3
|
+
// (Implementation details are delegated to MindMapTextureFactory, MindMapObjectManager, MindMapCss3DManager, MindMapPipeline)
|
|
4
|
+
(function () {
|
|
5
|
+
// ▼▼▼ [Perf] Debug logging - set to false in production ▼▼▼
|
|
6
|
+
const DEBUG = false;
|
|
7
|
+
const log = DEBUG ? console.log.bind(console) : () => { };
|
|
8
|
+
const warn = DEBUG ? console.warn.bind(console) : () => { };
|
|
9
|
+
// Keep halo/glow in CSS selected styles; do not render WebGL glow meshes.
|
|
10
|
+
const ENABLE_WEBGL_GLOW = false;
|
|
11
|
+
function waitForNonVisualFrame(hiddenDelay = 16) {
|
|
12
|
+
return new Promise(resolve => {
|
|
13
|
+
if (typeof requestAnimationFrame === 'function'
|
|
14
|
+
&& !(typeof document !== 'undefined' && document.visibilityState === 'hidden')) {
|
|
15
|
+
requestAnimationFrame(resolve);
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
setTimeout(resolve, hiddenDelay);
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
// ▲▲▲ [Perf] ▲▲▲
|
|
23
|
+
|
|
24
|
+
const MEMO_SELECTION_THEMES = {
|
|
25
|
+
coral: { border: 0xf7b08d, accent: 0xff8052 },
|
|
26
|
+
amber: { border: 0xf4d06f, accent: 0xf59e0b },
|
|
27
|
+
sun: { border: 0xe7d668, accent: 0xca8a04 },
|
|
28
|
+
sage: { border: 0xbed79f, accent: 0x65a30d },
|
|
29
|
+
mint: { border: 0x8fd8c1, accent: 0x10b981 },
|
|
30
|
+
sky: { border: 0x9dc7f7, accent: 0x3b82f6 },
|
|
31
|
+
indigo: { border: 0xbcc2ff, accent: 0x6366f1 },
|
|
32
|
+
violet: { border: 0xd6b8ff, accent: 0x8b5cf6 },
|
|
33
|
+
rose: { border: 0xf5b2c2, accent: 0xf43f5e },
|
|
34
|
+
gray: { border: 0xd1d5db, accent: 0x6b7280 },
|
|
35
|
+
slate: { border: 0xcbd5e1, accent: 0x64748b }
|
|
36
|
+
};
|
|
37
|
+
const NODE_TEXT_SELECTION_SELECTORS = '.node-response, .code-body, .text-content, .markdown-body, textarea, .map-node-memo__title, .map-node-memo__body, .map-node-memo__body-view, .map-node-memo__body-view *';
|
|
38
|
+
const NODE_INTERACTIVE_CONTENT_SELECTORS = `${NODE_TEXT_SELECTION_SELECTORS}, .embed-content, .embed-card, .embed-card *, .embed-action, .embed-play-button, iframe, .map-node-memo__icon-button, .map-node-memo__icon-option, .map-node-memo__icon-popover, .map-node-memo__agent-action, .map-node-memo__agent-result-link, .map-node-memo__agent-plan-panel, .map-node-memo__agent-plan-body, .map-node-memo__agent-plan-body *, .map-node-memo__agent-console-panel, .map-node-memo__agent-console-body, .map-node-memo__agent-console-body *, .map-node-memo__agent-console-resize, .map-node-memo__agent-console-resize *`;
|
|
39
|
+
const MEMO_EDIT_ONLY_SELECTORS = '.map-node-memo__title, .map-node-memo__body, .map-node-memo__icon-button, .map-node-memo__icon-option, .map-node-memo__icon-popover';
|
|
40
|
+
|
|
41
|
+
function getNodeMetadata(nodeModel) {
|
|
42
|
+
return nodeModel?.metadata || nodeModel?.Metadata || null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function getNodeSemanticType(nodeModel) {
|
|
46
|
+
const metadata = getNodeMetadata(nodeModel);
|
|
47
|
+
return String(metadata?.SemanticType || metadata?.semanticType || '').trim();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const BUSINESS_AUTOMATION_SEMANTIC_TYPE = 'BusinessAutomationNode';
|
|
51
|
+
const AUTOMATION_INPUT_PINS_METADATA_KEY = 'AutomationInputPins';
|
|
52
|
+
const AUTOMATION_OUTPUT_PINS_METADATA_KEY = 'AutomationOutputPins';
|
|
53
|
+
const AUTOMATION_PIN_RAIL_WIDTH = 118;
|
|
54
|
+
const AUTOMATION_EDGE_GUTTER_X = 80;
|
|
55
|
+
|
|
56
|
+
function parseAutomationPinsFromMetadata(metadata, key) {
|
|
57
|
+
const raw = metadata?.[key] ?? metadata?.[key.charAt(0).toLowerCase() + key.slice(1)];
|
|
58
|
+
if (Array.isArray(raw)) {
|
|
59
|
+
return raw;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (typeof raw !== 'string' || !raw.trim()) {
|
|
63
|
+
return [];
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
const parsed = JSON.parse(raw);
|
|
68
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
69
|
+
} catch {
|
|
70
|
+
return [];
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function getAutomationPinnedNodeCursorExtraPadding(nodeModel, direction) {
|
|
75
|
+
if (getNodeSemanticType(nodeModel) !== BUSINESS_AUTOMATION_SEMANTIC_TYPE
|
|
76
|
+
|| String(direction || '').toLowerCase() === 'vertical') {
|
|
77
|
+
return 0;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const metadata = getNodeMetadata(nodeModel);
|
|
81
|
+
const hasInputPins = parseAutomationPinsFromMetadata(metadata, AUTOMATION_INPUT_PINS_METADATA_KEY).length > 0;
|
|
82
|
+
const hasOutputPins = parseAutomationPinsFromMetadata(metadata, AUTOMATION_OUTPUT_PINS_METADATA_KEY).length > 0;
|
|
83
|
+
if (!hasInputPins && !hasOutputPins) {
|
|
84
|
+
return 0;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return (hasInputPins ? AUTOMATION_PIN_RAIL_WIDTH : 0)
|
|
88
|
+
+ (hasOutputPins ? AUTOMATION_PIN_RAIL_WIDTH : 0)
|
|
89
|
+
+ AUTOMATION_EDGE_GUTTER_X;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function isAgentStyledMemoNode(nodeModel) {
|
|
93
|
+
const semanticType = getNodeSemanticType(nodeModel);
|
|
94
|
+
return semanticType === 'MindCanvasAgent'
|
|
95
|
+
|| semanticType === 'AgentCommand'
|
|
96
|
+
|| semanticType === BUSINESS_AUTOMATION_SEMANTIC_TYPE;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function isAgentNodeModel(nodeModel) {
|
|
100
|
+
const nodeType = String(nodeModel?.nodeType ?? nodeModel?.NodeType ?? '').toLowerCase();
|
|
101
|
+
if (nodeType.startsWith('agent_')) {
|
|
102
|
+
return true;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const metadata = getNodeMetadata(nodeModel);
|
|
106
|
+
const agentId = metadata?.agentId ?? metadata?.AgentId;
|
|
107
|
+
return agentId !== undefined && agentId !== null && String(agentId) !== '';
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function isInlineEditableNodeModel(nodeModel) {
|
|
111
|
+
const contentType = String(nodeModel?.contentType ?? nodeModel?.ContentType ?? '').toLowerCase();
|
|
112
|
+
return contentType === 'note' || contentType === 'memo' || contentType === 'code' || isAgentNodeModel(nodeModel);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function isReadOnlyTextSelectionNodeModel(nodeModel) {
|
|
116
|
+
const contentType = String(nodeModel?.contentType ?? nodeModel?.ContentType ?? '').toLowerCase();
|
|
117
|
+
return (contentType === 'text' || contentType === 'markdown') && !isAgentNodeModel(nodeModel);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function getSelectionBorderColorHex(nodeModel, isSelected) {
|
|
121
|
+
if (String(nodeModel?.contentType ?? nodeModel?.ContentType ?? '').toLowerCase() !== 'memo') {
|
|
122
|
+
return isSelected ? 0x7b7ff2 : 0x374151;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const fallbackKey = 'coral';
|
|
126
|
+
const key = String(getNodeMetadata(nodeModel)?.memoColor || fallbackKey).toLowerCase();
|
|
127
|
+
const theme = MEMO_SELECTION_THEMES[key] || MEMO_SELECTION_THEMES[fallbackKey];
|
|
128
|
+
return isSelected ? theme.accent : theme.border;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function cloneNodeMetadata(nodeModel) {
|
|
132
|
+
const source = getNodeMetadata(nodeModel);
|
|
133
|
+
return source && typeof source === 'object' ? { ...source } : {};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function captureDeleteSnapshot(module, nodeId) {
|
|
137
|
+
const fallback = {
|
|
138
|
+
nodeId: String(nodeId || ''),
|
|
139
|
+
currentContent: '',
|
|
140
|
+
currentPrompt: '',
|
|
141
|
+
metadata: {}
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
const entry = module?.nodeObjectsById?.get(nodeId);
|
|
145
|
+
if (!entry?.model) {
|
|
146
|
+
return fallback;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const model = entry.model;
|
|
150
|
+
const contentType = String(model.contentType ?? model.ContentType ?? '').toLowerCase();
|
|
151
|
+
const metadata = cloneNodeMetadata(model);
|
|
152
|
+
let currentPrompt = model.prompt ?? model.Prompt ?? '';
|
|
153
|
+
let currentContent = model.response ?? model.Response ?? '';
|
|
154
|
+
|
|
155
|
+
const root = entry.cssObject?.element || null;
|
|
156
|
+
if (root) {
|
|
157
|
+
if (contentType === 'memo') {
|
|
158
|
+
const titleInput = root.querySelector('.map-node-memo__title');
|
|
159
|
+
const bodyTextarea = root.querySelector('.map-node-memo__body');
|
|
160
|
+
if (titleInput) {
|
|
161
|
+
currentPrompt = titleInput.value ?? '';
|
|
162
|
+
}
|
|
163
|
+
if (bodyTextarea) {
|
|
164
|
+
currentContent = bodyTextarea.value ?? '';
|
|
165
|
+
}
|
|
166
|
+
} else if (contentType === 'note') {
|
|
167
|
+
const textarea = root.querySelector('textarea, [id^="node-textarea-"]');
|
|
168
|
+
if (textarea) {
|
|
169
|
+
currentContent = textarea.value ?? '';
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
model.prompt = currentPrompt;
|
|
175
|
+
model.Prompt = currentPrompt;
|
|
176
|
+
model.response = currentContent;
|
|
177
|
+
model.Response = currentContent;
|
|
178
|
+
model.metadata = metadata;
|
|
179
|
+
model.Metadata = metadata;
|
|
180
|
+
|
|
181
|
+
return {
|
|
182
|
+
nodeId: String(nodeId),
|
|
183
|
+
currentContent,
|
|
184
|
+
currentPrompt,
|
|
185
|
+
metadata
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
// ▲▲▲ [Config] ▲▲▲
|
|
191
|
+
|
|
192
|
+
// --- Module state (caches, locks, etc.) ---
|
|
193
|
+
let textureCache = new Map();
|
|
194
|
+
let imageCache = new Map();
|
|
195
|
+
const failedImageUrlCache = new Map();
|
|
196
|
+
let nodeRenderLocks = new Map();
|
|
197
|
+
let pendingNodeUpdates = new Map();
|
|
198
|
+
|
|
199
|
+
// ▼▼▼ [New] Text Render Mode State ('canvas' | 'troika') ▼▼▼
|
|
200
|
+
let TEXT_RENDER_MODE = 'canvas'; // Default to Canvas 2D
|
|
201
|
+
// ▲▲▲ [New] ▲▲▲
|
|
202
|
+
|
|
203
|
+
// ★ 텍스처 메모리 추적
|
|
204
|
+
let totalTextureMemoryBytes = 0;
|
|
205
|
+
|
|
206
|
+
function calculateTextureMemory(textures, nodeId = 'unknown') {
|
|
207
|
+
if (!textures) return 0;
|
|
208
|
+
let bytes = 0;
|
|
209
|
+
|
|
210
|
+
// Canvas 텍스처 메모리 = width * height * 4 (RGBA)
|
|
211
|
+
const calcTexSize = (tex, name) => {
|
|
212
|
+
if (!tex || !tex.image) return 0;
|
|
213
|
+
if (tex._isShared) return 0;
|
|
214
|
+
const img = tex.image;
|
|
215
|
+
const w = img.width || img.naturalWidth || 0;
|
|
216
|
+
const h = img.height || img.naturalHeight || 0;
|
|
217
|
+
const size = w * h * 4; // RGBA
|
|
218
|
+
if (size > 1024 * 1024) { // 1MB 이상만 로그
|
|
219
|
+
console.log(` [TexMem] ${name}: ${w}x${h} = ${(size / 1024 / 1024).toFixed(2)}MB`);
|
|
220
|
+
}
|
|
221
|
+
return size;
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
// default body & tail
|
|
225
|
+
bytes += calcTexSize(textures.default?.body, `${nodeId}.default.body`);
|
|
226
|
+
bytes += calcTexSize(textures.default?.tail, `${nodeId}.default.tail`);
|
|
227
|
+
// selected body & tail
|
|
228
|
+
bytes += calcTexSize(textures.selected?.body, `${nodeId}.selected.body`);
|
|
229
|
+
bytes += calcTexSize(textures.selected?.tail, `${nodeId}.selected.tail`);
|
|
230
|
+
// glow
|
|
231
|
+
bytes += calcTexSize(textures.glow?.texture, `${nodeId}.glow`);
|
|
232
|
+
|
|
233
|
+
return bytes;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function logTextureMemory(action, nodeId, deltaBytes = 0) {
|
|
237
|
+
if (!deltaBytes) return;
|
|
238
|
+
totalTextureMemoryBytes += deltaBytes;
|
|
239
|
+
const totalMB = (totalTextureMemoryBytes / (1024 * 1024)).toFixed(2);
|
|
240
|
+
const deltaMB = (Math.abs(deltaBytes) / (1024 * 1024)).toFixed(2);
|
|
241
|
+
const sign = deltaBytes >= 0 ? '+' : '-';
|
|
242
|
+
console.log(`[📊 Texture Memory] ${action} ${nodeId}: ${sign}${deltaMB}MB | Total: ${totalMB}MB (${textureCache.size} nodes)`);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function estimateTextureCacheMemoryBytes(cache = textureCache) {
|
|
246
|
+
if (!(cache instanceof Map) || cache.size === 0) {
|
|
247
|
+
return 0;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
let bytes = 0;
|
|
251
|
+
cache.forEach((textures, nodeId) => {
|
|
252
|
+
bytes += calculateTextureMemory(textures, nodeId);
|
|
253
|
+
});
|
|
254
|
+
return bytes;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function syncExportedRuntimeCaches() {
|
|
258
|
+
if (!window.MindMapNodes) return;
|
|
259
|
+
window.MindMapNodes.textureCache = textureCache;
|
|
260
|
+
window.MindMapNodes.imageCache = imageCache;
|
|
261
|
+
window.MindMapNodes.nodeRenderLocks = nodeRenderLocks;
|
|
262
|
+
window.MindMapNodes.pendingNodeUpdates = pendingNodeUpdates;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function replaceRuntimeCaches(nextState = {}) {
|
|
266
|
+
if (nextState.textureCache instanceof Map) {
|
|
267
|
+
textureCache = nextState.textureCache;
|
|
268
|
+
}
|
|
269
|
+
if (nextState.imageCache instanceof Map) {
|
|
270
|
+
imageCache = nextState.imageCache;
|
|
271
|
+
}
|
|
272
|
+
if (nextState.nodeRenderLocks instanceof Map) {
|
|
273
|
+
nodeRenderLocks = nextState.nodeRenderLocks;
|
|
274
|
+
}
|
|
275
|
+
if (nextState.pendingNodeUpdates instanceof Map) {
|
|
276
|
+
pendingNodeUpdates = nextState.pendingNodeUpdates;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const nextTextureBytes = Number(nextState.totalTextureMemoryBytes);
|
|
280
|
+
totalTextureMemoryBytes = Number.isFinite(nextTextureBytes)
|
|
281
|
+
? Math.max(0, nextTextureBytes)
|
|
282
|
+
: estimateTextureCacheMemoryBytes(textureCache);
|
|
283
|
+
|
|
284
|
+
syncExportedRuntimeCaches();
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function getRuntimeCacheStats() {
|
|
288
|
+
return {
|
|
289
|
+
textureEntryCount: textureCache.size,
|
|
290
|
+
imageEntryCount: imageCache.size,
|
|
291
|
+
nodeRenderLockCount: nodeRenderLocks.size,
|
|
292
|
+
pendingNodeUpdateCount: pendingNodeUpdates.size,
|
|
293
|
+
textureBytesEstimate: Math.max(0, Number(totalTextureMemoryBytes) || 0)
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// ▼▼▼ [Restored] Generate background texture with border for code nodes ▼▼▼
|
|
298
|
+
|
|
299
|
+
// ▲▲▲ [Restored] ▲▲▲
|
|
300
|
+
|
|
301
|
+
// ▼▼▼ [New] Scrollbar Style Injection ▼▼▼
|
|
302
|
+
function injectScrollbarStyles() {
|
|
303
|
+
const styleId = 'mindmap-thin-scrollbar-style';
|
|
304
|
+
if (document.getElementById(styleId)) return;
|
|
305
|
+
|
|
306
|
+
const style = document.createElement('style');
|
|
307
|
+
style.id = styleId;
|
|
308
|
+
style.innerHTML = `
|
|
309
|
+
/* Narrow scrollbar width/height (approx 6-8px) */
|
|
310
|
+
.thin-scrollbar::-webkit-scrollbar {
|
|
311
|
+
width: 8px;
|
|
312
|
+
height: 8px;
|
|
313
|
+
}
|
|
314
|
+
/* Scrollbar Thumb */
|
|
315
|
+
.thin-scrollbar::-webkit-scrollbar-thumb {
|
|
316
|
+
background-color: rgba(0, 0, 0, 0.3);
|
|
317
|
+
border-radius: 4px;
|
|
318
|
+
}
|
|
319
|
+
/* Darken on hover */
|
|
320
|
+
.thin-scrollbar:hover {
|
|
321
|
+
scrollbar-color: rgba(0, 0, 0, 0.45) transparent;
|
|
322
|
+
}
|
|
323
|
+
.thin-scrollbar::-webkit-scrollbar-thumb:hover {
|
|
324
|
+
background-color: rgba(0, 0, 0, 0.45);
|
|
325
|
+
}
|
|
326
|
+
.thin-scrollbar:hover::-webkit-scrollbar-thumb,
|
|
327
|
+
.thin-scrollbar::-webkit-scrollbar-thumb:active {
|
|
328
|
+
background-color: rgba(0, 0, 0, 0.45);
|
|
329
|
+
}
|
|
330
|
+
/* Transparent Track */
|
|
331
|
+
.thin-scrollbar::-webkit-scrollbar-track {
|
|
332
|
+
background: transparent;
|
|
333
|
+
}
|
|
334
|
+
/* Firefox Compatibility */
|
|
335
|
+
.thin-scrollbar {
|
|
336
|
+
box-sizing: border-box;
|
|
337
|
+
scrollbar-gutter: stable;
|
|
338
|
+
padding-right: 14px;
|
|
339
|
+
scrollbar-width: thin;
|
|
340
|
+
scrollbar-color: rgba(0, 0, 0, 0.3) transparent;
|
|
341
|
+
}
|
|
342
|
+
`;
|
|
343
|
+
document.head.appendChild(style);
|
|
344
|
+
}
|
|
345
|
+
// ▲▲▲ [New] ▲▲▲
|
|
346
|
+
|
|
347
|
+
// --- Public API functions ---
|
|
348
|
+
|
|
349
|
+
// ▼▼▼ [New] Helper to update the node's spatial-grid registration ▼▼▼
|
|
350
|
+
function getNodeSpatialGridKeys(nodeEntry) {
|
|
351
|
+
if (nodeEntry?._spatialGridKeys instanceof Set) {
|
|
352
|
+
return nodeEntry._spatialGridKeys;
|
|
353
|
+
}
|
|
354
|
+
if (nodeEntry?.glObject?.userData?.spatialGridKeys instanceof Set) {
|
|
355
|
+
return nodeEntry.glObject.userData.spatialGridKeys;
|
|
356
|
+
}
|
|
357
|
+
if (nodeEntry?.cssObject?.userData?.spatialGridKeys instanceof Set) {
|
|
358
|
+
return nodeEntry.cssObject.userData.spatialGridKeys;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
return new Set();
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function setNodeSpatialGridKeys(nodeEntry, keys) {
|
|
365
|
+
if (!nodeEntry) return;
|
|
366
|
+
nodeEntry._spatialGridKeys = keys;
|
|
367
|
+
if (nodeEntry.glObject) {
|
|
368
|
+
nodeEntry.glObject.userData ??= {};
|
|
369
|
+
nodeEntry.glObject.userData.spatialGridKeys = keys;
|
|
370
|
+
}
|
|
371
|
+
if (nodeEntry.cssObject) {
|
|
372
|
+
nodeEntry.cssObject.userData ??= {};
|
|
373
|
+
nodeEntry.cssObject.userData.spatialGridKeys = keys;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function getNodeWorldBounds(nodeEntry) {
|
|
378
|
+
const sharedHelper = window.MindMapNodeBounds?.getNodeWorldBounds;
|
|
379
|
+
if (typeof sharedHelper === 'function') {
|
|
380
|
+
return sharedHelper(nodeEntry);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
const model = nodeEntry?.model || null;
|
|
384
|
+
const obj = nodeEntry?.glObject || nodeEntry?.cssObject || null;
|
|
385
|
+
if (!model && !obj) {
|
|
386
|
+
return null;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
const width = Math.max(1, Number(
|
|
390
|
+
nodeEntry?.glObject?.userData?.worldWidth
|
|
391
|
+
?? nodeEntry?.cssObject?.userData?.worldWidth
|
|
392
|
+
?? model?.width
|
|
393
|
+
?? 400
|
|
394
|
+
));
|
|
395
|
+
const height = Math.max(1, Number(
|
|
396
|
+
nodeEntry?.glObject?.userData?.worldHeight
|
|
397
|
+
?? nodeEntry?.cssObject?.userData?.worldHeight
|
|
398
|
+
?? model?.height
|
|
399
|
+
?? 200
|
|
400
|
+
));
|
|
401
|
+
const x = Number(obj?.position?.x ?? model?.positionX ?? model?.x ?? 0);
|
|
402
|
+
const y = Number(obj?.position?.y ?? model?.positionY ?? model?.y ?? 0);
|
|
403
|
+
const z = Number(obj?.position?.z ?? model?.positionZ ?? model?.z ?? 0);
|
|
404
|
+
|
|
405
|
+
return {
|
|
406
|
+
minX: x,
|
|
407
|
+
maxX: x + width,
|
|
408
|
+
minY: y - height,
|
|
409
|
+
maxY: y,
|
|
410
|
+
x,
|
|
411
|
+
y,
|
|
412
|
+
z,
|
|
413
|
+
width,
|
|
414
|
+
height
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
function updateNodeSpatialGrid(module, nodeId) {
|
|
419
|
+
const nodeEntry = module.nodeObjectsById.get(nodeId);
|
|
420
|
+
if (!nodeEntry) return;
|
|
421
|
+
|
|
422
|
+
const bounds = getNodeWorldBounds(nodeEntry);
|
|
423
|
+
if (!bounds) return;
|
|
424
|
+
|
|
425
|
+
const oldKeys = getNodeSpatialGridKeys(nodeEntry);
|
|
426
|
+
const newKeys = new Set();
|
|
427
|
+
const startCellX = Math.floor(bounds.minX / module.GRID_CELL_SIZE);
|
|
428
|
+
const endCellX = Math.floor(bounds.maxX / module.GRID_CELL_SIZE);
|
|
429
|
+
const startCellY = Math.floor(bounds.minY / module.GRID_CELL_SIZE);
|
|
430
|
+
const endCellY = Math.floor(bounds.maxY / module.GRID_CELL_SIZE);
|
|
431
|
+
|
|
432
|
+
for (let x = startCellX; x <= endCellX; x++) {
|
|
433
|
+
for (let y = startCellY; y <= endCellY; y++) {
|
|
434
|
+
newKeys.add(`${x}:${y}`);
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// Update only the changed keys
|
|
439
|
+
oldKeys.forEach(key => {
|
|
440
|
+
if (!newKeys.has(key)) {
|
|
441
|
+
const cell = module.spatialGrid.get(key);
|
|
442
|
+
if (cell) cell.delete(nodeId);
|
|
443
|
+
if (cell && cell.size === 0) module.spatialGrid.delete(key);
|
|
444
|
+
}
|
|
445
|
+
});
|
|
446
|
+
newKeys.forEach(key => {
|
|
447
|
+
if (!oldKeys.has(key)) {
|
|
448
|
+
if (!module.spatialGrid.has(key)) module.spatialGrid.set(key, new Set());
|
|
449
|
+
module.spatialGrid.get(key).add(nodeId);
|
|
450
|
+
}
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
setNodeSpatialGridKeys(nodeEntry, newKeys);
|
|
454
|
+
module.logicWorkerBridge?.upsertNodeEntry?.(nodeEntry, module, {
|
|
455
|
+
nodeId,
|
|
456
|
+
bounds,
|
|
457
|
+
cellKeys: newKeys
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
function snapNodeFrameDimension(module, value, fallback = null) {
|
|
462
|
+
const grid = Math.max(1, Number(module?.GRID_SIZE || 10));
|
|
463
|
+
const raw = Number(value);
|
|
464
|
+
if (Number.isFinite(raw) && raw > 0) {
|
|
465
|
+
return Math.max(grid, Math.round(raw / grid) * grid);
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
const fallbackValue = Number(fallback);
|
|
469
|
+
if (Number.isFinite(fallbackValue) && fallbackValue > 0) {
|
|
470
|
+
return Math.max(grid, Math.round(fallbackValue / grid) * grid);
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
return grid;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
function applyNodeWorldPosition(module, nodeEntry, x, y, z = null) {
|
|
477
|
+
if (!module || !nodeEntry) return;
|
|
478
|
+
|
|
479
|
+
const nextX = Number(x || 0);
|
|
480
|
+
const nextY = Number(y || 0);
|
|
481
|
+
const nextZ = Number.isFinite(Number(z))
|
|
482
|
+
? Number(z)
|
|
483
|
+
: Number(nodeEntry.model?.positionZ ?? nodeEntry.glObject?.position?.z ?? nodeEntry.cssObject?.position?.z ?? 0);
|
|
484
|
+
|
|
485
|
+
if (nodeEntry.model) {
|
|
486
|
+
nodeEntry.model.positionX = nextX;
|
|
487
|
+
nodeEntry.model.positionY = nextY;
|
|
488
|
+
nodeEntry.model.positionZ = nextZ;
|
|
489
|
+
nodeEntry.model.x = nextX;
|
|
490
|
+
nodeEntry.model.y = nextY;
|
|
491
|
+
nodeEntry.model.z = nextZ;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
if (nodeEntry.glObject) {
|
|
495
|
+
nodeEntry.glObject.position.set(nextX, nextY, nextZ);
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
if (nodeEntry.cssObject) {
|
|
499
|
+
const cssParentScene = module.cssScene || module.scene;
|
|
500
|
+
if (cssParentScene && nodeEntry.cssObject.parent !== cssParentScene) {
|
|
501
|
+
cssParentScene.add(nodeEntry.cssObject);
|
|
502
|
+
window.MindMapCss3DManager?.markCssParentRepairNeeded?.(module, nodeEntry.model?.id || nodeEntry.glObject?.userData?.nodeId);
|
|
503
|
+
}
|
|
504
|
+
nodeEntry.cssObject.position.set(nextX, nextY, nextZ);
|
|
505
|
+
nodeEntry.cssObject.updateMatrixWorld(true);
|
|
506
|
+
window.MindMapCss3DManager?.markNodeTransformDirty?.(module, nodeEntry.model?.id || nodeEntry.glObject?.userData?.nodeId);
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
const nodeId = nodeEntry.model?.id || nodeEntry.glObject?.userData?.nodeId;
|
|
510
|
+
updateNodeSpatialGrid(module, nodeId);
|
|
511
|
+
module.lodRenderer?.markPositionsDirty?.(nodeId);
|
|
512
|
+
module._forceUpdateFrames = Math.max(Number(module._forceUpdateFrames || 0), 2);
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
function moveCursorAfterNodePlacement(module, nodeModel, width, height, options = {}) {
|
|
516
|
+
if (!module || !nodeModel || typeof module.updateCursorPosition !== 'function') {
|
|
517
|
+
return;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
const moveCamera = options.moveCamera === true;
|
|
521
|
+
const persistViewState = options.persistViewState !== false;
|
|
522
|
+
const direction = String(options.direction || module.cursorDirection || 'horizontal').toLowerCase();
|
|
523
|
+
const basePadding = Number.isFinite(Number(options.padding)) ? Number(options.padding) : 30;
|
|
524
|
+
const padding = basePadding + getAutomationPinnedNodeCursorExtraPadding(nodeModel, direction);
|
|
525
|
+
const nodeX = Number(nodeModel.positionX || 0);
|
|
526
|
+
const nodeY = Number(nodeModel.positionY || 0);
|
|
527
|
+
const nodeWidth = Math.max(1, Number(width || nodeModel.width || 1));
|
|
528
|
+
const nodeHeight = Math.max(1, Number(height || nodeModel.height || 1));
|
|
529
|
+
|
|
530
|
+
let nextCursorX = nodeX;
|
|
531
|
+
let nextCursorY = nodeY;
|
|
532
|
+
if (direction === 'vertical') {
|
|
533
|
+
nextCursorY = nodeY - nodeHeight - padding;
|
|
534
|
+
} else {
|
|
535
|
+
nextCursorX = nodeX + nodeWidth + padding;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
const cameraTarget = {
|
|
539
|
+
x: nodeX + (nodeWidth / 2),
|
|
540
|
+
y: nodeY - (nodeHeight / 2)
|
|
541
|
+
};
|
|
542
|
+
|
|
543
|
+
module.updateCursorPosition(nextCursorX, nextCursorY, moveCamera, cameraTarget, persistViewState);
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
function prepareGeneratedImageLayoutUpdate(module, nodeEntry, requestedWidth, requestedHeight, previousResponse, previousIsLoading, nextIsLoading) {
|
|
547
|
+
const model = nodeEntry?.model;
|
|
548
|
+
if (!module || !model) {
|
|
549
|
+
return null;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
const contentType = String(model.contentType ?? model.ContentType ?? '').toLowerCase();
|
|
553
|
+
if (contentType !== 'image' || previousIsLoading !== true || nextIsLoading !== false) {
|
|
554
|
+
return null;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
const metadata = getNodeMetadata(model);
|
|
558
|
+
const isNewNode = String(metadata?.IsNew ?? metadata?.isNew ?? '').trim().toLowerCase() === 'true';
|
|
559
|
+
const hadPlaceholderResponse = !String(previousResponse || '').trim();
|
|
560
|
+
if (!isNewNode && !hadPlaceholderResponse) {
|
|
561
|
+
return null;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
const previousWidth = Math.max(1, Number(model.width || model.Width || 1));
|
|
565
|
+
const previousHeight = Math.max(1, Number(model.height || model.Height || 1));
|
|
566
|
+
const nextWidth = snapNodeFrameDimension(module, requestedWidth, previousWidth);
|
|
567
|
+
const nextHeight = snapNodeFrameDimension(module, requestedHeight, previousHeight);
|
|
568
|
+
const previousX = Number(model.positionX ?? model.x ?? 0);
|
|
569
|
+
const previousY = Number(model.positionY ?? model.y ?? 0);
|
|
570
|
+
const rightEdge = previousX + previousWidth;
|
|
571
|
+
const bottomEdge = previousY - previousHeight;
|
|
572
|
+
const snapAxis = typeof module.snapNodeToGrid === 'function'
|
|
573
|
+
? module.snapNodeToGrid.bind(module)
|
|
574
|
+
: (value) => {
|
|
575
|
+
const grid = Math.max(1, Number(module?.GRID_SIZE || 10));
|
|
576
|
+
return Math.round(Number(value || 0) / grid) * grid;
|
|
577
|
+
};
|
|
578
|
+
const snappedRightEdge = snapAxis(rightEdge);
|
|
579
|
+
const snappedBottomEdge = snapAxis(bottomEdge);
|
|
580
|
+
const nextX = snappedRightEdge - nextWidth;
|
|
581
|
+
const nextY = snappedBottomEdge + nextHeight;
|
|
582
|
+
|
|
583
|
+
return {
|
|
584
|
+
x: nextX,
|
|
585
|
+
y: nextY,
|
|
586
|
+
width: nextWidth,
|
|
587
|
+
height: nextHeight,
|
|
588
|
+
positionChanged: Math.abs(nextX - previousX) > 0.1 || Math.abs(nextY - previousY) > 0.1,
|
|
589
|
+
sizeChanged: Math.abs(nextWidth - previousWidth) > 0.1 || Math.abs(nextHeight - previousHeight) > 0.1,
|
|
590
|
+
shouldMoveCursor: isNewNode
|
|
591
|
+
};
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
function supportsDeferredShellNode(nodeModel) {
|
|
595
|
+
const contentType = String(nodeModel?.contentType ?? nodeModel?.ContentType ?? '').toLowerCase();
|
|
596
|
+
return contentType === 'text' ||
|
|
597
|
+
contentType === 'note' ||
|
|
598
|
+
contentType === 'memo' ||
|
|
599
|
+
contentType === 'markdown' ||
|
|
600
|
+
contentType === 'code' ||
|
|
601
|
+
contentType === 'embed';
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
function shouldCreateDeferredShellNode(module, nodeModel) {
|
|
605
|
+
return module?.isLoading === true && supportsDeferredShellNode(nodeModel);
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
async function ensureDeferredNodeMaterialized(module, nodeEntry, options = {}) {
|
|
609
|
+
if (!module || !nodeEntry || nodeEntry.isDeferredShell !== true) {
|
|
610
|
+
return nodeEntry?.glObject || null;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
const objectManager = window.MindMapObjectManager;
|
|
614
|
+
const textureFactory = window.MindMapTextureFactory;
|
|
615
|
+
if (!objectManager?.createOrUpdateCanvasTextureNode || !textureFactory?.generatePlaceholderTextures) {
|
|
616
|
+
return nodeEntry.glObject || null;
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
const model = nodeEntry.model || {};
|
|
620
|
+
const oldGroup = nodeEntry.glObject || null;
|
|
621
|
+
const preservedX = Number(oldGroup?.position?.x ?? model.positionX ?? 0);
|
|
622
|
+
const preservedY = Number(oldGroup?.position?.y ?? model.positionY ?? 0);
|
|
623
|
+
const preservedZ = Number(oldGroup?.position?.z ?? model.positionZ ?? 0);
|
|
624
|
+
const preservedVisible = oldGroup?.visible !== false;
|
|
625
|
+
const preservedSpatialKeys = new Set(getNodeSpatialGridKeys(nodeEntry));
|
|
626
|
+
const width = Number(model.width || 400);
|
|
627
|
+
const height = Number(model.height || 200);
|
|
628
|
+
|
|
629
|
+
model.positionX = preservedX;
|
|
630
|
+
model.positionY = preservedY;
|
|
631
|
+
model.positionZ = preservedZ;
|
|
632
|
+
|
|
633
|
+
let textures = textureCache.get(model.id) || null;
|
|
634
|
+
if (!textures) {
|
|
635
|
+
const generated = await textureFactory.generatePlaceholderTextures(module, model, width, height);
|
|
636
|
+
textures = generated?.textures || null;
|
|
637
|
+
if (textures) {
|
|
638
|
+
textureCache.set(model.id, textures);
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
const materializedGroup = await objectManager.createOrUpdateCanvasTextureNode(
|
|
643
|
+
module,
|
|
644
|
+
model,
|
|
645
|
+
null,
|
|
646
|
+
preservedZ,
|
|
647
|
+
textures,
|
|
648
|
+
width,
|
|
649
|
+
height
|
|
650
|
+
);
|
|
651
|
+
if (!materializedGroup) {
|
|
652
|
+
return oldGroup;
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
materializedGroup.position.set(preservedX, preservedY, preservedZ);
|
|
656
|
+
materializedGroup.visible = preservedVisible;
|
|
657
|
+
materializedGroup.userData ??= {};
|
|
658
|
+
materializedGroup.userData.spatialGridKeys = preservedSpatialKeys;
|
|
659
|
+
materializedGroup.userData.isDeferredShell = false;
|
|
660
|
+
|
|
661
|
+
if (oldGroup?.parent) {
|
|
662
|
+
oldGroup.parent.remove(oldGroup);
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
if (options.attachToScene !== false && module.scene && !materializedGroup.parent) {
|
|
666
|
+
module.scene.add(materializedGroup);
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
nodeEntry.glObject = materializedGroup;
|
|
670
|
+
nodeEntry.bodyMesh = materializedGroup.getObjectByName?.('body') || null;
|
|
671
|
+
nodeEntry.tailMesh = materializedGroup.getObjectByName?.('tail') || null;
|
|
672
|
+
nodeEntry.glowMesh = materializedGroup.getObjectByName?.('glow') || null;
|
|
673
|
+
nodeEntry.isDeferredShell = false;
|
|
674
|
+
|
|
675
|
+
updateNodeSpatialGrid(module, model.id);
|
|
676
|
+
return materializedGroup;
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
function removeNodeFromSpatialGrid(module, nodeId, nodeEntry = null) {
|
|
680
|
+
if (!module?.spatialGrid || !nodeId) return;
|
|
681
|
+
|
|
682
|
+
const keys = getNodeSpatialGridKeys(nodeEntry);
|
|
683
|
+
if (keys && typeof keys.forEach === 'function') {
|
|
684
|
+
keys.forEach(key => {
|
|
685
|
+
const cell = module.spatialGrid.get(key);
|
|
686
|
+
if (cell) cell.delete(nodeId);
|
|
687
|
+
if (cell && cell.size === 0) module.spatialGrid.delete(key);
|
|
688
|
+
});
|
|
689
|
+
setNodeSpatialGridKeys(nodeEntry, new Set());
|
|
690
|
+
return;
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
module.spatialGrid.forEach((cell, key) => {
|
|
694
|
+
if (!cell?.delete?.(nodeId)) return;
|
|
695
|
+
if (cell.size === 0) module.spatialGrid.delete(key);
|
|
696
|
+
});
|
|
697
|
+
}
|
|
698
|
+
// ▲▲▲ [New] ▲▲▲
|
|
699
|
+
|
|
700
|
+
function ensureNodeMeshCache(entry) {
|
|
701
|
+
if (!entry?.glObject) return;
|
|
702
|
+
if (!entry.bodyMesh) entry.bodyMesh = entry.glObject.getObjectByName('body');
|
|
703
|
+
if (!entry.tailMesh) entry.tailMesh = entry.glObject.getObjectByName('tail');
|
|
704
|
+
if (!entry.glowMesh) entry.glowMesh = entry.glObject.getObjectByName('glow');
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
function setNodeScrollbarVisibility(entry, visible) {
|
|
708
|
+
if (!entry?.glObject?.getObjectByName) return;
|
|
709
|
+
const track = entry.glObject.getObjectByName('scrollbarTrack');
|
|
710
|
+
const thumb = entry.glObject.getObjectByName('scrollbarThumb');
|
|
711
|
+
if (track) track.visible = !!visible;
|
|
712
|
+
if (thumb) thumb.visible = !!visible;
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
const IMAGE_SELECTION_GLOW_PAD = 24;
|
|
716
|
+
const IMAGE_SELECTION_GLOW_COLOR = 0x1e3a8a;
|
|
717
|
+
const IMAGE_SELECTION_GLOW_OPACITY = 0.24;
|
|
718
|
+
const IMAGE_SELECTION_SDF_GLOW_COLOR = 0x3b82f6;
|
|
719
|
+
const IMAGE_SELECTION_SDF_GLOW_OPACITY = 0.46;
|
|
720
|
+
const IMAGE_MID_GLOW_COLOR = 0x0f172a;
|
|
721
|
+
const IMAGE_MID_GLOW_OPACITY = 0.34;
|
|
722
|
+
|
|
723
|
+
function getImageLodThresholds() {
|
|
724
|
+
const thresholds = window.LODRenderer?.LOD_THRESHOLDS || null;
|
|
725
|
+
const mid = Number.isFinite(thresholds?.MID) ? thresholds.MID : 6000;
|
|
726
|
+
const far = Number.isFinite(thresholds?.FAR) ? thresholds.FAR : 25000;
|
|
727
|
+
return { mid, far };
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
function createRoundedRectFilledGeometry(width, height, radius) {
|
|
731
|
+
const shape = new THREE.Shape();
|
|
732
|
+
const x = 0;
|
|
733
|
+
const y = 0;
|
|
734
|
+
const r = Math.min(radius, width / 2, height / 2);
|
|
735
|
+
const h = -height;
|
|
736
|
+
|
|
737
|
+
shape.moveTo(x + r, y);
|
|
738
|
+
shape.lineTo(x + width - r, y);
|
|
739
|
+
shape.quadraticCurveTo(x + width, y, x + width, y - r);
|
|
740
|
+
shape.lineTo(x + width, y + h + r);
|
|
741
|
+
shape.quadraticCurveTo(x + width, y + h, x + width - r, y + h);
|
|
742
|
+
shape.lineTo(x + r, y + h);
|
|
743
|
+
shape.quadraticCurveTo(x, y + h, x, y + h + r);
|
|
744
|
+
shape.lineTo(x, y - r);
|
|
745
|
+
shape.quadraticCurveTo(x, y, x + r, y);
|
|
746
|
+
|
|
747
|
+
return new THREE.ShapeGeometry(shape);
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
function syncImageSelectionGlow(group, width, height, baseRenderOrder, isSelected) {
|
|
751
|
+
if (!group) return null;
|
|
752
|
+
|
|
753
|
+
let outline = group.getObjectByName('outline');
|
|
754
|
+
if (!outline) {
|
|
755
|
+
outline = new THREE.Mesh(
|
|
756
|
+
createRoundedRectFilledGeometry(
|
|
757
|
+
width + IMAGE_SELECTION_GLOW_PAD,
|
|
758
|
+
height + IMAGE_SELECTION_GLOW_PAD,
|
|
759
|
+
20 + (IMAGE_SELECTION_GLOW_PAD * 0.5)
|
|
760
|
+
),
|
|
761
|
+
new THREE.MeshBasicMaterial({
|
|
762
|
+
color: IMAGE_SELECTION_GLOW_COLOR,
|
|
763
|
+
transparent: true,
|
|
764
|
+
opacity: 0,
|
|
765
|
+
depthWrite: false,
|
|
766
|
+
depthTest: false
|
|
767
|
+
})
|
|
768
|
+
);
|
|
769
|
+
outline.name = 'outline';
|
|
770
|
+
group.add(outline);
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
outline.geometry?.dispose?.();
|
|
774
|
+
outline.geometry = createRoundedRectFilledGeometry(
|
|
775
|
+
width + IMAGE_SELECTION_GLOW_PAD,
|
|
776
|
+
height + IMAGE_SELECTION_GLOW_PAD,
|
|
777
|
+
20 + (IMAGE_SELECTION_GLOW_PAD * 0.5)
|
|
778
|
+
);
|
|
779
|
+
outline.position.set(-(IMAGE_SELECTION_GLOW_PAD / 2), IMAGE_SELECTION_GLOW_PAD / 2, 0);
|
|
780
|
+
outline.renderOrder = Math.max(0, (baseRenderOrder || 1) - 1);
|
|
781
|
+
outline.visible = !!isSelected;
|
|
782
|
+
|
|
783
|
+
if (outline.material) {
|
|
784
|
+
outline.material.color.setHex(IMAGE_SELECTION_GLOW_COLOR);
|
|
785
|
+
outline.material.opacity = isSelected ? IMAGE_SELECTION_GLOW_OPACITY : 0;
|
|
786
|
+
outline.material.transparent = true;
|
|
787
|
+
outline.material.depthWrite = false;
|
|
788
|
+
outline.material.depthTest = false;
|
|
789
|
+
outline.material.needsUpdate = true;
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
return outline;
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
function syncImageMidLodDecorations(group, width, height, baseRenderOrder, isSelected, visible) {
|
|
796
|
+
if (!group) return null;
|
|
797
|
+
|
|
798
|
+
const existingEdge = group.getObjectByName('imageMidEdge');
|
|
799
|
+
if (existingEdge) {
|
|
800
|
+
existingEdge.parent?.remove?.(existingEdge);
|
|
801
|
+
existingEdge.geometry?.dispose?.();
|
|
802
|
+
existingEdge.material?.dispose?.();
|
|
803
|
+
}
|
|
804
|
+
const existingGlow = group.getObjectByName('imageMidGlow');
|
|
805
|
+
const visibilityConfig =
|
|
806
|
+
visible && typeof visible === 'object'
|
|
807
|
+
? visible
|
|
808
|
+
: null;
|
|
809
|
+
const edgeVisible = false;
|
|
810
|
+
const glowVisible = visibilityConfig
|
|
811
|
+
? !!visibilityConfig.glowVisible
|
|
812
|
+
: (visible === undefined ? (existingGlow?.visible ?? false) : !!visible);
|
|
813
|
+
const previousState = group.userData?.imageMidLodDecorState || null;
|
|
814
|
+
|
|
815
|
+
if (
|
|
816
|
+
previousState &&
|
|
817
|
+
previousState.width === width &&
|
|
818
|
+
previousState.height === height &&
|
|
819
|
+
previousState.baseRenderOrder === baseRenderOrder &&
|
|
820
|
+
previousState.isSelected === !!isSelected &&
|
|
821
|
+
previousState.edgeVisible === edgeVisible &&
|
|
822
|
+
previousState.glowVisible === glowVisible &&
|
|
823
|
+
((existingGlow && existingGlow.visible === glowVisible) || (!existingGlow && glowVisible === false))
|
|
824
|
+
) {
|
|
825
|
+
return { edge: null, glow: existingGlow || null };
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
group.userData.imageMidLodDecorState = {
|
|
829
|
+
width,
|
|
830
|
+
height,
|
|
831
|
+
baseRenderOrder,
|
|
832
|
+
isSelected: !!isSelected,
|
|
833
|
+
edgeVisible,
|
|
834
|
+
glowVisible
|
|
835
|
+
};
|
|
836
|
+
|
|
837
|
+
let glow = existingGlow;
|
|
838
|
+
if (window.MindMapGlowShader?.createSDFGlowMesh) {
|
|
839
|
+
glow = window.MindMapGlowShader.createSDFGlowMesh(width, height, false, 'image', glow || null);
|
|
840
|
+
glow.name = 'imageMidGlow';
|
|
841
|
+
glow.raycast = () => { };
|
|
842
|
+
if (!glow.parent) {
|
|
843
|
+
group.add(glow);
|
|
844
|
+
}
|
|
845
|
+
} else {
|
|
846
|
+
const glowWidth = Math.max(1, width + 24);
|
|
847
|
+
const glowHeight = Math.max(1, height + 24);
|
|
848
|
+
if (!glow) {
|
|
849
|
+
glow = new THREE.Mesh(
|
|
850
|
+
new THREE.PlaneGeometry(glowWidth, glowHeight),
|
|
851
|
+
new THREE.MeshBasicMaterial({
|
|
852
|
+
color: IMAGE_MID_GLOW_COLOR,
|
|
853
|
+
transparent: true,
|
|
854
|
+
opacity: IMAGE_MID_GLOW_OPACITY,
|
|
855
|
+
depthWrite: false,
|
|
856
|
+
depthTest: false,
|
|
857
|
+
side: THREE.DoubleSide
|
|
858
|
+
})
|
|
859
|
+
);
|
|
860
|
+
glow.name = 'imageMidGlow';
|
|
861
|
+
glow.raycast = () => { };
|
|
862
|
+
group.add(glow);
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
const glowGeometry = glow.geometry?.parameters;
|
|
866
|
+
if (!glowGeometry || glowGeometry.width !== glowWidth || glowGeometry.height !== glowHeight) {
|
|
867
|
+
glow.geometry?.dispose?.();
|
|
868
|
+
glow.geometry = new THREE.PlaneGeometry(glowWidth, glowHeight);
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
if (glow) {
|
|
873
|
+
const glowColor = isSelected ? IMAGE_SELECTION_SDF_GLOW_COLOR : IMAGE_MID_GLOW_COLOR;
|
|
874
|
+
const glowOpacity = isSelected ? IMAGE_SELECTION_SDF_GLOW_OPACITY : IMAGE_MID_GLOW_OPACITY;
|
|
875
|
+
glow.position.set(width / 2, -height / 2, -0.02);
|
|
876
|
+
glow.renderOrder = Math.max(0, (baseRenderOrder || 1) - 3);
|
|
877
|
+
glow.visible = glowVisible;
|
|
878
|
+
|
|
879
|
+
if (glow.material?.uniforms) {
|
|
880
|
+
glow.material.uniforms.color.value.setHex(glowColor);
|
|
881
|
+
glow.material.uniforms.opacity.value = glowOpacity;
|
|
882
|
+
glow.material.depthWrite = false;
|
|
883
|
+
glow.material.depthTest = false;
|
|
884
|
+
glow.material.transparent = true;
|
|
885
|
+
glow.material.needsUpdate = true;
|
|
886
|
+
} else if (glow.material) {
|
|
887
|
+
glow.material.color.setHex(glowColor);
|
|
888
|
+
glow.material.opacity = glowOpacity;
|
|
889
|
+
glow.material.transparent = true;
|
|
890
|
+
glow.material.depthWrite = false;
|
|
891
|
+
glow.material.depthTest = false;
|
|
892
|
+
glow.material.needsUpdate = true;
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
return { edge: null, glow };
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
function applyImageCoverUv(bodyMesh, nodeWidth, nodeHeight) {
|
|
900
|
+
const material = bodyMesh?.material;
|
|
901
|
+
const texture = material?.map;
|
|
902
|
+
if (!texture) return;
|
|
903
|
+
|
|
904
|
+
const image = texture.image;
|
|
905
|
+
const imageWidth = Number(image?.videoWidth || image?.naturalWidth || image?.width || 0);
|
|
906
|
+
const imageHeight = Number(image?.videoHeight || image?.naturalHeight || image?.height || 0);
|
|
907
|
+
const targetWidth = Number(nodeWidth || bodyMesh?.geometry?.parameters?.width || 0);
|
|
908
|
+
const targetHeight = Number(nodeHeight || bodyMesh?.geometry?.parameters?.height || 0);
|
|
909
|
+
|
|
910
|
+
if (!(imageWidth > 0 && imageHeight > 0 && targetWidth > 0 && targetHeight > 0)) {
|
|
911
|
+
return;
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
const imageAspect = imageWidth / imageHeight;
|
|
915
|
+
const targetAspect = targetWidth / targetHeight;
|
|
916
|
+
|
|
917
|
+
let repeatX = 1;
|
|
918
|
+
let repeatY = 1;
|
|
919
|
+
let offsetX = 0;
|
|
920
|
+
let offsetY = 0;
|
|
921
|
+
|
|
922
|
+
// Match CSS `object-fit: cover` behavior in WebGL mode.
|
|
923
|
+
if (imageAspect > targetAspect) {
|
|
924
|
+
repeatX = targetAspect / imageAspect;
|
|
925
|
+
offsetX = (1 - repeatX) * 0.5;
|
|
926
|
+
} else if (imageAspect < targetAspect) {
|
|
927
|
+
repeatY = imageAspect / targetAspect;
|
|
928
|
+
offsetY = (1 - repeatY) * 0.5;
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
const requiresManualVFlip = texture.userData?.mindMapManualVFlip === true;
|
|
932
|
+
if (requiresManualVFlip) {
|
|
933
|
+
repeatY = -repeatY;
|
|
934
|
+
offsetY = 1 - offsetY;
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
const previousWrapS = texture.wrapS;
|
|
938
|
+
const previousWrapT = texture.wrapT;
|
|
939
|
+
const previousCenterX = Number(texture.center?.x ?? 0);
|
|
940
|
+
const previousCenterY = Number(texture.center?.y ?? 0);
|
|
941
|
+
const previousRepeatX = Number(texture.repeat?.x ?? 1);
|
|
942
|
+
const previousRepeatY = Number(texture.repeat?.y ?? 1);
|
|
943
|
+
const previousOffsetX = Number(texture.offset?.x ?? 0);
|
|
944
|
+
const previousOffsetY = Number(texture.offset?.y ?? 0);
|
|
945
|
+
const hasWrapChange =
|
|
946
|
+
previousWrapS !== THREE.ClampToEdgeWrapping ||
|
|
947
|
+
previousWrapT !== THREE.ClampToEdgeWrapping;
|
|
948
|
+
const hasCenterChange =
|
|
949
|
+
Math.abs(previousCenterX) > 1e-6 ||
|
|
950
|
+
Math.abs(previousCenterY) > 1e-6;
|
|
951
|
+
const hasRepeatChange =
|
|
952
|
+
Math.abs(previousRepeatX - repeatX) > 1e-6 ||
|
|
953
|
+
Math.abs(previousRepeatY - repeatY) > 1e-6;
|
|
954
|
+
const hasOffsetChange =
|
|
955
|
+
Math.abs(previousOffsetX - offsetX) > 1e-6 ||
|
|
956
|
+
Math.abs(previousOffsetY - offsetY) > 1e-6;
|
|
957
|
+
|
|
958
|
+
if (!hasWrapChange && !hasCenterChange && !hasRepeatChange && !hasOffsetChange) {
|
|
959
|
+
return;
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
texture.wrapS = THREE.ClampToEdgeWrapping;
|
|
963
|
+
texture.wrapT = THREE.ClampToEdgeWrapping;
|
|
964
|
+
// Keep center at the origin because offset already describes the centered crop window.
|
|
965
|
+
// Combining a 0.5 center with the offset shifts the sampled image in MID/FAR WebGL mode.
|
|
966
|
+
if (texture.center?.set) texture.center.set(0, 0);
|
|
967
|
+
if (texture.repeat?.set) texture.repeat.set(repeatX, repeatY);
|
|
968
|
+
if (texture.offset?.set) texture.offset.set(offsetX, offsetY);
|
|
969
|
+
if (hasWrapChange) {
|
|
970
|
+
texture.needsUpdate = true;
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
const IMAGE_STATUS_SPINNER_CYCLE_MS = 900;
|
|
975
|
+
const IMAGE_STATUS_ANIMATION_FRAME_MS = 140;
|
|
976
|
+
const IMAGE_STATUS_TEXTURE_MIN_SCALE = 2;
|
|
977
|
+
const IMAGE_STATUS_TEXTURE_MAX_SCALE = 4;
|
|
978
|
+
const IMAGE_STATUS_TEXTURE_MIN_BACKING = 160;
|
|
979
|
+
const IMAGE_STATUS_LOADING_TEXTURE_MIN_SCALE = 1;
|
|
980
|
+
const IMAGE_STATUS_LOADING_TEXTURE_MAX_SCALE = 2;
|
|
981
|
+
const IMAGE_STATUS_LOADING_TEXTURE_MIN_BACKING = 128;
|
|
982
|
+
|
|
983
|
+
function clampImageStatusMetric(value, min, max) {
|
|
984
|
+
return Math.max(min, Math.min(max, value));
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
function resolveImageStatusTextureScale(isLoading = false) {
|
|
988
|
+
const windowDpr = Math.max(1, Number(window.devicePixelRatio || 1));
|
|
989
|
+
const rendererDpr = Math.max(1, Number(window.mindMap?.renderer?.getPixelRatio?.() || 1));
|
|
990
|
+
const baseDpr = Math.max(windowDpr, rendererDpr);
|
|
991
|
+
const boostedScale = Math.ceil(baseDpr * (isLoading ? 1.0 : 1.35));
|
|
992
|
+
return clampImageStatusMetric(
|
|
993
|
+
boostedScale,
|
|
994
|
+
isLoading ? IMAGE_STATUS_LOADING_TEXTURE_MIN_SCALE : IMAGE_STATUS_TEXTURE_MIN_SCALE,
|
|
995
|
+
isLoading ? IMAGE_STATUS_LOADING_TEXTURE_MAX_SCALE : IMAGE_STATUS_TEXTURE_MAX_SCALE
|
|
996
|
+
);
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
function getImageStatusTypography(width, height) {
|
|
1000
|
+
const safeWidth = Math.max(160, Number(width || 0));
|
|
1001
|
+
const safeHeight = Math.max(120, Number(height || 0));
|
|
1002
|
+
const base = Math.min(safeWidth, safeHeight);
|
|
1003
|
+
return {
|
|
1004
|
+
titleSize: clampImageStatusMetric(Math.round(base * 0.095), 16, 28),
|
|
1005
|
+
subtitleSize: clampImageStatusMetric(Math.round(base * 0.07), 13, 22),
|
|
1006
|
+
radius: clampImageStatusMetric(base * 0.12, 16, 28),
|
|
1007
|
+
strokeWidth: clampImageStatusMetric(base * 0.018, 3, 5),
|
|
1008
|
+
titleGap: clampImageStatusMetric(base * 0.15, 24, 40),
|
|
1009
|
+
subtitleGap: clampImageStatusMetric(base * 0.28, 44, 68)
|
|
1010
|
+
};
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
function getImageStatusState(nodeModel) {
|
|
1014
|
+
const isLoading = !!(nodeModel?.isLoading ?? nodeModel?.IsLoading ?? false);
|
|
1015
|
+
const contentType = String(nodeModel?.contentType ?? nodeModel?.ContentType ?? '').trim().toLowerCase();
|
|
1016
|
+
const metadata = nodeModel?.metadata ?? nodeModel?.Metadata ?? {};
|
|
1017
|
+
const hasError = !!String(metadata?.ImageGenerationError || '').trim();
|
|
1018
|
+
const readyTitle = contentType === 'video'
|
|
1019
|
+
? 'Video'
|
|
1020
|
+
: (contentType === 'embed' ? 'Embed' : 'Image');
|
|
1021
|
+
const loadingTitle = contentType === 'video'
|
|
1022
|
+
? 'Loading video'
|
|
1023
|
+
: (contentType === 'embed' ? 'Loading embed' : 'Generating image');
|
|
1024
|
+
|
|
1025
|
+
return {
|
|
1026
|
+
isLoading,
|
|
1027
|
+
hasError,
|
|
1028
|
+
title: hasError
|
|
1029
|
+
? 'Image failed'
|
|
1030
|
+
: (isLoading ? loadingTitle : readyTitle),
|
|
1031
|
+
subtitle: hasError
|
|
1032
|
+
? 'Check console and retry'
|
|
1033
|
+
: (isLoading ? 'Please wait' : 'Preparing preview')
|
|
1034
|
+
};
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
function hasNodeResponseContent(nodeModel) {
|
|
1038
|
+
return !!String(nodeModel?.response ?? nodeModel?.Response ?? '').trim();
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
function shouldUseCssImageLoadingOverlay(nodeModel) {
|
|
1042
|
+
const contentType = String(nodeModel?.contentType ?? nodeModel?.ContentType ?? '').trim().toLowerCase();
|
|
1043
|
+
if (contentType !== 'image') {
|
|
1044
|
+
return false;
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
const isLoading = !!(nodeModel?.isLoading ?? nodeModel?.IsLoading ?? false);
|
|
1048
|
+
return isLoading && !hasNodeResponseContent(nodeModel);
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
function shouldShowImageBodyInCssMode(nodeModel) {
|
|
1052
|
+
const contentType = String(nodeModel?.contentType ?? nodeModel?.ContentType ?? '').trim().toLowerCase();
|
|
1053
|
+
if (contentType !== 'image') {
|
|
1054
|
+
return false;
|
|
1055
|
+
}
|
|
1056
|
+
return nodeModel?._showImageBodyInCssMode === true;
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
function drawImageStatusTextureFrame(ctx, width, height, statusState, rotationRadians = 0) {
|
|
1060
|
+
if (!ctx) {
|
|
1061
|
+
return;
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
const isLoading = statusState?.isLoading === true;
|
|
1065
|
+
const hasError = statusState?.hasError === true;
|
|
1066
|
+
const title = String(statusState?.title || '');
|
|
1067
|
+
const subtitle = String(statusState?.subtitle || '');
|
|
1068
|
+
|
|
1069
|
+
ctx.clearRect(0, 0, width, height);
|
|
1070
|
+
ctx.fillStyle = '#f8fafc';
|
|
1071
|
+
ctx.fillRect(0, 0, width, height);
|
|
1072
|
+
|
|
1073
|
+
ctx.strokeStyle = '#e2e8f0';
|
|
1074
|
+
ctx.lineWidth = 1;
|
|
1075
|
+
ctx.strokeRect(0.5, 0.5, width - 1, height - 1);
|
|
1076
|
+
|
|
1077
|
+
const centerX = width / 2;
|
|
1078
|
+
const centerY = Math.max(52, (height / 2) - 12);
|
|
1079
|
+
const typography = getImageStatusTypography(width, height);
|
|
1080
|
+
const radius = typography.radius;
|
|
1081
|
+
|
|
1082
|
+
ctx.lineCap = 'round';
|
|
1083
|
+
ctx.lineWidth = typography.strokeWidth;
|
|
1084
|
+
if (hasError) {
|
|
1085
|
+
ctx.strokeStyle = '#f59e0b';
|
|
1086
|
+
ctx.beginPath();
|
|
1087
|
+
ctx.moveTo(centerX - radius * 0.6, centerY - radius * 0.6);
|
|
1088
|
+
ctx.lineTo(centerX + radius * 0.6, centerY + radius * 0.6);
|
|
1089
|
+
ctx.moveTo(centerX + radius * 0.6, centerY - radius * 0.6);
|
|
1090
|
+
ctx.lineTo(centerX - radius * 0.6, centerY + radius * 0.6);
|
|
1091
|
+
ctx.stroke();
|
|
1092
|
+
} else {
|
|
1093
|
+
ctx.strokeStyle = 'rgba(148, 163, 184, 0.3)';
|
|
1094
|
+
ctx.beginPath();
|
|
1095
|
+
ctx.arc(centerX, centerY, radius, 0, Math.PI * 2);
|
|
1096
|
+
ctx.stroke();
|
|
1097
|
+
|
|
1098
|
+
const arcLength = isLoading ? Math.PI * 1.2 : Math.PI * 1.3;
|
|
1099
|
+
const startAngle = isLoading
|
|
1100
|
+
? (rotationRadians - (Math.PI * 0.5))
|
|
1101
|
+
: (-Math.PI * 0.45);
|
|
1102
|
+
ctx.strokeStyle = isLoading ? '#3b82f6' : '#94a3b8';
|
|
1103
|
+
ctx.beginPath();
|
|
1104
|
+
ctx.arc(centerX, centerY, radius, startAngle, startAngle + arcLength);
|
|
1105
|
+
ctx.stroke();
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
ctx.textAlign = 'center';
|
|
1109
|
+
ctx.fillStyle = '#0f172a';
|
|
1110
|
+
ctx.textBaseline = 'middle';
|
|
1111
|
+
ctx.font = `600 ${typography.titleSize}px "Segoe UI", sans-serif`;
|
|
1112
|
+
ctx.fillText(title, centerX, centerY + radius + typography.titleGap);
|
|
1113
|
+
|
|
1114
|
+
ctx.fillStyle = '#64748b';
|
|
1115
|
+
ctx.font = `500 ${typography.subtitleSize}px "Segoe UI", sans-serif`;
|
|
1116
|
+
ctx.fillText(subtitle, centerX, centerY + radius + typography.subtitleGap);
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
function createImageStatusTexture(nodeModel, nodeWidth, nodeHeight) {
|
|
1120
|
+
const statusState = getImageStatusState(nodeModel);
|
|
1121
|
+
const isLoading = statusState.isLoading === true;
|
|
1122
|
+
const scale = resolveImageStatusTextureScale(isLoading);
|
|
1123
|
+
const canvas = document.createElement('canvas');
|
|
1124
|
+
const minBacking = isLoading
|
|
1125
|
+
? IMAGE_STATUS_LOADING_TEXTURE_MIN_BACKING
|
|
1126
|
+
: IMAGE_STATUS_TEXTURE_MIN_BACKING;
|
|
1127
|
+
canvas.width = Math.max(minBacking, Math.round((nodeWidth || 300) * scale));
|
|
1128
|
+
canvas.height = Math.max(minBacking, Math.round((nodeHeight || 200) * scale));
|
|
1129
|
+
|
|
1130
|
+
const ctx = canvas.getContext('2d');
|
|
1131
|
+
if (!ctx) {
|
|
1132
|
+
return null;
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
ctx.scale(scale, scale);
|
|
1136
|
+
|
|
1137
|
+
const width = canvas.width / scale;
|
|
1138
|
+
const height = canvas.height / scale;
|
|
1139
|
+
drawImageStatusTextureFrame(ctx, width, height, statusState, 0);
|
|
1140
|
+
|
|
1141
|
+
const texture = new THREE.CanvasTexture(canvas);
|
|
1142
|
+
texture.colorSpace = THREE.SRGBColorSpace;
|
|
1143
|
+
texture.minFilter = THREE.LinearFilter;
|
|
1144
|
+
texture.magFilter = THREE.LinearFilter;
|
|
1145
|
+
texture.generateMipmaps = false;
|
|
1146
|
+
const maxAnisotropy = Number(window.mindMap?.renderer?.capabilities?.getMaxAnisotropy?.() || 1);
|
|
1147
|
+
texture.anisotropy = Math.max(1, Math.min(4, maxAnisotropy));
|
|
1148
|
+
texture.userData = texture.userData || {};
|
|
1149
|
+
texture.userData.mindMapImageStatus = {
|
|
1150
|
+
ctx,
|
|
1151
|
+
logicalWidth: width,
|
|
1152
|
+
logicalHeight: height,
|
|
1153
|
+
state: statusState,
|
|
1154
|
+
lastAnimationBucket: statusState.isLoading ? 0 : -1
|
|
1155
|
+
};
|
|
1156
|
+
texture.needsUpdate = true;
|
|
1157
|
+
return texture;
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
function updateAnimatedImageStatusTexture(texture, now = performance.now()) {
|
|
1161
|
+
const statusInfo = texture?.userData?.mindMapImageStatus;
|
|
1162
|
+
if (!statusInfo || statusInfo.state?.isLoading !== true || !statusInfo.ctx) {
|
|
1163
|
+
return false;
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
const animationBucket = Math.floor(now / IMAGE_STATUS_ANIMATION_FRAME_MS);
|
|
1167
|
+
if (statusInfo.lastAnimationBucket === animationBucket) {
|
|
1168
|
+
return false;
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
statusInfo.lastAnimationBucket = animationBucket;
|
|
1172
|
+
const progress = (now % IMAGE_STATUS_SPINNER_CYCLE_MS) / IMAGE_STATUS_SPINNER_CYCLE_MS;
|
|
1173
|
+
drawImageStatusTextureFrame(
|
|
1174
|
+
statusInfo.ctx,
|
|
1175
|
+
statusInfo.logicalWidth,
|
|
1176
|
+
statusInfo.logicalHeight,
|
|
1177
|
+
statusInfo.state,
|
|
1178
|
+
progress * Math.PI * 2
|
|
1179
|
+
);
|
|
1180
|
+
texture.needsUpdate = true;
|
|
1181
|
+
return true;
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
function updateAnimatedImageStatusTextures(module, now = performance.now(), options = {}) {
|
|
1185
|
+
if (options?.allowAnimation === false) {
|
|
1186
|
+
return;
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
if (!module?.nodeObjectsById?.forEach) {
|
|
1190
|
+
return;
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
module.nodeObjectsById.forEach(nodeEntry => {
|
|
1194
|
+
if (nodeEntry?.model?.contentType !== 'image' || nodeEntry?.glObject?.visible === false) {
|
|
1195
|
+
return;
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
const texture = nodeEntry?.bodyMesh?.material?.map;
|
|
1199
|
+
updateAnimatedImageStatusTexture(texture, now);
|
|
1200
|
+
});
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
function swapImageBodyTexture(bodyMesh, nextTexture) {
|
|
1204
|
+
const material = bodyMesh?.material;
|
|
1205
|
+
if (!material || !nextTexture) {
|
|
1206
|
+
return;
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
const previousTexture = material.map;
|
|
1210
|
+
material.map = nextTexture;
|
|
1211
|
+
material.color?.setHex?.(0xffffff);
|
|
1212
|
+
material.needsUpdate = true;
|
|
1213
|
+
const nodeId = bodyMesh?.userData?.nodeId;
|
|
1214
|
+
if (nodeId) {
|
|
1215
|
+
window.mindMap?.invalidateImageLodTexture?.(nodeId);
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
if (previousTexture && previousTexture !== nextTexture) {
|
|
1219
|
+
try { previousTexture.dispose?.(); } catch { }
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
function clearImageBodyTexture(bodyMesh, colorHex = 0xffffff) {
|
|
1224
|
+
const material = bodyMesh?.material;
|
|
1225
|
+
if (!material) {
|
|
1226
|
+
return;
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1229
|
+
const previousTexture = material.map;
|
|
1230
|
+
material.map = null;
|
|
1231
|
+
material.color?.setHex?.(colorHex);
|
|
1232
|
+
material.transparent = false;
|
|
1233
|
+
material.opacity = 1;
|
|
1234
|
+
material.needsUpdate = true;
|
|
1235
|
+
|
|
1236
|
+
if (previousTexture) {
|
|
1237
|
+
try { previousTexture.dispose?.(); } catch { }
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
function normalizeImageAssetUrl(url) {
|
|
1242
|
+
if (typeof url !== 'string') {
|
|
1243
|
+
return '';
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
const trimmed = url.trim();
|
|
1247
|
+
if (!trimmed) {
|
|
1248
|
+
return '';
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1251
|
+
return trimmed
|
|
1252
|
+
.replace(/^http:\/\/localhost(?=:\d+\/assets\/)/i, 'http://127.0.0.1')
|
|
1253
|
+
.replace(/^http:\/\/\[::1\](?=:\d+\/assets\/)/i, 'http://127.0.0.1');
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1256
|
+
function isThumbnailImageUrl(url) {
|
|
1257
|
+
return normalizeImageAssetUrl(url).toLowerCase().includes('/assets/thumbs/');
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
function isAnimatedGifImageUrl(url) {
|
|
1261
|
+
const normalized = normalizeImageAssetUrl(url);
|
|
1262
|
+
if (!normalized) return false;
|
|
1263
|
+
return normalized.split(/[?#]/)[0].toLowerCase().endsWith('.gif');
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
function getMetadataValueCaseInsensitive(metadata, ...keys) {
|
|
1267
|
+
if (!metadata || typeof metadata !== 'object') return '';
|
|
1268
|
+
|
|
1269
|
+
for (const key of keys) {
|
|
1270
|
+
const direct = metadata[key];
|
|
1271
|
+
if (direct !== undefined && direct !== null && String(direct).trim()) {
|
|
1272
|
+
return String(direct).trim();
|
|
1273
|
+
}
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
const wantedKeys = new Set(keys.map(key => String(key || '').toLowerCase()));
|
|
1277
|
+
for (const key of Object.keys(metadata)) {
|
|
1278
|
+
if (wantedKeys.has(String(key || '').toLowerCase())) {
|
|
1279
|
+
const value = metadata[key];
|
|
1280
|
+
if (value !== undefined && value !== null && String(value).trim()) {
|
|
1281
|
+
return String(value).trim();
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1286
|
+
return '';
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
function isAnimatedGifImageNodeModel(nodeModel) {
|
|
1290
|
+
if (!nodeModel || getNodeContentTypeLower(nodeModel) !== 'image') {
|
|
1291
|
+
return false;
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
const metadata = getNodeMetadata(nodeModel) || {};
|
|
1295
|
+
const mimeType = getMetadataValueCaseInsensitive(
|
|
1296
|
+
metadata,
|
|
1297
|
+
'fileMime',
|
|
1298
|
+
'FileMime',
|
|
1299
|
+
'mimeType',
|
|
1300
|
+
'MimeType'
|
|
1301
|
+
).toLowerCase();
|
|
1302
|
+
if (mimeType === 'image/gif') {
|
|
1303
|
+
return true;
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1306
|
+
return [
|
|
1307
|
+
nodeModel?.response,
|
|
1308
|
+
nodeModel?.Response,
|
|
1309
|
+
metadata?.OriginalPath,
|
|
1310
|
+
metadata?.OriginalUrl,
|
|
1311
|
+
metadata?.originalUrl,
|
|
1312
|
+
metadata?.ImageUrl,
|
|
1313
|
+
metadata?.imageUrl
|
|
1314
|
+
].some(isAnimatedGifImageUrl);
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
function getAnimatedGifOriginalReference(nodeModel, fallback = '') {
|
|
1318
|
+
const metadata = getNodeMetadata(nodeModel) || {};
|
|
1319
|
+
const candidates = [
|
|
1320
|
+
metadata?.OriginalPath,
|
|
1321
|
+
metadata?.OriginalUrl,
|
|
1322
|
+
metadata?.originalUrl,
|
|
1323
|
+
nodeModel?.response,
|
|
1324
|
+
nodeModel?.Response,
|
|
1325
|
+
fallback
|
|
1326
|
+
];
|
|
1327
|
+
|
|
1328
|
+
for (const value of candidates) {
|
|
1329
|
+
const normalized = normalizeImageAssetUrl(value);
|
|
1330
|
+
if (normalized && !isThumbnailImageUrl(normalized)) {
|
|
1331
|
+
return normalized;
|
|
1332
|
+
}
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1335
|
+
return '';
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1338
|
+
function getFailedImageUrlCacheKey(url) {
|
|
1339
|
+
const normalized = normalizeImageAssetUrl(url);
|
|
1340
|
+
if (!normalized) {
|
|
1341
|
+
return '';
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1344
|
+
const withoutQuery = normalized.split(/[?#]/)[0];
|
|
1345
|
+
const lower = withoutQuery.toLowerCase();
|
|
1346
|
+
const thumbsMarker = '/assets/thumbs/';
|
|
1347
|
+
const assetsMarker = '/assets/';
|
|
1348
|
+
|
|
1349
|
+
if (lower.includes(thumbsMarker)) {
|
|
1350
|
+
const assetName = withoutQuery.slice(lower.lastIndexOf(thumbsMarker) + thumbsMarker.length).split('/').pop() || '';
|
|
1351
|
+
const dotIndex = assetName.lastIndexOf('.');
|
|
1352
|
+
const assetKey = dotIndex > 0 ? assetName.slice(0, dotIndex) : assetName;
|
|
1353
|
+
return `thumbnail:${assetKey.toLowerCase()}`;
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
if (lower.includes(assetsMarker)) {
|
|
1357
|
+
const assetName = withoutQuery.slice(lower.lastIndexOf(assetsMarker) + assetsMarker.length).split('/').pop() || '';
|
|
1358
|
+
const dotIndex = assetName.lastIndexOf('.');
|
|
1359
|
+
const assetKey = dotIndex > 0 ? assetName.slice(0, dotIndex) : assetName;
|
|
1360
|
+
return `original:${assetKey.toLowerCase()}`;
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1363
|
+
return withoutQuery.toLowerCase();
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1366
|
+
function getFailedImageRetryDelayMs(errorStatus) {
|
|
1367
|
+
return Number(errorStatus || 0) === 404 ? 60 * 1000 : 15 * 1000;
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1370
|
+
function getFailedImageUrlEntry(url) {
|
|
1371
|
+
const cacheKey = getFailedImageUrlCacheKey(url);
|
|
1372
|
+
if (!cacheKey) {
|
|
1373
|
+
return null;
|
|
1374
|
+
}
|
|
1375
|
+
|
|
1376
|
+
const entry = failedImageUrlCache.get(cacheKey) || null;
|
|
1377
|
+
if (!entry) {
|
|
1378
|
+
return null;
|
|
1379
|
+
}
|
|
1380
|
+
|
|
1381
|
+
const now = performance.now();
|
|
1382
|
+
if (Number(entry.retryAfter || 0) <= now) {
|
|
1383
|
+
failedImageUrlCache.delete(cacheKey);
|
|
1384
|
+
return null;
|
|
1385
|
+
}
|
|
1386
|
+
|
|
1387
|
+
return entry;
|
|
1388
|
+
}
|
|
1389
|
+
|
|
1390
|
+
function rememberFailedImageUrl(url, errorStatus, retryAfter = 0) {
|
|
1391
|
+
const cacheKey = getFailedImageUrlCacheKey(url);
|
|
1392
|
+
if (!cacheKey) {
|
|
1393
|
+
return 0;
|
|
1394
|
+
}
|
|
1395
|
+
|
|
1396
|
+
const now = performance.now();
|
|
1397
|
+
const status = Number(errorStatus || 0);
|
|
1398
|
+
const nextRetryAfter = Number(retryAfter || 0) > now
|
|
1399
|
+
? Number(retryAfter || 0)
|
|
1400
|
+
: now + getFailedImageRetryDelayMs(status);
|
|
1401
|
+
|
|
1402
|
+
failedImageUrlCache.set(cacheKey, {
|
|
1403
|
+
url: normalizeImageAssetUrl(url),
|
|
1404
|
+
errorStatus: status,
|
|
1405
|
+
retryAfter: nextRetryAfter,
|
|
1406
|
+
failedAt: now
|
|
1407
|
+
});
|
|
1408
|
+
|
|
1409
|
+
return nextRetryAfter;
|
|
1410
|
+
}
|
|
1411
|
+
|
|
1412
|
+
function clearFailedImageUrl(url) {
|
|
1413
|
+
const cacheKey = getFailedImageUrlCacheKey(url);
|
|
1414
|
+
if (!cacheKey) {
|
|
1415
|
+
return;
|
|
1416
|
+
}
|
|
1417
|
+
|
|
1418
|
+
failedImageUrlCache.delete(cacheKey);
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1421
|
+
function ensureNodeMetadataObject(nodeModel) {
|
|
1422
|
+
if (!nodeModel) return {};
|
|
1423
|
+
|
|
1424
|
+
const existing = getNodeMetadata(nodeModel);
|
|
1425
|
+
if (existing && typeof existing === 'object') {
|
|
1426
|
+
nodeModel.metadata = existing;
|
|
1427
|
+
nodeModel.Metadata = existing;
|
|
1428
|
+
return existing;
|
|
1429
|
+
}
|
|
1430
|
+
|
|
1431
|
+
const metadata = {};
|
|
1432
|
+
nodeModel.metadata = metadata;
|
|
1433
|
+
nodeModel.Metadata = metadata;
|
|
1434
|
+
return metadata;
|
|
1435
|
+
}
|
|
1436
|
+
|
|
1437
|
+
function getNodeContentTypeLower(nodeModel) {
|
|
1438
|
+
return String(nodeModel?.contentType ?? nodeModel?.ContentType ?? '').toLowerCase();
|
|
1439
|
+
}
|
|
1440
|
+
|
|
1441
|
+
function isMediaNodeModel(nodeModel) {
|
|
1442
|
+
const contentTypeLower = getNodeContentTypeLower(nodeModel);
|
|
1443
|
+
return contentTypeLower === 'image' || contentTypeLower === 'video' || contentTypeLower === 'embed';
|
|
1444
|
+
}
|
|
1445
|
+
|
|
1446
|
+
function isGeneratedImageNodeModel(nodeModel) {
|
|
1447
|
+
if (!nodeModel || getNodeContentTypeLower(nodeModel) !== 'image') {
|
|
1448
|
+
return false;
|
|
1449
|
+
}
|
|
1450
|
+
|
|
1451
|
+
const metadata = getNodeMetadata(nodeModel) || {};
|
|
1452
|
+
const provider = String(metadata.ImageGenerationProvider || metadata.imageGenerationProvider || '').trim();
|
|
1453
|
+
const operation = String(metadata.RequestedMediaOperation || metadata.requestedMediaOperation || '').trim().toLowerCase();
|
|
1454
|
+
return !!provider ||
|
|
1455
|
+
!!(metadata.GeneratedImageSourceWidth || metadata.generatedImageSourceWidth) ||
|
|
1456
|
+
operation === 'text2image' ||
|
|
1457
|
+
operation === 'image2image' ||
|
|
1458
|
+
operation === 'inpaint';
|
|
1459
|
+
}
|
|
1460
|
+
|
|
1461
|
+
function isCss3dRendererEnabledForModule(module) {
|
|
1462
|
+
return module?.renderDebugFlags?.enableCss3d !== false;
|
|
1463
|
+
}
|
|
1464
|
+
|
|
1465
|
+
function syncCss3dMediaFrameForEntry(module, nodeId, entry, options = {}) {
|
|
1466
|
+
if (!entry?.model || !entry?.cssObject || !isMediaNodeModel(entry.model)) {
|
|
1467
|
+
return false;
|
|
1468
|
+
}
|
|
1469
|
+
|
|
1470
|
+
if (window.MindMapCss3DManager?.syncCss3dMediaNodeFrame) {
|
|
1471
|
+
return window.MindMapCss3DManager.syncCss3dMediaNodeFrame(entry.model, entry.cssObject, options) === true;
|
|
1472
|
+
}
|
|
1473
|
+
|
|
1474
|
+
const width = Math.max(1, Math.round(Number(entry.model.width ?? entry.model.Width ?? entry.cssObject.userData?.worldWidth ?? 1)));
|
|
1475
|
+
const height = Math.max(1, Math.round(Number(entry.model.height ?? entry.model.Height ?? entry.cssObject.userData?.worldHeight ?? 1)));
|
|
1476
|
+
const resolutionScale = Math.max(1, Number(entry.cssObject.userData?.resolutionScale || 1));
|
|
1477
|
+
const wrapper = entry.cssObject.element;
|
|
1478
|
+
const inner = wrapper?.firstElementChild;
|
|
1479
|
+
if (wrapper?.style) {
|
|
1480
|
+
wrapper.style.width = `${width * resolutionScale}px`;
|
|
1481
|
+
wrapper.style.height = `${height * resolutionScale}px`;
|
|
1482
|
+
wrapper.style.minWidth = `${width * resolutionScale}px`;
|
|
1483
|
+
wrapper.style.minHeight = `${height * resolutionScale}px`;
|
|
1484
|
+
wrapper.style.maxWidth = `${width * resolutionScale}px`;
|
|
1485
|
+
wrapper.style.maxHeight = `${height * resolutionScale}px`;
|
|
1486
|
+
wrapper.style.overflow = 'hidden';
|
|
1487
|
+
}
|
|
1488
|
+
if (inner?.style) {
|
|
1489
|
+
inner.style.width = `${width}px`;
|
|
1490
|
+
inner.style.height = `${height}px`;
|
|
1491
|
+
inner.style.minWidth = `${width}px`;
|
|
1492
|
+
inner.style.minHeight = `${height}px`;
|
|
1493
|
+
inner.style.maxWidth = `${width}px`;
|
|
1494
|
+
inner.style.maxHeight = `${height}px`;
|
|
1495
|
+
inner.style.transform = `scale(${resolutionScale})`;
|
|
1496
|
+
inner.style.transformOrigin = '0 0';
|
|
1497
|
+
inner.style.overflow = 'hidden';
|
|
1498
|
+
}
|
|
1499
|
+
if (module && nodeId) {
|
|
1500
|
+
updateNodeSpatialGrid(module, nodeId);
|
|
1501
|
+
}
|
|
1502
|
+
return true;
|
|
1503
|
+
}
|
|
1504
|
+
|
|
1505
|
+
function setNodeResponseValue(nodeModel, value) {
|
|
1506
|
+
if (!nodeModel) {
|
|
1507
|
+
return value == null ? '' : String(value);
|
|
1508
|
+
}
|
|
1509
|
+
|
|
1510
|
+
const normalized = value == null ? '' : String(value);
|
|
1511
|
+
nodeModel.response = normalized;
|
|
1512
|
+
nodeModel.Response = normalized;
|
|
1513
|
+
return normalized;
|
|
1514
|
+
}
|
|
1515
|
+
|
|
1516
|
+
function applyCanonicalNodeResponse(nodeModel, nextContent) {
|
|
1517
|
+
const contentTypeLower = getNodeContentTypeLower(nodeModel);
|
|
1518
|
+
if (contentTypeLower !== 'image') {
|
|
1519
|
+
return setNodeResponseValue(nodeModel, nextContent);
|
|
1520
|
+
}
|
|
1521
|
+
|
|
1522
|
+
const metadata = ensureNodeMetadataObject(nodeModel);
|
|
1523
|
+
const normalizedContent = normalizeImageAssetUrl(nextContent);
|
|
1524
|
+
const thumbnailUrl = normalizeImageAssetUrl(metadata?.ThumbnailUrl || metadata?.thumbnailUrl || '');
|
|
1525
|
+
const isAnimatedGif = isAnimatedGifImageNodeModel(nodeModel);
|
|
1526
|
+
|
|
1527
|
+
if (isAnimatedGif) {
|
|
1528
|
+
const originalReference = getAnimatedGifOriginalReference(nodeModel, normalizedContent);
|
|
1529
|
+
if (originalReference) {
|
|
1530
|
+
setNodeImageOriginalReference(nodeModel, originalReference);
|
|
1531
|
+
return setNodeResponseValue(nodeModel, originalReference);
|
|
1532
|
+
}
|
|
1533
|
+
}
|
|
1534
|
+
|
|
1535
|
+
if (normalizedContent && !isThumbnailImageUrl(normalizedContent)) {
|
|
1536
|
+
setNodeImageOriginalReference(nodeModel, normalizedContent);
|
|
1537
|
+
}
|
|
1538
|
+
|
|
1539
|
+
if (thumbnailUrl && !isAnimatedGif) {
|
|
1540
|
+
setNodeImagePreviewUrl(nodeModel, thumbnailUrl);
|
|
1541
|
+
return normalizeImageAssetUrl(nodeModel.response ?? nodeModel.Response ?? '');
|
|
1542
|
+
}
|
|
1543
|
+
|
|
1544
|
+
if (normalizedContent) {
|
|
1545
|
+
if (isThumbnailImageUrl(normalizedContent)) {
|
|
1546
|
+
setNodeImagePreviewUrl(nodeModel, normalizedContent);
|
|
1547
|
+
return normalizeImageAssetUrl(nodeModel.response ?? nodeModel.Response ?? '');
|
|
1548
|
+
}
|
|
1549
|
+
|
|
1550
|
+
return setNodeResponseValue(nodeModel, normalizedContent);
|
|
1551
|
+
}
|
|
1552
|
+
|
|
1553
|
+
return setNodeResponseValue(nodeModel, '');
|
|
1554
|
+
}
|
|
1555
|
+
|
|
1556
|
+
function setNodeImageOriginalUrl(nodeModel, url) {
|
|
1557
|
+
setNodeImageOriginalReference(nodeModel, url);
|
|
1558
|
+
}
|
|
1559
|
+
|
|
1560
|
+
function setNodeImagePreviewUrl(nodeModel, url) {
|
|
1561
|
+
const normalized = normalizeImageAssetUrl(url);
|
|
1562
|
+
if (!normalized || !nodeModel) return;
|
|
1563
|
+
|
|
1564
|
+
setNodeResponseValue(nodeModel, normalized);
|
|
1565
|
+
|
|
1566
|
+
const metadata = ensureNodeMetadataObject(nodeModel);
|
|
1567
|
+
if (isThumbnailImageUrl(normalized)) {
|
|
1568
|
+
metadata.ThumbnailUrl = normalized;
|
|
1569
|
+
metadata.thumbnailUrl = normalized;
|
|
1570
|
+
}
|
|
1571
|
+
}
|
|
1572
|
+
|
|
1573
|
+
function ensureNodeImageUsesPreviewResponse(nodeModel) {
|
|
1574
|
+
if (!nodeModel) return false;
|
|
1575
|
+
const previousResponse = normalizeImageAssetUrl(nodeModel.response ?? nodeModel.Response ?? '');
|
|
1576
|
+
const nextResponse = applyCanonicalNodeResponse(nodeModel, previousResponse);
|
|
1577
|
+
return previousResponse !== nextResponse;
|
|
1578
|
+
}
|
|
1579
|
+
|
|
1580
|
+
function isDirectImageUrl(url) {
|
|
1581
|
+
const normalized = normalizeImageAssetUrl(url).toLowerCase();
|
|
1582
|
+
return normalized.startsWith('http://')
|
|
1583
|
+
|| normalized.startsWith('https://')
|
|
1584
|
+
|| normalized.startsWith('blob:')
|
|
1585
|
+
|| normalized.startsWith('data:')
|
|
1586
|
+
|| normalized.startsWith('file:');
|
|
1587
|
+
}
|
|
1588
|
+
|
|
1589
|
+
function setNodeImageOriginalReference(nodeModel, url) {
|
|
1590
|
+
const normalized = normalizeImageAssetUrl(url);
|
|
1591
|
+
if (!normalized || !nodeModel) return;
|
|
1592
|
+
|
|
1593
|
+
const metadata = ensureNodeMetadataObject(nodeModel);
|
|
1594
|
+
if (isDirectImageUrl(normalized)) {
|
|
1595
|
+
metadata.OriginalUrl = normalized;
|
|
1596
|
+
metadata.originalUrl = normalized;
|
|
1597
|
+
} else {
|
|
1598
|
+
metadata.OriginalPath = normalized;
|
|
1599
|
+
}
|
|
1600
|
+
}
|
|
1601
|
+
|
|
1602
|
+
function canUseAsFullResImageUrl(url) {
|
|
1603
|
+
const normalized = normalizeImageAssetUrl(url);
|
|
1604
|
+
return !!normalized && !isThumbnailImageUrl(normalized);
|
|
1605
|
+
}
|
|
1606
|
+
|
|
1607
|
+
function markNodeImageAssetMissing(nodeModel, missingUrl) {
|
|
1608
|
+
if (!nodeModel) return;
|
|
1609
|
+
|
|
1610
|
+
const metadata = ensureNodeMetadataObject(nodeModel);
|
|
1611
|
+
const normalizedMissingUrl = normalizeImageAssetUrl(missingUrl);
|
|
1612
|
+
const responseUrl = normalizeImageAssetUrl(nodeModel.response ?? nodeModel.Response ?? '');
|
|
1613
|
+
const originalPath = normalizeImageAssetUrl(metadata.OriginalPath || '');
|
|
1614
|
+
const originalUrl = normalizeImageAssetUrl(metadata.OriginalUrl || metadata.originalUrl || '');
|
|
1615
|
+
const recoverableFullResUrl = canUseAsFullResImageUrl(originalPath)
|
|
1616
|
+
? originalPath
|
|
1617
|
+
: (canUseAsFullResImageUrl(originalUrl)
|
|
1618
|
+
? originalUrl
|
|
1619
|
+
: (canUseAsFullResImageUrl(responseUrl) ? responseUrl : ''));
|
|
1620
|
+
|
|
1621
|
+
if (isThumbnailImageUrl(normalizedMissingUrl)) {
|
|
1622
|
+
delete metadata.ThumbnailUrl;
|
|
1623
|
+
delete metadata.thumbnailUrl;
|
|
1624
|
+
}
|
|
1625
|
+
|
|
1626
|
+
if (normalizedMissingUrl) {
|
|
1627
|
+
metadata.MissingAssetUrl = normalizedMissingUrl;
|
|
1628
|
+
}
|
|
1629
|
+
|
|
1630
|
+
if (recoverableFullResUrl) {
|
|
1631
|
+
if (!responseUrl || isThumbnailImageUrl(responseUrl)) {
|
|
1632
|
+
nodeModel.response = recoverableFullResUrl;
|
|
1633
|
+
nodeModel.Response = recoverableFullResUrl;
|
|
1634
|
+
}
|
|
1635
|
+
|
|
1636
|
+
setNodeImageOriginalReference(nodeModel, recoverableFullResUrl);
|
|
1637
|
+
delete metadata.ImageGenerationError;
|
|
1638
|
+
return;
|
|
1639
|
+
}
|
|
1640
|
+
|
|
1641
|
+
if (normalizedMissingUrl) {
|
|
1642
|
+
delete metadata.ImageGenerationError;
|
|
1643
|
+
return;
|
|
1644
|
+
}
|
|
1645
|
+
|
|
1646
|
+
metadata.ImageGenerationError = 'Image failed';
|
|
1647
|
+
}
|
|
1648
|
+
|
|
1649
|
+
function getNodeImageAssetUrls(nodeModel) {
|
|
1650
|
+
const metadata = getNodeMetadata(nodeModel) || {};
|
|
1651
|
+
const contentTypeLower = getNodeContentTypeLower(nodeModel);
|
|
1652
|
+
const isImageContent = contentTypeLower === 'image';
|
|
1653
|
+
const responseUrl = normalizeImageAssetUrl(nodeModel?.response ?? nodeModel?.Response ?? '');
|
|
1654
|
+
const originalPath = normalizeImageAssetUrl(metadata?.OriginalPath || '');
|
|
1655
|
+
const thumbnailUrl = normalizeImageAssetUrl(metadata?.ThumbnailUrl || metadata?.thumbnailUrl || '');
|
|
1656
|
+
const originalUrl = normalizeImageAssetUrl(metadata?.OriginalUrl || metadata?.originalUrl || '');
|
|
1657
|
+
const snapshotUrl = normalizeImageAssetUrl(metadata?.SnapshotUrl || metadata?.snapshotUrl || '');
|
|
1658
|
+
const previewImageUrl = normalizeImageAssetUrl(
|
|
1659
|
+
metadata?.PreviewUrl ||
|
|
1660
|
+
metadata?.previewUrl ||
|
|
1661
|
+
metadata?.ImageUrl ||
|
|
1662
|
+
metadata?.imageUrl ||
|
|
1663
|
+
''
|
|
1664
|
+
);
|
|
1665
|
+
const missingAssetUrl = normalizeImageAssetUrl(metadata?.MissingAssetUrl || '');
|
|
1666
|
+
const isGeneratedImageNode = isGeneratedImageNodeModel(nodeModel);
|
|
1667
|
+
const isAnimatedGif = isAnimatedGifImageNodeModel(nodeModel);
|
|
1668
|
+
const responseFullResUrl = isImageContent && canUseAsFullResImageUrl(responseUrl) ? responseUrl : '';
|
|
1669
|
+
const missingFullResUrl = canUseAsFullResImageUrl(missingAssetUrl) ? missingAssetUrl : '';
|
|
1670
|
+
const mediaPreviewUrl = isAnimatedGif ? '' : (thumbnailUrl || snapshotUrl || previewImageUrl);
|
|
1671
|
+
const responsePreviewUrl = isImageContent && !isAnimatedGif ? responseUrl : '';
|
|
1672
|
+
const fullResUrl = isImageContent
|
|
1673
|
+
? (isGeneratedImageNode
|
|
1674
|
+
? (originalUrl || responseFullResUrl || originalPath || missingFullResUrl)
|
|
1675
|
+
: (originalUrl || originalPath || responseFullResUrl || missingFullResUrl))
|
|
1676
|
+
: (originalUrl || originalPath || snapshotUrl || previewImageUrl || thumbnailUrl || missingFullResUrl);
|
|
1677
|
+
const previewUrl = isAnimatedGif
|
|
1678
|
+
? (fullResUrl || responseUrl || originalPath || originalUrl || missingAssetUrl)
|
|
1679
|
+
: (mediaPreviewUrl || responsePreviewUrl || originalPath || originalUrl || missingAssetUrl);
|
|
1680
|
+
|
|
1681
|
+
return {
|
|
1682
|
+
responseUrl,
|
|
1683
|
+
originalPath,
|
|
1684
|
+
thumbnailUrl,
|
|
1685
|
+
originalUrl,
|
|
1686
|
+
snapshotUrl,
|
|
1687
|
+
previewImageUrl,
|
|
1688
|
+
missingAssetUrl,
|
|
1689
|
+
previewUrl,
|
|
1690
|
+
fullResUrl,
|
|
1691
|
+
animatedGif: isAnimatedGif
|
|
1692
|
+
};
|
|
1693
|
+
}
|
|
1694
|
+
|
|
1695
|
+
function hasNodeImagePreviewSource(assetUrls) {
|
|
1696
|
+
if (!assetUrls || typeof assetUrls !== 'object') return false;
|
|
1697
|
+
if (assetUrls.animatedGif === true) return false;
|
|
1698
|
+
return !!(
|
|
1699
|
+
assetUrls.thumbnailUrl ||
|
|
1700
|
+
(assetUrls.responseUrl && isThumbnailImageUrl(assetUrls.responseUrl))
|
|
1701
|
+
);
|
|
1702
|
+
}
|
|
1703
|
+
|
|
1704
|
+
function prewarmImageNodePreviewCache(nodeModel, module) {
|
|
1705
|
+
if (!nodeModel || getNodeContentTypeLower(nodeModel) !== 'image') {
|
|
1706
|
+
return;
|
|
1707
|
+
}
|
|
1708
|
+
|
|
1709
|
+
const textureFactory = window.MindMapTextureFactory;
|
|
1710
|
+
const activeImageCache = window.MindMapNodes?.imageCache || imageCache;
|
|
1711
|
+
if (!textureFactory?.ensureImageCached || !activeImageCache) {
|
|
1712
|
+
return;
|
|
1713
|
+
}
|
|
1714
|
+
|
|
1715
|
+
const nodeId = String(nodeModel.id ?? nodeModel.Id ?? '').trim();
|
|
1716
|
+
if (!nodeId) {
|
|
1717
|
+
return;
|
|
1718
|
+
}
|
|
1719
|
+
|
|
1720
|
+
const assetUrls = getNodeImageAssetUrls(nodeModel);
|
|
1721
|
+
if (!assetUrls.previewUrl) {
|
|
1722
|
+
return;
|
|
1723
|
+
}
|
|
1724
|
+
if (assetUrls.animatedGif === true) {
|
|
1725
|
+
return;
|
|
1726
|
+
}
|
|
1727
|
+
|
|
1728
|
+
const warmKey = `${assetUrls.previewUrl}||${assetUrls.fullResUrl || assetUrls.originalUrl || ''}`;
|
|
1729
|
+
if (nodeModel._previewWarmKey === warmKey) {
|
|
1730
|
+
return;
|
|
1731
|
+
}
|
|
1732
|
+
|
|
1733
|
+
nodeModel._previewWarmKey = warmKey;
|
|
1734
|
+
if (module) {
|
|
1735
|
+
module._boardLoadImagePreviewWarmQueued = Number(module._boardLoadImagePreviewWarmQueued || 0) + 1;
|
|
1736
|
+
}
|
|
1737
|
+
|
|
1738
|
+
Promise.resolve(
|
|
1739
|
+
textureFactory.ensureImageCached(nodeId, assetUrls.previewUrl, activeImageCache, {
|
|
1740
|
+
authToken: module?.authToken || window.mindMap?.authToken || '',
|
|
1741
|
+
resizeWidth: 256,
|
|
1742
|
+
preferOriginal: false,
|
|
1743
|
+
allowOriginalFallback: false,
|
|
1744
|
+
allowOriginalReuseForPreview: false,
|
|
1745
|
+
originalUrl: assetUrls.fullResUrl || assetUrls.originalUrl || '',
|
|
1746
|
+
onMissingAsset: (missingUrl) => {
|
|
1747
|
+
const normalized = normalizeImageAssetUrl(missingUrl);
|
|
1748
|
+
if (!normalized) return;
|
|
1749
|
+
markNodeImageAssetMissing(nodeModel, normalized);
|
|
1750
|
+
try { window.dotNetHelper?.invokeMethodAsync('ReportMissingNodeAsset', nodeId, normalized); } catch { }
|
|
1751
|
+
},
|
|
1752
|
+
onRefreshedUrl: (refreshedUrl) => {
|
|
1753
|
+
const normalized = normalizeImageAssetUrl(refreshedUrl);
|
|
1754
|
+
if (!normalized) return;
|
|
1755
|
+
|
|
1756
|
+
setNodeImageOriginalUrl(nodeModel, normalized);
|
|
1757
|
+
if (!hasNodeImagePreviewSource(assetUrls)) {
|
|
1758
|
+
setNodeImagePreviewUrl(nodeModel, normalized);
|
|
1759
|
+
}
|
|
1760
|
+
}
|
|
1761
|
+
})
|
|
1762
|
+
).then((cachedImage) => {
|
|
1763
|
+
if (!module) {
|
|
1764
|
+
return;
|
|
1765
|
+
}
|
|
1766
|
+
|
|
1767
|
+
if (cachedImage?.image && cachedImage?.isError !== true) {
|
|
1768
|
+
module._boardLoadImagePreviewWarmReady = Number(module._boardLoadImagePreviewWarmReady || 0) + 1;
|
|
1769
|
+
return;
|
|
1770
|
+
}
|
|
1771
|
+
|
|
1772
|
+
module._boardLoadImagePreviewWarmFailed = Number(module._boardLoadImagePreviewWarmFailed || 0) + 1;
|
|
1773
|
+
}).catch(() => {
|
|
1774
|
+
if (!module) {
|
|
1775
|
+
return;
|
|
1776
|
+
}
|
|
1777
|
+
|
|
1778
|
+
module._boardLoadImagePreviewWarmFailed = Number(module._boardLoadImagePreviewWarmFailed || 0) + 1;
|
|
1779
|
+
});
|
|
1780
|
+
}
|
|
1781
|
+
|
|
1782
|
+
function getImageTextureResizeWidth(nodeWidth, nodeHeight, preferOriginal, explicitResizeWidth) {
|
|
1783
|
+
const parsedExplicit = Number(explicitResizeWidth);
|
|
1784
|
+
if (Number.isFinite(parsedExplicit) && parsedExplicit > 0) {
|
|
1785
|
+
return Math.max(1, Math.round(parsedExplicit));
|
|
1786
|
+
}
|
|
1787
|
+
|
|
1788
|
+
const maxDimension = Math.max(1, Number(nodeWidth || 0), Number(nodeHeight || 0));
|
|
1789
|
+
const dpr = Math.min(2.5, Math.max(1, window.devicePixelRatio || 1));
|
|
1790
|
+
if (preferOriginal) {
|
|
1791
|
+
return Math.min(3072, Math.max(1024, Math.round(maxDimension * dpr * 2)));
|
|
1792
|
+
}
|
|
1793
|
+
|
|
1794
|
+
return Math.min(768, Math.max(512, Math.round(maxDimension * Math.min(1.5, dpr))));
|
|
1795
|
+
}
|
|
1796
|
+
|
|
1797
|
+
function createImageBodyTextureFromCache(cachedImage) {
|
|
1798
|
+
const image = cachedImage?.image || cachedImage;
|
|
1799
|
+
if (!image) return null;
|
|
1800
|
+
|
|
1801
|
+
const texture = new THREE.Texture(image);
|
|
1802
|
+
texture.userData ??= {};
|
|
1803
|
+
const requiresManualVFlip = cachedImage?.type === 'bitmap';
|
|
1804
|
+
texture.userData.mindMapManualVFlip = requiresManualVFlip;
|
|
1805
|
+
if (requiresManualVFlip) {
|
|
1806
|
+
// ImageBitmap uploads do not honor Texture.flipY consistently, so
|
|
1807
|
+
// NEAR/NORMAL body meshes compensate in UV space instead.
|
|
1808
|
+
texture.flipY = false;
|
|
1809
|
+
}
|
|
1810
|
+
texture.colorSpace = THREE.SRGBColorSpace;
|
|
1811
|
+
texture.wrapS = THREE.ClampToEdgeWrapping;
|
|
1812
|
+
texture.wrapT = THREE.ClampToEdgeWrapping;
|
|
1813
|
+
texture.minFilter = THREE.LinearMipmapLinearFilter;
|
|
1814
|
+
texture.magFilter = THREE.LinearFilter;
|
|
1815
|
+
texture.generateMipmaps = true;
|
|
1816
|
+
texture.needsUpdate = true;
|
|
1817
|
+
return texture;
|
|
1818
|
+
}
|
|
1819
|
+
|
|
1820
|
+
function syncImageNodeBodyTexture(bodyMesh, nodeModel, options = {}) {
|
|
1821
|
+
if (!bodyMesh || !nodeModel) {
|
|
1822
|
+
return;
|
|
1823
|
+
}
|
|
1824
|
+
|
|
1825
|
+
const material = bodyMesh.material;
|
|
1826
|
+
if (!material) {
|
|
1827
|
+
return;
|
|
1828
|
+
}
|
|
1829
|
+
|
|
1830
|
+
const isGeneratedImageNode = isGeneratedImageNodeModel(nodeModel);
|
|
1831
|
+
if (options.preferOriginal !== true && !isGeneratedImageNode) {
|
|
1832
|
+
ensureNodeImageUsesPreviewResponse(nodeModel);
|
|
1833
|
+
}
|
|
1834
|
+
|
|
1835
|
+
const width = Number(bodyMesh.geometry?.parameters?.width || nodeModel.width || 300);
|
|
1836
|
+
const height = Number(bodyMesh.geometry?.parameters?.height || nodeModel.height || 200);
|
|
1837
|
+
const preferOriginal = options.preferOriginal === true || isGeneratedImageNode;
|
|
1838
|
+
const assetUrls = getNodeImageAssetUrls(nodeModel);
|
|
1839
|
+
bodyMesh.userData ??= {};
|
|
1840
|
+
const now = performance.now();
|
|
1841
|
+
const pendingImageRenderKey = String(bodyMesh.userData.pendingImageRenderKey || '');
|
|
1842
|
+
const resolvedImageRenderKey = String(bodyMesh.userData.resolvedImageRenderKey || '');
|
|
1843
|
+
const fullImageRetryAfter = Number(bodyMesh.userData.fullImageRetryAfter || 0);
|
|
1844
|
+
const failedImageRenderKey = String(bodyMesh.userData.failedImageRenderKey || '');
|
|
1845
|
+
const failedImageRetryAfter = Number(bodyMesh.userData.failedImageRetryAfter || 0);
|
|
1846
|
+
const hasDistinctFullResUrl =
|
|
1847
|
+
!!assetUrls.fullResUrl &&
|
|
1848
|
+
assetUrls.fullResUrl !== assetUrls.previewUrl;
|
|
1849
|
+
const shouldHoldPreview =
|
|
1850
|
+
preferOriginal &&
|
|
1851
|
+
hasDistinctFullResUrl &&
|
|
1852
|
+
(
|
|
1853
|
+
fullImageRetryAfter > now ||
|
|
1854
|
+
pendingImageRenderKey.startsWith('image:preview:') ||
|
|
1855
|
+
(!resolvedImageRenderKey.startsWith('image:preview:') &&
|
|
1856
|
+
!resolvedImageRenderKey.startsWith('image:full:'))
|
|
1857
|
+
);
|
|
1858
|
+
const useOriginalForThisPass = preferOriginal && !shouldHoldPreview;
|
|
1859
|
+
const imageUrl = useOriginalForThisPass
|
|
1860
|
+
? (assetUrls.fullResUrl || assetUrls.previewUrl)
|
|
1861
|
+
: assetUrls.previewUrl;
|
|
1862
|
+
const resizeWidth = getImageTextureResizeWidth(width, height, useOriginalForThisPass, options.resizeWidth);
|
|
1863
|
+
const isLoading = !!(nodeModel.isLoading ?? nodeModel.IsLoading ?? false);
|
|
1864
|
+
const metadata = getNodeMetadata(nodeModel) || {};
|
|
1865
|
+
const nodeId = String(nodeModel.id ?? nodeModel.Id ?? '').trim();
|
|
1866
|
+
const imageError = String(metadata?.ImageGenerationError || '').trim();
|
|
1867
|
+
const renderKey = imageUrl
|
|
1868
|
+
? `image:${useOriginalForThisPass ? 'full' : 'preview'}:${resizeWidth}:${imageUrl}`
|
|
1869
|
+
: `placeholder:${isLoading ? 'loading' : (imageError ? 'error' : 'empty')}:${Math.round(width)}x${Math.round(height)}`;
|
|
1870
|
+
if (failedImageRenderKey === renderKey && failedImageRetryAfter > now) {
|
|
1871
|
+
if (imageUrl) {
|
|
1872
|
+
applyImageCoverUv(bodyMesh, width, height);
|
|
1873
|
+
}
|
|
1874
|
+
return;
|
|
1875
|
+
}
|
|
1876
|
+
if (bodyMesh.userData.imageRenderKey === renderKey) {
|
|
1877
|
+
if (imageUrl) {
|
|
1878
|
+
applyImageCoverUv(bodyMesh, width, height);
|
|
1879
|
+
}
|
|
1880
|
+
return;
|
|
1881
|
+
}
|
|
1882
|
+
|
|
1883
|
+
bodyMesh.userData.imageRenderKey = renderKey;
|
|
1884
|
+
|
|
1885
|
+
const preserveResolvedPreviewTexture = (retryAfter = 0) => {
|
|
1886
|
+
bodyMesh.userData.pendingImageUrl = '';
|
|
1887
|
+
bodyMesh.userData.pendingImageRenderKey = '';
|
|
1888
|
+
bodyMesh.userData.imageRenderKey = resolvedImageRenderKey;
|
|
1889
|
+
bodyMesh.userData.failedImageRenderKey = renderKey;
|
|
1890
|
+
const retryAt = Number(retryAfter || 0);
|
|
1891
|
+
bodyMesh.userData.failedImageRetryAfter = retryAt > performance.now()
|
|
1892
|
+
? retryAt
|
|
1893
|
+
: performance.now() + 1500;
|
|
1894
|
+
bodyMesh.userData.fullImageRetryAfter = bodyMesh.userData.failedImageRetryAfter;
|
|
1895
|
+
};
|
|
1896
|
+
|
|
1897
|
+
const applyImageFallbackTexture = (retryAfter = 0) => {
|
|
1898
|
+
bodyMesh.userData.pendingImageUrl = '';
|
|
1899
|
+
bodyMesh.userData.pendingImageRenderKey = '';
|
|
1900
|
+
bodyMesh.userData.resolvedImageRenderKey = '';
|
|
1901
|
+
bodyMesh.userData.failedImageRenderKey = renderKey;
|
|
1902
|
+
const retryAt = Number(retryAfter || 0);
|
|
1903
|
+
bodyMesh.userData.failedImageRetryAfter = retryAt > performance.now()
|
|
1904
|
+
? retryAt
|
|
1905
|
+
: performance.now() + 1500;
|
|
1906
|
+
bodyMesh.userData.fullImageRetryAfter = bodyMesh.userData.failedImageRetryAfter;
|
|
1907
|
+
const fallbackTexture = createImageStatusTexture(
|
|
1908
|
+
{
|
|
1909
|
+
...nodeModel,
|
|
1910
|
+
isLoading: false,
|
|
1911
|
+
metadata: {
|
|
1912
|
+
...(nodeModel.metadata ?? nodeModel.Metadata ?? {}),
|
|
1913
|
+
ImageGenerationError: imageError || 'Image failed'
|
|
1914
|
+
}
|
|
1915
|
+
},
|
|
1916
|
+
width,
|
|
1917
|
+
height);
|
|
1918
|
+
|
|
1919
|
+
if (fallbackTexture) {
|
|
1920
|
+
swapImageBodyTexture(bodyMesh, fallbackTexture);
|
|
1921
|
+
}
|
|
1922
|
+
};
|
|
1923
|
+
|
|
1924
|
+
if (!imageUrl) {
|
|
1925
|
+
bodyMesh.userData.pendingImageUrl = '';
|
|
1926
|
+
bodyMesh.userData.pendingImageRenderKey = '';
|
|
1927
|
+
bodyMesh.userData.resolvedImageRenderKey = '';
|
|
1928
|
+
bodyMesh.userData.fullImageRetryAfter = 0;
|
|
1929
|
+
bodyMesh.userData.failedImageRenderKey = '';
|
|
1930
|
+
bodyMesh.userData.failedImageRetryAfter = 0;
|
|
1931
|
+
clearImageBodyTexture(bodyMesh, 0xf8fafc);
|
|
1932
|
+
return;
|
|
1933
|
+
}
|
|
1934
|
+
|
|
1935
|
+
const textureFactory = window.MindMapTextureFactory;
|
|
1936
|
+
if (!textureFactory?.ensureImageCached) {
|
|
1937
|
+
applyImageFallbackTexture();
|
|
1938
|
+
return;
|
|
1939
|
+
}
|
|
1940
|
+
|
|
1941
|
+
const stickyFailedImage = getFailedImageUrlEntry(imageUrl);
|
|
1942
|
+
if (stickyFailedImage) {
|
|
1943
|
+
if (useOriginalForThisPass && resolvedImageRenderKey.startsWith('image:preview:')) {
|
|
1944
|
+
preserveResolvedPreviewTexture(stickyFailedImage.retryAfter);
|
|
1945
|
+
applyImageCoverUv(bodyMesh, width, height);
|
|
1946
|
+
return;
|
|
1947
|
+
}
|
|
1948
|
+
|
|
1949
|
+
applyImageFallbackTexture(stickyFailedImage.retryAfter);
|
|
1950
|
+
return;
|
|
1951
|
+
}
|
|
1952
|
+
|
|
1953
|
+
const activeImageCache = window.MindMapNodes?.imageCache || imageCache;
|
|
1954
|
+
|
|
1955
|
+
bodyMesh.userData.pendingImageUrl = imageUrl;
|
|
1956
|
+
bodyMesh.userData.pendingImageRenderKey = renderKey;
|
|
1957
|
+
|
|
1958
|
+
textureFactory.ensureImageCached(nodeId, imageUrl, activeImageCache, {
|
|
1959
|
+
authToken: options.authToken || window.mindMap?.authToken || '',
|
|
1960
|
+
resizeWidth,
|
|
1961
|
+
preferOriginal: useOriginalForThisPass,
|
|
1962
|
+
allowOriginalFallback: useOriginalForThisPass,
|
|
1963
|
+
allowOriginalReuseForPreview: useOriginalForThisPass,
|
|
1964
|
+
originalUrl: assetUrls.fullResUrl || assetUrls.originalUrl || '',
|
|
1965
|
+
onMissingAsset: (missingUrl) => {
|
|
1966
|
+
const normalized = normalizeImageAssetUrl(missingUrl);
|
|
1967
|
+
if (!normalized) return;
|
|
1968
|
+
markNodeImageAssetMissing(nodeModel, normalized);
|
|
1969
|
+
try { window.dotNetHelper?.invokeMethodAsync('ReportMissingNodeAsset', nodeId, normalized); } catch { }
|
|
1970
|
+
},
|
|
1971
|
+
onRefreshedUrl: (refreshedUrl) => {
|
|
1972
|
+
const normalized = normalizeImageAssetUrl(refreshedUrl);
|
|
1973
|
+
if (!normalized) return;
|
|
1974
|
+
|
|
1975
|
+
setNodeImageOriginalUrl(nodeModel, normalized);
|
|
1976
|
+
if (!hasNodeImagePreviewSource(assetUrls)) {
|
|
1977
|
+
setNodeImagePreviewUrl(nodeModel, normalized);
|
|
1978
|
+
}
|
|
1979
|
+
}
|
|
1980
|
+
})
|
|
1981
|
+
.then((cachedImage) => {
|
|
1982
|
+
if (bodyMesh.userData?.pendingImageRenderKey !== renderKey) {
|
|
1983
|
+
return;
|
|
1984
|
+
}
|
|
1985
|
+
|
|
1986
|
+
if (!cachedImage || cachedImage.isError === true || !cachedImage.image) {
|
|
1987
|
+
const retryAfter = rememberFailedImageUrl(
|
|
1988
|
+
cachedImage?.url || imageUrl,
|
|
1989
|
+
cachedImage?.errorStatus || 0,
|
|
1990
|
+
cachedImage?.retryAfter || 0
|
|
1991
|
+
);
|
|
1992
|
+
if (useOriginalForThisPass && resolvedImageRenderKey.startsWith('image:preview:')) {
|
|
1993
|
+
preserveResolvedPreviewTexture(retryAfter);
|
|
1994
|
+
return;
|
|
1995
|
+
}
|
|
1996
|
+
applyImageFallbackTexture(retryAfter);
|
|
1997
|
+
return;
|
|
1998
|
+
}
|
|
1999
|
+
|
|
2000
|
+
const nextTexture = createImageBodyTextureFromCache(cachedImage);
|
|
2001
|
+
if (!nextTexture) {
|
|
2002
|
+
const retryAfter = rememberFailedImageUrl(
|
|
2003
|
+
cachedImage?.url || imageUrl,
|
|
2004
|
+
cachedImage?.errorStatus || 0,
|
|
2005
|
+
cachedImage?.retryAfter || 0
|
|
2006
|
+
);
|
|
2007
|
+
if (useOriginalForThisPass && resolvedImageRenderKey.startsWith('image:preview:')) {
|
|
2008
|
+
preserveResolvedPreviewTexture(retryAfter);
|
|
2009
|
+
return;
|
|
2010
|
+
}
|
|
2011
|
+
applyImageFallbackTexture(retryAfter);
|
|
2012
|
+
return;
|
|
2013
|
+
}
|
|
2014
|
+
|
|
2015
|
+
bodyMesh.userData.assetRefreshAttemptedFor = '';
|
|
2016
|
+
bodyMesh.userData.pendingImageUrl = '';
|
|
2017
|
+
bodyMesh.userData.pendingImageRenderKey = '';
|
|
2018
|
+
bodyMesh.userData.resolvedImageRenderKey = renderKey;
|
|
2019
|
+
bodyMesh.userData.fullImageRetryAfter = 0;
|
|
2020
|
+
bodyMesh.userData.failedImageRenderKey = '';
|
|
2021
|
+
bodyMesh.userData.failedImageRetryAfter = 0;
|
|
2022
|
+
clearFailedImageUrl(cachedImage.url || imageUrl);
|
|
2023
|
+
swapImageBodyTexture(bodyMesh, nextTexture);
|
|
2024
|
+
applyImageCoverUv(bodyMesh, width, height);
|
|
2025
|
+
})
|
|
2026
|
+
.catch(() => {
|
|
2027
|
+
if (bodyMesh.userData?.pendingImageRenderKey !== renderKey) {
|
|
2028
|
+
return;
|
|
2029
|
+
}
|
|
2030
|
+
bodyMesh.userData.pendingImageUrl = '';
|
|
2031
|
+
bodyMesh.userData.pendingImageRenderKey = '';
|
|
2032
|
+
|
|
2033
|
+
if (useOriginalForThisPass && resolvedImageRenderKey.startsWith('image:preview:')) {
|
|
2034
|
+
preserveResolvedPreviewTexture(rememberFailedImageUrl(imageUrl, 0));
|
|
2035
|
+
return;
|
|
2036
|
+
}
|
|
2037
|
+
|
|
2038
|
+
applyImageFallbackTexture(rememberFailedImageUrl(imageUrl, 0));
|
|
2039
|
+
});
|
|
2040
|
+
}
|
|
2041
|
+
|
|
2042
|
+
async function addNode(module, nodeModel) {
|
|
2043
|
+
if (!nodeModel) return;
|
|
2044
|
+
|
|
2045
|
+
// Normalize model shape from both JS (camelCase) and C# interop (PascalCase).
|
|
2046
|
+
const normalizedContentType = nodeModel.contentType ?? nodeModel.ContentType ?? 'text';
|
|
2047
|
+
const fallbackWidth = normalizedContentType === 'video'
|
|
2048
|
+
? 470
|
|
2049
|
+
: (normalizedContentType === 'image' ? 300 : (module.aiNodeDefaultWidth || 400));
|
|
2050
|
+
const fallbackHeight = normalizedContentType === 'video'
|
|
2051
|
+
? 264
|
|
2052
|
+
: (normalizedContentType === 'image'
|
|
2053
|
+
? 300
|
|
2054
|
+
: (normalizedContentType === 'pdf' ? 56 : (module.aiNodeDefaultHeight || 200)));
|
|
2055
|
+
|
|
2056
|
+
const parsedWidth = Number(nodeModel.width ?? nodeModel.Width ?? fallbackWidth);
|
|
2057
|
+
const parsedHeight = Number(nodeModel.height ?? nodeModel.Height ?? fallbackHeight);
|
|
2058
|
+
|
|
2059
|
+
nodeModel = {
|
|
2060
|
+
...nodeModel,
|
|
2061
|
+
id: String(nodeModel.id ?? nodeModel.Id ?? ''),
|
|
2062
|
+
contentType: normalizedContentType,
|
|
2063
|
+
prompt: nodeModel.prompt ?? nodeModel.Prompt ?? '',
|
|
2064
|
+
response: nodeModel.response ?? nodeModel.Response ?? '',
|
|
2065
|
+
positionX: Number(nodeModel.positionX ?? nodeModel.PositionX ?? 0),
|
|
2066
|
+
positionY: Number(nodeModel.positionY ?? nodeModel.PositionY ?? 0),
|
|
2067
|
+
positionZ: Number(nodeModel.positionZ ?? nodeModel.PositionZ ?? 0),
|
|
2068
|
+
isLoading: !!(nodeModel.isLoading ?? nodeModel.IsLoading ?? false),
|
|
2069
|
+
width: (Number.isFinite(parsedWidth) && parsedWidth > 0) ? parsedWidth : fallbackWidth,
|
|
2070
|
+
height: (Number.isFinite(parsedHeight) && parsedHeight > 0) ? parsedHeight : fallbackHeight,
|
|
2071
|
+
isChunkedText: !!(nodeModel.isChunkedText ?? nodeModel.IsChunkedText ?? false),
|
|
2072
|
+
totalLineCount: Number(nodeModel.totalLineCount ?? nodeModel.TotalLineCount ?? 0),
|
|
2073
|
+
sourceFilePath: nodeModel.sourceFilePath ?? nodeModel.SourceFilePath ?? null,
|
|
2074
|
+
scrollOffset: Number(nodeModel.scrollOffset ?? nodeModel.ScrollOffset ?? 0),
|
|
2075
|
+
maxScroll: Number(nodeModel.maxScroll ?? nodeModel.MaxScroll ?? 0),
|
|
2076
|
+
isExpanded: !!(nodeModel.isExpanded ?? nodeModel.IsExpanded ?? false),
|
|
2077
|
+
originalHeight: Number(nodeModel.originalHeight ?? nodeModel.OriginalHeight ?? 0),
|
|
2078
|
+
metadata: nodeModel.metadata ?? nodeModel.Metadata ?? {}
|
|
2079
|
+
};
|
|
2080
|
+
|
|
2081
|
+
if (normalizedContentType === 'image') {
|
|
2082
|
+
ensureNodeImageUsesPreviewResponse(nodeModel);
|
|
2083
|
+
}
|
|
2084
|
+
|
|
2085
|
+
if (!nodeModel.id) {
|
|
2086
|
+
console.warn('[MindMapNodes] addNode skipped: missing node id');
|
|
2087
|
+
return;
|
|
2088
|
+
}
|
|
2089
|
+
|
|
2090
|
+
const nodeStartTime = performance.now();
|
|
2091
|
+
const timings = {}; // 세부 시간 측정용
|
|
2092
|
+
log(`[addNode] ENTER - Node ID: ${nodeModel.id}, Type: ${nodeModel.contentType}`);
|
|
2093
|
+
|
|
2094
|
+
// ▼▼▼ [Fix] Capture active board ID to detect stale async work after tab switch ▼▼▼
|
|
2095
|
+
const startBoardId = window.mindMap?.activeBoardId || null;
|
|
2096
|
+
const _isBoardStale = () => {
|
|
2097
|
+
const current = window.mindMap?.activeBoardId;
|
|
2098
|
+
return current != null && startBoardId != null && current !== startBoardId;
|
|
2099
|
+
};
|
|
2100
|
+
// ▲▲▲ [Fix] ▲▲▲
|
|
2101
|
+
|
|
2102
|
+
// ▼▼▼ [New] Ensure scrollbar styles are injected ▼▼▼
|
|
2103
|
+
injectScrollbarStyles();
|
|
2104
|
+
// ▲▲▲ [New] ▲▲▲
|
|
2105
|
+
|
|
2106
|
+
// ★ [Add] Safeguard: abort if factory is not loaded yet (avoid crash)
|
|
2107
|
+
// ★ [Add] Safeguard: abort if factory is not loaded yet (avoid crash)
|
|
2108
|
+
// ★ [수정] 텍스처가 필요 없는 CSS3D 타입(text, note, code, markdown)은 팩토리가 없어도 통과시킵니다.
|
|
2109
|
+
const isCss3dType = nodeModel.contentType === 'text' || nodeModel.contentType === 'note' || nodeModel.contentType === 'memo' || nodeModel.contentType === 'code' || nodeModel.contentType === 'markdown' || nodeModel.contentType === 'embed';
|
|
2110
|
+
|
|
2111
|
+
if (!isCss3dType && typeof window.MindMapTextureFactory === 'undefined') {
|
|
2112
|
+
console.error(`[MindMapNodes] Critical: MindMapTextureFactory is missing! Cannot add node ${nodeModel.id}. Retrying in 100ms...`);
|
|
2113
|
+
// Retry after 100ms (addresses a load race condition)
|
|
2114
|
+
setTimeout(() => {
|
|
2115
|
+
if (_isBoardStale()) { log(`[addNode] Board switched, aborting retry for ${nodeModel.id}`); return; }
|
|
2116
|
+
addNode(module, nodeModel);
|
|
2117
|
+
}, 100);
|
|
2118
|
+
return;
|
|
2119
|
+
}
|
|
2120
|
+
|
|
2121
|
+
if (module.nodeObjectsById.has(nodeModel.id)) {
|
|
2122
|
+
log(`[MindMapNodes] Node already exists: ${nodeModel.id}. Skipping (expected during board reload).`);
|
|
2123
|
+
const existingEntry = module.nodeObjectsById.get(nodeModel.id);
|
|
2124
|
+
if (existingEntry) {
|
|
2125
|
+
if (nodeModel.contentType === 'image') {
|
|
2126
|
+
const imageAssetUrls = getNodeImageAssetUrls(nodeModel);
|
|
2127
|
+
if (imageAssetUrls.previewUrl) {
|
|
2128
|
+
await window.MindMapTextureFactory.ensureImageCached(nodeModel.id, imageAssetUrls.previewUrl, imageCache, {
|
|
2129
|
+
authToken: module.authToken,
|
|
2130
|
+
allowOriginalFallback: false,
|
|
2131
|
+
allowOriginalReuseForPreview: false,
|
|
2132
|
+
originalUrl: imageAssetUrls.fullResUrl || imageAssetUrls.originalUrl || '',
|
|
2133
|
+
onRefreshedUrl: (refreshedUrl) => {
|
|
2134
|
+
setNodeImageOriginalUrl(nodeModel, refreshedUrl);
|
|
2135
|
+
if (!hasNodeImagePreviewSource(imageAssetUrls)) {
|
|
2136
|
+
setNodeImagePreviewUrl(nodeModel, refreshedUrl);
|
|
2137
|
+
}
|
|
2138
|
+
if (existingEntry.model) {
|
|
2139
|
+
setNodeImageOriginalUrl(existingEntry.model, refreshedUrl);
|
|
2140
|
+
const existingAssetUrls = getNodeImageAssetUrls(existingEntry.model);
|
|
2141
|
+
if (!hasNodeImagePreviewSource(existingAssetUrls)) {
|
|
2142
|
+
setNodeImagePreviewUrl(existingEntry.model, refreshedUrl);
|
|
2143
|
+
}
|
|
2144
|
+
}
|
|
2145
|
+
}
|
|
2146
|
+
});
|
|
2147
|
+
}
|
|
2148
|
+
}
|
|
2149
|
+
if (existingEntry.glObject) {
|
|
2150
|
+
// [Fix] Force Z=0 to eliminate parallax
|
|
2151
|
+
existingEntry.glObject.position.set(nodeModel.positionX, nodeModel.positionY, 0);
|
|
2152
|
+
}
|
|
2153
|
+
if (existingEntry.cssObject) {
|
|
2154
|
+
// [Fix] Force Z=0 to eliminate parallax
|
|
2155
|
+
const cssParentScene = module.cssScene || module.scene;
|
|
2156
|
+
if (cssParentScene && existingEntry.cssObject.parent !== cssParentScene) {
|
|
2157
|
+
cssParentScene.add(existingEntry.cssObject);
|
|
2158
|
+
window.MindMapCss3DManager?.markCssParentRepairNeeded?.(module, nodeModel.id);
|
|
2159
|
+
}
|
|
2160
|
+
existingEntry.cssObject.position.set(nodeModel.positionX, nodeModel.positionY, 0);
|
|
2161
|
+
existingEntry.cssObject.updateMatrixWorld(true);
|
|
2162
|
+
window.MindMapCss3DManager?.markNodeTransformDirty?.(module, nodeModel.id);
|
|
2163
|
+
}
|
|
2164
|
+
}
|
|
2165
|
+
return;
|
|
2166
|
+
}
|
|
2167
|
+
|
|
2168
|
+
timings.t1_init = performance.now();
|
|
2169
|
+
log(`[addNode] Step 1: Preparing position for ${nodeModel.id}`);
|
|
2170
|
+
let autoPositioned = false;
|
|
2171
|
+
const isNewNode = nodeModel.metadata && nodeModel.metadata['IsNew'] === 'true';
|
|
2172
|
+
|
|
2173
|
+
if (isNewNode && (nodeModel.contentType === 'note' || nodeModel.contentType === 'text' || nodeModel.contentType === 'memo' || nodeModel.contentType === 'image' || nodeModel.contentType === 'video' || nodeModel.contentType === 'embed') && nodeModel.positionX === 0 && nodeModel.positionY === 0) {
|
|
2174
|
+
if (module.cursorPosition) {
|
|
2175
|
+
nodeModel.positionX = module.cursorPosition.x;
|
|
2176
|
+
nodeModel.positionY = module.cursorPosition.y;
|
|
2177
|
+
autoPositioned = true;
|
|
2178
|
+
} else {
|
|
2179
|
+
const targetPos = new THREE.Vector3(0, 0, 0);
|
|
2180
|
+
module.raycaster.setFromCamera({ x: 0, y: 0.4 }, module.camera);
|
|
2181
|
+
module.raycaster.ray.intersectPlane(module.plane, targetPos);
|
|
2182
|
+
nodeModel.positionX = module.snapToGrid(targetPos.x - (nodeModel.width || 400) / 2);
|
|
2183
|
+
nodeModel.positionY = module.snapToGridY(targetPos.y + (nodeModel.height || 200) / 2);
|
|
2184
|
+
autoPositioned = true;
|
|
2185
|
+
}
|
|
2186
|
+
}
|
|
2187
|
+
|
|
2188
|
+
const nodeZ = 0;
|
|
2189
|
+
|
|
2190
|
+
const MAX_SIZE = 4096; // hard limit
|
|
2191
|
+
|
|
2192
|
+
let width = (nodeModel.width ?? nodeModel.Width) || (nodeModel.contentType === 'embed' ? 460 : (nodeModel.contentType === 'video' ? 470 : (nodeModel.contentType === 'image' ? 300 : (module.aiNodeDefaultWidth || 400))));
|
|
2193
|
+
let height = (nodeModel.height ?? nodeModel.Height) || (nodeModel.contentType === 'embed' ? 260 : (nodeModel.contentType === 'video' ? 264 : (nodeModel.contentType === 'image' ? 300 : (nodeModel.contentType === 'pdf' ? 56 : (module.aiNodeDefaultHeight || 200)))));
|
|
2194
|
+
|
|
2195
|
+
// ▼▼▼ [Fix] Initial size cap ▼▼▼
|
|
2196
|
+
width = Math.min(width, MAX_SIZE);
|
|
2197
|
+
height = Math.min(height, MAX_SIZE);
|
|
2198
|
+
nodeModel.width = width;
|
|
2199
|
+
nodeModel.Width = width;
|
|
2200
|
+
nodeModel.height = height;
|
|
2201
|
+
nodeModel.Height = height;
|
|
2202
|
+
// ▲▲▲ [Fix] ▲▲▲
|
|
2203
|
+
|
|
2204
|
+
const zoomLevel = 2;
|
|
2205
|
+
|
|
2206
|
+
log(`[addNode] Step 2: Measuring height for ${nodeModel.id} (type: ${nodeModel.contentType})`);
|
|
2207
|
+
const isTextLikeNode = nodeModel.contentType === 'text' || nodeModel.contentType === 'note';
|
|
2208
|
+
const hasResponseText = !!(`${nodeModel.response ?? nodeModel.Response ?? ''}`.trim());
|
|
2209
|
+
const isLoadingWithoutContent = !!nodeModel.isLoading && !hasResponseText;
|
|
2210
|
+
|
|
2211
|
+
// Keep newly-created AI nodes at configured default height while loading.
|
|
2212
|
+
// This prevents one-line collapse before the first streamed tokens arrive.
|
|
2213
|
+
if (isTextLikeNode && isLoadingWithoutContent) {
|
|
2214
|
+
const minLoadingHeight = module.aiNodeDefaultHeight || 200;
|
|
2215
|
+
height = Math.max(height, minLoadingHeight);
|
|
2216
|
+
nodeModel.height = height;
|
|
2217
|
+
nodeModel.maxScroll = 0;
|
|
2218
|
+
nodeModel.scrollOffset = 0;
|
|
2219
|
+
} else if (isTextLikeNode) {
|
|
2220
|
+
try {
|
|
2221
|
+
// ▼▼▼ [Refactor] Chunk-loading node handling - fetch content directly from C# (no cache) ▼▼▼
|
|
2222
|
+
if (nodeModel.isChunkedText && nodeModel.totalLineCount > 0) {
|
|
2223
|
+
// For chunk-loading nodes, compute virtual scroll height from totalLineCount
|
|
2224
|
+
const LINE_HEIGHT = 21; // 14px font * 1.5 line-height
|
|
2225
|
+
const totalContentHeight = nodeModel.totalLineCount * LINE_HEIGHT;
|
|
2226
|
+
nodeModel.totalContentHeight = totalContentHeight;
|
|
2227
|
+
|
|
2228
|
+
// Compute virtual scroll height
|
|
2229
|
+
nodeModel.maxScroll = Math.max(0, totalContentHeight - height);
|
|
2230
|
+
nodeModel.scrollOffset = Math.min(nodeModel.scrollOffset || 0, nodeModel.maxScroll);
|
|
2231
|
+
|
|
2232
|
+
// Fetch content from C# for the current scroll position
|
|
2233
|
+
if (module.dotNetHelper) {
|
|
2234
|
+
// ▼▼▼ [Fix] Fast initial retries; self-heal in the background ▼▼▼
|
|
2235
|
+
const MAX_RETRIES = 2; // 5 → 2 (background retries also run)
|
|
2236
|
+
const RETRY_DELAY = 200; // 500 → 200ms
|
|
2237
|
+
|
|
2238
|
+
// ▼▼▼ [Fix] If it fails at the saved scroll position, retry at scrollOffset=0 ▼▼▼
|
|
2239
|
+
const savedScrollOffset = nodeModel.scrollOffset || 0;
|
|
2240
|
+
let loadAttemptScrollOffsets = [savedScrollOffset];
|
|
2241
|
+
|
|
2242
|
+
// If the saved scroll position is not 0, also try 0
|
|
2243
|
+
if (savedScrollOffset > 0) {
|
|
2244
|
+
loadAttemptScrollOffsets.push(0);
|
|
2245
|
+
}
|
|
2246
|
+
|
|
2247
|
+
for (const tryScrollOffset of loadAttemptScrollOffsets) {
|
|
2248
|
+
for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
|
|
2249
|
+
try {
|
|
2250
|
+
log(`[addNode] Chunked node ${nodeModel.id}: fetching content for scroll position ${tryScrollOffset}px (attempt ${attempt}/${MAX_RETRIES})...`);
|
|
2251
|
+
const result = await module.dotNetHelper.invokeMethodAsync(
|
|
2252
|
+
'GetTextByScrollPosition',
|
|
2253
|
+
nodeModel.id,
|
|
2254
|
+
tryScrollOffset,
|
|
2255
|
+
height,
|
|
2256
|
+
LINE_HEIGHT
|
|
2257
|
+
);
|
|
2258
|
+
|
|
2259
|
+
if (result && result.content) {
|
|
2260
|
+
nodeModel.response = result.content;
|
|
2261
|
+
nodeModel._visibleContent = result.content;
|
|
2262
|
+
nodeModel._visibleScrollOffset = result.relativeScrollOffset;
|
|
2263
|
+
|
|
2264
|
+
// If loaded at 0, also set scroll position to 0
|
|
2265
|
+
if (tryScrollOffset === 0 && savedScrollOffset > 0) {
|
|
2266
|
+
nodeModel.scrollOffset = 0;
|
|
2267
|
+
log(`[addNode] Content loaded from scroll=0 (saved=${savedScrollOffset}px). Will scroll later after file is registered.`);
|
|
2268
|
+
}
|
|
2269
|
+
|
|
2270
|
+
log(`[addNode] Content loaded for ${nodeModel.id}: ${result.content.length} chars, startLine=${result.startLine}`);
|
|
2271
|
+
break;
|
|
2272
|
+
} else {
|
|
2273
|
+
if (attempt < MAX_RETRIES) {
|
|
2274
|
+
log(`[addNode] Result is null, retrying in ${RETRY_DELAY}ms...`);
|
|
2275
|
+
await new Promise(r => setTimeout(r, RETRY_DELAY));
|
|
2276
|
+
}
|
|
2277
|
+
}
|
|
2278
|
+
} catch (err) {
|
|
2279
|
+
console.warn(`[addNode] Failed to load content for ${nodeModel.id} (attempt ${attempt}):`, err);
|
|
2280
|
+
if (attempt < MAX_RETRIES) {
|
|
2281
|
+
await new Promise(r => setTimeout(r, RETRY_DELAY));
|
|
2282
|
+
}
|
|
2283
|
+
}
|
|
2284
|
+
}
|
|
2285
|
+
|
|
2286
|
+
// If content was loaded, exit loops
|
|
2287
|
+
if (nodeModel._visibleContent) break;
|
|
2288
|
+
|
|
2289
|
+
// Log before trying the next scroll offset
|
|
2290
|
+
if (tryScrollOffset !== 0) {
|
|
2291
|
+
log(`[addNode] Failed at scroll=${tryScrollOffset}px, trying scroll=0...`);
|
|
2292
|
+
}
|
|
2293
|
+
}
|
|
2294
|
+
// ▲▲▲ [Fix] ▲▲▲
|
|
2295
|
+
}
|
|
2296
|
+
|
|
2297
|
+
// If initial load fails, show a fallback message
|
|
2298
|
+
if (!nodeModel._visibleContent) {
|
|
2299
|
+
nodeModel._visibleContent = `Loading... (${nodeModel.totalLineCount} lines)`;
|
|
2300
|
+
nodeModel.response = nodeModel._visibleContent;
|
|
2301
|
+
log(`[addNode] ⚠️ Using fallback message for ${nodeModel.id}: "${nodeModel._visibleContent}"`);
|
|
2302
|
+
|
|
2303
|
+
// ▼▼▼ [New] Periodic retry logic - the node retries content loading by itself ▼▼▼
|
|
2304
|
+
const RETRY_INTERVAL = 2000; // retry every 2 seconds
|
|
2305
|
+
const MAX_BACKGROUND_RETRIES = 30; // max 30 attempts (~1 minute)
|
|
2306
|
+
let backgroundRetryCount = 0;
|
|
2307
|
+
const savedScrollOffset = nodeModel.scrollOffset || 0; // preserve saved scroll position
|
|
2308
|
+
|
|
2309
|
+
const retryLoadContent = async () => {
|
|
2310
|
+
// ▼▼▼ [Fix] Abort if board switched since addNode started ▼▼▼
|
|
2311
|
+
if (_isBoardStale()) {
|
|
2312
|
+
log(`[addNode] Board switched, aborting background retry for ${nodeModel.id}`);
|
|
2313
|
+
return;
|
|
2314
|
+
}
|
|
2315
|
+
// ▲▲▲ [Fix] ▲▲▲
|
|
2316
|
+
backgroundRetryCount++;
|
|
2317
|
+
log(`[addNode] 🔄 Background retry ${backgroundRetryCount}/${MAX_BACKGROUND_RETRIES} for ${nodeModel.id}...`);
|
|
2318
|
+
|
|
2319
|
+
try {
|
|
2320
|
+
// Start at 0 first to check whether the file is registered
|
|
2321
|
+
const result = await module.dotNetHelper.invokeMethodAsync(
|
|
2322
|
+
'GetTextByScrollPosition',
|
|
2323
|
+
nodeModel.id,
|
|
2324
|
+
0,
|
|
2325
|
+
height,
|
|
2326
|
+
LINE_HEIGHT
|
|
2327
|
+
);
|
|
2328
|
+
if (_isBoardStale()) {
|
|
2329
|
+
log(`[addNode] Board switched after background fetch, aborting retry for ${nodeModel.id}`);
|
|
2330
|
+
return;
|
|
2331
|
+
}
|
|
2332
|
+
|
|
2333
|
+
if (result && result.content) {
|
|
2334
|
+
log(`[addNode] ✅ Background load SUCCESS for ${nodeModel.id}: ${result.content.length} chars`);
|
|
2335
|
+
|
|
2336
|
+
// If a saved scroll position exists, jump to that position
|
|
2337
|
+
if (savedScrollOffset > 0) {
|
|
2338
|
+
log(`[addNode] 📍 Restoring saved scroll position: ${savedScrollOffset}px`);
|
|
2339
|
+
try {
|
|
2340
|
+
const savedResult = await module.dotNetHelper.invokeMethodAsync(
|
|
2341
|
+
'GetTextByScrollPosition',
|
|
2342
|
+
nodeModel.id,
|
|
2343
|
+
savedScrollOffset,
|
|
2344
|
+
height,
|
|
2345
|
+
LINE_HEIGHT
|
|
2346
|
+
);
|
|
2347
|
+
if (_isBoardStale()) {
|
|
2348
|
+
log(`[addNode] Board switched after saved-position fetch, aborting retry for ${nodeModel.id}`);
|
|
2349
|
+
return;
|
|
2350
|
+
}
|
|
2351
|
+
|
|
2352
|
+
if (savedResult && savedResult.content) {
|
|
2353
|
+
nodeModel.response = savedResult.content;
|
|
2354
|
+
nodeModel._visibleContent = savedResult.content;
|
|
2355
|
+
nodeModel._visibleScrollOffset = savedResult.relativeScrollOffset;
|
|
2356
|
+
nodeModel.scrollOffset = savedScrollOffset;
|
|
2357
|
+
log(`[addNode] ✅ Restored to saved position: ${savedScrollOffset}px`);
|
|
2358
|
+
} else {
|
|
2359
|
+
// If restoring the saved position fails, fall back to scroll=0
|
|
2360
|
+
nodeModel.response = result.content;
|
|
2361
|
+
nodeModel._visibleContent = result.content;
|
|
2362
|
+
nodeModel._visibleScrollOffset = result.relativeScrollOffset;
|
|
2363
|
+
nodeModel.scrollOffset = 0;
|
|
2364
|
+
}
|
|
2365
|
+
} catch (err) {
|
|
2366
|
+
console.warn(`[addNode] Failed to restore saved position, using scroll=0:`, err);
|
|
2367
|
+
nodeModel.response = result.content;
|
|
2368
|
+
nodeModel._visibleContent = result.content;
|
|
2369
|
+
nodeModel._visibleScrollOffset = result.relativeScrollOffset;
|
|
2370
|
+
nodeModel.scrollOffset = 0;
|
|
2371
|
+
}
|
|
2372
|
+
} else {
|
|
2373
|
+
nodeModel.response = result.content;
|
|
2374
|
+
nodeModel._visibleContent = result.content;
|
|
2375
|
+
nodeModel._visibleScrollOffset = result.relativeScrollOffset;
|
|
2376
|
+
nodeModel.scrollOffset = 0;
|
|
2377
|
+
}
|
|
2378
|
+
|
|
2379
|
+
if (_isBoardStale()) {
|
|
2380
|
+
log(`[addNode] Board switched before applying retry result for ${nodeModel.id}`);
|
|
2381
|
+
return;
|
|
2382
|
+
}
|
|
2383
|
+
|
|
2384
|
+
// Update texture and re-render
|
|
2385
|
+
window.MindMapNodes.pendingNodeUpdates.set(nodeModel.id, {
|
|
2386
|
+
content: nodeModel._visibleContent,
|
|
2387
|
+
width: nodeModel.width,
|
|
2388
|
+
height: nodeModel.height,
|
|
2389
|
+
scrollDelta: 0
|
|
2390
|
+
});
|
|
2391
|
+
await window.MindMapPipeline.processRenderQueue(module, nodeModel.id);
|
|
2392
|
+
return; // stop retrying on success
|
|
2393
|
+
}
|
|
2394
|
+
} catch (err) {
|
|
2395
|
+
console.warn(`[addNode] Background retry failed for ${nodeModel.id}:`, err);
|
|
2396
|
+
}
|
|
2397
|
+
|
|
2398
|
+
// Check if max retry count is exceeded
|
|
2399
|
+
if (backgroundRetryCount < MAX_BACKGROUND_RETRIES) {
|
|
2400
|
+
setTimeout(retryLoadContent, RETRY_INTERVAL);
|
|
2401
|
+
} else {
|
|
2402
|
+
log(`[addNode] ⚠️ Giving up background retries for ${nodeModel.id} after ${MAX_BACKGROUND_RETRIES} attempts.`);
|
|
2403
|
+
}
|
|
2404
|
+
};
|
|
2405
|
+
|
|
2406
|
+
// Kick off the first background retry
|
|
2407
|
+
setTimeout(retryLoadContent, RETRY_INTERVAL);
|
|
2408
|
+
// ▲▲▲ [New] ▲▲▲
|
|
2409
|
+
}
|
|
2410
|
+
|
|
2411
|
+
log(`[MindMapNodes] Chunked text node ${nodeModel.id}: ${nodeModel.totalLineCount} lines, totalContentHeight=${totalContentHeight}, maxScroll=${nodeModel.maxScroll}, hasContent=${!!nodeModel._visibleContent}`);
|
|
2412
|
+
} else {
|
|
2413
|
+
// ▲▲▲ [Refactor] ▲▲▲
|
|
2414
|
+
// Normal text node
|
|
2415
|
+
|
|
2416
|
+
// ▼▼▼ [Optimization] Skip measurement if height was pre-calculated OR already set (loaded from saved board) ▼▼▼
|
|
2417
|
+
const existingHeight = nodeModel.height ?? nodeModel.Height ?? 0;
|
|
2418
|
+
const hasPreCalculatedHeight = nodeModel._heightPreCalculated && existingHeight > 0;
|
|
2419
|
+
const hasSavedHeight = existingHeight > 0 && !nodeModel._heightPreCalculated; // From saved board
|
|
2420
|
+
|
|
2421
|
+
if (hasPreCalculatedHeight) {
|
|
2422
|
+
log(`[addNode] Step 2.1: SKIPPED measureHtmlHeightAsync (pre-calculated height=${existingHeight}, maxScroll=${nodeModel.maxScroll})`);
|
|
2423
|
+
height = existingHeight;
|
|
2424
|
+
nodeModel.scrollOffset = Math.min(nodeModel.scrollOffset || 0, nodeModel.maxScroll || 0);
|
|
2425
|
+
} else if (hasSavedHeight) {
|
|
2426
|
+
// Height was saved from previous session - skip measurement
|
|
2427
|
+
log(`[addNode] Step 2.1: SKIPPED measureHtmlHeightAsync (saved height=${existingHeight})`);
|
|
2428
|
+
height = existingHeight;
|
|
2429
|
+
nodeModel.height = height;
|
|
2430
|
+
// maxScroll may not be set - calculate it now or leave as is
|
|
2431
|
+
if (nodeModel.maxScroll === undefined) {
|
|
2432
|
+
nodeModel.maxScroll = 0; // Will be recalculated if user scrolls
|
|
2433
|
+
}
|
|
2434
|
+
nodeModel.scrollOffset = Math.min(nodeModel.scrollOffset || 0, nodeModel.maxScroll || 0);
|
|
2435
|
+
} else {
|
|
2436
|
+
// If showFullAiResponse is true, do not constrain height
|
|
2437
|
+
const defaultMaxHeight = module.aiNodeDefaultHeight || 200;
|
|
2438
|
+
const effectiveMaxHeight = module.showFullAiResponse ? 10000 : defaultMaxHeight;
|
|
2439
|
+
// ▼▼▼ [Fix] Pass forcedHeight if height is already set (for loaded nodes) ▼▼▼
|
|
2440
|
+
const forcedHeight = (existingHeight > 0) ? (existingHeight * zoomLevel) : null;
|
|
2441
|
+
log(`[addNode] Step 2.1: Calling measureHtmlHeightAsync for ${nodeModel.id}... (forcedHeight=${forcedHeight ? forcedHeight / zoomLevel : 'auto'})`);
|
|
2442
|
+
const { height: autoHeight, maxScrollPx } = await window.MindMapTextureFactory.measureHtmlHeightAsync(nodeModel, zoomLevel, width * zoomLevel, forcedHeight, effectiveMaxHeight);
|
|
2443
|
+
if (_isBoardStale()) { log(`[addNode] Board switched after measureHeight, aborting ${nodeModel.id}`); return; }
|
|
2444
|
+
log(`[addNode] Step 2.2: measureHtmlHeightAsync completed for ${nodeModel.id}. autoHeight=${autoHeight}, maxScrollPx=${maxScrollPx}`);
|
|
2445
|
+
// ▲▲▲ [Fix] ▲▲▲
|
|
2446
|
+
if (!existingHeight || existingHeight <= 0) {
|
|
2447
|
+
height = autoHeight;
|
|
2448
|
+
nodeModel.height = height;
|
|
2449
|
+
} else {
|
|
2450
|
+
// Fixed height should still be capped
|
|
2451
|
+
height = Math.min(existingHeight, MAX_SIZE);
|
|
2452
|
+
nodeModel.height = height;
|
|
2453
|
+
}
|
|
2454
|
+
nodeModel.maxScroll = maxScrollPx / zoomLevel;
|
|
2455
|
+
nodeModel.scrollOffset = Math.min(nodeModel.scrollOffset || 0, nodeModel.maxScroll);
|
|
2456
|
+
log(`[addNode] Step 2.3: maxScroll set to ${nodeModel.maxScroll} for ${nodeModel.id}`);
|
|
2457
|
+
}
|
|
2458
|
+
// ▲▲▲ [Optimization] ▲▲▲
|
|
2459
|
+
}
|
|
2460
|
+
} catch (heightError) {
|
|
2461
|
+
console.error(`🔧 [MindMapNodes] Failed to auto-measure height for ${nodeModel.id}, using defaults.`, heightError);
|
|
2462
|
+
}
|
|
2463
|
+
}
|
|
2464
|
+
|
|
2465
|
+
nodeModel.width = width;
|
|
2466
|
+
nodeModel.height = height;
|
|
2467
|
+
|
|
2468
|
+
const shouldUseDeferredShell = shouldCreateDeferredShellNode(module, nodeModel);
|
|
2469
|
+
|
|
2470
|
+
timings.t2_afterMeasure = performance.now();
|
|
2471
|
+
log(`[addNode] Step 3: Generating textures for ${nodeModel.id}`);
|
|
2472
|
+
// ▼▼▼ [Key Fix] For text/note/code, generate hi-res textures immediately when response exists ▼▼▼
|
|
2473
|
+
// ▼▼▼ [Fix] Chunk nodes prefer _visibleContent when available ▼▼▼
|
|
2474
|
+
const contentForTexture = nodeModel.isChunkedText
|
|
2475
|
+
? (nodeModel._visibleContent || nodeModel.response)
|
|
2476
|
+
: nodeModel.response;
|
|
2477
|
+
|
|
2478
|
+
let textures;
|
|
2479
|
+
|
|
2480
|
+
const isCodeNode = nodeModel.contentType === 'code';
|
|
2481
|
+
|
|
2482
|
+
// ▼▼▼ [Lazy Loading] Always start with placeholder. TextLOD will upgrade visible nodes sequentially.
|
|
2483
|
+
// This prevents main thread freeze during bulk load.
|
|
2484
|
+
// ▼▼▼ [Optimization] Skip expensive WebGL texture generation for CSS3D nodes ▼▼▼
|
|
2485
|
+
// Nodes that primarily use CSS3D (Text, Note, Code, Markdown) only need a lightweight placeholder
|
|
2486
|
+
// for the initial WebGL plane (used for Raycasting and far-distance LOD).
|
|
2487
|
+
// Actual content rendering happens via CSS3D or deferred TextureWorker.
|
|
2488
|
+
|
|
2489
|
+
let glObject;
|
|
2490
|
+
if (shouldUseDeferredShell) {
|
|
2491
|
+
timings.t3_afterTextures = performance.now();
|
|
2492
|
+
timings.t4_beforeWebGL = timings.t3_afterTextures;
|
|
2493
|
+
log(`[addNode] Step 5: Creating deferred shell for ${nodeModel.id}`);
|
|
2494
|
+
glObject = window.MindMapObjectManager?.createDeferredNodeShell?.(
|
|
2495
|
+
nodeModel,
|
|
2496
|
+
width,
|
|
2497
|
+
height,
|
|
2498
|
+
nodeZ,
|
|
2499
|
+
module.nodeZCounter++
|
|
2500
|
+
);
|
|
2501
|
+
} else {
|
|
2502
|
+
log(`[addNode] Step 3.1: Generating placeholder texture for ${nodeModel.id}...`);
|
|
2503
|
+
const result = await window.MindMapTextureFactory.generatePlaceholderTextures(module, nodeModel, width, height);
|
|
2504
|
+
if (_isBoardStale()) { log(`[addNode] Board switched after texture gen, aborting ${nodeModel.id}`); return; }
|
|
2505
|
+
textures = result.textures;
|
|
2506
|
+
// ▲▲▲ [Optimization] ▲▲▲
|
|
2507
|
+
// ▲▲▲ [Key Fix] ▲▲▲
|
|
2508
|
+
|
|
2509
|
+
timings.t3_afterTextures = performance.now();
|
|
2510
|
+
log(`[addNode] Step 4: Caching textures for ${nodeModel.id}`);
|
|
2511
|
+
const previous = textureCache.get(nodeModel.id);
|
|
2512
|
+
|
|
2513
|
+
// ★ 이전 텍스처 메모리 감산
|
|
2514
|
+
if (previous) {
|
|
2515
|
+
const prevMemory = calculateTextureMemory(previous, nodeModel.id);
|
|
2516
|
+
totalTextureMemoryBytes -= prevMemory;
|
|
2517
|
+
try { if (!previous.default.body?._isShared) previous.default.body?.dispose(); } catch { }
|
|
2518
|
+
try { if (!previous.selected.body?._isShared) previous.selected.body?.dispose(); } catch { }
|
|
2519
|
+
try { previous.glow?.texture?.dispose(); } catch { }
|
|
2520
|
+
}
|
|
2521
|
+
|
|
2522
|
+
// ★ 새 텍스처 메모리 가산 및 로그
|
|
2523
|
+
textureCache.set(nodeModel.id, textures);
|
|
2524
|
+
const newMemory = calculateTextureMemory(textures, nodeModel.id);
|
|
2525
|
+
logTextureMemory('ADD', nodeModel.id, newMemory);
|
|
2526
|
+
|
|
2527
|
+
timings.t4_beforeWebGL = performance.now();
|
|
2528
|
+
log(`[addNode] Step 5: Creating WebGL object for ${nodeModel.id}`);
|
|
2529
|
+
|
|
2530
|
+
// [Modified] CSS3D media/embed node implementation (image/video/embed)
|
|
2531
|
+
if (nodeModel.contentType === 'image' || nodeModel.contentType === 'video' || nodeModel.contentType === 'embed') {
|
|
2532
|
+
const node = nodeModel; // Alias for snippet compatibility
|
|
2533
|
+
const width = (node.width ?? node.Width) || (nodeModel.contentType === 'embed' ? 460 : (nodeModel.contentType === 'video' ? 470 : 300));
|
|
2534
|
+
const height = (node.height ?? node.Height) || (nodeModel.contentType === 'embed' ? 260 : (nodeModel.contentType === 'video' ? 264 : 300));
|
|
2535
|
+
|
|
2536
|
+
// 1. WebGL Mesh 생성 (LOD 원거리 표시 및 Raycaster 감지용)
|
|
2537
|
+
const hitGeometry = new THREE.PlaneGeometry(width, height);
|
|
2538
|
+
const initialBodyMap = nodeModel.contentType === 'image'
|
|
2539
|
+
? null
|
|
2540
|
+
: createImageStatusTexture(node, width, height);
|
|
2541
|
+
const hitMaterial = new THREE.MeshBasicMaterial({
|
|
2542
|
+
map: initialBodyMap,
|
|
2543
|
+
color: 0xffffff,
|
|
2544
|
+
transparent: false,
|
|
2545
|
+
depthWrite: true,
|
|
2546
|
+
depthTest: true,
|
|
2547
|
+
side: THREE.DoubleSide,
|
|
2548
|
+
polygonOffset: true,
|
|
2549
|
+
polygonOffsetFactor: -1,
|
|
2550
|
+
polygonOffsetUnits: -1
|
|
2551
|
+
});
|
|
2552
|
+
|
|
2553
|
+
const hitBody = new THREE.Mesh(hitGeometry, hitMaterial);
|
|
2554
|
+
hitBody.name = 'body'; // 중요: 시스템이 'body' 이름을 통해 노드를 식별함
|
|
2555
|
+
hitBody.castShadow = false;
|
|
2556
|
+
hitBody.receiveShadow = false;
|
|
2557
|
+
hitBody.userData = { nodeId: node.id }; // Ensure nodeId is present for Raycaster
|
|
2558
|
+
const shouldDeferImageBodyTexture =
|
|
2559
|
+
module?.isLoading === true ||
|
|
2560
|
+
module?._bulkAddingNodes === true;
|
|
2561
|
+
if (nodeModel.contentType === 'image' && !shouldDeferImageBodyTexture) {
|
|
2562
|
+
syncImageNodeBodyTexture(hitBody, node);
|
|
2563
|
+
} else if (nodeModel.contentType === 'image') {
|
|
2564
|
+
prewarmImageNodePreviewCache(node, module);
|
|
2565
|
+
}
|
|
2566
|
+
|
|
2567
|
+
// [FIX] Align HitBody Top-Left to Group Origin (Standard behavior)
|
|
2568
|
+
// PlaneGeometry is centered (0,0), so move Right(+w/2) and Down(-h/2)
|
|
2569
|
+
// ▼▼▼ [FIX] Move hitBody back slightly to avoid fighting with CSS3D at z=0 ▼▼▼
|
|
2570
|
+
// parallax 최소화를 위해 z=0으로 설정 (Grid z=0, CSS3D z=0)
|
|
2571
|
+
hitBody.position.set(width / 2, -height / 2, 0.0); // ★ z=0.0
|
|
2572
|
+
// ▲▲▲ [Changed] ▲▲▲
|
|
2573
|
+
|
|
2574
|
+
// Create a Group to hold both the hit mesh and the CSS object
|
|
2575
|
+
glObject = new THREE.Group();
|
|
2576
|
+
glObject.add(hitBody);
|
|
2577
|
+
|
|
2578
|
+
const isSelected = module.selectedNodeIdJs === nodeModel.id || module.multiSelectedNodeIds.has(nodeModel.id);
|
|
2579
|
+
const baseRenderOrder = module.nodeZCounter++;
|
|
2580
|
+
hitBody.renderOrder = baseRenderOrder;
|
|
2581
|
+
glObject.userData.baseRenderOrder = baseRenderOrder;
|
|
2582
|
+
|
|
2583
|
+
syncImageSelectionGlow(glObject, width, height, baseRenderOrder, false);
|
|
2584
|
+
syncImageMidLodDecorations(
|
|
2585
|
+
glObject,
|
|
2586
|
+
width,
|
|
2587
|
+
height,
|
|
2588
|
+
baseRenderOrder,
|
|
2589
|
+
isSelected,
|
|
2590
|
+
{
|
|
2591
|
+
edgeVisible: false,
|
|
2592
|
+
glowVisible: true
|
|
2593
|
+
}
|
|
2594
|
+
);
|
|
2595
|
+
|
|
2596
|
+
// Optional WebGL glow (disabled by default)
|
|
2597
|
+
if (ENABLE_WEBGL_GLOW && window.MindMapGlowShader) {
|
|
2598
|
+
const glowMesh = window.MindMapGlowShader.createSDFGlowMesh(width, height, isSelected, nodeModel.contentType === 'video' || nodeModel.contentType === 'embed' ? 'text' : 'image');
|
|
2599
|
+
glowMesh.position.set(width / 2, -height / 2, 0.0); // body(1.0)보다 뒤, 그리드(-10)보다 앞
|
|
2600
|
+
glowMesh.renderOrder = baseRenderOrder - 5;
|
|
2601
|
+
const cameraZ = module.camera?.position?.z ?? Infinity;
|
|
2602
|
+
const threshold = 4000;
|
|
2603
|
+
glowMesh.visible = cameraZ < threshold;
|
|
2604
|
+
glObject.add(glowMesh);
|
|
2605
|
+
}
|
|
2606
|
+
|
|
2607
|
+
// ▼▼▼ [FIX] Add corner resize handles to image nodes ▼▼▼
|
|
2608
|
+
// Image nodes use only corner hit-handles in WebGL; the visible affordance comes from the selection overlay.
|
|
2609
|
+
const HANDLE_SIZE = 16;
|
|
2610
|
+
|
|
2611
|
+
const handleConfigs = [
|
|
2612
|
+
{ name: 'resizeHandle_TL', corner: 'TL', x: HANDLE_SIZE / 2, y: -HANDLE_SIZE / 2, w: HANDLE_SIZE, h: HANDLE_SIZE },
|
|
2613
|
+
{ name: 'resizeHandle_TR', corner: 'TR', x: width - HANDLE_SIZE / 2, y: -HANDLE_SIZE / 2, w: HANDLE_SIZE, h: HANDLE_SIZE },
|
|
2614
|
+
{ name: 'resizeHandle_BL', corner: 'BL', x: HANDLE_SIZE / 2, y: -height + HANDLE_SIZE / 2, w: HANDLE_SIZE, h: HANDLE_SIZE },
|
|
2615
|
+
{ name: 'resizeHandle_BR', corner: 'BR', x: width - HANDLE_SIZE / 2, y: -height + HANDLE_SIZE / 2, w: HANDLE_SIZE, h: HANDLE_SIZE }
|
|
2616
|
+
];
|
|
2617
|
+
|
|
2618
|
+
handleConfigs.forEach(c => {
|
|
2619
|
+
const handleGeom = new THREE.PlaneGeometry(c.w, c.h);
|
|
2620
|
+
// 투명하지만 Raycast 감지 가능하도록 opacity 0.01 설정
|
|
2621
|
+
const handleMat = new THREE.MeshBasicMaterial({
|
|
2622
|
+
color: 0xff0000,
|
|
2623
|
+
transparent: true,
|
|
2624
|
+
opacity: 0.0,
|
|
2625
|
+
depthWrite: false,
|
|
2626
|
+
depthTest: false
|
|
2627
|
+
});
|
|
2628
|
+
handleMat.colorWrite = false;
|
|
2629
|
+
const handleMesh = new THREE.Mesh(handleGeom, handleMat);
|
|
2630
|
+
handleMesh.name = c.name;
|
|
2631
|
+
handleMesh.userData = { nodeId: nodeModel.id, isResizeHandle: true, corner: c.corner };
|
|
2632
|
+
handleMesh.renderOrder = baseRenderOrder + 10; // 높은 우선순위
|
|
2633
|
+
handleMesh.position.set(c.x, c.y, 0.0); // ★ Parallax 방지: 노드 평면(Z=0)에 완전 밀착 (renderOrder로 가시성 확보)
|
|
2634
|
+
glObject.add(handleMesh);
|
|
2635
|
+
});
|
|
2636
|
+
// ▲▲▲ [FIX] ▲▲▲
|
|
2637
|
+
|
|
2638
|
+
// 2. CSS3D creation delegated to MindMapCss3DManager (standardized)
|
|
2639
|
+
// Previously manual creation here is removed to avoid duplication.
|
|
2640
|
+
|
|
2641
|
+
// Set Group Position
|
|
2642
|
+
// [Fix] Force Z=0 to eliminate parallax - ignore nodeModel.positionZ completely
|
|
2643
|
+
glObject.position.set(nodeModel.positionX, nodeModel.positionY, 0);
|
|
2644
|
+
|
|
2645
|
+
// Essential Metadata
|
|
2646
|
+
glObject.userData.nodeId = nodeModel.id;
|
|
2647
|
+
glObject.userData.worldWidth = width;
|
|
2648
|
+
glObject.userData.worldHeight = height;
|
|
2649
|
+
|
|
2650
|
+
} else {
|
|
2651
|
+
// [Fix] Force zPos=0 to eliminate parallax
|
|
2652
|
+
glObject = await window.MindMapObjectManager.createOrUpdateCanvasTextureNode(module, nodeModel, null, 0, textures, width, height);
|
|
2653
|
+
if (_isBoardStale()) { log(`[addNode] Board switched after WebGL create, aborting ${nodeModel.id}`); return; }
|
|
2654
|
+
}
|
|
2655
|
+
}
|
|
2656
|
+
|
|
2657
|
+
if (!glObject) {
|
|
2658
|
+
console.error(`[MindMapNodes] Node ${nodeModel.id} failed to create WebGL object. Aborting addNode.`);
|
|
2659
|
+
if (!shouldUseDeferredShell) {
|
|
2660
|
+
textureCache.delete(nodeModel.id);
|
|
2661
|
+
}
|
|
2662
|
+
return;
|
|
2663
|
+
}
|
|
2664
|
+
glObject.userData.nodeId = nodeModel.id;
|
|
2665
|
+
|
|
2666
|
+
|
|
2667
|
+
|
|
2668
|
+
// ★ [LOD] 초기 LOD 레벨 저장 (첫 TextLOD 업데이트에서 품질 재결정)
|
|
2669
|
+
glObject.userData.lodLevel = 'NEEDS_UPDATE';
|
|
2670
|
+
|
|
2671
|
+
const nodeEntry = {
|
|
2672
|
+
glObject: glObject,
|
|
2673
|
+
// [Fix] For image nodes, we stored cssObject in userData. Assign it here for consistency.
|
|
2674
|
+
cssObject: glObject.userData?.css3dObject || null,
|
|
2675
|
+
currentType: 'GL', // Initially GL because (Image=Group, Others=Mesh)
|
|
2676
|
+
model: nodeModel,
|
|
2677
|
+
isDeferredShell: shouldUseDeferredShell,
|
|
2678
|
+
// ▼▼▼ [Optimization] Dirty flag for smart CSS3D sync (캐시 최적화) ▼▼▼
|
|
2679
|
+
isCssDirty: true, // 초기 생성 시 true (첫 표시 시 sync 필요)
|
|
2680
|
+
glowMesh: glObject?.getObjectByName?.('glow') || null,
|
|
2681
|
+
bodyMesh: glObject?.getObjectByName?.('body') || null,
|
|
2682
|
+
tailMesh: glObject?.getObjectByName?.('tail') || null
|
|
2683
|
+
// ▲▲▲ [Optimization] ▲▲▲
|
|
2684
|
+
};
|
|
2685
|
+
const isNearMode = module.lodRenderer ? !module.lodRenderer.isInLODMode : true;
|
|
2686
|
+
|
|
2687
|
+
// Rich cards use CSS3D in NEAR/NORMAL so wrapper glow/selection stays consistent.
|
|
2688
|
+
if (nodeModel.contentType === 'note' || nodeModel.contentType === 'memo' || nodeModel.contentType === 'text' || nodeModel.contentType === 'markdown' || nodeModel.contentType === 'code' || nodeModel.contentType === 'image' || nodeModel.contentType === 'video' || nodeModel.contentType === 'embed') {
|
|
2689
|
+
log(`[MindMapNodes] Creating cssObject for ${nodeModel.contentType} node ${nodeModel.id}`);
|
|
2690
|
+
const shouldDeferCssObject = module.isLoading && (
|
|
2691
|
+
nodeModel.contentType === 'text' ||
|
|
2692
|
+
nodeModel.contentType === 'note' ||
|
|
2693
|
+
nodeModel.contentType === 'memo' ||
|
|
2694
|
+
nodeModel.contentType === 'markdown' ||
|
|
2695
|
+
nodeModel.contentType === 'code' ||
|
|
2696
|
+
nodeModel.contentType === 'image' ||
|
|
2697
|
+
nodeModel.contentType === 'embed'
|
|
2698
|
+
);
|
|
2699
|
+
|
|
2700
|
+
if (!nodeEntry.cssObject && !shouldDeferCssObject) {
|
|
2701
|
+
nodeEntry.cssObject = window.MindMapCss3DManager.createCss3dObject(module, nodeModel);
|
|
2702
|
+
}
|
|
2703
|
+
if (nodeEntry.cssObject) {
|
|
2704
|
+
const glPos = glObject.position;
|
|
2705
|
+
// [FIX] Parallax issue fix: Set Z to match glObject (was + 0.2)
|
|
2706
|
+
nodeEntry.cssObject.position.set(glPos.x, glPos.y, glPos.z);
|
|
2707
|
+
const shouldAttachCss = isNearMode;
|
|
2708
|
+
if (shouldAttachCss) {
|
|
2709
|
+
(module.cssScene || module.scene).add(nodeEntry.cssObject);
|
|
2710
|
+
window.MindMapCss3DManager?.markCssParentRepairNeeded?.(module, nodeModel.id);
|
|
2711
|
+
} else if (nodeEntry.cssObject.parent) {
|
|
2712
|
+
nodeEntry.cssObject.parent.remove(nodeEntry.cssObject);
|
|
2713
|
+
}
|
|
2714
|
+
nodeEntry.cssObject.updateMatrixWorld(true);
|
|
2715
|
+
window.MindMapCss3DManager?.markNodeTransformDirty?.(module, nodeModel.id);
|
|
2716
|
+
|
|
2717
|
+
// ▼▼▼ [Fix] Apply thin scrollbar class to view mode element ▼▼▼
|
|
2718
|
+
const scrollableEl = nodeEntry.cssObject.element.querySelector('.node-response');
|
|
2719
|
+
if (scrollableEl) {
|
|
2720
|
+
scrollableEl.classList.add('thin-scrollbar');
|
|
2721
|
+
}
|
|
2722
|
+
// ▲▲▲ [Fix] ▲▲▲
|
|
2723
|
+
|
|
2724
|
+
// ▼▼▼ [Fix] Sync content immediately after CSS3D object creation ▼▼▼
|
|
2725
|
+
if (window.MindMapCss3DManager?.syncCss3dContent) {
|
|
2726
|
+
window.MindMapCss3DManager.syncCss3dContent(nodeModel, nodeEntry.cssObject);
|
|
2727
|
+
// ▼▼▼ [Optimization] 초기 sync 완료 후 clean 상태로 설정 ▼▼▼
|
|
2728
|
+
nodeEntry.isCssDirty = false;
|
|
2729
|
+
// ▲▲▲ [Optimization] ▲▲▲
|
|
2730
|
+
}
|
|
2731
|
+
// ▲▲▲ [Fix] ▲▲▲
|
|
2732
|
+
|
|
2733
|
+
// ▼▼▼ [Fix] NEAR 모드에서 새 노드 즉시 표시 ▼▼▼
|
|
2734
|
+
// CSS3D 객체는 기본적으로 visible=false로 생성됨
|
|
2735
|
+
// NEAR 모드(isInLODMode=false)일 때 즉시 표시해야 함
|
|
2736
|
+
if (shouldAttachCss) {
|
|
2737
|
+
nodeEntry.cssObject.visible = true;
|
|
2738
|
+
if (nodeEntry.cssObject.element) {
|
|
2739
|
+
nodeEntry.cssObject.element.style.removeProperty('display');
|
|
2740
|
+
nodeEntry.cssObject.element.style.removeProperty('visibility');
|
|
2741
|
+
nodeEntry.cssObject.element.style.removeProperty('opacity');
|
|
2742
|
+
nodeEntry.cssObject.element.style.setProperty('display', 'block', 'important');
|
|
2743
|
+
nodeEntry.cssObject.element.style.setProperty('visibility', 'visible', 'important');
|
|
2744
|
+
nodeEntry.cssObject.element.style.setProperty('opacity', '1', 'important');
|
|
2745
|
+
}
|
|
2746
|
+
nodeEntry.currentType = 'CSS';
|
|
2747
|
+
// WebGL body 숨기기 (이중 렌더링 방지)
|
|
2748
|
+
if (glObject) {
|
|
2749
|
+
ensureNodeMeshCache(nodeEntry);
|
|
2750
|
+
glObject.visible = true; // Group은 visible (glow 등 표시 위해)
|
|
2751
|
+
const body = nodeEntry.bodyMesh;
|
|
2752
|
+
const tail = nodeEntry.tailMesh;
|
|
2753
|
+
if (body) body.visible = false;
|
|
2754
|
+
if (tail) tail.visible = false;
|
|
2755
|
+
setNodeScrollbarVisibility(nodeEntry, false);
|
|
2756
|
+
}
|
|
2757
|
+
log(`[MindMapNodes] ✅ CSS3D visible=true for new node ${nodeModel.id} (NEAR mode)`);
|
|
2758
|
+
}
|
|
2759
|
+
// ▲▲▲ [Fix] ▲▲▲
|
|
2760
|
+
}
|
|
2761
|
+
}
|
|
2762
|
+
// ▲▲▲ [Changed] ▲▲▲
|
|
2763
|
+
|
|
2764
|
+
// ▼▼▼ [UX] Hide nodes while board is loading ▼▼▼
|
|
2765
|
+
if (module.isLoading) {
|
|
2766
|
+
if (glObject) glObject.visible = false;
|
|
2767
|
+
if (nodeEntry.cssObject) nodeEntry.cssObject.visible = false;
|
|
2768
|
+
}
|
|
2769
|
+
// ▲▲▲ [UX] ▲▲▲
|
|
2770
|
+
|
|
2771
|
+
// ▼▼▼ [Fix] Only enqueue when it was not rendered immediately ▼▼▼
|
|
2772
|
+
const alreadyRendered = (nodeModel.contentType === 'text' || nodeModel.contentType === 'note' || nodeModel.contentType === 'code') && nodeModel.response;
|
|
2773
|
+
if (!shouldUseDeferredShell && !alreadyRendered && !pendingNodeUpdates.has(nodeModel.id) && (nodeModel.contentType === 'text' || nodeModel.contentType === 'note' || nodeModel.contentType === 'code')) {
|
|
2774
|
+
pendingNodeUpdates.set(nodeModel.id, {
|
|
2775
|
+
content: nodeModel.response,
|
|
2776
|
+
width: width,
|
|
2777
|
+
height: height
|
|
2778
|
+
});
|
|
2779
|
+
setTimeout(() => window.MindMapPipeline.processRenderQueue(module, nodeModel.id), 0);
|
|
2780
|
+
}
|
|
2781
|
+
// ▲▲▲ [Fix] ▲▲▲
|
|
2782
|
+
|
|
2783
|
+
// ▼▼▼ [Fix] Trigger an immediate render update after image load completes ▼▼▼
|
|
2784
|
+
// Previously: cache only and wait for a C# call -> Now: enqueue rendering when caching succeeds
|
|
2785
|
+
// ▼▼▼ [Modified] Skip async texture load for CSS3D-based Image nodes ▼▼▼
|
|
2786
|
+
// Image nodes now use <img> tags via CSS3D, so we don't need to load the texture into WebGL.
|
|
2787
|
+
if (nodeModel.contentType === 'image' && nodeModel.response) {
|
|
2788
|
+
// WebGL texture is loaded directly in the image block above.
|
|
2789
|
+
}
|
|
2790
|
+
// ▲▲▲ [Modified] ▲▲▲
|
|
2791
|
+
|
|
2792
|
+
if (module.nodeObjectsById.has(nodeModel.id)) {
|
|
2793
|
+
console.warn(`[MindMapNodes] Race condition: Node was added by another call: ${nodeModel.id}`);
|
|
2794
|
+
if (glObject && glObject.geometry) try { glObject.geometry.dispose(); } catch { }
|
|
2795
|
+
return;
|
|
2796
|
+
}
|
|
2797
|
+
|
|
2798
|
+
module.nodeObjectsById.set(nodeModel.id, nodeEntry);
|
|
2799
|
+
const queuedUpdate = pendingNodeUpdates.get(nodeModel.id);
|
|
2800
|
+
if (queuedUpdate) {
|
|
2801
|
+
queuedUpdate.retryCount = 0;
|
|
2802
|
+
setTimeout(() => window.MindMapPipeline?.processRenderQueue?.(module, nodeModel.id), 0);
|
|
2803
|
+
}
|
|
2804
|
+
module.markAgentConsoleVisibilityTrackingDirty?.();
|
|
2805
|
+
scheduleMindCanvasAgentReportAutoFit(module, nodeModel.id);
|
|
2806
|
+
|
|
2807
|
+
if (module.GRID_CELL_SIZE && glObject && glObject.position) {
|
|
2808
|
+
// ▼▼▼ [Fix] Register the node in all occupied cells using the improved helper ▼▼▼
|
|
2809
|
+
updateNodeSpatialGrid(module, nodeModel.id);
|
|
2810
|
+
// ▲▲▲ [Fix] ▲▲▲
|
|
2811
|
+
}
|
|
2812
|
+
|
|
2813
|
+
if (!shouldUseDeferredShell) {
|
|
2814
|
+
module.scene.add(glObject);
|
|
2815
|
+
}
|
|
2816
|
+
|
|
2817
|
+
// Newly added nodes can already be selected programmatically (for example after paste).
|
|
2818
|
+
// Re-apply the current selection state now that the live entry exists so pointer events,
|
|
2819
|
+
// resize affordances, and click-cycle behavior start from a consistent base.
|
|
2820
|
+
const isSelectedOnAdd =
|
|
2821
|
+
module.selectedNodeIdJs === nodeModel.id ||
|
|
2822
|
+
module.multiSelectedNodeIds?.has?.(nodeModel.id) === true;
|
|
2823
|
+
updateNodeSelectionStyle(module, nodeModel.id, isSelectedOnAdd, false, true);
|
|
2824
|
+
|
|
2825
|
+
// Persist initial auto-position/size to C# state so board save captures them.
|
|
2826
|
+
if (module.dotNetHelper?.invokeMethodAsync) {
|
|
2827
|
+
if (autoPositioned) {
|
|
2828
|
+
try {
|
|
2829
|
+
await module.dotNetHelper.invokeMethodAsync(
|
|
2830
|
+
'UpdateNodePosition',
|
|
2831
|
+
nodeModel.id,
|
|
2832
|
+
Math.round(nodeModel.positionX || 0),
|
|
2833
|
+
Math.round(nodeModel.positionY || 0)
|
|
2834
|
+
);
|
|
2835
|
+
} catch (e) {
|
|
2836
|
+
console.warn('[MindMapNodes] Failed to persist auto-position:', e);
|
|
2837
|
+
}
|
|
2838
|
+
}
|
|
2839
|
+
|
|
2840
|
+
if (isNewNode) {
|
|
2841
|
+
try {
|
|
2842
|
+
await module.dotNetHelper.invokeMethodAsync(
|
|
2843
|
+
'UpdateNodeDimensions',
|
|
2844
|
+
nodeModel.id,
|
|
2845
|
+
Math.round(width || 0),
|
|
2846
|
+
Math.round(height || 0)
|
|
2847
|
+
);
|
|
2848
|
+
} catch (e) {
|
|
2849
|
+
console.warn('[MindMapNodes] Failed to persist initial dimensions:', e);
|
|
2850
|
+
}
|
|
2851
|
+
}
|
|
2852
|
+
}
|
|
2853
|
+
|
|
2854
|
+
if (autoPositioned) {
|
|
2855
|
+
moveCursorAfterNodePlacement(module, nodeModel, width, height, {
|
|
2856
|
+
padding: 30,
|
|
2857
|
+
moveCamera: true
|
|
2858
|
+
});
|
|
2859
|
+
}
|
|
2860
|
+
const nodeEndTime = performance.now();
|
|
2861
|
+
timings.t5_end = nodeEndTime;
|
|
2862
|
+
|
|
2863
|
+
// ▼▼▼ [Timing] 세부 시간 출력 ▼▼▼
|
|
2864
|
+
const t_init = timings.t1_init - nodeStartTime;
|
|
2865
|
+
const t_measure = (timings.t2_afterMeasure || timings.t1_init) - timings.t1_init;
|
|
2866
|
+
const t_textures = (timings.t3_afterTextures || timings.t2_afterMeasure || timings.t1_init) - (timings.t2_afterMeasure || timings.t1_init);
|
|
2867
|
+
const t_webgl = (timings.t5_end) - (timings.t4_beforeWebGL || timings.t3_afterTextures || timings.t1_init);
|
|
2868
|
+
const t_total = nodeEndTime - nodeStartTime;
|
|
2869
|
+
|
|
2870
|
+
if (!module.isLoading) {
|
|
2871
|
+
log(`[⏱️ Timing] addNode for ${nodeModel.id} (${nodeModel.contentType}):
|
|
2872
|
+
• Init: ${t_init.toFixed(1)}ms
|
|
2873
|
+
• MeasureHeight: ${t_measure.toFixed(1)}ms
|
|
2874
|
+
• Textures: ${t_textures.toFixed(1)}ms
|
|
2875
|
+
• WebGL+Finish: ${t_webgl.toFixed(1)}ms
|
|
2876
|
+
• TOTAL: ${t_total.toFixed(1)}ms`);
|
|
2877
|
+
}
|
|
2878
|
+
// ▲▲▲ [Timing] ▲▲▲
|
|
2879
|
+
|
|
2880
|
+
// ▼▼▼ [Fix] 노드 추가 후 LOD 렌더러 및 frustum culling 즉시 업데이트 ▼▼▼
|
|
2881
|
+
if (!module.isLoading) {
|
|
2882
|
+
if (module.lodRenderer?.requestResidentPatch) {
|
|
2883
|
+
module.lodRenderer.requestResidentPatch('add-node', nodeModel.id || nodeModel.Id);
|
|
2884
|
+
}
|
|
2885
|
+
// Frustum culling 강제 업데이트 (새 노드가 화면에 보이도록)
|
|
2886
|
+
if (module.updateVisibility && module.camera) {
|
|
2887
|
+
module.updateVisibility(module.camera);
|
|
2888
|
+
}
|
|
2889
|
+
}
|
|
2890
|
+
// ▲▲▲ [Fix] ▲▲▲
|
|
2891
|
+
|
|
2892
|
+
if (!module.isLoading) {
|
|
2893
|
+
log(`[MindMapNodes] Node added successfully: ${nodeModel.id} (${t_total.toFixed(1)}ms)`);
|
|
2894
|
+
}
|
|
2895
|
+
}
|
|
2896
|
+
|
|
2897
|
+
function removeNode(module, nodeId, options = null) {
|
|
2898
|
+
const id = String(nodeId || '').trim();
|
|
2899
|
+
if (!module || !id) return false;
|
|
2900
|
+
|
|
2901
|
+
const entry = module.nodeObjectsById.get(id);
|
|
2902
|
+
if (!entry) return false;
|
|
2903
|
+
|
|
2904
|
+
removeNodeFromSpatialGrid(module, id, entry);
|
|
2905
|
+
|
|
2906
|
+
if (entry.cssObject && entry.cssObject.element) {
|
|
2907
|
+
const domElement = entry.cssObject.element;
|
|
2908
|
+
if (domElement.parentElement) {
|
|
2909
|
+
domElement.parentElement.removeChild(domElement);
|
|
2910
|
+
}
|
|
2911
|
+
}
|
|
2912
|
+
|
|
2913
|
+
if (entry.glObject && module.scene) {
|
|
2914
|
+
|
|
2915
|
+
|
|
2916
|
+
try { module.scene.remove(entry.glObject); } catch { }
|
|
2917
|
+
if (entry.glObject.geometry) try { entry.glObject.geometry.dispose(); } catch { }
|
|
2918
|
+
try {
|
|
2919
|
+
entry.glObject.traverse(child => {
|
|
2920
|
+
if (child.geometry) try { child.geometry.dispose(); } catch { }
|
|
2921
|
+
if (child.material) {
|
|
2922
|
+
if (Array.isArray(child.material)) child.material.forEach(m => { try { m.dispose(); } catch { } });
|
|
2923
|
+
else try { child.material.dispose(); } catch { }
|
|
2924
|
+
}
|
|
2925
|
+
});
|
|
2926
|
+
} catch { }
|
|
2927
|
+
}
|
|
2928
|
+
|
|
2929
|
+
if (entry.cssObject) {
|
|
2930
|
+
try { (module.cssScene || module.scene).remove(entry.cssObject); } catch { }
|
|
2931
|
+
}
|
|
2932
|
+
|
|
2933
|
+
const cached = textureCache.get(id);
|
|
2934
|
+
if (cached) {
|
|
2935
|
+
// ★ 삭제되는 텍스처 메모리 감산 및 로그
|
|
2936
|
+
const freedMemory = calculateTextureMemory(cached);
|
|
2937
|
+
logTextureMemory('REMOVE', id, -freedMemory);
|
|
2938
|
+
|
|
2939
|
+
if (!cached.default.body?._isShared) try { cached.default.body?.dispose(); } catch { }
|
|
2940
|
+
if (!cached.selected.body?._isShared) try { cached.selected.body?.dispose(); } catch { }
|
|
2941
|
+
try { cached.glow?.texture?.dispose(); } catch { }
|
|
2942
|
+
textureCache.delete(id);
|
|
2943
|
+
}
|
|
2944
|
+
imageCache.delete(id);
|
|
2945
|
+
pendingNodeUpdates.delete(id);
|
|
2946
|
+
nodeRenderLocks.delete(id);
|
|
2947
|
+
|
|
2948
|
+
module.multiSelectedNodeIds?.delete?.(id);
|
|
2949
|
+
module.pendingSelectedActivationNodeIds?.delete?.(id);
|
|
2950
|
+
if (module.selectedNodeIdJs === id) module.selectedNodeIdJs = null;
|
|
2951
|
+
module.setAgentConsoleVisibilityState?.(id, false);
|
|
2952
|
+
module.logicWorkerBridge?.removeNode?.(id, module);
|
|
2953
|
+
module.nodeObjectsById.delete(id);
|
|
2954
|
+
|
|
2955
|
+
if (options?.deferRenderSync !== true) {
|
|
2956
|
+
finalizeRemovedNodes(module, [id]);
|
|
2957
|
+
}
|
|
2958
|
+
|
|
2959
|
+
return true;
|
|
2960
|
+
}
|
|
2961
|
+
|
|
2962
|
+
function removeNodes(module, nodeIds) {
|
|
2963
|
+
if (!module || !Array.isArray(nodeIds) || nodeIds.length === 0) return 0;
|
|
2964
|
+
|
|
2965
|
+
const uniqueIds = Array.from(new Set(
|
|
2966
|
+
nodeIds.map(id => String(id || '').trim()).filter(Boolean)
|
|
2967
|
+
));
|
|
2968
|
+
const removedIds = [];
|
|
2969
|
+
|
|
2970
|
+
uniqueIds.forEach(id => {
|
|
2971
|
+
if (removeNode(module, id, { deferRenderSync: true })) {
|
|
2972
|
+
removedIds.push(id);
|
|
2973
|
+
}
|
|
2974
|
+
});
|
|
2975
|
+
|
|
2976
|
+
finalizeRemovedNodes(module, removedIds);
|
|
2977
|
+
return removedIds.length;
|
|
2978
|
+
}
|
|
2979
|
+
|
|
2980
|
+
function finalizeRemovedNodes(module, nodeIds) {
|
|
2981
|
+
if (!module || !Array.isArray(nodeIds) || nodeIds.length === 0) return;
|
|
2982
|
+
|
|
2983
|
+
const lodRenderer = module.lodRenderer;
|
|
2984
|
+
if (lodRenderer) {
|
|
2985
|
+
if (typeof lodRenderer.removeNodes === 'function') {
|
|
2986
|
+
lodRenderer.removeNodes(nodeIds);
|
|
2987
|
+
} else if (typeof lodRenderer.removeNode === 'function') {
|
|
2988
|
+
nodeIds.forEach(id => lodRenderer.removeNode(id));
|
|
2989
|
+
} else if (typeof lodRenderer.requestResidentPatch === 'function') {
|
|
2990
|
+
lodRenderer.requestResidentPatch('remove-node');
|
|
2991
|
+
}
|
|
2992
|
+
}
|
|
2993
|
+
|
|
2994
|
+
window.MindMapTextOverlayV2?.invalidateView?.(module);
|
|
2995
|
+
|
|
2996
|
+
if (module.updateVisibility && module.camera) {
|
|
2997
|
+
module.updateVisibility(module.camera);
|
|
2998
|
+
}
|
|
2999
|
+
module._forceUpdateFrames = Math.max(module._forceUpdateFrames || 0, 4);
|
|
3000
|
+
}
|
|
3001
|
+
|
|
3002
|
+
function isMindCanvasAgentReportNode(model) {
|
|
3003
|
+
const metadata = model?.metadata || model?.Metadata || {};
|
|
3004
|
+
return String(metadata?.source || metadata?.Source || '').trim() === 'mind-canvas-agent-report';
|
|
3005
|
+
}
|
|
3006
|
+
|
|
3007
|
+
function measureMindCanvasAgentReportNaturalHeight(entry) {
|
|
3008
|
+
const sourceRoot = entry?.cssObject?.element;
|
|
3009
|
+
if (!sourceRoot) return null;
|
|
3010
|
+
|
|
3011
|
+
const probe = sourceRoot.cloneNode(true);
|
|
3012
|
+
probe.removeAttribute('id');
|
|
3013
|
+
probe.querySelectorAll?.('[id]')?.forEach(el => el.removeAttribute('id'));
|
|
3014
|
+
Object.assign(probe.style, {
|
|
3015
|
+
position: 'absolute',
|
|
3016
|
+
left: '-100000px',
|
|
3017
|
+
top: '-100000px',
|
|
3018
|
+
display: 'block',
|
|
3019
|
+
visibility: 'hidden',
|
|
3020
|
+
pointerEvents: 'none',
|
|
3021
|
+
overflow: 'visible',
|
|
3022
|
+
height: 'auto',
|
|
3023
|
+
zIndex: '-1000'
|
|
3024
|
+
});
|
|
3025
|
+
|
|
3026
|
+
const inner = probe.firstElementChild;
|
|
3027
|
+
if (!inner) return null;
|
|
3028
|
+
|
|
3029
|
+
inner.style.height = 'auto';
|
|
3030
|
+
inner.style.minHeight = '0';
|
|
3031
|
+
inner.style.overflow = 'visible';
|
|
3032
|
+
|
|
3033
|
+
const contentWrapper = inner.querySelector('.node-content-wrapper');
|
|
3034
|
+
if (contentWrapper) {
|
|
3035
|
+
contentWrapper.style.height = 'auto';
|
|
3036
|
+
contentWrapper.style.overflow = 'visible';
|
|
3037
|
+
}
|
|
3038
|
+
|
|
3039
|
+
const responseEl = inner.querySelector(`[id^="node-response-"]`) || inner.querySelector('.node-response');
|
|
3040
|
+
if (responseEl) {
|
|
3041
|
+
responseEl.style.height = 'auto';
|
|
3042
|
+
responseEl.style.flex = '0 0 auto';
|
|
3043
|
+
responseEl.style.overflowY = 'visible';
|
|
3044
|
+
responseEl.style.overflow = 'visible';
|
|
3045
|
+
}
|
|
3046
|
+
|
|
3047
|
+
document.body.appendChild(probe);
|
|
3048
|
+
try {
|
|
3049
|
+
const measured = Math.ceil(Math.max(
|
|
3050
|
+
inner.getBoundingClientRect?.().height || 0,
|
|
3051
|
+
inner.scrollHeight || 0,
|
|
3052
|
+
probe.getBoundingClientRect?.().height || 0
|
|
3053
|
+
));
|
|
3054
|
+
return Number.isFinite(measured) && measured > 0 ? measured : null;
|
|
3055
|
+
} finally {
|
|
3056
|
+
probe.remove();
|
|
3057
|
+
}
|
|
3058
|
+
}
|
|
3059
|
+
|
|
3060
|
+
function scheduleMindCanvasAgentReportAutoFit(module, nodeId) {
|
|
3061
|
+
const entry = module?.nodeObjectsById?.get?.(nodeId);
|
|
3062
|
+
if (!entry || !isMindCanvasAgentReportNode(entry.model) || entry.model?._agentReportAutoFitScheduled) return;
|
|
3063
|
+
|
|
3064
|
+
entry.model._agentReportAutoFitScheduled = true;
|
|
3065
|
+
setTimeout(() => {
|
|
3066
|
+
const latest = module?.nodeObjectsById?.get?.(nodeId);
|
|
3067
|
+
if (!latest || !isMindCanvasAgentReportNode(latest.model)) return;
|
|
3068
|
+
|
|
3069
|
+
const measured = measureMindCanvasAgentReportNaturalHeight(latest);
|
|
3070
|
+
if (!measured) return;
|
|
3071
|
+
|
|
3072
|
+
const currentHeight = Number(latest.model?.height || latest.model?.Height || latest.cssObject?.userData?.worldHeight || 0);
|
|
3073
|
+
const nextHeight = Math.max(100, Math.min(4000, measured + 2));
|
|
3074
|
+
if (Math.abs(nextHeight - currentHeight) <= 3) return;
|
|
3075
|
+
|
|
3076
|
+
const model = latest.model;
|
|
3077
|
+
const width = Number(model.width || model.Width || latest.cssObject?.userData?.worldWidth || 400);
|
|
3078
|
+
model.height = nextHeight;
|
|
3079
|
+
model.Height = nextHeight;
|
|
3080
|
+
|
|
3081
|
+
pendingNodeUpdates.set(nodeId, {
|
|
3082
|
+
content: model.response ?? model.Response ?? '',
|
|
3083
|
+
width,
|
|
3084
|
+
height: nextHeight,
|
|
3085
|
+
skipMeasure: true
|
|
3086
|
+
});
|
|
3087
|
+
|
|
3088
|
+
window.MindMapPipeline?.processRenderQueue?.(module, nodeId)
|
|
3089
|
+
?.catch?.(err => console.warn('[MindMapNodes] Failed to auto-fit agent report node:', err));
|
|
3090
|
+
}, 0);
|
|
3091
|
+
}
|
|
3092
|
+
|
|
3093
|
+
async function updateNodeContent(module, nodeId, content, w = null, h = null, isLoading = null) {
|
|
3094
|
+
if (!module.nodeObjectsById.has(nodeId)) {
|
|
3095
|
+
pendingNodeUpdates.set(nodeId, {
|
|
3096
|
+
content,
|
|
3097
|
+
width: w,
|
|
3098
|
+
height: h,
|
|
3099
|
+
isLoading,
|
|
3100
|
+
retryCount: 0,
|
|
3101
|
+
source: 'MindMapNodes.updateNodeContent-missing'
|
|
3102
|
+
});
|
|
3103
|
+
console.warn(`[MindMapNodes] updateNodeContent queued: Node ID not found yet: ${nodeId}`);
|
|
3104
|
+
return;
|
|
3105
|
+
}
|
|
3106
|
+
|
|
3107
|
+
const nodeEntry = module.nodeObjectsById.get(nodeId);
|
|
3108
|
+
const contentType = getNodeContentTypeLower(nodeEntry?.model) || 'unknown';
|
|
3109
|
+
const widthLabel = w ?? 'auto';
|
|
3110
|
+
const heightLabel = h ?? 'auto';
|
|
3111
|
+
const previousResponse = String(nodeEntry?.model?.response ?? nodeEntry?.model?.Response ?? '');
|
|
3112
|
+
const previousIsLoading = !!(nodeEntry?.model?.isLoading ?? nodeEntry?.model?.IsLoading);
|
|
3113
|
+
let nextContent = content;
|
|
3114
|
+
const hasExplicitContent = nextContent !== null && nextContent !== undefined;
|
|
3115
|
+
const model = nodeEntry?.model;
|
|
3116
|
+
if (model && hasExplicitContent) {
|
|
3117
|
+
nextContent = applyCanonicalNodeResponse(model, nextContent);
|
|
3118
|
+
}
|
|
3119
|
+
|
|
3120
|
+
const effectiveContent = hasExplicitContent ? nextContent : previousResponse;
|
|
3121
|
+
const contentLen = effectiveContent ? String(effectiveContent).length : 0;
|
|
3122
|
+
log(`[MindMapNodes] updateNodeContent for ${nodeId}: ContentType=${contentType}, New Size=${widthLabel}x${heightLabel}, ContentLen=${contentLen}, isLoading=${isLoading}`);
|
|
3123
|
+
|
|
3124
|
+
// ▼▼▼ [New] Update isLoading state in JS model ▼▼▼
|
|
3125
|
+
if (model && isLoading !== null) {
|
|
3126
|
+
model.isLoading = isLoading;
|
|
3127
|
+
model.IsLoading = isLoading;
|
|
3128
|
+
}
|
|
3129
|
+
// ▲▲▲ [New] ▲▲▲
|
|
3130
|
+
|
|
3131
|
+
let generatedImageLayoutUpdate = null;
|
|
3132
|
+
if (model) {
|
|
3133
|
+
generatedImageLayoutUpdate = prepareGeneratedImageLayoutUpdate(
|
|
3134
|
+
module,
|
|
3135
|
+
nodeEntry,
|
|
3136
|
+
w,
|
|
3137
|
+
h,
|
|
3138
|
+
previousResponse,
|
|
3139
|
+
previousIsLoading,
|
|
3140
|
+
isLoading
|
|
3141
|
+
);
|
|
3142
|
+
|
|
3143
|
+
if (generatedImageLayoutUpdate) {
|
|
3144
|
+
w = generatedImageLayoutUpdate.width;
|
|
3145
|
+
h = generatedImageLayoutUpdate.height;
|
|
3146
|
+
applyNodeWorldPosition(module, nodeEntry, generatedImageLayoutUpdate.x, generatedImageLayoutUpdate.y);
|
|
3147
|
+
log(
|
|
3148
|
+
`[MindMapNodes] Generated image frame aligned for ${nodeId}: ` +
|
|
3149
|
+
`${generatedImageLayoutUpdate.width}x${generatedImageLayoutUpdate.height} @ ` +
|
|
3150
|
+
`(${generatedImageLayoutUpdate.x}, ${generatedImageLayoutUpdate.y})`
|
|
3151
|
+
);
|
|
3152
|
+
}
|
|
3153
|
+
}
|
|
3154
|
+
|
|
3155
|
+
// ▼▼▼ [New] Auto-grow height during streaming for expanded nodes or full-response mode ▼▼▼
|
|
3156
|
+
const shouldAutoGrow = model && (contentType === 'text' || contentType === 'note') &&
|
|
3157
|
+
(model.isExpanded || !!module.showFullAiResponse);
|
|
3158
|
+
|
|
3159
|
+
// Check whether CSS3D DOM is available for direct measurement (most accurate)
|
|
3160
|
+
const isCss3dVisible = !!(nodeEntry?.cssObject?.visible && window.MindMapCss3DManager?.syncCss3dContent);
|
|
3161
|
+
|
|
3162
|
+
// [Fallback] When CSS3D is NOT visible, use measureHtmlHeightAsync (different CSS, 2x scale)
|
|
3163
|
+
if (shouldAutoGrow && !isCss3dVisible) {
|
|
3164
|
+
try {
|
|
3165
|
+
const zoomLevel = 2;
|
|
3166
|
+
const currentWidth = model.width || 400;
|
|
3167
|
+
const effectiveMaxHeight = 10000;
|
|
3168
|
+
const { height: measuredHeight } = await window.MindMapTextureFactory.measureHtmlHeightAsync(
|
|
3169
|
+
{ ...model, response: nextContent },
|
|
3170
|
+
zoomLevel,
|
|
3171
|
+
currentWidth * zoomLevel,
|
|
3172
|
+
null,
|
|
3173
|
+
effectiveMaxHeight
|
|
3174
|
+
);
|
|
3175
|
+
|
|
3176
|
+
const SAFETY_BUFFER = 4;
|
|
3177
|
+
const requiredHeight = measuredHeight + SAFETY_BUFFER;
|
|
3178
|
+
const MAX_HEIGHT = 4000;
|
|
3179
|
+
const newHeight = Math.min(requiredHeight, MAX_HEIGHT);
|
|
3180
|
+
const isFinalPass = isLoading === false;
|
|
3181
|
+
|
|
3182
|
+
if ((!isFinalPass && newHeight > model.height) ||
|
|
3183
|
+
(isFinalPass && Math.abs(newHeight - model.height) > 1)) {
|
|
3184
|
+
log(`[MindMapNodes] Auto-fit (fallback) node ${nodeId}: ${model.height} -> ${newHeight}px`);
|
|
3185
|
+
model.height = newHeight;
|
|
3186
|
+
h = newHeight;
|
|
3187
|
+
if (nodeEntry.cssObject?.userData) nodeEntry.cssObject.userData.worldHeight = newHeight;
|
|
3188
|
+
if (nodeEntry.glObject?.userData) nodeEntry.glObject.userData.worldHeight = newHeight;
|
|
3189
|
+
}
|
|
3190
|
+
} catch (err) {
|
|
3191
|
+
console.warn(`[MindMapNodes] Failed to auto-grow expanded node ${nodeId}:`, err);
|
|
3192
|
+
}
|
|
3193
|
+
}
|
|
3194
|
+
// ▲▲▲ [New] ▲▲▲
|
|
3195
|
+
|
|
3196
|
+
// ▼▼▼ [Optimization] 내용 변경 시 dirty 플래그 설정 ▼▼▼
|
|
3197
|
+
if (model && hasExplicitContent && previousResponse !== String(nextContent ?? '')) {
|
|
3198
|
+
nodeEntry.isCssDirty = true;
|
|
3199
|
+
}
|
|
3200
|
+
// ▲▲▲ [Optimization] ▲▲▲
|
|
3201
|
+
|
|
3202
|
+
// ▼▼▼ [Refactor] Update Model & Delegate CSS3D DOM updates to Manager ▼▼▼
|
|
3203
|
+
if (isCss3dVisible && nodeEntry.model) {
|
|
3204
|
+
// 1. Update content in DOM first
|
|
3205
|
+
if (hasExplicitContent) {
|
|
3206
|
+
setNodeResponseValue(nodeEntry.model, nextContent);
|
|
3207
|
+
}
|
|
3208
|
+
window.MindMapCss3DManager.syncCss3dContent(nodeEntry.model, nodeEntry.cssObject);
|
|
3209
|
+
|
|
3210
|
+
// 2. [Primary] Scrollbar-based precise height correction
|
|
3211
|
+
// After syncCss3dContent, response div has the content rendered.
|
|
3212
|
+
// If scrollHeight > clientHeight, the content overflows: grow by exactly that amount.
|
|
3213
|
+
// On final pass (isLoading=false), also allow shrink using auto-height measurement.
|
|
3214
|
+
if (shouldAutoGrow) {
|
|
3215
|
+
try {
|
|
3216
|
+
const cssRoot = nodeEntry.cssObject.element;
|
|
3217
|
+
const innerEl = cssRoot?.firstElementChild;
|
|
3218
|
+
const responseEl = innerEl?.querySelector(`[id^="node-response-"]`) || innerEl?.querySelector('.node-response');
|
|
3219
|
+
|
|
3220
|
+
if (responseEl) {
|
|
3221
|
+
const MAX_HEIGHT = 4000;
|
|
3222
|
+
const MIN_HEIGHT = module.aiNodeDefaultHeight || 60;
|
|
3223
|
+
const currentHeight = model.height || 0;
|
|
3224
|
+
const isFinalPass = isLoading === false;
|
|
3225
|
+
|
|
3226
|
+
const scrollH = responseEl.scrollHeight;
|
|
3227
|
+
const clientH = responseEl.clientHeight;
|
|
3228
|
+
const overflow = scrollH - clientH;
|
|
3229
|
+
|
|
3230
|
+
if (overflow > 1) {
|
|
3231
|
+
// Content overflows — grow by overflow + small buffer for sub-pixel rounding
|
|
3232
|
+
const SCROLL_BUFFER = 5;
|
|
3233
|
+
const GRID = module.GRID_SIZE || 10;
|
|
3234
|
+
const raw = currentHeight + Math.ceil(overflow) + SCROLL_BUFFER;
|
|
3235
|
+
const newHeight = Math.min(MAX_HEIGHT, Math.max(MIN_HEIGHT, Math.ceil(raw / GRID) * GRID));
|
|
3236
|
+
log(`[MindMapNodes] DOM-fit(scroll) node ${nodeId}: ${currentHeight} -> ${newHeight}px (overflow=${overflow})`);
|
|
3237
|
+
model.height = newHeight;
|
|
3238
|
+
h = newHeight;
|
|
3239
|
+
if (nodeEntry.cssObject?.userData) nodeEntry.cssObject.userData.worldHeight = newHeight;
|
|
3240
|
+
if (nodeEntry.glObject?.userData) nodeEntry.glObject.userData.worldHeight = newHeight;
|
|
3241
|
+
} else if (isFinalPass && overflow < -10) {
|
|
3242
|
+
// Final pass: node is too tall (excess whitespace).
|
|
3243
|
+
// Use auto-height trick to find exact needed height for shrinking.
|
|
3244
|
+
const wrapperEl = innerEl?.querySelector('.node-content-wrapper');
|
|
3245
|
+
if (wrapperEl) {
|
|
3246
|
+
const saved = {
|
|
3247
|
+
rootH: cssRoot.style.height, rootOv: cssRoot.style.overflow,
|
|
3248
|
+
innerH: innerEl.style.height, innerOv: innerEl.style.overflow,
|
|
3249
|
+
wrapperH: wrapperEl.style.height, wrapperOv: wrapperEl.style.overflow,
|
|
3250
|
+
responseH: responseEl.style.height, responseFlex: responseEl.style.flex,
|
|
3251
|
+
responseOv: responseEl.style.overflowY || responseEl.style.overflow,
|
|
3252
|
+
};
|
|
3253
|
+
cssRoot.style.height = 'auto'; cssRoot.style.overflow = 'visible';
|
|
3254
|
+
innerEl.style.height = 'auto'; innerEl.style.overflow = 'visible';
|
|
3255
|
+
wrapperEl.style.height = 'auto'; wrapperEl.style.overflow = 'visible';
|
|
3256
|
+
responseEl.style.height = 'auto'; responseEl.style.flex = '0 0 auto'; responseEl.style.overflowY = 'visible';
|
|
3257
|
+
|
|
3258
|
+
const naturalHeight = innerEl.scrollHeight;
|
|
3259
|
+
|
|
3260
|
+
cssRoot.style.height = saved.rootH; cssRoot.style.overflow = saved.rootOv;
|
|
3261
|
+
innerEl.style.height = saved.innerH; innerEl.style.overflow = saved.innerOv;
|
|
3262
|
+
wrapperEl.style.height = saved.wrapperH; wrapperEl.style.overflow = saved.wrapperOv;
|
|
3263
|
+
responseEl.style.height = saved.responseH; responseEl.style.flex = saved.responseFlex;
|
|
3264
|
+
responseEl.style.overflowY = saved.responseOv;
|
|
3265
|
+
|
|
3266
|
+
const shrunkHeight = Math.min(MAX_HEIGHT, Math.max(MIN_HEIGHT, Math.ceil(naturalHeight)));
|
|
3267
|
+
if (shrunkHeight < currentHeight - 1) {
|
|
3268
|
+
log(`[MindMapNodes] DOM-fit(shrink) node ${nodeId}: ${currentHeight} -> ${shrunkHeight}px`);
|
|
3269
|
+
model.height = shrunkHeight;
|
|
3270
|
+
h = shrunkHeight;
|
|
3271
|
+
if (nodeEntry.cssObject?.userData) nodeEntry.cssObject.userData.worldHeight = shrunkHeight;
|
|
3272
|
+
if (nodeEntry.glObject?.userData) nodeEntry.glObject.userData.worldHeight = shrunkHeight;
|
|
3273
|
+
}
|
|
3274
|
+
}
|
|
3275
|
+
}
|
|
3276
|
+
}
|
|
3277
|
+
} catch (err) {
|
|
3278
|
+
console.warn(`[MindMapNodes] DOM height fit failed for ${nodeId}:`, err);
|
|
3279
|
+
}
|
|
3280
|
+
}
|
|
3281
|
+
|
|
3282
|
+
nodeEntry.isCssDirty = false;
|
|
3283
|
+
}
|
|
3284
|
+
// ▲▲▲ [Refactor] ▲▲▲
|
|
3285
|
+
|
|
3286
|
+
if (window.MindMapTextOverlayV2?.invalidateNode) {
|
|
3287
|
+
const invalidateReason = hasExplicitContent ? 'content' : 'layout';
|
|
3288
|
+
window.MindMapTextOverlayV2.invalidateNode(module, nodeId, invalidateReason);
|
|
3289
|
+
}
|
|
3290
|
+
|
|
3291
|
+
pendingNodeUpdates.set(nodeId, { content: effectiveContent, width: w, height: h, isLoading });
|
|
3292
|
+
await window.MindMapPipeline.processRenderQueue(module, nodeId);
|
|
3293
|
+
|
|
3294
|
+
if (generatedImageLayoutUpdate?.shouldMoveCursor) {
|
|
3295
|
+
moveCursorAfterNodePlacement(module, model, model.width, model.height, {
|
|
3296
|
+
padding: 30,
|
|
3297
|
+
moveCamera: false
|
|
3298
|
+
});
|
|
3299
|
+
}
|
|
3300
|
+
|
|
3301
|
+
if ((generatedImageLayoutUpdate?.positionChanged || generatedImageLayoutUpdate?.sizeChanged) && module.dotNetHelper?.invokeMethodAsync) {
|
|
3302
|
+
try {
|
|
3303
|
+
if (generatedImageLayoutUpdate.sizeChanged) {
|
|
3304
|
+
await module.dotNetHelper.invokeMethodAsync(
|
|
3305
|
+
'UpdateNodeDimensions',
|
|
3306
|
+
nodeId,
|
|
3307
|
+
Math.round(Number(model?.width || generatedImageLayoutUpdate.width || 0)),
|
|
3308
|
+
Math.round(Number(model?.height || generatedImageLayoutUpdate.height || 0))
|
|
3309
|
+
);
|
|
3310
|
+
}
|
|
3311
|
+
|
|
3312
|
+
if (generatedImageLayoutUpdate.positionChanged) {
|
|
3313
|
+
await module.dotNetHelper.invokeMethodAsync(
|
|
3314
|
+
'UpdateNodePosition',
|
|
3315
|
+
nodeId,
|
|
3316
|
+
Math.round(Number(model?.positionX || generatedImageLayoutUpdate.x || 0)),
|
|
3317
|
+
Math.round(Number(model?.positionY || generatedImageLayoutUpdate.y || 0))
|
|
3318
|
+
);
|
|
3319
|
+
}
|
|
3320
|
+
} catch (error) {
|
|
3321
|
+
console.warn('[MindMapNodes] Failed to persist generated image layout update:', error);
|
|
3322
|
+
}
|
|
3323
|
+
}
|
|
3324
|
+
}
|
|
3325
|
+
|
|
3326
|
+
function markOverlayStackOrderChanged(module, forceFrames = 3) {
|
|
3327
|
+
if (!module) return;
|
|
3328
|
+
|
|
3329
|
+
module._deferPassiveOverlayRefresh = false;
|
|
3330
|
+
module._overlayDirty = true;
|
|
3331
|
+
module._forceUpdateFrames = Math.max(Number(module._forceUpdateFrames || 0), forceFrames);
|
|
3332
|
+
window.MindMapTextOverlayV2?.invalidateView?.(module);
|
|
3333
|
+
}
|
|
3334
|
+
|
|
3335
|
+
function bringNodeToFront(module, nodeId) {
|
|
3336
|
+
if (!module || !nodeId) return;
|
|
3337
|
+
|
|
3338
|
+
const entry = module.nodeObjectsById?.get(nodeId);
|
|
3339
|
+
if (!entry) return;
|
|
3340
|
+
const contentTypeLower = getNodeContentTypeLower(entry.model);
|
|
3341
|
+
const usesSafeMediaSelectionChrome = contentTypeLower === 'video' || contentTypeLower === 'embed';
|
|
3342
|
+
|
|
3343
|
+
// Keep relative renderOrder offsets (e.g., resize handles above body) and move whole node to top.
|
|
3344
|
+
if (entry.glObject) {
|
|
3345
|
+
let minOrder = Infinity;
|
|
3346
|
+
let maxOrder = -Infinity;
|
|
3347
|
+
|
|
3348
|
+
entry.glObject.traverse(child => {
|
|
3349
|
+
if (child.isMesh || child.isLine) {
|
|
3350
|
+
const order = child.renderOrder || 0;
|
|
3351
|
+
if (order < minOrder) minOrder = order;
|
|
3352
|
+
if (order > maxOrder) maxOrder = order;
|
|
3353
|
+
}
|
|
3354
|
+
});
|
|
3355
|
+
|
|
3356
|
+
if (!Number.isFinite(minOrder)) minOrder = 0;
|
|
3357
|
+
if (!Number.isFinite(maxOrder)) maxOrder = minOrder;
|
|
3358
|
+
|
|
3359
|
+
const targetBaseOrder = module.nodeZCounter++;
|
|
3360
|
+
const delta = targetBaseOrder - minOrder;
|
|
3361
|
+
|
|
3362
|
+
entry.glObject.traverse(child => {
|
|
3363
|
+
if (child.isMesh || child.isLine) {
|
|
3364
|
+
child.renderOrder = (child.renderOrder || 0) + delta;
|
|
3365
|
+
}
|
|
3366
|
+
});
|
|
3367
|
+
|
|
3368
|
+
const nextTop = maxOrder + delta + 1;
|
|
3369
|
+
if (nextTop > module.nodeZCounter) {
|
|
3370
|
+
module.nodeZCounter = nextTop;
|
|
3371
|
+
}
|
|
3372
|
+
}
|
|
3373
|
+
|
|
3374
|
+
// CSS3DObject re-parenting can trigger a one-frame detach flicker.
|
|
3375
|
+
// Keep DOM attached and raise stacking via z-index instead.
|
|
3376
|
+
if (entry.cssObject) {
|
|
3377
|
+
syncCss3dMediaFrameForEntry(module, nodeId, entry);
|
|
3378
|
+
const cssElement = entry.cssObject.element;
|
|
3379
|
+
if (cssElement) {
|
|
3380
|
+
const z = Math.max(1, module.nodeZCounter - 1);
|
|
3381
|
+
cssElement.style.zIndex = String(z);
|
|
3382
|
+
const hasLiveEmbedIframe =
|
|
3383
|
+
contentTypeLower === 'embed' &&
|
|
3384
|
+
window.MindMapCss3DManager?.hasMountedEmbedIframe?.(entry.cssObject) === true;
|
|
3385
|
+
if (usesSafeMediaSelectionChrome && !hasLiveEmbedIframe && cssElement.parentElement) {
|
|
3386
|
+
cssElement.parentElement.appendChild(cssElement);
|
|
3387
|
+
}
|
|
3388
|
+
}
|
|
3389
|
+
}
|
|
3390
|
+
|
|
3391
|
+
markOverlayStackOrderChanged(module);
|
|
3392
|
+
}
|
|
3393
|
+
|
|
3394
|
+
async function updateNodeSelectionStyle(module, nodeId, isSelected, bringToFront = false, asyncSchedule = false) {
|
|
3395
|
+
if (asyncSchedule) {
|
|
3396
|
+
setTimeout(() => updateNodeSelectionStyle(module, nodeId, isSelected, bringToFront, false), 0);
|
|
3397
|
+
return;
|
|
3398
|
+
}
|
|
3399
|
+
|
|
3400
|
+
if (!isSelected) {
|
|
3401
|
+
window.MindMapCss3DManager?.hideBusinessAutomationFloatingTooltipForNode?.(nodeId);
|
|
3402
|
+
}
|
|
3403
|
+
|
|
3404
|
+
const entry = module.nodeObjectsById.get(nodeId);
|
|
3405
|
+
if (!entry || !entry.glObject) return;
|
|
3406
|
+
|
|
3407
|
+
if (isMediaNodeModel(entry.model)) {
|
|
3408
|
+
syncCss3dMediaFrameForEntry(module, nodeId, entry);
|
|
3409
|
+
}
|
|
3410
|
+
|
|
3411
|
+
// ▼▼▼ [Fix] Keep image nodes visible on selection (avoid LOD/CSS conflicts) ▼▼▼
|
|
3412
|
+
if (entry.model?.contentType === 'image') {
|
|
3413
|
+
ensureNodeMeshCache(entry);
|
|
3414
|
+
entry.glObject.visible = true;
|
|
3415
|
+
const body = entry.bodyMesh;
|
|
3416
|
+
const tail = entry.tailMesh;
|
|
3417
|
+
const usesCssView =
|
|
3418
|
+
entry.currentType === 'CSS' &&
|
|
3419
|
+
!!entry.cssObject?.visible &&
|
|
3420
|
+
isCss3dRendererEnabledForModule(module);
|
|
3421
|
+
const baseRenderOrder = entry.glObject.userData?.baseRenderOrder ?? body?.renderOrder ?? 1;
|
|
3422
|
+
const nodeWidth = entry.model.width || body?.geometry?.parameters?.width || 300;
|
|
3423
|
+
const nodeHeight = entry.model.height || body?.geometry?.parameters?.height || 300;
|
|
3424
|
+
const showImageLodGlow = !usesCssView;
|
|
3425
|
+
syncImageSelectionGlow(entry.glObject, nodeWidth, nodeHeight, baseRenderOrder, false);
|
|
3426
|
+
syncImageMidLodDecorations(entry.glObject, nodeWidth, nodeHeight, baseRenderOrder, !!isSelected, {
|
|
3427
|
+
edgeVisible: false,
|
|
3428
|
+
glowVisible: showImageLodGlow
|
|
3429
|
+
});
|
|
3430
|
+
const outline = entry.glObject.getObjectByName('outline');
|
|
3431
|
+
if (body) body.visible = !usesCssView || shouldShowImageBodyInCssMode(entry.model);
|
|
3432
|
+
if (tail) tail.visible = false;
|
|
3433
|
+
if (outline) {
|
|
3434
|
+
outline.visible = false;
|
|
3435
|
+
if (outline.material) {
|
|
3436
|
+
outline.material.color.setHex(IMAGE_SELECTION_GLOW_COLOR);
|
|
3437
|
+
outline.material.opacity = 0;
|
|
3438
|
+
outline.material.needsUpdate = true;
|
|
3439
|
+
}
|
|
3440
|
+
}
|
|
3441
|
+
if (entry.glObject?.children) {
|
|
3442
|
+
entry.glObject.children.forEach(child => {
|
|
3443
|
+
if (child?.userData?.isResizeHandle) {
|
|
3444
|
+
child.visible = !usesCssView;
|
|
3445
|
+
if (usesCssView) {
|
|
3446
|
+
child.scale?.setScalar?.(1.0);
|
|
3447
|
+
}
|
|
3448
|
+
}
|
|
3449
|
+
});
|
|
3450
|
+
}
|
|
3451
|
+
|
|
3452
|
+
// Keep CSS-based glow in sync for image nodes as well.
|
|
3453
|
+
const imageCssElement = entry.cssObject?.element;
|
|
3454
|
+
const imageInner =
|
|
3455
|
+
imageCssElement?.querySelector('.map-node-image-container') ||
|
|
3456
|
+
imageCssElement?.querySelector('.map-node') ||
|
|
3457
|
+
null;
|
|
3458
|
+
if (imageCssElement?.classList) {
|
|
3459
|
+
imageCssElement.classList.remove('image-selected');
|
|
3460
|
+
imageCssElement.classList.toggle('selected', !!isSelected);
|
|
3461
|
+
}
|
|
3462
|
+
if (imageInner?.classList) {
|
|
3463
|
+
imageInner.classList.toggle('selected', !!isSelected);
|
|
3464
|
+
}
|
|
3465
|
+
|
|
3466
|
+
// ▼▼▼ [Fix] Glow update for image nodes ▼▼▼
|
|
3467
|
+
const glowMesh = entry.glowMesh;
|
|
3468
|
+
if (ENABLE_WEBGL_GLOW && glowMesh && window.MindMapGlowShader) {
|
|
3469
|
+
window.MindMapGlowShader.updateGlowSelection(glowMesh, isSelected);
|
|
3470
|
+
}
|
|
3471
|
+
// ▲▲▲ [Fix] ▲▲▲
|
|
3472
|
+
|
|
3473
|
+
if (bringToFront) {
|
|
3474
|
+
bringNodeToFront(module, nodeId);
|
|
3475
|
+
}
|
|
3476
|
+
|
|
3477
|
+
// Image nodes do not use textureCache/canvas generation logic below.
|
|
3478
|
+
// They rely on the static TextureLoader map assigned at creation.
|
|
3479
|
+
// So we return early to avoid overwriting the texture with a blank canvas.
|
|
3480
|
+
return;
|
|
3481
|
+
}
|
|
3482
|
+
// ▲▲▲ [Fix] ▲▲▲
|
|
3483
|
+
|
|
3484
|
+
// ▼▼▼ [Fix] Sync CSS3D selection glow/border with the node's active theme ▼▼▼
|
|
3485
|
+
const cssElement = entry.cssObject?.element;
|
|
3486
|
+
if (cssElement) {
|
|
3487
|
+
const isEditing = !!(module.isEditingNote && module.selectedNodeIdJs === nodeId);
|
|
3488
|
+
const shouldHighlight = !!(isSelected || isEditing);
|
|
3489
|
+
const bubbleElement =
|
|
3490
|
+
cssElement.querySelector('.map-node-note') ||
|
|
3491
|
+
cssElement.querySelector('.map-node-memo') ||
|
|
3492
|
+
cssElement.querySelector('.map-node-code') ||
|
|
3493
|
+
cssElement.querySelector('.map-node-agent') ||
|
|
3494
|
+
cssElement.querySelector('.map-node-bubble') ||
|
|
3495
|
+
cssElement.querySelector('.map-node') ||
|
|
3496
|
+
cssElement;
|
|
3497
|
+
|
|
3498
|
+
// Keep class-based styling in sync for CSS rules
|
|
3499
|
+
if (bubbleElement?.classList) {
|
|
3500
|
+
bubbleElement.classList.toggle('selected', !!shouldHighlight);
|
|
3501
|
+
}
|
|
3502
|
+
|
|
3503
|
+
// Keep wrapper glow/pointer state in sync for all CSS3D nodes.
|
|
3504
|
+
const contentType = String(entry.model?.contentType ?? entry.model?.ContentType ?? '').toLowerCase();
|
|
3505
|
+
const isMemoNode = contentType === 'memo';
|
|
3506
|
+
const isAgentLikeNode = isAgentNodeModel(entry.model);
|
|
3507
|
+
const usesSafeMediaSelectionChrome = contentType === 'video' || contentType === 'embed';
|
|
3508
|
+
|
|
3509
|
+
if (cssElement.classList) {
|
|
3510
|
+
cssElement.classList.toggle('selected', !usesSafeMediaSelectionChrome && !!shouldHighlight);
|
|
3511
|
+
cssElement.classList.toggle('selected-media-safe', usesSafeMediaSelectionChrome && !!shouldHighlight);
|
|
3512
|
+
}
|
|
3513
|
+
|
|
3514
|
+
if ((isMemoNode || isAgentStyledMemoNode(entry.model) || isAgentLikeNode) && window.MindMapCss3DManager?.applyMemoWrapperTheme) {
|
|
3515
|
+
const themedRoot =
|
|
3516
|
+
cssElement.querySelector('.map-node-memo') ||
|
|
3517
|
+
cssElement.querySelector('.map-node-agent') ||
|
|
3518
|
+
cssElement;
|
|
3519
|
+
window.MindMapCss3DManager.applyMemoWrapperTheme(
|
|
3520
|
+
themedRoot,
|
|
3521
|
+
String(entry.model?.metadata?.memoColor || entry.model?.Metadata?.memoColor || 'coral').toLowerCase()
|
|
3522
|
+
);
|
|
3523
|
+
}
|
|
3524
|
+
|
|
3525
|
+
// ▼▼▼ [NEW] 텍스트 기반 노드 선택 시 pointer-events 및 user-select 활성화 ▼▼▼
|
|
3526
|
+
// - note/memo/code/agent 노드: 편집 모드(isEditing)에서만 텍스트 선택 가능
|
|
3527
|
+
// - 일반 text/markdown 노드: 선택 시 텍스트 복사 가능
|
|
3528
|
+
const isNoteNode = contentType === 'note';
|
|
3529
|
+
const isEmbedNode = contentType === 'embed';
|
|
3530
|
+
const isInlineEditableNode = isInlineEditableNodeModel(entry.model);
|
|
3531
|
+
const isReadOnlyTextNode = isReadOnlyTextSelectionNodeModel(entry.model);
|
|
3532
|
+
const isTextBased = isInlineEditableNode || isReadOnlyTextNode;
|
|
3533
|
+
|
|
3534
|
+
if (isEmbedNode && window.MindMapCss3DManager?.syncCss3dEmbedInteractivity) {
|
|
3535
|
+
window.MindMapCss3DManager.syncCss3dEmbedInteractivity(entry.cssObject, {
|
|
3536
|
+
isInteractive: !!shouldHighlight
|
|
3537
|
+
});
|
|
3538
|
+
}
|
|
3539
|
+
|
|
3540
|
+
if (isTextBased) {
|
|
3541
|
+
// 선택 시 래퍼는 클릭 가능해야 하고, 실제 입력 컨트롤은 편집 중에만 연다.
|
|
3542
|
+
const shouldBeInteractive = isSelected || isEditing;
|
|
3543
|
+
|
|
3544
|
+
if (shouldBeInteractive) {
|
|
3545
|
+
cssElement.style.pointerEvents = 'auto';
|
|
3546
|
+
|
|
3547
|
+
// 내부 콘텐츠 영역 pointer-events 활성화
|
|
3548
|
+
const contentEls = cssElement.querySelectorAll(NODE_INTERACTIVE_CONTENT_SELECTORS);
|
|
3549
|
+
contentEls.forEach(el => {
|
|
3550
|
+
const isMemoControl = isMemoNode && el.matches(MEMO_EDIT_ONLY_SELECTORS);
|
|
3551
|
+
if (isMemoControl && !isEditing) {
|
|
3552
|
+
el.style.pointerEvents = 'none';
|
|
3553
|
+
el.style.userSelect = 'none';
|
|
3554
|
+
el.style.webkitUserSelect = 'none';
|
|
3555
|
+
el.style.cursor = 'grab';
|
|
3556
|
+
return;
|
|
3557
|
+
}
|
|
3558
|
+
|
|
3559
|
+
el.style.pointerEvents = 'auto';
|
|
3560
|
+
|
|
3561
|
+
if (el.matches('.map-node-memo__icon-button, .map-node-memo__icon-option')) {
|
|
3562
|
+
el.style.userSelect = 'none';
|
|
3563
|
+
el.style.webkitUserSelect = 'none';
|
|
3564
|
+
el.style.cursor = 'pointer';
|
|
3565
|
+
return;
|
|
3566
|
+
}
|
|
3567
|
+
|
|
3568
|
+
// ▼▼▼ [Fix] 노드 타입에 따라 텍스트 선택 허용 조건 분리 ▼▼▼
|
|
3569
|
+
// note/memo/code/agent 노드: 편집 모드에서만 텍스트 선택 허용
|
|
3570
|
+
// 일반 text/markdown 노드: 선택되면 텍스트 선택 허용 (복사를 위해)
|
|
3571
|
+
const allowTextSelection = isInlineEditableNode ? isEditing : isSelected;
|
|
3572
|
+
|
|
3573
|
+
if (allowTextSelection) {
|
|
3574
|
+
el.style.userSelect = 'text';
|
|
3575
|
+
el.style.webkitUserSelect = 'text';
|
|
3576
|
+
el.style.cursor = 'text';
|
|
3577
|
+
} else {
|
|
3578
|
+
el.style.userSelect = 'none';
|
|
3579
|
+
el.style.webkitUserSelect = 'none';
|
|
3580
|
+
el.style.cursor = 'grab';
|
|
3581
|
+
}
|
|
3582
|
+
// ▲▲▲ [Fix] ▲▲▲
|
|
3583
|
+
});
|
|
3584
|
+
} else {
|
|
3585
|
+
// 선택 해제 시: interaction 차단 + 텍스트 선택 비활성화
|
|
3586
|
+
cssElement.style.pointerEvents = 'none';
|
|
3587
|
+
const contentEls = cssElement.querySelectorAll(NODE_INTERACTIVE_CONTENT_SELECTORS);
|
|
3588
|
+
contentEls.forEach(el => {
|
|
3589
|
+
el.style.pointerEvents = 'none';
|
|
3590
|
+
el.style.userSelect = 'none';
|
|
3591
|
+
el.style.webkitUserSelect = 'none';
|
|
3592
|
+
});
|
|
3593
|
+
if (isMemoNode) {
|
|
3594
|
+
cssElement.querySelectorAll('.map-node-memo__icon-popover.is-open').forEach(popover => {
|
|
3595
|
+
popover.classList.remove('is-open');
|
|
3596
|
+
});
|
|
3597
|
+
}
|
|
3598
|
+
}
|
|
3599
|
+
}
|
|
3600
|
+
// ▲▲▲ [NEW] ▲▲▲
|
|
3601
|
+
}
|
|
3602
|
+
// ▲▲▲ [Fix] ▲▲▲
|
|
3603
|
+
|
|
3604
|
+
if (entry.isDeferredShell === true) {
|
|
3605
|
+
if (bringToFront && entry.cssObject) {
|
|
3606
|
+
bringNodeToFront(module, nodeId);
|
|
3607
|
+
}
|
|
3608
|
+
|
|
3609
|
+
if (module.lodRenderer && typeof module.lodRenderer.updateInstanceSelection === 'function') {
|
|
3610
|
+
module.lodRenderer.updateInstanceSelection(nodeId, isSelected);
|
|
3611
|
+
}
|
|
3612
|
+
|
|
3613
|
+
return;
|
|
3614
|
+
}
|
|
3615
|
+
|
|
3616
|
+
// ▼▼▼ [Fix] CSS3D 모드(HIGH)일 때 WebGL 겹침 방지 및 텍스처 생성 차단 ▼▼▼
|
|
3617
|
+
if (entry.currentType === 'CSS') {
|
|
3618
|
+
ensureNodeMeshCache(entry);
|
|
3619
|
+
const isCssRendererActive = isCss3dRendererEnabledForModule(module);
|
|
3620
|
+
// Image nodes should keep their WebGL body visible to prevent disappearance
|
|
3621
|
+
if (entry.model?.contentType !== 'image') {
|
|
3622
|
+
const bodyMesh = entry.bodyMesh;
|
|
3623
|
+
const tailMesh = entry.tailMesh;
|
|
3624
|
+
if (bodyMesh) {
|
|
3625
|
+
bodyMesh.visible = !isCssRendererActive && isMediaNodeModel(entry.model);
|
|
3626
|
+
if (!isCssRendererActive && isMediaNodeModel(entry.model) && bodyMesh.material) {
|
|
3627
|
+
if (entry.model?.contentType !== 'image' && bodyMesh.material.map) {
|
|
3628
|
+
bodyMesh.material.map = null;
|
|
3629
|
+
}
|
|
3630
|
+
const fallbackBodyHex = entry.model?.contentType === 'video'
|
|
3631
|
+
? 0x0f172a
|
|
3632
|
+
: 0xf8fafc;
|
|
3633
|
+
bodyMesh.material.color?.setHex?.(fallbackBodyHex);
|
|
3634
|
+
bodyMesh.material.needsUpdate = true;
|
|
3635
|
+
}
|
|
3636
|
+
}
|
|
3637
|
+
if (tailMesh) tailMesh.visible = false;
|
|
3638
|
+
}
|
|
3639
|
+
|
|
3640
|
+
// Glow는 업데이트 (선택 표시용) - WebGL Glow는 CSS3D 뒤에 깔려서 보임
|
|
3641
|
+
const glowMesh = entry.glowMesh;
|
|
3642
|
+
if (ENABLE_WEBGL_GLOW && glowMesh && window.MindMapGlowShader) {
|
|
3643
|
+
window.MindMapGlowShader.updateGlowSelection(glowMesh, isSelected);
|
|
3644
|
+
}
|
|
3645
|
+
|
|
3646
|
+
// Note 내용 동기화 (CSS3D가 활성이므로 여기서 처리)
|
|
3647
|
+
if (!isSelected && entry.model.contentType === 'note' && entry.cssObject) {
|
|
3648
|
+
const textarea = entry.cssObject.element?.querySelector('textarea');
|
|
3649
|
+
if (textarea && textarea.value !== entry.model.response) {
|
|
3650
|
+
entry.model.response = textarea.value;
|
|
3651
|
+
window.MindMapTextLOD?.invalidateNodeCache?.(nodeId);
|
|
3652
|
+
}
|
|
3653
|
+
}
|
|
3654
|
+
|
|
3655
|
+
// ▼▼▼ [Optimization] CSS3D 모드에서 dirty면 sync ▼▼▼
|
|
3656
|
+
if (entry.isCssDirty && window.MindMapCss3DManager?.syncCss3dContent) {
|
|
3657
|
+
window.MindMapCss3DManager.syncCss3dContent(entry.model, entry.cssObject);
|
|
3658
|
+
entry.isCssDirty = false;
|
|
3659
|
+
if (isMediaNodeModel(entry.model)) {
|
|
3660
|
+
syncCss3dMediaFrameForEntry(module, nodeId, entry);
|
|
3661
|
+
}
|
|
3662
|
+
}
|
|
3663
|
+
// ▲▲▲ [Optimization] ▲▲▲
|
|
3664
|
+
|
|
3665
|
+
// Z-Order 업데이트
|
|
3666
|
+
if (bringToFront) {
|
|
3667
|
+
bringNodeToFront(module, nodeId);
|
|
3668
|
+
}
|
|
3669
|
+
|
|
3670
|
+
if (module.lodRenderer && typeof module.lodRenderer.updateInstanceSelection === 'function') {
|
|
3671
|
+
module.lodRenderer.updateInstanceSelection(nodeId, isSelected);
|
|
3672
|
+
}
|
|
3673
|
+
|
|
3674
|
+
return; // WebGL 텍스처 업데이트 중단
|
|
3675
|
+
}
|
|
3676
|
+
// ▲▲▲ [Fix] ▲▲▲
|
|
3677
|
+
|
|
3678
|
+
// ▼▼▼ [Fix] Sync CSS3D textarea content to model before deselection ▼▼▼
|
|
3679
|
+
// When deselecting a note node, the textarea may have newer content than model.response.
|
|
3680
|
+
// This can happen if switchToWebGL set currentType='GL' but hasn't completed updateNodeContent yet.
|
|
3681
|
+
// Sync from textarea regardless of currentType as long as cssObject exists and has a textarea.
|
|
3682
|
+
if (!isSelected && entry.model.contentType === 'note' && entry.cssObject) {
|
|
3683
|
+
const textarea = entry.cssObject.element?.querySelector('textarea');
|
|
3684
|
+
if (textarea && textarea.value !== entry.model.response) {
|
|
3685
|
+
log(`[updateNodeSelectionStyle] Syncing CSS3D textarea content for ${nodeId} (${textarea.value.length} chars)`);
|
|
3686
|
+
entry.model.response = textarea.value;
|
|
3687
|
+
// ★ Invalidate LOD cache so zoom-in will regenerate with new content
|
|
3688
|
+
window.MindMapTextLOD?.invalidateNodeCache?.(nodeId);
|
|
3689
|
+
}
|
|
3690
|
+
// Ensure focus-within is cleared so selected styling doesn't stick
|
|
3691
|
+
try { textarea?.blur(); } catch { }
|
|
3692
|
+
}
|
|
3693
|
+
// ▲▲▲ [Fix] ▲▲▲
|
|
3694
|
+
|
|
3695
|
+
if (!textureCache.has(nodeId)) {
|
|
3696
|
+
// [Refactor] Troika support removed. Using standard textures.
|
|
3697
|
+
textureCache.set(nodeId, textures);
|
|
3698
|
+
}
|
|
3699
|
+
|
|
3700
|
+
let textures = textureCache.get(nodeId);
|
|
3701
|
+
|
|
3702
|
+
// ▼▼▼ [Optimization B] On-demand selected texture creation ▼▼▼
|
|
3703
|
+
if (isSelected && (!textures.selected.body || !textures.selected.tail)) {
|
|
3704
|
+
log(`[updateNodeSelectionStyle] Creating selected textures on-demand for ${nodeId}`);
|
|
3705
|
+
const t0 = performance.now();
|
|
3706
|
+
|
|
3707
|
+
|
|
3708
|
+
|
|
3709
|
+
// Canvas 모드: 기존 방식
|
|
3710
|
+
const { textures: newTextures } = await window.MindMapTextureFactory.generateAndCacheTextures(
|
|
3711
|
+
module,
|
|
3712
|
+
entry.model,
|
|
3713
|
+
imageCache,
|
|
3714
|
+
{ skipMeasure: true, skipSelected: false }
|
|
3715
|
+
);
|
|
3716
|
+
textures.selected = newTextures.selected;
|
|
3717
|
+
textureCache.set(nodeId, textures);
|
|
3718
|
+
|
|
3719
|
+
const t1 = performance.now();
|
|
3720
|
+
log(`[⏱️ Timing] On-demand selected texture for ${nodeId}: ${(t1 - t0).toFixed(1)}ms`);
|
|
3721
|
+
}
|
|
3722
|
+
// ▲▲▲ [Optimization B] ▲▲▲
|
|
3723
|
+
|
|
3724
|
+
// ▼▼▼ [Fix] Regenerate default texture only when content changed ▼▼▼
|
|
3725
|
+
if (!isSelected && textures.default?.body) {
|
|
3726
|
+
const cachedContent = textures.default._cachedContent ?? '';
|
|
3727
|
+
const currentContent = entry.model.response ?? '';
|
|
3728
|
+
|
|
3729
|
+
const needsContentUpdate = cachedContent !== currentContent;
|
|
3730
|
+
|
|
3731
|
+
if (needsContentUpdate) {
|
|
3732
|
+
log(`[updateNodeSelectionStyle] Regenerating default textures for ${nodeId} (content changed)`);
|
|
3733
|
+
const t0 = performance.now();
|
|
3734
|
+
|
|
3735
|
+
const { textures: newTextures } = await window.MindMapTextureFactory.generateAndCacheTextures(
|
|
3736
|
+
module,
|
|
3737
|
+
entry.model,
|
|
3738
|
+
imageCache,
|
|
3739
|
+
{ skipMeasure: true, skipSelected: true }
|
|
3740
|
+
);
|
|
3741
|
+
// Dispose old default textures
|
|
3742
|
+
try { if (!textures.default.body?._isShared) textures.default.body?.dispose(); } catch { }
|
|
3743
|
+
try { if (!textures.default.tail?._isShared) textures.default.tail?.dispose(); } catch { }
|
|
3744
|
+
textures.default = newTextures.default;
|
|
3745
|
+
textures.default._cachedContent = currentContent;
|
|
3746
|
+
textureCache.set(nodeId, textures);
|
|
3747
|
+
|
|
3748
|
+
const t1 = performance.now();
|
|
3749
|
+
log(`[⏱️ Timing] On-demand default texture regeneration for ${nodeId}: ${(t1 - t0).toFixed(1)}ms`);
|
|
3750
|
+
}
|
|
3751
|
+
}
|
|
3752
|
+
// ▲▲▲ [Fix] ▲▲▲
|
|
3753
|
+
|
|
3754
|
+
const active = isSelected ? textures.selected : textures.default;
|
|
3755
|
+
|
|
3756
|
+
ensureNodeMeshCache(entry);
|
|
3757
|
+
const bodyMesh = entry.bodyMesh;
|
|
3758
|
+
const tailMesh = entry.tailMesh;
|
|
3759
|
+
const outline = entry.glObject.getObjectByName('outline');
|
|
3760
|
+
|
|
3761
|
+
if (outline) {
|
|
3762
|
+
outline.visible = false; // Disabled - using SDF glow instead
|
|
3763
|
+
}
|
|
3764
|
+
if (bodyMesh && bodyMesh.material) {
|
|
3765
|
+
bodyMesh.material.map = active.body;
|
|
3766
|
+
bodyMesh.material.needsUpdate = true;
|
|
3767
|
+
}
|
|
3768
|
+
if (tailMesh && tailMesh.material) {
|
|
3769
|
+
tailMesh.material.map = active.tail;
|
|
3770
|
+
tailMesh.material.needsUpdate = true;
|
|
3771
|
+
}
|
|
3772
|
+
|
|
3773
|
+
const glowMesh = entry.glowMesh;
|
|
3774
|
+
if (ENABLE_WEBGL_GLOW && glowMesh && window.MindMapGlowShader) {
|
|
3775
|
+
window.MindMapGlowShader.updateGlowSelection(glowMesh, isSelected);
|
|
3776
|
+
}
|
|
3777
|
+
|
|
3778
|
+
// ▼▼▼ [Fix] Update SDF body borderColor for Troika code nodes ▼▼▼
|
|
3779
|
+
if (bodyMesh && bodyMesh.material && bodyMesh.material.uniforms && bodyMesh.material.uniforms.borderColor) {
|
|
3780
|
+
bodyMesh.material.uniforms.borderColor.value.setHex(getSelectionBorderColorHex(entry.model, isSelected));
|
|
3781
|
+
}
|
|
3782
|
+
// ▲▲▲ [Fix] ▲▲▲
|
|
3783
|
+
|
|
3784
|
+
// ▼▼▼ [FIX] Update LOD InstancedMesh selection state immediately ▼▼▼
|
|
3785
|
+
if (module.lodRenderer && typeof module.lodRenderer.updateInstanceSelection === 'function') {
|
|
3786
|
+
module.lodRenderer.updateInstanceSelection(nodeId, isSelected);
|
|
3787
|
+
}
|
|
3788
|
+
// ▲▲▲ [FIX] ▲▲▲
|
|
3789
|
+
|
|
3790
|
+
if (bringToFront) {
|
|
3791
|
+
bringNodeToFront(module, nodeId);
|
|
3792
|
+
// ▼▼▼ [Fix] Keep Z position stable to align with grid (avoid perspective lift) ▼▼▼
|
|
3793
|
+
// renderOrder is sufficient for draw order without shifting world Z.
|
|
3794
|
+
// ▲▲▲ [Fix] ▲▲▲
|
|
3795
|
+
}
|
|
3796
|
+
}
|
|
3797
|
+
|
|
3798
|
+
function refreshLodSelectionState(module, changedIds = null) {
|
|
3799
|
+
const lodRenderer = module?.lodRenderer;
|
|
3800
|
+
if (!lodRenderer || lodRenderer.isInLODMode !== true) {
|
|
3801
|
+
if (module) {
|
|
3802
|
+
const currentIds = new Set();
|
|
3803
|
+
if (module.selectedNodeIdJs) {
|
|
3804
|
+
currentIds.add(module.selectedNodeIdJs);
|
|
3805
|
+
}
|
|
3806
|
+
module.multiSelectedNodeIds?.forEach?.((id) => {
|
|
3807
|
+
if (id) {
|
|
3808
|
+
currentIds.add(id);
|
|
3809
|
+
}
|
|
3810
|
+
});
|
|
3811
|
+
module._lastLodSelectionIds = currentIds;
|
|
3812
|
+
}
|
|
3813
|
+
return;
|
|
3814
|
+
}
|
|
3815
|
+
|
|
3816
|
+
const currentIds = new Set();
|
|
3817
|
+
if (module.selectedNodeIdJs) {
|
|
3818
|
+
currentIds.add(module.selectedNodeIdJs);
|
|
3819
|
+
}
|
|
3820
|
+
module.multiSelectedNodeIds?.forEach?.((id) => {
|
|
3821
|
+
if (id) {
|
|
3822
|
+
currentIds.add(id);
|
|
3823
|
+
}
|
|
3824
|
+
});
|
|
3825
|
+
|
|
3826
|
+
let idsToRefresh = [];
|
|
3827
|
+
if (Array.isArray(changedIds) && changedIds.length > 0) {
|
|
3828
|
+
idsToRefresh = changedIds
|
|
3829
|
+
.map(id => String(id || '').trim())
|
|
3830
|
+
.filter(Boolean);
|
|
3831
|
+
} else {
|
|
3832
|
+
const previousIds = module._lastLodSelectionIds instanceof Set
|
|
3833
|
+
? module._lastLodSelectionIds
|
|
3834
|
+
: new Set();
|
|
3835
|
+
idsToRefresh = Array.from(new Set([...previousIds, ...currentIds]));
|
|
3836
|
+
}
|
|
3837
|
+
|
|
3838
|
+
for (let i = 0; i < idsToRefresh.length; i++) {
|
|
3839
|
+
const id = idsToRefresh[i];
|
|
3840
|
+
lodRenderer.updateInstanceSelection?.(id, currentIds.has(id));
|
|
3841
|
+
}
|
|
3842
|
+
|
|
3843
|
+
module._lastLodSelectionIds = currentIds;
|
|
3844
|
+
const primarySelectionId = module.selectedNodeIdJs || '';
|
|
3845
|
+
const multiSelectedIds = module.multiSelectedNodeIds;
|
|
3846
|
+
if (!multiSelectedIds || multiSelectedIds.size === 0) {
|
|
3847
|
+
lodRenderer._lastAppliedLodSelectionKey = primarySelectionId;
|
|
3848
|
+
} else if (multiSelectedIds.size === 1) {
|
|
3849
|
+
const onlyId = Array.from(multiSelectedIds)[0] || '';
|
|
3850
|
+
lodRenderer._lastAppliedLodSelectionKey = `${primarySelectionId}|${onlyId}`;
|
|
3851
|
+
} else {
|
|
3852
|
+
lodRenderer._lastAppliedLodSelectionKey = `${primarySelectionId}|${Array.from(multiSelectedIds).sort().join(',')}`;
|
|
3853
|
+
}
|
|
3854
|
+
module._forceUpdateFrames = Math.max(module._forceUpdateFrames || 0, 1);
|
|
3855
|
+
}
|
|
3856
|
+
|
|
3857
|
+
function refreshSelectionOverlayState(module, forceFrames = 2) {
|
|
3858
|
+
if (!module) return;
|
|
3859
|
+
|
|
3860
|
+
module._deferPassiveOverlayRefresh = false;
|
|
3861
|
+
module._overlayDirty = true;
|
|
3862
|
+
module._forceUpdateFrames = Math.max(Number(module._forceUpdateFrames || 0), forceFrames);
|
|
3863
|
+
|
|
3864
|
+
const isMotionInteraction =
|
|
3865
|
+
module.isZooming === true ||
|
|
3866
|
+
module.isPanning === true ||
|
|
3867
|
+
module.isResizing === true ||
|
|
3868
|
+
module.isDraggingNode === true ||
|
|
3869
|
+
module.isDraggingMultipleNodes === true ||
|
|
3870
|
+
module.isWindowResizing === true;
|
|
3871
|
+
if (!isMotionInteraction) {
|
|
3872
|
+
module._cameraNavigationOverlayInputTime = 0;
|
|
3873
|
+
module._passiveOverlayResumeAt = 0;
|
|
3874
|
+
window.MindMapTextOverlayV2?.setPassiveOverlaySuppressed?.(module, false);
|
|
3875
|
+
}
|
|
3876
|
+
|
|
3877
|
+
window.MindMapTextOverlayV2?.invalidateView?.(module);
|
|
3878
|
+
}
|
|
3879
|
+
|
|
3880
|
+
// ▼▼▼ [New] Single selection unified path ▼▼▼
|
|
3881
|
+
function applySingleSelection(module, newNodeId, options = {}) {
|
|
3882
|
+
if (!module) return;
|
|
3883
|
+
|
|
3884
|
+
const { notifyBlazor = true, bringToFront = true } = options;
|
|
3885
|
+
const oldNodeId = module.selectedNodeIdJs || null;
|
|
3886
|
+
|
|
3887
|
+
if (oldNodeId && oldNodeId !== newNodeId) {
|
|
3888
|
+
updateNodeSelectionStyle(module, oldNodeId, false, false, true);
|
|
3889
|
+
}
|
|
3890
|
+
|
|
3891
|
+
// ▼▼▼ [Fix] 다중 선택된 노드들의 스타일도 함께 초기화 ▼▼▼
|
|
3892
|
+
if (typeof clearMultiSelection === 'function') {
|
|
3893
|
+
clearMultiSelection(module, newNodeId); // Clears others and updates styles
|
|
3894
|
+
} else {
|
|
3895
|
+
module.multiSelectedNodeIds?.clear?.();
|
|
3896
|
+
}
|
|
3897
|
+
// ▲▲▲ [Fix] ▲▲▲
|
|
3898
|
+
if (newNodeId) {
|
|
3899
|
+
module.multiSelectedNodeIds?.add?.(newNodeId);
|
|
3900
|
+
module.selectedNodeIdJs = newNodeId;
|
|
3901
|
+
if (bringToFront) {
|
|
3902
|
+
// Selection styling is scheduled, but stack order must update before
|
|
3903
|
+
// overlay/CSS3D overlap filtering runs on the next frame.
|
|
3904
|
+
bringNodeToFront(module, newNodeId);
|
|
3905
|
+
}
|
|
3906
|
+
updateNodeSelectionStyle(module, newNodeId, true, false, true);
|
|
3907
|
+
} else {
|
|
3908
|
+
module.selectedNodeIdJs = null;
|
|
3909
|
+
}
|
|
3910
|
+
|
|
3911
|
+
if (notifyBlazor && module.dotNetHelper) {
|
|
3912
|
+
try { module.dotNetHelper.invokeMethodAsync('SelectNodeInBlazor', newNodeId); } catch { }
|
|
3913
|
+
}
|
|
3914
|
+
|
|
3915
|
+
const changedIds = [];
|
|
3916
|
+
if (oldNodeId && oldNodeId !== newNodeId) {
|
|
3917
|
+
changedIds.push(oldNodeId);
|
|
3918
|
+
}
|
|
3919
|
+
if (newNodeId) {
|
|
3920
|
+
changedIds.push(newNodeId);
|
|
3921
|
+
}
|
|
3922
|
+
refreshLodSelectionState(module, changedIds);
|
|
3923
|
+
refreshSelectionOverlayState(module);
|
|
3924
|
+
}
|
|
3925
|
+
// ▲▲▲ [New] ▲▲▲
|
|
3926
|
+
|
|
3927
|
+
function clearMultiSelection(module, keepId = null) {
|
|
3928
|
+
const toDeselect = Array.from(module.multiSelectedNodeIds).filter(id => id !== keepId);
|
|
3929
|
+
toDeselect.forEach(id => updateNodeSelectionStyle(module, id, false, false, true));
|
|
3930
|
+
if (keepId) {
|
|
3931
|
+
module.multiSelectedNodeIds = new Set([keepId]);
|
|
3932
|
+
module.selectedNodeIdJs = keepId;
|
|
3933
|
+
} else {
|
|
3934
|
+
module.multiSelectedNodeIds.clear();
|
|
3935
|
+
module.selectedNodeIdJs = null;
|
|
3936
|
+
}
|
|
3937
|
+
|
|
3938
|
+
// ▼▼▼ [Fix] Force LOD renderer to rebuild instances so selection borders update in MID/FAR ▼▼▼
|
|
3939
|
+
refreshLodSelectionState(module);
|
|
3940
|
+
// ▲▲▲ [Fix] ▲▲▲
|
|
3941
|
+
refreshSelectionOverlayState(module);
|
|
3942
|
+
}
|
|
3943
|
+
|
|
3944
|
+
function resetState(module) {
|
|
3945
|
+
try {
|
|
3946
|
+
textureCache.forEach(t => {
|
|
3947
|
+
try { if (!t.default.body?._isShared) t.default.body?.dispose(); } catch { }
|
|
3948
|
+
try { if (!t.selected.body?._isShared) t.selected.body?.dispose(); } catch { }
|
|
3949
|
+
try { t.glow?.texture?.dispose(); } catch { }
|
|
3950
|
+
});
|
|
3951
|
+
} catch { }
|
|
3952
|
+
textureCache.clear();
|
|
3953
|
+
imageCache.clear();
|
|
3954
|
+
failedImageUrlCache.clear();
|
|
3955
|
+
window.MindMapTextureFactory.clearCache();
|
|
3956
|
+
nodeRenderLocks.clear();
|
|
3957
|
+
pendingNodeUpdates.clear();
|
|
3958
|
+
totalTextureMemoryBytes = 0;
|
|
3959
|
+
|
|
3960
|
+
if (module) {
|
|
3961
|
+
try { module.nodeObjectsById.clear(); } catch { }
|
|
3962
|
+
try { module.spatialGrid.clear(); } catch { }
|
|
3963
|
+
try { module.multiSelectedNodeIds.clear(); } catch { }
|
|
3964
|
+
try { module.pendingSelectedActivationNodeIds?.clear?.(); } catch { }
|
|
3965
|
+
module.selectedNodeIdJs = null;
|
|
3966
|
+
module.lodUpdateQueue = [];
|
|
3967
|
+
}
|
|
3968
|
+
}
|
|
3969
|
+
|
|
3970
|
+
async function switchToCss3D(module, nodeId) {
|
|
3971
|
+
const nodeEntry = module.nodeObjectsById.get(nodeId);
|
|
3972
|
+
// ▼▼▼ [Log] Reason for skipping switch (SwitchToCss3D) ▼▼▼
|
|
3973
|
+
const supportedTypes = ['note', 'memo', 'code', 'text', 'markdown', 'image', 'video', 'embed'];
|
|
3974
|
+
if (!nodeEntry || !supportedTypes.includes(nodeEntry.model.contentType) || !nodeEntry.cssObject) {
|
|
3975
|
+
log(`[MindMapNodes] SwitchToCss3D(${nodeId}) SKIPPED (Not a valid, initialized text-based node).`);
|
|
3976
|
+
return;
|
|
3977
|
+
}
|
|
3978
|
+
// ▼▼▼ [New] Loading 상태에서는 CSS3D로 전환하지 않음 (WebGL 모드 유지) ▼▼▼
|
|
3979
|
+
const modelIsLoading = nodeEntry.model.isLoading ?? nodeEntry.model.IsLoading ?? false;
|
|
3980
|
+
const isMediaNode = nodeEntry.model.contentType === 'image' || nodeEntry.model.contentType === 'video' || nodeEntry.model.contentType === 'embed';
|
|
3981
|
+
if (modelIsLoading && !isMediaNode) {
|
|
3982
|
+
log(`[MindMapNodes] SwitchToCss3D(${nodeId}) SKIPPED (Node is still loading).`);
|
|
3983
|
+
return;
|
|
3984
|
+
}
|
|
3985
|
+
// ▲▲▲ [New] ▲▲▲
|
|
3986
|
+
if (nodeEntry.currentType === 'CSS') {
|
|
3987
|
+
log(`[MindMapNodes] SwitchToCss3D(${nodeId}) SKIPPED (Already in CSS mode).`);
|
|
3988
|
+
return;
|
|
3989
|
+
}
|
|
3990
|
+
log(`[MindMapNodes] Switching node ${nodeId} to CSS3D mode`);
|
|
3991
|
+
// ▲▲▲ [Log] ▲▲▲
|
|
3992
|
+
|
|
3993
|
+
const glPos = nodeEntry.glObject.position;
|
|
3994
|
+
nodeEntry.cssObject.position.copy(glPos);
|
|
3995
|
+
|
|
3996
|
+
// ▼▼▼ [Fix] Sync CSS3D element size with current model dimensions ▼▼▼
|
|
3997
|
+
const model = nodeEntry.model;
|
|
3998
|
+
const wrapper = nodeEntry.cssObject.element;
|
|
3999
|
+
const resolutionScale = nodeEntry.cssObject.userData?.resolutionScale || 1;
|
|
4000
|
+
const modelWidth = Number(model.width ?? model.Width ?? 400);
|
|
4001
|
+
const modelHeight = Number(model.height ?? model.Height ?? 200);
|
|
4002
|
+
if (isMediaNode) {
|
|
4003
|
+
syncCss3dMediaFrameForEntry(module, nodeId, nodeEntry);
|
|
4004
|
+
} else if (wrapper) {
|
|
4005
|
+
wrapper.style.width = `${modelWidth * resolutionScale}px`;
|
|
4006
|
+
wrapper.style.height = `${modelHeight * resolutionScale}px`;
|
|
4007
|
+
|
|
4008
|
+
// Also update inner content element if exists
|
|
4009
|
+
const innerContent = wrapper.firstElementChild;
|
|
4010
|
+
if (innerContent) {
|
|
4011
|
+
innerContent.style.width = `${modelWidth}px`;
|
|
4012
|
+
innerContent.style.height = `${modelHeight}px`;
|
|
4013
|
+
innerContent.style.transform = `scale(${resolutionScale})`;
|
|
4014
|
+
innerContent.style.transformOrigin = '0 0';
|
|
4015
|
+
}
|
|
4016
|
+
|
|
4017
|
+
// Update textarea size as well
|
|
4018
|
+
const textarea = wrapper.querySelector('textarea');
|
|
4019
|
+
if (textarea) {
|
|
4020
|
+
textarea.style.width = '100%';
|
|
4021
|
+
textarea.style.height = '100%';
|
|
4022
|
+
}
|
|
4023
|
+
|
|
4024
|
+
log(`[MindMapNodes] CSS3D size synced: ${modelWidth}x${modelHeight}px (scale=${resolutionScale})`);
|
|
4025
|
+
}
|
|
4026
|
+
// ▲▲▲ [Fix] ▲▲▲
|
|
4027
|
+
|
|
4028
|
+
if (nodeEntry.glObject) {
|
|
4029
|
+
ensureNodeMeshCache(nodeEntry);
|
|
4030
|
+
nodeEntry.glObject.visible = true;
|
|
4031
|
+
const body = nodeEntry.bodyMesh;
|
|
4032
|
+
const tail = nodeEntry.tailMesh;
|
|
4033
|
+
if (body) body.visible = shouldShowImageBodyInCssMode(nodeEntry.model);
|
|
4034
|
+
if (tail) tail.visible = false;
|
|
4035
|
+
nodeEntry.glObject.children?.forEach?.(child => {
|
|
4036
|
+
if (child?.userData?.isResizeHandle) {
|
|
4037
|
+
child.visible = false;
|
|
4038
|
+
child.scale?.setScalar?.(1.0);
|
|
4039
|
+
}
|
|
4040
|
+
});
|
|
4041
|
+
if (isMediaNode) {
|
|
4042
|
+
['outline', 'imageMidGlow', 'imageMidEdge', 'glow'].forEach(name => {
|
|
4043
|
+
const decoration = nodeEntry.glObject.getObjectByName?.(name);
|
|
4044
|
+
if (!decoration) return;
|
|
4045
|
+
decoration.visible = false;
|
|
4046
|
+
if (decoration.material) {
|
|
4047
|
+
decoration.material.opacity = 0;
|
|
4048
|
+
decoration.material.needsUpdate = true;
|
|
4049
|
+
}
|
|
4050
|
+
});
|
|
4051
|
+
}
|
|
4052
|
+
}
|
|
4053
|
+
|
|
4054
|
+
nodeEntry.cssObject.visible = true;
|
|
4055
|
+
|
|
4056
|
+
// ▼▼▼ [Optimization] dirty 체크 후 필요시에만 sync ▼▼▼
|
|
4057
|
+
if (nodeEntry.isCssDirty && window.MindMapCss3DManager?.syncCss3dContent) {
|
|
4058
|
+
window.MindMapCss3DManager.syncCss3dContent(nodeEntry.model, nodeEntry.cssObject);
|
|
4059
|
+
nodeEntry.isCssDirty = false;
|
|
4060
|
+
if (isMediaNode) {
|
|
4061
|
+
syncCss3dMediaFrameForEntry(module, nodeId, nodeEntry);
|
|
4062
|
+
}
|
|
4063
|
+
}
|
|
4064
|
+
// ▲▲▲ [Optimization] ▲▲▲
|
|
4065
|
+
|
|
4066
|
+
// ▼▼▼ [Fix] 다른 노드들의 텍스트 선택을 비활성화하여 선택 번짐 방지 ▼▼▼
|
|
4067
|
+
module.nodeObjectsById.forEach((otherEntry, otherId) => {
|
|
4068
|
+
if (otherId === nodeId) return; // 현재 노드는 건너뛰기
|
|
4069
|
+
if (!otherEntry.cssObject?.element) return;
|
|
4070
|
+
|
|
4071
|
+
const otherWrapper = otherEntry.cssObject.element;
|
|
4072
|
+
// 다른 노드들의 텍스트 선택 비활성화
|
|
4073
|
+
const contentEls = otherWrapper.querySelectorAll(NODE_TEXT_SELECTION_SELECTORS);
|
|
4074
|
+
contentEls.forEach(el => {
|
|
4075
|
+
el.style.userSelect = 'none';
|
|
4076
|
+
el.style.webkitUserSelect = 'none';
|
|
4077
|
+
});
|
|
4078
|
+
});
|
|
4079
|
+
// ▲▲▲ [Fix] ▲▲▲
|
|
4080
|
+
|
|
4081
|
+
// ▼▼▼ [Fix] 현재 노드의 텍스트 선택 활성화 ▼▼▼
|
|
4082
|
+
const currentWrapper = nodeEntry.cssObject.element;
|
|
4083
|
+
if (currentWrapper) {
|
|
4084
|
+
const contentEls = currentWrapper.querySelectorAll(NODE_TEXT_SELECTION_SELECTORS);
|
|
4085
|
+
contentEls.forEach(el => {
|
|
4086
|
+
el.style.userSelect = 'text';
|
|
4087
|
+
el.style.webkitUserSelect = 'text';
|
|
4088
|
+
});
|
|
4089
|
+
}
|
|
4090
|
+
// ▲▲▲ [Fix] ▲▲▲
|
|
4091
|
+
|
|
4092
|
+
// Sync CSS3D scroll position with WebGL/model scrollOffset (ratio-based).
|
|
4093
|
+
const textareaEl = nodeEntry.cssObject.element.querySelector('textarea');
|
|
4094
|
+
if (nodeEntry.cssObject?.element) {
|
|
4095
|
+
setTimeout(() => {
|
|
4096
|
+
if (textareaEl) {
|
|
4097
|
+
textareaEl.focus({ preventScroll: true });
|
|
4098
|
+
}
|
|
4099
|
+
|
|
4100
|
+
const synced = window.MindMapCss3DManager?.syncCss3dScrollFromModel?.(module, nodeId, {
|
|
4101
|
+
source: 'switchToCss3D'
|
|
4102
|
+
}) === true;
|
|
4103
|
+
if (synced || !textareaEl) {
|
|
4104
|
+
return;
|
|
4105
|
+
}
|
|
4106
|
+
|
|
4107
|
+
// Fallback for older note editing surfaces.
|
|
4108
|
+
const maxScroll = nodeEntry.model.maxScroll || 1;
|
|
4109
|
+
const scrollOffset = nodeEntry.model.scrollOffset || 0;
|
|
4110
|
+
const scrollRatio = maxScroll > 0 ? (scrollOffset / maxScroll) : 0;
|
|
4111
|
+
|
|
4112
|
+
const maxCssScroll = textareaEl.scrollHeight - textareaEl.clientHeight;
|
|
4113
|
+
const targetScrollTop = Math.round(scrollRatio * maxCssScroll);
|
|
4114
|
+
|
|
4115
|
+
textareaEl.scrollTop = targetScrollTop;
|
|
4116
|
+
log(`[MindMapNodes] CSS3D scroll synced: ratio=${scrollRatio.toFixed(2)}, scrollTop=${targetScrollTop}px`);
|
|
4117
|
+
}, 50);
|
|
4118
|
+
}
|
|
4119
|
+
|
|
4120
|
+
nodeEntry.currentType = 'CSS';
|
|
4121
|
+
setNodeScrollbarVisibility(nodeEntry, false);
|
|
4122
|
+
log(`[MindMapNodes] ✓ Node ${nodeId} is now in CSS3D mode`);
|
|
4123
|
+
}
|
|
4124
|
+
|
|
4125
|
+
async function switchToWebGL(module, nodeId) {
|
|
4126
|
+
const nodeEntry = module.nodeObjectsById.get(nodeId);
|
|
4127
|
+
// ▼▼▼ [Log] Reason for skipping switch (SwitchToWebGL) ▼▼▼
|
|
4128
|
+
if (!nodeEntry || !nodeEntry.cssObject) {
|
|
4129
|
+
// Note: contentType check removed here to allow generic handling if needed,
|
|
4130
|
+
// but usually this is for text/note/markdown.
|
|
4131
|
+
return;
|
|
4132
|
+
}
|
|
4133
|
+
|
|
4134
|
+
// ▼▼▼ [Changed] HIGH 모드(CSS3D 유효 구간)에서는 WebGL로 전환 금지 ▼▼▼
|
|
4135
|
+
const cameraZ = module.camera?.position?.z || 0;
|
|
4136
|
+
const threshold = 4000;
|
|
4137
|
+
if (cameraZ < threshold) {
|
|
4138
|
+
const supportsCss3d = nodeEntry.model.contentType === 'text' ||
|
|
4139
|
+
nodeEntry.model.contentType === 'note' ||
|
|
4140
|
+
nodeEntry.model.contentType === 'memo' ||
|
|
4141
|
+
nodeEntry.model.contentType === 'markdown' ||
|
|
4142
|
+
nodeEntry.model.contentType === 'code' ||
|
|
4143
|
+
nodeEntry.model.contentType === 'image' ||
|
|
4144
|
+
nodeEntry.model.contentType === 'video' ||
|
|
4145
|
+
nodeEntry.model.contentType === 'embed';
|
|
4146
|
+
if (supportsCss3d) {
|
|
4147
|
+
console.log(`[MindMapNodes] SwitchToWebGL(${nodeId}) SKIPPED (HIGH Mode - Keeping CSS3D).`);
|
|
4148
|
+
return;
|
|
4149
|
+
}
|
|
4150
|
+
}
|
|
4151
|
+
// ▲▲▲ [Changed] ▲▲▲
|
|
4152
|
+
|
|
4153
|
+
if (nodeEntry.currentType === 'GL') {
|
|
4154
|
+
log(`[MindMapNodes] SwitchToWebGL(${nodeId}) SKIPPED (Already in GL mode).`);
|
|
4155
|
+
return;
|
|
4156
|
+
}
|
|
4157
|
+
log(`[MindMapNodes] Switching node ${nodeId} to WebGL mode`);
|
|
4158
|
+
// ▲▲▲ [Log] ▲▲▲
|
|
4159
|
+
|
|
4160
|
+
if (nodeEntry.isDeferredShell === true) {
|
|
4161
|
+
await ensureDeferredNodeMaterialized(module, nodeEntry, { attachToScene: true });
|
|
4162
|
+
}
|
|
4163
|
+
|
|
4164
|
+
// Store CSS3D textarea scroll ratio into WebGL scrollOffset
|
|
4165
|
+
const textarea = nodeEntry.cssObject.element.querySelector('textarea');
|
|
4166
|
+
if (textarea) {
|
|
4167
|
+
// Compute CSS3D scroll ratio
|
|
4168
|
+
const maxCssScroll = textarea.scrollHeight - textarea.clientHeight;
|
|
4169
|
+
const cssScrollTop = textarea.scrollTop || 0;
|
|
4170
|
+
const scrollRatio = maxCssScroll > 0 ? (cssScrollTop / maxCssScroll) : 0;
|
|
4171
|
+
|
|
4172
|
+
// Apply ratio to WebGL maxScroll to compute scrollOffset
|
|
4173
|
+
const maxScroll = nodeEntry.model.maxScroll || 0;
|
|
4174
|
+
const newScrollOffset = Math.round(scrollRatio * maxScroll);
|
|
4175
|
+
|
|
4176
|
+
if (nodeEntry.model.scrollOffset !== newScrollOffset) {
|
|
4177
|
+
nodeEntry.model.scrollOffset = newScrollOffset;
|
|
4178
|
+
log(`[MindMapNodes] WebGL scroll synced: ratio=${scrollRatio.toFixed(2)}, scrollOffset=${newScrollOffset}px`);
|
|
4179
|
+
}
|
|
4180
|
+
|
|
4181
|
+
if (nodeEntry.model.response !== textarea.value) {
|
|
4182
|
+
// ▼▼▼ [Log] Data sync log ▼▼▼
|
|
4183
|
+
log(`[MindMapNodes] -> Content updated from textarea (Length: ${textarea.value.length})`);
|
|
4184
|
+
// ▲▲▲ [Log] ▲▲▲
|
|
4185
|
+
nodeEntry.model.response = textarea.value;
|
|
4186
|
+
}
|
|
4187
|
+
}
|
|
4188
|
+
|
|
4189
|
+
const resolutionScale = nodeEntry.cssObject.userData?.resolutionScale || 1;
|
|
4190
|
+
const rawWidth = parseFloat(nodeEntry.cssObject.element.style.width) || nodeEntry.model.width;
|
|
4191
|
+
const rawHeight = parseFloat(nodeEntry.cssObject.element.style.height) || nodeEntry.model.height;
|
|
4192
|
+
const newWidth = rawWidth / resolutionScale;
|
|
4193
|
+
const newHeight = rawHeight / resolutionScale;
|
|
4194
|
+
|
|
4195
|
+
// ▼▼▼ [Log] Data sync log ▼▼▼
|
|
4196
|
+
if (newWidth !== nodeEntry.model.width || newHeight !== nodeEntry.model.height) {
|
|
4197
|
+
log(`[MindMapNodes] -> Dimensions updated from CSS (${newWidth}x${newHeight})`);
|
|
4198
|
+
}
|
|
4199
|
+
// ▲▲▲ [Log] ▲▲▲
|
|
4200
|
+
nodeEntry.model.width = newWidth;
|
|
4201
|
+
nodeEntry.model.height = newHeight;
|
|
4202
|
+
if (nodeEntry.cssObject?.userData) {
|
|
4203
|
+
nodeEntry.cssObject.userData.worldWidth = newWidth;
|
|
4204
|
+
nodeEntry.cssObject.userData.worldHeight = newHeight;
|
|
4205
|
+
}
|
|
4206
|
+
|
|
4207
|
+
const cssPos = nodeEntry.cssObject.position;
|
|
4208
|
+
// [Fix] Force Z=0 to eliminate parallax
|
|
4209
|
+
nodeEntry.glObject.position.set(cssPos.x, cssPos.y, 0);
|
|
4210
|
+
if (nodeEntry.glObject) {
|
|
4211
|
+
ensureNodeMeshCache(nodeEntry);
|
|
4212
|
+
nodeEntry.glObject.visible = true;
|
|
4213
|
+
const body = nodeEntry.bodyMesh;
|
|
4214
|
+
const tail = nodeEntry.tailMesh;
|
|
4215
|
+
if (body) body.visible = true;
|
|
4216
|
+
if (tail) tail.visible = true;
|
|
4217
|
+
setNodeScrollbarVisibility(nodeEntry, Number(nodeEntry.model?.maxScroll || 0) > 0);
|
|
4218
|
+
}
|
|
4219
|
+
|
|
4220
|
+
nodeEntry.currentType = 'GL';
|
|
4221
|
+
log(`[MindMapNodes] ✓ Node ${nodeId} is now in WebGL mode`);
|
|
4222
|
+
|
|
4223
|
+
await updateNodeContent(module, nodeId, nodeEntry.model.response, nodeEntry.model.width, nodeEntry.model.height);
|
|
4224
|
+
}
|
|
4225
|
+
|
|
4226
|
+
// ▼▼▼ [New] Update node content type (used for converting text to note) ▼▼▼
|
|
4227
|
+
async function updateNodeContentType(module, nodeId, newContentType) {
|
|
4228
|
+
const nodeEntry = module.nodeObjectsById.get(nodeId);
|
|
4229
|
+
if (!nodeEntry) {
|
|
4230
|
+
console.warn(`[MindMapNodes] updateNodeContentType: Node not found: ${nodeId}`);
|
|
4231
|
+
return;
|
|
4232
|
+
}
|
|
4233
|
+
|
|
4234
|
+
const model = nodeEntry.model;
|
|
4235
|
+
const oldContentType = model.contentType;
|
|
4236
|
+
|
|
4237
|
+
log(`[MindMapNodes] Updating node ${nodeId} content type: ${oldContentType} -> ${newContentType}`);
|
|
4238
|
+
|
|
4239
|
+
// Store current position and data
|
|
4240
|
+
const position = nodeEntry.glObject.position.clone();
|
|
4241
|
+
const nodeData = {
|
|
4242
|
+
id: model.id,
|
|
4243
|
+
contentType: newContentType,
|
|
4244
|
+
prompt: model.prompt,
|
|
4245
|
+
response: model.response,
|
|
4246
|
+
positionX: position.x,
|
|
4247
|
+
positionY: position.y,
|
|
4248
|
+
positionZ: position.z,
|
|
4249
|
+
width: model.width ?? model.Width,
|
|
4250
|
+
height: model.height ?? model.Height,
|
|
4251
|
+
isLoading: false,
|
|
4252
|
+
isAiEnabled: model.isAiEnabled,
|
|
4253
|
+
scrollOffset: model.scrollOffset || 0,
|
|
4254
|
+
maxScroll: model.maxScroll || 0,
|
|
4255
|
+
isChunkedText: model.isChunkedText,
|
|
4256
|
+
totalLineCount: model.totalLineCount,
|
|
4257
|
+
sourceFilePath: model.sourceFilePath,
|
|
4258
|
+
metadata: model.metadata ? { ...model.metadata } : {}
|
|
4259
|
+
};
|
|
4260
|
+
|
|
4261
|
+
// Remove old node
|
|
4262
|
+
removeNode(module, nodeId);
|
|
4263
|
+
|
|
4264
|
+
// Ensure node is fully removed from the map
|
|
4265
|
+
if (module.nodeObjectsById.has(nodeId)) {
|
|
4266
|
+
console.warn(`[MindMapNodes] Node ${nodeId} still in map after removeNode, force deleting...`);
|
|
4267
|
+
module.nodeObjectsById.delete(nodeId);
|
|
4268
|
+
}
|
|
4269
|
+
|
|
4270
|
+
// Wait for a frame to ensure cleanup is complete
|
|
4271
|
+
await waitForNonVisualFrame();
|
|
4272
|
+
|
|
4273
|
+
// Add new node with updated content type
|
|
4274
|
+
log(`[MindMapNodes] Re-adding node ${nodeId} with type ${newContentType}`);
|
|
4275
|
+
await addNode(module, nodeData);
|
|
4276
|
+
|
|
4277
|
+
// If converting to note, switch to CSS3D mode for editing
|
|
4278
|
+
if (newContentType === 'note') {
|
|
4279
|
+
// Wait for node to be fully created
|
|
4280
|
+
await new Promise(resolve => setTimeout(resolve, 150));
|
|
4281
|
+
|
|
4282
|
+
const newEntry = module.nodeObjectsById.get(nodeId);
|
|
4283
|
+
log(`[MindMapNodes] Checking for CSS3D object: exists=${!!newEntry}, hasCssObject=${!!newEntry?.cssObject}`);
|
|
4284
|
+
|
|
4285
|
+
if (newEntry && newEntry.cssObject) {
|
|
4286
|
+
await switchToCss3D(module, nodeId);
|
|
4287
|
+
// Focus the textarea
|
|
4288
|
+
const textarea = newEntry.cssObject.element?.querySelector('textarea');
|
|
4289
|
+
if (textarea) {
|
|
4290
|
+
textarea.focus();
|
|
4291
|
+
log(`[MindMapNodes] Focused textarea for node ${nodeId}`);
|
|
4292
|
+
}
|
|
4293
|
+
} else {
|
|
4294
|
+
console.warn(`[MindMapNodes] CSS3D object not ready for node ${nodeId}`);
|
|
4295
|
+
}
|
|
4296
|
+
}
|
|
4297
|
+
|
|
4298
|
+
log(`[MindMapNodes] Node ${nodeId} content type updated successfully`);
|
|
4299
|
+
}
|
|
4300
|
+
// ▲▲▲ [New] ▲▲▲
|
|
4301
|
+
|
|
4302
|
+
// ▼▼▼ [New] Update Troika scroll for code nodes ▼▼▼
|
|
4303
|
+
/**
|
|
4304
|
+
* Update scroll position for Troika code nodes
|
|
4305
|
+
* @param {Object} module - MindMap module
|
|
4306
|
+
* @param {Object} nodeEntry - Node entry from nodeObjectsById
|
|
4307
|
+
* @param {number} scrollOffset - New scroll offset
|
|
4308
|
+
*/
|
|
4309
|
+
function updateTroikaScroll(module, nodeEntry, scrollOffset) {
|
|
4310
|
+
if (!nodeEntry || !nodeEntry.glObject) return;
|
|
4311
|
+
|
|
4312
|
+
// Find Troika text mesh in glObject
|
|
4313
|
+
const troikaText = nodeEntry.glObject.userData?.troikaText;
|
|
4314
|
+
if (!troikaText || !troikaText.userData) return;
|
|
4315
|
+
|
|
4316
|
+
const ud = troikaText.userData;
|
|
4317
|
+
const padding = ud.padding || 16;
|
|
4318
|
+
const visibleHeight = ud.visibleHeight || (nodeEntry.model.height - padding * 2);
|
|
4319
|
+
const textWidth = ud.textWidth || (nodeEntry.model.width - padding * 2);
|
|
4320
|
+
const initialY = ud.initialY || -padding;
|
|
4321
|
+
|
|
4322
|
+
// Update scroll state
|
|
4323
|
+
ud.scrollY = Math.max(0, scrollOffset);
|
|
4324
|
+
|
|
4325
|
+
// 1. Move text position UP by scrollY
|
|
4326
|
+
troikaText.position.y = initialY + ud.scrollY;
|
|
4327
|
+
|
|
4328
|
+
// 2. Shift clipRect DOWN to compensate (keep visible window at node top)
|
|
4329
|
+
const clipTop = -ud.scrollY;
|
|
4330
|
+
const clipBottom = -ud.scrollY - visibleHeight;
|
|
4331
|
+
troikaText.clipRect = [0, clipBottom, textWidth, clipTop];
|
|
4332
|
+
|
|
4333
|
+
troikaText.sync();
|
|
4334
|
+
|
|
4335
|
+
// 3. Update scrollbar thumb position (ShapeGeometry anchor is top-left, not center)
|
|
4336
|
+
const scrollbarThumb = nodeEntry.glObject.getObjectByName('scrollbarThumb');
|
|
4337
|
+
if (scrollbarThumb && nodeEntry.model.maxScroll > 0) {
|
|
4338
|
+
const thumbData = scrollbarThumb.userData;
|
|
4339
|
+
const scrollRatio = ud.scrollY / nodeEntry.model.maxScroll;
|
|
4340
|
+
const thumbTravel = thumbData.trackHeight - thumbData.thumbHeight;
|
|
4341
|
+
const thumbY = -thumbData.topMargin - (scrollRatio * thumbTravel);
|
|
4342
|
+
scrollbarThumb.position.y = thumbY;
|
|
4343
|
+
}
|
|
4344
|
+
|
|
4345
|
+
log(`[updateTroikaScroll] Node ${nodeEntry.model.id}: scrollY=${ud.scrollY}, clipRect=[0, ${clipBottom.toFixed(0)}, ${textWidth}, ${clipTop.toFixed(0)}]`);
|
|
4346
|
+
}
|
|
4347
|
+
// ▲▲▲ [New] ▲▲▲
|
|
4348
|
+
|
|
4349
|
+
// Expose MindMapNodes globally
|
|
4350
|
+
window.MindMapNodes = {
|
|
4351
|
+
// Module state (helper modules need access)
|
|
4352
|
+
get textureCache() { return textureCache; },
|
|
4353
|
+
set textureCache(value) {
|
|
4354
|
+
textureCache = value instanceof Map ? value : new Map();
|
|
4355
|
+
totalTextureMemoryBytes = estimateTextureCacheMemoryBytes(textureCache);
|
|
4356
|
+
},
|
|
4357
|
+
get imageCache() { return imageCache; },
|
|
4358
|
+
set imageCache(value) {
|
|
4359
|
+
imageCache = value instanceof Map ? value : new Map();
|
|
4360
|
+
},
|
|
4361
|
+
get nodeRenderLocks() { return nodeRenderLocks; },
|
|
4362
|
+
set nodeRenderLocks(value) {
|
|
4363
|
+
nodeRenderLocks = value instanceof Map ? value : new Map();
|
|
4364
|
+
},
|
|
4365
|
+
get pendingNodeUpdates() { return pendingNodeUpdates; },
|
|
4366
|
+
set pendingNodeUpdates(value) {
|
|
4367
|
+
pendingNodeUpdates = value instanceof Map ? value : new Map();
|
|
4368
|
+
},
|
|
4369
|
+
get totalTextureMemoryBytes() { return totalTextureMemoryBytes; },
|
|
4370
|
+
set totalTextureMemoryBytes(value) {
|
|
4371
|
+
const nextValue = Number(value);
|
|
4372
|
+
totalTextureMemoryBytes = Number.isFinite(nextValue)
|
|
4373
|
+
? Math.max(0, nextValue)
|
|
4374
|
+
: estimateTextureCacheMemoryBytes(textureCache);
|
|
4375
|
+
},
|
|
4376
|
+
|
|
4377
|
+
// Public API
|
|
4378
|
+
addNode: addNode,
|
|
4379
|
+
removeNode: removeNode,
|
|
4380
|
+
removeNodes: removeNodes,
|
|
4381
|
+
updateNodeContent: updateNodeContent,
|
|
4382
|
+
clearMultiSelection: clearMultiSelection,
|
|
4383
|
+
refreshLodSelectionState: refreshLodSelectionState,
|
|
4384
|
+
bringNodeToFront: bringNodeToFront,
|
|
4385
|
+
updateNodeSelectionStyle: updateNodeSelectionStyle,
|
|
4386
|
+
applySingleSelection: applySingleSelection,
|
|
4387
|
+
resetState: resetState,
|
|
4388
|
+
captureDeleteSnapshot: captureDeleteSnapshot,
|
|
4389
|
+
// ▼▼▼ [New] Expose helper function for external callers ▼▼▼
|
|
4390
|
+
getNodeWorldBounds: getNodeWorldBounds,
|
|
4391
|
+
updateNodeSpatialGrid: updateNodeSpatialGrid,
|
|
4392
|
+
applyImageCoverUv: applyImageCoverUv,
|
|
4393
|
+
syncImageNodeBodyTexture: syncImageNodeBodyTexture,
|
|
4394
|
+
updateAnimatedImageStatusTextures: updateAnimatedImageStatusTextures,
|
|
4395
|
+
applyCanonicalNodeResponse: applyCanonicalNodeResponse,
|
|
4396
|
+
isGeneratedImageNodeModel: isGeneratedImageNodeModel,
|
|
4397
|
+
isAnimatedGifImageNodeModel: isAnimatedGifImageNodeModel,
|
|
4398
|
+
getNodeImageAssetUrls: getNodeImageAssetUrls,
|
|
4399
|
+
setNodeImageOriginalUrl: setNodeImageOriginalUrl,
|
|
4400
|
+
setNodeImagePreviewUrl: setNodeImagePreviewUrl,
|
|
4401
|
+
clearImageBodyTexture: clearImageBodyTexture,
|
|
4402
|
+
shouldUseCssImageLoadingOverlay: shouldUseCssImageLoadingOverlay,
|
|
4403
|
+
shouldShowImageBodyInCssMode: shouldShowImageBodyInCssMode,
|
|
4404
|
+
ensureNodeImageUsesPreviewResponse: ensureNodeImageUsesPreviewResponse,
|
|
4405
|
+
markNodeImageAssetMissing: markNodeImageAssetMissing,
|
|
4406
|
+
syncImageMidLodDecorations: syncImageMidLodDecorations,
|
|
4407
|
+
setNodeScrollbarVisibility: setNodeScrollbarVisibility,
|
|
4408
|
+
estimateTextureCacheMemoryBytes: estimateTextureCacheMemoryBytes,
|
|
4409
|
+
getCacheStats: getRuntimeCacheStats,
|
|
4410
|
+
replaceRuntimeCaches: replaceRuntimeCaches,
|
|
4411
|
+
// ▲▲▲ [New] ▲▲▲
|
|
4412
|
+
// ▼▼▼ [New] Troika scroll update ▼▼▼
|
|
4413
|
+
updateTroikaScroll: updateTroikaScroll,
|
|
4414
|
+
// ▲▲▲ [New] ▲▲▲
|
|
4415
|
+
switchToCss3D: switchToCss3D,
|
|
4416
|
+
switchToWebGL: switchToWebGL,
|
|
4417
|
+
// ▼▼▼ [New] Convert node content type ▼▼▼
|
|
4418
|
+
updateNodeContentType: updateNodeContentType,
|
|
4419
|
+
// ▲▲▲ [New] ▲▲▲
|
|
4420
|
+
|
|
4421
|
+
// ▼▼▼ [New] Text Render Mode API ▼▼▼
|
|
4422
|
+
/**
|
|
4423
|
+
* Get current text rendering mode
|
|
4424
|
+
* @returns {'canvas'|'troika'}
|
|
4425
|
+
*/
|
|
4426
|
+
getTextRenderMode: () => TEXT_RENDER_MODE,
|
|
4427
|
+
|
|
4428
|
+
/**
|
|
4429
|
+
* Set text rendering mode
|
|
4430
|
+
* @param {'canvas'|'troika'} mode
|
|
4431
|
+
*/
|
|
4432
|
+
setTextRenderMode: (mode) => {
|
|
4433
|
+
if (mode === 'canvas' || mode === 'troika') {
|
|
4434
|
+
TEXT_RENDER_MODE = mode;
|
|
4435
|
+
console.log(`[MindMapNodes] Text render mode changed to: ${mode}`);
|
|
4436
|
+
} else {
|
|
4437
|
+
console.warn(`[MindMapNodes] Invalid render mode: ${mode}. Use 'canvas' or 'troika'.`);
|
|
4438
|
+
}
|
|
4439
|
+
},
|
|
4440
|
+
|
|
4441
|
+
/**
|
|
4442
|
+
* Check if Troika rendering is available
|
|
4443
|
+
* @returns {boolean}
|
|
4444
|
+
*/
|
|
4445
|
+
isTroikaAvailable: () => window.MindMapTroikaText?.isTroikaReady() || false,
|
|
4446
|
+
// ▲▲▲ [New] ▲▲▲
|
|
4447
|
+
|
|
4448
|
+
// ▼▼▼ [New] Word wrap toggle for code nodes ▼▼▼
|
|
4449
|
+
/**
|
|
4450
|
+
* Update word wrap setting for a code node (Troika text)
|
|
4451
|
+
* @param {Object} module - MindMap module
|
|
4452
|
+
* @param {string} nodeId - Node ID
|
|
4453
|
+
* @param {boolean} isWordWrap - Whether to wrap text
|
|
4454
|
+
*/
|
|
4455
|
+
updateNodeWordWrap: (module, nodeId, isWordWrap) => {
|
|
4456
|
+
const nodeEntry = module?.nodeObjectsById?.get(nodeId);
|
|
4457
|
+
if (!nodeEntry) {
|
|
4458
|
+
console.warn(`[MindMapNodes] updateNodeWordWrap: Node ${nodeId} not found`);
|
|
4459
|
+
return;
|
|
4460
|
+
}
|
|
4461
|
+
|
|
4462
|
+
// ▼▼▼ [FIX] CSS3D Mode Handling (Priority) ▼▼▼
|
|
4463
|
+
if (nodeEntry.cssObject && nodeEntry.cssObject.element) {
|
|
4464
|
+
// Try to find the content element (code-body for code nodes, or generic response)
|
|
4465
|
+
const textElement = nodeEntry.cssObject.element.querySelector('.code-body') ||
|
|
4466
|
+
nodeEntry.cssObject.element.querySelector('.node-response') ||
|
|
4467
|
+
nodeEntry.cssObject.element.querySelector('textarea');
|
|
4468
|
+
|
|
4469
|
+
if (textElement) {
|
|
4470
|
+
console.log(`[MindMapNodes] Updating CSS3D word wrap for ${nodeId}: ${isWordWrap}`);
|
|
4471
|
+
if (isWordWrap) {
|
|
4472
|
+
textElement.style.whiteSpace = 'pre-wrap';
|
|
4473
|
+
textElement.style.wordWrap = 'break-word'; // legacy support
|
|
4474
|
+
textElement.style.overflowWrap = 'break-word';
|
|
4475
|
+
textElement.style.overflowX = 'hidden'; // No horizontal scroll needed
|
|
4476
|
+
} else {
|
|
4477
|
+
textElement.style.whiteSpace = 'pre';
|
|
4478
|
+
textElement.style.wordWrap = 'normal';
|
|
4479
|
+
textElement.style.overflowWrap = 'normal';
|
|
4480
|
+
textElement.style.overflowX = 'auto'; // Enable horizontal scroll
|
|
4481
|
+
}
|
|
4482
|
+
// Continue to Troika check in case it's a hybrid node, or return?
|
|
4483
|
+
// Usually returning here is safe if we are purely in CSS3D mode for Code nodes.
|
|
4484
|
+
// But if fallback WebGL text exists, we might want to update that too (though it's likely hidden).
|
|
4485
|
+
// Let's allow fall-through but don't warn if Troika is missing.
|
|
4486
|
+
}
|
|
4487
|
+
}
|
|
4488
|
+
// ▲▲▲ [FIX] ▲▲▲
|
|
4489
|
+
|
|
4490
|
+
// Find Troika text mesh in the GL object
|
|
4491
|
+
const glObject = nodeEntry.glObject;
|
|
4492
|
+
if (!glObject) return;
|
|
4493
|
+
|
|
4494
|
+
let troikaText = null;
|
|
4495
|
+
glObject.traverse((child) => {
|
|
4496
|
+
if (child.isTroikaText) {
|
|
4497
|
+
troikaText = child;
|
|
4498
|
+
}
|
|
4499
|
+
});
|
|
4500
|
+
|
|
4501
|
+
if (!troikaText) {
|
|
4502
|
+
// If we already handled CSS3D, don't warn about missing Troika
|
|
4503
|
+
if (!nodeEntry.cssObject) {
|
|
4504
|
+
console.warn(`[MindMapNodes] updateNodeWordWrap: No Troika text found for ${nodeId}`);
|
|
4505
|
+
}
|
|
4506
|
+
if (window.MindMapTextOverlayV2?.invalidateNode) {
|
|
4507
|
+
window.MindMapTextOverlayV2.invalidateNode(module, nodeId, 'layout');
|
|
4508
|
+
}
|
|
4509
|
+
return;
|
|
4510
|
+
}
|
|
4511
|
+
|
|
4512
|
+
// Update Troika text properties
|
|
4513
|
+
const padding = 16;
|
|
4514
|
+
const nodeWidth = nodeEntry.model.width || 470;
|
|
4515
|
+
|
|
4516
|
+
if (isWordWrap) {
|
|
4517
|
+
// Enable word wrap
|
|
4518
|
+
troikaText.whiteSpace = 'pre-wrap';
|
|
4519
|
+
troikaText.overflowWrap = 'break-word';
|
|
4520
|
+
troikaText.maxWidth = nodeWidth - (padding * 2);
|
|
4521
|
+
} else {
|
|
4522
|
+
// Disable word wrap - allow horizontal overflow
|
|
4523
|
+
troikaText.whiteSpace = 'pre';
|
|
4524
|
+
troikaText.overflowWrap = 'normal';
|
|
4525
|
+
troikaText.maxWidth = Infinity;
|
|
4526
|
+
}
|
|
4527
|
+
|
|
4528
|
+
// Trigger re-render
|
|
4529
|
+
troikaText.sync();
|
|
4530
|
+
|
|
4531
|
+
if (window.MindMapTextOverlayV2?.invalidateNode) {
|
|
4532
|
+
window.MindMapTextOverlayV2.invalidateNode(module, nodeId, 'layout');
|
|
4533
|
+
}
|
|
4534
|
+
|
|
4535
|
+
console.log(`[MindMapNodes] Troika Word wrap updated for ${nodeId}: ${isWordWrap}, maxWidth: ${troikaText.maxWidth}`);
|
|
4536
|
+
}
|
|
4537
|
+
// ▲▲▲ [New] ▲▲▲
|
|
4538
|
+
};
|
|
4539
|
+
|
|
4540
|
+
log('✅ mind-map-nodes.js loaded (Refactored Core/Orchestrator)');
|
|
4541
|
+
})();
|