@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,2075 @@
|
|
|
1
|
+
// File name: X:\Dev@Work\MindExecution\Client\wwwroot\js\mind-map-pipeline.js
|
|
2
|
+
// [Refactor] Owns the async rendering queue and node resizing logic.
|
|
3
|
+
(function () {
|
|
4
|
+
'use strict';
|
|
5
|
+
|
|
6
|
+
const CSS3D_WRAPPER_TRANSITION = 'box-shadow .22s ease, filter .22s ease';
|
|
7
|
+
const CSS3D_WRAPPER_WEBKIT_TRANSITION = '-webkit-box-shadow .22s ease, -webkit-filter .22s ease';
|
|
8
|
+
|
|
9
|
+
// ▼▼▼ [Changed] Add state variables to switch from debounce to throttle ▼▼▼
|
|
10
|
+
const debounceTimers = new Map();
|
|
11
|
+
const scrollUpdateThrottleTimers = new Map();
|
|
12
|
+
const lastScrollCall = new Map();
|
|
13
|
+
const resizeThrottleTimers = new Map(); // [New] throttle for texture updates during resize
|
|
14
|
+
const lastResizeUpdateCall = new Map(); // [New] last resize-update timestamp
|
|
15
|
+
const THROTTLE_INTERVAL = 16; // 16ms -> ~60fps
|
|
16
|
+
const RESIZE_HANDLE_SIZE = 16;
|
|
17
|
+
const ENABLE_WEBGL_GLOW = false;
|
|
18
|
+
function isDocumentHidden() {
|
|
19
|
+
return typeof document !== 'undefined' && document.visibilityState === 'hidden';
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function scheduleNonVisualFrame(callback, hiddenDelay = 16) {
|
|
23
|
+
if (typeof requestAnimationFrame === 'function' && !isDocumentHidden()) {
|
|
24
|
+
return requestAnimationFrame(callback);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return setTimeout(() => callback(typeof performance !== 'undefined' ? performance.now() : Date.now()), hiddenDelay);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// ▼▼▼ [Perf] Render queue (time-sliced) ▼▼▼
|
|
31
|
+
const renderQueue = [];
|
|
32
|
+
const renderQueueSet = new Set();
|
|
33
|
+
let renderQueueScheduled = false;
|
|
34
|
+
let renderModuleRef = null;
|
|
35
|
+
const RENDER_BUDGET_MS = 8;
|
|
36
|
+
let renderSeq = 0;
|
|
37
|
+
|
|
38
|
+
function computeRenderPriority(module, nodeId) {
|
|
39
|
+
const entry = module?.nodeObjectsById?.get(nodeId);
|
|
40
|
+
if (!entry) return 1000000;
|
|
41
|
+
|
|
42
|
+
const obj = entry.glObject || entry.cssObject;
|
|
43
|
+
const visible = obj?.visible === true;
|
|
44
|
+
const pos = obj?.position || { x: 0, y: 0 };
|
|
45
|
+
const cam = module.camera?.position || { x: 0, y: 0 };
|
|
46
|
+
|
|
47
|
+
const dx = pos.x - cam.x;
|
|
48
|
+
const dy = pos.y - cam.y;
|
|
49
|
+
const dist = Math.sqrt(dx * dx + dy * dy);
|
|
50
|
+
|
|
51
|
+
const isSelected = module.multiSelectedNodeIds?.has(nodeId) || module.selectedNodeIdJs === nodeId;
|
|
52
|
+
|
|
53
|
+
// 낮은 값이 우선: 보이는 노드 > 선택 노드 > 카메라 근접
|
|
54
|
+
let priority = dist;
|
|
55
|
+
if (!visible) priority += 100000;
|
|
56
|
+
if (isSelected) priority -= 5000;
|
|
57
|
+
|
|
58
|
+
return priority;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function enqueueRender(module, nodeId) {
|
|
62
|
+
if (!nodeId) return;
|
|
63
|
+
renderModuleRef = module;
|
|
64
|
+
if (!renderQueueSet.has(nodeId)) {
|
|
65
|
+
renderQueue.push({ nodeId, priority: computeRenderPriority(module, nodeId), seq: renderSeq++ });
|
|
66
|
+
renderQueueSet.add(nodeId);
|
|
67
|
+
}
|
|
68
|
+
scheduleRenderQueue();
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function scheduleRenderQueue() {
|
|
72
|
+
if (renderQueueScheduled) return;
|
|
73
|
+
renderQueueScheduled = true;
|
|
74
|
+
scheduleNonVisualFrame(() => processRenderQueueBatch());
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async function processRenderQueueBatch() {
|
|
78
|
+
const start = performance.now();
|
|
79
|
+
|
|
80
|
+
// 우선순위 정렬 (가시/선택/거리)
|
|
81
|
+
if (renderQueue.length > 1) {
|
|
82
|
+
renderQueue.sort((a, b) => (a.priority - b.priority) || (a.seq - b.seq));
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
while (renderQueue.length > 0) {
|
|
86
|
+
const item = renderQueue.shift();
|
|
87
|
+
const nodeId = item.nodeId;
|
|
88
|
+
renderQueueSet.delete(nodeId);
|
|
89
|
+
if (renderModuleRef) {
|
|
90
|
+
await processRenderQueue(renderModuleRef, nodeId);
|
|
91
|
+
}
|
|
92
|
+
if ((performance.now() - start) >= RENDER_BUDGET_MS) {
|
|
93
|
+
break;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
renderQueueScheduled = false;
|
|
98
|
+
if (renderQueue.length > 0) {
|
|
99
|
+
scheduleRenderQueue();
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
// ▲▲▲ [Perf] ▲▲▲
|
|
103
|
+
|
|
104
|
+
// ▼▼▼ [New] Chunked-node virtual scroll constants ▼▼▼
|
|
105
|
+
const CHUNK_LINE_HEIGHT = 21; // 14px font * 1.5 line-height (matches plain text rendering)
|
|
106
|
+
const CHUNK_SIZE = 100; // lines per chunk (matches C# TextChunkService)
|
|
107
|
+
// ▲▲▲ [New] ▲▲▲
|
|
108
|
+
|
|
109
|
+
// ▼▼▼ [Perf] Deferred disposal to avoid GC/driver spikes ▼▼▼
|
|
110
|
+
const disposeQueue = [];
|
|
111
|
+
let disposeScheduled = false;
|
|
112
|
+
const DISPOSE_BUDGET_MS = 6; // soft budget per frame
|
|
113
|
+
|
|
114
|
+
function enqueueDispose(resource) {
|
|
115
|
+
if (!resource || typeof resource.dispose !== 'function') return;
|
|
116
|
+
disposeQueue.push(resource);
|
|
117
|
+
scheduleDispose();
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function scheduleDispose() {
|
|
121
|
+
if (disposeScheduled) return;
|
|
122
|
+
disposeScheduled = true;
|
|
123
|
+
if (typeof requestIdleCallback === 'function') {
|
|
124
|
+
requestIdleCallback(processDisposeQueue);
|
|
125
|
+
} else {
|
|
126
|
+
scheduleNonVisualFrame(() => processDisposeQueue());
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function processDisposeQueue(deadline) {
|
|
131
|
+
const start = performance.now();
|
|
132
|
+
while (disposeQueue.length > 0) {
|
|
133
|
+
const item = disposeQueue.shift();
|
|
134
|
+
try { item.dispose(); } catch { }
|
|
135
|
+
|
|
136
|
+
if (deadline && typeof deadline.timeRemaining === 'function') {
|
|
137
|
+
if (deadline.timeRemaining() < 1) break;
|
|
138
|
+
} else if ((performance.now() - start) >= DISPOSE_BUDGET_MS) {
|
|
139
|
+
break;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
disposeScheduled = false;
|
|
144
|
+
if (disposeQueue.length > 0) {
|
|
145
|
+
scheduleDispose();
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
// ▲▲▲ [Perf] ▲▲▲
|
|
149
|
+
|
|
150
|
+
function removeGlowMesh(group) {
|
|
151
|
+
if (!group?.getObjectByName) return null;
|
|
152
|
+
const glowMesh = group.getObjectByName('glow');
|
|
153
|
+
if (!glowMesh) return null;
|
|
154
|
+
|
|
155
|
+
group.remove(glowMesh);
|
|
156
|
+
enqueueDispose(glowMesh.geometry);
|
|
157
|
+
enqueueDispose(glowMesh.material);
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function applyImageCoverUv(bodyMesh, width, height) {
|
|
162
|
+
if (!bodyMesh) return;
|
|
163
|
+
if (window.MindMapNodes?.applyImageCoverUv) {
|
|
164
|
+
window.MindMapNodes.applyImageCoverUv(bodyMesh, width, height);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// ▼▼▼ [Changed] Geometry helper for rounded corners ▼▼▼
|
|
169
|
+
function createRoundedRectGeometry(width, height, radius) {
|
|
170
|
+
const shape = new THREE.Shape();
|
|
171
|
+
const x = -width / 2;
|
|
172
|
+
const y = -height / 2;
|
|
173
|
+
|
|
174
|
+
// Start at bottom-left, go clockwise
|
|
175
|
+
shape.moveTo(x, y + radius);
|
|
176
|
+
shape.quadraticCurveTo(x, y, x + radius, y);
|
|
177
|
+
|
|
178
|
+
shape.lineTo(x + width - radius, y);
|
|
179
|
+
shape.quadraticCurveTo(x + width, y, x + width, y + radius);
|
|
180
|
+
|
|
181
|
+
shape.lineTo(x + width, y + height - radius);
|
|
182
|
+
shape.quadraticCurveTo(x + width, y + height, x + width - radius, y + height);
|
|
183
|
+
|
|
184
|
+
shape.lineTo(x + radius, y + height);
|
|
185
|
+
shape.quadraticCurveTo(x, y + height, x, y + height - radius);
|
|
186
|
+
|
|
187
|
+
shape.lineTo(x, y + radius);
|
|
188
|
+
const points = shape.getPoints();
|
|
189
|
+
return new THREE.BufferGeometry().setFromPoints(points);
|
|
190
|
+
}
|
|
191
|
+
// ▲▲▲ [Changed] ▲▲▲
|
|
192
|
+
|
|
193
|
+
function createRoundedRectFilledGeometry(width, height, radius) {
|
|
194
|
+
const shape = new THREE.Shape();
|
|
195
|
+
const x = 0;
|
|
196
|
+
const y = 0;
|
|
197
|
+
const r = Math.min(radius, width / 2, height / 2);
|
|
198
|
+
const h = -height;
|
|
199
|
+
|
|
200
|
+
shape.moveTo(x + r, y);
|
|
201
|
+
shape.lineTo(x + width - r, y);
|
|
202
|
+
shape.quadraticCurveTo(x + width, y, x + width, y - r);
|
|
203
|
+
shape.lineTo(x + width, y + h + r);
|
|
204
|
+
shape.quadraticCurveTo(x + width, y + h, x + width - r, y + h);
|
|
205
|
+
shape.lineTo(x + r, y + h);
|
|
206
|
+
shape.quadraticCurveTo(x, y + h, x, y + h + r);
|
|
207
|
+
shape.lineTo(x, y - r);
|
|
208
|
+
shape.quadraticCurveTo(x, y, x + r, y);
|
|
209
|
+
|
|
210
|
+
return new THREE.ShapeGeometry(shape);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const IMAGE_SELECTION_GLOW_PAD = 18;
|
|
214
|
+
const IMAGE_SELECTION_GLOW_COLOR = 0x1e3a8a;
|
|
215
|
+
const IMAGE_SELECTION_GLOW_OPACITY = 0.18;
|
|
216
|
+
|
|
217
|
+
function syncImageSelectionGlow(group, width, height, baseRenderOrder, isSelected) {
|
|
218
|
+
if (!group) return null;
|
|
219
|
+
|
|
220
|
+
let outline = group.getObjectByName('outline');
|
|
221
|
+
if (!outline) {
|
|
222
|
+
outline = new THREE.Mesh(
|
|
223
|
+
createRoundedRectFilledGeometry(
|
|
224
|
+
width + IMAGE_SELECTION_GLOW_PAD,
|
|
225
|
+
height + IMAGE_SELECTION_GLOW_PAD,
|
|
226
|
+
20 + (IMAGE_SELECTION_GLOW_PAD * 0.5)
|
|
227
|
+
),
|
|
228
|
+
new THREE.MeshBasicMaterial({
|
|
229
|
+
color: IMAGE_SELECTION_GLOW_COLOR,
|
|
230
|
+
transparent: true,
|
|
231
|
+
opacity: 0,
|
|
232
|
+
depthWrite: false,
|
|
233
|
+
depthTest: false
|
|
234
|
+
})
|
|
235
|
+
);
|
|
236
|
+
outline.name = 'outline';
|
|
237
|
+
outline.userData = { ...(outline.userData || {}), isImageSelectionGlow: true };
|
|
238
|
+
group.add(outline);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
outline.geometry?.dispose?.();
|
|
242
|
+
outline.geometry = createRoundedRectFilledGeometry(
|
|
243
|
+
width + IMAGE_SELECTION_GLOW_PAD,
|
|
244
|
+
height + IMAGE_SELECTION_GLOW_PAD,
|
|
245
|
+
20 + (IMAGE_SELECTION_GLOW_PAD * 0.5)
|
|
246
|
+
);
|
|
247
|
+
outline.position.set(-(IMAGE_SELECTION_GLOW_PAD / 2), IMAGE_SELECTION_GLOW_PAD / 2, 0);
|
|
248
|
+
outline.renderOrder = baseRenderOrder - 1;
|
|
249
|
+
outline.visible = !!isSelected;
|
|
250
|
+
|
|
251
|
+
if (outline.material) {
|
|
252
|
+
outline.material.color.setHex(IMAGE_SELECTION_GLOW_COLOR);
|
|
253
|
+
outline.material.opacity = isSelected ? IMAGE_SELECTION_GLOW_OPACITY : 0;
|
|
254
|
+
outline.material.transparent = true;
|
|
255
|
+
outline.material.depthWrite = false;
|
|
256
|
+
outline.material.depthTest = false;
|
|
257
|
+
outline.material.needsUpdate = true;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
return outline;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// ▼▼▼ [New] Update visible content for virtual scrolling ▼▼▼
|
|
264
|
+
function updateVisibleContent(model, lineHeight, chunkSize) {
|
|
265
|
+
if (!model.isChunkedText || !model._chunks) {
|
|
266
|
+
model._visibleContent = model.response || '';
|
|
267
|
+
model._visibleScrollOffset = model.scrollOffset || 0;
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const scrollOffset = model.scrollOffset || 0;
|
|
272
|
+
const nodeHeight = model.height || 400;
|
|
273
|
+
|
|
274
|
+
// Visible line range in the current viewport
|
|
275
|
+
const visibleStartLine = Math.floor(scrollOffset / lineHeight);
|
|
276
|
+
const visibleLineCount = Math.ceil(nodeHeight / lineHeight) + 5; // small buffer
|
|
277
|
+
|
|
278
|
+
// Chunk indices needed for that range
|
|
279
|
+
const startChunkIndex = Math.floor(visibleStartLine / chunkSize) * chunkSize;
|
|
280
|
+
const endChunkIndex = Math.floor((visibleStartLine + visibleLineCount) / chunkSize) * chunkSize;
|
|
281
|
+
|
|
282
|
+
// Merge only the chunks that intersect the current viewport
|
|
283
|
+
let visibleContent = '';
|
|
284
|
+
let firstChunkStart = -1;
|
|
285
|
+
let hasContent = false;
|
|
286
|
+
|
|
287
|
+
for (let chunkStart = startChunkIndex; chunkStart <= endChunkIndex; chunkStart += chunkSize) {
|
|
288
|
+
if (model._chunks[chunkStart]) {
|
|
289
|
+
if (firstChunkStart === -1) firstChunkStart = chunkStart;
|
|
290
|
+
visibleContent += model._chunks[chunkStart] + '\n';
|
|
291
|
+
hasContent = true;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// ▼▼▼ [Changed] If no chunks exist in range, use the nearest loaded chunk ▼▼▼
|
|
296
|
+
if (!hasContent) {
|
|
297
|
+
// Find the nearest loaded chunk
|
|
298
|
+
const loadedChunks = Object.keys(model._chunks).map(Number).sort((a, b) => a - b);
|
|
299
|
+
if (loadedChunks.length > 0) {
|
|
300
|
+
// Pick the chunk closest to the current position
|
|
301
|
+
let closestChunk = loadedChunks[0];
|
|
302
|
+
for (const chunkStart of loadedChunks) {
|
|
303
|
+
if (chunkStart <= startChunkIndex) {
|
|
304
|
+
closestChunk = chunkStart;
|
|
305
|
+
} else {
|
|
306
|
+
break;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
firstChunkStart = closestChunk;
|
|
310
|
+
visibleContent = model._chunks[closestChunk] + '\n';
|
|
311
|
+
|
|
312
|
+
// Add the next chunk as well if present
|
|
313
|
+
const nextChunk = closestChunk + chunkSize;
|
|
314
|
+
if (model._chunks[nextChunk]) {
|
|
315
|
+
visibleContent += model._chunks[nextChunk] + '\n';
|
|
316
|
+
}
|
|
317
|
+
} else {
|
|
318
|
+
// If no chunks exist at all, keep the previous response
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
// ▲▲▲ [Changed] ▲▲▲
|
|
323
|
+
|
|
324
|
+
if (firstChunkStart === -1) firstChunkStart = 0;
|
|
325
|
+
|
|
326
|
+
// Compute relative scroll offset from the first chunk
|
|
327
|
+
const firstChunkPixelOffset = firstChunkStart * lineHeight;
|
|
328
|
+
const relativeScrollOffset = scrollOffset - firstChunkPixelOffset;
|
|
329
|
+
|
|
330
|
+
model._visibleContent = visibleContent;
|
|
331
|
+
model._visibleScrollOffset = Math.max(0, relativeScrollOffset);
|
|
332
|
+
|
|
333
|
+
// ▼▼▼ [Changed] Do not overwrite response for chunk nodes (saved/restored via full file path) ▼▼▼
|
|
334
|
+
// Chunk nodes are restored based on SourceFilePath, so avoid mutating response.
|
|
335
|
+
// model.response = visibleContent; // removed
|
|
336
|
+
// ▲▲▲ [Changed] ▲▲▲
|
|
337
|
+
}
|
|
338
|
+
// ▲▲▲ [New] ▲▲▲
|
|
339
|
+
// ▲▲▲ [Changed] ▲▲▲
|
|
340
|
+
|
|
341
|
+
async function processRenderQueue(module, nodeId) {
|
|
342
|
+
// ▼▼▼ [Profiling] Start per-node render timing ▼▼▼
|
|
343
|
+
// console.time(`[Pipeline] 6. Render Node ${nodeId}`);
|
|
344
|
+
// ▲▲▲ [Profiling] ▲▲▲
|
|
345
|
+
|
|
346
|
+
// ▼▼▼ [Changed] module.nodeRenderLocks -> window.MindMapNodes.nodeRenderLocks ▼▼▼
|
|
347
|
+
if (window.MindMapNodes.nodeRenderLocks.get(nodeId)) {
|
|
348
|
+
// console.log(`[Pipeline] [Log Detail] Node ${nodeId} skipped: Render lock active.`);
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// ▼▼▼ [Changed] module.pendingNodeUpdates -> window.MindMapNodes.pendingNodeUpdates ▼▼▼
|
|
353
|
+
const updateData = window.MindMapNodes.pendingNodeUpdates.get(nodeId);
|
|
354
|
+
if (!updateData) {
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// console.log(`[Pipeline] [Log Detail] Node ${nodeId} processing queue.`);
|
|
359
|
+
|
|
360
|
+
const HANDLE_SIZE = RESIZE_HANDLE_SIZE;
|
|
361
|
+
const nodeEntry = module.nodeObjectsById.get(nodeId);
|
|
362
|
+
if (!nodeEntry) {
|
|
363
|
+
const retryCount = Math.max(0, Number(updateData.retryCount || 0)) + 1;
|
|
364
|
+
if (retryCount <= 80) {
|
|
365
|
+
updateData.retryCount = retryCount;
|
|
366
|
+
window.MindMapNodes.pendingNodeUpdates.set(nodeId, updateData);
|
|
367
|
+
const delayMs = Math.min(1000, 50 + retryCount * 50);
|
|
368
|
+
setTimeout(() => {
|
|
369
|
+
if (window.MindMapNodes?.pendingNodeUpdates?.has?.(nodeId)) {
|
|
370
|
+
window.MindMapPipeline?.processRenderQueue?.(module, nodeId);
|
|
371
|
+
}
|
|
372
|
+
}, delayMs);
|
|
373
|
+
} else {
|
|
374
|
+
window.MindMapNodes.pendingNodeUpdates.delete(nodeId);
|
|
375
|
+
console.warn(`[MindMapPipeline] Dropped pending node update after waiting for missing node: ${nodeId}`);
|
|
376
|
+
}
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
window.MindMapNodes.nodeRenderLocks.set(nodeId, true);
|
|
381
|
+
|
|
382
|
+
try {
|
|
383
|
+
const model = nodeEntry.model;
|
|
384
|
+
if (Object.prototype.hasOwnProperty.call(updateData, 'isLoading')
|
|
385
|
+
&& updateData.isLoading !== null
|
|
386
|
+
&& updateData.isLoading !== undefined) {
|
|
387
|
+
model.isLoading = !!updateData.isLoading;
|
|
388
|
+
model.IsLoading = !!updateData.isLoading;
|
|
389
|
+
}
|
|
390
|
+
const zoomLevel = 2;
|
|
391
|
+
|
|
392
|
+
// ▼▼▼ [Perf] CSS3D 지원 노드는 거리와 무관하게 텍스처 생성 스킵 ▼▼▼
|
|
393
|
+
const cameraZ = module.camera?.position?.z || 0;
|
|
394
|
+
const threshold = 4000;
|
|
395
|
+
const isHighMode = cameraZ < threshold;
|
|
396
|
+
const supportsCss3d = model.contentType === 'text' ||
|
|
397
|
+
model.contentType === 'note' ||
|
|
398
|
+
model.contentType === 'memo' ||
|
|
399
|
+
model.contentType === 'markdown' ||
|
|
400
|
+
model.contentType === 'code';
|
|
401
|
+
|
|
402
|
+
// ★ [Fix] currentType이 'CSS'이면 카메라 거리와 무관하게 텍스처 생성 스킵
|
|
403
|
+
const isCssMode = nodeEntry.currentType === 'CSS';
|
|
404
|
+
const shouldSkipForCss = supportsCss3d && (isHighMode || cameraZ >= threshold || isCssMode);
|
|
405
|
+
|
|
406
|
+
if (shouldSkipForCss) {
|
|
407
|
+
const prevWidth = model.width || 0;
|
|
408
|
+
const prevHeight = model.height || 0;
|
|
409
|
+
const hasExplicitSizeUpdate =
|
|
410
|
+
(updateData.width !== null && updateData.width !== undefined) ||
|
|
411
|
+
(updateData.height !== null && updateData.height !== undefined);
|
|
412
|
+
|
|
413
|
+
// Update model state
|
|
414
|
+
model.response = updateData.content;
|
|
415
|
+
model.Response = updateData.content;
|
|
416
|
+
if (updateData.width !== null && updateData.width !== undefined) model.width = updateData.width;
|
|
417
|
+
if (updateData.height !== null && updateData.height !== undefined) model.height = updateData.height;
|
|
418
|
+
|
|
419
|
+
// ▼▼▼ [Fix] Sync CSS3D/GL userData with updated model dimensions ▼▼▼
|
|
420
|
+
// Without this, syncCss3dContent() reads stale userData.worldWidth/Height
|
|
421
|
+
// and reverts the DOM size, causing content/node size mismatch.
|
|
422
|
+
if (nodeEntry.cssObject?.userData) {
|
|
423
|
+
nodeEntry.cssObject.userData.worldWidth = model.width || 400;
|
|
424
|
+
nodeEntry.cssObject.userData.worldHeight = model.height || 200;
|
|
425
|
+
}
|
|
426
|
+
if (nodeEntry.glObject?.userData) {
|
|
427
|
+
nodeEntry.glObject.userData.worldWidth = model.width || 400;
|
|
428
|
+
nodeEntry.glObject.userData.worldHeight = model.height || 200;
|
|
429
|
+
}
|
|
430
|
+
// ▲▲▲ [Fix] ▲▲▲
|
|
431
|
+
|
|
432
|
+
// CSS3D-capable text/note nodes intentionally skip WebGL texture rebuilds.
|
|
433
|
+
// Keep the live DOM/overlay in sync here so async AI completions do not
|
|
434
|
+
// require a full board reload before the final answer becomes visible.
|
|
435
|
+
if (nodeEntry.cssObject && window.MindMapCss3DManager?.syncCss3dContent) {
|
|
436
|
+
nodeEntry.isCssDirty = true;
|
|
437
|
+
window.MindMapCss3DManager.syncCss3dContent(model, nodeEntry.cssObject);
|
|
438
|
+
nodeEntry.isCssDirty = false;
|
|
439
|
+
} else {
|
|
440
|
+
nodeEntry.isCssDirty = true;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
window.MindMapTextOverlayV2?.invalidateNode?.(module, nodeId, 'content');
|
|
444
|
+
module.lodRenderer?.requestResidentPatch?.('update-node', nodeId);
|
|
445
|
+
module._overlayDirty = true;
|
|
446
|
+
module._lastOverlayKey = '';
|
|
447
|
+
module._forceUpdateFrames = Math.max(Number(module._forceUpdateFrames || 0), 3);
|
|
448
|
+
|
|
449
|
+
if (isCssMode) {
|
|
450
|
+
window.MindMapNodes?.setNodeScrollbarVisibility?.(nodeEntry, false);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// Sync CSS3D element size (if present)
|
|
454
|
+
const wrapper = nodeEntry.cssObject?.element;
|
|
455
|
+
if (wrapper) {
|
|
456
|
+
const w = model.width || 400;
|
|
457
|
+
const h = model.height || 200;
|
|
458
|
+
const resolutionScale = nodeEntry.cssObject?.userData?.resolutionScale || 1;
|
|
459
|
+
wrapper.style.width = (w * resolutionScale) + 'px';
|
|
460
|
+
wrapper.style.height = (h * resolutionScale) + 'px';
|
|
461
|
+
wrapper.style.minWidth = (w * resolutionScale) + 'px';
|
|
462
|
+
wrapper.style.minHeight = (h * resolutionScale) + 'px';
|
|
463
|
+
wrapper.style.maxWidth = (w * resolutionScale) + 'px';
|
|
464
|
+
wrapper.style.maxHeight = (h * resolutionScale) + 'px';
|
|
465
|
+
const inner = wrapper.firstElementChild;
|
|
466
|
+
if (inner) {
|
|
467
|
+
inner.style.width = w + 'px';
|
|
468
|
+
inner.style.height = h + 'px';
|
|
469
|
+
inner.style.minWidth = w + 'px';
|
|
470
|
+
inner.style.minHeight = h + 'px';
|
|
471
|
+
inner.style.maxWidth = w + 'px';
|
|
472
|
+
inner.style.maxHeight = h + 'px';
|
|
473
|
+
inner.style.transform = `scale(${resolutionScale})`;
|
|
474
|
+
inner.style.transformOrigin = '0 0';
|
|
475
|
+
}
|
|
476
|
+
const textarea = wrapper.querySelector('textarea');
|
|
477
|
+
if (textarea) {
|
|
478
|
+
textarea.style.width = '100%';
|
|
479
|
+
textarea.style.height = '100%';
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
// Update glow size if needed (no texture work)
|
|
484
|
+
const group = nodeEntry.glObject;
|
|
485
|
+
if (group) {
|
|
486
|
+
if (!ENABLE_WEBGL_GLOW) {
|
|
487
|
+
removeGlowMesh(group);
|
|
488
|
+
} else if (window.MindMapGlowShader) {
|
|
489
|
+
const glowMesh = group.getObjectByName('glow');
|
|
490
|
+
const w = model.width || 400;
|
|
491
|
+
const h = model.height || 200;
|
|
492
|
+
if (glowMesh) {
|
|
493
|
+
window.MindMapGlowShader.updateGlowSize(glowMesh, w, h, model.contentType || 'text');
|
|
494
|
+
glowMesh.position.set(w / 2, -h / 2, 0); // [Fix] Z=0
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// Keep WebGL body/tail geometry aligned with CSS3D size (prevents split)
|
|
500
|
+
if (nodeEntry.glObject) {
|
|
501
|
+
const bodyMesh = nodeEntry.glObject.getObjectByName('body');
|
|
502
|
+
const tailMesh = nodeEntry.glObject.getObjectByName('tail');
|
|
503
|
+
const w = model.width || 400;
|
|
504
|
+
const h = model.height || 200;
|
|
505
|
+
|
|
506
|
+
if (bodyMesh) {
|
|
507
|
+
const gp = bodyMesh.geometry && bodyMesh.geometry.parameters;
|
|
508
|
+
if (!gp || gp.width !== w || gp.height !== h) {
|
|
509
|
+
bodyMesh.geometry.dispose();
|
|
510
|
+
bodyMesh.geometry = new THREE.PlaneGeometry(w, h);
|
|
511
|
+
}
|
|
512
|
+
bodyMesh.position.set(w / 2, -h / 2, 0);
|
|
513
|
+
if (bodyMesh.material) {
|
|
514
|
+
bodyMesh.material.color?.setHex?.(0xffffff);
|
|
515
|
+
bodyMesh.material.needsUpdate = true;
|
|
516
|
+
}
|
|
517
|
+
if (model.contentType === 'image') {
|
|
518
|
+
window.MindMapNodes?.syncImageNodeBodyTexture?.(bodyMesh, model);
|
|
519
|
+
applyImageCoverUv(bodyMesh, w, h);
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
if (tailMesh) {
|
|
524
|
+
tailMesh.position.set(w / 2, -h - 6, 0);
|
|
525
|
+
if (tailMesh.material) {
|
|
526
|
+
tailMesh.material.color?.setHex?.(0xffffff);
|
|
527
|
+
tailMesh.material.needsUpdate = true;
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
// Persist size changes for CSS-rendered nodes as well.
|
|
533
|
+
// Without this, auto-grown AI note dimensions are not saved to the board.
|
|
534
|
+
const sizeChanged = Math.abs((model.width || 0) - prevWidth) > 0.5
|
|
535
|
+
|| Math.abs((model.height || 0) - prevHeight) > 0.5;
|
|
536
|
+
if (!updateData.skipPersistDimensions && (sizeChanged || hasExplicitSizeUpdate) && module.dotNetHelper?.invokeMethodAsync) {
|
|
537
|
+
try {
|
|
538
|
+
await module.dotNetHelper.invokeMethodAsync(
|
|
539
|
+
'UpdateNodeDimensions',
|
|
540
|
+
nodeId,
|
|
541
|
+
Math.round(model.width || 0),
|
|
542
|
+
Math.round(model.height || 0)
|
|
543
|
+
);
|
|
544
|
+
} catch (e) {
|
|
545
|
+
console.warn('[MindMapPipeline] Failed to persist CSS node dimensions:', e);
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
window.MindMapNodes.pendingNodeUpdates.delete(nodeId);
|
|
550
|
+
window.MindMapNodes.nodeRenderLocks.set(nodeId, false);
|
|
551
|
+
return;
|
|
552
|
+
}
|
|
553
|
+
// ▲▲▲ [Perf] ▲▲▲
|
|
554
|
+
|
|
555
|
+
const width = (updateData.width !== null && updateData.width !== undefined) ?
|
|
556
|
+
updateData.width : (model.width || (model.contentType === 'image' ? 300 : 400));
|
|
557
|
+
|
|
558
|
+
// ▼▼▼ [Key change] Reduce height-change sensitivity (avoid jitter) ▼▼▼
|
|
559
|
+
// ▼▼▼ [Changed] Chunk nodes already have fixed maxScroll; skip recalculation ▼▼▼
|
|
560
|
+
// ▼▼▼ [Changed] Skip height recalculation for expanded nodes (they auto-grow in updateNodeContent) ▼▼▼
|
|
561
|
+
if ((model.contentType === 'text' || model.contentType === 'note') && !model.isChunkedText && !model.isExpanded) {
|
|
562
|
+
if (updateData.height === null || updateData.height === undefined) {
|
|
563
|
+
const tempModel = { ...model, response: updateData.content };
|
|
564
|
+
// When showFullAiResponse is true, do not cap height
|
|
565
|
+
const defaultMaxHeight = module.aiNodeDefaultHeight || 200;
|
|
566
|
+
const effectiveMaxHeight = module.showFullAiResponse ? 10000 : defaultMaxHeight;
|
|
567
|
+
const { height: requiredHeight, maxScrollPx } = await window.MindMapTextureFactory.measureHtmlHeightAsync(tempModel, zoomLevel, width * zoomLevel, null, effectiveMaxHeight);
|
|
568
|
+
|
|
569
|
+
// Keep AI/loading note nodes at least at the configured default height.
|
|
570
|
+
// This prevents initial one-line collapse while streaming starts.
|
|
571
|
+
const defaultPrompts = ['Note', 'Pasted Text', '에이전트 메모'];
|
|
572
|
+
const promptText = model.prompt ?? model.Prompt ?? '';
|
|
573
|
+
const isAiPrompt = !!promptText && !defaultPrompts.includes(promptText);
|
|
574
|
+
const minPreferredHeight = (model.isLoading || isAiPrompt)
|
|
575
|
+
? (module.aiNodeDefaultHeight || 200)
|
|
576
|
+
: 60;
|
|
577
|
+
const normalizedRequiredHeight = Math.max(requiredHeight, minPreferredHeight);
|
|
578
|
+
|
|
579
|
+
const currentHeight = (model.height || 0);
|
|
580
|
+
// Update only if delta is >= 5px (was 2px -> 5px)
|
|
581
|
+
if (Math.abs(currentHeight - normalizedRequiredHeight) > 5) {
|
|
582
|
+
model.height = normalizedRequiredHeight;
|
|
583
|
+
model.maxScroll = maxScrollPx / zoomLevel;
|
|
584
|
+
model.scrollOffset = Math.min(model.scrollOffset || 0, model.maxScroll);
|
|
585
|
+
updateData.height = normalizedRequiredHeight;
|
|
586
|
+
try {
|
|
587
|
+
module.dotNetHelper?.invokeMethodAsync('UpdateNodeDimensions', nodeId, Math.round(width), Math.round(normalizedRequiredHeight));
|
|
588
|
+
} catch { }
|
|
589
|
+
} else {
|
|
590
|
+
// If the change is tiny, keep the existing height (avoid infinite loop)
|
|
591
|
+
updateData.height = currentHeight;
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
// ▲▲▲ [Changed] ▲▲▲
|
|
596
|
+
// ▲▲▲ [Key change] ▲▲▲
|
|
597
|
+
|
|
598
|
+
const previousContent = String(model.response ?? model.Response ?? '');
|
|
599
|
+
const hasContentUpdate = updateData.content !== null && updateData.content !== undefined;
|
|
600
|
+
let nextContent = hasContentUpdate ? String(updateData.content ?? '') : previousContent;
|
|
601
|
+
|
|
602
|
+
if (model.contentType === 'image' && hasContentUpdate) {
|
|
603
|
+
nextContent = window.MindMapNodes?.applyCanonicalNodeResponse?.(model, nextContent) ?? nextContent;
|
|
604
|
+
updateData.content = nextContent;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
if (model.contentType === 'image' && nextContent) {
|
|
608
|
+
// ▼▼▼ [Changed] module.imageCache -> window.MindMapNodes.imageCache ▼▼▼
|
|
609
|
+
const assetUrls = window.MindMapNodes?.getNodeImageAssetUrls?.(model) || {};
|
|
610
|
+
const previewUrl = String(assetUrls.previewUrl || nextContent).trim();
|
|
611
|
+
const originalUrl = String(assetUrls.fullResUrl || assetUrls.originalUrl || '').trim();
|
|
612
|
+
await window.MindMapTextureFactory.ensureImageCached(nodeId, previewUrl, window.MindMapNodes.imageCache, {
|
|
613
|
+
authToken: module.authToken,
|
|
614
|
+
allowOriginalFallback: false,
|
|
615
|
+
allowOriginalReuseForPreview: false,
|
|
616
|
+
originalUrl,
|
|
617
|
+
onRefreshedUrl: (refreshedUrl) => {
|
|
618
|
+
const normalized = String(refreshedUrl || '').trim();
|
|
619
|
+
if (!normalized) {
|
|
620
|
+
return;
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
window.MindMapNodes?.setNodeImageOriginalUrl?.(model, normalized);
|
|
624
|
+
const refreshedAssetUrls = window.MindMapNodes?.getNodeImageAssetUrls?.(model) || {};
|
|
625
|
+
if (!refreshedAssetUrls.thumbnailUrl && !String(model.response ?? model.Response ?? '').trim()) {
|
|
626
|
+
updateData.content = normalized;
|
|
627
|
+
model.response = normalized;
|
|
628
|
+
model.Response = normalized;
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
});
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
window.MindMapNodes.pendingNodeUpdates.delete(nodeId);
|
|
635
|
+
const contentChanged = previousContent !== nextContent;
|
|
636
|
+
const hasExplicitSizeUpdate =
|
|
637
|
+
(updateData.width !== null && updateData.width !== undefined) ||
|
|
638
|
+
(updateData.height !== null && updateData.height !== undefined);
|
|
639
|
+
if (hasContentUpdate) {
|
|
640
|
+
model.response = nextContent;
|
|
641
|
+
model.Response = nextContent;
|
|
642
|
+
}
|
|
643
|
+
if (updateData.width !== null && updateData.width !== undefined) model.width = updateData.width;
|
|
644
|
+
if (updateData.height !== null && updateData.height !== undefined) model.height = updateData.height;
|
|
645
|
+
|
|
646
|
+
if (nodeEntry.cssObject?.userData) {
|
|
647
|
+
nodeEntry.cssObject.userData.worldWidth = model.width || 400;
|
|
648
|
+
nodeEntry.cssObject.userData.worldHeight = model.height || 200;
|
|
649
|
+
}
|
|
650
|
+
if (nodeEntry.glObject?.userData) {
|
|
651
|
+
nodeEntry.glObject.userData.worldWidth = model.width || 400;
|
|
652
|
+
nodeEntry.glObject.userData.worldHeight = model.height || 200;
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
if (model.contentType === 'image' && (contentChanged || hasExplicitSizeUpdate)) {
|
|
656
|
+
nodeEntry.isCssDirty = true;
|
|
657
|
+
nodeEntry._imageLodVisualState = null;
|
|
658
|
+
const bodyMeshForImage = nodeEntry.bodyMesh || nodeEntry.glObject?.getObjectByName?.('body') || null;
|
|
659
|
+
if (bodyMeshForImage?.userData && contentChanged) {
|
|
660
|
+
bodyMeshForImage.userData.imageRenderKey = null;
|
|
661
|
+
bodyMeshForImage.userData.pendingImageUrl = '';
|
|
662
|
+
}
|
|
663
|
+
if (nodeEntry.cssObject && window.MindMapCss3DManager?.syncCss3dContent) {
|
|
664
|
+
window.MindMapCss3DManager.syncCss3dContent(model, nodeEntry.cssObject);
|
|
665
|
+
nodeEntry.isCssDirty = false;
|
|
666
|
+
}
|
|
667
|
+
module.lodRenderer?.requestResidentPatch?.('update-node', nodeId);
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
// ★ Invalidate LOD cache when content changes
|
|
671
|
+
window.MindMapTextLOD?.invalidateNodeCache?.(nodeId);
|
|
672
|
+
|
|
673
|
+
const updateDataWithScroll = {
|
|
674
|
+
...updateData,
|
|
675
|
+
scrollDelta: updateData.scrollDelta || 0
|
|
676
|
+
};
|
|
677
|
+
|
|
678
|
+
// ★ Troika 모드 체크: text/note/code 노드면 배경 전용 텍스처 사용
|
|
679
|
+
// ★ Code nodes always use Troika for text rendering
|
|
680
|
+
const isTroikaMode = (model.contentType === 'code') || // Code nodes always use Troika
|
|
681
|
+
(window.MindMapNodes?.getTextRenderMode?.() === 'troika'
|
|
682
|
+
&& (model.contentType === 'text' || model.contentType === 'note')
|
|
683
|
+
&& !model.isChunkedText);
|
|
684
|
+
|
|
685
|
+
let textures, w, h;
|
|
686
|
+
const existingTextures = window.MindMapNodes.textureCache.get(nodeId);
|
|
687
|
+
|
|
688
|
+
if (isTroikaMode) {
|
|
689
|
+
// ★ Troika 최적화: 이미 캐시된 텍스처가 있고, 크기와 내용이 동일하면 재사용
|
|
690
|
+
const canReuseTexture = existingTextures?.default?.body
|
|
691
|
+
&& existingTextures._cachedWidth === model.width
|
|
692
|
+
&& existingTextures._cachedHeight === model.height
|
|
693
|
+
&& existingTextures._cachedContent === model.response; // ★ Content change check
|
|
694
|
+
|
|
695
|
+
if (canReuseTexture) {
|
|
696
|
+
// 캐시된 텍스처 재사용 (텍스처 재생성 스킵)
|
|
697
|
+
textures = existingTextures;
|
|
698
|
+
w = model.width;
|
|
699
|
+
h = model.height;
|
|
700
|
+
} else {
|
|
701
|
+
// 배경 텍스처 생성
|
|
702
|
+
const result = await window.MindMapTextureFactory.generateTroikaBackgroundTextures(module, model, model.width, model.height);
|
|
703
|
+
textures = result.textures;
|
|
704
|
+
// 캐시 메타데이터 저장
|
|
705
|
+
textures._cachedWidth = model.width;
|
|
706
|
+
textures._cachedHeight = model.height;
|
|
707
|
+
textures._cachedContent = model.response; // ★ Track content for change detection
|
|
708
|
+
w = result.width;
|
|
709
|
+
h = result.height;
|
|
710
|
+
}
|
|
711
|
+
} else {
|
|
712
|
+
// Canvas 모드: 기존 방식 (텍스트 포함)
|
|
713
|
+
const isResizeUpdate = updateData.width !== undefined || updateData.height !== undefined;
|
|
714
|
+
const isBatchExpand = updateData.skipSelected === true; // ★ Batch expand operation
|
|
715
|
+
|
|
716
|
+
// 고정 품질 렌더링 (LOD 품질 스케일 제거)
|
|
717
|
+
const qualityScale = 1.0;
|
|
718
|
+
console.log(`[processRenderQueue] Canvas mode for ${nodeId}, qualityScale=${qualityScale}, isResize=${isResizeUpdate}, isBatch=${isBatchExpand}`);
|
|
719
|
+
const result = await window.MindMapTextureFactory.generateAndCacheTextures(module, model, window.MindMapNodes.imageCache, updateDataWithScroll, qualityScale);
|
|
720
|
+
textures = result.textures;
|
|
721
|
+
w = result.width;
|
|
722
|
+
h = result.height;
|
|
723
|
+
|
|
724
|
+
// 캐시 메타데이터 (콘텐츠 변경 감지용)
|
|
725
|
+
if (textures.default?.body) {
|
|
726
|
+
textures.default.body._cachedContent = model.response ?? '';
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
// ★ 기존 텍스처 dispose (Troika에서 재사용한 경우 제외)
|
|
731
|
+
if (existingTextures && existingTextures !== textures) {
|
|
732
|
+
enqueueDispose(existingTextures.default?.body);
|
|
733
|
+
enqueueDispose(existingTextures.selected?.body);
|
|
734
|
+
enqueueDispose(existingTextures.glow?.texture);
|
|
735
|
+
}
|
|
736
|
+
window.MindMapNodes.textureCache.set(nodeId, textures);
|
|
737
|
+
|
|
738
|
+
const isSelected = module.multiSelectedNodeIds.has(nodeId);
|
|
739
|
+
// ★ [Fix] If selected texture is null (skipSelected mode), use default texture
|
|
740
|
+
const active = (isSelected && textures.selected?.body) ? textures.selected : textures.default;
|
|
741
|
+
const group = nodeEntry.glObject;
|
|
742
|
+
|
|
743
|
+
const CORNER_RADIUS = 20;
|
|
744
|
+
const OUTLINE_SHRINK = 2;
|
|
745
|
+
const OUTLINE_RADIUS = Math.max(1, CORNER_RADIUS - 1);
|
|
746
|
+
const CHAMFER_SIZE = 16;
|
|
747
|
+
|
|
748
|
+
if (group) {
|
|
749
|
+
const bodyMesh = group.getObjectByName('body');
|
|
750
|
+
const tailMesh = group.getObjectByName('tail');
|
|
751
|
+
let glowMesh = group.getObjectByName('glow');
|
|
752
|
+
const outline = group.getObjectByName('outline');
|
|
753
|
+
if (!ENABLE_WEBGL_GLOW) {
|
|
754
|
+
glowMesh = removeGlowMesh(group);
|
|
755
|
+
}
|
|
756
|
+
const baseRenderOrder = group.userData?.baseRenderOrder ?? bodyMesh?.renderOrder ?? module.nodeZCounter++;
|
|
757
|
+
group.userData = { ...(group.userData || {}), baseRenderOrder };
|
|
758
|
+
|
|
759
|
+
if (bodyMesh) {
|
|
760
|
+
const gp = bodyMesh.geometry && bodyMesh.geometry.parameters;
|
|
761
|
+
if (!gp || gp.width !== w || gp.height !== h) {
|
|
762
|
+
bodyMesh.geometry.dispose();
|
|
763
|
+
bodyMesh.geometry = new THREE.PlaneGeometry(w, h);
|
|
764
|
+
|
|
765
|
+
if (outline) {
|
|
766
|
+
outline.geometry.dispose();
|
|
767
|
+
outline.geometry = createRoundedRectGeometry(
|
|
768
|
+
w - OUTLINE_SHRINK,
|
|
769
|
+
h - OUTLINE_SHRINK,
|
|
770
|
+
OUTLINE_RADIUS
|
|
771
|
+
);
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
if (active?.body) {
|
|
775
|
+
bodyMesh.material.map = active.body;
|
|
776
|
+
}
|
|
777
|
+
bodyMesh.material.color.setHex(0xffffff);
|
|
778
|
+
bodyMesh.material.needsUpdate = true;
|
|
779
|
+
bodyMesh.position.set(w / 2, -h / 2, 0);
|
|
780
|
+
if (model.contentType === 'image') {
|
|
781
|
+
if (contentChanged) {
|
|
782
|
+
bodyMesh.userData ??= {};
|
|
783
|
+
bodyMesh.userData.imageRenderKey = null;
|
|
784
|
+
bodyMesh.userData.pendingImageUrl = '';
|
|
785
|
+
}
|
|
786
|
+
applyImageCoverUv(bodyMesh, w, h);
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
if (model.contentType === 'image') {
|
|
790
|
+
syncImageSelectionGlow(group, w, h, baseRenderOrder, isSelected);
|
|
791
|
+
} else if (outline) {
|
|
792
|
+
outline.position.copy(bodyMesh.position);
|
|
793
|
+
outline.position.z = 0; // [Fix] Z=0
|
|
794
|
+
outline.visible = false; // Disabled - using SDF glow instead
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
if (tailMesh) {
|
|
798
|
+
if (active?.tail) {
|
|
799
|
+
tailMesh.material.map = active.tail;
|
|
800
|
+
tailMesh.material.needsUpdate = true;
|
|
801
|
+
}
|
|
802
|
+
tailMesh.position.set(w / 2, -h - 6, 0);
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
// ▼▼▼ [SDF Glow] Use shader-based glow (no textures!) ▼▼▼
|
|
806
|
+
if (ENABLE_WEBGL_GLOW && window.MindMapGlowShader) {
|
|
807
|
+
const contentType = model.contentType || 'text';
|
|
808
|
+
if (glowMesh) {
|
|
809
|
+
window.MindMapGlowShader.updateGlowSize(glowMesh, w, h, contentType);
|
|
810
|
+
window.MindMapGlowShader.updateGlowSelection(glowMesh, isSelected);
|
|
811
|
+
glowMesh.position.set(w / 2, -h / 2, 0); // [Fix] Z=0
|
|
812
|
+
glowMesh.renderOrder = baseRenderOrder - 5;
|
|
813
|
+
} else {
|
|
814
|
+
const newGlow = window.MindMapGlowShader.createSDFGlowMesh(w, h, isSelected, contentType);
|
|
815
|
+
newGlow.position.set(w / 2, -h / 2, 0); // [Fix] Z=0
|
|
816
|
+
newGlow.renderOrder = baseRenderOrder - 5;
|
|
817
|
+
// ★ 카메라 거리에 따라 초기 가시성 설정
|
|
818
|
+
const cameraZ = module.camera?.position?.z ?? Infinity;
|
|
819
|
+
const threshold = 4000;
|
|
820
|
+
newGlow.visible = cameraZ < threshold;
|
|
821
|
+
group.add(newGlow);
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
// ▲▲▲ [SDF Glow] ▲▲▲
|
|
825
|
+
|
|
826
|
+
// ▼▼▼ [Changed] Update resize handles (4 corners + optional 4 edges) ▼▼▼
|
|
827
|
+
if (model.contentType !== 'pdf') {
|
|
828
|
+
// [Fix] 이미지 노드 리사이즈 핸들 크기 통일 (Note와 동일한 16px)
|
|
829
|
+
const actualHandleSize = HANDLE_SIZE;
|
|
830
|
+
|
|
831
|
+
const edgeWidthLength = Math.max(10, w - actualHandleSize * 2);
|
|
832
|
+
const edgeHeightLength = Math.max(10, h - actualHandleSize * 2);
|
|
833
|
+
|
|
834
|
+
const handleConfigs = {
|
|
835
|
+
// ▼▼▼ [Fix] Corners - Z값을 0으로 통일하여 시차(Parallax) 완전 제거 ▼▼▼
|
|
836
|
+
'resizeHandle_TL': { x: actualHandleSize / 2, y: -actualHandleSize / 2, w: actualHandleSize, h: actualHandleSize, z: 0 },
|
|
837
|
+
'resizeHandle_TR': { x: w - actualHandleSize / 2, y: -actualHandleSize / 2, w: actualHandleSize, h: actualHandleSize, z: 0 },
|
|
838
|
+
'resizeHandle_BL': { x: actualHandleSize / 2, y: -h + actualHandleSize / 2, w: actualHandleSize, h: actualHandleSize, z: 0 },
|
|
839
|
+
'resizeHandle_BR': { x: w - actualHandleSize / 2, y: -h + actualHandleSize / 2, w: actualHandleSize, h: actualHandleSize, z: 0 }
|
|
840
|
+
// ▲▲▲ [Fix] ▲▲▲
|
|
841
|
+
};
|
|
842
|
+
|
|
843
|
+
if (model.contentType !== 'image') {
|
|
844
|
+
Object.assign(handleConfigs, {
|
|
845
|
+
// ▼▼▼ [Fix] Edges - Z값을 0으로 통일 ▼▼▼
|
|
846
|
+
'resizeHandle_T': { x: w / 2, y: -actualHandleSize / 2, w: edgeWidthLength, h: actualHandleSize, z: 0 },
|
|
847
|
+
'resizeHandle_B': { x: w / 2, y: -h + actualHandleSize / 2, w: edgeWidthLength, h: actualHandleSize, z: 0 },
|
|
848
|
+
'resizeHandle_L': { x: actualHandleSize / 2, y: -h / 2, w: actualHandleSize, h: edgeHeightLength, z: 0 },
|
|
849
|
+
'resizeHandle_R': { x: w - actualHandleSize / 2, y: -h / 2, w: actualHandleSize, h: edgeHeightLength, z: 0 }
|
|
850
|
+
// ▲▲▲ [Fix] ▲▲▲
|
|
851
|
+
});
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
// Iterate all possible handles to update position OR hide if not needed
|
|
855
|
+
const allHandleNames = [
|
|
856
|
+
'resizeHandle_TL', 'resizeHandle_TR', 'resizeHandle_BL', 'resizeHandle_BR',
|
|
857
|
+
'resizeHandle_T', 'resizeHandle_B', 'resizeHandle_L', 'resizeHandle_R'
|
|
858
|
+
];
|
|
859
|
+
|
|
860
|
+
allHandleNames.forEach(name => {
|
|
861
|
+
const handle = group.getObjectByName(name);
|
|
862
|
+
const cfg = handleConfigs[name];
|
|
863
|
+
|
|
864
|
+
if (handle) {
|
|
865
|
+
if (cfg) {
|
|
866
|
+
// Active handle: update position/size and ensure visible
|
|
867
|
+
handle.visible = true; // Ensure visible (might have been hidden)
|
|
868
|
+
handle.position.set(cfg.x, cfg.y, cfg.z);
|
|
869
|
+
const gp = handle.geometry?.parameters;
|
|
870
|
+
if (gp && (gp.width !== cfg.w || gp.height !== cfg.h)) {
|
|
871
|
+
handle.geometry.dispose();
|
|
872
|
+
handle.geometry = new THREE.PlaneGeometry(cfg.w, cfg.h);
|
|
873
|
+
}
|
|
874
|
+
if (model.contentType === 'image' && handle.material) {
|
|
875
|
+
handle.material.opacity = 0.0;
|
|
876
|
+
handle.material.colorWrite = false;
|
|
877
|
+
handle.material.needsUpdate = true;
|
|
878
|
+
}
|
|
879
|
+
} else {
|
|
880
|
+
// Inactive handle (e.g. edge on image node): hide it
|
|
881
|
+
handle.visible = false;
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
});
|
|
885
|
+
}
|
|
886
|
+
// ▲▲▲ [Changed] ▲▲▲
|
|
887
|
+
|
|
888
|
+
group.userData.worldWidth = w;
|
|
889
|
+
group.userData.worldHeight = h;
|
|
890
|
+
|
|
891
|
+
const isCssMediaNode =
|
|
892
|
+
(model.contentType === 'image' || model.contentType === 'video' || model.contentType === 'embed')
|
|
893
|
+
&& !!nodeEntry.cssObject;
|
|
894
|
+
const mediaFrameResynced =
|
|
895
|
+
isCssMediaNode &&
|
|
896
|
+
window.MindMapCss3DManager?.syncCss3dMediaNodeFrame?.(model, nodeEntry.cssObject) === true;
|
|
897
|
+
|
|
898
|
+
if (group.visible === false && nodeEntry.currentType === 'GL') {
|
|
899
|
+
group.visible = true;
|
|
900
|
+
if (nodeEntry.cssObject) {
|
|
901
|
+
nodeEntry.cssObject.visible = false;
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
if (!mediaFrameResynced &&
|
|
906
|
+
window.MindMapNodes &&
|
|
907
|
+
typeof window.MindMapNodes.updateNodeSpatialGrid === 'function') {
|
|
908
|
+
window.MindMapNodes.updateNodeSpatialGrid(module, nodeId);
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
} catch (err) {
|
|
912
|
+
console.error(`[Pipeline] Render Node ${nodeId} Failed:`, err);
|
|
913
|
+
} finally {
|
|
914
|
+
window.MindMapNodes.nodeRenderLocks.set(nodeId, false);
|
|
915
|
+
// console.timeEnd(`[Pipeline] 6. Render Node ${nodeId}`);
|
|
916
|
+
|
|
917
|
+
// If another update arrived while processing, re-run (with a small delay)
|
|
918
|
+
if (window.MindMapNodes.pendingNodeUpdates.has(nodeId)) {
|
|
919
|
+
setTimeout(() => enqueueRender(module, nodeId), 50);
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
// --- Resizing handlers (keep existing behavior) ---
|
|
925
|
+
|
|
926
|
+
// ▼▼▼ [Changed] Add corner parameter for four-corner resizing ▼▼▼
|
|
927
|
+
function startResizing(module, nodeId, startX, startY, corner = 'BR') {
|
|
928
|
+
const nodeEntry = module.nodeObjectsById.get(nodeId);
|
|
929
|
+
if (!nodeEntry) return false;
|
|
930
|
+
if (nodeEntry.model.contentType === 'pdf') return false;
|
|
931
|
+
|
|
932
|
+
module.isResizing = true;
|
|
933
|
+
module.resizingNodeId = nodeId;
|
|
934
|
+
module.resizeCorner = corner; // TL, TR, BL, BR
|
|
935
|
+
const cssRendererEnabled = module?.renderDebugFlags?.enableCss3d !== false;
|
|
936
|
+
const shouldResizeCssObject = !!nodeEntry.cssObject &&
|
|
937
|
+
cssRendererEnabled &&
|
|
938
|
+
(nodeEntry.currentType === 'CSS' || nodeEntry.cssObject.visible === true);
|
|
939
|
+
const activeResizeType = shouldResizeCssObject ? 'CSS' : 'GL';
|
|
940
|
+
module.resizeNodeObject = activeResizeType === 'CSS' ? nodeEntry.cssObject : nodeEntry.glObject;
|
|
941
|
+
if (!module.resizeNodeObject && nodeEntry.cssObject) {
|
|
942
|
+
module.resizeNodeObject = nodeEntry.cssObject;
|
|
943
|
+
nodeEntry.currentResizeType = 'CSS';
|
|
944
|
+
} else {
|
|
945
|
+
nodeEntry.currentResizeType = activeResizeType;
|
|
946
|
+
}
|
|
947
|
+
window.MindMapNodes?.bringNodeToFront?.(module, nodeId);
|
|
948
|
+
window.MindMapInteractions?.setBrowserSelectionLock?.(true, 'resize');
|
|
949
|
+
document.body.classList.add('is-resizing');
|
|
950
|
+
module.resizeStartMousePos = { x: startX, y: startY };
|
|
951
|
+
module.resizeStartDimensions = {
|
|
952
|
+
w: nodeEntry.model.width || 400,
|
|
953
|
+
h: nodeEntry.model.height || 200
|
|
954
|
+
};
|
|
955
|
+
// Store initial position from the active render object (CSS/GL) to avoid
|
|
956
|
+
// image-node jumps when CSS and GL are slightly out of sync.
|
|
957
|
+
const activePos = module.resizeNodeObject?.position
|
|
958
|
+
|| nodeEntry.cssObject?.position
|
|
959
|
+
|| nodeEntry.glObject?.position
|
|
960
|
+
|| { x: 0, y: 0 };
|
|
961
|
+
module.resizeStartPosition = {
|
|
962
|
+
x: activePos.x,
|
|
963
|
+
y: activePos.y
|
|
964
|
+
};
|
|
965
|
+
|
|
966
|
+
// Force immediate CSS/GL transform alignment at resize start.
|
|
967
|
+
if (nodeEntry.glObject) {
|
|
968
|
+
nodeEntry.glObject.position.x = activePos.x;
|
|
969
|
+
nodeEntry.glObject.position.y = activePos.y;
|
|
970
|
+
}
|
|
971
|
+
if (nodeEntry.cssObject) {
|
|
972
|
+
nodeEntry.cssObject.position.x = activePos.x;
|
|
973
|
+
nodeEntry.cssObject.position.y = activePos.y;
|
|
974
|
+
nodeEntry.cssObject.updateMatrixWorld(true);
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
// ▼▼▼ [Fix] 리사이즈 중 CSS transition 비활성화하여 즉시 크기 변경 ▼▼▼
|
|
978
|
+
if (nodeEntry.cssObject && nodeEntry.cssObject.element) {
|
|
979
|
+
const wrapper = nodeEntry.cssObject.element;
|
|
980
|
+
wrapper.style.transition = 'none';
|
|
981
|
+
const inner = wrapper.firstElementChild;
|
|
982
|
+
if (inner) {
|
|
983
|
+
inner.style.transition = 'none';
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
// ▲▲▲ [Fix] ▲▲▲
|
|
987
|
+
|
|
988
|
+
// ▼▼▼ [UndoRedo] Capture before-state for undo ▼▼▼
|
|
989
|
+
if (module.dotNetHelper) {
|
|
990
|
+
try { module.dotNetHelper.invokeMethodAsync('StartResizeCapture', nodeId); } catch { }
|
|
991
|
+
}
|
|
992
|
+
// ▲▲▲ [UndoRedo] ▲▲▲
|
|
993
|
+
|
|
994
|
+
return true;
|
|
995
|
+
}
|
|
996
|
+
// ▲▲▲ [Changed] ▲▲▲
|
|
997
|
+
|
|
998
|
+
function doResizing(module, currentX, currentY) {
|
|
999
|
+
if (!module.isResizing || !module.resizeNodeObject) return;
|
|
1000
|
+
// ▼▼▼ [Changed] Cap max node size: X=2000, Y=4000 ▼▼▼
|
|
1001
|
+
const MAX_NODE_WIDTH = 2000;
|
|
1002
|
+
const MAX_NODE_HEIGHT = 4000;
|
|
1003
|
+
const MIN_WIDTH = 200;
|
|
1004
|
+
const MIN_HEIGHT = 100;
|
|
1005
|
+
// ▲▲▲ [Changed] ▲▲▲
|
|
1006
|
+
|
|
1007
|
+
const vfov = (module.camera.fov * Math.PI) / 180;
|
|
1008
|
+
const viewHeight = 2 * Math.tan(vfov / 2) * module.camera.position.z;
|
|
1009
|
+
const viewWidth = viewHeight * module.camera.aspect;
|
|
1010
|
+
const screenToWorldFactorX = viewWidth / module.container.clientWidth;
|
|
1011
|
+
const screenToWorldFactorY = viewHeight / module.container.clientHeight;
|
|
1012
|
+
const worldDeltaX = (currentX - module.resizeStartMousePos.x) * screenToWorldFactorX;
|
|
1013
|
+
// ▼▼▼ [Fix] Negate Y delta: screen Y increases downward, world Y increases upward ▼▼▼
|
|
1014
|
+
const worldDeltaY = -(currentY - module.resizeStartMousePos.y) * screenToWorldFactorY;
|
|
1015
|
+
|
|
1016
|
+
const HANDLE_SIZE = RESIZE_HANDLE_SIZE;
|
|
1017
|
+
// ▼▼▼ [Changed] Use stored nodeId (CSS3D mode compatible) ▼▼▼
|
|
1018
|
+
const nodeId = module.resizingNodeId || (module.resizeNodeObject.userData?.nodeId || module.resizeNodeObject.element?.dataset?.nodeId);
|
|
1019
|
+
const nodeEntry = module.nodeObjectsById.get(nodeId);
|
|
1020
|
+
if (!nodeEntry) {
|
|
1021
|
+
console.warn('[Pipeline] doResizing: nodeEntry not found for', nodeId);
|
|
1022
|
+
return;
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
const corner = module.resizeCorner || 'BR';
|
|
1026
|
+
const startW = module.resizeStartDimensions.w;
|
|
1027
|
+
const startH = module.resizeStartDimensions.h;
|
|
1028
|
+
const startX = module.resizeStartPosition.x;
|
|
1029
|
+
const startY = module.resizeStartPosition.y;
|
|
1030
|
+
|
|
1031
|
+
let finalWidth, finalHeight, newPosX, newPosY;
|
|
1032
|
+
|
|
1033
|
+
// ▼▼▼ [Changed] Eight-direction resize logic (4 corners + 4 edges) ▼▼▼
|
|
1034
|
+
// Node position is top-left corner in Three.js (Y increases upward)
|
|
1035
|
+
// Depending on handle, we compute size and position differently
|
|
1036
|
+
switch (corner) {
|
|
1037
|
+
// === Corners (diagonal resize) ===
|
|
1038
|
+
case 'TL': // Top-Left: anchor bottom-right, move top-left
|
|
1039
|
+
finalWidth = startW - worldDeltaX;
|
|
1040
|
+
finalHeight = startH + worldDeltaY;
|
|
1041
|
+
newPosX = startX + worldDeltaX;
|
|
1042
|
+
newPosY = startY + worldDeltaY;
|
|
1043
|
+
break;
|
|
1044
|
+
case 'TR': // Top-Right: anchor bottom-left, expand right/up
|
|
1045
|
+
finalWidth = startW + worldDeltaX;
|
|
1046
|
+
finalHeight = startH + worldDeltaY;
|
|
1047
|
+
newPosX = startX;
|
|
1048
|
+
newPosY = startY + worldDeltaY;
|
|
1049
|
+
break;
|
|
1050
|
+
case 'BL': // Bottom-Left: anchor top-right, expand left/down
|
|
1051
|
+
finalWidth = startW - worldDeltaX;
|
|
1052
|
+
finalHeight = startH - worldDeltaY;
|
|
1053
|
+
newPosX = startX + worldDeltaX;
|
|
1054
|
+
newPosY = startY;
|
|
1055
|
+
break;
|
|
1056
|
+
case 'BR': // Bottom-Right: anchor top-left (original behavior)
|
|
1057
|
+
finalWidth = startW + worldDeltaX;
|
|
1058
|
+
finalHeight = startH - worldDeltaY;
|
|
1059
|
+
newPosX = startX;
|
|
1060
|
+
newPosY = startY;
|
|
1061
|
+
break;
|
|
1062
|
+
// === Edges (single-direction resize) ===
|
|
1063
|
+
case 'T': // Top edge: vertical only, anchor bottom
|
|
1064
|
+
finalWidth = startW;
|
|
1065
|
+
finalHeight = startH + worldDeltaY;
|
|
1066
|
+
newPosX = startX;
|
|
1067
|
+
newPosY = startY + worldDeltaY;
|
|
1068
|
+
break;
|
|
1069
|
+
case 'B': // Bottom edge: vertical only, anchor top
|
|
1070
|
+
finalWidth = startW;
|
|
1071
|
+
finalHeight = startH - worldDeltaY;
|
|
1072
|
+
newPosX = startX;
|
|
1073
|
+
newPosY = startY;
|
|
1074
|
+
break;
|
|
1075
|
+
case 'L': // Left edge: horizontal only, anchor right
|
|
1076
|
+
finalWidth = startW - worldDeltaX;
|
|
1077
|
+
finalHeight = startH;
|
|
1078
|
+
newPosX = startX + worldDeltaX;
|
|
1079
|
+
newPosY = startY;
|
|
1080
|
+
break;
|
|
1081
|
+
case 'R': // Right edge: horizontal only, anchor left
|
|
1082
|
+
finalWidth = startW + worldDeltaX;
|
|
1083
|
+
finalHeight = startH;
|
|
1084
|
+
newPosX = startX;
|
|
1085
|
+
newPosY = startY;
|
|
1086
|
+
break;
|
|
1087
|
+
default:
|
|
1088
|
+
finalWidth = startW + worldDeltaX;
|
|
1089
|
+
finalHeight = startH - worldDeltaY;
|
|
1090
|
+
newPosX = startX;
|
|
1091
|
+
newPosY = startY;
|
|
1092
|
+
break;
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
// Clamp size
|
|
1096
|
+
let clampedWidth = Math.min(MAX_NODE_WIDTH, Math.max(MIN_WIDTH, finalWidth));
|
|
1097
|
+
let clampedHeight = Math.min(MAX_NODE_HEIGHT, Math.max(MIN_HEIGHT, finalHeight));
|
|
1098
|
+
|
|
1099
|
+
// ▼▼▼ [Image Resize Fix] Remove specific Ratio Preservation for now (Treat as normal nodes) ▼▼▼
|
|
1100
|
+
// Normal nodes Logic (apply to all)
|
|
1101
|
+
// Adjust position if size was clamped (keep anchor point fixed)
|
|
1102
|
+
if (corner === 'TL' || corner === 'BL' || corner === 'L') {
|
|
1103
|
+
// Left handles: position adjusts with width changes
|
|
1104
|
+
const widthDiff = finalWidth - clampedWidth;
|
|
1105
|
+
newPosX += widthDiff;
|
|
1106
|
+
}
|
|
1107
|
+
if (corner === 'TL' || corner === 'TR' || corner === 'T') {
|
|
1108
|
+
// Top handles: position adjusts with height changes
|
|
1109
|
+
const heightDiff = finalHeight - clampedHeight;
|
|
1110
|
+
newPosY -= heightDiff;
|
|
1111
|
+
}
|
|
1112
|
+
// ▲▲▲ [Image Resize Fix] ▲▲▲
|
|
1113
|
+
// ▲▲▲ [Image Resize Fix] ▲▲▲
|
|
1114
|
+
|
|
1115
|
+
finalWidth = clampedWidth;
|
|
1116
|
+
finalHeight = clampedHeight;
|
|
1117
|
+
// ▲▲▲ [Changed] ▲▲▲
|
|
1118
|
+
|
|
1119
|
+
// ▼▼▼ [Refactor] 스냅 현상 제거: 실수형 크기/좌표 그대로 사용 ▼▼▼
|
|
1120
|
+
nodeEntry.model.width = finalWidth;
|
|
1121
|
+
nodeEntry.model.height = finalHeight;
|
|
1122
|
+
|
|
1123
|
+
// ▼▼▼ [Changed] Update node position for corner resizing ▼▼▼
|
|
1124
|
+
if (nodeEntry.glObject) {
|
|
1125
|
+
nodeEntry.glObject.position.x = newPosX;
|
|
1126
|
+
nodeEntry.glObject.position.y = newPosY;
|
|
1127
|
+
}
|
|
1128
|
+
if (nodeEntry.cssObject) {
|
|
1129
|
+
nodeEntry.cssObject.position.x = newPosX;
|
|
1130
|
+
nodeEntry.cssObject.position.y = newPosY;
|
|
1131
|
+
// ▼▼▼ [Fix] CSS3D matrixWorld 즉시 갱신하여 CSS3DRenderer가 새 위치를 반영하도록 보장 ▼▼▼
|
|
1132
|
+
nodeEntry.cssObject.updateMatrixWorld(true);
|
|
1133
|
+
// ▲▲▲ [Fix] ▲▲▲
|
|
1134
|
+
}
|
|
1135
|
+
// ▼▼▼ [Fix] model position도 동기화하여 다른 sync 로직과의 일관성 보장 ▼▼▼
|
|
1136
|
+
nodeEntry.model.positionX = newPosX;
|
|
1137
|
+
nodeEntry.model.positionY = newPosY;
|
|
1138
|
+
// ▲▲▲ [Fix] ▲▲▲
|
|
1139
|
+
// ▲▲▲ [Changed] ▲▲▲
|
|
1140
|
+
|
|
1141
|
+
if (nodeEntry.cssObject) {
|
|
1142
|
+
const wrapper = nodeEntry.cssObject.element;
|
|
1143
|
+
// ▼▼▼ [Fix] CSS3D 노드는 resolutionScale을 고려하여 DOM 크기 설정 ▼▼▼
|
|
1144
|
+
// CSS3D 객체의 scale은 1/resolutionScale이므로, DOM 크기는 worldSize * resolutionScale
|
|
1145
|
+
const resolutionScale = nodeEntry.cssObject.userData?.resolutionScale || 1;
|
|
1146
|
+
const domWidth = nodeEntry.model.width * resolutionScale;
|
|
1147
|
+
const domHeight = nodeEntry.model.height * resolutionScale;
|
|
1148
|
+
|
|
1149
|
+
wrapper.style.width = domWidth + 'px';
|
|
1150
|
+
wrapper.style.height = domHeight + 'px';
|
|
1151
|
+
wrapper.style.minWidth = domWidth + 'px';
|
|
1152
|
+
wrapper.style.minHeight = domHeight + 'px';
|
|
1153
|
+
wrapper.style.maxWidth = domWidth + 'px';
|
|
1154
|
+
wrapper.style.maxHeight = domHeight + 'px';
|
|
1155
|
+
|
|
1156
|
+
const innerContent = wrapper.firstElementChild;
|
|
1157
|
+
if (innerContent) {
|
|
1158
|
+
// innerContent는 transform: scale(resolutionScale)이 적용되므로 원본 크기 사용
|
|
1159
|
+
innerContent.style.width = nodeEntry.model.width + 'px';
|
|
1160
|
+
innerContent.style.height = nodeEntry.model.height + 'px';
|
|
1161
|
+
innerContent.style.minWidth = nodeEntry.model.width + 'px';
|
|
1162
|
+
innerContent.style.minHeight = nodeEntry.model.height + 'px';
|
|
1163
|
+
innerContent.style.maxWidth = nodeEntry.model.width + 'px';
|
|
1164
|
+
innerContent.style.maxHeight = nodeEntry.model.height + 'px';
|
|
1165
|
+
|
|
1166
|
+
// ▼▼▼ [Fix] 이미지 노드의 경우 내부 img 요소 크기도 업데이트 ▼▼▼
|
|
1167
|
+
if (nodeEntry.model.contentType === 'image') {
|
|
1168
|
+
const imgEl = innerContent.querySelector('img');
|
|
1169
|
+
if (imgEl) {
|
|
1170
|
+
imgEl.style.width = '100%';
|
|
1171
|
+
imgEl.style.height = '100%';
|
|
1172
|
+
}
|
|
1173
|
+
// contentWrapper와 responseDiv 크기도 업데이트
|
|
1174
|
+
const contentWrapper = innerContent.querySelector('.node-content-wrapper');
|
|
1175
|
+
if (contentWrapper) {
|
|
1176
|
+
contentWrapper.style.width = '100%';
|
|
1177
|
+
contentWrapper.style.height = '100%';
|
|
1178
|
+
}
|
|
1179
|
+
const responseDiv = innerContent.querySelector('.node-response');
|
|
1180
|
+
if (responseDiv) {
|
|
1181
|
+
responseDiv.style.width = '100%';
|
|
1182
|
+
responseDiv.style.height = '100%';
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
// ▲▲▲ [Fix] ▲▲▲
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
// ▼▼▼ [Fix] userData도 업데이트하여 syncCss3dContent에서 올바른 크기 사용 ▼▼▼
|
|
1189
|
+
nodeEntry.cssObject.userData.worldWidth = nodeEntry.model.width;
|
|
1190
|
+
nodeEntry.cssObject.userData.worldHeight = nodeEntry.model.height;
|
|
1191
|
+
// ▲▲▲ [Fix] ▲▲▲
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
if (window.MindMapCss3DManager?.syncAgentPlanStack) {
|
|
1195
|
+
window.MindMapCss3DManager.syncAgentPlanStack(module, nodeId, { persist: false });
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
if (nodeEntry.glObject) {
|
|
1199
|
+
const group = nodeEntry.glObject;
|
|
1200
|
+
|
|
1201
|
+
const bodyMesh = group.getObjectByName('body');
|
|
1202
|
+
const outline = group.getObjectByName('outline');
|
|
1203
|
+
let glowMesh = group.getObjectByName('glow');
|
|
1204
|
+
if (!ENABLE_WEBGL_GLOW) {
|
|
1205
|
+
glowMesh = removeGlowMesh(group);
|
|
1206
|
+
}
|
|
1207
|
+
const CORNER_RADIUS = 20;
|
|
1208
|
+
const OUTLINE_SHRINK = 2;
|
|
1209
|
+
const OUTLINE_RADIUS = Math.max(1, CORNER_RADIUS - 1);
|
|
1210
|
+
const CHAMFER_SIZE = 16;
|
|
1211
|
+
if (bodyMesh) {
|
|
1212
|
+
bodyMesh.geometry.dispose();
|
|
1213
|
+
bodyMesh.geometry = new THREE.PlaneGeometry(nodeEntry.model.width, nodeEntry.model.height);
|
|
1214
|
+
bodyMesh.position.set(nodeEntry.model.width / 2, -nodeEntry.model.height / 2, 0);
|
|
1215
|
+
if (nodeEntry.model.contentType === 'image') {
|
|
1216
|
+
applyImageCoverUv(bodyMesh, nodeEntry.model.width, nodeEntry.model.height);
|
|
1217
|
+
}
|
|
1218
|
+
// ▼▼▼ [Fix] CSS 모드에서 리사이즈 중 body mesh 숨김 유지 (이중 렌더링 방지) ▼▼▼
|
|
1219
|
+
if (nodeEntry.currentResizeType === 'CSS') {
|
|
1220
|
+
bodyMesh.visible = false;
|
|
1221
|
+
}
|
|
1222
|
+
// ▲▲▲ [Fix] ▲▲▲
|
|
1223
|
+
|
|
1224
|
+
// ▼▼▼ [Image Resize Fix] Don't clear map during resize to prevent white flash/loss ▼▼▼
|
|
1225
|
+
// if (bodyMesh.material.map) {
|
|
1226
|
+
// bodyMesh.material.map = null;
|
|
1227
|
+
// bodyMesh.material.color.setHex(0xffffff);
|
|
1228
|
+
// bodyMesh.material.needsUpdate = true;
|
|
1229
|
+
// }
|
|
1230
|
+
// ▲▲▲ [Image Resize Fix] ▲▲▲
|
|
1231
|
+
const isSelected = module.multiSelectedNodeIds.has(module.resizingNodeId) || module.selectedNodeIdJs === module.resizingNodeId;
|
|
1232
|
+
if (nodeEntry.model.contentType === 'image') {
|
|
1233
|
+
const usesCssViewDuringResize =
|
|
1234
|
+
nodeEntry.currentResizeType === 'CSS' ||
|
|
1235
|
+
(nodeEntry.currentType === 'CSS' && !!nodeEntry.cssObject?.visible);
|
|
1236
|
+
syncImageSelectionGlow(
|
|
1237
|
+
group,
|
|
1238
|
+
nodeEntry.model.width,
|
|
1239
|
+
nodeEntry.model.height,
|
|
1240
|
+
group.userData?.baseRenderOrder ?? bodyMesh?.renderOrder ?? 0,
|
|
1241
|
+
isSelected && !usesCssViewDuringResize
|
|
1242
|
+
);
|
|
1243
|
+
window.MindMapNodes?.syncImageMidLodDecorations?.(
|
|
1244
|
+
group,
|
|
1245
|
+
nodeEntry.model.width,
|
|
1246
|
+
nodeEntry.model.height,
|
|
1247
|
+
group.userData?.baseRenderOrder ?? bodyMesh?.renderOrder ?? 0,
|
|
1248
|
+
isSelected,
|
|
1249
|
+
usesCssViewDuringResize
|
|
1250
|
+
? { edgeVisible: false, glowVisible: false }
|
|
1251
|
+
: undefined
|
|
1252
|
+
);
|
|
1253
|
+
} else if (outline) {
|
|
1254
|
+
outline.geometry.dispose();
|
|
1255
|
+
outline.geometry = createRoundedRectGeometry(
|
|
1256
|
+
nodeEntry.model.width - OUTLINE_SHRINK,
|
|
1257
|
+
nodeEntry.model.height - OUTLINE_SHRINK,
|
|
1258
|
+
OUTLINE_RADIUS
|
|
1259
|
+
);
|
|
1260
|
+
outline.position.copy(bodyMesh.position);
|
|
1261
|
+
outline.position.z = 0; // [Fix] Z=0
|
|
1262
|
+
outline.visible = false; // Disabled - using SDF glow instead
|
|
1263
|
+
if (outline.material) {
|
|
1264
|
+
outline.material.color.setHex(isSelected ? 0x7b7ff2 : 0x9ca3af);
|
|
1265
|
+
outline.material.opacity = 1.0;
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
// ▼▼▼ [Changed] Update glow position during resize (keep visible, no flickering) ▼▼▼
|
|
1271
|
+
if (ENABLE_WEBGL_GLOW && glowMesh) {
|
|
1272
|
+
// Update position to match bodyMesh
|
|
1273
|
+
glowMesh.position.set(nodeEntry.model.width / 2, -nodeEntry.model.height / 2, 0); // [Fix] Z=0
|
|
1274
|
+
// Don't hide - causes flickering
|
|
1275
|
+
}
|
|
1276
|
+
// ▲▲▲ [Changed] ▲▲▲
|
|
1277
|
+
const tailMesh = group.getObjectByName('tail');
|
|
1278
|
+
if (tailMesh) {
|
|
1279
|
+
tailMesh.position.set(nodeEntry.model.width / 2, -nodeEntry.model.height - 6, 0);
|
|
1280
|
+
}
|
|
1281
|
+
// ▼▼▼ [Changed] Update eight resize handles (4 corners + 4 edges with geometry) ▼▼▼
|
|
1282
|
+
const edgeWidthLength = Math.max(10, nodeEntry.model.width - HANDLE_SIZE * 2);
|
|
1283
|
+
const edgeHeightLength = Math.max(10, nodeEntry.model.height - HANDLE_SIZE * 2);
|
|
1284
|
+
const handleConfigs = {
|
|
1285
|
+
// ▼▼▼ [Fix] Corners Z=0 for parallax elimination ▼▼▼
|
|
1286
|
+
'resizeHandle_TL': { x: HANDLE_SIZE / 2, y: -HANDLE_SIZE / 2, w: HANDLE_SIZE, h: HANDLE_SIZE, z: 0 },
|
|
1287
|
+
'resizeHandle_TR': { x: nodeEntry.model.width - HANDLE_SIZE / 2, y: -HANDLE_SIZE / 2, w: HANDLE_SIZE, h: HANDLE_SIZE, z: 0 },
|
|
1288
|
+
'resizeHandle_BL': { x: HANDLE_SIZE / 2, y: -nodeEntry.model.height + HANDLE_SIZE / 2, w: HANDLE_SIZE, h: HANDLE_SIZE, z: 0 },
|
|
1289
|
+
'resizeHandle_BR': { x: nodeEntry.model.width - HANDLE_SIZE / 2, y: -nodeEntry.model.height + HANDLE_SIZE / 2, w: HANDLE_SIZE, h: HANDLE_SIZE, z: 0 },
|
|
1290
|
+
// Edges Z=0
|
|
1291
|
+
'resizeHandle_T': { x: nodeEntry.model.width / 2, y: -HANDLE_SIZE / 2, w: edgeWidthLength, h: HANDLE_SIZE, z: 0 },
|
|
1292
|
+
'resizeHandle_B': { x: nodeEntry.model.width / 2, y: -nodeEntry.model.height + HANDLE_SIZE / 2, w: edgeWidthLength, h: HANDLE_SIZE, z: 0 },
|
|
1293
|
+
'resizeHandle_L': { x: HANDLE_SIZE / 2, y: -nodeEntry.model.height / 2, w: HANDLE_SIZE, h: edgeHeightLength, z: 0 },
|
|
1294
|
+
'resizeHandle_R': { x: nodeEntry.model.width - HANDLE_SIZE / 2, y: -nodeEntry.model.height / 2, w: HANDLE_SIZE, h: edgeHeightLength, z: 0 }
|
|
1295
|
+
// ▲▲▲ [Fix] ▲▲▲
|
|
1296
|
+
};
|
|
1297
|
+
Object.entries(handleConfigs).forEach(([name, cfg]) => {
|
|
1298
|
+
const handle = group.getObjectByName(name);
|
|
1299
|
+
if (handle) {
|
|
1300
|
+
handle.position.set(cfg.x, cfg.y, cfg.z);
|
|
1301
|
+
const gp = handle.geometry?.parameters;
|
|
1302
|
+
if (gp && (gp.width !== cfg.w || gp.height !== cfg.h)) {
|
|
1303
|
+
handle.geometry.dispose();
|
|
1304
|
+
handle.geometry = new THREE.PlaneGeometry(cfg.w, cfg.h);
|
|
1305
|
+
}
|
|
1306
|
+
}
|
|
1307
|
+
});
|
|
1308
|
+
// ▲▲▲ [Changed] ▲▲▲
|
|
1309
|
+
|
|
1310
|
+
// ▼▼▼ [New] Update Troika text clipRect on resize for code nodes ▼▼▼
|
|
1311
|
+
if (nodeEntry.model.contentType === 'code') {
|
|
1312
|
+
const troikaText = group.userData.troikaText;
|
|
1313
|
+
if (troikaText && troikaText.userData) {
|
|
1314
|
+
const padding = troikaText.userData.padding || 16;
|
|
1315
|
+
const filenameHeight = troikaText.userData.filenameHeight || 0; // ★ Include filename height
|
|
1316
|
+
const newWidth = nodeEntry.model.width;
|
|
1317
|
+
const newHeight = nodeEntry.model.height;
|
|
1318
|
+
const newTextWidth = newWidth - padding * 2;
|
|
1319
|
+
const newVisibleHeight = newHeight - padding * 2 - filenameHeight; // ★ Subtract filename height
|
|
1320
|
+
|
|
1321
|
+
// Update userData dimensions
|
|
1322
|
+
troikaText.userData.nodeWidth = newWidth;
|
|
1323
|
+
troikaText.userData.nodeHeight = newHeight;
|
|
1324
|
+
troikaText.userData.textWidth = newTextWidth;
|
|
1325
|
+
troikaText.userData.visibleHeight = newVisibleHeight;
|
|
1326
|
+
|
|
1327
|
+
// ★ DO NOT update maxWidth - code nodes should keep maxWidth: Infinity (no word wrap)
|
|
1328
|
+
// troikaText.maxWidth = newTextWidth; // REMOVED - this forces word wrap
|
|
1329
|
+
|
|
1330
|
+
// Keep existing scroll and recalc maxScroll based on totalHeight
|
|
1331
|
+
const totalHeight = troikaText.userData.totalHeight || newVisibleHeight;
|
|
1332
|
+
const maxScroll = Math.max(0, totalHeight - newVisibleHeight);
|
|
1333
|
+
nodeEntry.model.maxScroll = maxScroll;
|
|
1334
|
+
|
|
1335
|
+
// Clamp scroll position
|
|
1336
|
+
let scrollY = troikaText.userData.scrollY || 0;
|
|
1337
|
+
if (scrollY > maxScroll) {
|
|
1338
|
+
scrollY = maxScroll;
|
|
1339
|
+
troikaText.userData.scrollY = scrollY;
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
// Update clipRect - clip to visible area only
|
|
1343
|
+
const clipTop = -scrollY;
|
|
1344
|
+
const clipBottom = -scrollY - newVisibleHeight;
|
|
1345
|
+
troikaText.clipRect = [0, clipBottom, newTextWidth, clipTop];
|
|
1346
|
+
troikaText.sync();
|
|
1347
|
+
|
|
1348
|
+
// Update scrollbar - show/hide based on whether scrolling is needed
|
|
1349
|
+
const scrollbarTrack = group.getObjectByName('scrollbarTrack');
|
|
1350
|
+
const scrollbarThumb = group.getObjectByName('scrollbarThumb');
|
|
1351
|
+
const isCssMode = nodeEntry.currentType === 'CSS';
|
|
1352
|
+
|
|
1353
|
+
if (scrollbarTrack && scrollbarThumb) {
|
|
1354
|
+
// Keep native CSS scrollbar as the single source of truth in CSS mode.
|
|
1355
|
+
if (isCssMode || maxScroll <= 0) {
|
|
1356
|
+
scrollbarTrack.visible = false;
|
|
1357
|
+
scrollbarThumb.visible = false;
|
|
1358
|
+
} else {
|
|
1359
|
+
scrollbarTrack.visible = true;
|
|
1360
|
+
scrollbarThumb.visible = true;
|
|
1361
|
+
|
|
1362
|
+
const SCROLLBAR_WIDTH = 6;
|
|
1363
|
+
const SCROLLBAR_PADDING = 4;
|
|
1364
|
+
const SCROLLBAR_TOP_MARGIN = 16;
|
|
1365
|
+
const SCROLLBAR_BOTTOM_MARGIN = 16;
|
|
1366
|
+
const SCROLLBAR_RADIUS = 3;
|
|
1367
|
+
const trackHeight = newHeight - SCROLLBAR_TOP_MARGIN - SCROLLBAR_BOTTOM_MARGIN - filenameHeight; // ★ Include filenameHeight
|
|
1368
|
+
const trackX = newWidth - SCROLLBAR_WIDTH - SCROLLBAR_PADDING;
|
|
1369
|
+
const trackY = -SCROLLBAR_TOP_MARGIN - filenameHeight; // ★ Include filenameHeight
|
|
1370
|
+
|
|
1371
|
+
// Update track position and geometry
|
|
1372
|
+
scrollbarTrack.position.set(trackX, trackY, 3);
|
|
1373
|
+
if (scrollbarTrack.geometry) {
|
|
1374
|
+
scrollbarTrack.geometry.dispose();
|
|
1375
|
+
if (window.MindMapObjectManager?.createRoundedRectFilledGeometry) {
|
|
1376
|
+
scrollbarTrack.geometry = window.MindMapObjectManager.createRoundedRectFilledGeometry(SCROLLBAR_WIDTH, trackHeight, SCROLLBAR_RADIUS);
|
|
1377
|
+
} else {
|
|
1378
|
+
scrollbarTrack.geometry = new THREE.PlaneGeometry(SCROLLBAR_WIDTH, trackHeight);
|
|
1379
|
+
}
|
|
1380
|
+
}
|
|
1381
|
+
|
|
1382
|
+
// Update thumb
|
|
1383
|
+
const thumbHeightRatio = Math.min(1, newVisibleHeight / (maxScroll + newVisibleHeight));
|
|
1384
|
+
const thumbHeight = Math.max(24, trackHeight * thumbHeightRatio);
|
|
1385
|
+
const scrollRatio = scrollY / maxScroll;
|
|
1386
|
+
const thumbTravel = trackHeight - thumbHeight;
|
|
1387
|
+
const thumbY = trackY - (scrollRatio * thumbTravel); // ★ Use trackY which includes filenameHeight
|
|
1388
|
+
|
|
1389
|
+
scrollbarThumb.position.set(trackX, thumbY, 3.1);
|
|
1390
|
+
if (scrollbarThumb.geometry) {
|
|
1391
|
+
scrollbarThumb.geometry.dispose();
|
|
1392
|
+
if (window.MindMapObjectManager?.createRoundedRectFilledGeometry) {
|
|
1393
|
+
scrollbarThumb.geometry = window.MindMapObjectManager.createRoundedRectFilledGeometry(SCROLLBAR_WIDTH, thumbHeight, SCROLLBAR_RADIUS);
|
|
1394
|
+
} else {
|
|
1395
|
+
scrollbarThumb.geometry = new THREE.PlaneGeometry(SCROLLBAR_WIDTH, thumbHeight);
|
|
1396
|
+
}
|
|
1397
|
+
}
|
|
1398
|
+
scrollbarThumb.userData.trackHeight = trackHeight;
|
|
1399
|
+
scrollbarThumb.userData.thumbHeight = thumbHeight;
|
|
1400
|
+
}
|
|
1401
|
+
}
|
|
1402
|
+
}
|
|
1403
|
+
}
|
|
1404
|
+
// ▲▲▲ [New] ▲▲▲
|
|
1405
|
+
|
|
1406
|
+
// ▼▼▼ [New] Update glow shader dimensions on resize ▼▼▼
|
|
1407
|
+
if (ENABLE_WEBGL_GLOW && glowMesh && window.MindMapGlowShader?.updateGlowSize) {
|
|
1408
|
+
window.MindMapGlowShader.updateGlowSize(glowMesh, nodeEntry.model.width, nodeEntry.model.height, nodeEntry.model.contentType);
|
|
1409
|
+
}
|
|
1410
|
+
// ▲▲▲ [New] ▲▲▲
|
|
1411
|
+
|
|
1412
|
+
group.userData.worldWidth = nodeEntry.model.width;
|
|
1413
|
+
group.userData.worldHeight = nodeEntry.model.height;
|
|
1414
|
+
}
|
|
1415
|
+
|
|
1416
|
+
// ▼▼▼ [Fix] LOD 모드에서 리사이즈 시 InstancedMesh 실시간 업데이트 ▼▼▼
|
|
1417
|
+
if (module.lodRenderer?.isInLODMode && module.lodRenderer?.requestResidentPatch) {
|
|
1418
|
+
module.lodRenderer.requestResidentPatch('update-node', nodeId);
|
|
1419
|
+
}
|
|
1420
|
+
// ▲▲▲ [Fix] ▲▲▲
|
|
1421
|
+
}
|
|
1422
|
+
|
|
1423
|
+
function debouncedProcessRenderQueue(module, nodeId, updateData) {
|
|
1424
|
+
if (debounceTimers.has(nodeId)) {
|
|
1425
|
+
clearTimeout(debounceTimers.get(nodeId));
|
|
1426
|
+
}
|
|
1427
|
+
window.MindMapNodes.pendingNodeUpdates.set(nodeId, updateData);
|
|
1428
|
+
const timer = setTimeout(() => {
|
|
1429
|
+
enqueueRender(module, nodeId);
|
|
1430
|
+
debounceTimers.delete(nodeId);
|
|
1431
|
+
}, 100);
|
|
1432
|
+
debounceTimers.set(nodeId, timer);
|
|
1433
|
+
}
|
|
1434
|
+
|
|
1435
|
+
function throttledProcessRenderQueueDuringResize(module, nodeId, updateData) {
|
|
1436
|
+
const now = performance.now();
|
|
1437
|
+
const lastCallTime = lastResizeUpdateCall.get(nodeId) || 0;
|
|
1438
|
+
|
|
1439
|
+
if (now - lastCallTime < THROTTLE_INTERVAL) {
|
|
1440
|
+
window.MindMapNodes.pendingNodeUpdates.set(nodeId, updateData);
|
|
1441
|
+
if (resizeThrottleTimers.has(nodeId)) return;
|
|
1442
|
+
const timer = setTimeout(() => {
|
|
1443
|
+
const queuedUpdateData = window.MindMapNodes.pendingNodeUpdates.get(nodeId);
|
|
1444
|
+
if (queuedUpdateData) {
|
|
1445
|
+
enqueueRender(module, nodeId);
|
|
1446
|
+
}
|
|
1447
|
+
resizeThrottleTimers.delete(nodeId);
|
|
1448
|
+
}, THROTTLE_INTERVAL);
|
|
1449
|
+
resizeThrottleTimers.set(nodeId, timer);
|
|
1450
|
+
return;
|
|
1451
|
+
}
|
|
1452
|
+
|
|
1453
|
+
lastResizeUpdateCall.set(nodeId, now);
|
|
1454
|
+
window.MindMapNodes.pendingNodeUpdates.set(nodeId, updateData);
|
|
1455
|
+
enqueueRender(module, nodeId);
|
|
1456
|
+
|
|
1457
|
+
// ▼▼▼ [Fix] Maintain appropriate resize cursor during doResizing ▼▼▼
|
|
1458
|
+
let cursor = 'nwse-resize';
|
|
1459
|
+
switch (corner) {
|
|
1460
|
+
case 'TL': case 'BR': cursor = 'nwse-resize'; break;
|
|
1461
|
+
case 'TR': case 'BL': cursor = 'nesw-resize'; break;
|
|
1462
|
+
case 'T': case 'B': cursor = 'ns-resize'; break;
|
|
1463
|
+
case 'L': case 'R': cursor = 'ew-resize'; break;
|
|
1464
|
+
}
|
|
1465
|
+
document.body.style.cursor = cursor;
|
|
1466
|
+
// ▲▲▲ [Fix] ▲▲▲
|
|
1467
|
+
}
|
|
1468
|
+
|
|
1469
|
+
function throttledScrollUpdate(module, nodeId, scrollDelta) {
|
|
1470
|
+
const now = performance.now();
|
|
1471
|
+
const lastCallTime = lastScrollCall.get(nodeId) || 0;
|
|
1472
|
+
|
|
1473
|
+
if (now - lastCallTime < THROTTLE_INTERVAL) {
|
|
1474
|
+
return;
|
|
1475
|
+
}
|
|
1476
|
+
lastScrollCall.set(nodeId, now);
|
|
1477
|
+
|
|
1478
|
+
const nodeEntry = module.nodeObjectsById.get(nodeId);
|
|
1479
|
+
if (!nodeEntry) return;
|
|
1480
|
+
|
|
1481
|
+
const model = nodeEntry.model;
|
|
1482
|
+
const oldOffset = model.scrollOffset || 0;
|
|
1483
|
+
const maxScroll = model.maxScroll || 0;
|
|
1484
|
+
const newOffset = Math.max(0, Math.min(oldOffset + scrollDelta, maxScroll));
|
|
1485
|
+
|
|
1486
|
+
if (Math.abs(newOffset - oldOffset) < 0.1) return;
|
|
1487
|
+
|
|
1488
|
+
model.scrollOffset = newOffset;
|
|
1489
|
+
window.MindMapCss3DManager?.syncCss3dScrollFromModel?.(module, nodeId);
|
|
1490
|
+
|
|
1491
|
+
// ▼▼▼ [New] Code nodes: update Troika text via scrollCodeText ▼▼▼
|
|
1492
|
+
if (model.contentType === 'code' && nodeEntry.glObject) {
|
|
1493
|
+
const troikaText = nodeEntry.glObject.userData.troikaText;
|
|
1494
|
+
if (troikaText && window.MindMapTroikaText?.scrollCodeText) {
|
|
1495
|
+
// Use scrollCodeText which handles both position and clipRect
|
|
1496
|
+
const scrollResult = window.MindMapTroikaText.scrollCodeText(troikaText, scrollDelta);
|
|
1497
|
+
if (scrollResult) {
|
|
1498
|
+
model.scrollOffset = scrollResult.scrollY;
|
|
1499
|
+
|
|
1500
|
+
// ★ Update scrollbar thumb position (ShapeGeometry anchor is top-left, not center)
|
|
1501
|
+
const scrollbarThumb = nodeEntry.glObject.getObjectByName('scrollbarThumb');
|
|
1502
|
+
if (scrollbarThumb && scrollResult.maxScroll > 0) {
|
|
1503
|
+
const thumbData = scrollbarThumb.userData;
|
|
1504
|
+
const thumbTravel = thumbData.trackHeight - thumbData.thumbHeight;
|
|
1505
|
+
const thumbY = -thumbData.topMargin - (scrollResult.scrollRatio * thumbTravel);
|
|
1506
|
+
scrollbarThumb.position.y = thumbY;
|
|
1507
|
+
}
|
|
1508
|
+
}
|
|
1509
|
+
|
|
1510
|
+
// Persist scroll position to C#
|
|
1511
|
+
throttledUpdateScrollInCSharp(module, nodeId, model.scrollOffset);
|
|
1512
|
+
return;
|
|
1513
|
+
}
|
|
1514
|
+
}
|
|
1515
|
+
// ▲▲▲ [New] ▲▲▲
|
|
1516
|
+
|
|
1517
|
+
// ▼▼▼ [Refactor] Fetch content directly from C# (no cache) ▼▼▼
|
|
1518
|
+
if (model.isChunkedText && module.dotNetHelper) {
|
|
1519
|
+
// Cancel the previous request (process only the latest)
|
|
1520
|
+
if (model._scrollFetchController) {
|
|
1521
|
+
model._scrollFetchController.abort = true;
|
|
1522
|
+
}
|
|
1523
|
+
const controller = { abort: false };
|
|
1524
|
+
model._scrollFetchController = controller;
|
|
1525
|
+
|
|
1526
|
+
// Debounce: render after scrolling stops
|
|
1527
|
+
if (model._scrollDebounceTimer) {
|
|
1528
|
+
clearTimeout(model._scrollDebounceTimer);
|
|
1529
|
+
}
|
|
1530
|
+
model._scrollDebounceTimer = setTimeout(async () => {
|
|
1531
|
+
if (controller.abort) return;
|
|
1532
|
+
|
|
1533
|
+
try {
|
|
1534
|
+
const result = await module.dotNetHelper.invokeMethodAsync(
|
|
1535
|
+
'GetTextByScrollPosition',
|
|
1536
|
+
nodeId,
|
|
1537
|
+
model.scrollOffset,
|
|
1538
|
+
model.height || 400,
|
|
1539
|
+
CHUNK_LINE_HEIGHT
|
|
1540
|
+
);
|
|
1541
|
+
|
|
1542
|
+
if (controller.abort || !result) return;
|
|
1543
|
+
|
|
1544
|
+
// Set content used for rendering
|
|
1545
|
+
model._visibleContent = result.content;
|
|
1546
|
+
model._visibleScrollOffset = result.relativeScrollOffset;
|
|
1547
|
+
|
|
1548
|
+
window.MindMapNodes.pendingNodeUpdates.set(nodeId, {
|
|
1549
|
+
content: result.content,
|
|
1550
|
+
width: model.width,
|
|
1551
|
+
height: model.height,
|
|
1552
|
+
scrollDelta: 0
|
|
1553
|
+
});
|
|
1554
|
+
enqueueRender(module, nodeId);
|
|
1555
|
+
} catch (e) {
|
|
1556
|
+
console.error(`[Pipeline] GetTextByScrollPosition error:`, e);
|
|
1557
|
+
}
|
|
1558
|
+
}, 16); // 16ms - 60fps throttle
|
|
1559
|
+
|
|
1560
|
+
// Persist scroll position to C#
|
|
1561
|
+
throttledUpdateScrollInCSharp(module, nodeId, model.scrollOffset);
|
|
1562
|
+
return;
|
|
1563
|
+
}
|
|
1564
|
+
// ▲▲▲ [Refactor] ▲▲▲
|
|
1565
|
+
|
|
1566
|
+
// Regular nodes (non-chunked)
|
|
1567
|
+
const updateData = {
|
|
1568
|
+
content: model.response,
|
|
1569
|
+
width: model.width,
|
|
1570
|
+
height: model.height,
|
|
1571
|
+
scrollDelta: 0
|
|
1572
|
+
};
|
|
1573
|
+
window.MindMapNodes.pendingNodeUpdates.set(nodeId, updateData);
|
|
1574
|
+
enqueueRender(module, nodeId);
|
|
1575
|
+
|
|
1576
|
+
// ▼▼▼ [New] Persist scroll position for regular nodes too ▼▼▼
|
|
1577
|
+
throttledUpdateScrollInCSharp(module, nodeId, model.scrollOffset);
|
|
1578
|
+
// ▲▲▲ [New] ▲▲▲
|
|
1579
|
+
}
|
|
1580
|
+
|
|
1581
|
+
// ▼▼▼ [New] Set absolute scroll position (for scrollbar drag) ▼▼▼
|
|
1582
|
+
function setScrollPosition(module, nodeId, absoluteScrollPosition) {
|
|
1583
|
+
const now = performance.now();
|
|
1584
|
+
const lastCallTime = lastScrollCall.get(nodeId) || 0;
|
|
1585
|
+
|
|
1586
|
+
// Throttling (ignore calls that arrive too quickly)
|
|
1587
|
+
if (now - lastCallTime < THROTTLE_INTERVAL) {
|
|
1588
|
+
return;
|
|
1589
|
+
}
|
|
1590
|
+
lastScrollCall.set(nodeId, now);
|
|
1591
|
+
|
|
1592
|
+
const nodeEntry = module.nodeObjectsById.get(nodeId);
|
|
1593
|
+
if (!nodeEntry) return;
|
|
1594
|
+
|
|
1595
|
+
const model = nodeEntry.model;
|
|
1596
|
+
const oldOffset = model.scrollOffset || 0;
|
|
1597
|
+
const maxScroll = model.maxScroll || 0;
|
|
1598
|
+
const newOffset = Math.max(0, Math.min(absoluteScrollPosition, maxScroll));
|
|
1599
|
+
|
|
1600
|
+
if (Math.abs(newOffset - oldOffset) < 0.1) return;
|
|
1601
|
+
|
|
1602
|
+
model.scrollOffset = newOffset;
|
|
1603
|
+
window.MindMapCss3DManager?.syncCss3dScrollFromModel?.(module, nodeId);
|
|
1604
|
+
|
|
1605
|
+
// ▼▼▼ [Refactor] Fetch content directly from C# (no cache) ▼▼▼
|
|
1606
|
+
if (model.isChunkedText && module.dotNetHelper) {
|
|
1607
|
+
// Cancel the previous request (process only the latest)
|
|
1608
|
+
if (model._scrollFetchController) {
|
|
1609
|
+
model._scrollFetchController.abort = true;
|
|
1610
|
+
}
|
|
1611
|
+
const controller = { abort: false };
|
|
1612
|
+
model._scrollFetchController = controller;
|
|
1613
|
+
|
|
1614
|
+
// Throttle: update every 16ms even during fast scrolling (~60fps)
|
|
1615
|
+
if (model._scrollDebounceTimer) {
|
|
1616
|
+
clearTimeout(model._scrollDebounceTimer);
|
|
1617
|
+
}
|
|
1618
|
+
model._scrollDebounceTimer = setTimeout(async () => {
|
|
1619
|
+
if (controller.abort) return;
|
|
1620
|
+
|
|
1621
|
+
try {
|
|
1622
|
+
const result = await module.dotNetHelper.invokeMethodAsync(
|
|
1623
|
+
'GetTextByScrollPosition',
|
|
1624
|
+
nodeId,
|
|
1625
|
+
model.scrollOffset,
|
|
1626
|
+
model.height || 400,
|
|
1627
|
+
CHUNK_LINE_HEIGHT
|
|
1628
|
+
);
|
|
1629
|
+
|
|
1630
|
+
if (controller.abort || !result) return;
|
|
1631
|
+
|
|
1632
|
+
// Set content used for rendering
|
|
1633
|
+
model._visibleContent = result.content;
|
|
1634
|
+
model._visibleScrollOffset = result.relativeScrollOffset;
|
|
1635
|
+
|
|
1636
|
+
window.MindMapNodes.pendingNodeUpdates.set(nodeId, {
|
|
1637
|
+
content: result.content,
|
|
1638
|
+
width: model.width,
|
|
1639
|
+
height: model.height,
|
|
1640
|
+
scrollDelta: 0
|
|
1641
|
+
});
|
|
1642
|
+
enqueueRender(module, nodeId);
|
|
1643
|
+
} catch (e) {
|
|
1644
|
+
console.error(`[Pipeline] GetTextByScrollPosition error:`, e);
|
|
1645
|
+
}
|
|
1646
|
+
}, 16); // 16ms - 60fps throttle
|
|
1647
|
+
|
|
1648
|
+
// Persist scroll position to C#
|
|
1649
|
+
throttledUpdateScrollInCSharp(module, nodeId, model.scrollOffset);
|
|
1650
|
+
return;
|
|
1651
|
+
}
|
|
1652
|
+
// ▲▲▲ [Refactor] ▲▲▲
|
|
1653
|
+
|
|
1654
|
+
// Regular nodes (non-chunked)
|
|
1655
|
+
const updateData = {
|
|
1656
|
+
content: model.response,
|
|
1657
|
+
width: model.width,
|
|
1658
|
+
height: model.height,
|
|
1659
|
+
scrollDelta: 0
|
|
1660
|
+
};
|
|
1661
|
+
window.MindMapNodes.pendingNodeUpdates.set(nodeId, updateData);
|
|
1662
|
+
enqueueRender(module, nodeId);
|
|
1663
|
+
|
|
1664
|
+
// ▼▼▼ [New] Persist scroll position for regular nodes too ▼▼▼
|
|
1665
|
+
throttledUpdateScrollInCSharp(module, nodeId, model.scrollOffset);
|
|
1666
|
+
// ▲▲▲ [New] ▲▲▲
|
|
1667
|
+
}
|
|
1668
|
+
// ▲▲▲ [New] Set absolute scroll position (for scrollbar drag) ▲▲▲
|
|
1669
|
+
|
|
1670
|
+
function throttledUpdateScrollInCSharp(module, nodeId, scrollOffset) {
|
|
1671
|
+
if (scrollUpdateThrottleTimers.has(nodeId)) {
|
|
1672
|
+
clearTimeout(scrollUpdateThrottleTimers.get(nodeId));
|
|
1673
|
+
}
|
|
1674
|
+
const timer = setTimeout(() => {
|
|
1675
|
+
try {
|
|
1676
|
+
module.dotNetHelper?.invokeMethodAsync('UpdateNodeScroll', nodeId, scrollOffset);
|
|
1677
|
+
} catch (e) {
|
|
1678
|
+
console.error(`[Pipeline] Failed to update scroll in C# for node ${nodeId}:`, e);
|
|
1679
|
+
}
|
|
1680
|
+
scrollUpdateThrottleTimers.delete(nodeId);
|
|
1681
|
+
}, 100);
|
|
1682
|
+
scrollUpdateThrottleTimers.set(nodeId, timer);
|
|
1683
|
+
}
|
|
1684
|
+
|
|
1685
|
+
async function stopResizing(module) {
|
|
1686
|
+
if (!module.isResizing || !module.resizeNodeObject) return;
|
|
1687
|
+
|
|
1688
|
+
// ▼▼▼ [Changed] Use stored nodeId (CSS3D mode compatible) ▼▼▼
|
|
1689
|
+
const nodeId = module.resizingNodeId || (module.resizeNodeObject.userData?.nodeId || module.resizeNodeObject.element?.dataset?.nodeId);
|
|
1690
|
+
const nodeEntry = module.nodeObjectsById.get(nodeId);
|
|
1691
|
+
|
|
1692
|
+
if (nodeEntry && module.dotNetHelper) {
|
|
1693
|
+
const currentPosX = nodeEntry.glObject?.position?.x ?? nodeEntry.cssObject?.position?.x ?? 0;
|
|
1694
|
+
const currentPosY = nodeEntry.glObject?.position?.y ?? nodeEntry.cssObject?.position?.y ?? 0;
|
|
1695
|
+
let finalPosX = currentPosX;
|
|
1696
|
+
let finalPosY = currentPosY;
|
|
1697
|
+
|
|
1698
|
+
// Respect global snap toggle. Resize itself stays free-form while dragging;
|
|
1699
|
+
// snapping is applied only at the end when enabled.
|
|
1700
|
+
if (module.gridSnapEnabled !== false) {
|
|
1701
|
+
const GRID_SIZE = module.GRID_SIZE || 10;
|
|
1702
|
+
nodeEntry.model.width = Math.round(nodeEntry.model.width / GRID_SIZE) * GRID_SIZE;
|
|
1703
|
+
nodeEntry.model.height = Math.round(nodeEntry.model.height / GRID_SIZE) * GRID_SIZE;
|
|
1704
|
+
finalPosX = Math.round(currentPosX / GRID_SIZE) * GRID_SIZE;
|
|
1705
|
+
finalPosY = Math.round(currentPosY / GRID_SIZE) * GRID_SIZE;
|
|
1706
|
+
}
|
|
1707
|
+
|
|
1708
|
+
if (nodeEntry.glObject) {
|
|
1709
|
+
nodeEntry.glObject.position.x = finalPosX;
|
|
1710
|
+
nodeEntry.glObject.position.y = finalPosY;
|
|
1711
|
+
}
|
|
1712
|
+
if (nodeEntry.cssObject) {
|
|
1713
|
+
nodeEntry.cssObject.position.x = finalPosX;
|
|
1714
|
+
nodeEntry.cssObject.position.y = finalPosY;
|
|
1715
|
+
nodeEntry.cssObject.updateMatrixWorld(true);
|
|
1716
|
+
|
|
1717
|
+
// ▼▼▼ [Fix] 그리드 스냅 후 CSS3D DOM 크기도 업데이트 ▼▼▼
|
|
1718
|
+
const wrapper = nodeEntry.cssObject.element;
|
|
1719
|
+
if (wrapper) {
|
|
1720
|
+
const resolutionScale = nodeEntry.cssObject.userData?.resolutionScale || 1;
|
|
1721
|
+
const domWidth = nodeEntry.model.width * resolutionScale;
|
|
1722
|
+
const domHeight = nodeEntry.model.height * resolutionScale;
|
|
1723
|
+
|
|
1724
|
+
wrapper.style.width = domWidth + 'px';
|
|
1725
|
+
wrapper.style.height = domHeight + 'px';
|
|
1726
|
+
wrapper.style.minWidth = domWidth + 'px';
|
|
1727
|
+
wrapper.style.minHeight = domHeight + 'px';
|
|
1728
|
+
wrapper.style.maxWidth = domWidth + 'px';
|
|
1729
|
+
wrapper.style.maxHeight = domHeight + 'px';
|
|
1730
|
+
|
|
1731
|
+
const innerContent = wrapper.firstElementChild;
|
|
1732
|
+
if (innerContent) {
|
|
1733
|
+
innerContent.style.width = nodeEntry.model.width + 'px';
|
|
1734
|
+
innerContent.style.height = nodeEntry.model.height + 'px';
|
|
1735
|
+
innerContent.style.minWidth = nodeEntry.model.width + 'px';
|
|
1736
|
+
innerContent.style.minHeight = nodeEntry.model.height + 'px';
|
|
1737
|
+
innerContent.style.maxWidth = nodeEntry.model.width + 'px';
|
|
1738
|
+
innerContent.style.maxHeight = nodeEntry.model.height + 'px';
|
|
1739
|
+
|
|
1740
|
+
// ▼▼▼ [Fix] 이미지 노드의 경우 내부 img 요소 크기도 업데이트 ▼▼▼
|
|
1741
|
+
if (nodeEntry.model.contentType === 'image') {
|
|
1742
|
+
const imgEl = innerContent.querySelector('img');
|
|
1743
|
+
if (imgEl) {
|
|
1744
|
+
imgEl.style.width = '100%';
|
|
1745
|
+
imgEl.style.height = '100%';
|
|
1746
|
+
}
|
|
1747
|
+
const contentWrapper = innerContent.querySelector('.node-content-wrapper');
|
|
1748
|
+
if (contentWrapper) {
|
|
1749
|
+
contentWrapper.style.width = '100%';
|
|
1750
|
+
contentWrapper.style.height = '100%';
|
|
1751
|
+
}
|
|
1752
|
+
const responseDiv = innerContent.querySelector('.node-response');
|
|
1753
|
+
if (responseDiv) {
|
|
1754
|
+
responseDiv.style.width = '100%';
|
|
1755
|
+
responseDiv.style.height = '100%';
|
|
1756
|
+
}
|
|
1757
|
+
}
|
|
1758
|
+
// ▲▲▲ [Fix] ▲▲▲
|
|
1759
|
+
}
|
|
1760
|
+
|
|
1761
|
+
// userData 업데이트
|
|
1762
|
+
nodeEntry.cssObject.userData.worldWidth = nodeEntry.model.width;
|
|
1763
|
+
nodeEntry.cssObject.userData.worldHeight = nodeEntry.model.height;
|
|
1764
|
+
}
|
|
1765
|
+
// ▲▲▲ [Fix] ▲▲▲
|
|
1766
|
+
}
|
|
1767
|
+
try {
|
|
1768
|
+
await module.dotNetHelper.invokeMethodAsync(
|
|
1769
|
+
'UpdateNodeDimensions',
|
|
1770
|
+
nodeId,
|
|
1771
|
+
Math.round(nodeEntry.model.width),
|
|
1772
|
+
Math.round(nodeEntry.model.height)
|
|
1773
|
+
);
|
|
1774
|
+
// ▼▼▼ [Changed] Also update position in C# ▼▼▼
|
|
1775
|
+
await module.dotNetHelper.invokeMethodAsync(
|
|
1776
|
+
'UpdateNodePosition',
|
|
1777
|
+
nodeId,
|
|
1778
|
+
Math.round(finalPosX),
|
|
1779
|
+
Math.round(finalPosY)
|
|
1780
|
+
);
|
|
1781
|
+
// ▲▲▲ [Changed] ▲▲▲
|
|
1782
|
+
} catch (e) {
|
|
1783
|
+
console.error('[MindMapPipeline] Error updating dimensions/position:', e);
|
|
1784
|
+
}
|
|
1785
|
+
|
|
1786
|
+
if (window.MindMapCss3DManager?.syncAgentPlanStack) {
|
|
1787
|
+
try {
|
|
1788
|
+
await window.MindMapCss3DManager.syncAgentPlanStack(module, nodeId, { persist: true });
|
|
1789
|
+
} catch (error) {
|
|
1790
|
+
console.warn('[MindMapPipeline] Failed to sync agent plan stack after resize:', error);
|
|
1791
|
+
}
|
|
1792
|
+
}
|
|
1793
|
+
|
|
1794
|
+
// ▼▼▼ [UndoRedo] Record resize command after dimensions/position are persisted ▼▼▼
|
|
1795
|
+
try { await module.dotNetHelper.invokeMethodAsync('EndResizeWithUndo'); } catch { }
|
|
1796
|
+
// ▲▲▲ [UndoRedo] ▲▲▲
|
|
1797
|
+
|
|
1798
|
+
if (nodeEntry.glObject) {
|
|
1799
|
+
const outline = nodeEntry.glObject.getObjectByName('outline');
|
|
1800
|
+
let glowMesh = nodeEntry.glObject.getObjectByName('glow');
|
|
1801
|
+
if (!ENABLE_WEBGL_GLOW) {
|
|
1802
|
+
glowMesh = removeGlowMesh(nodeEntry.glObject);
|
|
1803
|
+
}
|
|
1804
|
+
const bodyMesh = nodeEntry.glObject.getObjectByName('body');
|
|
1805
|
+
const isSelected = module.multiSelectedNodeIds.has(nodeId) || module.selectedNodeIdJs === nodeId;
|
|
1806
|
+
|
|
1807
|
+
// ▼▼▼ [Fix] 그리드 스냅 후 body mesh 크기/위치 업데이트 ▼▼▼
|
|
1808
|
+
if (bodyMesh) {
|
|
1809
|
+
bodyMesh.geometry.dispose();
|
|
1810
|
+
bodyMesh.geometry = new THREE.PlaneGeometry(nodeEntry.model.width, nodeEntry.model.height);
|
|
1811
|
+
bodyMesh.position.set(nodeEntry.model.width / 2, -nodeEntry.model.height / 2, 0);
|
|
1812
|
+
if (nodeEntry.model.contentType === 'image') {
|
|
1813
|
+
applyImageCoverUv(bodyMesh, nodeEntry.model.width, nodeEntry.model.height);
|
|
1814
|
+
}
|
|
1815
|
+
// ▼▼▼ [Fix] CSS 모드에서 body mesh 숨김 유지 (이중 렌더링 방지) ▼▼▼
|
|
1816
|
+
if (nodeEntry.currentResizeType === 'CSS') {
|
|
1817
|
+
bodyMesh.visible = false;
|
|
1818
|
+
}
|
|
1819
|
+
// ▲▲▲ [Fix] ▲▲▲
|
|
1820
|
+
}
|
|
1821
|
+
// ▲▲▲ [Fix] ▲▲▲
|
|
1822
|
+
|
|
1823
|
+
if (nodeEntry.model.contentType === 'image') {
|
|
1824
|
+
const usesCssViewAfterResize =
|
|
1825
|
+
nodeEntry.currentResizeType === 'CSS' ||
|
|
1826
|
+
(nodeEntry.currentType === 'CSS' && !!nodeEntry.cssObject?.visible);
|
|
1827
|
+
syncImageSelectionGlow(
|
|
1828
|
+
nodeEntry.glObject,
|
|
1829
|
+
nodeEntry.model.width,
|
|
1830
|
+
nodeEntry.model.height,
|
|
1831
|
+
nodeEntry.glObject.userData?.baseRenderOrder ?? bodyMesh?.renderOrder ?? 0,
|
|
1832
|
+
isSelected && !usesCssViewAfterResize
|
|
1833
|
+
);
|
|
1834
|
+
window.MindMapNodes?.syncImageMidLodDecorations?.(
|
|
1835
|
+
nodeEntry.glObject,
|
|
1836
|
+
nodeEntry.model.width,
|
|
1837
|
+
nodeEntry.model.height,
|
|
1838
|
+
nodeEntry.glObject.userData?.baseRenderOrder ?? bodyMesh?.renderOrder ?? 0,
|
|
1839
|
+
isSelected,
|
|
1840
|
+
usesCssViewAfterResize
|
|
1841
|
+
? { edgeVisible: false, glowVisible: false }
|
|
1842
|
+
: undefined
|
|
1843
|
+
);
|
|
1844
|
+
} else if (outline && outline.material) {
|
|
1845
|
+
outline.material.color.setHex(isSelected ? 0x7b7ff2 : 0x374151);
|
|
1846
|
+
outline.visible = false; // Disabled - using SDF glow instead
|
|
1847
|
+
}
|
|
1848
|
+
|
|
1849
|
+
// ▼▼▼ [Changed] After resizing, update glow using SDF shader ▼▼▼
|
|
1850
|
+
if (ENABLE_WEBGL_GLOW && glowMesh && window.MindMapGlowShader?.updateGlowSize) {
|
|
1851
|
+
const contentType = nodeEntry.model.contentType || 'text';
|
|
1852
|
+
window.MindMapGlowShader.updateGlowSize(glowMesh, nodeEntry.model.width, nodeEntry.model.height, contentType);
|
|
1853
|
+
glowMesh.position.set(
|
|
1854
|
+
nodeEntry.model.width / 2,
|
|
1855
|
+
-nodeEntry.model.height / 2,
|
|
1856
|
+
0
|
|
1857
|
+
);
|
|
1858
|
+
// ★ 카메라 거리에 따라 가시성 설정
|
|
1859
|
+
const cameraZ = module.camera?.position?.z ?? Infinity;
|
|
1860
|
+
const threshold = 4000;
|
|
1861
|
+
glowMesh.visible = cameraZ < threshold;
|
|
1862
|
+
}
|
|
1863
|
+
// ▲▲▲ [Changed] ▲▲▲
|
|
1864
|
+
|
|
1865
|
+
// ▼▼▼ [Fix] 리사이즈 종료 후 8방향 핸들 위치 업데이트 ▼▼▼
|
|
1866
|
+
const HANDLE_SIZE = RESIZE_HANDLE_SIZE;
|
|
1867
|
+
const w = nodeEntry.model.width;
|
|
1868
|
+
const h = nodeEntry.model.height;
|
|
1869
|
+
const edgeWidthLength = Math.max(10, w - HANDLE_SIZE * 2);
|
|
1870
|
+
const edgeHeightLength = Math.max(10, h - HANDLE_SIZE * 2);
|
|
1871
|
+
const handleConfigs = {
|
|
1872
|
+
// Corners
|
|
1873
|
+
'resizeHandle_TL': { x: HANDLE_SIZE / 2, y: -HANDLE_SIZE / 2, w: HANDLE_SIZE, h: HANDLE_SIZE, z: 0 },
|
|
1874
|
+
'resizeHandle_TR': { x: w - HANDLE_SIZE / 2, y: -HANDLE_SIZE / 2, w: HANDLE_SIZE, h: HANDLE_SIZE, z: 0 },
|
|
1875
|
+
'resizeHandle_BL': { x: HANDLE_SIZE / 2, y: -h + HANDLE_SIZE / 2, w: HANDLE_SIZE, h: HANDLE_SIZE, z: 0 },
|
|
1876
|
+
'resizeHandle_BR': { x: w - HANDLE_SIZE / 2, y: -h + HANDLE_SIZE / 2, w: HANDLE_SIZE, h: HANDLE_SIZE, z: 0 }
|
|
1877
|
+
};
|
|
1878
|
+
if (nodeEntry.model.contentType !== 'image') {
|
|
1879
|
+
Object.assign(handleConfigs, {
|
|
1880
|
+
'resizeHandle_T': { x: w / 2, y: -HANDLE_SIZE / 2, w: edgeWidthLength, h: HANDLE_SIZE, z: 0 },
|
|
1881
|
+
'resizeHandle_B': { x: w / 2, y: -h + HANDLE_SIZE / 2, w: edgeWidthLength, h: HANDLE_SIZE, z: 0 },
|
|
1882
|
+
'resizeHandle_L': { x: HANDLE_SIZE / 2, y: -h / 2, w: HANDLE_SIZE, h: edgeHeightLength, z: 0 },
|
|
1883
|
+
'resizeHandle_R': { x: w - HANDLE_SIZE / 2, y: -h / 2, w: HANDLE_SIZE, h: edgeHeightLength, z: 0 }
|
|
1884
|
+
});
|
|
1885
|
+
}
|
|
1886
|
+
['resizeHandle_TL', 'resizeHandle_TR', 'resizeHandle_BL', 'resizeHandle_BR', 'resizeHandle_T', 'resizeHandle_B', 'resizeHandle_L', 'resizeHandle_R'].forEach(name => {
|
|
1887
|
+
const handle = nodeEntry.glObject.getObjectByName(name);
|
|
1888
|
+
const cfg = handleConfigs[name];
|
|
1889
|
+
if (!handle) {
|
|
1890
|
+
return;
|
|
1891
|
+
}
|
|
1892
|
+
if (!cfg) {
|
|
1893
|
+
handle.visible = false;
|
|
1894
|
+
return;
|
|
1895
|
+
}
|
|
1896
|
+
handle.position.set(cfg.x, cfg.y, cfg.z);
|
|
1897
|
+
const gp = handle.geometry?.parameters;
|
|
1898
|
+
if (gp && (gp.width !== cfg.w || gp.height !== cfg.h)) {
|
|
1899
|
+
handle.geometry.dispose();
|
|
1900
|
+
handle.geometry = new THREE.PlaneGeometry(cfg.w, cfg.h);
|
|
1901
|
+
}
|
|
1902
|
+
if (nodeEntry.model.contentType === 'image' && handle.material) {
|
|
1903
|
+
handle.material.opacity = 0.0;
|
|
1904
|
+
handle.material.colorWrite = false;
|
|
1905
|
+
handle.material.needsUpdate = true;
|
|
1906
|
+
}
|
|
1907
|
+
});
|
|
1908
|
+
// ▲▲▲ [Fix] ▲▲▲
|
|
1909
|
+
}
|
|
1910
|
+
|
|
1911
|
+
if (debounceTimers.has(nodeId)) {
|
|
1912
|
+
clearTimeout(debounceTimers.get(nodeId));
|
|
1913
|
+
debounceTimers.delete(nodeId);
|
|
1914
|
+
}
|
|
1915
|
+
if (resizeThrottleTimers.has(nodeId)) {
|
|
1916
|
+
clearTimeout(resizeThrottleTimers.get(nodeId));
|
|
1917
|
+
resizeThrottleTimers.delete(nodeId);
|
|
1918
|
+
}
|
|
1919
|
+
|
|
1920
|
+
if (nodeEntry.currentResizeType === 'CSS') {
|
|
1921
|
+
console.log(`[stopResizing] CSS type - queuing texture update for ${nodeId}`);
|
|
1922
|
+
|
|
1923
|
+
// ▼▼▼ [Fix] CSS3D 노드의 최종 크기를 resolutionScale 고려하여 설정 ▼▼▼
|
|
1924
|
+
if (nodeEntry.cssObject && nodeEntry.cssObject.element) {
|
|
1925
|
+
const wrapper = nodeEntry.cssObject.element;
|
|
1926
|
+
const resolutionScale = nodeEntry.cssObject.userData?.resolutionScale || 1;
|
|
1927
|
+
const domWidth = nodeEntry.model.width * resolutionScale;
|
|
1928
|
+
const domHeight = nodeEntry.model.height * resolutionScale;
|
|
1929
|
+
|
|
1930
|
+
wrapper.style.width = domWidth + 'px';
|
|
1931
|
+
wrapper.style.height = domHeight + 'px';
|
|
1932
|
+
wrapper.style.minWidth = domWidth + 'px';
|
|
1933
|
+
wrapper.style.minHeight = domHeight + 'px';
|
|
1934
|
+
wrapper.style.maxWidth = domWidth + 'px';
|
|
1935
|
+
wrapper.style.maxHeight = domHeight + 'px';
|
|
1936
|
+
|
|
1937
|
+
const innerContent = wrapper.firstElementChild;
|
|
1938
|
+
if (innerContent) {
|
|
1939
|
+
innerContent.style.width = nodeEntry.model.width + 'px';
|
|
1940
|
+
innerContent.style.height = nodeEntry.model.height + 'px';
|
|
1941
|
+
innerContent.style.minWidth = nodeEntry.model.width + 'px';
|
|
1942
|
+
innerContent.style.minHeight = nodeEntry.model.height + 'px';
|
|
1943
|
+
innerContent.style.maxWidth = nodeEntry.model.width + 'px';
|
|
1944
|
+
innerContent.style.maxHeight = nodeEntry.model.height + 'px';
|
|
1945
|
+
}
|
|
1946
|
+
|
|
1947
|
+
// userData 업데이트
|
|
1948
|
+
nodeEntry.cssObject.userData.worldWidth = nodeEntry.model.width;
|
|
1949
|
+
nodeEntry.cssObject.userData.worldHeight = nodeEntry.model.height;
|
|
1950
|
+
}
|
|
1951
|
+
// ▲▲▲ [Fix] ▲▲▲
|
|
1952
|
+
|
|
1953
|
+
if (window.MindMapCss3DManager?.syncCss3dContent && nodeEntry.cssObject) {
|
|
1954
|
+
try {
|
|
1955
|
+
window.MindMapCss3DManager.syncCss3dContent(nodeEntry.model, nodeEntry.cssObject);
|
|
1956
|
+
} catch (error) {
|
|
1957
|
+
console.warn(`[stopResizing] Failed to sync CSS3D content after resize for ${nodeId}`, error);
|
|
1958
|
+
}
|
|
1959
|
+
}
|
|
1960
|
+
|
|
1961
|
+
window.MindMapTextOverlayV2?.updateNodePlacement?.(module, nodeId);
|
|
1962
|
+
window.MindMapTextOverlayV2?.invalidateNode?.(module, nodeId, 'layout');
|
|
1963
|
+
module._overlayDirty = true;
|
|
1964
|
+
module._lastOverlayKey = '';
|
|
1965
|
+
|
|
1966
|
+
const textarea = nodeEntry.cssObject.element.querySelector('textarea');
|
|
1967
|
+
if (textarea) {
|
|
1968
|
+
window.MindMapNodes.pendingNodeUpdates.set(nodeId, {
|
|
1969
|
+
content: textarea.value,
|
|
1970
|
+
width: nodeEntry.model.width,
|
|
1971
|
+
height: nodeEntry.model.height
|
|
1972
|
+
});
|
|
1973
|
+
enqueueRender(module, nodeId);
|
|
1974
|
+
}
|
|
1975
|
+
} else if (nodeEntry.currentResizeType === 'GL') {
|
|
1976
|
+
// ▼▼▼ [Refactor] After resizing a chunk node, fetch content directly from C# ▼▼▼
|
|
1977
|
+
if (nodeEntry.model.isChunkedText && module.dotNetHelper) {
|
|
1978
|
+
try {
|
|
1979
|
+
const result = await module.dotNetHelper.invokeMethodAsync(
|
|
1980
|
+
'GetTextByScrollPosition',
|
|
1981
|
+
nodeId,
|
|
1982
|
+
nodeEntry.model.scrollOffset || 0,
|
|
1983
|
+
nodeEntry.model.height || 400,
|
|
1984
|
+
CHUNK_LINE_HEIGHT
|
|
1985
|
+
);
|
|
1986
|
+
|
|
1987
|
+
if (result) {
|
|
1988
|
+
nodeEntry.model._visibleContent = result.content;
|
|
1989
|
+
nodeEntry.model._visibleScrollOffset = result.relativeScrollOffset;
|
|
1990
|
+
|
|
1991
|
+
window.MindMapNodes.pendingNodeUpdates.set(nodeId, {
|
|
1992
|
+
content: result.content,
|
|
1993
|
+
width: nodeEntry.model.width,
|
|
1994
|
+
height: nodeEntry.model.height,
|
|
1995
|
+
scrollDelta: 0
|
|
1996
|
+
});
|
|
1997
|
+
enqueueRender(module, nodeId);
|
|
1998
|
+
}
|
|
1999
|
+
} catch (e) {
|
|
2000
|
+
console.error(`[Pipeline] GetTextByScrollPosition error on resize:`, e);
|
|
2001
|
+
}
|
|
2002
|
+
} else {
|
|
2003
|
+
// ▲▲▲ [Refactor] ▲▲▲
|
|
2004
|
+
// Regular nodes (non-chunked) + IMAGE NODES
|
|
2005
|
+
|
|
2006
|
+
// ▼▼▼ [Fix] 이미지 노드 리사이즈 시 기존 텍스처 유지 ▼▼▼
|
|
2007
|
+
if (nodeEntry.model.contentType === 'image') {
|
|
2008
|
+
// 이미지 노드는 URL이 변하지 않으므로 텍스처 재생성 불필요
|
|
2009
|
+
// geometry만 업데이트하고 기존 텍스처를 유지
|
|
2010
|
+
const bodyMesh = nodeEntry.glObject?.getObjectByName('body');
|
|
2011
|
+
if (bodyMesh && bodyMesh.material.map) {
|
|
2012
|
+
// 기존 텍스처 유지 - 아무것도 하지 않음
|
|
2013
|
+
console.log(`[stopResizing] Image node ${nodeId} - keeping existing texture`);
|
|
2014
|
+
} else {
|
|
2015
|
+
console.warn(`[stopResizing] Image node ${nodeId} - no texture found, may need reload`);
|
|
2016
|
+
}
|
|
2017
|
+
} else {
|
|
2018
|
+
// [Fix] Ensure regular nodes have content for texture generation
|
|
2019
|
+
const content = nodeEntry.model.response;
|
|
2020
|
+
|
|
2021
|
+
console.log(`[stopResizing] GL type - queuing texture update for ${nodeId}, size ${nodeEntry.model.width}x${nodeEntry.model.height}`);
|
|
2022
|
+
window.MindMapNodes.pendingNodeUpdates.set(nodeId, {
|
|
2023
|
+
content: content,
|
|
2024
|
+
width: nodeEntry.model.width,
|
|
2025
|
+
height: nodeEntry.model.height
|
|
2026
|
+
});
|
|
2027
|
+
enqueueRender(module, nodeId);
|
|
2028
|
+
}
|
|
2029
|
+
// ▲▲▲ [Fix] ▲▲▲
|
|
2030
|
+
}
|
|
2031
|
+
}
|
|
2032
|
+
|
|
2033
|
+
window.MindMapNodes.updateNodeSpatialGrid(module, nodeId);
|
|
2034
|
+
|
|
2035
|
+
// ▼▼▼ [Fix] 리사이즈 종료 후 CSS transition 복원 ▼▼▼
|
|
2036
|
+
if (nodeEntry.cssObject && nodeEntry.cssObject.element) {
|
|
2037
|
+
const wrapper = nodeEntry.cssObject.element;
|
|
2038
|
+
wrapper.style.transition = CSS3D_WRAPPER_TRANSITION;
|
|
2039
|
+
wrapper.style.webkitTransition = CSS3D_WRAPPER_WEBKIT_TRANSITION;
|
|
2040
|
+
const inner = wrapper.firstElementChild;
|
|
2041
|
+
if (inner) {
|
|
2042
|
+
inner.style.transition = 'none';
|
|
2043
|
+
inner.style.webkitTransition = 'none';
|
|
2044
|
+
}
|
|
2045
|
+
}
|
|
2046
|
+
// ▲▲▲ [Fix] ▲▲▲
|
|
2047
|
+
}
|
|
2048
|
+
|
|
2049
|
+
module.isResizing = false;
|
|
2050
|
+
module.resizeNodeObject = null;
|
|
2051
|
+
module.resizingNodeId = null; // ▼▼▼ [Changed] clear resizingNodeId ▼▼▼
|
|
2052
|
+
window.MindMapInteractions?.setBrowserSelectionLock?.(false, 'resize');
|
|
2053
|
+
document.body.classList.remove('is-resizing');
|
|
2054
|
+
|
|
2055
|
+
// ▼▼▼ [Fix] Reset cursor after resizing ▼▼▼
|
|
2056
|
+
document.body.style.cursor = '';
|
|
2057
|
+
// ▲▲▲ [Fix] ▲▲▲
|
|
2058
|
+
}
|
|
2059
|
+
|
|
2060
|
+
// Expose MindMapPipeline globally
|
|
2061
|
+
window.MindMapPipeline = {
|
|
2062
|
+
processRenderQueue: processRenderQueue,
|
|
2063
|
+
debouncedProcessRenderQueue: debouncedProcessRenderQueue,
|
|
2064
|
+
throttledScrollUpdate: throttledScrollUpdate,
|
|
2065
|
+
setScrollPosition: setScrollPosition, // [New] for scrollbar dragging
|
|
2066
|
+
throttledProcessRenderQueueDuringResize: throttledProcessRenderQueueDuringResize,
|
|
2067
|
+
startResizing: startResizing,
|
|
2068
|
+
doResizing: doResizing,
|
|
2069
|
+
stopResizing: stopResizing,
|
|
2070
|
+
throttledUpdateScrollInCSharp: throttledUpdateScrollInCSharp
|
|
2071
|
+
};
|
|
2072
|
+
|
|
2073
|
+
console.log('? mind-map-pipeline.js loaded');
|
|
2074
|
+
})();
|
|
2075
|
+
|