@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,1258 @@
|
|
|
1
|
+
(function () {
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const tailTextureCache = new Map();
|
|
5
|
+
const renderers = new Map();
|
|
6
|
+
let sharedMeasurer = null;
|
|
7
|
+
|
|
8
|
+
function getSharedMeasurer() {
|
|
9
|
+
if (sharedMeasurer) return sharedMeasurer;
|
|
10
|
+
sharedMeasurer = document.createElement('div');
|
|
11
|
+
Object.assign(sharedMeasurer.style, {
|
|
12
|
+
position: 'absolute',
|
|
13
|
+
visibility: 'hidden',
|
|
14
|
+
left: '-9999px',
|
|
15
|
+
top: '-9999px',
|
|
16
|
+
zIndex: '-1000',
|
|
17
|
+
pointerEvents: 'none',
|
|
18
|
+
margin: '0',
|
|
19
|
+
padding: '0',
|
|
20
|
+
boxSizing: 'border-box',
|
|
21
|
+
width: 'auto'
|
|
22
|
+
});
|
|
23
|
+
document.body.appendChild(sharedMeasurer);
|
|
24
|
+
return sharedMeasurer;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function preprocessMarkdown(text) {
|
|
28
|
+
if (!text) return '';
|
|
29
|
+
let processed = text.trim();
|
|
30
|
+
processed = processed.replace(/([).]*)(\*\*)(?=[가-힣])/g, '$1 $2');
|
|
31
|
+
processed = processed.replace(/([^\n])\n([\*]{1,2}[\d]+\.|[\d]+\.)/g, '$1\n\n$2');
|
|
32
|
+
processed = processed.replace(/([^\n])\n([-*] )/g, '$1\n\n$2');
|
|
33
|
+
return processed;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const ICON_SPINNER_SVG = `
|
|
37
|
+
<svg viewBox="0 0 512 512" style="width:1em; height:1em; fill:currentColor; vertical-align:-0.125em;">
|
|
38
|
+
<path d="M304 48a48 48 0 1 0 -96 0 48 48 0 1 0 96 0zm0 416a48 48 0 1 0 -96 0 48 48 0 1 0 96 0zM48 304a48 48 0 1 0 0-96 48 48 0 1 0 0 96zm464-48a48 48 0 1 0 -96 0 48 48 0 1 0 96 0zM142.9 437A48 48 0 1 0 75 369.1 48 48 0 1 0 142.9 437zm0-294.2A48 48 0 1 0 75 75a48 48 0 1 0 67.9 67.9zM369.1 437A48 48 0 1 0 437 369.1 48 48 0 1 0 369.1 437z"/>
|
|
39
|
+
</svg>`;
|
|
40
|
+
|
|
41
|
+
const BROKEN_IMAGE_ICON_SVG = `
|
|
42
|
+
<svg viewBox="0 0 24 24" style="width:100%; height:100%; fill:none; stroke:#9ca3af; stroke-width:2;">
|
|
43
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
|
|
44
|
+
</svg>`;
|
|
45
|
+
const BROKEN_IMAGE_ICON_DATA_URL = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(BROKEN_IMAGE_ICON_SVG)}`;
|
|
46
|
+
const brokenImageIcon = new Image();
|
|
47
|
+
brokenImageIcon.src = BROKEN_IMAGE_ICON_DATA_URL;
|
|
48
|
+
|
|
49
|
+
function createTailTexture(color, borderColor, zoomLevel, tailSize, isSelected) {
|
|
50
|
+
const key = `${color}-${borderColor}-${zoomLevel}-${isSelected}`;
|
|
51
|
+
if (tailTextureCache.has(key)) return tailTextureCache.get(key);
|
|
52
|
+
|
|
53
|
+
const borderWidth = (isSelected ? 4 : 3) * zoomLevel;
|
|
54
|
+
const tailHeight = tailSize * zoomLevel;
|
|
55
|
+
const tailWidth = tailHeight * 2;
|
|
56
|
+
const canvas = document.createElement('canvas');
|
|
57
|
+
canvas.width = tailWidth + borderWidth * 2;
|
|
58
|
+
canvas.height = tailHeight + borderWidth;
|
|
59
|
+
const ctx = canvas.getContext('2d');
|
|
60
|
+
|
|
61
|
+
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
62
|
+
ctx.fillStyle = color;
|
|
63
|
+
ctx.beginPath();
|
|
64
|
+
ctx.moveTo(borderWidth, borderWidth / 2);
|
|
65
|
+
ctx.lineTo(canvas.width / 2, canvas.height - borderWidth / 2);
|
|
66
|
+
ctx.lineTo(canvas.width - borderWidth, borderWidth / 2);
|
|
67
|
+
ctx.closePath();
|
|
68
|
+
ctx.fill();
|
|
69
|
+
|
|
70
|
+
ctx.strokeStyle = borderColor;
|
|
71
|
+
ctx.lineWidth = borderWidth;
|
|
72
|
+
ctx.beginPath();
|
|
73
|
+
ctx.moveTo(borderWidth, borderWidth / 2);
|
|
74
|
+
ctx.lineTo(canvas.width / 2, canvas.height - borderWidth / 2);
|
|
75
|
+
ctx.lineTo(canvas.width - borderWidth, borderWidth / 2);
|
|
76
|
+
ctx.stroke();
|
|
77
|
+
|
|
78
|
+
const texture = new THREE.CanvasTexture(canvas);
|
|
79
|
+
texture.generateMipmaps = true;
|
|
80
|
+
texture.minFilter = THREE.LinearMipmapLinearFilter;
|
|
81
|
+
texture.magFilter = THREE.LinearFilter;
|
|
82
|
+
tailTextureCache.set(key, texture);
|
|
83
|
+
return texture;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function createGlowTexture(width, height, radius, zoomLevel) {
|
|
87
|
+
// ▼▼▼ [수정] glow는 블러 효과이므로 zoomLevel 2로 고정하여 크기 일정하게 유지 ▼▼▼
|
|
88
|
+
const GLOW_ZOOM = 2;
|
|
89
|
+
// 입력된 width/height가 zoomLevel 기준이므로, GLOW_ZOOM 기준으로 스케일 조정
|
|
90
|
+
const scaleFactor = GLOW_ZOOM / zoomLevel;
|
|
91
|
+
const scaledWidth = width * scaleFactor;
|
|
92
|
+
const scaledHeight = height * scaleFactor;
|
|
93
|
+
const scaledRadius = radius * scaleFactor;
|
|
94
|
+
|
|
95
|
+
const glowPadding = 30 * GLOW_ZOOM;
|
|
96
|
+
const canvasWidth = scaledWidth + (glowPadding * 2);
|
|
97
|
+
const canvasHeight = scaledHeight + (glowPadding * 2);
|
|
98
|
+
// ▲▲▲ [수정] ▲▲▲
|
|
99
|
+
|
|
100
|
+
const canvas = document.createElement('canvas');
|
|
101
|
+
canvas.width = canvasWidth;
|
|
102
|
+
canvas.height = canvasHeight;
|
|
103
|
+
const ctx = canvas.getContext('2d');
|
|
104
|
+
|
|
105
|
+
ctx.shadowColor = 'white';
|
|
106
|
+
ctx.shadowBlur = 25 * GLOW_ZOOM;
|
|
107
|
+
ctx.shadowOffsetX = 0;
|
|
108
|
+
ctx.shadowOffsetY = 0;
|
|
109
|
+
ctx.fillStyle = 'white';
|
|
110
|
+
const shrink = 3 * GLOW_ZOOM;
|
|
111
|
+
drawRoundedRect(ctx, glowPadding + shrink, glowPadding + shrink, scaledWidth - (shrink * 2), scaledHeight - (shrink * 2), Math.max(0, scaledRadius - shrink));
|
|
112
|
+
ctx.fill();
|
|
113
|
+
|
|
114
|
+
const texture = new THREE.CanvasTexture(canvas);
|
|
115
|
+
texture.minFilter = THREE.LinearFilter;
|
|
116
|
+
texture.magFilter = THREE.LinearFilter;
|
|
117
|
+
|
|
118
|
+
return { texture, width: canvasWidth, height: canvasHeight, padding: glowPadding };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function drawRoundedRect(ctx, x, y, width, height, radius) {
|
|
122
|
+
ctx.beginPath();
|
|
123
|
+
ctx.moveTo(x + radius, y);
|
|
124
|
+
ctx.lineTo(x + width - radius, y);
|
|
125
|
+
ctx.arcTo(x + width, y, x + width, y + radius, radius);
|
|
126
|
+
ctx.lineTo(x + width, y + height - radius);
|
|
127
|
+
ctx.arcTo(x + width, y + height, x + width - radius, y + height, radius);
|
|
128
|
+
ctx.lineTo(x + radius, y + height);
|
|
129
|
+
ctx.lineTo(x, y + height - radius);
|
|
130
|
+
ctx.lineTo(x, y + radius);
|
|
131
|
+
ctx.arcTo(x, y, x + radius, y, radius);
|
|
132
|
+
ctx.closePath();
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// ▼▼▼ [New] Fully rounded rect for image nodes (all corners rounded) ▼▼▼
|
|
136
|
+
function drawFullyRoundedRect(ctx, x, y, width, height, radius) {
|
|
137
|
+
ctx.beginPath();
|
|
138
|
+
ctx.moveTo(x + radius, y);
|
|
139
|
+
ctx.lineTo(x + width - radius, y);
|
|
140
|
+
ctx.arcTo(x + width, y, x + width, y + radius, radius);
|
|
141
|
+
ctx.lineTo(x + width, y + height - radius);
|
|
142
|
+
ctx.arcTo(x + width, y + height, x + width - radius, y + height, radius);
|
|
143
|
+
ctx.lineTo(x + radius, y + height);
|
|
144
|
+
ctx.arcTo(x, y + height, x, y + height - radius, radius); // Bottom-left corner (now rounded)
|
|
145
|
+
ctx.lineTo(x, y + radius);
|
|
146
|
+
ctx.arcTo(x, y, x + radius, y, radius);
|
|
147
|
+
ctx.closePath();
|
|
148
|
+
}
|
|
149
|
+
// ▲▲▲ [New] ▲▲▲
|
|
150
|
+
|
|
151
|
+
function drawScrollbar(ctx, nodeModel, styleCanvasWidth, styleCanvasHeight, zoomLevel, isHovered = false) {
|
|
152
|
+
if (!nodeModel.maxScroll || nodeModel.maxScroll <= 1) return;
|
|
153
|
+
|
|
154
|
+
const scrollbarWidth = 6 * zoomLevel;
|
|
155
|
+
const scrollbarPadding = 4 * zoomLevel;
|
|
156
|
+
const trackColor = 'rgba(0, 0, 0, 0.05)';
|
|
157
|
+
|
|
158
|
+
const thumbColor = isHovered ? 'rgba(0, 0, 0, 0.5)' : 'rgba(0, 0, 0, 0.25)';
|
|
159
|
+
const thumbRadius = 3 * zoomLevel;
|
|
160
|
+
const minThumbHeight = 24 * zoomLevel;
|
|
161
|
+
|
|
162
|
+
const topMargin = (nodeModel.contentType === 'note' ? 20 : 16) * zoomLevel;
|
|
163
|
+
const bottomMargin = 16 * zoomLevel;
|
|
164
|
+
|
|
165
|
+
const trackX = styleCanvasWidth - scrollbarWidth - scrollbarPadding;
|
|
166
|
+
const trackY = topMargin;
|
|
167
|
+
const trackHeight = styleCanvasHeight - topMargin - bottomMargin;
|
|
168
|
+
|
|
169
|
+
if (trackHeight <= minThumbHeight) return;
|
|
170
|
+
|
|
171
|
+
ctx.fillStyle = trackColor;
|
|
172
|
+
drawRoundedRect(ctx, trackX, trackY, scrollbarWidth, trackHeight, thumbRadius);
|
|
173
|
+
ctx.fill();
|
|
174
|
+
|
|
175
|
+
const topPaddingContent = (nodeModel.contentType === 'note' ? 16 : 12) * zoomLevel;
|
|
176
|
+
const bottomPaddingContent = 16 * zoomLevel;
|
|
177
|
+
const borderWidth = 3 * zoomLevel;
|
|
178
|
+
const visibleContentHeight = styleCanvasHeight - (topPaddingContent + borderWidth) - (bottomPaddingContent + borderWidth);
|
|
179
|
+
|
|
180
|
+
let totalContentHeight;
|
|
181
|
+
if (nodeModel.isChunkedText && nodeModel.totalContentHeight > 0) {
|
|
182
|
+
totalContentHeight = nodeModel.totalContentHeight * zoomLevel;
|
|
183
|
+
} else {
|
|
184
|
+
totalContentHeight = visibleContentHeight + (nodeModel.maxScroll * zoomLevel);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
let thumbHeight = (visibleContentHeight / totalContentHeight) * trackHeight;
|
|
188
|
+
thumbHeight = Math.max(minThumbHeight, Math.min(trackHeight, thumbHeight));
|
|
189
|
+
|
|
190
|
+
const currentScroll = (nodeModel.scrollOffset || 0) * zoomLevel;
|
|
191
|
+
const maxScrollWorld = nodeModel.maxScroll * zoomLevel;
|
|
192
|
+
|
|
193
|
+
let scrollRatio = currentScroll / maxScrollWorld;
|
|
194
|
+
scrollRatio = Math.max(0, Math.min(1, isNaN(scrollRatio) ? 0 : scrollRatio));
|
|
195
|
+
|
|
196
|
+
const availableTrackSpace = trackHeight - thumbHeight;
|
|
197
|
+
const thumbY = trackY + (availableTrackSpace * scrollRatio);
|
|
198
|
+
|
|
199
|
+
ctx.fillStyle = thumbColor;
|
|
200
|
+
drawRoundedRect(ctx, trackX, thumbY, scrollbarWidth, thumbHeight, thumbRadius);
|
|
201
|
+
ctx.fill();
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function getBodyColors(contentType, isSelected) {
|
|
205
|
+
return {
|
|
206
|
+
fill: '#fefefe',
|
|
207
|
+
stroke: isSelected ? '#2563eb' : 'rgba(55, 65, 81, 0.9)'
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function drawNodeBodyBackground(ctx, canvasWidth, canvasHeight, zoomLevel, isSelected, contentType = 'text', nodeModel = null) {
|
|
212
|
+
const radius = 12 * zoomLevel;
|
|
213
|
+
const borderWidth = 3 * zoomLevel;
|
|
214
|
+
const halfBorder = borderWidth / 2;
|
|
215
|
+
|
|
216
|
+
const x = halfBorder;
|
|
217
|
+
const y = halfBorder;
|
|
218
|
+
const w = canvasWidth - borderWidth;
|
|
219
|
+
const h = canvasHeight - borderWidth;
|
|
220
|
+
const right = x + w;
|
|
221
|
+
const bottom = y + h;
|
|
222
|
+
|
|
223
|
+
ctx.clearRect(0, 0, canvasWidth, canvasHeight);
|
|
224
|
+
ctx.beginPath();
|
|
225
|
+
ctx.moveTo(x + radius, y);
|
|
226
|
+
ctx.lineTo(right - radius, y);
|
|
227
|
+
ctx.quadraticCurveTo(right, y, right, y + radius);
|
|
228
|
+
ctx.lineTo(right, bottom - radius);
|
|
229
|
+
ctx.quadraticCurveTo(right, bottom, right - radius, bottom);
|
|
230
|
+
ctx.lineTo(x + radius, bottom);
|
|
231
|
+
ctx.quadraticCurveTo(x, bottom, x, bottom - radius);
|
|
232
|
+
ctx.lineTo(x, y + radius);
|
|
233
|
+
ctx.quadraticCurveTo(x, y, x + radius, y);
|
|
234
|
+
ctx.closePath();
|
|
235
|
+
|
|
236
|
+
const { fill, stroke } = getBodyColors(contentType, isSelected);
|
|
237
|
+
ctx.fillStyle = fill;
|
|
238
|
+
ctx.fill();
|
|
239
|
+
|
|
240
|
+
ctx.strokeStyle = stroke;
|
|
241
|
+
ctx.lineWidth = borderWidth;
|
|
242
|
+
ctx.stroke();
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function drawNodeBodyOnCanvas(ctx, nodeModel, canvasWidth, canvasHeight, zoomLevel, isSelected) {
|
|
246
|
+
drawNodeBodyBackground(ctx, canvasWidth, canvasHeight, zoomLevel, isSelected, nodeModel.contentType, nodeModel);
|
|
247
|
+
ctx.save();
|
|
248
|
+
|
|
249
|
+
const BORDER_WIDTH = 3 * zoomLevel;
|
|
250
|
+
const CSS_PADDING = 16 * zoomLevel;
|
|
251
|
+
const totalPadding = CSS_PADDING + BORDER_WIDTH;
|
|
252
|
+
const topPadding = ((nodeModel.contentType === 'note' ? 16 : 12) * zoomLevel) + BORDER_WIDTH;
|
|
253
|
+
const paddingBottom = 16 * zoomLevel;
|
|
254
|
+
const textWidth = canvasWidth - totalPadding * 2;
|
|
255
|
+
const lineHeight = 14 * zoomLevel * 1.5;
|
|
256
|
+
let currentY = topPadding;
|
|
257
|
+
|
|
258
|
+
ctx.textBaseline = 'top';
|
|
259
|
+
// 기본 Prompt 값들은 표시하지 않음
|
|
260
|
+
const defaultPrompts = ['Note', 'Pasted Text', '에이전트 메모'];
|
|
261
|
+
const nodePrompt = nodeModel.prompt ?? nodeModel.Prompt ?? '';
|
|
262
|
+
const nodeResponse = nodeModel.response ?? nodeModel.Response ?? '';
|
|
263
|
+
const shouldShowPrompt = nodePrompt && !defaultPrompts.includes(nodePrompt);
|
|
264
|
+
if (shouldShowPrompt) {
|
|
265
|
+
ctx.fillStyle = '#111827';
|
|
266
|
+
ctx.font = `bold ${14 * zoomLevel}px sans-serif`;
|
|
267
|
+
ctx.fillText(nodePrompt, totalPadding, currentY, textWidth);
|
|
268
|
+
currentY += 24 * zoomLevel;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
ctx.fillStyle = '#1f2937';
|
|
272
|
+
ctx.font = `${14 * zoomLevel}px sans-serif`;
|
|
273
|
+
const lines = (nodeResponse || '').split('\n');
|
|
274
|
+
for (const line of lines) {
|
|
275
|
+
ctx.fillText(line, totalPadding, currentY, textWidth);
|
|
276
|
+
currentY += lineHeight;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
ctx.restore();
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function normalizeCachedImage(cachedImage) {
|
|
283
|
+
if (!cachedImage) return { image: null, width: 0, height: 0, isError: false };
|
|
284
|
+
|
|
285
|
+
const isError = cachedImage.isError === true;
|
|
286
|
+
|
|
287
|
+
if (cachedImage.image) {
|
|
288
|
+
const img = cachedImage.image;
|
|
289
|
+
const width = cachedImage.width || img.naturalWidth || img.width || 0;
|
|
290
|
+
const height = cachedImage.height || img.naturalHeight || img.height || 0;
|
|
291
|
+
return { image: img, width, height, isError };
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const img = cachedImage;
|
|
295
|
+
const width = img.naturalWidth || img.width || 0;
|
|
296
|
+
const height = img.naturalHeight || img.height || 0;
|
|
297
|
+
return { image: img, width, height, isError };
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function drawCachedImageBody(cachedImage, ctx, nodeModel, canvasWidth, canvasHeight, zoomLevel, isSelected) {
|
|
301
|
+
const radius = 12 * zoomLevel;
|
|
302
|
+
ctx.save();
|
|
303
|
+
drawRoundedRect(ctx, 0, 0, canvasWidth, canvasHeight, radius);
|
|
304
|
+
ctx.clip();
|
|
305
|
+
ctx.fillStyle = '#fefefe';
|
|
306
|
+
ctx.fillRect(0, 0, canvasWidth, canvasHeight);
|
|
307
|
+
|
|
308
|
+
const { image, width: imgWidth, height: imgHeight, isError } = normalizeCachedImage(cachedImage);
|
|
309
|
+
|
|
310
|
+
if (image && imgWidth > 0 && imgHeight > 0) {
|
|
311
|
+
const canvasAspect = canvasWidth / canvasHeight;
|
|
312
|
+
const imgAspect = imgWidth / imgHeight;
|
|
313
|
+
let sx = 0, sy = 0, sWidth = imgWidth, sHeight = imgHeight;
|
|
314
|
+
if (imgAspect > canvasAspect) {
|
|
315
|
+
sWidth = imgHeight * canvasAspect;
|
|
316
|
+
sx = (imgWidth - sWidth) / 2;
|
|
317
|
+
} else {
|
|
318
|
+
sHeight = imgWidth / canvasAspect;
|
|
319
|
+
sy = (imgHeight - sHeight) / 2;
|
|
320
|
+
}
|
|
321
|
+
ctx.drawImage(image, sx, sy, sWidth, sHeight, 0, 0, canvasWidth, canvasHeight);
|
|
322
|
+
}
|
|
323
|
+
else if (isError) {
|
|
324
|
+
ctx.fillStyle = '#f3f4f6';
|
|
325
|
+
ctx.fillRect(0, 0, canvasWidth, canvasHeight);
|
|
326
|
+
ctx.fillStyle = '#9ca3af';
|
|
327
|
+
ctx.font = `bold ${16 * zoomLevel}px sans-serif`;
|
|
328
|
+
ctx.textAlign = 'center';
|
|
329
|
+
ctx.textBaseline = 'middle';
|
|
330
|
+
const textY = canvasHeight * 0.55;
|
|
331
|
+
ctx.fillText('Image Not Found', canvasWidth / 2, textY);
|
|
332
|
+
|
|
333
|
+
if (brokenImageIcon.complete) {
|
|
334
|
+
const iconSize = Math.min(canvasWidth, canvasHeight) * 0.4;
|
|
335
|
+
ctx.drawImage(brokenImageIcon, (canvasWidth - iconSize) / 2, (canvasHeight - iconSize) / 2 - (8 * zoomLevel), iconSize, iconSize);
|
|
336
|
+
} else {
|
|
337
|
+
ctx.strokeStyle = '#9ca3af';
|
|
338
|
+
ctx.lineWidth = 2 * zoomLevel;
|
|
339
|
+
ctx.beginPath();
|
|
340
|
+
ctx.moveTo(canvasWidth * 0.3, canvasHeight * 0.3);
|
|
341
|
+
ctx.lineTo(canvasWidth * 0.7, canvasHeight * 0.7);
|
|
342
|
+
ctx.moveTo(canvasWidth * 0.7, canvasHeight * 0.3);
|
|
343
|
+
ctx.lineTo(canvasWidth * 0.3, canvasHeight * 0.7);
|
|
344
|
+
ctx.stroke();
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
ctx.strokeStyle = '#ef4444';
|
|
348
|
+
ctx.setLineDash([5 * zoomLevel, 5 * zoomLevel]);
|
|
349
|
+
ctx.lineWidth = 2 * zoomLevel;
|
|
350
|
+
ctx.strokeRect(0, 0, canvasWidth, canvasHeight);
|
|
351
|
+
ctx.setLineDash([]);
|
|
352
|
+
}
|
|
353
|
+
else {
|
|
354
|
+
ctx.fillStyle = '#f9fafb';
|
|
355
|
+
ctx.fillRect(0, 0, canvasWidth, canvasHeight);
|
|
356
|
+
ctx.fillStyle = '#6b7280';
|
|
357
|
+
ctx.font = `${14 * zoomLevel}px sans-serif`;
|
|
358
|
+
ctx.textAlign = 'center';
|
|
359
|
+
ctx.textBaseline = 'middle';
|
|
360
|
+
ctx.fillText('Loading...', canvasWidth / 2, canvasHeight / 2);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
const borderWidth = 3 * zoomLevel;
|
|
364
|
+
ctx.strokeStyle = isSelected ? '#2563eb' : 'rgba(55, 65, 81, 0.9)';
|
|
365
|
+
ctx.lineWidth = borderWidth;
|
|
366
|
+
drawFullyRoundedRect(ctx, borderWidth / 2, borderWidth / 2, canvasWidth - borderWidth, canvasHeight - borderWidth, radius - borderWidth / 2);
|
|
367
|
+
ctx.stroke();
|
|
368
|
+
ctx.restore();
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function getMarkdownStyles(zoomLevel, contentType = 'text') {
|
|
372
|
+
const z = zoomLevel;
|
|
373
|
+
const fontStack = "ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif";
|
|
374
|
+
const baseFontSize = 14 * z;
|
|
375
|
+
const baseLineHeight = 1.5;
|
|
376
|
+
const styles = `
|
|
377
|
+
div { color: #1f2937; line-height: ${baseLineHeight}; font-family: ${fontStack}; font-size: ${baseFontSize}px; margin: 0; padding: 0; }
|
|
378
|
+
p { margin: 0 0 ${8 * z}px 0; }
|
|
379
|
+
h1, h2, h3, h4 { font-size: ${16 * z}px; font-weight: 700; margin: ${12 * z}px 0 ${6 * z}px 0; color: #111827; }
|
|
380
|
+
ul, ol { margin: ${6 * z}px 0 ${10 * z}px ${20 * z}px; padding: 0; }
|
|
381
|
+
li { margin-bottom: ${4 * z}px; }
|
|
382
|
+
.response > *:first-child { margin-top: 0 !important; }
|
|
383
|
+
.response > *:last-child { margin-bottom: 0 !important; }
|
|
384
|
+
li:last-child { margin-bottom: 0 !important; }
|
|
385
|
+
strong, b { font-weight: 700; color: #111827; }
|
|
386
|
+
.prompt { font-weight: 700; color: #111827; margin: 0 0 ${8 * z}px 0; padding-bottom: 0; display: flex; align-items: center; font-size: ${baseFontSize}px; }
|
|
387
|
+
.icon-wrapper { display: inline-flex; align-items: center; justify-content: center; width: ${16 * z}px; height: ${16 * z}px; color: #4b5563; }
|
|
388
|
+
hr.node-separator { border: 0; border-top: ${1 * z}px solid #e5e7eb; margin: ${8 * z}px 0 ${12 * z}px 0; }
|
|
389
|
+
.thinking { color: #6b7280; font-style: italic; display: flex; align-items: center; gap: ${6 * z}px; }
|
|
390
|
+
.response { margin: 0; }
|
|
391
|
+
code { background-color: #e5e7eb; padding: ${2 * z}px ${4 * z}px; border-radius: ${4 * z}px; font-family: Consolas, 'Courier New', monospace; font-size: ${13 * z}px; }
|
|
392
|
+
pre { background-color: #e5e7eb; padding: ${8 * z}px; border-radius: ${8 * z}px; overflow-x: auto; margin: ${8 * z}px 0; font-family: Consolas, 'Courier New', monospace; }
|
|
393
|
+
pre code { background-color: transparent; padding: 0; border-radius: 0; }
|
|
394
|
+
blockquote { border-left: ${4 * z}px solid #d1d5db; padding-left: ${12 * z}px; margin: ${8 * z}px 0; color: #4b5563; }
|
|
395
|
+
table { border-collapse: collapse; width: 100%; margin: ${8 * z}px 0; font-size: ${13 * z}px; }
|
|
396
|
+
th, td { border: ${1 * z}px solid #d1d5db; padding: ${6 * z}px ${8 * z}px; text-align: left; }
|
|
397
|
+
th { background-color: #f3f4f6; font-weight: 600; color: #111827; }
|
|
398
|
+
tr:nth-child(even) { background-color: #f9fafb; }
|
|
399
|
+
`;
|
|
400
|
+
return { styles, fontStack };
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
function escapeHtml(value) {
|
|
404
|
+
return (value || '').replace(/[&<>\"]/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"' }[c]));
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
const ALLOWED_HTML_TAGS = new Set([
|
|
408
|
+
'div', 'span', 'p', 'br', 'hr', 'a', 'b', 'i', 'u', 's', 'em', 'strong', 'mark',
|
|
409
|
+
'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
|
|
410
|
+
'ul', 'ol', 'li', 'dl', 'dt', 'dd',
|
|
411
|
+
'table', 'thead', 'tbody', 'tfoot', 'tr', 'th', 'td', 'caption', 'colgroup', 'col',
|
|
412
|
+
'pre', 'code', 'blockquote', 'q', 'cite',
|
|
413
|
+
'sub', 'sup', 'small', 'big', 'abbr', 'acronym', 'ins', 'del',
|
|
414
|
+
'input', 'label', 'button', 'select', 'option', 'textarea',
|
|
415
|
+
'figure', 'figcaption', 'article', 'section', 'aside', 'header', 'footer', 'nav', 'main',
|
|
416
|
+
'details', 'summary', 'time', 'address', 'wbr', 'kbd', 'samp', 'var', 'dfn'
|
|
417
|
+
]);
|
|
418
|
+
|
|
419
|
+
function sanitizeHtml(html) {
|
|
420
|
+
if (!html) return '';
|
|
421
|
+
|
|
422
|
+
let sanitized = html
|
|
423
|
+
.replace(/<(script|iframe|object|embed|meta|link|style)[^>]*>[\s\S]*?<\/\1>/gi, '')
|
|
424
|
+
.replace(/<(script|iframe|object|embed|meta|link)[^>]*\/?>/gi, '');
|
|
425
|
+
|
|
426
|
+
sanitized = sanitized.replace(/<img[^>]*\/?>/gi, '');
|
|
427
|
+
|
|
428
|
+
sanitized = sanitized.replace(/<([a-zA-Z][a-zA-Z0-9]*)\s*([^>]*)>/g, (match, tagName, attrs) => {
|
|
429
|
+
if (ALLOWED_HTML_TAGS.has(tagName.toLowerCase())) {
|
|
430
|
+
return match;
|
|
431
|
+
}
|
|
432
|
+
return `<${tagName}${attrs ? ' ' + attrs : ''}>`;
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
sanitized = sanitized.replace(/<\/([a-zA-Z][a-zA-Z0-9]*)>/g, (match, tagName) => {
|
|
436
|
+
if (ALLOWED_HTML_TAGS.has(tagName.toLowerCase())) {
|
|
437
|
+
return match;
|
|
438
|
+
}
|
|
439
|
+
return `</${tagName}>`;
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
sanitized = sanitized
|
|
443
|
+
.replace(/<br\s*\/?>/gi, '<br/>')
|
|
444
|
+
.replace(/<hr\s*\/?>/gi, '<hr/>')
|
|
445
|
+
.replace(/<input([^>]*[^\/])>/gi, '<input$1 />');
|
|
446
|
+
|
|
447
|
+
sanitized = sanitized.replace(/\]\]>/g, ']]>');
|
|
448
|
+
|
|
449
|
+
sanitized = sanitized.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F]/g, '');
|
|
450
|
+
|
|
451
|
+
return sanitized;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
function renderMarkdownToCanvasAsync(ctx, texture, nodeModel, canvasWidth, canvasHeight, zoomLevel, isSelected, onComplete = null) {
|
|
455
|
+
const hasMarked = typeof window !== 'undefined' && window.marked && typeof window.marked.parse === 'function';
|
|
456
|
+
const promptValue = nodeModel.prompt || '';
|
|
457
|
+
// 기본 Prompt 값들은 표시하지 않음
|
|
458
|
+
const defaultPrompts = ['Note', 'Pasted Text', '에이전트 메모'];
|
|
459
|
+
const shouldShowPrompt = promptValue && !defaultPrompts.includes(promptValue);
|
|
460
|
+
// C#에서 PascalCase (IsLoading)로 전달될 수 있으므로 둘 다 확인
|
|
461
|
+
const nodeIsLoading = nodeModel.isLoading ?? nodeModel.IsLoading ?? false;
|
|
462
|
+
const nodeResponse = nodeModel.response ?? nodeModel.Response ?? '';
|
|
463
|
+
const isLoadingState = nodeIsLoading && !nodeResponse;
|
|
464
|
+
// note 타입에서도 프롬프트 표시 (CSS3D와 동일하게)
|
|
465
|
+
const showPromptNow = shouldShowPrompt;
|
|
466
|
+
let htmlContent = '';
|
|
467
|
+
|
|
468
|
+
if (showPromptNow) {
|
|
469
|
+
htmlContent += `<div class="prompt">${escapeHtml(promptValue)}</div>`;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
if (isLoadingState) {
|
|
473
|
+
if (showPromptNow) {
|
|
474
|
+
htmlContent += '<hr class="node-separator" />';
|
|
475
|
+
}
|
|
476
|
+
htmlContent += `<div class="thinking">AI is generating a response... <span class="icon-wrapper">${ICON_SPINNER_SVG}</span></div>`;
|
|
477
|
+
} else if (nodeResponse) {
|
|
478
|
+
// Separator between prompt and response (for both text and note types)
|
|
479
|
+
if (shouldShowPrompt) {
|
|
480
|
+
htmlContent += '<hr class="node-separator" />';
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
if (nodeModel.isChunkedText) {
|
|
484
|
+
const contentToRender = nodeModel._visibleContent || nodeModel.response || '';
|
|
485
|
+
const escapedText = escapeHtml(contentToRender);
|
|
486
|
+
|
|
487
|
+
htmlContent += `<div class="response" style="font-family: 'Consolas', 'Monaco', 'Courier New', monospace; white-space: pre; line-height: 1.5; font-size: ${14 * zoomLevel}px;">${escapedText}</div>`;
|
|
488
|
+
|
|
489
|
+
if (nodeModel.totalLineCount > 0 && (nodeModel.scrollOffset || 0) === 0) {
|
|
490
|
+
htmlContent += `<div style="margin-top:12px; padding:8px; background:rgba(59,130,246,0.1); border-radius:4px; font-size:0.85em; color:#3b82f6; text-align:center;">
|
|
491
|
+
Large file (${nodeModel.totalLineCount.toLocaleString()} lines) - Scroll to view more
|
|
492
|
+
</div>`;
|
|
493
|
+
}
|
|
494
|
+
} else {
|
|
495
|
+
const processedResponse = preprocessMarkdown(nodeModel.response);
|
|
496
|
+
|
|
497
|
+
let parsed;
|
|
498
|
+
if (hasMarked) {
|
|
499
|
+
try {
|
|
500
|
+
const renderer = new window.marked.Renderer();
|
|
501
|
+
|
|
502
|
+
renderer.code = function (codeOrObj, language, isEscaped) {
|
|
503
|
+
const code = typeof codeOrObj === 'object' ? codeOrObj.text : codeOrObj;
|
|
504
|
+
const lang = typeof codeOrObj === 'object' ? codeOrObj.lang : language;
|
|
505
|
+
|
|
506
|
+
const escaped = escapeHtml(code || '');
|
|
507
|
+
const langClass = lang ? ` class="language-${lang}"` : '';
|
|
508
|
+
return `<pre><code${langClass}>${escaped}</code></pre>`;
|
|
509
|
+
};
|
|
510
|
+
|
|
511
|
+
renderer.codespan = function (codeOrObj) {
|
|
512
|
+
const code = typeof codeOrObj === 'object' ? codeOrObj.text : codeOrObj;
|
|
513
|
+
return `<code>${escapeHtml(code || '')}</code>`;
|
|
514
|
+
};
|
|
515
|
+
|
|
516
|
+
parsed = window.marked.parse(processedResponse, {
|
|
517
|
+
breaks: true,
|
|
518
|
+
xhtml: true,
|
|
519
|
+
renderer: renderer
|
|
520
|
+
});
|
|
521
|
+
} catch (e) {
|
|
522
|
+
console.warn('[TextureFactory] marked.parse with custom renderer failed, using default:', e);
|
|
523
|
+
parsed = window.marked.parse(processedResponse, { breaks: true, xhtml: true });
|
|
524
|
+
}
|
|
525
|
+
} else {
|
|
526
|
+
parsed = `<div style="white-space: pre-wrap;">${escapeHtml(processedResponse)}</div>`;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
parsed = parsed.replace(/<br\s*\/?>/gi, '<br/>').replace(/<hr\s*\/?>/gi, '<hr/>');
|
|
530
|
+
parsed = sanitizeHtml(parsed);
|
|
531
|
+
htmlContent += `<div class="response">${parsed}</div>`;
|
|
532
|
+
}
|
|
533
|
+
} else if (nodeModel.isChunkedText && !nodeModel.response) {
|
|
534
|
+
htmlContent += `<div style="padding:16px; color:#6b7280; text-align:center;">
|
|
535
|
+
<span style="animation: pulse 1.5s infinite;">⏳</span> Loading...
|
|
536
|
+
</div>`;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
const { styles, fontStack } = getMarkdownStyles(zoomLevel, nodeModel.contentType);
|
|
540
|
+
const BORDER_WIDTH = 3 * zoomLevel;
|
|
541
|
+
const CSS_PADDING = 16 * zoomLevel;
|
|
542
|
+
const totalPaddingX = CSS_PADDING + BORDER_WIDTH;
|
|
543
|
+
const cssTopPadding = (nodeModel.contentType === 'note' ? 16 : 12) * zoomLevel;
|
|
544
|
+
const totalPaddingY = cssTopPadding + BORDER_WIDTH;
|
|
545
|
+
const paddingBottom = 16 * zoomLevel;
|
|
546
|
+
const textWidth = Math.max(1, canvasWidth - totalPaddingX * 2);
|
|
547
|
+
const textHeight = Math.max(1, canvasHeight - totalPaddingY - paddingBottom);
|
|
548
|
+
|
|
549
|
+
drawNodeBodyBackground(ctx, canvasWidth, canvasHeight, zoomLevel, isSelected, nodeModel.contentType, nodeModel);
|
|
550
|
+
|
|
551
|
+
const effectiveScrollOffset = nodeModel.isChunkedText && nodeModel._visibleScrollOffset !== undefined
|
|
552
|
+
? nodeModel._visibleScrollOffset
|
|
553
|
+
: (nodeModel.scrollOffset || 0);
|
|
554
|
+
|
|
555
|
+
let svgString;
|
|
556
|
+
|
|
557
|
+
if (nodeModel.isChunkedText && nodeModel.prompt) {
|
|
558
|
+
const headerHeight = 30 * zoomLevel;
|
|
559
|
+
const bodyHeight = Math.max(1, textHeight - headerHeight);
|
|
560
|
+
|
|
561
|
+
const headerHtml = `
|
|
562
|
+
<div xmlns="http://www.w3.org/1999/xhtml" style="width:100%; font-family:${fontStack}; font-size:${14 * zoomLevel}px; line-height:1.5; color:#1f2937;">
|
|
563
|
+
<div class="prompt">${escapeHtml(nodeModel.prompt)}</div>
|
|
564
|
+
<hr class="node-separator" />
|
|
565
|
+
</div>`;
|
|
566
|
+
|
|
567
|
+
const visibleText = nodeModel._visibleContent || nodeModel.response || '';
|
|
568
|
+
const bodyContent = visibleText ? escapeHtml(visibleText) : '<span style="color:#9ca3af;">Loading...</span>';
|
|
569
|
+
const bodyHtml = `
|
|
570
|
+
<div xmlns="http://www.w3.org/1999/xhtml" style="width:100%; height:auto; font-family:'Consolas', 'Monaco', 'Courier New', monospace; font-size:${14 * zoomLevel}px; line-height:1.5; color:#1f2937; white-space:pre; transform:translateY(-${effectiveScrollOffset * zoomLevel}px);">
|
|
571
|
+
${bodyContent}
|
|
572
|
+
</div>`;
|
|
573
|
+
|
|
574
|
+
svgString = `
|
|
575
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="${canvasWidth}" height="${canvasHeight}">
|
|
576
|
+
<defs>
|
|
577
|
+
<style type="text/css"><![CDATA[
|
|
578
|
+
${styles}
|
|
579
|
+
]]></style>
|
|
580
|
+
</defs>
|
|
581
|
+
<!-- Fixed header -->
|
|
582
|
+
<foreignObject x="${totalPaddingX}" y="${totalPaddingY}" width="${textWidth}" height="${headerHeight}">
|
|
583
|
+
${headerHtml}
|
|
584
|
+
</foreignObject>
|
|
585
|
+
<!-- Scrollable body -->
|
|
586
|
+
<foreignObject x="${totalPaddingX}" y="${totalPaddingY + headerHeight}" width="${textWidth}" height="${bodyHeight}">
|
|
587
|
+
${bodyHtml}
|
|
588
|
+
</foreignObject>
|
|
589
|
+
</svg>`;
|
|
590
|
+
} else {
|
|
591
|
+
const htmlBlock = `
|
|
592
|
+
<div xmlns="http://www.w3.org/1999/xhtml" style="width:100%; height:auto; font-family:${fontStack}; font-size:${14 * zoomLevel}px; line-height:1.5; color:#1f2937; overflow-wrap:break-word; transform:translateY(-${effectiveScrollOffset * zoomLevel}px);">
|
|
593
|
+
${htmlContent}
|
|
594
|
+
</div>`;
|
|
595
|
+
|
|
596
|
+
svgString = `
|
|
597
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="${canvasWidth}" height="${canvasHeight}">
|
|
598
|
+
<defs>
|
|
599
|
+
<style type="text/css"><![CDATA[
|
|
600
|
+
${styles}
|
|
601
|
+
]]></style>
|
|
602
|
+
</defs>
|
|
603
|
+
<foreignObject x="${totalPaddingX}" y="${totalPaddingY}" width="${textWidth}" height="${textHeight}">
|
|
604
|
+
${htmlBlock}
|
|
605
|
+
</foreignObject>
|
|
606
|
+
</svg>`;
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
const dataUrl = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svgString)}`;
|
|
610
|
+
const img = new Image();
|
|
611
|
+
|
|
612
|
+
img.onload = () => {
|
|
613
|
+
ctx.drawImage(img, 0, 0);
|
|
614
|
+
drawScrollbar(ctx, nodeModel, canvasWidth, canvasHeight, zoomLevel, nodeModel.isScrollbarHovered);
|
|
615
|
+
texture.needsUpdate = true;
|
|
616
|
+
if (onComplete) onComplete(true);
|
|
617
|
+
};
|
|
618
|
+
|
|
619
|
+
img.onerror = (err) => {
|
|
620
|
+
console.error('[TextureFactory] SVG render failed for node:', nodeModel.id);
|
|
621
|
+
|
|
622
|
+
try {
|
|
623
|
+
const parser = new DOMParser();
|
|
624
|
+
const doc = parser.parseFromString(svgString, 'image/svg+xml');
|
|
625
|
+
const parseError = doc.querySelector('parsererror');
|
|
626
|
+
|
|
627
|
+
if (parseError) {
|
|
628
|
+
console.error('[TextureFactory] SVG Parse Error:', parseError.textContent);
|
|
629
|
+
} else {
|
|
630
|
+
console.error('[TextureFactory] SVG parsed OK but Image failed to load');
|
|
631
|
+
}
|
|
632
|
+
} catch (e) {
|
|
633
|
+
console.error('[TextureFactory] DOMParser error:', e);
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
console.error('[TextureFactory] HTML content (first 500 chars):', htmlContent.substring(0, 500));
|
|
637
|
+
console.error('[TextureFactory] HTML content (last 200 chars):', htmlContent.substring(Math.max(0, htmlContent.length - 200)));
|
|
638
|
+
|
|
639
|
+
drawNodeBodyOnCanvas(ctx, nodeModel, canvasWidth, canvasHeight, zoomLevel, isSelected);
|
|
640
|
+
drawScrollbar(ctx, nodeModel, canvasWidth, canvasHeight, zoomLevel, nodeModel.isScrollbarHovered);
|
|
641
|
+
texture.needsUpdate = true;
|
|
642
|
+
if (onComplete) onComplete(false);
|
|
643
|
+
};
|
|
644
|
+
|
|
645
|
+
img.src = dataUrl;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
async function createRenderedTextureAsync(module, nodeModel, isSelected, imageCache) {
|
|
649
|
+
const zoomLevel = 2;
|
|
650
|
+
const styleW = (nodeModel.width || 400) * zoomLevel;
|
|
651
|
+
const styleH = (nodeModel.height || 200) * zoomLevel;
|
|
652
|
+
|
|
653
|
+
const MAX_TEXTURE_WIDTH = 2048;
|
|
654
|
+
const MAX_TEXTURE_HEIGHT = 8192;
|
|
655
|
+
|
|
656
|
+
const scaleX = styleW > MAX_TEXTURE_WIDTH ? MAX_TEXTURE_WIDTH / styleW : 1;
|
|
657
|
+
const scaleY = styleH > MAX_TEXTURE_HEIGHT ? MAX_TEXTURE_HEIGHT / styleH : 1;
|
|
658
|
+
const textureScale = Math.min(scaleX, scaleY);
|
|
659
|
+
|
|
660
|
+
const renderW = Math.round(styleW * textureScale);
|
|
661
|
+
const renderH = Math.round(styleH * textureScale);
|
|
662
|
+
|
|
663
|
+
const renderResolutionScale = 1;
|
|
664
|
+
const canvas = document.createElement('canvas');
|
|
665
|
+
canvas.width = renderW * renderResolutionScale;
|
|
666
|
+
canvas.height = renderH * renderResolutionScale;
|
|
667
|
+
const ctx = canvas.getContext('2d');
|
|
668
|
+
ctx.scale(renderResolutionScale * textureScale, renderResolutionScale * textureScale);
|
|
669
|
+
|
|
670
|
+
const texture = new THREE.CanvasTexture(canvas);
|
|
671
|
+
texture.generateMipmaps = true;
|
|
672
|
+
texture.minFilter = THREE.LinearMipmapLinearFilter;
|
|
673
|
+
texture.magFilter = THREE.LinearFilter;
|
|
674
|
+
texture.anisotropy = module.renderer.capabilities.getMaxAnisotropy();
|
|
675
|
+
|
|
676
|
+
const pluginRenderer = renderers.get(nodeModel.contentType);
|
|
677
|
+
if (pluginRenderer) {
|
|
678
|
+
try {
|
|
679
|
+
pluginRenderer.draw(ctx, nodeModel, styleW, styleH, zoomLevel, isSelected);
|
|
680
|
+
drawScrollbar(ctx, nodeModel, styleW, styleH, zoomLevel, nodeModel.isScrollbarHovered);
|
|
681
|
+
texture.needsUpdate = true;
|
|
682
|
+
return texture;
|
|
683
|
+
} catch (error) {
|
|
684
|
+
console.error('Plugin renderer failed', error);
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
if (nodeModel.contentType === 'text' || nodeModel.contentType === 'note') {
|
|
689
|
+
await new Promise((resolve) => {
|
|
690
|
+
renderMarkdownToCanvasAsync(ctx, texture, nodeModel, styleW, styleH, zoomLevel, isSelected, () => resolve());
|
|
691
|
+
});
|
|
692
|
+
} else if (nodeModel.contentType === 'image') {
|
|
693
|
+
drawCachedImageBody(imageCache.get(nodeModel.id), ctx, nodeModel, styleW, styleH, zoomLevel, isSelected);
|
|
694
|
+
} else {
|
|
695
|
+
drawNodeBodyOnCanvas(ctx, nodeModel, styleW, styleH, zoomLevel, isSelected);
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
drawScrollbar(ctx, nodeModel, styleW, styleH, zoomLevel, nodeModel.isScrollbarHovered);
|
|
699
|
+
texture.needsUpdate = true;
|
|
700
|
+
return texture;
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
async function measureHtmlHeightAsync(nodeModel, zoomLevel, canvasWidth, forcedHeight = null, maxHeight = 800) {
|
|
704
|
+
console.log(`[measureHtmlHeightAsync] ENTER - nodeId: ${nodeModel.id}, responseLen: ${nodeModel.response?.length || 0}`);
|
|
705
|
+
|
|
706
|
+
const hasMarked = typeof window !== 'undefined' && window.marked && typeof window.marked.parse === 'function';
|
|
707
|
+
const { styles, fontStack } = getMarkdownStyles(zoomLevel, nodeModel.contentType);
|
|
708
|
+
let htmlContent = '';
|
|
709
|
+
|
|
710
|
+
const promptValue = nodeModel.prompt ?? nodeModel.Prompt ?? '';
|
|
711
|
+
// 기본 Prompt 값들은 표시하지 않음
|
|
712
|
+
const defaultPrompts = ['Note', 'Pasted Text', '에이전트 메모'];
|
|
713
|
+
const shouldShowPrompt = promptValue && !defaultPrompts.includes(promptValue);
|
|
714
|
+
// C#에서 PascalCase (IsLoading)로 전달될 수 있으므로 둘 다 확인
|
|
715
|
+
const nodeIsLoading = nodeModel.isLoading ?? nodeModel.IsLoading ?? false;
|
|
716
|
+
const nodeResponse = nodeModel.response ?? nodeModel.Response ?? '';
|
|
717
|
+
const nodeContentType = nodeModel.contentType ?? nodeModel.ContentType ?? 'text';
|
|
718
|
+
const isLoadingState = nodeIsLoading && !nodeResponse;
|
|
719
|
+
// note 타입에서도 프롬프트 표시 (CSS3D와 동일하게)
|
|
720
|
+
const showPromptNow = shouldShowPrompt;
|
|
721
|
+
if (showPromptNow) {
|
|
722
|
+
htmlContent += `<div class="prompt">${escapeHtml(promptValue)}</div>`;
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
// Separator between prompt and response (for both text and note types)
|
|
726
|
+
if (showPromptNow && (nodeResponse || isLoadingState)) {
|
|
727
|
+
htmlContent += '<hr class="node-separator" />';
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
if (nodeResponse) {
|
|
731
|
+
console.log(`[measureHtmlHeightAsync] Step 1: Preprocessing markdown...`);
|
|
732
|
+
const processedResponse = preprocessMarkdown(nodeModel.response);
|
|
733
|
+
console.log(`[measureHtmlHeightAsync] Step 1: Done. Preprocessed length: ${processedResponse.length}`);
|
|
734
|
+
|
|
735
|
+
let parsed;
|
|
736
|
+
if (hasMarked) {
|
|
737
|
+
try {
|
|
738
|
+
console.log(`[measureHtmlHeightAsync] Step 2: Parsing with marked...`);
|
|
739
|
+
|
|
740
|
+
const renderer = new window.marked.Renderer();
|
|
741
|
+
renderer.code = function (codeOrObj, language, isEscaped) {
|
|
742
|
+
const code = typeof codeOrObj === 'object' ? codeOrObj.text : codeOrObj;
|
|
743
|
+
const lang = typeof codeOrObj === 'object' ? codeOrObj.lang : language;
|
|
744
|
+
const escaped = escapeHtml(code || '');
|
|
745
|
+
const langClass = lang ? ` class="language-${lang}"` : '';
|
|
746
|
+
return `<pre><code${langClass}>${escaped}</code></pre>`;
|
|
747
|
+
};
|
|
748
|
+
renderer.codespan = function (codeOrObj) {
|
|
749
|
+
const code = typeof codeOrObj === 'object' ? codeOrObj.text : codeOrObj;
|
|
750
|
+
return `<code>${escapeHtml(code || '')}</code>`;
|
|
751
|
+
};
|
|
752
|
+
parsed = window.marked.parse(processedResponse, {
|
|
753
|
+
breaks: true,
|
|
754
|
+
xhtml: true,
|
|
755
|
+
renderer: renderer
|
|
756
|
+
});
|
|
757
|
+
console.log(`[measureHtmlHeightAsync] Step 2: Marked parsing done. Length: ${parsed.length}`);
|
|
758
|
+
} catch (e) {
|
|
759
|
+
console.warn(`[measureHtmlHeightAsync] Marked parse failed, using fallback`, e);
|
|
760
|
+
parsed = window.marked.parse(processedResponse, { breaks: true, xhtml: true });
|
|
761
|
+
}
|
|
762
|
+
} else {
|
|
763
|
+
parsed = `<div style="white-space: pre-wrap;">${escapeHtml(processedResponse)}</div>`;
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
console.log(`[measureHtmlHeightAsync] Step 3: Sanitizing HTML...`);
|
|
767
|
+
parsed = sanitizeHtml(parsed);
|
|
768
|
+
console.log(`[measureHtmlHeightAsync] Step 3: Sanitizing done. Length: ${parsed.length}`);
|
|
769
|
+
|
|
770
|
+
htmlContent += `<div class="response">${parsed}</div>`;
|
|
771
|
+
} else if (nodeModel.isLoading) {
|
|
772
|
+
if (nodeModel.prompt) {
|
|
773
|
+
htmlContent += '<hr class="node-separator" />';
|
|
774
|
+
}
|
|
775
|
+
htmlContent += `<div class=\"thinking\">AI is generating a response... <span class=\"icon-wrapper\">${ICON_SPINNER_SVG}</span></div>`;
|
|
776
|
+
} else {
|
|
777
|
+
htmlContent += '<div class="response"> </div>';
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
console.log(`[measureHtmlHeightAsync] Step 4: Setting measurer innerHTML...`);
|
|
781
|
+
const measurer = getSharedMeasurer();
|
|
782
|
+
const BORDER_WIDTH = 3 * zoomLevel;
|
|
783
|
+
const PADDING_X = 16 * zoomLevel;
|
|
784
|
+
const PADDING_BOTTOM = 16 * zoomLevel;
|
|
785
|
+
const PADDING_TOP = (nodeModel.contentType === 'note' ? 16 : 12) * zoomLevel;
|
|
786
|
+
const totalHorizontalPadding = (PADDING_X + BORDER_WIDTH) * 2;
|
|
787
|
+
|
|
788
|
+
const totalVerticalPadding = PADDING_TOP + PADDING_BOTTOM + (BORDER_WIDTH * 2);
|
|
789
|
+
const contentWidth = Math.max(1, canvasWidth - totalHorizontalPadding);
|
|
790
|
+
measurer.style.width = `${contentWidth}px`;
|
|
791
|
+
measurer.style.fontFamily = fontStack;
|
|
792
|
+
measurer.style.fontSize = `${14 * zoomLevel}px`;
|
|
793
|
+
measurer.style.lineHeight = '1.5';
|
|
794
|
+
measurer.style.overflowWrap = 'break-word';
|
|
795
|
+
measurer.style.whiteSpace = 'normal';
|
|
796
|
+
measurer.style.overflow = 'hidden';
|
|
797
|
+
measurer.innerHTML = `<style>${styles}</style>${htmlContent}`;
|
|
798
|
+
console.log(`[measureHtmlHeightAsync] Step 4: innerHTML set. Total length: ${measurer.innerHTML.length}`);
|
|
799
|
+
|
|
800
|
+
console.log(`[measureHtmlHeightAsync] Step 5: Calling getBoundingClientRect...`);
|
|
801
|
+
const rect = measurer.getBoundingClientRect();
|
|
802
|
+
const contentHeight = Math.ceil(rect.height);
|
|
803
|
+
console.log(`[measureHtmlHeightAsync] Step 5: Done. contentHeight: ${contentHeight}`);
|
|
804
|
+
|
|
805
|
+
const totalRequiredHeight = contentHeight + totalVerticalPadding;
|
|
806
|
+
|
|
807
|
+
const MAX_HEIGHT = maxHeight * zoomLevel;
|
|
808
|
+
const MIN_HEIGHT = 60 * zoomLevel;
|
|
809
|
+
|
|
810
|
+
let visibleHeight;
|
|
811
|
+
if (forcedHeight !== null && forcedHeight > 0) {
|
|
812
|
+
visibleHeight = Math.max(forcedHeight, MIN_HEIGHT);
|
|
813
|
+
} else {
|
|
814
|
+
visibleHeight = Math.min(Math.max(totalRequiredHeight, MIN_HEIGHT), MAX_HEIGHT);
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
// ▼▼▼ [수정] 스크롤 끝에서 마지막 줄이 잘리지 않도록 여분 추가 ▼▼▼
|
|
818
|
+
// 콘텐츠 영역의 하단 패딩만큼 추가 스크롤 허용
|
|
819
|
+
const extraScrollMargin = PADDING_BOTTOM;
|
|
820
|
+
const maxScrollPx = Math.max(0, totalRequiredHeight - visibleHeight + extraScrollMargin);
|
|
821
|
+
// ▲▲▲ [수정] ▲▲▲
|
|
822
|
+
|
|
823
|
+
return {
|
|
824
|
+
height: Math.ceil(Math.max(60, visibleHeight / zoomLevel)),
|
|
825
|
+
maxScrollPx
|
|
826
|
+
};
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
async function generatePlaceholderTextures(module, nodeModel, width, height) {
|
|
830
|
+
const zoomLevel = 2;
|
|
831
|
+
const styleW = width * zoomLevel;
|
|
832
|
+
const styleH = height * zoomLevel;
|
|
833
|
+
|
|
834
|
+
const createBody = (selected) => {
|
|
835
|
+
const canvas = document.createElement('canvas');
|
|
836
|
+
canvas.width = styleW;
|
|
837
|
+
canvas.height = styleH;
|
|
838
|
+
const ctx = canvas.getContext('2d');
|
|
839
|
+
drawNodeBodyOnCanvas(ctx, nodeModel, styleW, styleH, zoomLevel, selected);
|
|
840
|
+
drawScrollbar(ctx, nodeModel, styleW, styleH, zoomLevel, nodeModel.isScrollbarHovered);
|
|
841
|
+
const tex = new THREE.CanvasTexture(canvas);
|
|
842
|
+
tex.generateMipmaps = true;
|
|
843
|
+
tex.minFilter = THREE.LinearMipmapLinearFilter;
|
|
844
|
+
tex.magFilter = THREE.LinearFilter;
|
|
845
|
+
return tex;
|
|
846
|
+
};
|
|
847
|
+
|
|
848
|
+
const defaultBody = createBody(false);
|
|
849
|
+
const selectedBody = createBody(true);
|
|
850
|
+
const tailDefault = createTailTexture('#fefefe', 'rgba(55, 65, 81, 0.9)', zoomLevel, 12, false);
|
|
851
|
+
const tailSelected = createTailTexture('#fefefe', '#2563eb', zoomLevel, 12, true);
|
|
852
|
+
|
|
853
|
+
const radius = 12 * zoomLevel;
|
|
854
|
+
const glowData = createGlowTexture(styleW, styleH, radius, zoomLevel);
|
|
855
|
+
|
|
856
|
+
return {
|
|
857
|
+
textures: {
|
|
858
|
+
default: { body: defaultBody, tail: tailDefault },
|
|
859
|
+
selected: { body: selectedBody, tail: tailSelected },
|
|
860
|
+
glow: glowData
|
|
861
|
+
},
|
|
862
|
+
width,
|
|
863
|
+
height
|
|
864
|
+
};
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
async function ensureImageCached(nodeId, url, imageCache, options = {}) {
|
|
868
|
+
if (!url) return null;
|
|
869
|
+
|
|
870
|
+
if (url.startsWith('⏳') || url.includes('Uploading')) {
|
|
871
|
+
return null;
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
const existing = imageCache.get(nodeId);
|
|
875
|
+
const existingUrl = existing?.url || existing?.src || existing?.image?.src;
|
|
876
|
+
if (existing && existingUrl === url) {
|
|
877
|
+
return existing;
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
const resizeWidth = options.resizeWidth || 512;
|
|
881
|
+
let blob = null;
|
|
882
|
+
|
|
883
|
+
try {
|
|
884
|
+
const response = await fetch(url);
|
|
885
|
+
if (!response.ok) {
|
|
886
|
+
throw new Error(`HTTP ${response.status}`);
|
|
887
|
+
}
|
|
888
|
+
blob = await response.blob();
|
|
889
|
+
} catch (error) {
|
|
890
|
+
console.warn(`[TextureFactory] Image fetch failed for ${nodeId}:`, error);
|
|
891
|
+
const errorEntry = { image: null, url, width: 0, height: 0, isError: true };
|
|
892
|
+
imageCache.set(nodeId, errorEntry);
|
|
893
|
+
return errorEntry;
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
if (blob && typeof createImageBitmap === 'function') {
|
|
897
|
+
try {
|
|
898
|
+
const bitmap = await createImageBitmap(blob, { resizeWidth, resizeQuality: 'medium' });
|
|
899
|
+
const cached = { image: bitmap, url, width: bitmap.width, height: bitmap.height, type: 'bitmap', isError: false };
|
|
900
|
+
imageCache.set(nodeId, cached);
|
|
901
|
+
return cached;
|
|
902
|
+
} catch (error) {
|
|
903
|
+
console.warn('[TextureFactory] createImageBitmap failed, fallback to Image()', error);
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
return new Promise((resolve) => {
|
|
908
|
+
const img = new Image();
|
|
909
|
+
if (!url.startsWith('blob:') && !url.startsWith('data:') && !url.startsWith('file:')) {
|
|
910
|
+
img.crossOrigin = 'anonymous';
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
let objectUrl = null;
|
|
914
|
+
if (blob) {
|
|
915
|
+
objectUrl = URL.createObjectURL(blob);
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
img.onload = () => {
|
|
919
|
+
if (objectUrl) URL.revokeObjectURL(objectUrl);
|
|
920
|
+
const cached = {
|
|
921
|
+
image: img,
|
|
922
|
+
url,
|
|
923
|
+
width: img.naturalWidth || img.width || 0,
|
|
924
|
+
height: img.naturalHeight || img.height || 0,
|
|
925
|
+
type: 'image',
|
|
926
|
+
isError: false
|
|
927
|
+
};
|
|
928
|
+
imageCache.set(nodeId, cached);
|
|
929
|
+
resolve(cached);
|
|
930
|
+
};
|
|
931
|
+
|
|
932
|
+
img.onerror = () => {
|
|
933
|
+
if (objectUrl) URL.revokeObjectURL(objectUrl);
|
|
934
|
+
console.warn(`[TextureFactory] Image load error for ${nodeId}`);
|
|
935
|
+
const errorEntry = { image: null, url, width: 0, height: 0, isError: true };
|
|
936
|
+
imageCache.set(nodeId, errorEntry);
|
|
937
|
+
resolve(errorEntry);
|
|
938
|
+
};
|
|
939
|
+
|
|
940
|
+
img.src = objectUrl || url;
|
|
941
|
+
});
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
async function generateAndCacheTextures(module, nodeModel, imageCache, updateData = {}) {
|
|
945
|
+
const t0 = performance.now();
|
|
946
|
+
|
|
947
|
+
const defaultWidth = module.aiNodeDefaultWidth || 400;
|
|
948
|
+
const defaultHeight = module.aiNodeDefaultHeight || 200;
|
|
949
|
+
|
|
950
|
+
const effectiveMaxHeight = module.showFullAiResponse ? 10000 : defaultHeight;
|
|
951
|
+
const width = nodeModel.width || (nodeModel.contentType === 'image' ? 300 : defaultWidth);
|
|
952
|
+
let height = nodeModel.height || (nodeModel.contentType === 'image' ? 200 : defaultHeight);
|
|
953
|
+
|
|
954
|
+
const zoomLevel = 2;
|
|
955
|
+
|
|
956
|
+
const t1 = performance.now();
|
|
957
|
+
|
|
958
|
+
// ▼▼▼ [Optimization A] Skip measureHeight if already measured ▼▼▼
|
|
959
|
+
const skipMeasure = updateData.skipMeasure === true;
|
|
960
|
+
|
|
961
|
+
if ((nodeModel.contentType === 'text' || nodeModel.contentType === 'note') && !nodeModel.isChunkedText) {
|
|
962
|
+
if (!skipMeasure) {
|
|
963
|
+
try {
|
|
964
|
+
const forcedHeight = (nodeModel.height && nodeModel.height > 0) ? (nodeModel.height * zoomLevel) : null;
|
|
965
|
+
|
|
966
|
+
const { height: measuredHeight, maxScrollPx } = await measureHtmlHeightAsync(nodeModel, zoomLevel, width * zoomLevel, forcedHeight, effectiveMaxHeight);
|
|
967
|
+
if (!nodeModel.height || nodeModel.height <= 0) {
|
|
968
|
+
height = measuredHeight;
|
|
969
|
+
nodeModel.height = height;
|
|
970
|
+
} else {
|
|
971
|
+
height = nodeModel.height;
|
|
972
|
+
}
|
|
973
|
+
nodeModel.maxScroll = maxScrollPx / zoomLevel;
|
|
974
|
+
if (updateData.scrollDelta) {
|
|
975
|
+
const oldScroll = nodeModel.scrollOffset || 0;
|
|
976
|
+
const newScroll = Math.max(0, Math.min(oldScroll + updateData.scrollDelta, nodeModel.maxScroll));
|
|
977
|
+
nodeModel.scrollOffset = newScroll;
|
|
978
|
+
try { module.dotNetHelper?.invokeMethodAsync('UpdateNodeScroll', nodeModel.id, newScroll); } catch (error) { console.warn(error); }
|
|
979
|
+
} else {
|
|
980
|
+
nodeModel.scrollOffset = Math.min(nodeModel.scrollOffset || 0, nodeModel.maxScroll);
|
|
981
|
+
}
|
|
982
|
+
} catch (error) {
|
|
983
|
+
console.warn('[TextureFactory] height measure failed', error);
|
|
984
|
+
if (!nodeModel.height || nodeModel.height <= 0) {
|
|
985
|
+
height = 200;
|
|
986
|
+
nodeModel.height = height;
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
} else {
|
|
990
|
+
// Height already measured - just handle scroll
|
|
991
|
+
height = nodeModel.height || height;
|
|
992
|
+
// ▼▼▼ [Fix] Ensure scrollOffset is within valid range ▼▼▼
|
|
993
|
+
if (nodeModel.maxScroll !== undefined && nodeModel.maxScroll >= 0) {
|
|
994
|
+
nodeModel.scrollOffset = Math.min(nodeModel.scrollOffset || 0, nodeModel.maxScroll);
|
|
995
|
+
} else {
|
|
996
|
+
// maxScroll not set yet - set to 0 (no scroll needed)
|
|
997
|
+
nodeModel.maxScroll = 0;
|
|
998
|
+
nodeModel.scrollOffset = 0;
|
|
999
|
+
}
|
|
1000
|
+
// ▲▲▲ [Fix] ▲▲▲
|
|
1001
|
+
if (updateData.scrollDelta) {
|
|
1002
|
+
const oldScroll = nodeModel.scrollOffset || 0;
|
|
1003
|
+
const newScroll = Math.max(0, Math.min(oldScroll + updateData.scrollDelta, nodeModel.maxScroll || 0));
|
|
1004
|
+
nodeModel.scrollOffset = newScroll;
|
|
1005
|
+
try { module.dotNetHelper?.invokeMethodAsync('UpdateNodeScroll', nodeModel.id, newScroll); } catch (error) { console.warn(error); }
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
} else if (nodeModel.isChunkedText) {
|
|
1009
|
+
height = nodeModel.height || 400;
|
|
1010
|
+
if (updateData.scrollDelta) {
|
|
1011
|
+
const oldScroll = nodeModel.scrollOffset || 0;
|
|
1012
|
+
const newScroll = Math.max(0, Math.min(oldScroll + updateData.scrollDelta, nodeModel.maxScroll || 0));
|
|
1013
|
+
nodeModel.scrollOffset = newScroll;
|
|
1014
|
+
try { module.dotNetHelper?.invokeMethodAsync('UpdateNodeScroll', nodeModel.id, newScroll); } catch (error) { console.warn(error); }
|
|
1015
|
+
}
|
|
1016
|
+
} else {
|
|
1017
|
+
nodeModel.height = height;
|
|
1018
|
+
}
|
|
1019
|
+
// ▲▲▲ [Optimization A] ▲▲▲
|
|
1020
|
+
|
|
1021
|
+
const t2 = performance.now();
|
|
1022
|
+
|
|
1023
|
+
const styleW = width * zoomLevel;
|
|
1024
|
+
const styleH = height * zoomLevel;
|
|
1025
|
+
const radius = 12 * zoomLevel;
|
|
1026
|
+
const glowData = createGlowTexture(styleW, styleH, radius, zoomLevel);
|
|
1027
|
+
|
|
1028
|
+
const t3 = performance.now();
|
|
1029
|
+
|
|
1030
|
+
// ▼▼▼ [Optimization B] Skip selected texture on initial load ▼▼▼
|
|
1031
|
+
const skipSelected = updateData.skipSelected === true;
|
|
1032
|
+
|
|
1033
|
+
let bodyDefault, bodySelected;
|
|
1034
|
+
if (skipSelected) {
|
|
1035
|
+
// Only create default texture for initial load
|
|
1036
|
+
bodyDefault = await createRenderedTextureAsync(module, nodeModel, false, imageCache);
|
|
1037
|
+
bodySelected = null; // Will be created on-demand when selected
|
|
1038
|
+
} else {
|
|
1039
|
+
// Create both textures (for updates, selection changes, etc.)
|
|
1040
|
+
[bodyDefault, bodySelected] = await Promise.all([
|
|
1041
|
+
createRenderedTextureAsync(module, nodeModel, false, imageCache),
|
|
1042
|
+
createRenderedTextureAsync(module, nodeModel, true, imageCache)
|
|
1043
|
+
]);
|
|
1044
|
+
}
|
|
1045
|
+
// ▲▲▲ [Optimization B] ▲▲▲
|
|
1046
|
+
|
|
1047
|
+
const t4 = performance.now();
|
|
1048
|
+
|
|
1049
|
+
const tailColor = '#fefefe';
|
|
1050
|
+
const tailDefault = createTailTexture(tailColor, '#374151', zoomLevel, 12, false);
|
|
1051
|
+
const tailSelected = skipSelected ? null : createTailTexture(tailColor, '#2563eb', zoomLevel, 12, true);
|
|
1052
|
+
|
|
1053
|
+
const t5 = performance.now();
|
|
1054
|
+
|
|
1055
|
+
// ▼▼▼ [Timing] 세부 시간 측정 로그 ▼▼▼
|
|
1056
|
+
console.log(`[⏱️ Timing] generateAndCacheTextures for ${nodeModel.id} (${nodeModel.contentType}):
|
|
1057
|
+
• Init: ${(t1 - t0).toFixed(1)}ms
|
|
1058
|
+
• MeasureHeight: ${(t2 - t1).toFixed(1)}ms ${skipMeasure ? '(SKIPPED)' : ''}
|
|
1059
|
+
• GlowTexture: ${(t3 - t2).toFixed(1)}ms
|
|
1060
|
+
• BodyTextures: ${(t4 - t3).toFixed(1)}ms ${skipSelected ? '(default only)' : '(x2)'}
|
|
1061
|
+
• TailTextures: ${(t5 - t4).toFixed(1)}ms ${skipSelected ? '(default only)' : '(x2)'}
|
|
1062
|
+
• TOTAL: ${(t5 - t0).toFixed(1)}ms`);
|
|
1063
|
+
// ▲▲▲ [Timing] ▲▲▲
|
|
1064
|
+
|
|
1065
|
+
// ▼▼▼ [New] Generate LOD thumbnail if LODRenderer is enabled ▼▼▼
|
|
1066
|
+
const sourceCanvas = bodyDefault?.image;
|
|
1067
|
+
if (module.useLODRendering && module.lodRenderer && sourceCanvas) {
|
|
1068
|
+
try {
|
|
1069
|
+
module.lodRenderer.addNode(nodeModel.id, sourceCanvas, width, height);
|
|
1070
|
+
} catch (e) {
|
|
1071
|
+
console.warn('[TextureFactory] LOD thumbnail generation failed:', e);
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
// ▲▲▲ [New] ▲▲▲
|
|
1075
|
+
|
|
1076
|
+
return {
|
|
1077
|
+
textures: {
|
|
1078
|
+
default: { body: bodyDefault, tail: tailDefault },
|
|
1079
|
+
selected: { body: bodySelected, tail: tailSelected },
|
|
1080
|
+
glow: glowData
|
|
1081
|
+
},
|
|
1082
|
+
width,
|
|
1083
|
+
height,
|
|
1084
|
+
sourceCanvas // For external LOD use if needed
|
|
1085
|
+
};
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
// ========================================
|
|
1089
|
+
// Atlas Integration Functions
|
|
1090
|
+
// ========================================
|
|
1091
|
+
|
|
1092
|
+
/**
|
|
1093
|
+
* Render node texture to a canvas (for Atlas packing)
|
|
1094
|
+
* Returns the canvas instead of a THREE.Texture
|
|
1095
|
+
*/
|
|
1096
|
+
async function renderNodeToCanvas(module, nodeModel, isSelected, imageCache) {
|
|
1097
|
+
const zoomLevel = 2;
|
|
1098
|
+
const styleW = (nodeModel.width || 400) * zoomLevel;
|
|
1099
|
+
const styleH = (nodeModel.height || 200) * zoomLevel;
|
|
1100
|
+
|
|
1101
|
+
const MAX_TEXTURE_WIDTH = 2048;
|
|
1102
|
+
const MAX_TEXTURE_HEIGHT = 8192;
|
|
1103
|
+
|
|
1104
|
+
const scaleX = styleW > MAX_TEXTURE_WIDTH ? MAX_TEXTURE_WIDTH / styleW : 1;
|
|
1105
|
+
const scaleY = styleH > MAX_TEXTURE_HEIGHT ? MAX_TEXTURE_HEIGHT / styleH : 1;
|
|
1106
|
+
const textureScale = Math.min(scaleX, scaleY);
|
|
1107
|
+
|
|
1108
|
+
const renderW = Math.round(styleW * textureScale);
|
|
1109
|
+
const renderH = Math.round(styleH * textureScale);
|
|
1110
|
+
|
|
1111
|
+
const canvas = document.createElement('canvas');
|
|
1112
|
+
canvas.width = renderW;
|
|
1113
|
+
canvas.height = renderH;
|
|
1114
|
+
const ctx = canvas.getContext('2d');
|
|
1115
|
+
ctx.scale(textureScale, textureScale);
|
|
1116
|
+
|
|
1117
|
+
const pluginRenderer = renderers.get(nodeModel.contentType);
|
|
1118
|
+
if (pluginRenderer) {
|
|
1119
|
+
try {
|
|
1120
|
+
pluginRenderer.draw(ctx, nodeModel, styleW, styleH, zoomLevel, isSelected);
|
|
1121
|
+
drawScrollbar(ctx, nodeModel, styleW, styleH, zoomLevel, nodeModel.isScrollbarHovered);
|
|
1122
|
+
return { canvas, width: renderW, height: renderH };
|
|
1123
|
+
} catch (error) {
|
|
1124
|
+
console.error('Plugin renderer failed', error);
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
if (nodeModel.contentType === 'text' || nodeModel.contentType === 'note') {
|
|
1129
|
+
await new Promise((resolve) => {
|
|
1130
|
+
// Create a dummy texture just to satisfy the API
|
|
1131
|
+
const dummyTexture = { needsUpdate: false };
|
|
1132
|
+
renderMarkdownToCanvasAsync(ctx, dummyTexture, nodeModel, styleW, styleH, zoomLevel, isSelected, () => resolve());
|
|
1133
|
+
});
|
|
1134
|
+
} else if (nodeModel.contentType === 'image') {
|
|
1135
|
+
drawCachedImageBody(imageCache.get(nodeModel.id), ctx, nodeModel, styleW, styleH, zoomLevel, isSelected);
|
|
1136
|
+
} else {
|
|
1137
|
+
drawNodeBodyOnCanvas(ctx, nodeModel, styleW, styleH, zoomLevel, isSelected);
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
drawScrollbar(ctx, nodeModel, styleW, styleH, zoomLevel, nodeModel.isScrollbarHovered);
|
|
1141
|
+
return { canvas, width: renderW, height: renderH };
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
/**
|
|
1145
|
+
* Generate textures and add to Atlas (if atlas mode is enabled)
|
|
1146
|
+
* Falls back to regular texture creation if atlas is not available
|
|
1147
|
+
*/
|
|
1148
|
+
async function generateTexturesWithAtlas(module, nodeModel, imageCache, updateData = {}) {
|
|
1149
|
+
// Check if atlas rendering is enabled
|
|
1150
|
+
if (!module.useAtlasRendering || !module.atlasManager) {
|
|
1151
|
+
// Fall back to regular texture generation
|
|
1152
|
+
return generateAndCacheTextures(module, nodeModel, imageCache, updateData);
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
const t0 = performance.now();
|
|
1156
|
+
const atlasManager = module.atlasManager;
|
|
1157
|
+
|
|
1158
|
+
// Prepare dimensions
|
|
1159
|
+
const width = nodeModel.width || 400;
|
|
1160
|
+
const height = nodeModel.height || 200;
|
|
1161
|
+
const zoomLevel = 2;
|
|
1162
|
+
|
|
1163
|
+
// Handle height measurement (same as regular path)
|
|
1164
|
+
const defaultHeight = module.aiNodeDefaultHeight || 200;
|
|
1165
|
+
const effectiveMaxHeight = module.showFullAiResponse ? 10000 : defaultHeight;
|
|
1166
|
+
const skipMeasure = updateData.skipMeasure === true;
|
|
1167
|
+
|
|
1168
|
+
if ((nodeModel.contentType === 'text' || nodeModel.contentType === 'note') && !nodeModel.isChunkedText && !skipMeasure) {
|
|
1169
|
+
try {
|
|
1170
|
+
const forcedHeight = (nodeModel.height && nodeModel.height > 0) ? (nodeModel.height * zoomLevel) : null;
|
|
1171
|
+
const { height: measuredHeight, maxScrollPx } = await measureHtmlHeightAsync(nodeModel, zoomLevel, width * zoomLevel, forcedHeight, effectiveMaxHeight);
|
|
1172
|
+
if (!nodeModel.height || nodeModel.height <= 0) {
|
|
1173
|
+
nodeModel.height = measuredHeight;
|
|
1174
|
+
}
|
|
1175
|
+
nodeModel.maxScroll = maxScrollPx / zoomLevel;
|
|
1176
|
+
nodeModel.scrollOffset = Math.min(nodeModel.scrollOffset || 0, nodeModel.maxScroll);
|
|
1177
|
+
} catch (error) {
|
|
1178
|
+
console.warn('[TextureFactory] Atlas mode height measure failed', error);
|
|
1179
|
+
if (!nodeModel.height || nodeModel.height <= 0) {
|
|
1180
|
+
nodeModel.height = 200;
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
// Render canvases
|
|
1186
|
+
const [bodyCanvasData, selectedCanvasData] = await Promise.all([
|
|
1187
|
+
renderNodeToCanvas(module, nodeModel, false, imageCache),
|
|
1188
|
+
updateData.skipSelected ? Promise.resolve(null) : renderNodeToCanvas(module, nodeModel, true, imageCache)
|
|
1189
|
+
]);
|
|
1190
|
+
|
|
1191
|
+
// Create glow and tail canvases
|
|
1192
|
+
const styleW = (nodeModel.width || 400) * zoomLevel;
|
|
1193
|
+
const styleH = (nodeModel.height || 200) * zoomLevel;
|
|
1194
|
+
const radius = 12 * zoomLevel;
|
|
1195
|
+
const glowData = createGlowTexture(styleW, styleH, radius, zoomLevel);
|
|
1196
|
+
|
|
1197
|
+
const tailColor = '#fefefe';
|
|
1198
|
+
const tailDefaultCanvas = createTailTexture(tailColor, '#374151', zoomLevel, 12, false);
|
|
1199
|
+
const tailSelectedCanvas = updateData.skipSelected ? null : createTailTexture(tailColor, '#2563eb', zoomLevel, 12, true);
|
|
1200
|
+
|
|
1201
|
+
// Add to Atlas
|
|
1202
|
+
const textures = {
|
|
1203
|
+
body: bodyCanvasData.canvas,
|
|
1204
|
+
selected: selectedCanvasData?.canvas || null,
|
|
1205
|
+
tail: tailDefaultCanvas,
|
|
1206
|
+
tailSelected: tailSelectedCanvas,
|
|
1207
|
+
glow: glowData.canvas
|
|
1208
|
+
};
|
|
1209
|
+
|
|
1210
|
+
const uvMap = atlasManager.addNode(nodeModel.id, textures, {
|
|
1211
|
+
width: bodyCanvasData.width,
|
|
1212
|
+
height: bodyCanvasData.height
|
|
1213
|
+
});
|
|
1214
|
+
|
|
1215
|
+
const t1 = performance.now();
|
|
1216
|
+
console.log(`[⏱️ Atlas] generateTexturesWithAtlas for ${nodeModel.id}: ${(t1 - t0).toFixed(1)}ms, UV: ${uvMap ? 'OK' : 'FAILED'}`);
|
|
1217
|
+
|
|
1218
|
+
// Return in compatible format
|
|
1219
|
+
// Note: For atlas mode, we don't create individual THREE.Textures
|
|
1220
|
+
// The mesh will use shared atlas textures from atlasManager
|
|
1221
|
+
return {
|
|
1222
|
+
textures: {
|
|
1223
|
+
default: { body: null, tail: null, uvMap: uvMap?.body },
|
|
1224
|
+
selected: { body: null, tail: null, uvMap: uvMap?.selected },
|
|
1225
|
+
glow: { texture: null, canvas: glowData.canvas, uvMap: uvMap?.glow }
|
|
1226
|
+
},
|
|
1227
|
+
uvMap,
|
|
1228
|
+
width: nodeModel.width,
|
|
1229
|
+
height: nodeModel.height,
|
|
1230
|
+
isAtlasMode: true
|
|
1231
|
+
};
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
/**
|
|
1235
|
+
* Update a node's texture in the atlas (in-place update)
|
|
1236
|
+
*/
|
|
1237
|
+
async function updateAtlasTexture(module, nodeModel, textureType, imageCache) {
|
|
1238
|
+
if (!module.atlasManager) return false;
|
|
1239
|
+
|
|
1240
|
+
const canvasData = await renderNodeToCanvas(module, nodeModel, textureType === 'selected', imageCache);
|
|
1241
|
+
return module.atlasManager.updateNode(nodeModel.id, textureType, canvasData.canvas);
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
window.MindMapTextureFactory = {
|
|
1245
|
+
generateAndCacheTextures,
|
|
1246
|
+
generatePlaceholderTextures,
|
|
1247
|
+
measureHtmlHeightAsync,
|
|
1248
|
+
ensureImageCached,
|
|
1249
|
+
// Atlas integration
|
|
1250
|
+
renderNodeToCanvas,
|
|
1251
|
+
generateTexturesWithAtlas,
|
|
1252
|
+
updateAtlasTexture,
|
|
1253
|
+
clearCache: () => tailTextureCache.clear(),
|
|
1254
|
+
registerRenderer: (type, renderer) => renderers.set(type, renderer)
|
|
1255
|
+
};
|
|
1256
|
+
|
|
1257
|
+
console.log('mind-map-texture-factory.js initialized (folded corners, spinner, atlas support)');
|
|
1258
|
+
})();
|