@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,2260 @@
|
|
|
1
|
+
(function () {
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
// ▼▼▼ [Perf] Debug logging - set to false in production ▼▼▼
|
|
5
|
+
const DEBUG = false;
|
|
6
|
+
const log = DEBUG ? console.log.bind(console) : () => { };
|
|
7
|
+
const warn = DEBUG ? console.warn.bind(console) : () => { };
|
|
8
|
+
// ▲▲▲ [Perf] ▲▲▲
|
|
9
|
+
|
|
10
|
+
// ★ [메모리 보호] 텍스처 최대 높이 제한
|
|
11
|
+
// 4000px 높이 노드 = 64MB+ 메모리 → 2048px로 제한 = 최대 ~8MB
|
|
12
|
+
const MAX_TEXTURE_HEIGHT = 2048;
|
|
13
|
+
|
|
14
|
+
const tailTextureCache = new Map();
|
|
15
|
+
const renderers = new Map();
|
|
16
|
+
const failedImageAssetCache = new Map();
|
|
17
|
+
const pendingImageAssetFetches = new Map();
|
|
18
|
+
let sharedMeasurer = null;
|
|
19
|
+
|
|
20
|
+
function getSharedMeasurer() {
|
|
21
|
+
if (sharedMeasurer) return sharedMeasurer;
|
|
22
|
+
sharedMeasurer = document.createElement('div');
|
|
23
|
+
Object.assign(sharedMeasurer.style, {
|
|
24
|
+
position: 'absolute',
|
|
25
|
+
visibility: 'hidden',
|
|
26
|
+
left: '-9999px',
|
|
27
|
+
top: '-9999px',
|
|
28
|
+
zIndex: '-1000',
|
|
29
|
+
pointerEvents: 'none',
|
|
30
|
+
margin: '0',
|
|
31
|
+
padding: '0',
|
|
32
|
+
boxSizing: 'border-box',
|
|
33
|
+
width: 'auto'
|
|
34
|
+
});
|
|
35
|
+
document.body.appendChild(sharedMeasurer);
|
|
36
|
+
return sharedMeasurer;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function isNonRefreshableLocalUrl(url) {
|
|
40
|
+
if (!url || typeof url !== 'string') return true;
|
|
41
|
+
const normalized = url.trim().toLowerCase();
|
|
42
|
+
return normalized.startsWith('blob:')
|
|
43
|
+
|| normalized.startsWith('data:')
|
|
44
|
+
|| normalized.startsWith('file:');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function requestFreshNodeAssetUrl(nodeId, failedUrl, options = {}) {
|
|
48
|
+
if (!nodeId || options.skipAssetRefresh === true || isNonRefreshableLocalUrl(failedUrl)) {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const helper = window.dotNetHelper;
|
|
53
|
+
if (!helper || typeof helper.invokeMethodAsync !== 'function') {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
try {
|
|
58
|
+
const resolveMethodName = options.preferOriginal === true
|
|
59
|
+
? 'ResolveNodeAssetUrl'
|
|
60
|
+
: 'ResolveNodePreviewAssetUrl';
|
|
61
|
+
const refreshedUrl = await helper.invokeMethodAsync(resolveMethodName, String(nodeId));
|
|
62
|
+
const normalized = typeof refreshedUrl === 'string' ? refreshedUrl.trim() : '';
|
|
63
|
+
if (!normalized || normalized === String(failedUrl || '').trim()) {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return normalized;
|
|
68
|
+
} catch (error) {
|
|
69
|
+
warn('[TextureFactory] Failed to refresh node asset URL', nodeId, error);
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function normalizeImageAssetUrl(url) {
|
|
75
|
+
if (typeof url !== 'string') {
|
|
76
|
+
return '';
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const trimmed = url.trim();
|
|
80
|
+
if (!trimmed) {
|
|
81
|
+
return '';
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return trimmed
|
|
85
|
+
.replace(/^http:\/\/localhost(?=:\d+\/assets\/)/i, 'http://127.0.0.1')
|
|
86
|
+
.replace(/^http:\/\/\[::1\](?=:\d+\/assets\/)/i, 'http://127.0.0.1');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function isLoopbackImageAssetUrl(url) {
|
|
90
|
+
return /^http:\/\/(?:127(?:\.\d{1,3}){3}|localhost|\[::1\])(?::\d+)?\/assets\//i
|
|
91
|
+
.test(String(url || '').trim());
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function getCanonicalImageAssetKey(url) {
|
|
95
|
+
const normalized = normalizeImageAssetUrl(url);
|
|
96
|
+
if (!normalized) {
|
|
97
|
+
return '';
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const withoutQuery = normalized.split(/[?#]/)[0];
|
|
101
|
+
const lower = withoutQuery.toLowerCase();
|
|
102
|
+
const thumbsMarker = '/assets/thumbs/';
|
|
103
|
+
const assetsMarker = '/assets/';
|
|
104
|
+
|
|
105
|
+
if (lower.includes(thumbsMarker)) {
|
|
106
|
+
const assetName = withoutQuery.slice(lower.lastIndexOf(thumbsMarker) + thumbsMarker.length).split('/').pop() || '';
|
|
107
|
+
const dotIndex = assetName.lastIndexOf('.');
|
|
108
|
+
return (dotIndex > 0 ? assetName.slice(0, dotIndex) : assetName).toLowerCase();
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (lower.includes(assetsMarker)) {
|
|
112
|
+
const assetName = withoutQuery.slice(lower.lastIndexOf(assetsMarker) + assetsMarker.length).split('/').pop() || '';
|
|
113
|
+
const dotIndex = assetName.lastIndexOf('.');
|
|
114
|
+
return (dotIndex > 0 ? assetName.slice(0, dotIndex) : assetName).toLowerCase();
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return withoutQuery.toLowerCase();
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function areImageAssetUrlsEquivalent(leftUrl, rightUrl) {
|
|
121
|
+
const leftKey = getCanonicalImageAssetKey(leftUrl);
|
|
122
|
+
const rightKey = getCanonicalImageAssetKey(rightUrl);
|
|
123
|
+
if (leftKey && rightKey) {
|
|
124
|
+
return leftKey === rightKey;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return normalizeImageAssetUrl(leftUrl) === normalizeImageAssetUrl(rightUrl);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function isThumbnailAssetUrl(url) {
|
|
131
|
+
return normalizeImageAssetUrl(url).toLowerCase().includes('/assets/thumbs/');
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function getImageSourceKind(url) {
|
|
135
|
+
return isThumbnailAssetUrl(url) ? 'thumbnail' : 'original';
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function looksLikeExtensionlessAssetUrl(url) {
|
|
139
|
+
const normalized = normalizeImageAssetUrl(url);
|
|
140
|
+
if (!normalized || isNonRefreshableLocalUrl(normalized) || isThumbnailAssetUrl(normalized)) {
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const withoutQuery = normalized.split(/[?#]/)[0];
|
|
145
|
+
const lower = withoutQuery.toLowerCase();
|
|
146
|
+
if (!lower.includes('/assets/')) {
|
|
147
|
+
return false;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const lastSegment = withoutQuery.split('/').pop() || '';
|
|
151
|
+
return !!lastSegment && !lastSegment.includes('.');
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function looksLikeRelativeAssetUrl(url) {
|
|
155
|
+
const normalized = normalizeImageAssetUrl(url);
|
|
156
|
+
if (!normalized || isNonRefreshableLocalUrl(normalized)) {
|
|
157
|
+
return false;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const lower = normalized.toLowerCase();
|
|
161
|
+
if (lower.startsWith('/assets/')) {
|
|
162
|
+
return true;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return lower.startsWith('assets/');
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function shouldResolveNodeAssetUrlBeforeFetch(url, options = {}) {
|
|
169
|
+
const normalized = normalizeImageAssetUrl(url);
|
|
170
|
+
if (!normalized || isNonRefreshableLocalUrl(normalized)) {
|
|
171
|
+
return false;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (looksLikeExtensionlessAssetUrl(normalized) || looksLikeRelativeAssetUrl(normalized)) {
|
|
175
|
+
return true;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return false;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function getImageCacheNow() {
|
|
182
|
+
return typeof performance !== 'undefined' ? performance.now() : Date.now();
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function getImageErrorStatus(error) {
|
|
186
|
+
const status = Number(error?.status || error?.errorStatus || error?.response?.status || 0);
|
|
187
|
+
return Number.isFinite(status) && status > 0 ? status : 0;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function getImageErrorRetryDelayMs(errorStatus) {
|
|
191
|
+
if (errorStatus === 404) {
|
|
192
|
+
return 60 * 1000;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return 15 * 1000;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function isImageContentType(value) {
|
|
199
|
+
const normalized = String(value || '').trim().toLowerCase();
|
|
200
|
+
return !normalized || normalized.startsWith('image/');
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function createUnexpectedImageContentTypeError(targetUrl, contentType, blob = null) {
|
|
204
|
+
const error = new Error(`Unexpected image content-type: ${contentType || blob?.type || 'unknown'}`);
|
|
205
|
+
error.status = 415;
|
|
206
|
+
error.url = targetUrl;
|
|
207
|
+
error.contentType = contentType || '';
|
|
208
|
+
error.blobType = blob?.type || '';
|
|
209
|
+
error.blobSize = Number(blob?.size || 0);
|
|
210
|
+
return error;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
async function readImageBlobFromResponse(response, targetUrl) {
|
|
214
|
+
const contentType = response.headers?.get?.('content-type') || '';
|
|
215
|
+
if (!isImageContentType(contentType)) {
|
|
216
|
+
throw createUnexpectedImageContentTypeError(targetUrl, contentType);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const blob = await response.blob();
|
|
220
|
+
if (!isImageContentType(blob?.type || contentType)) {
|
|
221
|
+
throw createUnexpectedImageContentTypeError(targetUrl, contentType, blob);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return blob;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function createCachedImageErrorEntry(url, resizeWidth, errorStatus, overrides = {}) {
|
|
228
|
+
const overrideFailedAt = Number(overrides?.failedAt || 0);
|
|
229
|
+
const now = Number.isFinite(overrideFailedAt) && overrideFailedAt > 0
|
|
230
|
+
? overrideFailedAt
|
|
231
|
+
: getImageCacheNow();
|
|
232
|
+
const overrideRetryAfter = Number(overrides?.retryAfter || 0);
|
|
233
|
+
const retryAfter = Number.isFinite(overrideRetryAfter) && overrideRetryAfter > 0
|
|
234
|
+
? overrideRetryAfter
|
|
235
|
+
: now + getImageErrorRetryDelayMs(errorStatus);
|
|
236
|
+
const errorCacheKey = String(overrides?.errorCacheKey || getImageAssetErrorCacheKey(url) || '');
|
|
237
|
+
return {
|
|
238
|
+
image: null,
|
|
239
|
+
url,
|
|
240
|
+
width: 0,
|
|
241
|
+
height: 0,
|
|
242
|
+
requestedWidth: resizeWidth,
|
|
243
|
+
sourceKind: getImageSourceKind(url),
|
|
244
|
+
assetKey: String(overrides?.assetKey || getCanonicalImageAssetKey(url) || ''),
|
|
245
|
+
errorCacheKey,
|
|
246
|
+
isError: true,
|
|
247
|
+
errorStatus,
|
|
248
|
+
failedAt: now,
|
|
249
|
+
retryAfter
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function getImageAssetErrorCacheKey(url) {
|
|
254
|
+
const normalized = normalizeImageAssetUrl(url);
|
|
255
|
+
if (!normalized) {
|
|
256
|
+
return '';
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const sourceKind = getImageSourceKind(normalized);
|
|
260
|
+
const assetKey = getCanonicalImageAssetKey(normalized) || normalized;
|
|
261
|
+
return `${sourceKind}:${assetKey}`;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function getFailedImageAssetEntry(url) {
|
|
265
|
+
const cacheKey = getImageAssetErrorCacheKey(url);
|
|
266
|
+
if (!cacheKey) {
|
|
267
|
+
return null;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const failedEntry = failedImageAssetCache.get(cacheKey) || null;
|
|
271
|
+
if (!failedEntry) {
|
|
272
|
+
return null;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const retryAfter = Number(failedEntry.retryAfter || 0);
|
|
276
|
+
if (retryAfter <= getImageCacheNow()) {
|
|
277
|
+
failedImageAssetCache.delete(cacheKey);
|
|
278
|
+
return null;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
return failedEntry;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function storeFailedImageAssetEntry(errorEntry) {
|
|
285
|
+
const cacheKey = String(errorEntry?.errorCacheKey || getImageAssetErrorCacheKey(errorEntry?.url || '') || '');
|
|
286
|
+
if (!cacheKey) {
|
|
287
|
+
return errorEntry;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
failedImageAssetCache.set(cacheKey, {
|
|
291
|
+
url: String(errorEntry?.url || ''),
|
|
292
|
+
assetKey: String(errorEntry?.assetKey || ''),
|
|
293
|
+
sourceKind: String(errorEntry?.sourceKind || ''),
|
|
294
|
+
errorStatus: Number(errorEntry?.errorStatus || 0),
|
|
295
|
+
failedAt: Number(errorEntry?.failedAt || 0),
|
|
296
|
+
retryAfter: Number(errorEntry?.retryAfter || 0),
|
|
297
|
+
errorCacheKey: cacheKey
|
|
298
|
+
});
|
|
299
|
+
return errorEntry;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function clearFailedImageAssetEntry(url) {
|
|
303
|
+
const cacheKey = getImageAssetErrorCacheKey(url);
|
|
304
|
+
if (!cacheKey) {
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
failedImageAssetCache.delete(cacheKey);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function createSyntheticImageFetchError(failedEntry, url) {
|
|
312
|
+
const status = Number(failedEntry?.errorStatus || 0);
|
|
313
|
+
const error = new Error(status > 0 ? `HTTP ${status}` : 'Cached image fetch failure');
|
|
314
|
+
error.status = status;
|
|
315
|
+
error.url = url;
|
|
316
|
+
error.retryAfter = Number(failedEntry?.retryAfter || 0);
|
|
317
|
+
error.failedAt = Number(failedEntry?.failedAt || 0);
|
|
318
|
+
error.assetKey = String(failedEntry?.assetKey || '');
|
|
319
|
+
error.errorCacheKey = String(failedEntry?.errorCacheKey || '');
|
|
320
|
+
return error;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function getRequestedResizeWidth(options = {}) {
|
|
324
|
+
const parsed = Number(options.resizeWidth);
|
|
325
|
+
if (Number.isFinite(parsed) && parsed > 0) {
|
|
326
|
+
return Math.max(1, Math.round(parsed));
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
return 512;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const IMAGE_CACHE_RECORD_FLAG = '__mindMapImageRecord';
|
|
333
|
+
|
|
334
|
+
function isImageCacheRecord(entry) {
|
|
335
|
+
return !!entry && entry[IMAGE_CACHE_RECORD_FLAG] === true;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function normalizeImageCacheVariantKind(value) {
|
|
339
|
+
if (String(value || '').toLowerCase() === 'thumbnail' || isThumbnailAssetUrl(value || '')) {
|
|
340
|
+
return 'thumbnail';
|
|
341
|
+
}
|
|
342
|
+
return 'original';
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function createImageCacheRecord(existing = null) {
|
|
346
|
+
if (isImageCacheRecord(existing)) {
|
|
347
|
+
return existing;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
const record = {
|
|
351
|
+
[IMAGE_CACHE_RECORD_FLAG]: true,
|
|
352
|
+
thumbnail: null,
|
|
353
|
+
original: null,
|
|
354
|
+
fullResError: null,
|
|
355
|
+
lastRequestedKind: '',
|
|
356
|
+
lastAccessAt: 0
|
|
357
|
+
};
|
|
358
|
+
|
|
359
|
+
if (existing && typeof existing === 'object') {
|
|
360
|
+
const variantKind = normalizeImageCacheVariantKind(
|
|
361
|
+
existing.sourceKind || existing.url || existing.src || existing.image?.src || ''
|
|
362
|
+
);
|
|
363
|
+
record[variantKind] = existing;
|
|
364
|
+
if (existing.fullResError) {
|
|
365
|
+
record.fullResError = { ...existing.fullResError };
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
return record;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function getImageCacheOriginalError(existing) {
|
|
373
|
+
if (!existing) return null;
|
|
374
|
+
if (isImageCacheRecord(existing)) {
|
|
375
|
+
return existing.fullResError
|
|
376
|
+
|| (existing.original?.isError === true ? existing.original : null)
|
|
377
|
+
|| existing.thumbnail?.fullResError
|
|
378
|
+
|| existing.original?.fullResError
|
|
379
|
+
|| null;
|
|
380
|
+
}
|
|
381
|
+
return existing.fullResError
|
|
382
|
+
|| ((existing.isError === true && normalizeImageCacheVariantKind(existing.sourceKind || existing.url || '') === 'original') ? existing : null)
|
|
383
|
+
|| null;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function setImageCacheOriginalError(existing, errorEntry) {
|
|
387
|
+
if (!existing || !errorEntry) return null;
|
|
388
|
+
|
|
389
|
+
const nextError = {
|
|
390
|
+
url: errorEntry.url,
|
|
391
|
+
assetKey: errorEntry.assetKey,
|
|
392
|
+
errorCacheKey: errorEntry.errorCacheKey,
|
|
393
|
+
errorStatus: errorEntry.errorStatus,
|
|
394
|
+
failedAt: errorEntry.failedAt,
|
|
395
|
+
retryAfter: errorEntry.retryAfter
|
|
396
|
+
};
|
|
397
|
+
|
|
398
|
+
if (isImageCacheRecord(existing)) {
|
|
399
|
+
existing.fullResError = nextError;
|
|
400
|
+
if (existing.thumbnail && existing.thumbnail.isError !== true) {
|
|
401
|
+
existing.thumbnail.fullResError = nextError;
|
|
402
|
+
}
|
|
403
|
+
if (existing.original && existing.original.isError !== true) {
|
|
404
|
+
existing.original.fullResError = nextError;
|
|
405
|
+
}
|
|
406
|
+
return existing;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
existing.fullResError = nextError;
|
|
410
|
+
return existing;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
function clearImageCacheOriginalError(existing) {
|
|
414
|
+
if (!existing) return;
|
|
415
|
+
if (isImageCacheRecord(existing)) {
|
|
416
|
+
existing.fullResError = null;
|
|
417
|
+
if (existing.thumbnail) existing.thumbnail.fullResError = null;
|
|
418
|
+
if (existing.original) existing.original.fullResError = null;
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
existing.fullResError = null;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
function getUsableCachedImageEntry(existing) {
|
|
425
|
+
if (!existing) return null;
|
|
426
|
+
if (isImageCacheRecord(existing)) {
|
|
427
|
+
if (existing.thumbnail?.isError !== true && existing.thumbnail?.image) return existing.thumbnail;
|
|
428
|
+
if (existing.original?.isError !== true && existing.original?.image) return existing.original;
|
|
429
|
+
return null;
|
|
430
|
+
}
|
|
431
|
+
return existing.isError !== true && existing.image ? existing : null;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
function canReuseSingleCachedImageEntry(existing, requestedUrl, options = {}) {
|
|
435
|
+
if (!existing) {
|
|
436
|
+
return false;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
const normalizedRequestedUrl = normalizeImageAssetUrl(requestedUrl);
|
|
440
|
+
if (!normalizedRequestedUrl) {
|
|
441
|
+
return false;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
const normalizedOriginalUrl = normalizeImageAssetUrl(options.originalUrl);
|
|
445
|
+
const preferOriginal = options.preferOriginal === true;
|
|
446
|
+
const effectiveRequestedUrl = preferOriginal && normalizedOriginalUrl
|
|
447
|
+
? normalizedOriginalUrl
|
|
448
|
+
: normalizedRequestedUrl;
|
|
449
|
+
const existingUrl = normalizeImageAssetUrl(existing.url || existing.src || existing.image?.src);
|
|
450
|
+
if (!existingUrl) {
|
|
451
|
+
return false;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
if (existing.isError === true) {
|
|
455
|
+
const retryAfter = Number(existing.retryAfter || 0);
|
|
456
|
+
return areImageAssetUrlsEquivalent(existingUrl, effectiveRequestedUrl) && retryAfter > getImageCacheNow();
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
const existingSourceKind = existing.sourceKind || getImageSourceKind(existingUrl);
|
|
460
|
+
const requestedWidth = getRequestedResizeWidth(options);
|
|
461
|
+
const existingRequestedWidth = Number(existing.requestedWidth || 0);
|
|
462
|
+
const existingWidth = Number(existing.width || existing.image?.naturalWidth || existing.image?.width || 0);
|
|
463
|
+
|
|
464
|
+
const matchesRequestedUrl = existingUrl === normalizedRequestedUrl;
|
|
465
|
+
const matchesOriginalUrl = !!normalizedOriginalUrl && existingUrl === normalizedOriginalUrl;
|
|
466
|
+
const canUseOriginalForPreview =
|
|
467
|
+
options.allowOriginalReuseForPreview !== false &&
|
|
468
|
+
preferOriginal !== true &&
|
|
469
|
+
existingSourceKind === 'original' &&
|
|
470
|
+
(matchesOriginalUrl || isThumbnailAssetUrl(normalizedRequestedUrl));
|
|
471
|
+
|
|
472
|
+
if (!(matchesRequestedUrl || matchesOriginalUrl || canUseOriginalForPreview)) {
|
|
473
|
+
return false;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
if (preferOriginal && existingSourceKind !== 'original') {
|
|
477
|
+
return false;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
if (existingSourceKind === 'thumbnail') {
|
|
481
|
+
return true;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
if (!(requestedWidth > 0)) {
|
|
485
|
+
return true;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
return existingRequestedWidth >= requestedWidth || existingWidth >= (requestedWidth * 0.95);
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
function resolveCachedImageEntry(existing, requestedUrl, options = {}) {
|
|
492
|
+
if (!existing) {
|
|
493
|
+
return null;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
if (!isImageCacheRecord(existing)) {
|
|
497
|
+
return canReuseSingleCachedImageEntry(existing, requestedUrl, options) ? existing : null;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
const preferOriginal = options.preferOriginal === true;
|
|
501
|
+
const candidates = preferOriginal
|
|
502
|
+
? [existing.original, existing.thumbnail]
|
|
503
|
+
: [existing.thumbnail, existing.original];
|
|
504
|
+
|
|
505
|
+
for (let i = 0; i < candidates.length; i++) {
|
|
506
|
+
const candidate = candidates[i];
|
|
507
|
+
if (canReuseSingleCachedImageEntry(candidate, requestedUrl, options)) {
|
|
508
|
+
existing.lastRequestedKind = normalizeImageCacheVariantKind(candidate?.sourceKind || candidate?.url || '');
|
|
509
|
+
existing.lastAccessAt = getImageCacheNow();
|
|
510
|
+
return candidate;
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
return null;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
function storeImageCacheEntry(imageCache, nodeId, cachedEntry) {
|
|
518
|
+
const existing = imageCache.get(nodeId) || null;
|
|
519
|
+
const record = createImageCacheRecord(existing);
|
|
520
|
+
const variantKind = normalizeImageCacheVariantKind(cachedEntry?.sourceKind || cachedEntry?.url || '');
|
|
521
|
+
record.lastRequestedKind = variantKind;
|
|
522
|
+
record.lastAccessAt = getImageCacheNow();
|
|
523
|
+
record[variantKind] = cachedEntry;
|
|
524
|
+
|
|
525
|
+
if (variantKind === 'thumbnail') {
|
|
526
|
+
const originalError = getImageCacheOriginalError(record);
|
|
527
|
+
if (originalError && cachedEntry && cachedEntry.isError !== true) {
|
|
528
|
+
cachedEntry.fullResError = originalError;
|
|
529
|
+
}
|
|
530
|
+
} else if (cachedEntry?.isError === true) {
|
|
531
|
+
setImageCacheOriginalError(record, cachedEntry);
|
|
532
|
+
} else {
|
|
533
|
+
clearImageCacheOriginalError(record);
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
imageCache.set(nodeId, record);
|
|
537
|
+
return cachedEntry;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
function getDeferredOriginalImageError(existing, requestedUrl, resizeWidth, options = {}) {
|
|
541
|
+
if (!existing || options.preferOriginal !== true) {
|
|
542
|
+
return null;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
const fallbackEntry = getUsableCachedImageEntry(existing);
|
|
546
|
+
if (!fallbackEntry) {
|
|
547
|
+
return null;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
const originalError = getImageCacheOriginalError(existing);
|
|
551
|
+
const retryAfter = Number(originalError?.retryAfter || 0);
|
|
552
|
+
if (!(retryAfter > getImageCacheNow())) {
|
|
553
|
+
return null;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
const normalizedRequestedUrl = normalizeImageAssetUrl(requestedUrl);
|
|
557
|
+
const normalizedOriginalUrl = normalizeImageAssetUrl(options.originalUrl);
|
|
558
|
+
const effectiveRequestedUrl = normalizedOriginalUrl || normalizedRequestedUrl;
|
|
559
|
+
if (!effectiveRequestedUrl) {
|
|
560
|
+
return null;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
if (!areImageAssetUrlsEquivalent(originalError?.url || '', effectiveRequestedUrl)) {
|
|
564
|
+
return null;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
return createCachedImageErrorEntry(
|
|
568
|
+
effectiveRequestedUrl,
|
|
569
|
+
resizeWidth,
|
|
570
|
+
getImageErrorStatus(originalError),
|
|
571
|
+
{
|
|
572
|
+
failedAt: Number(originalError?.failedAt || 0),
|
|
573
|
+
retryAfter,
|
|
574
|
+
assetKey: String(originalError?.assetKey || getCanonicalImageAssetKey(effectiveRequestedUrl) || '')
|
|
575
|
+
}
|
|
576
|
+
);
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
function rememberOriginalImageError(existing, requestedUrl, resizeWidth, errorStatus, options = {}) {
|
|
580
|
+
if (!existing || options.preferOriginal !== true) {
|
|
581
|
+
return null;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
const fallbackEntry = getUsableCachedImageEntry(existing);
|
|
585
|
+
if (!fallbackEntry) {
|
|
586
|
+
return null;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
const errorEntry = createCachedImageErrorEntry(requestedUrl, resizeWidth, errorStatus);
|
|
590
|
+
setImageCacheOriginalError(existing, errorEntry);
|
|
591
|
+
return storeFailedImageAssetEntry(errorEntry);
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
function canReuseCachedImageEntry(existing, requestedUrl, options = {}) {
|
|
595
|
+
return resolveCachedImageEntry(existing, requestedUrl, options);
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
function preprocessMarkdown(text) {
|
|
599
|
+
if (!text) return '';
|
|
600
|
+
let processed = text.trim();
|
|
601
|
+
processed = processed.replace(/([).]*)(\*\*)(?=[가-힣])/g, '$1 $2');
|
|
602
|
+
processed = processed.replace(/([^\n])\n([\*]{1,2}[\d]+\.|[\d]+\.)/g, '$1\n\n$2');
|
|
603
|
+
processed = processed.replace(/([^\n])\n([-*] )/g, '$1\n\n$2');
|
|
604
|
+
return processed;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
function getFileExtensionLower(pathOrName) {
|
|
608
|
+
if (!pathOrName || typeof pathOrName !== 'string') return '';
|
|
609
|
+
const normalized = pathOrName.split(/[?#]/)[0].trim();
|
|
610
|
+
const fileName = normalized.split(/[\\/]/).pop() || '';
|
|
611
|
+
const dot = fileName.lastIndexOf('.');
|
|
612
|
+
if (dot < 0 || dot === fileName.length - 1) return '';
|
|
613
|
+
return fileName.slice(dot).toLowerCase();
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
function withTypePrefix(label, prefix) {
|
|
617
|
+
if (!label) return '';
|
|
618
|
+
const trimmed = String(label).trim();
|
|
619
|
+
if (!trimmed) return '';
|
|
620
|
+
if (trimmed.startsWith(prefix)) return trimmed;
|
|
621
|
+
return `${prefix} ${trimmed}`;
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
function getDisplayPrompt(nodeModel, rawPrompt) {
|
|
625
|
+
const prompt = String(rawPrompt || '').trim();
|
|
626
|
+
if (!prompt) return '';
|
|
627
|
+
|
|
628
|
+
const contentType = String(nodeModel?.contentType ?? nodeModel?.ContentType ?? '').toLowerCase();
|
|
629
|
+
const sourceFilePath = String(nodeModel?.sourceFilePath ?? nodeModel?.SourceFilePath ?? '');
|
|
630
|
+
const ext = getFileExtensionLower(sourceFilePath) || getFileExtensionLower(prompt);
|
|
631
|
+
const defaultNotePrompts = ['Note', 'Pasted Text', '에이전트 메모'];
|
|
632
|
+
|
|
633
|
+
// AI 노드는 현재 note 타입으로 생성되므로, 기본 노트명이 아닌 note 프롬프트는 ✨ 접두어로 구분
|
|
634
|
+
if (contentType === 'note' && !defaultNotePrompts.includes(prompt)) {
|
|
635
|
+
return withTypePrefix(prompt, '✨');
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
if (contentType === 'directory' || contentType === 'folder') {
|
|
639
|
+
return withTypePrefix(prompt, '📂');
|
|
640
|
+
}
|
|
641
|
+
if (contentType === 'code') {
|
|
642
|
+
return withTypePrefix(prompt, '</>');
|
|
643
|
+
}
|
|
644
|
+
if ((contentType === 'text' || contentType === 'markdown') && (ext === '.txt' || ext === '.md')) {
|
|
645
|
+
return withTypePrefix(prompt, '📄');
|
|
646
|
+
}
|
|
647
|
+
return prompt;
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
const ICON_SPINNER_SVG = `
|
|
651
|
+
<svg viewBox="0 0 512 512" style="width:1em; height:1em; fill:currentColor; vertical-align:-0.125em;">
|
|
652
|
+
<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"/>
|
|
653
|
+
</svg>`;
|
|
654
|
+
|
|
655
|
+
const BROKEN_IMAGE_ICON_SVG = `
|
|
656
|
+
<svg viewBox="0 0 24 24" style="width:100%; height:100%; fill:none; stroke:#9ca3af; stroke-width:2;">
|
|
657
|
+
<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" />
|
|
658
|
+
</svg>`;
|
|
659
|
+
const BROKEN_IMAGE_ICON_DATA_URL = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(BROKEN_IMAGE_ICON_SVG)}`;
|
|
660
|
+
const brokenImageIcon = new Image();
|
|
661
|
+
brokenImageIcon.src = BROKEN_IMAGE_ICON_DATA_URL;
|
|
662
|
+
|
|
663
|
+
const MEMO_TEXTURE_THEMES = {
|
|
664
|
+
coral: { fill: '#fff1eb', border: '#f7b08d', accent: '#ff8052' },
|
|
665
|
+
amber: { fill: '#fff7df', border: '#f4d06f', accent: '#f59e0b' },
|
|
666
|
+
sun: { fill: '#fff9d7', border: '#e7d668', accent: '#ca8a04' },
|
|
667
|
+
sage: { fill: '#eef6e7', border: '#bed79f', accent: '#65a30d' },
|
|
668
|
+
mint: { fill: '#e8faf4', border: '#8fd8c1', accent: '#10b981' },
|
|
669
|
+
sky: { fill: '#e8f4ff', border: '#9dc7f7', accent: '#3b82f6' },
|
|
670
|
+
indigo: { fill: '#eef0ff', border: '#bcc2ff', accent: '#6366f1' },
|
|
671
|
+
violet: { fill: '#f5ecff', border: '#d6b8ff', accent: '#8b5cf6' },
|
|
672
|
+
rose: { fill: '#ffeef2', border: '#f5b2c2', accent: '#f43f5e' },
|
|
673
|
+
gray: { fill: '#f3f4f6', border: '#d1d5db', accent: '#6b7280' },
|
|
674
|
+
slate: { fill: '#f3f5f7', border: '#cbd5e1', accent: '#64748b' }
|
|
675
|
+
};
|
|
676
|
+
|
|
677
|
+
function getNodeMetadata(nodeModel) {
|
|
678
|
+
return nodeModel?.metadata || nodeModel?.Metadata || null;
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
function isAgentStyledMemoNode(nodeModel) {
|
|
682
|
+
const metadata = getNodeMetadata(nodeModel);
|
|
683
|
+
const semanticType = String(metadata?.SemanticType || metadata?.semanticType || '').trim();
|
|
684
|
+
return semanticType === 'MindCanvasAgent' || semanticType === 'AgentCommand';
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
function getMemoTextureTheme(nodeModel) {
|
|
688
|
+
const fallbackKey = 'coral';
|
|
689
|
+
const key = String(getNodeMetadata(nodeModel)?.memoColor || fallbackKey).toLowerCase();
|
|
690
|
+
return MEMO_TEXTURE_THEMES[key] || MEMO_TEXTURE_THEMES[fallbackKey];
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
function getTailColors(nodeModel, isSelected) {
|
|
694
|
+
const contentType = String(nodeModel?.contentType ?? nodeModel?.ContentType ?? '').toLowerCase();
|
|
695
|
+
if (contentType !== 'memo') {
|
|
696
|
+
const neutralSelectedStroke = ['text', 'note', 'code', 'markdown'].includes(contentType)
|
|
697
|
+
? '#7b7ff2'
|
|
698
|
+
: '#7b7ff2';
|
|
699
|
+
const defaultStroke =
|
|
700
|
+
contentType === 'note' || contentType === 'image' || contentType === 'video'
|
|
701
|
+
? 'transparent'
|
|
702
|
+
: 'rgba(55, 65, 81, 0.9)';
|
|
703
|
+
return {
|
|
704
|
+
fill: '#fefefe',
|
|
705
|
+
stroke: isSelected ? neutralSelectedStroke : defaultStroke
|
|
706
|
+
};
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
const theme = getMemoTextureTheme(nodeModel);
|
|
710
|
+
return {
|
|
711
|
+
fill: theme.fill,
|
|
712
|
+
stroke: isSelected ? theme.accent : theme.border
|
|
713
|
+
};
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
function createTailTexture(color, borderColor, zoomLevel, tailSize, isSelected) {
|
|
717
|
+
const key = `${color}-${borderColor}-${zoomLevel}-${isSelected}`;
|
|
718
|
+
if (tailTextureCache.has(key)) return tailTextureCache.get(key);
|
|
719
|
+
|
|
720
|
+
const borderWidth = (isSelected ? 4 : 3) * zoomLevel;
|
|
721
|
+
const tailHeight = tailSize * zoomLevel;
|
|
722
|
+
const tailWidth = tailHeight * 2;
|
|
723
|
+
const canvas = document.createElement('canvas');
|
|
724
|
+
canvas.width = tailWidth + borderWidth * 2;
|
|
725
|
+
canvas.height = tailHeight + borderWidth;
|
|
726
|
+
const ctx = canvas.getContext('2d');
|
|
727
|
+
|
|
728
|
+
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
729
|
+
ctx.fillStyle = color;
|
|
730
|
+
ctx.beginPath();
|
|
731
|
+
ctx.moveTo(borderWidth, borderWidth / 2);
|
|
732
|
+
ctx.lineTo(canvas.width / 2, canvas.height - borderWidth / 2);
|
|
733
|
+
ctx.lineTo(canvas.width - borderWidth, borderWidth / 2);
|
|
734
|
+
ctx.closePath();
|
|
735
|
+
ctx.fill();
|
|
736
|
+
|
|
737
|
+
const hasVisibleBorder = !!borderColor && borderColor !== 'transparent' && borderColor !== 'rgba(0, 0, 0, 0)' && borderColor !== 'rgba(0,0,0,0)';
|
|
738
|
+
if (hasVisibleBorder) {
|
|
739
|
+
ctx.strokeStyle = borderColor;
|
|
740
|
+
ctx.lineWidth = borderWidth;
|
|
741
|
+
ctx.beginPath();
|
|
742
|
+
ctx.moveTo(borderWidth, borderWidth / 2);
|
|
743
|
+
ctx.lineTo(canvas.width / 2, canvas.height - borderWidth / 2);
|
|
744
|
+
ctx.lineTo(canvas.width - borderWidth, borderWidth / 2);
|
|
745
|
+
ctx.stroke();
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
const texture = new THREE.CanvasTexture(canvas);
|
|
749
|
+
texture.colorSpace = THREE.SRGBColorSpace;
|
|
750
|
+
texture.generateMipmaps = true;
|
|
751
|
+
texture.minFilter = THREE.LinearMipmapLinearFilter;
|
|
752
|
+
texture.magFilter = THREE.LinearFilter;
|
|
753
|
+
texture._isShared = true; // Shared across nodes via cache
|
|
754
|
+
tailTextureCache.set(key, texture);
|
|
755
|
+
return texture;
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
|
|
759
|
+
function drawRoundedRect(ctx, x, y, width, height, radius) {
|
|
760
|
+
ctx.beginPath();
|
|
761
|
+
ctx.moveTo(x + radius, y);
|
|
762
|
+
ctx.lineTo(x + width - radius, y);
|
|
763
|
+
ctx.arcTo(x + width, y, x + width, y + radius, radius);
|
|
764
|
+
ctx.lineTo(x + width, y + height - radius);
|
|
765
|
+
ctx.arcTo(x + width, y + height, x + width - radius, y + height, radius);
|
|
766
|
+
ctx.lineTo(x + radius, y + height);
|
|
767
|
+
ctx.arcTo(x, y + height, x, y + height - radius, radius); // Bottom-left corner (now rounded)
|
|
768
|
+
ctx.lineTo(x, y + radius);
|
|
769
|
+
ctx.arcTo(x, y, x + radius, y, radius);
|
|
770
|
+
ctx.closePath();
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
// ▼▼▼ [New] Fully rounded rect for image nodes (all corners rounded) ▼▼▼
|
|
774
|
+
function drawFullyRoundedRect(ctx, x, y, width, height, radius) {
|
|
775
|
+
ctx.beginPath();
|
|
776
|
+
ctx.moveTo(x + radius, y);
|
|
777
|
+
ctx.lineTo(x + width - radius, y);
|
|
778
|
+
ctx.arcTo(x + width, y, x + width, y + radius, radius);
|
|
779
|
+
ctx.lineTo(x + width, y + height - radius);
|
|
780
|
+
ctx.arcTo(x + width, y + height, x + width - radius, y + height, radius);
|
|
781
|
+
ctx.lineTo(x + radius, y + height);
|
|
782
|
+
ctx.arcTo(x, y + height, x, y + height - radius, radius); // Bottom-left corner (now rounded)
|
|
783
|
+
ctx.lineTo(x, y + radius);
|
|
784
|
+
ctx.arcTo(x, y, x + radius, y, radius);
|
|
785
|
+
ctx.closePath();
|
|
786
|
+
}
|
|
787
|
+
// ▲▲▲ [New] ▲▲▲
|
|
788
|
+
|
|
789
|
+
function drawScrollbar(ctx, nodeModel, styleCanvasWidth, styleCanvasHeight, zoomLevel, isHovered = false) {
|
|
790
|
+
if (!nodeModel.maxScroll || nodeModel.maxScroll <= 1) return;
|
|
791
|
+
|
|
792
|
+
// ▼▼▼ [FIX] 슬림 스크롤바 - 너비와 패딩 축소 ▼▼▼
|
|
793
|
+
const scrollbarWidth = 4 * zoomLevel; // 6 → 4 (더 슬림하게)
|
|
794
|
+
const scrollbarPadding = 2 * zoomLevel; // 4 → 2 (오른쪽 여백 최소화)
|
|
795
|
+
// ▲▲▲ [FIX] ▲▲▲
|
|
796
|
+
|
|
797
|
+
const trackColor = 'rgba(0, 0, 0, 0.05)';
|
|
798
|
+
|
|
799
|
+
const thumbColor = isHovered ? 'rgba(0, 0, 0, 0.5)' : 'rgba(0, 0, 0, 0.25)';
|
|
800
|
+
const thumbRadius = 2 * zoomLevel; // 3 → 2 (슬림하게)
|
|
801
|
+
const minThumbHeight = 24 * zoomLevel;
|
|
802
|
+
|
|
803
|
+
const topMargin = (nodeModel.contentType === 'note' ? 20 : 16) * zoomLevel;
|
|
804
|
+
const bottomMargin = 16 * zoomLevel;
|
|
805
|
+
|
|
806
|
+
const trackX = styleCanvasWidth - scrollbarWidth - scrollbarPadding;
|
|
807
|
+
const trackY = topMargin;
|
|
808
|
+
const trackHeight = styleCanvasHeight - topMargin - bottomMargin;
|
|
809
|
+
|
|
810
|
+
if (trackHeight <= minThumbHeight) return;
|
|
811
|
+
|
|
812
|
+
ctx.fillStyle = trackColor;
|
|
813
|
+
drawRoundedRect(ctx, trackX, trackY, scrollbarWidth, trackHeight, thumbRadius);
|
|
814
|
+
ctx.fill();
|
|
815
|
+
|
|
816
|
+
const topPaddingContent = (nodeModel.contentType === 'note' ? 16 : 12) * zoomLevel;
|
|
817
|
+
const bottomPaddingContent = 16 * zoomLevel;
|
|
818
|
+
const borderWidth = 3 * zoomLevel;
|
|
819
|
+
const visibleContentHeight = styleCanvasHeight - (topPaddingContent + borderWidth) - (bottomPaddingContent + borderWidth);
|
|
820
|
+
|
|
821
|
+
let totalContentHeight;
|
|
822
|
+
if (nodeModel.isChunkedText && nodeModel.totalContentHeight > 0) {
|
|
823
|
+
totalContentHeight = nodeModel.totalContentHeight * zoomLevel;
|
|
824
|
+
} else {
|
|
825
|
+
totalContentHeight = visibleContentHeight + (nodeModel.maxScroll * zoomLevel);
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
let thumbHeight = (visibleContentHeight / totalContentHeight) * trackHeight;
|
|
829
|
+
thumbHeight = Math.max(minThumbHeight, Math.min(trackHeight, thumbHeight));
|
|
830
|
+
|
|
831
|
+
const currentScroll = (nodeModel.scrollOffset || 0) * zoomLevel;
|
|
832
|
+
const maxScrollWorld = nodeModel.maxScroll * zoomLevel;
|
|
833
|
+
|
|
834
|
+
let scrollRatio = currentScroll / maxScrollWorld;
|
|
835
|
+
scrollRatio = Math.max(0, Math.min(1, isNaN(scrollRatio) ? 0 : scrollRatio));
|
|
836
|
+
|
|
837
|
+
const availableTrackSpace = trackHeight - thumbHeight;
|
|
838
|
+
const thumbY = trackY + (availableTrackSpace * scrollRatio);
|
|
839
|
+
|
|
840
|
+
ctx.fillStyle = thumbColor;
|
|
841
|
+
drawRoundedRect(ctx, trackX, thumbY, scrollbarWidth, thumbHeight, thumbRadius);
|
|
842
|
+
ctx.fill();
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
function getBodyColors(contentType, isSelected, nodeModel = null) {
|
|
846
|
+
const contentTypeLower = String(contentType || '').toLowerCase();
|
|
847
|
+
if (contentTypeLower === 'memo') {
|
|
848
|
+
const theme = getMemoTextureTheme(nodeModel);
|
|
849
|
+
return {
|
|
850
|
+
fill: theme.fill,
|
|
851
|
+
stroke: isSelected ? theme.accent : theme.border
|
|
852
|
+
};
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
const neutralSelectedStroke = ['text', 'note', 'code', 'markdown'].includes(contentTypeLower)
|
|
856
|
+
? '#7b7ff2'
|
|
857
|
+
: '#7b7ff2';
|
|
858
|
+
|
|
859
|
+
return {
|
|
860
|
+
fill: '#fefefe',
|
|
861
|
+
stroke: isSelected ? neutralSelectedStroke : 'rgba(55, 65, 81, 0.9)'
|
|
862
|
+
};
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
function shouldDrawBodyStroke(contentType, isSelected, options = {}) {
|
|
866
|
+
if (options.skipBorder) {
|
|
867
|
+
return false;
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
const contentTypeLower = String(contentType || '').toLowerCase();
|
|
871
|
+
if (contentTypeLower === 'image') {
|
|
872
|
+
return false;
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
if (contentTypeLower === 'note') {
|
|
876
|
+
return false;
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
return true;
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
function drawNodeBodyBackground(ctx, canvasWidth, canvasHeight, zoomLevel, isSelected, contentType = 'text', nodeModel = null, options = {}) {
|
|
883
|
+
const radius = 0; // No rounded corners
|
|
884
|
+
const borderWidth = 3 * zoomLevel;
|
|
885
|
+
const halfBorder = borderWidth / 2;
|
|
886
|
+
|
|
887
|
+
const x = halfBorder;
|
|
888
|
+
const y = halfBorder;
|
|
889
|
+
const w = canvasWidth - borderWidth;
|
|
890
|
+
const h = canvasHeight - borderWidth;
|
|
891
|
+
const right = x + w;
|
|
892
|
+
const bottom = y + h;
|
|
893
|
+
|
|
894
|
+
ctx.clearRect(0, 0, canvasWidth, canvasHeight);
|
|
895
|
+
ctx.beginPath();
|
|
896
|
+
ctx.moveTo(x + radius, y);
|
|
897
|
+
ctx.lineTo(right - radius, y);
|
|
898
|
+
ctx.quadraticCurveTo(right, y, right, y + radius);
|
|
899
|
+
ctx.lineTo(right, bottom - radius);
|
|
900
|
+
ctx.quadraticCurveTo(right, bottom, right - radius, bottom);
|
|
901
|
+
ctx.lineTo(x + radius, bottom);
|
|
902
|
+
ctx.quadraticCurveTo(x, bottom, x, bottom - radius);
|
|
903
|
+
ctx.lineTo(x, y + radius);
|
|
904
|
+
ctx.quadraticCurveTo(x, y, x + radius, y);
|
|
905
|
+
ctx.closePath();
|
|
906
|
+
|
|
907
|
+
const { fill, stroke } = getBodyColors(contentType, isSelected, nodeModel);
|
|
908
|
+
ctx.fillStyle = fill;
|
|
909
|
+
ctx.fill();
|
|
910
|
+
|
|
911
|
+
if (shouldDrawBodyStroke(contentType, isSelected, options)) {
|
|
912
|
+
ctx.strokeStyle = stroke;
|
|
913
|
+
ctx.lineWidth = borderWidth;
|
|
914
|
+
ctx.stroke();
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
function drawNodeBodyOnCanvas(ctx, nodeModel, canvasWidth, canvasHeight, zoomLevel, isSelected, options = {}) {
|
|
919
|
+
const { skipText = false, skipBorder = false } = options; // ★ Troika 사용 시 텍스트 렌더링 건너뛰기
|
|
920
|
+
|
|
921
|
+
drawNodeBodyBackground(ctx, canvasWidth, canvasHeight, zoomLevel, isSelected, nodeModel.contentType, nodeModel, { skipBorder });
|
|
922
|
+
|
|
923
|
+
// ★ skipText가 true면 배경만 그리고 텍스트는 건너뛰기 (Troika가 처리)
|
|
924
|
+
if (skipText) {
|
|
925
|
+
return;
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
ctx.save();
|
|
929
|
+
|
|
930
|
+
const BORDER_WIDTH = 3 * zoomLevel;
|
|
931
|
+
const CSS_PADDING = 16 * zoomLevel;
|
|
932
|
+
const totalPadding = CSS_PADDING + BORDER_WIDTH;
|
|
933
|
+
const topPadding = ((nodeModel.contentType === 'note' ? 16 : 12) * zoomLevel) + BORDER_WIDTH;
|
|
934
|
+
const paddingBottom = 16 * zoomLevel;
|
|
935
|
+
const textWidth = canvasWidth - totalPadding * 2;
|
|
936
|
+
const lineHeight = 14 * zoomLevel * 1.5;
|
|
937
|
+
let currentY = topPadding;
|
|
938
|
+
|
|
939
|
+
ctx.textBaseline = 'top';
|
|
940
|
+
// 기본 Prompt 값들은 표시하지 않음
|
|
941
|
+
const defaultPrompts = ['Note', 'Pasted Text', '에이전트 메모'];
|
|
942
|
+
const rawPrompt = nodeModel.prompt ?? nodeModel.Prompt ?? '';
|
|
943
|
+
const nodePrompt = getDisplayPrompt(nodeModel, rawPrompt);
|
|
944
|
+
const nodeResponse = nodeModel.response ?? nodeModel.Response ?? '';
|
|
945
|
+
const shouldShowPrompt = rawPrompt && !defaultPrompts.includes(rawPrompt);
|
|
946
|
+
if (shouldShowPrompt) {
|
|
947
|
+
ctx.fillStyle = '#111827';
|
|
948
|
+
ctx.font = `bold ${14 * zoomLevel}px sans-serif`;
|
|
949
|
+
ctx.fillText(nodePrompt, totalPadding, currentY, textWidth);
|
|
950
|
+
currentY += 24 * zoomLevel;
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
ctx.fillStyle = '#1f2937';
|
|
954
|
+
ctx.font = `${14 * zoomLevel}px sans-serif`;
|
|
955
|
+
const lines = (nodeResponse || '').split('\n');
|
|
956
|
+
for (const line of lines) {
|
|
957
|
+
ctx.fillText(line, totalPadding, currentY, textWidth);
|
|
958
|
+
currentY += lineHeight;
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
ctx.restore();
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
function normalizeCachedImage(cachedImage) {
|
|
965
|
+
if (!cachedImage) return { image: null, width: 0, height: 0, isError: false };
|
|
966
|
+
|
|
967
|
+
const isError = cachedImage.isError === true;
|
|
968
|
+
|
|
969
|
+
if (cachedImage.image) {
|
|
970
|
+
const img = cachedImage.image;
|
|
971
|
+
const width = cachedImage.width || img.naturalWidth || img.width || 0;
|
|
972
|
+
const height = cachedImage.height || img.naturalHeight || img.height || 0;
|
|
973
|
+
return { image: img, width, height, isError };
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
const img = cachedImage;
|
|
977
|
+
const width = img.naturalWidth || img.width || 0;
|
|
978
|
+
const height = img.naturalHeight || img.height || 0;
|
|
979
|
+
return { image: img, width, height, isError };
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
function drawCachedImageBody(cachedImage, ctx, nodeModel, canvasWidth, canvasHeight, zoomLevel, isSelected) {
|
|
983
|
+
ctx.save();
|
|
984
|
+
// Use simple rect for sharp corners (no arcTo issues with radius=0)
|
|
985
|
+
ctx.beginPath();
|
|
986
|
+
ctx.rect(0, 0, canvasWidth, canvasHeight);
|
|
987
|
+
ctx.clip();
|
|
988
|
+
ctx.fillStyle = '#fefefe';
|
|
989
|
+
ctx.fillRect(0, 0, canvasWidth, canvasHeight);
|
|
990
|
+
|
|
991
|
+
const { image, width: imgWidth, height: imgHeight, isError } = normalizeCachedImage(cachedImage);
|
|
992
|
+
|
|
993
|
+
if (image && imgWidth > 0 && imgHeight > 0) {
|
|
994
|
+
const canvasAspect = canvasWidth / canvasHeight;
|
|
995
|
+
const imgAspect = imgWidth / imgHeight;
|
|
996
|
+
let sx = 0, sy = 0, sWidth = imgWidth, sHeight = imgHeight;
|
|
997
|
+
if (imgAspect > canvasAspect) {
|
|
998
|
+
sWidth = imgHeight * canvasAspect;
|
|
999
|
+
sx = (imgWidth - sWidth) / 2;
|
|
1000
|
+
} else {
|
|
1001
|
+
sHeight = imgWidth / canvasAspect;
|
|
1002
|
+
sy = (imgHeight - sHeight) / 2;
|
|
1003
|
+
}
|
|
1004
|
+
ctx.drawImage(image, sx, sy, sWidth, sHeight, 0, 0, canvasWidth, canvasHeight);
|
|
1005
|
+
}
|
|
1006
|
+
else if (isError) {
|
|
1007
|
+
ctx.fillStyle = '#f3f4f6';
|
|
1008
|
+
ctx.fillRect(0, 0, canvasWidth, canvasHeight);
|
|
1009
|
+
ctx.fillStyle = '#9ca3af';
|
|
1010
|
+
ctx.font = `bold ${16 * zoomLevel}px sans-serif`;
|
|
1011
|
+
ctx.textAlign = 'center';
|
|
1012
|
+
ctx.textBaseline = 'middle';
|
|
1013
|
+
const textY = canvasHeight * 0.55;
|
|
1014
|
+
ctx.fillText('Image Not Found', canvasWidth / 2, textY);
|
|
1015
|
+
|
|
1016
|
+
if (brokenImageIcon.complete) {
|
|
1017
|
+
const iconSize = Math.min(canvasWidth, canvasHeight) * 0.4;
|
|
1018
|
+
ctx.drawImage(brokenImageIcon, (canvasWidth - iconSize) / 2, (canvasHeight - iconSize) / 2 - (8 * zoomLevel), iconSize, iconSize);
|
|
1019
|
+
} else {
|
|
1020
|
+
ctx.strokeStyle = '#9ca3af';
|
|
1021
|
+
ctx.lineWidth = 2 * zoomLevel;
|
|
1022
|
+
ctx.beginPath();
|
|
1023
|
+
ctx.moveTo(canvasWidth * 0.3, canvasHeight * 0.3);
|
|
1024
|
+
ctx.lineTo(canvasWidth * 0.7, canvasHeight * 0.7);
|
|
1025
|
+
ctx.moveTo(canvasWidth * 0.7, canvasHeight * 0.3);
|
|
1026
|
+
ctx.lineTo(canvasWidth * 0.3, canvasHeight * 0.7);
|
|
1027
|
+
ctx.stroke();
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
ctx.strokeStyle = '#ef4444';
|
|
1031
|
+
ctx.setLineDash([5 * zoomLevel, 5 * zoomLevel]);
|
|
1032
|
+
ctx.lineWidth = 2 * zoomLevel;
|
|
1033
|
+
ctx.strokeRect(0, 0, canvasWidth, canvasHeight);
|
|
1034
|
+
ctx.setLineDash([]);
|
|
1035
|
+
}
|
|
1036
|
+
else {
|
|
1037
|
+
ctx.fillStyle = '#f9fafb';
|
|
1038
|
+
ctx.fillRect(0, 0, canvasWidth, canvasHeight);
|
|
1039
|
+
ctx.fillStyle = '#6b7280';
|
|
1040
|
+
ctx.font = `${14 * zoomLevel}px sans-serif`;
|
|
1041
|
+
ctx.textAlign = 'center';
|
|
1042
|
+
ctx.textBaseline = 'middle';
|
|
1043
|
+
ctx.fillText('Loading...', canvasWidth / 2, canvasHeight / 2);
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
const borderWidth = 3 * zoomLevel;
|
|
1047
|
+
ctx.strokeStyle = 'transparent'; // Image nodes have no visible border
|
|
1048
|
+
ctx.lineWidth = borderWidth;
|
|
1049
|
+
// Sharp corners - use strokeRect instead of drawFullyRoundedRect
|
|
1050
|
+
ctx.strokeRect(borderWidth / 2, borderWidth / 2, canvasWidth - borderWidth, canvasHeight - borderWidth);
|
|
1051
|
+
ctx.restore();
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
function getMarkdownStyles(zoomLevel, contentType = 'text', options = {}) {
|
|
1055
|
+
const z = zoomLevel;
|
|
1056
|
+
const fontStack = "ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif";
|
|
1057
|
+
const baseFontSize = 14 * z;
|
|
1058
|
+
const baseLineHeight = 1.6;
|
|
1059
|
+
|
|
1060
|
+
// NOTE:
|
|
1061
|
+
// Markdown is rasterized via SVG <foreignObject> -> Image(). In that path, native scrollbars are
|
|
1062
|
+
// problematic (styling may be ignored, and interaction isn't meaningful on a texture anyway).
|
|
1063
|
+
// We therefore always wrap long lines and avoid native horizontal scrollbars.
|
|
1064
|
+
const preOverflowX = 'hidden';
|
|
1065
|
+
const preWhiteSpace = 'pre-wrap';
|
|
1066
|
+
const preOverflowWrap = 'anywhere';
|
|
1067
|
+
const styles = `
|
|
1068
|
+
div { color: #1f2937; line-height: ${baseLineHeight}; font-family: ${fontStack}; font-size: ${baseFontSize}px; margin: 0; padding: 0; text-align: left; }
|
|
1069
|
+
p { margin: 0 0 ${11 * z}px 0; white-space: normal; }
|
|
1070
|
+
h1 { font-size: ${Math.round(baseFontSize * 1.8)}px; font-weight: 700; margin: ${16 * z}px 0 ${8 * z}px 0; color: #111827; border-bottom: ${1 * z}px solid #eaecef; padding-bottom: ${5 * z}px; }
|
|
1071
|
+
h2 { font-size: ${Math.round(baseFontSize * 1.4)}px; font-weight: 700; margin: ${14 * z}px 0 ${8 * z}px 0; color: #111827; border-bottom: ${1 * z}px solid #eaecef; padding-bottom: ${5 * z}px; }
|
|
1072
|
+
h3, h4 { font-size: ${Math.round(baseFontSize * 1.2)}px; font-weight: 700; margin: ${12 * z}px 0 ${8 * z}px 0; color: #111827; }
|
|
1073
|
+
ul, ol { margin: ${6 * z}px 0 ${14 * z}px ${20 * z}px; padding-left: ${8 * z}px; }
|
|
1074
|
+
li { margin-bottom: ${4 * z}px; }
|
|
1075
|
+
.response > *:first-child { margin-top: 0 !important; }
|
|
1076
|
+
.response > *:last-child { margin-bottom: 0 !important; }
|
|
1077
|
+
li:last-child { margin-bottom: 0 !important; }
|
|
1078
|
+
strong, b { font-weight: 700; color: #111827; }
|
|
1079
|
+
.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; }
|
|
1080
|
+
.icon-wrapper { display: inline-flex; align-items: center; justify-content: center; width: ${16 * z}px; height: ${16 * z}px; color: #4b5563; }
|
|
1081
|
+
hr.node-separator { border: 0; border-top: ${1 * z}px solid #e5e7eb; margin: ${8 * z}px 0 ${12 * z}px 0; }
|
|
1082
|
+
.thinking { color: #6b7280; font-style: italic; display: flex; align-items: center; gap: ${6 * z}px; }
|
|
1083
|
+
.response { margin: 0; }
|
|
1084
|
+
code { background-color: rgba(27,31,35,0.05); padding: ${2 * z}px ${4 * z}px; border-radius: ${3 * z}px; font-family: Consolas, 'Courier New', monospace; font-size: ${Math.round(baseFontSize * 0.85)}px; }
|
|
1085
|
+
pre { background-color: #f6f8fa; padding: ${12 * z}px; border-radius: ${3 * z}px; overflow-x: ${preOverflowX}; margin: ${8 * z}px 0 ${12 * z}px 0; font-family: Consolas, 'Courier New', monospace; white-space: ${preWhiteSpace}; overflow-wrap: ${preOverflowWrap}; }
|
|
1086
|
+
pre code { background-color: transparent; padding: 0; border-radius: 0; }
|
|
1087
|
+
|
|
1088
|
+
/* Scrollbar styling (best-effort)
|
|
1089
|
+
- Some browsers ignore ::-webkit-scrollbar during SVG foreignObject rasterization.
|
|
1090
|
+
- LOD path avoids native scrollbars entirely via overflow-x:hidden above.
|
|
1091
|
+
*/
|
|
1092
|
+
pre { scrollbar-width: thin; scrollbar-color: rgba(0,0,0,0.28) transparent; }
|
|
1093
|
+
pre::-webkit-scrollbar { height: ${Math.max(2, 6 * z)}px; width: ${Math.max(2, 6 * z)}px; }
|
|
1094
|
+
pre::-webkit-scrollbar-track { background: transparent; }
|
|
1095
|
+
pre::-webkit-scrollbar-thumb { background-color: rgba(0,0,0,0.28); border-radius: ${Math.max(2, 6 * z)}px; }
|
|
1096
|
+
pre::-webkit-scrollbar-thumb:hover { background-color: rgba(0,0,0,0.42); }
|
|
1097
|
+
|
|
1098
|
+
blockquote { border-left: ${4 * z}px solid #dfe2e5; padding: 0 ${12 * z}px; margin: 0 0 ${12 * z}px 0; color: #6a737d; }
|
|
1099
|
+
table { border-collapse: collapse; width: 100%; margin: ${8 * z}px 0 ${12 * z}px 0; font-size: ${13 * z}px; }
|
|
1100
|
+
th, td { border: ${1 * z}px solid #d1d5db; padding: ${6 * z}px ${8 * z}px; text-align: left; }
|
|
1101
|
+
th { background-color: #f3f4f6; font-weight: 600; color: #111827; }
|
|
1102
|
+
tr:nth-child(even) { background-color: #f9fafb; }
|
|
1103
|
+
`;
|
|
1104
|
+
return { styles, fontStack };
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
function scopeCss(styles, scopeSelector) {
|
|
1108
|
+
if (!scopeSelector) return styles;
|
|
1109
|
+
return styles.split('\n').map(line => {
|
|
1110
|
+
const trimmed = line.trim();
|
|
1111
|
+
if (!trimmed || trimmed.startsWith('@') || !trimmed.includes('{')) return line;
|
|
1112
|
+
const parts = line.split('{');
|
|
1113
|
+
const selectors = parts[0].trim();
|
|
1114
|
+
const rest = parts.slice(1).join('{');
|
|
1115
|
+
const scopedSelectors = selectors
|
|
1116
|
+
.split(',')
|
|
1117
|
+
.map(s => `${scopeSelector} ${s.trim()}`)
|
|
1118
|
+
.join(', ');
|
|
1119
|
+
return line.replace(selectors, scopedSelectors);
|
|
1120
|
+
}).join('\n');
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
function escapeHtml(value) {
|
|
1124
|
+
return (value || '').replace(/[&<>\"]/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"' }[c]));
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
const ALLOWED_HTML_TAGS = new Set([
|
|
1128
|
+
'div', 'span', 'p', 'br', 'hr', 'a', 'b', 'i', 'u', 's', 'em', 'strong', 'mark',
|
|
1129
|
+
'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
|
|
1130
|
+
'ul', 'ol', 'li', 'dl', 'dt', 'dd',
|
|
1131
|
+
'table', 'thead', 'tbody', 'tfoot', 'tr', 'th', 'td', 'caption', 'colgroup', 'col',
|
|
1132
|
+
'pre', 'code', 'blockquote', 'q', 'cite',
|
|
1133
|
+
'sub', 'sup', 'small', 'big', 'abbr', 'acronym', 'ins', 'del',
|
|
1134
|
+
'input', 'label', 'button', 'select', 'option', 'textarea',
|
|
1135
|
+
'figure', 'figcaption', 'article', 'section', 'aside', 'header', 'footer', 'nav', 'main',
|
|
1136
|
+
'details', 'summary', 'time', 'address', 'wbr', 'kbd', 'samp', 'var', 'dfn'
|
|
1137
|
+
]);
|
|
1138
|
+
|
|
1139
|
+
function sanitizeHtml(html) {
|
|
1140
|
+
if (!html) return '';
|
|
1141
|
+
|
|
1142
|
+
let sanitized = html
|
|
1143
|
+
.replace(/<(script|iframe|object|embed|meta|link|style)[^>]*>[\s\S]*?<\/\1>/gi, '')
|
|
1144
|
+
.replace(/<(script|iframe|object|embed|meta|link)[^>]*\/?>/gi, '');
|
|
1145
|
+
|
|
1146
|
+
sanitized = sanitized.replace(/<img[^>]*\/?>/gi, '');
|
|
1147
|
+
|
|
1148
|
+
sanitized = sanitized.replace(/<([a-zA-Z][a-zA-Z0-9]*)\s*([^>]*)>/g, (match, tagName, attrs) => {
|
|
1149
|
+
if (ALLOWED_HTML_TAGS.has(tagName.toLowerCase())) {
|
|
1150
|
+
return match;
|
|
1151
|
+
}
|
|
1152
|
+
return `<${tagName}${attrs ? ' ' + attrs : ''}>`;
|
|
1153
|
+
});
|
|
1154
|
+
|
|
1155
|
+
sanitized = sanitized.replace(/<\/([a-zA-Z][a-zA-Z0-9]*)>/g, (match, tagName) => {
|
|
1156
|
+
if (ALLOWED_HTML_TAGS.has(tagName.toLowerCase())) {
|
|
1157
|
+
return match;
|
|
1158
|
+
}
|
|
1159
|
+
return `</${tagName}>`;
|
|
1160
|
+
});
|
|
1161
|
+
|
|
1162
|
+
sanitized = sanitized
|
|
1163
|
+
.replace(/<br\s*\/?>/gi, '<br/>')
|
|
1164
|
+
.replace(/<hr\s*\/?>/gi, '<hr/>')
|
|
1165
|
+
.replace(/<input([^>]*[^\/])>/gi, '<input$1 />');
|
|
1166
|
+
|
|
1167
|
+
sanitized = sanitized.replace(/\]\]>/g, ']]>');
|
|
1168
|
+
|
|
1169
|
+
sanitized = sanitized.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F]/g, '');
|
|
1170
|
+
|
|
1171
|
+
return sanitized;
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
function renderMarkdownToCanvasAsync(ctx, texture, nodeModel, canvasWidth, canvasHeight, zoomLevel, isSelected, onComplete = null, options = {}) {
|
|
1175
|
+
const hasMarked = typeof window !== 'undefined' && window.marked && typeof window.marked.parse === 'function';
|
|
1176
|
+
const promptValue = nodeModel.prompt ?? nodeModel.Prompt ?? '';
|
|
1177
|
+
const displayPromptValue = getDisplayPrompt(nodeModel, promptValue);
|
|
1178
|
+
// 기본 Prompt 값들은 표시하지 않음
|
|
1179
|
+
const defaultPrompts = ['Note', 'Pasted Text', '에이전트 메모'];
|
|
1180
|
+
const shouldShowPrompt = promptValue && !defaultPrompts.includes(promptValue);
|
|
1181
|
+
// C#에서 PascalCase (IsLoading)로 전달될 수 있으므로 둘 다 확인
|
|
1182
|
+
const nodeIsLoading = nodeModel.isLoading ?? nodeModel.IsLoading ?? false;
|
|
1183
|
+
const nodeResponse = nodeModel.response ?? nodeModel.Response ?? '';
|
|
1184
|
+
const isLoadingState = nodeIsLoading && !nodeResponse;
|
|
1185
|
+
// note 타입에서도 프롬프트 표시 (CSS3D와 동일하게)
|
|
1186
|
+
const showPromptNow = shouldShowPrompt;
|
|
1187
|
+
let htmlContent = '';
|
|
1188
|
+
|
|
1189
|
+
if (showPromptNow) {
|
|
1190
|
+
htmlContent += `<div class="prompt">${escapeHtml(displayPromptValue)}</div>`;
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
if (isLoadingState) {
|
|
1194
|
+
if (showPromptNow) {
|
|
1195
|
+
htmlContent += '<hr class="node-separator" />';
|
|
1196
|
+
}
|
|
1197
|
+
htmlContent += `<div class="thinking">AI is generating a response... <span class="icon-wrapper">${ICON_SPINNER_SVG}</span></div>`;
|
|
1198
|
+
} else if (nodeResponse) {
|
|
1199
|
+
// Separator between prompt and response (for both text and note types)
|
|
1200
|
+
if (shouldShowPrompt) {
|
|
1201
|
+
htmlContent += '<hr class="node-separator" />';
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
if (nodeModel.isChunkedText) {
|
|
1205
|
+
const contentToRender = nodeModel._visibleContent || nodeModel.response || '';
|
|
1206
|
+
const escapedText = escapeHtml(contentToRender);
|
|
1207
|
+
|
|
1208
|
+
const chunkWhiteSpace = 'pre-wrap';
|
|
1209
|
+
const chunkOverflowWrap = 'anywhere';
|
|
1210
|
+
htmlContent += `<div class="response" style="font-family: 'Consolas', 'Monaco', 'Courier New', monospace; white-space: ${chunkWhiteSpace}; overflow-wrap: ${chunkOverflowWrap}; line-height: 1.5; font-size: ${14 * zoomLevel}px;">${escapedText}</div>`;
|
|
1211
|
+
|
|
1212
|
+
if (nodeModel.totalLineCount > 0 && (nodeModel.scrollOffset || 0) === 0) {
|
|
1213
|
+
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;">
|
|
1214
|
+
Large file (${nodeModel.totalLineCount.toLocaleString()} lines) - Scroll to view more
|
|
1215
|
+
</div>`;
|
|
1216
|
+
}
|
|
1217
|
+
} else {
|
|
1218
|
+
const processedResponse = preprocessMarkdown(nodeModel.response);
|
|
1219
|
+
|
|
1220
|
+
let parsed;
|
|
1221
|
+
if (hasMarked) {
|
|
1222
|
+
try {
|
|
1223
|
+
const renderer = new window.marked.Renderer();
|
|
1224
|
+
|
|
1225
|
+
renderer.code = function (codeOrObj, language, isEscaped) {
|
|
1226
|
+
const code = typeof codeOrObj === 'object' ? codeOrObj.text : codeOrObj;
|
|
1227
|
+
const lang = typeof codeOrObj === 'object' ? codeOrObj.lang : language;
|
|
1228
|
+
|
|
1229
|
+
const escaped = escapeHtml(code || '');
|
|
1230
|
+
const langClass = lang ? ` class="language-${lang}"` : '';
|
|
1231
|
+
return `<pre><code${langClass}>${escaped}</code></pre>`;
|
|
1232
|
+
};
|
|
1233
|
+
|
|
1234
|
+
renderer.codespan = function (codeOrObj) {
|
|
1235
|
+
const code = typeof codeOrObj === 'object' ? codeOrObj.text : codeOrObj;
|
|
1236
|
+
return `<code>${escapeHtml(code || '')}</code>`;
|
|
1237
|
+
};
|
|
1238
|
+
|
|
1239
|
+
parsed = window.marked.parse(processedResponse, {
|
|
1240
|
+
breaks: true,
|
|
1241
|
+
xhtml: true,
|
|
1242
|
+
renderer: renderer
|
|
1243
|
+
});
|
|
1244
|
+
} catch (e) {
|
|
1245
|
+
console.warn('[TextureFactory] marked.parse with custom renderer failed, using default:', e);
|
|
1246
|
+
parsed = window.marked.parse(processedResponse, { breaks: true, xhtml: true });
|
|
1247
|
+
}
|
|
1248
|
+
} else {
|
|
1249
|
+
parsed = `<div style="white-space: pre-wrap;">${escapeHtml(processedResponse)}</div>`;
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1252
|
+
parsed = parsed.replace(/<br\s*\/?>/gi, '<br/>').replace(/<hr\s*\/?>/gi, '<hr/>');
|
|
1253
|
+
parsed = sanitizeHtml(parsed);
|
|
1254
|
+
htmlContent += `<div class="response">${parsed}</div>`;
|
|
1255
|
+
}
|
|
1256
|
+
} else if (nodeModel.isChunkedText && !nodeModel.response) {
|
|
1257
|
+
htmlContent += `<div style="padding:16px; color:#6b7280; text-align:center;">
|
|
1258
|
+
<span style="animation: pulse 1.5s infinite;">⏳</span> Loading...
|
|
1259
|
+
</div>`;
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
const { styles, fontStack } = getMarkdownStyles(zoomLevel, nodeModel.contentType, options);
|
|
1263
|
+
const BORDER_WIDTH = 3 * zoomLevel;
|
|
1264
|
+
const CSS_PADDING = 16 * zoomLevel;
|
|
1265
|
+
const totalPaddingX = CSS_PADDING + BORDER_WIDTH;
|
|
1266
|
+
const cssTopPadding = (nodeModel.contentType === 'note' ? 16 : 12) * zoomLevel;
|
|
1267
|
+
const totalPaddingY = cssTopPadding + BORDER_WIDTH;
|
|
1268
|
+
const paddingBottom = 16 * zoomLevel;
|
|
1269
|
+
const textWidth = Math.max(1, canvasWidth - totalPaddingX * 2);
|
|
1270
|
+
const textHeight = Math.max(1, canvasHeight - totalPaddingY - paddingBottom);
|
|
1271
|
+
|
|
1272
|
+
drawNodeBodyBackground(ctx, canvasWidth, canvasHeight, zoomLevel, isSelected, nodeModel.contentType, nodeModel, options);
|
|
1273
|
+
|
|
1274
|
+
const effectiveScrollOffset = nodeModel.isChunkedText && nodeModel._visibleScrollOffset !== undefined
|
|
1275
|
+
? nodeModel._visibleScrollOffset
|
|
1276
|
+
: (nodeModel.scrollOffset || 0);
|
|
1277
|
+
|
|
1278
|
+
let svgString;
|
|
1279
|
+
|
|
1280
|
+
if (nodeModel.isChunkedText && promptValue) {
|
|
1281
|
+
const headerHeight = 30 * zoomLevel;
|
|
1282
|
+
const bodyHeight = Math.max(1, textHeight - headerHeight);
|
|
1283
|
+
|
|
1284
|
+
const headerHtml = `
|
|
1285
|
+
<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;">
|
|
1286
|
+
<div class="prompt">${escapeHtml(displayPromptValue)}</div>
|
|
1287
|
+
<hr class="node-separator" />
|
|
1288
|
+
</div>`;
|
|
1289
|
+
|
|
1290
|
+
const visibleText = nodeModel._visibleContent || nodeModel.response || '';
|
|
1291
|
+
const bodyContent = visibleText ? escapeHtml(visibleText) : '<span style="color:#9ca3af;">Loading...</span>';
|
|
1292
|
+
const bodyWhiteSpace = 'pre-wrap';
|
|
1293
|
+
const bodyOverflowWrap = 'anywhere';
|
|
1294
|
+
const bodyHtml = `
|
|
1295
|
+
<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:${bodyWhiteSpace}; overflow-wrap:${bodyOverflowWrap}; transform:translateY(-${effectiveScrollOffset * zoomLevel}px);">
|
|
1296
|
+
${bodyContent}
|
|
1297
|
+
</div>`;
|
|
1298
|
+
|
|
1299
|
+
svgString = `
|
|
1300
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="${canvasWidth}" height="${canvasHeight}">
|
|
1301
|
+
<defs>
|
|
1302
|
+
<style type="text/css"><![CDATA[
|
|
1303
|
+
${styles}
|
|
1304
|
+
]]></style>
|
|
1305
|
+
</defs>
|
|
1306
|
+
<!-- Fixed header -->
|
|
1307
|
+
<foreignObject x="${totalPaddingX}" y="${totalPaddingY}" width="${textWidth}" height="${headerHeight}">
|
|
1308
|
+
${headerHtml}
|
|
1309
|
+
</foreignObject>
|
|
1310
|
+
<!-- Scrollable body -->
|
|
1311
|
+
<foreignObject x="${totalPaddingX}" y="${totalPaddingY + headerHeight}" width="${textWidth}" height="${bodyHeight}">
|
|
1312
|
+
${bodyHtml}
|
|
1313
|
+
</foreignObject>
|
|
1314
|
+
</svg>`;
|
|
1315
|
+
} else {
|
|
1316
|
+
const htmlBlock = `
|
|
1317
|
+
<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);">
|
|
1318
|
+
${htmlContent}
|
|
1319
|
+
</div>`;
|
|
1320
|
+
|
|
1321
|
+
svgString = `
|
|
1322
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="${canvasWidth}" height="${canvasHeight}">
|
|
1323
|
+
<defs>
|
|
1324
|
+
<style type="text/css"><![CDATA[
|
|
1325
|
+
${styles}
|
|
1326
|
+
]]></style>
|
|
1327
|
+
</defs>
|
|
1328
|
+
<foreignObject x="${totalPaddingX}" y="${totalPaddingY}" width="${textWidth}" height="${textHeight}">
|
|
1329
|
+
${htmlBlock}
|
|
1330
|
+
</foreignObject>
|
|
1331
|
+
</svg>`;
|
|
1332
|
+
}
|
|
1333
|
+
|
|
1334
|
+
const dataUrl = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svgString)}`;
|
|
1335
|
+
const img = new Image();
|
|
1336
|
+
|
|
1337
|
+
img.onload = () => {
|
|
1338
|
+
ctx.drawImage(img, 0, 0);
|
|
1339
|
+
// ★ skipScrollbar 옵션이 true이면 스크롤바를 여기서 그리지 않음 (외부에서 별도 호출)
|
|
1340
|
+
if (!options.skipScrollbar) {
|
|
1341
|
+
drawScrollbar(ctx, nodeModel, canvasWidth, canvasHeight, zoomLevel, nodeModel.isScrollbarHovered);
|
|
1342
|
+
}
|
|
1343
|
+
texture.needsUpdate = true;
|
|
1344
|
+
if (onComplete) onComplete(true);
|
|
1345
|
+
};
|
|
1346
|
+
|
|
1347
|
+
img.onerror = (err) => {
|
|
1348
|
+
console.error('[TextureFactory] SVG render failed for node:', nodeModel.id);
|
|
1349
|
+
|
|
1350
|
+
try {
|
|
1351
|
+
const parser = new DOMParser();
|
|
1352
|
+
const doc = parser.parseFromString(svgString, 'image/svg+xml');
|
|
1353
|
+
const parseError = doc.querySelector('parsererror');
|
|
1354
|
+
|
|
1355
|
+
if (parseError) {
|
|
1356
|
+
console.error('[TextureFactory] SVG Parse Error:', parseError.textContent);
|
|
1357
|
+
} else {
|
|
1358
|
+
console.error('[TextureFactory] SVG parsed OK but Image failed to load');
|
|
1359
|
+
}
|
|
1360
|
+
} catch (e) {
|
|
1361
|
+
console.error('[TextureFactory] DOMParser error:', e);
|
|
1362
|
+
}
|
|
1363
|
+
|
|
1364
|
+
console.error('[TextureFactory] HTML content (first 500 chars):', htmlContent.substring(0, 500));
|
|
1365
|
+
console.error('[TextureFactory] HTML content (last 200 chars):', htmlContent.substring(Math.max(0, htmlContent.length - 200)));
|
|
1366
|
+
|
|
1367
|
+
drawNodeBodyOnCanvas(ctx, nodeModel, canvasWidth, canvasHeight, zoomLevel, isSelected, options);
|
|
1368
|
+
// ★ skipScrollbar 옵션 체크 (img.onload와 동일하게)
|
|
1369
|
+
if (!options.skipScrollbar) {
|
|
1370
|
+
drawScrollbar(ctx, nodeModel, canvasWidth, canvasHeight, zoomLevel, nodeModel.isScrollbarHovered);
|
|
1371
|
+
}
|
|
1372
|
+
texture.needsUpdate = true;
|
|
1373
|
+
if (onComplete) onComplete(false);
|
|
1374
|
+
};
|
|
1375
|
+
|
|
1376
|
+
img.src = dataUrl;
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1379
|
+
// [LOD] Simplified Texture generation: Only Image nodes use textures now.
|
|
1380
|
+
// Text/Note/Code nodes use CSS3D for near view and InstancedMesh(Lines) for far view.
|
|
1381
|
+
async function createRenderedTextureAsync(module, nodeModel, isSelected, imageCache, qualityScale = 1.0) {
|
|
1382
|
+
if (nodeModel.contentType === 'image') {
|
|
1383
|
+
const zoomLevel = 2;
|
|
1384
|
+
const styleW = (nodeModel.width || 400) * zoomLevel;
|
|
1385
|
+
const styleH = (nodeModel.height || 200) * zoomLevel;
|
|
1386
|
+
|
|
1387
|
+
const canvas = document.createElement('canvas');
|
|
1388
|
+
canvas.width = styleW;
|
|
1389
|
+
canvas.height = styleH;
|
|
1390
|
+
const ctx = canvas.getContext('2d');
|
|
1391
|
+
|
|
1392
|
+
// Draw image texture
|
|
1393
|
+
// TODO: Ensure drawCachedImageBody is available or inline it
|
|
1394
|
+
const cachedImage = imageCache ? imageCache.get(nodeModel.response) : null;
|
|
1395
|
+
drawCachedImageBody(cachedImage, ctx, nodeModel, styleW, styleH, zoomLevel, isSelected);
|
|
1396
|
+
|
|
1397
|
+
const texture = new THREE.CanvasTexture(canvas);
|
|
1398
|
+
texture.colorSpace = THREE.SRGBColorSpace;
|
|
1399
|
+
texture.generateMipmaps = true;
|
|
1400
|
+
texture.minFilter = THREE.LinearMipmapLinearFilter;
|
|
1401
|
+
texture.magFilter = THREE.LinearFilter;
|
|
1402
|
+
texture.needsUpdate = true;
|
|
1403
|
+
return texture;
|
|
1404
|
+
}
|
|
1405
|
+
|
|
1406
|
+
// For other types, we don't generate textures anymore (CSS3D / InstancedMesh handles them)
|
|
1407
|
+
return null;
|
|
1408
|
+
}
|
|
1409
|
+
|
|
1410
|
+
async function measureHtmlHeightAsync(nodeModel, zoomLevel, canvasWidth, forcedHeight = null, maxHeight = 800) {
|
|
1411
|
+
log(`[measureHtmlHeightAsync] ENTER - nodeId: ${nodeModel.id}, responseLen: ${nodeModel.response?.length || 0}`);
|
|
1412
|
+
|
|
1413
|
+
const hasMarked = typeof window !== 'undefined' && window.marked && typeof window.marked.parse === 'function';
|
|
1414
|
+
const { styles, fontStack } = getMarkdownStyles(zoomLevel, nodeModel.contentType);
|
|
1415
|
+
let htmlContent = '';
|
|
1416
|
+
|
|
1417
|
+
const promptValue = nodeModel.prompt ?? nodeModel.Prompt ?? '';
|
|
1418
|
+
const displayPromptValue = getDisplayPrompt(nodeModel, promptValue);
|
|
1419
|
+
// 기본 Prompt 값들은 표시하지 않음
|
|
1420
|
+
const defaultPrompts = ['Note', 'Pasted Text', '에이전트 메모'];
|
|
1421
|
+
const shouldShowPrompt = promptValue && !defaultPrompts.includes(promptValue);
|
|
1422
|
+
// C#에서 PascalCase (IsLoading)로 전달될 수 있으므로 둘 다 확인
|
|
1423
|
+
const nodeIsLoading = nodeModel.isLoading ?? nodeModel.IsLoading ?? false;
|
|
1424
|
+
const nodeResponse = nodeModel.response ?? nodeModel.Response ?? '';
|
|
1425
|
+
const nodeContentType = nodeModel.contentType ?? nodeModel.ContentType ?? 'text';
|
|
1426
|
+
const isLoadingState = nodeIsLoading && !nodeResponse;
|
|
1427
|
+
// note 타입에서도 프롬프트 표시 (CSS3D와 동일하게)
|
|
1428
|
+
const showPromptNow = shouldShowPrompt;
|
|
1429
|
+
if (showPromptNow) {
|
|
1430
|
+
htmlContent += `<div class="prompt">${escapeHtml(displayPromptValue)}</div>`;
|
|
1431
|
+
}
|
|
1432
|
+
|
|
1433
|
+
// Separator between prompt and response (for both text and note types)
|
|
1434
|
+
if (showPromptNow && (nodeResponse || isLoadingState)) {
|
|
1435
|
+
htmlContent += '<hr class="node-separator" />';
|
|
1436
|
+
}
|
|
1437
|
+
|
|
1438
|
+
if (nodeResponse) {
|
|
1439
|
+
log(`[measureHtmlHeightAsync] Step 1: Preprocessing markdown...`);
|
|
1440
|
+
const processedResponse = preprocessMarkdown(nodeModel.response);
|
|
1441
|
+
log(`[measureHtmlHeightAsync] Step 1: Done. Preprocessed length: ${processedResponse.length}`);
|
|
1442
|
+
|
|
1443
|
+
let parsed;
|
|
1444
|
+
if (hasMarked) {
|
|
1445
|
+
try {
|
|
1446
|
+
log(`[measureHtmlHeightAsync] Step 2: Parsing with marked...`);
|
|
1447
|
+
|
|
1448
|
+
const renderer = new window.marked.Renderer();
|
|
1449
|
+
renderer.code = function (codeOrObj, language, isEscaped) {
|
|
1450
|
+
const code = typeof codeOrObj === 'object' ? codeOrObj.text : codeOrObj;
|
|
1451
|
+
const lang = typeof codeOrObj === 'object' ? codeOrObj.lang : language;
|
|
1452
|
+
const escaped = escapeHtml(code || '');
|
|
1453
|
+
const langClass = lang ? ` class="language-${lang}"` : '';
|
|
1454
|
+
return `<pre><code${langClass}>${escaped}</code></pre>`;
|
|
1455
|
+
};
|
|
1456
|
+
renderer.codespan = function (codeOrObj) {
|
|
1457
|
+
const code = typeof codeOrObj === 'object' ? codeOrObj.text : codeOrObj;
|
|
1458
|
+
return `<code>${escapeHtml(code || '')}</code>`;
|
|
1459
|
+
};
|
|
1460
|
+
parsed = window.marked.parse(processedResponse, {
|
|
1461
|
+
breaks: true,
|
|
1462
|
+
xhtml: true,
|
|
1463
|
+
renderer: renderer
|
|
1464
|
+
});
|
|
1465
|
+
log(`[measureHtmlHeightAsync] Step 2: Marked parsing done. Length: ${parsed.length}`);
|
|
1466
|
+
} catch (e) {
|
|
1467
|
+
console.warn(`[measureHtmlHeightAsync] Marked parse failed, using fallback`, e);
|
|
1468
|
+
parsed = window.marked.parse(processedResponse, { breaks: true, xhtml: true });
|
|
1469
|
+
}
|
|
1470
|
+
} else {
|
|
1471
|
+
parsed = `<div style="white-space: pre-wrap;">${escapeHtml(processedResponse)}</div>`;
|
|
1472
|
+
}
|
|
1473
|
+
|
|
1474
|
+
log(`[measureHtmlHeightAsync] Step 3: Sanitizing HTML...`);
|
|
1475
|
+
parsed = sanitizeHtml(parsed);
|
|
1476
|
+
log(`[measureHtmlHeightAsync] Step 3: Sanitizing done. Length: ${parsed.length}`);
|
|
1477
|
+
|
|
1478
|
+
htmlContent += `<div class="response">${parsed}</div>`;
|
|
1479
|
+
} else if (nodeModel.isLoading) {
|
|
1480
|
+
if (promptValue) {
|
|
1481
|
+
htmlContent += '<hr class="node-separator" />';
|
|
1482
|
+
}
|
|
1483
|
+
htmlContent += `<div class=\"thinking\">AI is generating a response... <span class=\"icon-wrapper\">${ICON_SPINNER_SVG}</span></div>`;
|
|
1484
|
+
} else {
|
|
1485
|
+
htmlContent += '<div class="response"> </div>';
|
|
1486
|
+
}
|
|
1487
|
+
|
|
1488
|
+
log(`[measureHtmlHeightAsync] Step 4: Setting measurer innerHTML...`);
|
|
1489
|
+
const measurer = getSharedMeasurer();
|
|
1490
|
+
const scopeClass = 'mm-measure-root';
|
|
1491
|
+
const scopedStyles = scopeCss(styles, `.${scopeClass}`);
|
|
1492
|
+
const BORDER_WIDTH = 3 * zoomLevel;
|
|
1493
|
+
const PADDING_X = 16 * zoomLevel;
|
|
1494
|
+
const PADDING_BOTTOM = 16 * zoomLevel;
|
|
1495
|
+
const PADDING_TOP = (nodeModel.contentType === 'note' ? 16 : 12) * zoomLevel;
|
|
1496
|
+
const totalHorizontalPadding = (PADDING_X + BORDER_WIDTH) * 2;
|
|
1497
|
+
|
|
1498
|
+
const totalVerticalPadding = PADDING_TOP + PADDING_BOTTOM + (BORDER_WIDTH * 2);
|
|
1499
|
+
const contentWidth = Math.max(1, canvasWidth - totalHorizontalPadding);
|
|
1500
|
+
measurer.style.width = `${contentWidth}px`;
|
|
1501
|
+
measurer.style.fontFamily = fontStack;
|
|
1502
|
+
measurer.style.fontSize = `${14 * zoomLevel}px`;
|
|
1503
|
+
measurer.style.lineHeight = '1.5';
|
|
1504
|
+
measurer.style.overflowWrap = 'break-word';
|
|
1505
|
+
measurer.style.whiteSpace = 'normal';
|
|
1506
|
+
measurer.style.overflow = 'hidden';
|
|
1507
|
+
measurer.innerHTML = `<style>${scopedStyles}</style><div class="${scopeClass}">${htmlContent}</div>`;
|
|
1508
|
+
log(`[measureHtmlHeightAsync] Step 4: innerHTML set. Total length: ${measurer.innerHTML.length}`);
|
|
1509
|
+
|
|
1510
|
+
log(`[measureHtmlHeightAsync] Step 5: Calling getBoundingClientRect...`);
|
|
1511
|
+
const rect = measurer.getBoundingClientRect();
|
|
1512
|
+
const contentHeight = Math.ceil(rect.height);
|
|
1513
|
+
log(`[measureHtmlHeightAsync] Step 5: Done. contentHeight: ${contentHeight}`);
|
|
1514
|
+
|
|
1515
|
+
const totalRequiredHeight = contentHeight + totalVerticalPadding;
|
|
1516
|
+
|
|
1517
|
+
const MAX_HEIGHT = maxHeight * zoomLevel;
|
|
1518
|
+
const MIN_HEIGHT = 60 * zoomLevel;
|
|
1519
|
+
|
|
1520
|
+
let visibleHeight;
|
|
1521
|
+
if (forcedHeight !== null && forcedHeight > 0) {
|
|
1522
|
+
visibleHeight = Math.max(forcedHeight, MIN_HEIGHT);
|
|
1523
|
+
} else {
|
|
1524
|
+
visibleHeight = Math.min(Math.max(totalRequiredHeight, MIN_HEIGHT), MAX_HEIGHT);
|
|
1525
|
+
}
|
|
1526
|
+
|
|
1527
|
+
// ▼▼▼ [수정] 스크롤 끝에서 마지막 줄이 잘리지 않도록 여분 추가 ▼▼▼
|
|
1528
|
+
// 콘텐츠 영역의 하단 패딩만큼 추가 스크롤 허용
|
|
1529
|
+
const extraScrollMargin = PADDING_BOTTOM;
|
|
1530
|
+
const maxScrollPx = Math.max(0, totalRequiredHeight - visibleHeight + extraScrollMargin);
|
|
1531
|
+
// ▲▲▲ [수정] ▲▲▲
|
|
1532
|
+
|
|
1533
|
+
return {
|
|
1534
|
+
height: Math.ceil(Math.max(60, visibleHeight / zoomLevel)),
|
|
1535
|
+
maxScrollPx
|
|
1536
|
+
};
|
|
1537
|
+
}
|
|
1538
|
+
|
|
1539
|
+
/**
|
|
1540
|
+
* [Optimization] Batch measure HTML heights to prevent DOM Layout Thrashing.
|
|
1541
|
+
* Instead of N individual Write→Read cycles, this does 1 Write→1 Read cycle.
|
|
1542
|
+
* @param {Array} contentsWithIds - Array of { id, content, maxWidth, zoomLevel, contentType }
|
|
1543
|
+
* @returns {Object} Map of nodeId -> { height, maxScrollPx }
|
|
1544
|
+
*/
|
|
1545
|
+
async function measureBatchHtmlHeights(contentsWithIds) {
|
|
1546
|
+
if (!contentsWithIds || contentsWithIds.length === 0) return {};
|
|
1547
|
+
|
|
1548
|
+
const startTime = performance.now();
|
|
1549
|
+
const measurer = getSharedMeasurer();
|
|
1550
|
+
if (!measurer) return {};
|
|
1551
|
+
|
|
1552
|
+
const hasMarked = typeof window !== 'undefined' && window.marked && typeof window.marked.parse === 'function';
|
|
1553
|
+
const results = {};
|
|
1554
|
+
|
|
1555
|
+
// 1. Prepare all HTML contents (CPU work, no DOM)
|
|
1556
|
+
const preparedItems = contentsWithIds.map(item => {
|
|
1557
|
+
const zoomLevel = item.zoomLevel || 2;
|
|
1558
|
+
const contentType = item.contentType ?? item.ContentType ?? 'text';
|
|
1559
|
+
const maxWidth = item.maxWidth || 800;
|
|
1560
|
+
const { styles, fontStack } = getMarkdownStyles(zoomLevel, contentType);
|
|
1561
|
+
const scopeClass = `mm-measure-root-${item.id}`;
|
|
1562
|
+
const scopedStyles = scopeCss(styles, `.${scopeClass}`);
|
|
1563
|
+
|
|
1564
|
+
let htmlContent = '';
|
|
1565
|
+
const promptValue = item.prompt ?? item.Prompt ?? '';
|
|
1566
|
+
const sourceFilePath = item.sourceFilePath ?? item.SourceFilePath ?? '';
|
|
1567
|
+
const displayPromptValue = getDisplayPrompt({ contentType, sourceFilePath }, promptValue);
|
|
1568
|
+
const defaultPrompts = ['Note', 'Pasted Text', '에이전트 메모'];
|
|
1569
|
+
const shouldShowPrompt = promptValue && !defaultPrompts.includes(promptValue);
|
|
1570
|
+
|
|
1571
|
+
if (shouldShowPrompt) {
|
|
1572
|
+
htmlContent += `<div class="prompt">${escapeHtml(displayPromptValue)}</div>`;
|
|
1573
|
+
}
|
|
1574
|
+
|
|
1575
|
+
if (item.content) {
|
|
1576
|
+
if (shouldShowPrompt) {
|
|
1577
|
+
htmlContent += '<hr class="node-separator" />';
|
|
1578
|
+
}
|
|
1579
|
+
|
|
1580
|
+
const processedResponse = preprocessMarkdown(item.content);
|
|
1581
|
+
let parsed;
|
|
1582
|
+
|
|
1583
|
+
if (hasMarked) {
|
|
1584
|
+
try {
|
|
1585
|
+
const renderer = new window.marked.Renderer();
|
|
1586
|
+
renderer.code = function (codeOrObj, language) {
|
|
1587
|
+
const code = typeof codeOrObj === 'object' ? codeOrObj.text : codeOrObj;
|
|
1588
|
+
const lang = typeof codeOrObj === 'object' ? codeOrObj.lang : language;
|
|
1589
|
+
const escaped = escapeHtml(code || '');
|
|
1590
|
+
const langClass = lang ? ` class="language-${lang}"` : '';
|
|
1591
|
+
return `<pre><code${langClass}>${escaped}</code></pre>`;
|
|
1592
|
+
};
|
|
1593
|
+
renderer.codespan = function (codeOrObj) {
|
|
1594
|
+
const code = typeof codeOrObj === 'object' ? codeOrObj.text : codeOrObj;
|
|
1595
|
+
return `<code>${escapeHtml(code || '')}</code>`;
|
|
1596
|
+
};
|
|
1597
|
+
parsed = window.marked.parse(processedResponse, { breaks: true, xhtml: true, renderer });
|
|
1598
|
+
} catch (e) {
|
|
1599
|
+
parsed = window.marked.parse(processedResponse, { breaks: true, xhtml: true });
|
|
1600
|
+
}
|
|
1601
|
+
} else {
|
|
1602
|
+
parsed = `<div style="white-space: pre-wrap;">${escapeHtml(processedResponse)}</div>`;
|
|
1603
|
+
}
|
|
1604
|
+
|
|
1605
|
+
parsed = sanitizeHtml(parsed);
|
|
1606
|
+
htmlContent += `<div class="response">${parsed}</div>`;
|
|
1607
|
+
}
|
|
1608
|
+
|
|
1609
|
+
// Calculate dimensions
|
|
1610
|
+
const BORDER_WIDTH = 3 * zoomLevel;
|
|
1611
|
+
const PADDING_X = 16 * zoomLevel;
|
|
1612
|
+
const PADDING_BOTTOM = 16 * zoomLevel;
|
|
1613
|
+
const PADDING_TOP = (contentType === 'note' ? 16 : 12) * zoomLevel;
|
|
1614
|
+
const totalHorizontalPadding = (PADDING_X + BORDER_WIDTH) * 2;
|
|
1615
|
+
const totalVerticalPadding = PADDING_TOP + PADDING_BOTTOM + (BORDER_WIDTH * 2);
|
|
1616
|
+
const contentWidth = Math.max(1, maxWidth - totalHorizontalPadding);
|
|
1617
|
+
|
|
1618
|
+
return {
|
|
1619
|
+
id: item.id,
|
|
1620
|
+
htmlContent,
|
|
1621
|
+
styles: scopedStyles,
|
|
1622
|
+
fontStack,
|
|
1623
|
+
scopeClass,
|
|
1624
|
+
contentWidth,
|
|
1625
|
+
totalVerticalPadding,
|
|
1626
|
+
zoomLevel,
|
|
1627
|
+
maxHeight: (item.maxHeight || 800) * zoomLevel,
|
|
1628
|
+
PADDING_BOTTOM
|
|
1629
|
+
};
|
|
1630
|
+
});
|
|
1631
|
+
|
|
1632
|
+
// 2. WRITE: Inject ALL HTML at once (single DOM write)
|
|
1633
|
+
const combinedHtml = preparedItems.map((item, idx) => {
|
|
1634
|
+
return `<div id="batch-measure-${idx}"
|
|
1635
|
+
style="width:${item.contentWidth}px;
|
|
1636
|
+
font-family:${item.fontStack};
|
|
1637
|
+
font-size:${14 * item.zoomLevel}px;
|
|
1638
|
+
line-height:1.5;
|
|
1639
|
+
overflow-wrap:break-word;
|
|
1640
|
+
white-space:normal;
|
|
1641
|
+
overflow:hidden;
|
|
1642
|
+
position:absolute;
|
|
1643
|
+
left:0;
|
|
1644
|
+
top:${idx * 10000}px;">
|
|
1645
|
+
<style>${item.styles}</style>
|
|
1646
|
+
<div class="${item.scopeClass}">${item.htmlContent}</div>
|
|
1647
|
+
</div>`;
|
|
1648
|
+
}).join('');
|
|
1649
|
+
|
|
1650
|
+
measurer.innerHTML = combinedHtml;
|
|
1651
|
+
|
|
1652
|
+
// 3. Yield to browser for layout calculation (single reflow)
|
|
1653
|
+
await new Promise(resolve => setTimeout(resolve, 0));
|
|
1654
|
+
|
|
1655
|
+
// 4. READ: Get ALL heights at once (single DOM read after reflow)
|
|
1656
|
+
for (let i = 0; i < preparedItems.length; i++) {
|
|
1657
|
+
const item = preparedItems[i];
|
|
1658
|
+
const el = document.getElementById(`batch-measure-${i}`);
|
|
1659
|
+
|
|
1660
|
+
if (el) {
|
|
1661
|
+
const rect = el.getBoundingClientRect();
|
|
1662
|
+
const contentHeight = Math.ceil(rect.height);
|
|
1663
|
+
const totalRequiredHeight = contentHeight + item.totalVerticalPadding;
|
|
1664
|
+
|
|
1665
|
+
const MIN_HEIGHT = 60 * item.zoomLevel;
|
|
1666
|
+
let visibleHeight = Math.min(Math.max(totalRequiredHeight, MIN_HEIGHT), item.maxHeight);
|
|
1667
|
+
|
|
1668
|
+
const extraScrollMargin = item.PADDING_BOTTOM;
|
|
1669
|
+
const maxScrollPx = Math.max(0, totalRequiredHeight - visibleHeight + extraScrollMargin);
|
|
1670
|
+
|
|
1671
|
+
results[item.id] = {
|
|
1672
|
+
height: Math.ceil(Math.max(60, visibleHeight / item.zoomLevel)),
|
|
1673
|
+
maxScrollPx
|
|
1674
|
+
};
|
|
1675
|
+
}
|
|
1676
|
+
}
|
|
1677
|
+
|
|
1678
|
+
// 5. Cleanup
|
|
1679
|
+
measurer.innerHTML = '';
|
|
1680
|
+
|
|
1681
|
+
const elapsed = (performance.now() - startTime).toFixed(1);
|
|
1682
|
+
log(`[measureBatchHtmlHeights] ✅ Measured ${contentsWithIds.length} nodes in ${elapsed}ms (${(elapsed / contentsWithIds.length).toFixed(1)}ms/node avg)`);
|
|
1683
|
+
|
|
1684
|
+
return results;
|
|
1685
|
+
}
|
|
1686
|
+
|
|
1687
|
+
|
|
1688
|
+
|
|
1689
|
+
|
|
1690
|
+
// ▼▼▼ [Memory Optimization] Shared 1x1 placeholder texture ▼▼▼
|
|
1691
|
+
let sharedPlaceholderTexture = null;
|
|
1692
|
+
function getSharedPlaceholderTexture() {
|
|
1693
|
+
if (!sharedPlaceholderTexture) {
|
|
1694
|
+
const canvas = document.createElement('canvas');
|
|
1695
|
+
canvas.width = 1;
|
|
1696
|
+
canvas.height = 1;
|
|
1697
|
+
const ctx = canvas.getContext('2d');
|
|
1698
|
+
ctx.fillStyle = '#ffffff';
|
|
1699
|
+
ctx.fillRect(0, 0, 1, 1);
|
|
1700
|
+
|
|
1701
|
+
const tex = new THREE.CanvasTexture(canvas);
|
|
1702
|
+
tex.colorSpace = THREE.SRGBColorSpace;
|
|
1703
|
+
tex.minFilter = THREE.NearestFilter;
|
|
1704
|
+
tex.magFilter = THREE.NearestFilter;
|
|
1705
|
+
tex.generateMipmaps = false;
|
|
1706
|
+
tex._isShared = true; // Flag to prevent disposal via dispose()
|
|
1707
|
+
|
|
1708
|
+
sharedPlaceholderTexture = tex;
|
|
1709
|
+
}
|
|
1710
|
+
return sharedPlaceholderTexture;
|
|
1711
|
+
}
|
|
1712
|
+
// ▲▲▲ [Memory Optimization] ▲▲▲
|
|
1713
|
+
|
|
1714
|
+
async function generatePlaceholderTextures(module, nodeModel, width, height) {
|
|
1715
|
+
const zoomLevel = 2;
|
|
1716
|
+
// Skip canvas creation entirely!
|
|
1717
|
+
|
|
1718
|
+
const sharedTex = getSharedPlaceholderTexture();
|
|
1719
|
+
|
|
1720
|
+
const tailDefault = createTailTexture('#fefefe', 'rgba(55, 65, 81, 0.9)', zoomLevel, 12, false);
|
|
1721
|
+
const tailSelected = createTailTexture('#fefefe', '#7b7ff2', zoomLevel, 12, true);
|
|
1722
|
+
|
|
1723
|
+
// ▼▼▼ [SDF Glow] Glow is now rendered via SDF shader - no texture needed! ▼▼▼
|
|
1724
|
+
const glowData = null;
|
|
1725
|
+
// ▲▲▲ [SDF Glow] ▲▲▲
|
|
1726
|
+
|
|
1727
|
+
const result = {
|
|
1728
|
+
textures: {
|
|
1729
|
+
default: {
|
|
1730
|
+
body: sharedTex,
|
|
1731
|
+
tail: tailDefault,
|
|
1732
|
+
// Attach metadata to the wrapper object, NOT the shared texture
|
|
1733
|
+
_qualityScale: 0.1,
|
|
1734
|
+
_cachedContent: nodeModel.response ?? ''
|
|
1735
|
+
},
|
|
1736
|
+
selected: {
|
|
1737
|
+
body: sharedTex,
|
|
1738
|
+
tail: tailSelected,
|
|
1739
|
+
_qualityScale: 0.1
|
|
1740
|
+
},
|
|
1741
|
+
glow: glowData
|
|
1742
|
+
},
|
|
1743
|
+
width,
|
|
1744
|
+
height
|
|
1745
|
+
};
|
|
1746
|
+
|
|
1747
|
+
return result;
|
|
1748
|
+
}
|
|
1749
|
+
|
|
1750
|
+
async function ensureImageCached(nodeId, url, imageCache, options = {}) {
|
|
1751
|
+
if (!url) return null;
|
|
1752
|
+
|
|
1753
|
+
if (url.startsWith('⏳') || url.includes('Uploading')) {
|
|
1754
|
+
return null;
|
|
1755
|
+
}
|
|
1756
|
+
|
|
1757
|
+
const preferOriginal = options.preferOriginal === true;
|
|
1758
|
+
const allowOriginalFallback = options.allowOriginalFallback !== false;
|
|
1759
|
+
const resizeWidth = getRequestedResizeWidth(options);
|
|
1760
|
+
const existing = imageCache.get(nodeId) || null;
|
|
1761
|
+
const deferredOriginalError = getDeferredOriginalImageError(existing, url, resizeWidth, options);
|
|
1762
|
+
if (deferredOriginalError) {
|
|
1763
|
+
return deferredOriginalError;
|
|
1764
|
+
}
|
|
1765
|
+
|
|
1766
|
+
const reusableEntry = canReuseCachedImageEntry(existing, url, options);
|
|
1767
|
+
if (reusableEntry) {
|
|
1768
|
+
return reusableEntry;
|
|
1769
|
+
}
|
|
1770
|
+
|
|
1771
|
+
const normalizedUrl = normalizeImageAssetUrl(url);
|
|
1772
|
+
const normalizedOriginalUrl = normalizeImageAssetUrl(options.originalUrl);
|
|
1773
|
+
const requestedUrl = preferOriginal && normalizedOriginalUrl
|
|
1774
|
+
? normalizedOriginalUrl
|
|
1775
|
+
: normalizedUrl;
|
|
1776
|
+
let blob = null;
|
|
1777
|
+
let actualUrl = requestedUrl; // Track which URL was actually loaded
|
|
1778
|
+
let candidateUrl = requestedUrl;
|
|
1779
|
+
const authToken = typeof options.authToken === 'string' ? options.authToken.trim() : '';
|
|
1780
|
+
const notifyMissingAsset = (missingUrl, errorStatus) => {
|
|
1781
|
+
if (Number(errorStatus || 0) !== 404) {
|
|
1782
|
+
return;
|
|
1783
|
+
}
|
|
1784
|
+
|
|
1785
|
+
const normalizedMissingUrl =
|
|
1786
|
+
normalizeImageAssetUrl(missingUrl)
|
|
1787
|
+
|| normalizeImageAssetUrl(options.originalUrl)
|
|
1788
|
+
|| normalizeImageAssetUrl(requestedUrl)
|
|
1789
|
+
|| normalizeImageAssetUrl(url);
|
|
1790
|
+
if (!normalizedMissingUrl) {
|
|
1791
|
+
return;
|
|
1792
|
+
}
|
|
1793
|
+
|
|
1794
|
+
try { options.onMissingAsset?.(normalizedMissingUrl); } catch { }
|
|
1795
|
+
};
|
|
1796
|
+
|
|
1797
|
+
const getFetchOptions = (targetUrl) => {
|
|
1798
|
+
const normalized = (targetUrl || '').toLowerCase();
|
|
1799
|
+
if (normalized.startsWith('blob:') || normalized.startsWith('data:') || normalized.startsWith('file:')) {
|
|
1800
|
+
return undefined;
|
|
1801
|
+
}
|
|
1802
|
+
|
|
1803
|
+
const requestOptions = {};
|
|
1804
|
+
if (isLoopbackImageAssetUrl(targetUrl)) {
|
|
1805
|
+
requestOptions.targetAddressSpace = 'loopback';
|
|
1806
|
+
}
|
|
1807
|
+
|
|
1808
|
+
if (authToken) {
|
|
1809
|
+
requestOptions.headers = {
|
|
1810
|
+
Authorization: `Bearer ${authToken}`
|
|
1811
|
+
};
|
|
1812
|
+
}
|
|
1813
|
+
|
|
1814
|
+
return Object.keys(requestOptions).length > 0 ? requestOptions : undefined;
|
|
1815
|
+
};
|
|
1816
|
+
|
|
1817
|
+
const fetchWithAuth = (targetUrl) => fetch(targetUrl, getFetchOptions(targetUrl));
|
|
1818
|
+
const fetchBlobWithAuth = async (targetUrl) => {
|
|
1819
|
+
const requestKey = getImageAssetErrorCacheKey(targetUrl) || normalizeImageAssetUrl(targetUrl);
|
|
1820
|
+
if (!requestKey) {
|
|
1821
|
+
const response = await fetchWithAuth(targetUrl);
|
|
1822
|
+
if (!response.ok) {
|
|
1823
|
+
const httpError = new Error(`HTTP ${response.status}`);
|
|
1824
|
+
httpError.status = response.status;
|
|
1825
|
+
httpError.url = targetUrl;
|
|
1826
|
+
storeFailedImageAssetEntry(createCachedImageErrorEntry(targetUrl, resizeWidth, response.status));
|
|
1827
|
+
throw httpError;
|
|
1828
|
+
}
|
|
1829
|
+
return await readImageBlobFromResponse(response, targetUrl);
|
|
1830
|
+
}
|
|
1831
|
+
|
|
1832
|
+
const existingPendingFetch = pendingImageAssetFetches.get(requestKey);
|
|
1833
|
+
if (existingPendingFetch) {
|
|
1834
|
+
return await existingPendingFetch;
|
|
1835
|
+
}
|
|
1836
|
+
|
|
1837
|
+
const pendingFetch = (async () => {
|
|
1838
|
+
const failedAssetEntry = getFailedImageAssetEntry(targetUrl);
|
|
1839
|
+
if (failedAssetEntry) {
|
|
1840
|
+
throw createSyntheticImageFetchError(failedAssetEntry, targetUrl);
|
|
1841
|
+
}
|
|
1842
|
+
|
|
1843
|
+
const response = await fetchWithAuth(targetUrl);
|
|
1844
|
+
if (!response.ok) {
|
|
1845
|
+
const httpError = new Error(`HTTP ${response.status}`);
|
|
1846
|
+
httpError.status = response.status;
|
|
1847
|
+
httpError.url = targetUrl;
|
|
1848
|
+
storeFailedImageAssetEntry(createCachedImageErrorEntry(targetUrl, resizeWidth, response.status));
|
|
1849
|
+
throw httpError;
|
|
1850
|
+
}
|
|
1851
|
+
return await readImageBlobFromResponse(response, targetUrl);
|
|
1852
|
+
})();
|
|
1853
|
+
|
|
1854
|
+
pendingImageAssetFetches.set(requestKey, pendingFetch);
|
|
1855
|
+
try {
|
|
1856
|
+
return await pendingFetch;
|
|
1857
|
+
} finally {
|
|
1858
|
+
if (pendingImageAssetFetches.get(requestKey) === pendingFetch) {
|
|
1859
|
+
pendingImageAssetFetches.delete(requestKey);
|
|
1860
|
+
}
|
|
1861
|
+
}
|
|
1862
|
+
};
|
|
1863
|
+
|
|
1864
|
+
if (shouldResolveNodeAssetUrlBeforeFetch(candidateUrl, options)) {
|
|
1865
|
+
const refreshedUrl = await requestFreshNodeAssetUrl(nodeId, candidateUrl, options);
|
|
1866
|
+
const normalizedRefreshedUrl = normalizeImageAssetUrl(refreshedUrl);
|
|
1867
|
+
if (normalizedRefreshedUrl) {
|
|
1868
|
+
candidateUrl = normalizedRefreshedUrl;
|
|
1869
|
+
actualUrl = normalizedRefreshedUrl;
|
|
1870
|
+
try { options.onRefreshedUrl?.(normalizedRefreshedUrl); } catch { }
|
|
1871
|
+
}
|
|
1872
|
+
}
|
|
1873
|
+
|
|
1874
|
+
// ▼▼▼ [Fallback] Preview requests may opt into original fallback, but startup preview paths keep this disabled. ▼▼▼
|
|
1875
|
+
try {
|
|
1876
|
+
blob = await fetchBlobWithAuth(candidateUrl);
|
|
1877
|
+
actualUrl = candidateUrl;
|
|
1878
|
+
} catch (error) {
|
|
1879
|
+
// Only escalate a thumbnail miss to the original asset when the caller explicitly allows it.
|
|
1880
|
+
const isThumbnail = isThumbnailAssetUrl(candidateUrl);
|
|
1881
|
+
if (isThumbnail && allowOriginalFallback) {
|
|
1882
|
+
// Convert thumbnail URL to original URL
|
|
1883
|
+
const originalUrl = candidateUrl.replace('/assets/thumbs/', '/assets/').replace(/\.jpg$/, '');
|
|
1884
|
+
// Try common extensions
|
|
1885
|
+
const extensions = ['.png', '.jpg', '.jpeg', '.gif', '.webp', '.bmp'];
|
|
1886
|
+
let fallbackSuccess = false;
|
|
1887
|
+
let lastFallbackError = error;
|
|
1888
|
+
|
|
1889
|
+
for (const ext of extensions) {
|
|
1890
|
+
const tryUrl = originalUrl.includes('.') ? originalUrl : originalUrl + ext;
|
|
1891
|
+
try {
|
|
1892
|
+
blob = await fetchBlobWithAuth(tryUrl);
|
|
1893
|
+
actualUrl = tryUrl;
|
|
1894
|
+
fallbackSuccess = true;
|
|
1895
|
+
log(`[TextureFactory] Thumbnail not found, using original: ${tryUrl}`);
|
|
1896
|
+
break;
|
|
1897
|
+
} catch (fallbackError) {
|
|
1898
|
+
lastFallbackError = fallbackError;
|
|
1899
|
+
// Try next extension
|
|
1900
|
+
}
|
|
1901
|
+
}
|
|
1902
|
+
|
|
1903
|
+
// Also try the original URL from options if provided
|
|
1904
|
+
if (!fallbackSuccess && normalizedOriginalUrl) {
|
|
1905
|
+
try {
|
|
1906
|
+
blob = await fetchBlobWithAuth(normalizedOriginalUrl);
|
|
1907
|
+
actualUrl = normalizedOriginalUrl;
|
|
1908
|
+
fallbackSuccess = true;
|
|
1909
|
+
log(`[TextureFactory] Using provided originalUrl: ${normalizedOriginalUrl}`);
|
|
1910
|
+
} catch (originalError) {
|
|
1911
|
+
lastFallbackError = originalError;
|
|
1912
|
+
// Ignore
|
|
1913
|
+
}
|
|
1914
|
+
}
|
|
1915
|
+
|
|
1916
|
+
if (!fallbackSuccess) {
|
|
1917
|
+
const refreshedUrl = await requestFreshNodeAssetUrl(nodeId, actualUrl || candidateUrl, options);
|
|
1918
|
+
if (refreshedUrl) {
|
|
1919
|
+
try { options.onRefreshedUrl?.(refreshedUrl); } catch { }
|
|
1920
|
+
return ensureImageCached(nodeId, refreshedUrl, imageCache, {
|
|
1921
|
+
...options,
|
|
1922
|
+
originalUrl: refreshedUrl,
|
|
1923
|
+
skipAssetRefresh: true
|
|
1924
|
+
});
|
|
1925
|
+
}
|
|
1926
|
+
|
|
1927
|
+
console.warn(`[TextureFactory] Thumbnail fetch failed and no usable fallback remained for ${nodeId}`);
|
|
1928
|
+
const errorStatus = getImageErrorStatus(lastFallbackError);
|
|
1929
|
+
notifyMissingAsset(actualUrl || candidateUrl || requestedUrl, errorStatus);
|
|
1930
|
+
const preservedOriginalError = rememberOriginalImageError(
|
|
1931
|
+
existing,
|
|
1932
|
+
actualUrl || candidateUrl || requestedUrl,
|
|
1933
|
+
resizeWidth,
|
|
1934
|
+
errorStatus,
|
|
1935
|
+
options
|
|
1936
|
+
);
|
|
1937
|
+
if (preservedOriginalError) {
|
|
1938
|
+
return preservedOriginalError;
|
|
1939
|
+
}
|
|
1940
|
+
const errorEntry = createCachedImageErrorEntry(
|
|
1941
|
+
candidateUrl,
|
|
1942
|
+
resizeWidth,
|
|
1943
|
+
errorStatus
|
|
1944
|
+
);
|
|
1945
|
+
storeFailedImageAssetEntry(errorEntry);
|
|
1946
|
+
return storeImageCacheEntry(imageCache, nodeId, errorEntry);
|
|
1947
|
+
}
|
|
1948
|
+
} else {
|
|
1949
|
+
const refreshedUrl = await requestFreshNodeAssetUrl(nodeId, actualUrl || candidateUrl, options);
|
|
1950
|
+
if (refreshedUrl) {
|
|
1951
|
+
try { options.onRefreshedUrl?.(refreshedUrl); } catch { }
|
|
1952
|
+
return ensureImageCached(nodeId, refreshedUrl, imageCache, {
|
|
1953
|
+
...options,
|
|
1954
|
+
originalUrl: refreshedUrl,
|
|
1955
|
+
skipAssetRefresh: true
|
|
1956
|
+
});
|
|
1957
|
+
}
|
|
1958
|
+
|
|
1959
|
+
console.warn(`[TextureFactory] Image fetch failed for ${nodeId}:`, error);
|
|
1960
|
+
const errorStatus = getImageErrorStatus(error);
|
|
1961
|
+
notifyMissingAsset(actualUrl || candidateUrl || requestedUrl, errorStatus);
|
|
1962
|
+
const preservedOriginalError = rememberOriginalImageError(
|
|
1963
|
+
existing,
|
|
1964
|
+
actualUrl || candidateUrl || requestedUrl,
|
|
1965
|
+
resizeWidth,
|
|
1966
|
+
errorStatus,
|
|
1967
|
+
options
|
|
1968
|
+
);
|
|
1969
|
+
if (preservedOriginalError) {
|
|
1970
|
+
return preservedOriginalError;
|
|
1971
|
+
}
|
|
1972
|
+
const errorEntry = createCachedImageErrorEntry(
|
|
1973
|
+
candidateUrl,
|
|
1974
|
+
resizeWidth,
|
|
1975
|
+
errorStatus
|
|
1976
|
+
);
|
|
1977
|
+
storeFailedImageAssetEntry(errorEntry);
|
|
1978
|
+
return storeImageCacheEntry(imageCache, nodeId, errorEntry);
|
|
1979
|
+
}
|
|
1980
|
+
}
|
|
1981
|
+
// ▲▲▲ [Fallback] ▲▲▲
|
|
1982
|
+
|
|
1983
|
+
if (blob && typeof createImageBitmap === 'function') {
|
|
1984
|
+
try {
|
|
1985
|
+
// ▼▼▼ [Optimization] Skip resize for thumbnails (already 512px or smaller) ▼▼▼
|
|
1986
|
+
const isThumbnail = isThumbnailAssetUrl(actualUrl);
|
|
1987
|
+
const sourceKind = getImageSourceKind(actualUrl);
|
|
1988
|
+
const resizeQuality = preferOriginal ? 'high' : 'medium';
|
|
1989
|
+
|
|
1990
|
+
if (isThumbnail) {
|
|
1991
|
+
// Thumbnail: decode directly without resize (faster - single decode)
|
|
1992
|
+
const bitmap = await createImageBitmap(blob);
|
|
1993
|
+
const cached = {
|
|
1994
|
+
image: bitmap,
|
|
1995
|
+
url: actualUrl,
|
|
1996
|
+
width: bitmap.width,
|
|
1997
|
+
height: bitmap.height,
|
|
1998
|
+
requestedWidth: bitmap.width,
|
|
1999
|
+
sourceKind,
|
|
2000
|
+
assetKey: getCanonicalImageAssetKey(actualUrl),
|
|
2001
|
+
type: 'bitmap',
|
|
2002
|
+
isError: false,
|
|
2003
|
+
fullResError: null
|
|
2004
|
+
};
|
|
2005
|
+
clearFailedImageAssetEntry(actualUrl);
|
|
2006
|
+
return storeImageCacheEntry(imageCache, nodeId, cached);
|
|
2007
|
+
}
|
|
2008
|
+
|
|
2009
|
+
// Non-thumbnail (original image): keep a larger, DPR-aware cached bitmap for visible image meshes.
|
|
2010
|
+
const bitmap = await createImageBitmap(blob, { resizeWidth, resizeQuality });
|
|
2011
|
+
const cached = {
|
|
2012
|
+
image: bitmap,
|
|
2013
|
+
url: actualUrl,
|
|
2014
|
+
width: bitmap.width,
|
|
2015
|
+
height: bitmap.height,
|
|
2016
|
+
requestedWidth: resizeWidth,
|
|
2017
|
+
sourceKind,
|
|
2018
|
+
assetKey: getCanonicalImageAssetKey(actualUrl),
|
|
2019
|
+
type: 'bitmap',
|
|
2020
|
+
isError: false,
|
|
2021
|
+
fullResError: null
|
|
2022
|
+
};
|
|
2023
|
+
clearFailedImageAssetEntry(actualUrl);
|
|
2024
|
+
return storeImageCacheEntry(imageCache, nodeId, cached);
|
|
2025
|
+
// ▲▲▲ [Optimization] ▲▲▲
|
|
2026
|
+
} catch (error) {
|
|
2027
|
+
console.warn('[TextureFactory] createImageBitmap failed, fallback to Image()', {
|
|
2028
|
+
nodeId,
|
|
2029
|
+
actualUrl,
|
|
2030
|
+
blobType: blob?.type || '',
|
|
2031
|
+
blobSize: Number(blob?.size || 0)
|
|
2032
|
+
}, error);
|
|
2033
|
+
}
|
|
2034
|
+
}
|
|
2035
|
+
|
|
2036
|
+
return new Promise((resolve) => {
|
|
2037
|
+
const img = new Image();
|
|
2038
|
+
if (!actualUrl.startsWith('blob:') && !actualUrl.startsWith('data:') && !actualUrl.startsWith('file:')) {
|
|
2039
|
+
img.crossOrigin = 'anonymous';
|
|
2040
|
+
}
|
|
2041
|
+
|
|
2042
|
+
let objectUrl = null;
|
|
2043
|
+
if (blob) {
|
|
2044
|
+
objectUrl = URL.createObjectURL(blob);
|
|
2045
|
+
}
|
|
2046
|
+
|
|
2047
|
+
img.onload = () => {
|
|
2048
|
+
if (objectUrl) URL.revokeObjectURL(objectUrl);
|
|
2049
|
+
const sourceKind = getImageSourceKind(actualUrl);
|
|
2050
|
+
const cached = {
|
|
2051
|
+
image: img,
|
|
2052
|
+
url: actualUrl,
|
|
2053
|
+
width: img.naturalWidth || img.width || 0,
|
|
2054
|
+
height: img.naturalHeight || img.height || 0,
|
|
2055
|
+
requestedWidth: resizeWidth,
|
|
2056
|
+
sourceKind,
|
|
2057
|
+
assetKey: getCanonicalImageAssetKey(actualUrl),
|
|
2058
|
+
type: 'image',
|
|
2059
|
+
isError: false,
|
|
2060
|
+
fullResError: null
|
|
2061
|
+
};
|
|
2062
|
+
clearFailedImageAssetEntry(actualUrl);
|
|
2063
|
+
resolve(storeImageCacheEntry(imageCache, nodeId, cached));
|
|
2064
|
+
};
|
|
2065
|
+
|
|
2066
|
+
img.onerror = () => {
|
|
2067
|
+
if (objectUrl) URL.revokeObjectURL(objectUrl);
|
|
2068
|
+
console.warn(`[TextureFactory] Image load error for ${nodeId}`, {
|
|
2069
|
+
actualUrl,
|
|
2070
|
+
blobType: blob?.type || '',
|
|
2071
|
+
blobSize: Number(blob?.size || 0)
|
|
2072
|
+
});
|
|
2073
|
+
const errorEntry = createCachedImageErrorEntry(actualUrl, resizeWidth, 0);
|
|
2074
|
+
storeFailedImageAssetEntry(errorEntry);
|
|
2075
|
+
resolve(storeImageCacheEntry(imageCache, nodeId, errorEntry));
|
|
2076
|
+
};
|
|
2077
|
+
|
|
2078
|
+
img.src = objectUrl || actualUrl;
|
|
2079
|
+
});
|
|
2080
|
+
}
|
|
2081
|
+
|
|
2082
|
+
/**
|
|
2083
|
+
* 텍스처 생성 및 캐싱 (LOD 품질 지원)
|
|
2084
|
+
* @param {number} qualityScale - 품질 스케일 (1.0=고해상도, 0.25=저해상도)
|
|
2085
|
+
*/
|
|
2086
|
+
// Simplified Texture Factory for Image Nodes & Measurement Only
|
|
2087
|
+
|
|
2088
|
+
function getMarkdownStyles(zoomLevel, contentType = 'text', options = {}) {
|
|
2089
|
+
// Kept for measurement purposes only
|
|
2090
|
+
const z = zoomLevel;
|
|
2091
|
+
const fontStack = "ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif";
|
|
2092
|
+
const baseFontSize = 14 * z;
|
|
2093
|
+
const baseLineHeight = 1.5;
|
|
2094
|
+
|
|
2095
|
+
// Simplified styles just for height measurement
|
|
2096
|
+
const styles = `
|
|
2097
|
+
div { line-height: ${baseLineHeight}; font-family: ${fontStack}; font-size: ${baseFontSize}px; margin: 0; padding: 0; }
|
|
2098
|
+
p { margin: 0 0 ${8 * z}px 0; }
|
|
2099
|
+
h1, h2, h3, h4 { font-size: ${16 * z}px; font-weight: 700; margin: ${12 * z}px 0 ${6 * z}px 0; }
|
|
2100
|
+
ul, ol { margin: ${6 * z}px 0 ${10 * z}px ${20 * z}px; padding: 0; }
|
|
2101
|
+
li { margin-bottom: ${4 * z}px; }
|
|
2102
|
+
pre { margin: ${8 * z}px 0; white-space: pre-wrap; }
|
|
2103
|
+
`;
|
|
2104
|
+
return { styles, fontStack };
|
|
2105
|
+
}
|
|
2106
|
+
|
|
2107
|
+
// [Deleted] renderMarkdownToCanvasAsync - No longer needed (CSS3D handles rendering)
|
|
2108
|
+
// [Deleted] drawNodeBodyOnCanvas - No longer needed
|
|
2109
|
+
|
|
2110
|
+
// ... (measureHtmlHeightAsync and helper functions kept for auto-sizing logic) ...
|
|
2111
|
+
|
|
2112
|
+
/**
|
|
2113
|
+
* 텍스처 생성 및 캐싱 (Simplified)
|
|
2114
|
+
*/
|
|
2115
|
+
async function generateAndCacheTextures(module, nodeModel, imageCache, updateData = {}, qualityScale = 1.0) {
|
|
2116
|
+
// Image Node: Full processing
|
|
2117
|
+
if (nodeModel.contentType === 'image') {
|
|
2118
|
+
// ... existing image logic ...
|
|
2119
|
+
const defaultWidth = module.aiNodeDefaultWidth || 400;
|
|
2120
|
+
const defaultHeight = module.aiNodeDefaultHeight || 200;
|
|
2121
|
+
const width = nodeModel.width || 300;
|
|
2122
|
+
const height = nodeModel.height || 200;
|
|
2123
|
+
|
|
2124
|
+
const bodyDefault = await createRenderedTextureAsync(module, nodeModel, false, imageCache, qualityScale);
|
|
2125
|
+
// Skip selected texture for optimization if requested, or create it
|
|
2126
|
+
const skipSelected = updateData.skipSelected === true;
|
|
2127
|
+
const bodySelected = skipSelected ? null : await createRenderedTextureAsync(module, nodeModel, true, imageCache, qualityScale);
|
|
2128
|
+
|
|
2129
|
+
// Tail textures
|
|
2130
|
+
const zoomLevel = 2;
|
|
2131
|
+
const tailDefaultColors = getTailColors(nodeModel, false);
|
|
2132
|
+
const tailSelectedColors = getTailColors(nodeModel, true);
|
|
2133
|
+
const tailDefault = createTailTexture(tailDefaultColors.fill, tailDefaultColors.stroke, zoomLevel, 12, false);
|
|
2134
|
+
const tailSelected = createTailTexture(tailSelectedColors.fill, tailSelectedColors.stroke, zoomLevel, 12, true);
|
|
2135
|
+
|
|
2136
|
+
return {
|
|
2137
|
+
textures: {
|
|
2138
|
+
default: { body: bodyDefault, tail: tailDefault },
|
|
2139
|
+
selected: { body: bodySelected, tail: tailSelected },
|
|
2140
|
+
glow: null
|
|
2141
|
+
},
|
|
2142
|
+
width,
|
|
2143
|
+
height,
|
|
2144
|
+
sourceCanvas: bodyDefault?.image
|
|
2145
|
+
};
|
|
2146
|
+
}
|
|
2147
|
+
|
|
2148
|
+
// Text/Note/Code Nodes: Measurement ONLY, No Textures
|
|
2149
|
+
const isTextLike = nodeModel.contentType === 'text' || nodeModel.contentType === 'note' || nodeModel.contentType === 'code' || nodeModel.contentType === 'markdown';
|
|
2150
|
+
|
|
2151
|
+
// 1. Measure Height (Crucial for Layout)
|
|
2152
|
+
if (isTextLike && !nodeModel.isChunkedText) { // Code nodes now measured like text
|
|
2153
|
+
const skipMeasure = updateData.skipMeasure === true;
|
|
2154
|
+
if (!skipMeasure) {
|
|
2155
|
+
const zoomLevel = 2;
|
|
2156
|
+
const defaultWidth = module.aiNodeDefaultWidth || 400;
|
|
2157
|
+
const width = nodeModel.width || defaultWidth;
|
|
2158
|
+
const defaultHeight = module.aiNodeDefaultHeight || 200;
|
|
2159
|
+
const effectiveMaxHeight = module.showFullAiResponse ? 10000 : defaultHeight;
|
|
2160
|
+
|
|
2161
|
+
// Reuse existing measurement logic
|
|
2162
|
+
try {
|
|
2163
|
+
const forcedHeight = (nodeModel.height && nodeModel.height > 0) ? (nodeModel.height * zoomLevel) : null;
|
|
2164
|
+
const { height: measuredHeight, maxScrollPx } = await measureHtmlHeightAsync(nodeModel, zoomLevel, width * zoomLevel, forcedHeight, effectiveMaxHeight);
|
|
2165
|
+
|
|
2166
|
+
if (!nodeModel.height || nodeModel.height <= 0) {
|
|
2167
|
+
nodeModel.height = measuredHeight;
|
|
2168
|
+
}
|
|
2169
|
+
nodeModel.maxScroll = maxScrollPx / zoomLevel;
|
|
2170
|
+
} catch (e) {
|
|
2171
|
+
console.warn('[TextureFactory] Height measure failed', e);
|
|
2172
|
+
if (!nodeModel.height) nodeModel.height = 200;
|
|
2173
|
+
}
|
|
2174
|
+
}
|
|
2175
|
+
}
|
|
2176
|
+
|
|
2177
|
+
// 2. Return Placeholder/Null Textures (CSS3D will handle visibility)
|
|
2178
|
+
// We technically don't need *any* texture for the content, just the tail.
|
|
2179
|
+
// But mind-map-nodes.js expects a structure. We will return minimized data.
|
|
2180
|
+
|
|
2181
|
+
const zoomLevel = 2;
|
|
2182
|
+
const tailDefaultColors = getTailColors(nodeModel, false);
|
|
2183
|
+
const tailSelectedColors = getTailColors(nodeModel, true);
|
|
2184
|
+
const tailDefault = createTailTexture(tailDefaultColors.fill, tailDefaultColors.stroke, zoomLevel, 12, false);
|
|
2185
|
+
const tailSelected = createTailTexture(tailSelectedColors.fill, tailSelectedColors.stroke, zoomLevel, 12, true);
|
|
2186
|
+
|
|
2187
|
+
return {
|
|
2188
|
+
textures: {
|
|
2189
|
+
// No body textures! mind-map-nodes.js should be updated to handle null/undefined body texture
|
|
2190
|
+
default: { body: null, tail: tailDefault },
|
|
2191
|
+
selected: { body: null, tail: tailSelected },
|
|
2192
|
+
glow: null
|
|
2193
|
+
},
|
|
2194
|
+
width: nodeModel.width || 400,
|
|
2195
|
+
height: nodeModel.height || 200
|
|
2196
|
+
};
|
|
2197
|
+
}
|
|
2198
|
+
|
|
2199
|
+
// Placeholder generation optimized for CSS3D adoption
|
|
2200
|
+
async function generatePlaceholderTextures(module, nodeModel, width, height) {
|
|
2201
|
+
// ▼▼▼ [Optimization] Skip texture generation for CSS3D nodes ▼▼▼
|
|
2202
|
+
const isCss3dType = nodeModel.contentType === 'text' || nodeModel.contentType === 'note' || nodeModel.contentType === 'memo' || nodeModel.contentType === 'markdown' || nodeModel.contentType === 'code';
|
|
2203
|
+
|
|
2204
|
+
// For CSS3D types, we return dummy/null textures because they are rendered via CSS3D or InstancedMesh (lines).
|
|
2205
|
+
// Exception: Glow might still need a texture, but usually that comes from a shared sprite or simple mesh.
|
|
2206
|
+
// We still need the 'glow' texture if the interaction model expects it on the glObject.
|
|
2207
|
+
|
|
2208
|
+
const glowTextureInfo = null;
|
|
2209
|
+
|
|
2210
|
+
// Image nodes stay on the WebGL path and manage their own body texture directly.
|
|
2211
|
+
|
|
2212
|
+
if (isCss3dType) {
|
|
2213
|
+
const zoomLevel = 2;
|
|
2214
|
+
const tailDefaultColors = getTailColors(nodeModel, false);
|
|
2215
|
+
const tailSelectedColors = getTailColors(nodeModel, true);
|
|
2216
|
+
const tailDefault = createTailTexture(tailDefaultColors.fill, tailDefaultColors.stroke, zoomLevel, 12, false);
|
|
2217
|
+
const tailSelected = createTailTexture(tailSelectedColors.fill, tailSelectedColors.stroke, zoomLevel, 12, true);
|
|
2218
|
+
return {
|
|
2219
|
+
textures: {
|
|
2220
|
+
default: { body: null, tail: tailDefault },
|
|
2221
|
+
selected: { body: null, tail: tailSelected },
|
|
2222
|
+
glow: glowTextureInfo
|
|
2223
|
+
}
|
|
2224
|
+
};
|
|
2225
|
+
}
|
|
2226
|
+
// ▲▲▲ [Optimization] ▲▲▲
|
|
2227
|
+
|
|
2228
|
+
// Legacy behavior for other types (e.g. PDF, custom)
|
|
2229
|
+
const zoomLevel = 2; // Fixed resolution scale
|
|
2230
|
+
// ... (Original logic for creating canvas textures for non-CSS types)
|
|
2231
|
+
|
|
2232
|
+
// Return placeholder
|
|
2233
|
+
return {
|
|
2234
|
+
textures: {
|
|
2235
|
+
default: { body: null, tail: null },
|
|
2236
|
+
selected: { body: null, tail: null },
|
|
2237
|
+
glow: glowTextureInfo
|
|
2238
|
+
}
|
|
2239
|
+
};
|
|
2240
|
+
}
|
|
2241
|
+
|
|
2242
|
+
|
|
2243
|
+
window.MindMapTextureFactory = {
|
|
2244
|
+
generateAndCacheTextures,
|
|
2245
|
+
generatePlaceholderTextures,
|
|
2246
|
+
measureHtmlHeightAsync,
|
|
2247
|
+
measureBatchHtmlHeights, // [Optimization] Batch measure for Layout Thrashing prevention
|
|
2248
|
+
ensureImageCached,
|
|
2249
|
+
resolveCachedImageEntry,
|
|
2250
|
+
createTailTexture, // ★ Export for code node tail creation
|
|
2251
|
+
clearCache: () => {
|
|
2252
|
+
tailTextureCache.clear();
|
|
2253
|
+
failedImageAssetCache.clear();
|
|
2254
|
+
pendingImageAssetFetches.clear();
|
|
2255
|
+
},
|
|
2256
|
+
registerRenderer: (type, renderer) => renderers.set(type, renderer)
|
|
2257
|
+
};
|
|
2258
|
+
|
|
2259
|
+
log('mind-map-texture-factory.js initialized (folded corners, spinner)');
|
|
2260
|
+
})();
|