From a726ca20e16894c4d1928ea8a87ba0da289cd5c1 Mon Sep 17 00:00:00 2001 From: Kur0mi Date: Sun, 21 Jun 2026 21:05:56 +0200 Subject: [PATCH] temp savepoints. port succeeds. minor errors to be fixed. --- .gitignore | 3 + README.md | 25 +- esbuild.config.mjs | 99 + eslint.config.mts | 35 + legacy/main.legacy.js | 8957 ++++++++++++++++++ main.js | 13025 +++++++++----------------- package-lock.json | 7032 ++++++++++++++ package.json | 35 + src/bench/bench.ts | 83 + src/constants.ts | 34 + src/data/GraphStore.ts | 109 + src/data/buildGraph.ts | 151 + src/data/seed.ts | 29 + src/i18n.ts | 309 + src/interactions/CameraDirector.ts | 271 + src/layout/LayoutEngine.ts | 23 + src/layout/MainThreadForceLayout.ts | 109 + src/layout/WorkerForceLayout.ts | 115 + src/layout/forceWorker.ts | 166 + src/layout/radial/layoutRadial.ts | 1316 +++ src/main.ts | 281 + src/overlay/ControlPanel.ts | 347 + src/overlay/OverlayManager.ts | 237 + src/overlay/Slider.ts | 110 + src/quality/tiers.ts | 54 + src/render/AggregateRenderer.ts | 672 ++ src/render/RadialRenderer.ts | 790 ++ src/render/colorThemes.ts | 49 + src/render/palette.ts | 57 + src/render/presets.ts | 41 + src/render/shaders.ts | 47 + src/render/starfield.ts | 120 + src/render/stylePresets.ts | 44 + src/settings.ts | 344 + src/settings/graphJsonImport.ts | 31 + src/types.ts | 47 + src/typings/d3-force-3d.d.ts | 90 + src/typings/galaxy-dev.d.ts | 2 + src/typings/inline-worker.d.ts | 5 + src/view/GalaxyView.ts | 106 + src/view/GraphController.ts | 779 ++ src/view/Map3DController.ts | 3 + src/view/Radial2DController.ts | 1069 +++ src/view/SearchModal.ts | 61 + src/world/WorldMapIndex.ts | 98 + src/world/buildWorldMap.ts | 255 + src/world/types.ts | 103 + src/world/visibleGraph.ts | 509 + styles.css | 1715 ++-- tests/buildGraph.test.ts | 96 + tests/settings.test.ts | 62 + tests/worldMap.test.ts | 163 + tsconfig.json | 19 + version-bump.mjs | 17 + versions.json | 4 + 55 files changed, 30746 insertions(+), 9607 deletions(-) create mode 100644 esbuild.config.mjs create mode 100644 eslint.config.mts create mode 100644 legacy/main.legacy.js create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 src/bench/bench.ts create mode 100644 src/constants.ts create mode 100644 src/data/GraphStore.ts create mode 100644 src/data/buildGraph.ts create mode 100644 src/data/seed.ts create mode 100644 src/i18n.ts create mode 100644 src/interactions/CameraDirector.ts create mode 100644 src/layout/LayoutEngine.ts create mode 100644 src/layout/MainThreadForceLayout.ts create mode 100644 src/layout/WorkerForceLayout.ts create mode 100644 src/layout/forceWorker.ts create mode 100644 src/layout/radial/layoutRadial.ts create mode 100644 src/main.ts create mode 100644 src/overlay/ControlPanel.ts create mode 100644 src/overlay/OverlayManager.ts create mode 100644 src/overlay/Slider.ts create mode 100644 src/quality/tiers.ts create mode 100644 src/render/AggregateRenderer.ts create mode 100644 src/render/RadialRenderer.ts create mode 100644 src/render/colorThemes.ts create mode 100644 src/render/palette.ts create mode 100644 src/render/presets.ts create mode 100644 src/render/shaders.ts create mode 100644 src/render/starfield.ts create mode 100644 src/render/stylePresets.ts create mode 100644 src/settings.ts create mode 100644 src/settings/graphJsonImport.ts create mode 100644 src/types.ts create mode 100644 src/typings/d3-force-3d.d.ts create mode 100644 src/typings/galaxy-dev.d.ts create mode 100644 src/typings/inline-worker.d.ts create mode 100644 src/view/GalaxyView.ts create mode 100644 src/view/GraphController.ts create mode 100644 src/view/Map3DController.ts create mode 100644 src/view/Radial2DController.ts create mode 100644 src/view/SearchModal.ts create mode 100644 src/world/WorldMapIndex.ts create mode 100644 src/world/buildWorldMap.ts create mode 100644 src/world/types.ts create mode 100644 src/world/visibleGraph.ts create mode 100644 tests/buildGraph.test.ts create mode 100644 tests/settings.test.ts create mode 100644 tests/worldMap.test.ts create mode 100644 tsconfig.json create mode 100644 version-bump.mjs create mode 100644 versions.json diff --git a/.gitignore b/.gitignore index a5d4a89..49bf647 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,6 @@ node_modules/ data.json *.map +dist/ +dev-vault/ +*.log diff --git a/README.md b/README.md index f3a4b4b..41cd0b1 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,9 @@ # Mini World Map -Mini World Map is an Obsidian plugin that visualizes your vault as a hierarchy-first world map, with Obsidian note links layered on top. +Mini World Map is an Obsidian plugin that visualizes your vault as a hierarchy-first world map, with Obsidian note links layered on top. It now has two rendering modes: + +- **2D radial rings:** the default hierarchy-first Mini World Map view. +- **3D map:** a Galaxy-derived Three.js map for flying through the vault link graph. It is designed for people who use folders as meaningful topic structure and links as cross-topic associations. The map helps you see both at once: where a note lives in the vault hierarchy, and which other notes or topics it connects to. @@ -18,6 +21,7 @@ The goal is to complement Obsidian's native Graph View and conventional mind-map ## Features +- **Two render modes:** switch between 2D radial rings and a 3D map from the view panel. - **Atlas view:** browse folders and notes as a radial world map rooted at the whole vault or at any folder. - **Focus view:** center the map around the active note and show its ancestors, siblings, outgoing links, and backlinks. - **Concentric hierarchy rings:** render parent folders closer to the center and child folders or notes farther outward. @@ -32,9 +36,22 @@ The goal is to complement Obsidian's native Graph View and conventional mind-map - **Canvas controls:** pan, zoom, drag nodes temporarily, reset the view, and switch between bounded and complete root detail. - **Adaptive detail:** automatically adjusts depth, node budget, and link budget when the map is large. - **Display controls:** configure atlas depth, node limit, link limit, outside-link detail, label visibility, spin speed, color scheme, and ignored folders. -- **Appearance modes:** follow Obsidian automatically, or force Mini World Map's Day or Night palette. +- **Appearance modes:** follow Obsidian/system by default, or force Mini World Map's Light or Night palette. +- **Panel language:** use English by default, with Chinese available for both 2D and 3D panels. - **Local-only indexing:** builds the map from your local vault metadata without network services or telemetry. +## Development + +Mini World Map is now built from TypeScript source. + +```bash +npm install +npm test +npm run build +``` + +`npm run build` validates TypeScript and writes release assets. The checked-in `main.js` is the generated bundle; the previous legacy bundle is preserved at `legacy/main.legacy.js`. + ## Usage 1. Enable Mini World Map in **Settings -> Community plugins**. @@ -54,7 +71,9 @@ Useful interactions: Mini World Map includes settings for: -- Color scheme: Auto, Day, or Night. +- Language: English or Chinese. +- Color scheme: System, Light, or Night. +- Default render mode: 2D radial rings or 3D map. - Default atlas depth. - Default link overlay limit. - Default render node limit. diff --git a/esbuild.config.mjs b/esbuild.config.mjs new file mode 100644 index 0000000..ab56885 --- /dev/null +++ b/esbuild.config.mjs @@ -0,0 +1,99 @@ +import esbuild from 'esbuild'; +import process from 'process'; +import fs from 'node:fs'; +import path from 'node:path'; +import { builtinModules } from 'node:module'; + +const banner = `/* +THIS IS A GENERATED/BUNDLED FILE BY ESBUILD +if you want to view the source, please visit the github repository of this plugin +*/ +`; + +const prod = process.argv[2] === 'production'; + +// dev 构建直接输出到本地 dev vault 的插件目录(可用 MINI_WORLD_MAP_OUTDIR 覆盖); +// prod 输出到 ./dist 作为 release 资产。 +const outDir = prod + ? path.resolve('./dist') + : (process.env.MINI_WORLD_MAP_OUTDIR ?? + path.resolve('./dev-vault/.obsidian/plugins/mini-world-map')); + +fs.mkdirSync(outDir, { recursive: true }); + +// 'worker:' 前缀导入 → 独立打包成 IIFE 文本(Blob URL Worker 用) +const inlineWorker = { + name: 'inline-worker', + setup(build) { + build.onResolve({ filter: /^worker:/ }, (args) => ({ + path: path.resolve(path.dirname(args.importer), args.path.slice('worker:'.length)), + namespace: 'inline-worker', + })); + build.onLoad({ filter: /.*/, namespace: 'inline-worker' }, async (args) => { + const result = await esbuild.build({ + entryPoints: [args.path], + bundle: true, + write: false, + format: 'iife', + target: 'es2021', + minify: prod, + }); + return { contents: result.outputFiles[0].text, loader: 'text' }; + }); + }, +}; + +const copyAssets = { + name: 'copy-assets', + setup(build) { + build.onEnd((result) => { + if (result.errors.length > 0) return; + fs.copyFileSync('manifest.json', path.join(outDir, 'manifest.json')); + fs.copyFileSync('styles.css', path.join(outDir, 'styles.css')); + if (!prod) { + // pjeby/hot-reload 监听该标记文件所在的插件目录 + fs.writeFileSync(path.join(outDir, '.hotreload'), ''); + } + }); + }, +}; + +const context = await esbuild.context({ + banner: { + js: banner, + }, + entryPoints: ['src/main.ts'], + bundle: true, + external: [ + 'obsidian', + 'electron', + '@codemirror/autocomplete', + '@codemirror/collab', + '@codemirror/commands', + '@codemirror/language', + '@codemirror/lint', + '@codemirror/search', + '@codemirror/state', + '@codemirror/view', + '@lezer/common', + '@lezer/highlight', + '@lezer/lr', + ...builtinModules, + ], + format: 'cjs', + target: 'es2021', + define: { __GALAXY_DEV__: prod ? 'false' : 'true' }, + logLevel: 'info', + sourcemap: prod ? false : 'inline', + treeShaking: true, + outfile: path.join(outDir, 'main.js'), + minify: prod, + plugins: [inlineWorker, copyAssets], +}); + +if (prod) { + await context.rebuild(); + process.exit(0); +} else { + await context.watch(); +} diff --git a/eslint.config.mts b/eslint.config.mts new file mode 100644 index 0000000..5179d36 --- /dev/null +++ b/eslint.config.mts @@ -0,0 +1,35 @@ +import tseslint from 'typescript-eslint'; +import globals from 'globals'; +import { globalIgnores } from 'eslint/config'; + +export default tseslint.config( + globalIgnores([ + 'node_modules', + 'dist', + 'dev-vault', + 'esbuild.config.mjs', + 'version-bump.mjs', + 'versions.json', + 'main.js', + 'legacy', + 'package.json', + 'package-lock.json', + 'tsconfig.json', + ]), + { + languageOptions: { + globals: { + ...globals.browser, + __GALAXY_DEV__: 'readonly', + }, + parserOptions: { + projectService: { + allowDefaultProject: ['eslint.config.mts', 'manifest.json'], + }, + tsconfigRootDir: import.meta.dirname, + extraFileExtensions: ['.json'], + }, + }, + }, + ...tseslint.configs.recommended, +); diff --git a/legacy/main.legacy.js b/legacy/main.legacy.js new file mode 100644 index 0000000..c1e0809 --- /dev/null +++ b/legacy/main.legacy.js @@ -0,0 +1,8957 @@ +const { + ItemView, + Menu, + Notice, + Plugin, + PluginSettingTab, + Setting, + TFile, + TFolder, + setIcon +} = require("obsidian"); + +const VIEW_TYPE_MINI_WORLD_MAP = "mini-world-map-view"; +const ROOT_ID = ""; +const ROOT_TITLE = "Vault"; +const MAX_ATLAS_DEPTH = 80; +const MAX_RENDER_NODE_LIMIT = 20000; +const MAX_LINK_LIMIT = 30000; +const MAX_EXTERNAL_LINK_ANCHOR_LIMIT = 20000; +const MIN_CANVAS_ZOOM = 0.01; +const DEFAULT_MIN_CANVAS_ZOOM = 0.04; +const MAX_CANVAS_ZOOM = 2; +const ZOOM_SLIDER_STEPS = 1000; +const ZOOM_SLIDER_CURVE = 1.35; +const ZOOM_WHEEL_BASE = 1.5; +const ZOOM_ANIMATION_FRICTION = 0.85; +const ZOOM_BUTTON_STEP = 1.14; +const COLOR_SCHEME_OPTIONS = ["auto", "day", "night"]; +const LABEL_VISIBILITY_OPTIONS = [ + ["auto", "Auto"], + ["hover", "Hover only"] +]; +const HOVER_HIGHLIGHT_MODE_OPTIONS = [ + ["none", "None"], + ["note-links", "Note links"], + ["hierarchy-parents", "Hierarchy parents"], + ["hierarchy-direct-children", "Hierarchy direct children"], + ["hierarchy-descendants", "Hierarchy all children"], + ["hierarchy-parents-direct", "Hierarchy parents + direct"], + ["hierarchy-all", "Hierarchy parents + all children"] +]; +const LEGEND_ITEM_DEFINITIONS = [ + ["root", "root", "Current atlas root", "mwm-legend-root"], + ["folder", "folder", "Folder / subtree", "mwm-legend-folder"], + ["folder-meta", "folder+", "Folder with merged meta file", "mwm-legend-meta"], + ["file", "file", "Markdown file", "mwm-legend-note"], + ["outside", "outside", "Grouped outside branch", "mwm-legend-external"], + ["outside-file", "outside file", "Exact linked file outside root", "mwm-legend-outside-file"], + ["missing", "missing", "Unresolved internal link", "mwm-legend-unresolved"], + ["tree", "tree", "Parent-child hierarchy", "mwm-legend-tree"], + ["link", "link", "Internal file links", "mwm-legend-link"], + ["outside-link", "outside link", "Crosses current root", "mwm-legend-link-external"], + ["dashed-link", "dashed", "Includes unresolved links", "mwm-legend-link-unresolved"] +]; +const DEFAULT_RING_SPACING = 960; +const MIN_RING_SPACING = 720; +const MAX_RING_SPACING = 2800; +const DEFAULT_NODE_SPACING = 126; +const MIN_NODE_SPACING = 72; +const MAX_NODE_SPACING = 360; +const DEFAULT_SWIRL_STRENGTH = 0; +const DEFAULT_SWIRL_BUTTON_STRENGTH = 32; +const MAX_SWIRL_STRENGTH = 100; +const SWIRL_FRAME_INTERVAL_MS = 42; +const SWIRL_BASE_SPEED_RAD_PER_SEC = 0.072; +const RING_JAGGED_BAND_FACTOR = 0.26; +const RING_JAGGED_MAX_FACTOR = 0.22; +const FAST_CANVAS_NODE_THRESHOLD = 2600; +const FAST_CANVAS_EDGE_THRESHOLD = 6500; +const FAST_CANVAS_LABEL_LIMIT = 90; +const MAX_DYNAMIC_ROUTE_EDGES = 12000; + +const LEGACY_DEFAULT_SETTINGS = { + atlasDepth: 4, + focusSiblingLimit: 80, + linkLimit: 220, + renderNodeLimit: 1400, + externalLinkAnchorLimit: 220, + enableLinkHover: false +}; + +const DEFAULT_SETTINGS = { + atlasDepth: 6, + focusSiblingLimit: 160, + linkLimit: 1200, + renderNodeLimit: 4200, + externalLinkAnchorLimit: 700, + adaptiveDetail: true, + includeUnresolvedLinks: true, + showLinkOverlay: false, + enableLinkHover: false, + hoverHighlightMode: "hierarchy-all", + showExternalLinks: true, + externalDetailMode: "grouped", + colorScheme: "auto", + labelVisibility: "auto", + swirlStrength: DEFAULT_SWIRL_STRENGTH, + hiddenLegendItems: [], + ignoreFolders: [ + ".git", + ".obsidian" + ] +}; + +const DEFAULT_NATIVE_GRAPH_SETTINGS = { + showArrow: true, + textFadeMultiplier: 0, + nodeSizeMultiplier: 1, + lineSizeMultiplier: 1, + repelStrength: 10, + linkStrength: 1, + linkDistance: 250, + scale: 1 +}; + +const OBSIDIAN_PIXI_ASSET_PATH = "lib/pixi.min.js"; +let miniWorldMapPixiRuntime = null; +let miniWorldMapAsarCandidatesCache = null; + +function miniWorldMapNodeRequire(moduleName) { + try { + return require(moduleName); + } catch (error) { + return null; + } +} + +function miniWorldMapAsarCandidates() { + if (miniWorldMapAsarCandidatesCache) return miniWorldMapAsarCandidatesCache; + const fs = miniWorldMapNodeRequire("fs"); + const path = miniWorldMapNodeRequire("path"); + const os = miniWorldMapNodeRequire("os"); + const processRef = typeof process !== "undefined" ? process : null; + if (!fs || !path) { + miniWorldMapAsarCandidatesCache = []; + return miniWorldMapAsarCandidatesCache; + } + + const candidates = []; + const add = filePath => { + if (!filePath || candidates.includes(filePath)) return; + try { + if (fs.existsSync(filePath)) candidates.push(filePath); + } catch (error) { + // Ignore unreadable install locations; another candidate may work. + } + }; + const addVersioned = directory => { + if (!directory) return; + try { + const files = fs.readdirSync(directory) + .filter(file => /^obsidian-.+\.asar$/i.test(file)) + .map(file => { + const filePath = path.join(directory, file); + let mtime = 0; + try { + mtime = fs.statSync(filePath).mtimeMs; + } catch (error) { + // Keep a neutral sort value if stat is unavailable. + } + return { filePath, mtime }; + }) + .sort((a, b) => b.mtime - a.mtime); + for (const item of files) add(item.filePath); + } catch (error) { + // Not every install keeps versioned ASARs in userData. + } + }; + + if (processRef?.resourcesPath) add(path.join(processRef.resourcesPath, "obsidian.asar")); + + const electron = miniWorldMapNodeRequire("electron"); + const electronApp = electron?.remote?.app || electron?.app; + try { + const userData = electronApp?.getPath?.("userData"); + const version = electronApp?.getVersion?.(); + if (userData && version) add(path.join(userData, `obsidian-${version}.asar`)); + addVersioned(userData); + } catch (error) { + // Obsidian may not expose electron.remote from the renderer. + } + + const home = os?.homedir?.(); + if (home) { + if (processRef?.platform === "darwin") { + addVersioned(path.join(home, "Library", "Application Support", "obsidian")); + add("/Applications/Obsidian.app/Contents/Resources/obsidian.asar"); + } else if (processRef?.platform === "win32") { + addVersioned(path.join(processRef.env?.APPDATA || path.join(home, "AppData", "Roaming"), "obsidian")); + } else { + addVersioned(path.join(processRef?.env?.XDG_CONFIG_HOME || path.join(home, ".config"), "obsidian")); + } + } + + miniWorldMapAsarCandidatesCache = candidates; + return candidates; +} + +function findMiniWorldMapAsarEntry(files, assetPath) { + const parts = String(assetPath || "").replace(/^\/+/, "").split("/").filter(Boolean); + let entry = { files }; + for (const part of parts) { + entry = entry?.files?.[part]; + if (!entry) return null; + } + return entry; +} + +function readMiniWorldMapAsarFile(asarPath, assetPath) { + const fs = miniWorldMapNodeRequire("fs"); + const path = miniWorldMapNodeRequire("path"); + if (!fs || !path) return null; + let fd = null; + try { + fd = fs.openSync(asarPath, "r"); + const sizeHeader = Buffer.alloc(16); + fs.readSync(fd, sizeHeader, 0, sizeHeader.length, 0); + const headerSize = sizeHeader.readUInt32LE(8); + const jsonSize = sizeHeader.readUInt32LE(12); + if (!Number.isFinite(headerSize) || !Number.isFinite(jsonSize) || jsonSize <= 0 || jsonSize > headerSize) return null; + + const headerBuffer = Buffer.alloc(jsonSize); + fs.readSync(fd, headerBuffer, 0, jsonSize, 16); + const header = JSON.parse(headerBuffer.toString("utf8")); + const normalizedAssetPath = String(assetPath || "").replace(/^\/+/, ""); + const entry = findMiniWorldMapAsarEntry(header.files, normalizedAssetPath); + if (!entry) return null; + if (entry.unpacked) { + return fs.readFileSync(path.join(`${asarPath}.unpacked`, normalizedAssetPath)).toString("utf8"); + } + if (!Number.isFinite(entry.size) || entry.offset === undefined) return null; + const start = 12 + headerSize + Number(entry.offset); + const content = Buffer.alloc(entry.size); + fs.readSync(fd, content, 0, entry.size, start); + return content.toString("utf8"); + } catch (error) { + return null; + } finally { + if (fd !== null) { + try { + fs.closeSync(fd); + } catch (error) { + // Ignore close failures; the renderer can fall back to canvas. + } + } + } +} + +function loadMiniWorldMapPixiRuntime() { + if (typeof window !== "undefined" && window.PIXI) return window.PIXI; + if (miniWorldMapPixiRuntime) return miniWorldMapPixiRuntime; + + for (const asarPath of miniWorldMapAsarCandidates()) { + const source = readMiniWorldMapAsarFile(asarPath, OBSIDIAN_PIXI_ASSET_PATH); + if (!source) continue; + try { + const PIXI = new Function(`${source}\nreturn PIXI;`)(); + if (PIXI) { + miniWorldMapPixiRuntime = PIXI; + if (typeof window !== "undefined") window.PIXI = PIXI; + return PIXI; + } + } catch (error) { + console.error("Mini World Map failed to initialize Pixi from Obsidian bundle", error); + } + } + + return null; +} + +class MiniWorldMapPixiRenderer { + constructor(containerEl, PIXI) { + this.containerEl = containerEl; + this.PIXI = PIXI; + this.view = containerEl.createEl("canvas", { + cls: "mwm-canvas mwm-pixi-canvas", + attr: { + role: "img", + "aria-label": "Mini World Map graph", + tabindex: "0" + } + }); + this.app = new PIXI.Application({ + view: this.view, + antialias: true, + backgroundAlpha: 0, + autoStart: false, + resolution: window.devicePixelRatio || 1, + autoDensity: true + }); + this.background = new PIXI.Graphics(); + this.world = new PIXI.Container(); + this.rings = new PIXI.Graphics(); + this.baseEdges = new PIXI.Graphics(); + this.baseEdgeSprites = new PIXI.Container(); + this.highlightEdges = new PIXI.Graphics(); + this.focusNodes = new PIXI.Graphics(); + this.nodes = new PIXI.Container(); + this.labels = new PIXI.Container(); + this.measureCanvas = document.createElement("canvas"); + this.measureCtx = this.measureCanvas.getContext("2d"); + this.labelPool = []; + this.activeLabels = 0; + this.nodeSprites = new Map(); + this.nodeTextureCache = new Map(); + this.hasFrame = false; + this.lastActiveKey = ""; + this.lastViewportWidth = 0; + this.lastViewportHeight = 0; + this.rendererWidth = 0; + this.rendererHeight = 0; + this.rendererDpr = 0; + this.textStyleCache = new Map(); + this.baseEdgeSpritePool = []; + this.activeBaseEdgeSprites = 0; + this.edgeTexture = null; + this.backgroundKey = ""; + this.sceneSignature = ""; + this.dataRef = null; + this.layoutRef = null; + this.lastPaletteKey = ""; + this.lastZoomLayerBucket = ""; + this.lastLabelZoomBucket = ""; + this.lastTransform = { panX: 0, panY: 0, zoom: 1 }; + this.currentZoom = 1; + + this.app.stage.addChild(this.background); + this.app.stage.addChild(this.world); + this.world.addChild(this.rings); + this.world.addChild(this.baseEdges); + this.world.addChild(this.baseEdgeSprites); + this.world.addChild(this.highlightEdges); + this.world.addChild(this.focusNodes); + this.world.addChild(this.nodes); + this.app.stage.addChild(this.labels); + } + + destroy() { + this.nodeTextureCache.clear(); + if (this.app) { + this.app.destroy(true, { children: true, texture: true, baseTexture: true }); + this.app = null; + } + this.edgeTexture = null; + this.view?.remove(); + this.view = null; + } + + resize(viewport) { + if (!this.app || !viewport) return; + const width = Math.max(1, Math.floor(viewport.width || 1)); + const height = Math.max(1, Math.floor(viewport.height || 1)); + const dpr = Math.max(1, Math.min(3, window.devicePixelRatio || 1)); + if (this.rendererWidth === width && this.rendererHeight === height && this.rendererDpr === dpr) return; + this.rendererWidth = width; + this.rendererHeight = height; + this.rendererDpr = dpr; + if (this.app.renderer.resolution !== dpr) this.app.renderer.resolution = dpr; + this.app.renderer.resize(width, height); + this.view.style.width = `${width}px`; + this.view.style.height = `${height}px`; + } + + render(frame) { + if (!this.app || !frame || !frame.data || !frame.bundle) return; + const { data, bundle, palette, viewport, panX, panY, zoom } = frame; + this.currentZoom = clampFloat(zoom, MIN_CANVAS_ZOOM, MAX_CANVAS_ZOOM, 1); + const zoomLayerBucket = this.zoomLayerBucket(this.currentZoom); + const labelZoomBucket = this.labelZoomBucket(this.currentZoom); + const sceneSignature = this.createSceneSignature(frame); + const geometryDirty = Boolean(frame.geometryDirty); + const activeKey = frame.activeKey || ""; + const zoomLayerChanged = this.lastZoomLayerBucket !== zoomLayerBucket; + this.showArrow = data.showArrow !== false; + this.hasFrame = true; + this.lastViewportWidth = Math.max(1, Math.floor(viewport.width || 1)); + this.lastViewportHeight = Math.max(1, Math.floor(viewport.height || 1)); + + this.resize(viewport); + this.drawBackground(palette, viewport); + this.applyTransform(panX, panY, zoom); + + const sceneChanged = geometryDirty + || this.dataRef !== data + || this.layoutRef !== bundle.layout + || this.sceneSignature !== sceneSignature; + if (sceneChanged) { + this.rebuildScene(frame); + this.sceneSignature = sceneSignature; + this.dataRef = data; + this.layoutRef = bundle.layout; + this.lastZoomLayerBucket = zoomLayerBucket; + this.lastLabelZoomBucket = ""; + this.lastActiveKey = ""; + } else if (zoomLayerChanged) { + this.redrawZoomLayers(frame, { refreshRoutedEdges: frame.mode !== "interactive" }); + } + + const needsLabelRefresh = sceneChanged + || this.lastActiveKey !== activeKey + || this.lastLabelZoomBucket !== labelZoomBucket; + if (sceneChanged || this.lastActiveKey !== activeKey || zoomLayerChanged || needsLabelRefresh) { + this.applyActiveState(frame, { updateLabels: needsLabelRefresh }); + this.lastActiveKey = activeKey; + if (needsLabelRefresh) this.lastLabelZoomBucket = labelZoomBucket; + } else { + this.updateLabelPositions(); + } + + this.app.render(); + } + + canTransformOnly(frame) { + if (!this.hasFrame || !frame || !frame.viewport || frame.geometryDirty) return false; + const width = Math.max(1, Math.floor(frame.viewport.width || 1)); + const height = Math.max(1, Math.floor(frame.viewport.height || 1)); + const zoom = clampFloat(frame.zoom, MIN_CANVAS_ZOOM, MAX_CANVAS_ZOOM, 1); + const previousZoom = clampFloat(this.lastTransform?.zoom, MIN_CANVAS_ZOOM, MAX_CANVAS_ZOOM, 1); + const zoomMatches = Math.abs(previousZoom - zoom) < 0.0005; + return this.lastActiveKey === (frame.activeKey || "") + && this.lastViewportWidth === width + && this.lastViewportHeight === height + && (zoomMatches || frame.allowZoomTransformOnly); + } + + transformOnly(frame) { + if (!this.app || !frame || !this.canTransformOnly(frame)) return false; + const previousZoom = clampFloat(this.lastTransform?.zoom, MIN_CANVAS_ZOOM, MAX_CANVAS_ZOOM, 1); + this.currentZoom = clampFloat(frame.zoom, MIN_CANVAS_ZOOM, MAX_CANVAS_ZOOM, 1); + this.applyTransform(frame.panX, frame.panY, frame.zoom); + const zoomChanged = Math.abs(previousZoom - this.currentZoom) >= 0.0005; + if (zoomChanged && frame.allowZoomTransformOnly && frame.data && frame.bundle && frame.palette) { + this.redrawZoomLayers(frame); + this.applyActiveState(frame, { updateLabels: false }); + } else { + this.updateLabelPositions(); + } + this.app.render(); + return true; + } + + createSceneSignature(frame) { + const data = frame.data || {}; + const bundle = frame.bundle || {}; + const graph = bundle.graph || {}; + const layout = bundle.layout || {}; + const paletteKey = this.paletteSignature(frame.palette); + return [ + graph.rootId || "", + graph.focusId || "", + data.nodes?.length || 0, + data.hierarchy?.length || 0, + data.links?.length || 0, + data.hoverLinks?.length || 0, + Math.round(layout.width || 0), + Math.round(layout.height || 0), + Math.round((layout.centerX || 0) * 10), + Math.round((layout.centerY || 0) * 10), + Math.round((layout.swirlStrength || 0) * 1000), + data.showArrow !== false ? 1 : 0, + paletteKey + ].join("|"); + } + + zoomLayerBucket(zoom) { + return String(Math.round(clampFloat(zoom, MIN_CANVAS_ZOOM, MAX_CANVAS_ZOOM, 1) * 1000)); + } + + labelZoomBucket(zoom) { + return String(Math.round(clampFloat(zoom, MIN_CANVAS_ZOOM, MAX_CANVAS_ZOOM, 1) * 120)); + } + + paletteSignature(palette = {}) { + return [ + palette.bg, + palette.line, + palette.lineHighlight, + palette.node, + palette.note, + palette.folder, + palette.folderMeta, + palette.folderRing, + palette.tree, + palette.ringGuide, + palette.fileRing, + palette.focus, + palette.circle, + palette.labelText, + palette.labelStroke, + palette.link, + palette.external, + palette.externalLink, + palette.unresolved, + palette.stroke, + palette.glow, + palette.fontFamily + ].join("|"); + } + + drawBackground(palette, viewport) { + const key = `${palette.bg}|${Math.round(viewport.width)}|${Math.round(viewport.height)}`; + if (this.backgroundKey === key) return; + this.backgroundKey = key; + this.background.clear(); + this.fillRect(this.background, palette.bg, 0, 0, viewport.width, viewport.height, 1); + } + + applyTransform(panX, panY, zoom) { + this.world.position.set(panX, panY); + this.world.scale.set(zoom); + this.lastTransform = { panX, panY, zoom }; + } + + rebuildScene(frame) { + const { data, bundle, palette } = frame; + const paletteKey = this.paletteSignature(palette); + const paletteChanged = paletteKey !== this.lastPaletteKey; + this.rings.clear(); + this.baseEdges.clear(); + this.resetBaseEdgeSprites(); + this.highlightEdges.clear(); + this.focusNodes.clear(); + this.clearNodeSprites(); + if (paletteChanged) { + this.clearNodeTextureCache(); + this.lastPaletteKey = paletteKey; + } + this.hideLabels(); + this.drawRings(bundle.layout, palette, { underlay: true, now: frame.now }); + this.drawBaseEdges(data.links, palette, false); + this.drawBaseEdges(data.hierarchy, palette, true); + this.drawRings(bundle.layout, palette, { overlay: true, now: frame.now }); + this.buildNodeSprites(data.nodes, palette, bundle.graph || {}); + } + + redrawZoomLayers(frame, options = {}) { + const { data, bundle, palette } = frame; + if (!bundle || !bundle.layout || !palette) return; + this.rings.clear(); + if (options.refreshRoutedEdges) { + this.baseEdges.clear(); + this.drawRoutedBaseEdges(data?.links, palette, false); + this.drawRoutedBaseEdges(data?.hierarchy, palette, true); + } + this.updateBaseEdgeSpriteWidths(); + this.drawRings(bundle.layout, palette, { underlay: true, now: frame.now }); + this.drawRings(bundle.layout, palette, { overlay: true, now: frame.now }); + this.lastZoomLayerBucket = this.zoomLayerBucket(this.currentZoom); + } + + color(value, fallback = "#888888") { + return pixiParseColor(value) || pixiParseColor(fallback) || { rgb: 0x888888, alpha: 1 }; + } + + fillRect(graphics, colorValue, x, y, width, height, alpha = 1) { + const color = this.color(colorValue); + graphics.beginFill(color.rgb, color.alpha * alpha); + graphics.drawRect(x, y, width, height); + graphics.endFill(); + } + + lineStyle(graphics, width, colorValue, alpha = 1) { + const color = this.color(colorValue); + graphics.lineStyle(width, color.rgb, color.alpha * alpha); + } + + screenScaledWidth(width) { + const zoom = Math.max(MIN_CANVAS_ZOOM, this.currentZoom || 1); + return Math.max(0.01, width / zoom); + } + + nodeZoomScale() { + const zoom = Math.max(MIN_CANVAS_ZOOM, this.currentZoom || 1); + return clampFloat(Math.pow(1 / zoom, 0.3), 0.78, 1.34, 1); + } + + drawRings(layout, palette, options = {}) { + if (!layout || !Array.isArray(layout.rings) || !layout.rings.length) return; + const centerX = Number.isFinite(layout.centerX) ? layout.centerX : layout.width / 2; + const centerY = Number.isFinite(layout.centerY) ? layout.centerY : layout.height / 2; + const alphaScale = options.overlay ? 1.16 : options.underlay ? 0.58 : 1; + const swirlStrength = clampFloat(layout.swirlStrength, 0, 1, 0); + const spinSpeed = clampFloat(layout.spinSpeed || 0, 0, 1, 0); + const elapsedSeconds = Number.isFinite(options.now) && layout.spinStartedAt + ? Math.max(0, (options.now - layout.spinStartedAt) / 1000) + : 0; + + for (const ring of layout.rings) { + if (!Number.isFinite(ring.radius) || ring.radius <= 0) continue; + const density = Math.min(1, Math.sqrt(Math.max(1, ring.count || 1)) / 9); + const phase = swirlOrbitAngleForRing(ring.depth, ring.radius, spinSpeed, elapsedSeconds); + if (options.overlay) { + this.lineStyle(this.rings, this.screenScaledWidth(5.5), palette.ringGuide || palette.folderRing || palette.line, (0.032 + density * 0.022) * alphaScale); + pixiDrawRingPath(this.rings, centerX, centerY, ring.radius, ring.depth, swirlStrength, phase); + } + this.lineStyle(this.rings, this.screenScaledWidth(options.overlay ? 1.35 : 0.78), palette.ringGuide || palette.folderRing || palette.line, (0.13 + density * 0.09) * alphaScale); + pixiDrawRingPath(this.rings, centerX, centerY, ring.radius, ring.depth, swirlStrength, phase); + } + } + + drawBaseEdges(edges, palette, hierarchy) { + for (const item of edges || []) { + if (item.hoverOnly) continue; + const style = this.baseEdgeStyle(item, palette, hierarchy); + if (this.canDrawEdgeSprite(item)) this.drawBaseEdgeSprite(item, style); + else this.drawBaseEdgePath(item, style); + } + } + + drawRoutedBaseEdges(edges, palette, hierarchy) { + for (const item of edges || []) { + if (item.hoverOnly || this.canDrawEdgeSprite(item)) continue; + this.drawBaseEdgePath(item, this.baseEdgeStyle(item, palette, hierarchy)); + } + } + + baseEdgeStyle(item, palette, hierarchy) { + const unresolved = item.edge && item.edge.unresolvedCount; + const external = item.external || (item.edge && item.edge.externalCount); + const externalHierarchy = hierarchy && external; + const outsideLink = !hierarchy && external; + const color = unresolved + ? palette.unresolved + : outsideLink + ? (palette.externalLink || palette.external) + : hierarchy + ? (palette.tree || palette.folderRing || palette.line) + : palette.link; + const baseAlpha = (hierarchy + ? (externalHierarchy ? 0.18 : 0.34) + : (outsideLink ? 0.082 : 0.056)); + const minAlpha = hierarchy ? 0.09 : outsideLink ? 0.03 : 0.024; + const width = hierarchy + ? (externalHierarchy ? 1.12 : 1.48) + : (outsideLink ? item.width * 0.72 : item.width * 0.7); + return { + color, + alpha: Math.max(minAlpha, baseAlpha), + width: Math.max(outsideLink ? 0.58 : 0.45, width) + }; + } + + drawBaseEdgePath(item, style) { + this.lineStyle(this.baseEdges, this.screenScaledWidth(style.width), style.color, style.alpha); + this.drawEdgePath(this.baseEdges, item); + } + + canDrawEdgeSprite(item) { + if (!item || item.hoverOnly) return false; + if (item.dynamicRoute || item.route) return false; + const source = item.sourcePoint; + const target = item.targetPoint; + return Boolean(source && target && Math.hypot(target.x - source.x, target.y - source.y) > 0.5); + } + + drawBaseEdgeSprite(item, style) { + const source = item.sourcePoint; + const target = item.targetPoint; + const dx = target.x - source.x; + const dy = target.y - source.y; + const length = Math.hypot(dx, dy); + if (length <= 0.5) return; + const color = this.color(style.color); + const sprite = this.nextBaseEdgeSprite(); + sprite.visible = true; + sprite.position.set(source.x, source.y); + sprite.rotation = Math.atan2(dy, dx); + sprite.tint = color.rgb; + sprite.alpha = color.alpha * style.alpha; + sprite.width = length; + sprite.__mwmScreenWidth = style.width; + this.setBaseEdgeSpriteWidth(sprite); + } + + nextBaseEdgeSprite() { + let sprite = this.baseEdgeSpritePool[this.activeBaseEdgeSprites]; + if (!sprite) { + sprite = new this.PIXI.Sprite(this.edgeLineTexture()); + if (sprite.anchor && typeof sprite.anchor.set === "function") sprite.anchor.set(0, 0.5); + sprite.eventMode = "none"; + sprite.interactive = false; + sprite.roundPixels = false; + this.baseEdgeSpritePool.push(sprite); + this.baseEdgeSprites.addChild(sprite); + } + this.activeBaseEdgeSprites += 1; + return sprite; + } + + edgeLineTexture() { + if (this.edgeTexture) return this.edgeTexture; + const graphics = new this.PIXI.Graphics(); + graphics.beginFill(0xffffff, 1); + graphics.drawRect(0, 0, 1, 1); + graphics.endFill(); + this.edgeTexture = this.app.renderer.generateTexture(graphics); + graphics.destroy(); + return this.edgeTexture; + } + + resetBaseEdgeSprites() { + this.activeBaseEdgeSprites = 0; + for (const sprite of this.baseEdgeSpritePool) { + sprite.visible = false; + } + } + + updateBaseEdgeSpriteWidths() { + for (let index = 0; index < this.activeBaseEdgeSprites; index += 1) { + this.setBaseEdgeSpriteWidth(this.baseEdgeSpritePool[index]); + } + } + + setBaseEdgeSpriteWidth(sprite) { + if (!sprite) return; + sprite.height = this.screenScaledWidth(sprite.__mwmScreenWidth || 1); + } + + drawHighlightedEdges(frame) { + const data = frame.data; + const active = frame.active || {}; + const palette = frame.palette; + this.highlightEdges.clear(); + if (!active.highlightedEdges || !active.highlightedEdges.size) return; + for (const key of active.highlightedEdges) { + const item = data.edgesById.get(key); + if (!item) continue; + const hierarchy = data.hierarchyParentByNode.get(item.target) === item + || data.hierarchyChildrenByNode.get(item.source)?.includes(item); + const outsideLink = !hierarchy && (item.external || (item.edge && item.edge.externalCount)); + const color = outsideLink ? (palette.externalLink || palette.external) : (palette.lineHighlight || palette.focus); + this.lineStyle(this.highlightEdges, this.screenScaledWidth(hierarchy ? 2.35 : outsideLink ? 2.2 : 2.45), color, hierarchy ? 0.78 : outsideLink ? 0.76 : 0.88); + this.drawEdgePath(this.highlightEdges, item); + if (!hierarchy && this.showArrow) this.drawArrow(this.highlightEdges, item, color, 0.95); + } + } + + drawEdgePath(graphics, item) { + const source = item.sourcePoint; + const target = item.targetPoint; + const route = item.dynamicRoute || item.route; + if (!source || !target) return; + graphics.moveTo(source.x, source.y); + if (route && (route.kind === "outer" || route.kind === "external" || route.kind === "dynamic-orbit")) { + const startAngle = route.sourceAngle; + const endAngle = Number.isFinite(route.endAngle) ? route.endAngle : route.targetAngle; + const arcStart = radialPoint(route.centerX, route.centerY, route.radius, startAngle); + const arcEnd = radialPoint(route.centerX, route.centerY, route.radius, endAngle); + graphics.lineTo(arcStart.x, arcStart.y); + graphics.arc(route.centerX, route.centerY, route.radius, startAngle, endAngle); + graphics.lineTo(arcEnd.x, arcEnd.y); + graphics.lineTo(target.x, target.y); + return; + } + if (route && route.kind === "curve") { + const centerX = Number.isFinite(source.centerX) ? source.centerX : (source.x + target.x) / 2; + const centerY = Number.isFinite(source.centerY) ? source.centerY : (source.y + target.y) / 2; + const midX = (source.x + target.x) / 2; + const midY = (source.y + target.y) / 2; + const pull = Number.isFinite(route.curveStrength) ? route.curveStrength : (item.external ? 0.28 : 0.16); + graphics.quadraticCurveTo(midX + (centerX - midX) * pull, midY + (centerY - midY) * pull, target.x, target.y); + return; + } + graphics.lineTo(target.x, target.y); + } + + drawArrow(graphics, item, colorValue, alpha = 1) { + const source = this.arrowSourcePoint(item); + const target = item.targetPoint; + if (!source || !target) return; + const dx = target.x - source.x; + const dy = target.y - source.y; + const length = Math.hypot(dx, dy); + if (length < 0.001) return; + const angle = Math.atan2(dy, dx); + const targetRadius = Math.max(4, item.targetPoint?.nodeRadius || 6); + const zoom = Math.max(MIN_CANVAS_ZOOM, this.currentZoom || 1); + const size = 7 / zoom; + const spread = 0.48; + const color = this.color(colorValue); + const tipX = target.x - Math.cos(angle) * (targetRadius + 2 / zoom); + const tipY = target.y - Math.sin(angle) * (targetRadius + 2 / zoom); + graphics.beginFill(color.rgb, color.alpha * alpha); + graphics.moveTo(tipX, tipY); + graphics.lineTo(tipX - Math.cos(angle - spread) * size, tipY - Math.sin(angle - spread) * size); + graphics.lineTo(tipX - Math.cos(angle + spread) * size, tipY - Math.sin(angle + spread) * size); + graphics.closePath(); + graphics.endFill(); + } + + arrowSourcePoint(item) { + const route = item.dynamicRoute || item.route; + if (route && (route.kind === "outer" || route.kind === "external" || route.kind === "dynamic-orbit")) { + const endAngle = Number.isFinite(route.endAngle) ? route.endAngle : route.targetAngle; + return radialPoint(route.centerX, route.centerY, route.radius, endAngle); + } + if (route && route.kind === "curve") { + const source = item.sourcePoint; + const target = item.targetPoint; + const centerX = Number.isFinite(source.centerX) ? source.centerX : (source.x + target.x) / 2; + const centerY = Number.isFinite(source.centerY) ? source.centerY : (source.y + target.y) / 2; + const midX = (source.x + target.x) / 2; + const midY = (source.y + target.y) / 2; + const pull = Number.isFinite(route.curveStrength) ? route.curveStrength : (item.external ? 0.28 : 0.16); + return { + x: midX + (centerX - midX) * pull, + y: midY + (centerY - midY) * pull + }; + } + return item.sourcePoint; + } + + buildNodeSprites(nodes, palette, graph = {}) { + for (const item of nodes || []) { + const texture = this.getNodeTexture(item, palette, graph); + const sprite = new this.PIXI.Sprite(texture); + if (sprite.anchor && typeof sprite.anchor.set === "function") sprite.anchor.set(0.5); + sprite.position.set(item.point.x, item.point.y); + sprite.alpha = item.node.type === "external" && !item.node.externalProxy ? 0.78 : 0.92; + this.nodes.addChild(sprite); + this.nodeSprites.set(item.node.id, { + sprite, + item, + baseAlpha: sprite.alpha + }); + } + } + + clearNodeSprites() { + this.nodes.removeChildren(); + this.nodeSprites.clear(); + } + + clearNodeTextureCache() { + for (const texture of this.nodeTextureCache.values()) { + if (texture && typeof texture.destroy === "function") texture.destroy(true); + } + this.nodeTextureCache.clear(); + } + + getNodeTexture(item, palette, graph = {}) { + const colors = this.nodeColors(item, palette, graph); + const radius = Math.max(3, Math.round(item.radius * 2) / 2); + const rootNode = item.node.id === graph.rootId; + const folderNode = item.node.type === "folder"; + const strokeWidth = Math.max(1.1, rootNode ? 1.75 : folderNode ? 1.35 : 1.1); + const fill = this.color(colors.fill); + const stroke = this.color(colors.stroke); + const key = [ + radius, + strokeWidth, + fill.rgb, + Math.round(fill.alpha * 100), + stroke.rgb, + Math.round(stroke.alpha * 100), + item.node.type, + item.node.externalProxy ? 1 : 0 + ].join("|"); + const cached = this.nodeTextureCache.get(key); + if (cached) return cached; + + const pad = Math.ceil(strokeWidth + 4); + const size = Math.ceil((radius + pad) * 2); + const center = size / 2; + const graphics = new this.PIXI.Graphics(); + graphics.beginFill(fill.rgb, fill.alpha); + graphics.drawCircle(center, center, radius); + graphics.endFill(); + graphics.lineStyle(strokeWidth, stroke.rgb, stroke.alpha); + graphics.drawCircle(center, center, radius); + if (item.node.type === "external" || item.node.externalProxy || item.node.type === "unresolved") { + graphics.lineStyle(0.8, stroke.rgb, stroke.alpha * 0.48); + graphics.drawCircle(center, center, Math.max(1, radius + 2.2)); + } + + let texture; + try { + texture = this.app.renderer.generateTexture(graphics, { resolution: 2 }); + } catch (error) { + texture = this.app.renderer.generateTexture(graphics); + } + graphics.destroy(); + this.nodeTextureCache.set(key, texture); + if (this.nodeTextureCache.size > 320) { + const firstKey = this.nodeTextureCache.keys().next().value; + const firstTexture = this.nodeTextureCache.get(firstKey); + if (firstTexture && typeof firstTexture.destroy === "function") firstTexture.destroy(true); + this.nodeTextureCache.delete(firstKey); + } + return texture; + } + + nodeColors(item, palette, graph = {}) { + const nodeId = item.node.id; + const rootNode = nodeId === graph.rootId; + const folderNode = item.node.type === "folder"; + const folderWithMeta = folderNode && Boolean(item.node.representativeFile); + const externalGroup = item.node.type === "external" && !item.node.externalProxy; + const externalFile = Boolean(item.node.externalProxy); + const unresolvedNode = item.node.type === "unresolved"; + const fill = unresolvedNode + ? palette.unresolved + : externalGroup + ? palette.node + : folderWithMeta + ? palette.folderMeta + : folderNode + ? palette.folder + : (palette.note || palette.node); + const stroke = rootNode + ? palette.circle + : unresolvedNode + ? palette.unresolved + : externalGroup || externalFile + ? palette.external + : folderNode + ? palette.folderRing + : palette.fileRing || palette.stroke; + return { fill, stroke }; + } + + applyActiveState(frame, options = {}) { + const active = frame.active || {}; + const data = frame.data; + const palette = frame.palette; + const graph = frame.bundle?.graph || {}; + const related = active.relatedNodes || new Set(); + const pinned = active.pinnedNodeIds || new Set(); + const focusColor = this.color(palette.focus || palette.circle); + const glowColor = this.color(palette.glow || palette.focus || palette.circle); + const nodeZoomScale = this.nodeZoomScale(); + this.focusNodes.clear(); + this.drawHighlightedEdges(frame); + + for (const [nodeId, record] of this.nodeSprites.entries()) { + const item = record.item; + const selected = nodeId === frame.selectedNodeId; + const focused = nodeId === active.activeNodeId + || selected + || pinned.has(nodeId) + || nodeId === graph.focusId + || item.searchMatch; + const isRelated = related.has(nodeId); + const dimmed = active.hasActive && !focused && !isRelated; + record.sprite.alpha = dimmed ? Math.max(0.22, record.baseAlpha * 0.32) : focused ? 1 : isRelated ? Math.min(1, record.baseAlpha + 0.08) : record.baseAlpha; + const scale = focused ? 1.16 : isRelated ? 1.06 : 1; + record.sprite.scale.set(scale * nodeZoomScale); + if (focused || (isRelated && related.size <= 900)) { + const color = focused ? focusColor : glowColor; + const alpha = focused ? 0.24 : 0.1; + this.focusNodes.beginFill(color.rgb, color.alpha * alpha); + this.focusNodes.drawCircle(item.point.x, item.point.y, item.radius * nodeZoomScale * (focused ? 2.05 : 1.55)); + this.focusNodes.endFill(); + } + } + + if (options.updateLabels !== false) this.updateLabels(frame); + this.updateLabelPositions(); + } + + updateLabels(frame) { + const data = frame.data; + const graph = frame.bundle?.graph || {}; + const active = frame.active || {}; + const palette = frame.palette; + const hoverOnlyLabels = normalizeLabelVisibility(frame.labelVisibility) === "hover"; + const interactive = frame.mode === "interactive" || frame.mode === "spin"; + const budget = interactive ? 120 : 220; + const items = []; + const seen = new Set(); + const pushItem = id => { + if (id === null || id === undefined || seen.has(id)) return; + const item = data.nodesById.get(id); + if (!item) return; + seen.add(id); + items.push(item); + }; + + pushItem(active.activeNodeId); + pushItem(frame.selectedNodeId); + pushItem(graph.focusId); + for (const id of active.pinnedNodeIds || []) pushItem(id); + for (const item of data.searchMatchItems || []) pushItem(item.node.id); + for (const id of active.labelNodes || []) { + if (items.length >= budget) break; + pushItem(id); + } + if (!hoverOnlyLabels) { + for (const item of data.labelItems || []) { + if (items.length >= budget) break; + if (seen.has(item.node.id)) continue; + if (zoomLabelStrength(item, frame.zoom, graph) <= 0.08 && !item.searchMatch) continue; + pushItem(item.node.id); + } + } + + this.hideLabels(); + const fontSize = 12; + const lineHeight = 14; + const screenScale = labelScreenScale(frame.zoom); + for (const item of items) { + const labelStrength = this.labelStrengthForItem(item, frame, active, graph, hoverOnlyLabels); + if (labelStrength <= 0.02) continue; + const rootNode = item.node.id === graph.rootId; + const folderNode = item.node.type === "folder"; + const leading = Number.isFinite(item.labelRank) && item.labelRank < 36; + const localScale = screenScale * (rootNode ? 1.18 : item.radius >= 24 ? 1.1 : item.radius >= 15 ? 1.04 : 1); + const weight = rootNode || leading || folderNode ? "600" : "400"; + const maxWidth = (rootNode ? 190 : folderNode ? 156 : leading ? 146 : 124) * localScale; + const maxLines = rootNode || item.node.id === active.activeNodeId || item.node.id === frame.selectedNodeId ? 4 : 3; + this.measureCtx.font = `${weight} ${fontSize * localScale}px ${palette.fontFamily}`; + const lines = wrapCanvasLabel(this.measureCtx, item.label, maxWidth, maxLines); + if (!lines.length) continue; + + const text = this.nextLabel(); + text.text = lines.join("\n"); + text.visible = true; + text.alpha = labelStrength * (rootNode ? 0.98 : leading || active.labelNodes?.has(item.node.id) ? 0.94 : 0.86); + text.anchor.set(0.5, 0); + text.__mwmItem = item; + text.__mwmLocalScale = localScale; + text.style = this.getTextStyle({ + fontFamily: palette.fontFamily, + fontSize: fontSize * localScale, + fontWeight: weight, + lineHeight: lineHeight * localScale, + fill: this.color(rootNode ? (palette.circle || palette.labelText || palette.text) : (palette.labelText || palette.text)).rgb, + stroke: this.color(palette.labelStroke || palette.bg).rgb, + strokeThickness: (rootNode ? 6 : 5) * localScale + }); + } + } + + labelStrengthForItem(item, frame, active, graph, hoverOnlyLabels) { + if (!item || !item.node) return 0; + const nodeId = item.node.id; + const directlyRequested = nodeId === active.activeNodeId + || nodeId === frame.selectedNodeId + || active.pinnedNodeIds?.has(nodeId) + || item.searchMatch; + if (hoverOnlyLabels) return directlyRequested ? 1 : 0; + if (directlyRequested || active.labelNodes?.has(nodeId)) return 1; + return zoomLabelStrength(item, frame.zoom, graph); + } + + updateLabelPositions() { + const { panX, panY, zoom } = this.lastTransform || { panX: 0, panY: 0, zoom: 1 }; + for (let index = 0; index < this.activeLabels; index += 1) { + const text = this.labelPool[index]; + const item = text?.__mwmItem; + if (!text || !item) continue; + const localScale = text.__mwmLocalScale || 1; + const nodeZoomScale = this.nodeZoomScale(); + text.x = panX + item.point.x * zoom; + text.y = panY + item.point.y * zoom + item.radius * nodeZoomScale * zoom + 8 * localScale; + } + } + + nextLabel() { + let text = this.labelPool[this.activeLabels]; + if (!text) { + text = new this.PIXI.Text(""); + text.eventMode = "none"; + text.resolution = 2; + this.labelPool.push(text); + this.labels.addChild(text); + } + this.activeLabels += 1; + return text; + } + + hideLabels() { + for (let index = 0; index < this.labelPool.length; index += 1) { + this.labelPool[index].visible = false; + } + this.activeLabels = 0; + } + + getTextStyle(options) { + const key = [ + options.fontFamily, + Math.round(options.fontSize * 10) / 10, + options.fontWeight, + Math.round(options.lineHeight * 10) / 10, + options.fill, + options.stroke, + Math.round(options.strokeThickness * 10) / 10 + ].join("|"); + let style = this.textStyleCache.get(key); + if (!style) { + style = new this.PIXI.TextStyle({ + fontFamily: options.fontFamily, + fontSize: options.fontSize, + fontWeight: options.fontWeight, + lineHeight: options.lineHeight, + align: "center", + fill: options.fill, + stroke: options.stroke, + strokeThickness: options.strokeThickness, + wordWrap: false + }); + this.textStyleCache.set(key, style); + if (this.textStyleCache.size > 160) { + const firstKey = this.textStyleCache.keys().next().value; + this.textStyleCache.delete(firstKey); + } + } + return style; + } +} + +function pixiParseColor(value) { + if (!value || typeof value !== "string") return null; + const input = value.trim(); + const hex = input.match(/^#([0-9a-f]{3}|[0-9a-f]{6}|[0-9a-f]{8})$/i); + if (hex) { + let raw = hex[1]; + if (raw.length === 3) raw = raw.split("").map(ch => ch + ch).join(""); + const rgb = parseInt(raw.slice(0, 6), 16); + const alpha = raw.length === 8 ? parseInt(raw.slice(6, 8), 16) / 255 : 1; + return { rgb, alpha }; + } + + const rgb = input.match(/^rgba?\((.+)\)$/i); + if (rgb) { + const parts = rgb[1] + .replace(/\s*\/\s*/g, " ") + .replace(/,/g, " ") + .split(/\s+/) + .filter(Boolean); + if (parts.length >= 3) { + const r = parsePixiCssChannel(parts[0]); + const g = parsePixiCssChannel(parts[1]); + const b = parsePixiCssChannel(parts[2]); + const alpha = parts.length >= 4 ? parsePixiCssAlpha(parts[3]) : 1; + if ([r, g, b, alpha].every(Number.isFinite)) return { rgb: (r << 16) | (g << 8) | b, alpha }; + } + } + + const srgb = input.match(/^color\(\s*srgb\s+(.+)\)$/i); + if (srgb) { + const parts = srgb[1] + .replace(/\s*\/\s*/g, " ") + .split(/\s+/) + .filter(Boolean); + if (parts.length >= 3) { + const r = parsePixiCssUnitChannel(parts[0]); + const g = parsePixiCssUnitChannel(parts[1]); + const b = parsePixiCssUnitChannel(parts[2]); + const alpha = parts.length >= 4 ? parsePixiCssAlpha(parts[3]) : 1; + if ([r, g, b, alpha].every(Number.isFinite)) return { rgb: (r << 16) | (g << 8) | b, alpha }; + } + } + + return null; +} + +function parsePixiCssChannel(value) { + if (String(value).endsWith("%")) return clampNumber(parseFloat(value) * 2.55, 0, 255, 0); + return clampNumber(parseFloat(value), 0, 255, 0); +} + +function parsePixiCssUnitChannel(value) { + if (String(value).endsWith("%")) return clampNumber(parseFloat(value) * 2.55, 0, 255, 0); + return clampNumber(parseFloat(value) * 255, 0, 255, 0); +} + +function parsePixiCssAlpha(value) { + if (String(value).endsWith("%")) return clampFloat(parseFloat(value) / 100, 0, 1, 1); + return clampFloat(parseFloat(value), 0, 1, 1); +} + +function pixiDrawRingPath(graphics, centerX, centerY, radius, depth = 0, swirlStrength = 0, orbitPhase = 0) { + const strength = clampFloat(swirlStrength, 0, 1, 0); + if (strength <= 0.001) { + graphics.drawCircle(centerX, centerY, radius); + return; + } + + const fullCircle = Math.PI * 2; + const steps = 180; + const amplitude = Math.min(radius * 0.045, 58) * strength; + const phase = depth * 0.74 + strength * 0.9 + orbitPhase; + + for (let step = 0; step <= steps; step += 1) { + const t = step / steps; + const angle = t * fullCircle; + const twist = Math.sin(angle - phase) * 0.08 * strength; + const ripple = Math.sin(angle * 2 + phase) * amplitude + + Math.sin(angle * 5 - phase * 0.7) * amplitude * 0.26; + const r = Math.max(1, radius + ripple); + const x = centerX + Math.cos(angle + twist) * r; + const y = centerY + Math.sin(angle + twist) * r; + if (step === 0) graphics.moveTo(x, y); + else graphics.lineTo(x, y); + } + graphics.closePath(); +} + +class MiniWorldMapPlugin extends Plugin { + async onload() { + this.settings = normalizeSettings(await this.loadData()); + this.nativeGraphSettings = Object.assign({}, DEFAULT_NATIVE_GRAPH_SETTINGS); + this.index = new WorldMapIndex(this.app, this.settings); + this.rebuildTimer = null; + this.themeRefreshTimer = null; + this.themeObserver = null; + + this.registerView( + VIEW_TYPE_MINI_WORLD_MAP, + leaf => new MiniWorldMapView(leaf, this) + ); + + this.addRibbonIcon("network", "Open Mini World Map", () => { + this.activateView(); + }); + + this.addCommand({ + id: "open-map", + name: "Open Mini World Map", + callback: () => this.activateView() + }); + + this.addCommand({ + id: "rebuild-index", + name: "Rebuild Mini World Map index", + callback: () => this.rebuildIndex("manual") + }); + + this.addSettingTab(new MiniWorldMapSettingTab(this.app, this)); + + this.registerEvent(this.app.vault.on("create", () => this.scheduleRebuild("vault create"))); + this.registerEvent(this.app.vault.on("modify", file => { + if (file instanceof TFile && file.extension === "md") this.scheduleRebuild("vault modify"); + })); + this.registerEvent(this.app.vault.on("delete", () => this.scheduleRebuild("vault delete"))); + this.registerEvent(this.app.vault.on("rename", () => this.scheduleRebuild("vault rename"))); + this.registerEvent(this.app.metadataCache.on("resolved", () => this.scheduleRebuild("metadata resolved"))); + this.installThemeRefresh(); + + this.app.workspace.onLayoutReady(() => { + this.loadNativeGraphSettings(); + }); + } + + onunload() { + if (this.rebuildTimer) window.clearTimeout(this.rebuildTimer); + if (this.themeRefreshTimer) window.clearTimeout(this.themeRefreshTimer); + if (this.themeObserver) this.themeObserver.disconnect(); + } + + async activateView() { + const leaves = this.app.workspace.getLeavesOfType(VIEW_TYPE_MINI_WORLD_MAP); + let leaf = leaves.first(); + + if (!leaf) { + leaf = this.app.workspace.getLeaf("tab"); + await leaf.setViewState({ type: VIEW_TYPE_MINI_WORLD_MAP, active: true }); + } + + this.app.workspace.revealLeaf(leaf); + if (!this.index.ready) await this.rebuildIndex("open"); + } + + scheduleRebuild(reason) { + if (!this.index.ready && !this.hasOpenMapView()) return; + if (this.rebuildTimer) window.clearTimeout(this.rebuildTimer); + this.rebuildTimer = window.setTimeout(() => { + this.rebuildTimer = null; + this.rebuildIndex(reason); + }, 900); + } + + hasOpenMapView() { + return this.app.workspace.getLeavesOfType(VIEW_TYPE_MINI_WORLD_MAP).length > 0; + } + + async rebuildIndex(reason, options = {}) { + try { + const started = performance.now(); + this.index = new WorldMapIndex(this.app, this.settings); + this.index.rebuild(); + const elapsed = Math.round(performance.now() - started); + this.refreshViews(options); + if (reason === "manual") { + new Notice(`Mini World Map rebuilt in ${elapsed} ms`); + } + } catch (error) { + console.error("Mini World Map index rebuild failed", error); + new Notice("Mini World Map index rebuild failed. See developer console."); + } + } + + refreshViews(options = {}) { + for (const leaf of this.app.workspace.getLeavesOfType(VIEW_TYPE_MINI_WORLD_MAP)) { + const view = leaf.view; + if (view && typeof view.refresh === "function") view.refresh(options); + } + } + + installThemeRefresh() { + const refresh = () => this.scheduleThemeRefresh(); + if (this.app.workspace && typeof this.app.workspace.on === "function") { + this.registerEvent(this.app.workspace.on("css-change", refresh)); + } + + if (typeof MutationObserver === "undefined" || typeof document === "undefined" || !document.body) return; + this.themeObserver = new MutationObserver(records => { + if (records.some(record => record.attributeName === "class")) refresh(); + }); + this.themeObserver.observe(document.body, { + attributes: true, + attributeFilter: ["class"] + }); + } + + scheduleThemeRefresh() { + if (this.themeRefreshTimer) window.clearTimeout(this.themeRefreshTimer); + this.themeRefreshTimer = window.setTimeout(() => { + this.themeRefreshTimer = null; + this.refreshViews(); + }, 60); + } + + async saveSettings(options = {}) { + await this.saveData(this.settings); + if (options.rebuild !== false) { + if (options.immediateRebuild) { + if (this.rebuildTimer) { + window.clearTimeout(this.rebuildTimer); + this.rebuildTimer = null; + } + await this.rebuildIndex("settings", { preservePanel: options.preservePanel === true }); + } + else this.scheduleRebuild("settings"); + } else if (options.refresh !== false) this.refreshViews(); + } + + applySettingsToOpenViews(keys, options = {}) { + const settingKeys = Array.isArray(keys) ? keys : [keys].filter(Boolean); + if (!settingKeys.length) return; + for (const leaf of this.app.workspace.getLeavesOfType(VIEW_TYPE_MINI_WORLD_MAP)) { + const view = leaf.view; + if (view && typeof view.applyPluginSettings === "function") { + view.applyPluginSettings(settingKeys, options); + } + } + } + + async loadNativeGraphSettings() { + try { + const raw = await this.app.vault.adapter.read(".obsidian/graph.json"); + this.nativeGraphSettings = Object.assign( + {}, + DEFAULT_NATIVE_GRAPH_SETTINGS, + JSON.parse(raw || "{}") + ); + this.refreshViews(); + } catch (error) { + this.nativeGraphSettings = Object.assign({}, DEFAULT_NATIVE_GRAPH_SETTINGS); + } + } +} + +class WorldMapIndex { + constructor(app, settings) { + this.app = app; + this.settings = settings; + this.nodes = new Map(); + this.hierarchyEdges = []; + this.linkEdges = []; + this.childrenByParent = new Map(); + this.linkEdgesBySource = new Map(); + this.linkEdgesByTarget = new Map(); + this.folderRepresentatives = new Map(); + this.stats = { + loadedEntries: 0, + scannedMarkdown: 0, + folders: 0, + notes: 0, + unresolved: 0, + hierarchyEdges: 0, + linkEdges: 0, + maxDepth: 0 + }; + this.ready = false; + } + + rebuild() { + this.addNode({ + id: ROOT_ID, + path: ROOT_ID, + title: vaultRootTitle(this.app), + type: "folder", + parentId: null, + depth: 0, + noteCount: 0, + linkCount: 0, + backlinkCount: 0, + descendantCount: 0 + }); + + const { folders, markdownFiles, totalEntries } = this.collectVaultEntries(); + this.stats.loadedEntries = totalEntries; + this.stats.scannedMarkdown = markdownFiles.length; + + for (const folder of folders) { + this.addFolder(folder); + } + + for (const file of markdownFiles) { + this.ensureFolderPath(parentPath(file.path)); + this.addNote(file); + } + + this.identifyFolderRepresentatives(); + this.buildHierarchyEdges(); + this.buildLinkEdges(); + this.computeStats(); + this.ready = true; + } + + collectVaultEntries() { + const folders = []; + const markdownFiles = []; + let totalEntries = 0; + const root = typeof this.app.vault.getRoot === "function" + ? this.app.vault.getRoot() + : null; + const stack = root && Array.isArray(root.children) + ? root.children.slice() + : []; + + while (stack.length) { + const entry = stack.pop(); + totalEntries += 1; + + if (entry instanceof TFolder) { + folders.push(entry); + if (Array.isArray(entry.children)) { + for (let index = entry.children.length - 1; index >= 0; index -= 1) { + stack.push(entry.children[index]); + } + } + } else if (entry instanceof TFile && entry.extension === "md") { + markdownFiles.push(entry); + } + } + + return { folders, markdownFiles, totalEntries }; + } + + addFolder(folder) { + this.ensureFolderPath(normalizeVaultPath(folder.path)); + } + + ensureFolderPath(folderPath) { + const normalizedPath = normalizeVaultPath(folderPath); + if (!normalizedPath) return; + const parts = normalizedPath.split("/").filter(Boolean); + let current = ROOT_ID; + + for (const part of parts) { + const next = current ? `${current}/${part}` : part; + if (this.shouldIgnorePath(next)) return; + if (!this.nodes.has(next)) { + this.addNode({ + id: next, + path: next, + title: basename(next), + type: "folder", + parentId: current, + depth: depthOfPath(next), + noteCount: 0, + linkCount: 0, + backlinkCount: 0, + descendantCount: 0 + }); + } + current = next; + } + } + + addNote(file) { + if (this.shouldIgnorePath(file.path)) return; + + const parentId = normalizeVaultPath(parentPath(file.path)); + this.addNode({ + id: file.path, + path: file.path, + title: file.basename, + type: "note", + parentId, + depth: depthOfPath(file.path), + noteCount: 1, + linkCount: 0, + backlinkCount: 0, + descendantCount: 0, + file + }); + } + + addUnresolvedNode(sourcePath, linkText) { + const id = `unresolved:${sourcePath}:${linkText}`; + if (!this.nodes.has(id)) { + const source = this.nodes.get(sourcePath); + this.addNode({ + id, + path: linkText, + title: linkText, + type: "unresolved", + parentId: source ? source.parentId : ROOT_ID, + depth: source ? source.depth + 1 : 1, + noteCount: 0, + linkCount: 0, + backlinkCount: 0, + descendantCount: 0 + }); + } + return id; + } + + addNode(node) { + if (!node || this.nodes.has(node.id)) return; + this.nodes.set(node.id, node); + } + + identifyFolderRepresentatives() { + const notes = Array.from(this.nodes.values()).filter(node => node.type === "note"); + const notesByParent = new Map(); + + for (const note of notes) { + if (!notesByParent.has(note.parentId)) notesByParent.set(note.parentId, []); + notesByParent.get(note.parentId).push(note); + } + + for (const folder of this.nodes.values()) { + if (folder.type !== "folder" || folder.id === ROOT_ID) continue; + const siblings = notesByParent.get(folder.id) || []; + const folderTitle = comparableTitle(folder.title); + const representative = siblings.find(note => comparableTitle(note.title) === folderTitle); + if (representative) { + folder.representativeFile = representative.path; + representative.isRepresentativeFile = true; + representative.representativeFor = folder.id; + this.folderRepresentatives.set(folder.id, representative.path); + } + } + } + + buildHierarchyEdges() { + this.childrenByParent.clear(); + + for (const node of this.nodes.values()) { + if (node.parentId === null) continue; + if (!this.nodes.has(node.parentId)) continue; + this.hierarchyEdges.push({ + id: `hierarchy:${node.parentId}->${node.id}`, + type: "hierarchy", + source: node.parentId, + target: node.id, + weight: 1 + }); + if (!this.childrenByParent.has(node.parentId)) this.childrenByParent.set(node.parentId, []); + this.childrenByParent.get(node.parentId).push(node.id); + } + + for (const children of this.childrenByParent.values()) { + children.sort((a, b) => compareNodes(this.nodes.get(a), this.nodes.get(b))); + } + } + + buildLinkEdges() { + const resolved = this.app.metadataCache.resolvedLinks || {}; + const unresolved = this.app.metadataCache.unresolvedLinks || {}; + + for (const [sourcePath, targets] of Object.entries(resolved)) { + if (!this.nodes.has(sourcePath) || this.shouldIgnorePath(sourcePath)) continue; + for (const [targetPath, count] of Object.entries(targets || {})) { + if (!this.nodes.has(targetPath) || this.shouldIgnorePath(targetPath)) continue; + this.addLinkEdge(sourcePath, targetPath, Math.max(1, Number(count) || 1), "link"); + } + } + + if (this.settings.includeUnresolvedLinks) { + for (const [sourcePath, targets] of Object.entries(unresolved)) { + if (!this.nodes.has(sourcePath) || this.shouldIgnorePath(sourcePath)) continue; + for (const [linkText, count] of Object.entries(targets || {})) { + const targetId = this.addUnresolvedNode(sourcePath, linkText); + this.addLinkEdge(sourcePath, targetId, Math.max(1, Number(count) || 1), "unresolved-link"); + } + } + } + } + + addLinkEdge(source, target, weight, type) { + if (source === target) return; + const edge = { + id: `${type}:${source}->${target}:${this.linkEdges.length}`, + type, + source, + target, + weight + }; + this.linkEdges.push(edge); + + const sourceNode = this.nodes.get(source); + const targetNode = this.nodes.get(target); + if (sourceNode) sourceNode.linkCount += weight; + if (targetNode) targetNode.backlinkCount += weight; + + if (!this.linkEdgesBySource.has(source)) this.linkEdgesBySource.set(source, []); + if (!this.linkEdgesByTarget.has(target)) this.linkEdgesByTarget.set(target, []); + this.linkEdgesBySource.get(source).push(edge); + this.linkEdgesByTarget.get(target).push(edge); + } + + computeStats() { + const sorted = Array.from(this.nodes.values()).sort((a, b) => b.depth - a.depth); + + for (const node of sorted) { + node.descendantCount = node.type === "note" ? 1 : node.noteCount; + if (!node.parentId || !this.nodes.has(node.parentId)) continue; + const parent = this.nodes.get(node.parentId); + const subtreeNoteCount = node.type === "note" ? 1 : node.noteCount; + parent.descendantCount += subtreeNoteCount; + parent.noteCount += subtreeNoteCount; + parent.linkCount += node.linkCount; + parent.backlinkCount += node.backlinkCount; + } + + this.stats = { + loadedEntries: this.stats.loadedEntries, + scannedMarkdown: this.stats.scannedMarkdown, + folders: Array.from(this.nodes.values()).filter(node => node.type === "folder").length, + notes: Array.from(this.nodes.values()).filter(node => node.type === "note").length, + unresolved: Array.from(this.nodes.values()).filter(node => node.type === "unresolved").length, + hierarchyEdges: this.hierarchyEdges.length, + linkEdges: this.linkEdges.length, + maxDepth: Math.max(...Array.from(this.nodes.values()).map(node => node.depth), 0) + }; + } + + shouldIgnorePath(path) { + if (!path) return false; + const ignores = this.settings.ignoreFolders || []; + return ignores.some(prefix => path === prefix || path.startsWith(`${prefix}/`)); + } + + getActiveNotePath() { + const active = this.app.workspace.getActiveFile(); + if (active && this.nodes.has(active.path)) return active.path; + if (this.nodes.has("Universe, Self-Awareness, and Intelligence.md")) { + return "Universe, Self-Awareness, and Intelligence.md"; + } + const firstNote = Array.from(this.nodes.values()).find(node => node.type === "note"); + return firstNote ? firstNote.id : null; + } + + buildVisibleGraph(state) { + if (!this.ready) return { nodes: [], hierarchyEdges: [], linkEdges: [], rootId: ROOT_ID }; + + if (state.mode === "focus") return this.buildFocusGraph(state); + return this.buildAtlasGraph(state); + } + + buildAtlasGraph(state) { + const rootId = this.nodes.has(state.rootPath) ? state.rootPath : ROOT_ID; + const rootDepth = this.nodes.get(rootId).depth; + const maxDepth = clampNumber( + state.atlasDepth === undefined || state.atlasDepth === null ? this.settings.atlasDepth : state.atlasDepth, + 1, + MAX_ATLAS_DEPTH, + this.settings.atlasDepth + ); + const visible = new Set(); + const query = normalizedQuery(state.search); + + const stack = [rootId]; + while (stack.length) { + const id = stack.pop(); + const node = this.nodes.get(id); + if (!node) continue; + const relDepth = Math.max(0, node.depth - rootDepth); + const isWithinDepth = relDepth <= maxDepth; + const matchesSearch = query && nodeMatches(node, query); + + if (isWithinDepth || matchesSearch) { + visible.add(id); + if (matchesSearch) this.addAncestors(id, visible, rootId); + } + + if (isWithinDepth || matchesSearch) { + const children = this.childrenByParent.get(id) || []; + for (let i = children.length - 1; i >= 0; i -= 1) stack.push(children[i]); + } + } + + if (query) { + for (const node of this.nodes.values()) { + if (nodeMatches(node, query)) { + visible.add(node.id); + this.addAncestors(node.id, visible, rootId); + } + } + } + + this.addDirectFilesForVisibleFolders(visible); + return this.materializeVisibleGraph(visible, rootId, state); + } + + buildFocusGraph(state) { + const activePath = state.focusPath && this.nodes.has(state.focusPath) + ? state.focusPath + : this.getActiveNotePath(); + const rootId = activePath || ROOT_ID; + const visible = new Set([ROOT_ID]); + const siblingLimit = Math.max(10, Number(this.settings.focusSiblingLimit) || 80); + + if (activePath) { + visible.add(activePath); + this.addAncestors(activePath, visible, ROOT_ID); + + const active = this.nodes.get(activePath); + if (active && active.parentId) { + const siblings = this.childrenByParent.get(active.parentId) || []; + for (const siblingId of siblings.slice(0, siblingLimit)) visible.add(siblingId); + } + + const connected = [ + ...(this.linkEdgesBySource.get(activePath) || []), + ...(this.linkEdgesByTarget.get(activePath) || []) + ]; + connected + .sort((a, b) => b.weight - a.weight) + .slice(0, Math.max(30, siblingLimit)) + .forEach(edge => { + visible.add(edge.source); + visible.add(edge.target); + this.addAncestors(edge.source, visible, ROOT_ID); + this.addAncestors(edge.target, visible, ROOT_ID); + }); + } + + const query = normalizedQuery(state.search); + if (query) { + for (const node of this.nodes.values()) { + if (nodeMatches(node, query)) { + visible.add(node.id); + this.addAncestors(node.id, visible, ROOT_ID); + } + } + } + + this.addDirectFilesForVisibleFolders(visible); + return this.materializeVisibleGraph(visible, ROOT_ID, Object.assign({}, state, { focusPath: activePath, rootPath: ROOT_ID }), rootId); + } + + addDirectFilesForVisibleFolders(visible) { + for (const id of Array.from(visible)) { + const node = this.nodes.get(id); + if (!node || node.type !== "folder") continue; + for (const childId of this.childrenByParent.get(id) || []) { + const child = this.nodes.get(childId); + if (child && (child.type === "note" || child.type === "unresolved")) { + visible.add(childId); + } + } + } + } + + addAncestors(id, visible, stopId) { + let current = this.nodes.get(id); + while (current && current.parentId !== null) { + visible.add(current.id); + if (current.id === stopId) break; + current = this.nodes.get(current.parentId); + } + visible.add(stopId || ROOT_ID); + } + + materializeVisibleGraph(visible, rootId, state, focusId) { + let nodes = Array.from(visible) + .map(id => this.nodes.get(id)) + .filter(Boolean) + .sort(compareNodes); + + const budget = this.applyNodeBudget(nodes, rootId, state, focusId); + nodes = budget.nodes; + nodes = this.foldRepresentativeNodes(nodes); + + const nodeSet = new Set(nodes.map(node => node.id)); + const hierarchyEdges = this.hierarchyEdges.filter(edge => nodeSet.has(edge.source) && nodeSet.has(edge.target)); + const linkBundle = this.aggregateVisibleLinkEdges(nodeSet, state, rootId); + nodes = nodes.concat(linkBundle.externalNodes); + + const nodesById = new Map(nodes.map(node => [node.id, node])); + + return { + nodes, + nodesById, + hierarchyEdges: hierarchyEdges.concat(linkBundle.externalHierarchyEdges), + linkEdges: linkBundle.linkEdges, + hoverLinkEdges: linkBundle.hoverLinkEdges, + rootId, + focusId: this.visualNodeId(focusId) || null, + hiddenNodeCount: budget.hiddenNodeCount, + externalNodeCount: linkBundle.externalNodes.length, + externalFileCount: linkBundle.externalFileCount || 0, + externalGroupCount: linkBundle.externalGroupCount || 0 + }; + } + + foldRepresentativeNodes(nodes) { + const visibleIds = new Set(nodes.map(node => node.id)); + return nodes.filter(node => { + if (!node || !node.isRepresentativeFile) return true; + return !visibleIds.has(node.representativeFor); + }); + } + + visualNodeId(id) { + const node = this.nodes.get(id); + if (node && node.isRepresentativeFile && node.representativeFor && this.nodes.has(node.representativeFor)) { + return node.representativeFor; + } + return id; + } + + aggregateVisibleLinkEdges(visible, state, rootId) { + const needsHoverLinks = hoverHighlightsNoteLinks(state.hoverHighlightMode) || Boolean(state.pinNeedsHoverLinks); + const hiddenLegend = hiddenLegendSetFromState(state); + const wantsInternalLinkOverlay = !hiddenLegend.has("link"); + const wantsUnresolvedLinkOverlay = wantsInternalLinkOverlay && !hiddenLegend.has("dashed-link"); + const wantsExternalLinkOverlay = !hiddenLegend.has("outside-link"); + const wantsExternalNodes = !hiddenLegend.has("outside") || !hiddenLegend.has("outside-file"); + const showExternalLinks = state.showExternalLinks !== false && rootId !== ROOT_ID && (wantsExternalNodes || wantsExternalLinkOverlay); + if (!wantsInternalLinkOverlay && !wantsUnresolvedLinkOverlay && !needsHoverLinks && !showExternalLinks) return { + linkEdges: [], + hoverLinkEdges: [], + externalNodes: [], + externalHierarchyEdges: [], + externalFileCount: 0, + externalGroupCount: 0 + }; + const aggregate = new Map(); + const externalNodes = new Map(); + const externalHierarchyEdges = []; + const externalDetailMode = state.externalDetailMode || this.settings.externalDetailMode || DEFAULT_SETTINGS.externalDetailMode; + const externalLimit = clampNumber( + state.externalLinkAnchorLimit === undefined || state.externalLinkAnchorLimit === null ? this.settings.externalLinkAnchorLimit : state.externalLinkAnchorLimit, + 0, + MAX_EXTERNAL_LINK_ANCHOR_LIMIT, + DEFAULT_SETTINGS.externalLinkAnchorLimit + ); + const externalContext = { fileCount: 0, groupIds: new Set(), overflowId: null }; + + for (const edge of this.linkEdges) { + let source = this.nearestVisibleAncestor(edge.source, visible, rootId); + let target = this.nearestVisibleAncestor(edge.target, visible, rootId); + let externalCount = 0; + + if (showExternalLinks && rootId !== ROOT_ID) { + if (!source && target) { + source = this.externalEndpointFor( + edge.source, + rootId, + externalNodes, + externalHierarchyEdges, + externalLimit, + externalContext, + this.shouldUseExactExternalEndpoint(edge, state, externalDetailMode, target) + ); + if (source) externalCount = 1; + } + if (source && !target) { + target = this.externalEndpointFor( + edge.target, + rootId, + externalNodes, + externalHierarchyEdges, + externalLimit, + externalContext, + this.shouldUseExactExternalEndpoint(edge, state, externalDetailMode, source) + ); + if (target) externalCount = 1; + } + } + + if (!source || !target || source === target) continue; + const key = `${source}->${target}`; + const existing = aggregate.get(key) || { + id: `visible-link:${key}`, + type: "visible-link", + source, + target, + weight: 0, + rawCount: 0, + unresolvedCount: 0, + externalCount: 0 + }; + existing.weight += edge.weight; + existing.rawCount += 1; + if (edge.type === "unresolved-link") existing.unresolvedCount += 1; + existing.externalCount += externalCount; + aggregate.set(key, existing); + } + + const allLinkEdges = Array.from(aggregate.values()) + .sort((a, b) => linkRenderScore(b) - linkRenderScore(a)); + const linkLimit = clampNumber( + state.linkLimit === undefined || state.linkLimit === null ? this.settings.linkLimit : state.linkLimit, + 0, + MAX_LINK_LIMIT, + DEFAULT_SETTINGS.linkLimit + ); + const showAllLinks = Boolean(state.showCompleteRoot); + const completeLinkLimit = showAllLinks ? MAX_LINK_LIMIT : linkLimit; + const linkEdges = []; + const visibleLinkIds = new Set(); + const allowsVisibleLink = edge => { + if (!edge) return false; + if (edge.externalCount) return wantsExternalLinkOverlay && (!edge.unresolvedCount || wantsUnresolvedLinkOverlay); + if (edge.unresolvedCount) return wantsUnresolvedLinkOverlay; + return wantsInternalLinkOverlay; + }; + const pushVisibleLink = edge => { + if (!allowsVisibleLink(edge) || visibleLinkIds.has(edge.id)) return; + visibleLinkIds.add(edge.id); + linkEdges.push(edge); + }; + const visibleLinkLimit = showAllLinks + ? MAX_LINK_LIMIT + : clampNumber( + Math.max(completeLinkLimit, showExternalLinks ? Math.min(MAX_LINK_LIMIT, Math.max(externalLimit, 120)) : 0), + 0, + MAX_LINK_LIMIT, + DEFAULT_SETTINGS.linkLimit + ); + for (const edge of allLinkEdges) { + if (linkEdges.length >= visibleLinkLimit) break; + pushVisibleLink(edge); + } + const hoverLinkEdges = showAllLinks ? linkEdges : allLinkEdges.filter(allowsVisibleLink); + + const usedExternalIds = new Set(); + const externalNodeEdges = showExternalLinks + ? allLinkEdges.filter(edge => edge.externalCount) + : needsHoverLinks + ? hoverLinkEdges + : []; + for (const edge of externalNodeEdges) { + if (externalNodes.has(edge.source)) usedExternalIds.add(edge.source); + if (externalNodes.has(edge.target)) usedExternalIds.add(edge.target); + } + + const collectedExternalNodes = this.collectUsedExternalNodes(usedExternalIds, externalNodes); + + return { + linkEdges, + hoverLinkEdges, + externalNodes: collectedExternalNodes, + externalHierarchyEdges: externalHierarchyEdges.filter(edge => usedExternalIds.has(edge.target)), + externalFileCount: collectedExternalNodes.filter(node => node.externalProxy).length, + externalGroupCount: collectedExternalNodes.filter(node => node.type === "external" && !node.externalProxy).length + }; + } + + nearestVisibleAncestor(id, visible, rootId) { + let current = this.nodes.get(id); + while (current) { + if (visible.has(current.id)) return current.id; + if (current.id === rootId) return rootId; + current = current.parentId === null ? null : this.nodes.get(current.parentId); + } + return visible.has(ROOT_ID) ? ROOT_ID : null; + } + + collectUsedExternalNodes(usedExternalIds, externalNodes) { + const collected = new Map(); + for (const id of usedExternalIds) { + const node = externalNodes.get(id); + if (!node) continue; + if (node.externalParentId && externalNodes.has(node.externalParentId)) { + collected.set(node.externalParentId, externalNodes.get(node.externalParentId)); + } + collected.set(id, node); + } + return Array.from(collected.values()); + } + + shouldUseExactExternalEndpoint(edge, state, detailMode, visibleEndpoint) { + if (detailMode === "exact") return true; + if (detailMode === "grouped") return false; + + const selectedNodeId = state.selectedNodeId; + if (selectedNodeId !== null && selectedNodeId !== undefined && ( + edge.source === selectedNodeId + || edge.target === selectedNodeId + || visibleEndpoint === selectedNodeId + )) { + return true; + } + + const selectedLink = state.selectedLink; + return Boolean(selectedLink && ( + selectedLink.source === visibleEndpoint + || selectedLink.target === visibleEndpoint + || selectedLink.source === edge.source + || selectedLink.target === edge.target + )); + } + + externalEndpointFor(nodeId, rootId, externalNodes, externalHierarchyEdges, externalLimit, context, exactFiles) { + const node = this.nodes.get(nodeId); + if (!node) return null; + const anchorPath = externalAnchorPath(node, rootId); + if (!anchorPath) return null; + + const groupId = this.externalGroupFor(anchorPath, rootId, externalNodes, context); + if (!groupId) return null; + if (!exactFiles) return groupId; + + if (node.type !== "note" && node.type !== "unresolved") return groupId; + + if (externalNodes.has(node.id)) return node.id; + if (context.fileCount >= externalLimit) return this.externalOverflowFor(rootId, groupId, externalNodes, externalHierarchyEdges, context); + + externalNodes.set(node.id, Object.assign({}, node, { + externalProxy: true, + externalParentId: groupId, + externalAnchorPath: anchorPath + })); + context.fileCount += 1; + externalHierarchyEdges.push({ + id: `external-hierarchy:${groupId}->${node.id}`, + type: "external-hierarchy", + source: groupId, + target: node.id, + weight: 1 + }); + + return node.id; + } + + externalGroupFor(anchorPath, rootId, externalNodes, context) { + const id = `external-group:${rootId}:${anchorPath}`; + if (!externalNodes.has(id)) { + const anchorNode = this.nodes.get(anchorPath); + externalNodes.set(id, { + id, + path: anchorPath, + title: `Outside: ${anchorNode ? anchorNode.title : basename(anchorPath)}`, + type: "external", + parentId: null, + depth: (this.nodes.get(rootId)?.depth || 0) + 1, + noteCount: anchorNode ? (anchorNode.noteCount || anchorNode.descendantCount || 0) : 0, + linkCount: 0, + backlinkCount: 0, + descendantCount: anchorNode ? (anchorNode.descendantCount || anchorNode.noteCount || 0) : 0, + externalAnchorPath: anchorPath + }); + context.groupIds.add(id); + } + return id; + } + + externalOverflowFor(rootId, groupId, externalNodes, externalHierarchyEdges, context) { + if (!context.overflowId) { + context.overflowId = `external-overflow:${rootId}`; + externalNodes.set(context.overflowId, { + id: context.overflowId, + path: "outside current root", + title: "More outside files", + type: "external", + parentId: null, + depth: (this.nodes.get(rootId)?.depth || 0) + 1, + noteCount: 0, + linkCount: 0, + backlinkCount: 0, + descendantCount: 0, + externalProxy: true, + externalParentId: groupId, + externalAnchorPath: null + }); + externalHierarchyEdges.push({ + id: `external-hierarchy:${groupId}->${context.overflowId}`, + type: "external-hierarchy", + source: groupId, + target: context.overflowId, + weight: 1 + }); + } + return context.overflowId; + } + + applyNodeBudget(nodes, rootId, state, focusId) { + if (state.showCompleteRoot && nodes.length <= MAX_RENDER_NODE_LIMIT) { + return { nodes, hiddenNodeCount: 0 }; + } + const limit = clampNumber( + state.nodeLimit === undefined || state.nodeLimit === null ? this.settings.renderNodeLimit : state.nodeLimit, + 200, + MAX_RENDER_NODE_LIMIT, + DEFAULT_SETTINGS.renderNodeLimit + ); + if (!limit || nodes.length <= limit) { + return { nodes, hiddenNodeCount: 0 }; + } + + const nodeById = new Map(nodes.map(node => [node.id, node])); + const childrenByParent = buildBudgetChildrenByParent(nodes); + const query = normalizedQuery(state.search); + const keep = new Set([rootId ?? ROOT_ID, focusId].filter(id => id !== null && id !== undefined)); + const candidates = nodes + .slice() + .sort((a, b) => nodeRenderScore(b, rootId, focusId, query) - nodeRenderScore(a, rootId, focusId, query)); + const fileCandidates = candidates.filter(node => node.type === "note" || node.type === "unresolved"); + const targetFileCount = Math.min( + fileCandidates.length, + Math.max(24, Math.floor(limit * (state.showCompleteRoot ? 0.46 : 0.38))) + ); + let keptFileCount = 0; + this.addDirectFileChildrenToSet( + rootId ?? ROOT_ID, + keep, + nodeById, + childrenByParent, + limit, + state.showCompleteRoot ? 128 : 64 + ); + + for (const node of fileCandidates) { + if (keep.size >= limit || keptFileCount >= targetFileCount) break; + const before = keep.size; + this.addNodeWithAncestorsToSet(node.id, keep, nodeById, limit); + if (keep.has(node.id) && keep.size > before) keptFileCount += 1; + } + + for (const node of candidates) { + if (keep.size >= limit) break; + this.addNodeWithAncestorsToSet(node.id, keep, nodeById, limit); + if (node.type === "folder") { + this.addDirectFileChildrenToSet( + node.id, + keep, + nodeById, + childrenByParent, + limit, + state.showCompleteRoot ? 18 : 8 + ); + } + } + + const keptNodes = nodes.filter(node => keep.has(node.id)); + return { + nodes: keptNodes, + hiddenNodeCount: Math.max(0, nodes.length - keptNodes.length) + }; + } + + addNodeWithAncestorsToSet(id, keep, nodeById, limit) { + let current = nodeById.get(id); + const chain = []; + while (current && !keep.has(current.id)) { + chain.push(current.id); + current = current.parentId === null ? null : nodeById.get(current.parentId); + } + + for (let i = chain.length - 1; i >= 0; i -= 1) { + if (keep.size >= limit) break; + keep.add(chain[i]); + } + } + + addDirectFileChildrenToSet(parentId, keep, nodeById, childrenByParent, limit, perParentLimit) { + const children = childrenByParent.get(parentId) || []; + if (!children.length || keep.size >= limit) return; + + let added = 0; + for (const childId of children) { + if (keep.size >= limit || added >= perParentLimit) break; + if (keep.has(childId)) continue; + const child = nodeById.get(childId); + if (!child || (child.type !== "note" && child.type !== "unresolved")) continue; + keep.add(childId); + added += 1; + } + } +} + +class MiniWorldMapView extends ItemView { + constructor(leaf, plugin) { + super(leaf); + this.plugin = plugin; + const hoverHighlightMode = normalizeHoverHighlightMode(plugin.settings.hoverHighlightMode); + this.state = { + mode: "atlas", + search: "", + rootPath: ROOT_ID, + atlasDepth: plugin.settings.atlasDepth, + linkLimit: plugin.settings.linkLimit, + nodeLimit: plugin.settings.renderNodeLimit, + externalLinkAnchorLimit: plugin.settings.externalLinkAnchorLimit, + autoDetail: plugin.settings.adaptiveDetail, + showLinkOverlay: plugin.settings.showLinkOverlay, + enableLinkHover: hoverHighlightsNoteLinks(hoverHighlightMode), + hoverHighlightMode, + showExternalLinks: plugin.settings.showExternalLinks, + externalDetailMode: plugin.settings.externalDetailMode || DEFAULT_SETTINGS.externalDetailMode, + colorScheme: plugin.settings.colorScheme || DEFAULT_SETTINGS.colorScheme, + labelVisibility: normalizeLabelVisibility(plugin.settings.labelVisibility), + focusPath: null, + showCompleteRoot: false, + zoom: 1, + columnSpacing: DEFAULT_RING_SPACING, + rowSpacing: DEFAULT_NODE_SPACING, + swirlStrength: clampNumber(plugin.settings.swirlStrength, 0, MAX_SWIRL_STRENGTH, DEFAULT_SWIRL_STRENGTH), + hiddenLegendItems: Array.isArray(plugin.settings.hiddenLegendItems) ? plugin.settings.hiddenLegendItems.slice() : [], + sidePanelWidth: 360, + sidePage: "inspect", + fullscreen: false + }; + this.preCompleteState = null; + this.positions = new Map(); + this.selectedNodeId = null; + this.selectedLink = null; + this.pinnedPaths = []; + this.pinGroups = []; + this.selectedPinIds = new Set(); + this.nextPinId = 1; + this.nextPinGroupId = 1; + this.pinGroupName = ""; + this.lastLayout = null; + this.renderTimer = null; + this.activeHighlightElements = new Set(); + this.nodeElementsById = new Map(); + this.edgeElementsByNode = new Map(); + this.edgeElementsById = new Map(); + this.canvas = null; + this.pixiRenderer = null; + this.pixiUnavailable = false; + this.canvasData = null; + this.canvasPalette = null; + this.canvasPanX = 0; + this.canvasPanY = 0; + this.canvasDpr = 1; + this.canvasViewportSize = { width: 0, height: 0 }; + this.dragState = null; + this.hoverNodeId = null; + this.hoverLink = null; + this.graphViewSignature = null; + this.viewInitialized = false; + this.lastCanvasBundle = null; + this.canvasVisualNodes = new Map(); + this.canvasVisualEdges = new Map(); + this.canvasVisualInitialized = false; + this.canvasAnimationFrame = null; + this.canvasSettleTimer = null; + this.canvasPanAnimationFrame = null; + this.canvasPanVelocityX = 0; + this.canvasPanVelocityY = 0; + this.canvasZoomAnimationFrame = null; + this.canvasTargetZoom = null; + this.canvasZoomAnchorPoint = null; + this.canvasSpringAnimationFrame = null; + this.canvasSpringState = null; + this.canvasSwirlAnimationFrame = null; + this.canvasSwirlStartedAt = 0; + this.canvasSwirlLastFrameAt = 0; + this.nextCanvasDrawMode = "full"; + this.canvasInteractionUntil = 0; + this.lastCanvasFrameAt = 0; + this.adaptiveInitialized = false; + this.lastAutoTuneAt = 0; + this.autoTuneCount = 0; + this.lastRenderMs = 0; + this.pendingRenderOptions = null; + this.viewportStateByKey = new Map(); + this.currentViewportStateKey = null; + } + + getViewType() { + return VIEW_TYPE_MINI_WORLD_MAP; + } + + getDisplayText() { + return "Mini World Map"; + } + + getIcon() { + return "network"; + } + + async onOpen() { + this.contentEl.empty(); + this.contentEl.addClass("mini-world-map-view"); + this.syncColorSchemeClass(); + this.renderShell(); + if (!this.plugin.index.ready) await this.plugin.rebuildIndex("open"); + this.refresh(); + } + + async onClose() { + if (this.resizeCleanup) this.resizeCleanup(); + if (this.renderTimer) window.clearTimeout(this.renderTimer); + if (this.canvasAnimationFrame) window.cancelAnimationFrame(this.canvasAnimationFrame); + if (this.canvasSettleTimer) window.clearTimeout(this.canvasSettleTimer); + this.cancelCanvasPanMomentum(); + this.cancelCanvasZoomAnimation(); + this.cancelCanvasSpringBack(); + this.cancelCanvasSwirlAnimation(); + this.destroyPixiRenderer(); + this.contentEl.empty(); + } + + refresh(options = {}) { + if (!this.containerEl || !this.graphHost) return; + this.render(options); + } + + renderShell() { + this.contentEl.empty(); + + this.metaEl = this.contentEl.createDiv({ cls: "mwm-meta" }); + + const body = this.contentEl.createDiv({ cls: "mwm-body" }); + this.bodyEl = body; + this.bodyEl.style.setProperty("--mwm-side-width", `${this.state.sidePanelWidth}px`); + this.graphHost = body.createDiv({ cls: "mwm-graph-host" }); + this.graphHost.addEventListener("wheel", evt => { + evt.preventDefault(); + this.zoomAtClientPoint(evt.clientX, evt.clientY, evt.deltaY, evt.deltaMode); + }, { passive: false }); + this.splitter = body.createDiv({ + cls: "mwm-splitter", + attr: { role: "separator", title: "Drag to resize right panel" } + }); + this.installPanelResize(); + this.sidePanel = body.createDiv({ cls: "mwm-side-panel" }); + + if (typeof ResizeObserver !== "undefined") { + const observer = new ResizeObserver(() => this.requestCanvasDraw("full")); + observer.observe(this.graphHost); + this.resizeCleanup = () => observer.disconnect(); + } + } + + createIconButton(parent, icon, title, onClick, extraClass) { + const button = parent.createEl("button", { + cls: extraClass ? `mwm-icon-button ${extraClass}` : "mwm-icon-button", + attr: { type: "button", "aria-label": title, title } + }); + setIcon(button, icon); + button.addEventListener("click", onClick); + return button; + } + + sidePanelPages() { + return [ + ["inspect", "Inspect", "info"], + ["pins", "Pins", "pin"], + ["view", "View", "navigation"], + ["controls", "Controls", "sliders-horizontal"], + ["defaults", "Defaults", "settings"] + ]; + } + + clearControlRefs() { + this.searchInput = null; + this.depthInput = null; + this.linkInput = null; + this.nodeInput = null; + this.autoToggle = null; + this.linkHoverSelect = null; + this.externalToggle = null; + this.externalModeSelect = null; + this.externalLimitInput = null; + this.zoomInput = null; + this.zoomLabel = null; + this.columnInput = null; + this.rowInput = null; + this.labelVisibilitySelect = null; + this.swirlInput = null; + this.swirlLabel = null; + this.pinGroupInput = null; + this.panelControlRefs = new Map(); + } + + capturePanelFocus() { + const doc = this.contentEl?.ownerDocument; + const active = doc?.activeElement; + if (!active || !this.sidePanel || !this.sidePanel.contains(active)) return null; + const key = active.getAttribute("data-mwm-control"); + if (!key) return null; + return { + key, + start: typeof active.selectionStart === "number" ? active.selectionStart : null, + end: typeof active.selectionEnd === "number" ? active.selectionEnd : null + }; + } + + restorePanelFocus(focusState) { + if (!focusState || !this.panelControlRefs) return; + const element = this.panelControlRefs.get(focusState.key); + if (!element) return; + element.focus({ preventScroll: true }); + if (focusState.start !== null && typeof element.setSelectionRange === "function") { + element.setSelectionRange(focusState.start, focusState.end ?? focusState.start); + } + } + + registerPanelControl(key, element) { + if (!element) return element; + element.setAttribute("data-mwm-control", key); + if (!this.panelControlRefs) this.panelControlRefs = new Map(); + this.panelControlRefs.set(key, element); + return element; + } + + getSidePanelTarget() { + return this.sidePanelContent || this.sidePanel; + } + + createPanelPageTitle(parent, title, desc) { + parent.createDiv({ cls: "mwm-panel-page-title", text: title }); + if (desc) parent.createDiv({ cls: "mwm-panel-page-desc", text: desc }); + } + + createPanelSection(parent, title) { + const section = parent.createDiv({ cls: "mwm-panel-section" }); + if (title) section.createDiv({ cls: "mwm-side-heading", text: title }); + return section; + } + + createPanelAction(parent, icon, label, onClick, active = false) { + const button = parent.createEl("button", { + cls: active ? "mwm-panel-action is-active" : "mwm-panel-action", + attr: { type: "button", title: label } + }); + setIcon(button, icon); + button.createSpan({ text: label }); + button.addEventListener("click", onClick); + return button; + } + + createPanelText(parent, key, label, value, placeholder, onInput) { + const field = parent.createEl("label", { cls: "mwm-panel-field" }); + field.createSpan({ cls: "mwm-panel-label", text: label }); + const input = field.createEl("input", { + attr: { type: "search", placeholder, value } + }); + this.registerPanelControl(key, input); + input.addEventListener("input", () => onInput(input.value, input)); + return input; + } + + createPanelNumber(parent, key, label, value, attr, onChange) { + const field = parent.createEl("label", { cls: "mwm-panel-field" }); + field.createSpan({ cls: "mwm-panel-label", text: label }); + const input = field.createEl("input", { + attr: Object.assign({ type: "number", value: String(value) }, attr || {}) + }); + this.registerPanelControl(key, input); + let commitTimer = null; + const commit = () => { + if (commitTimer) { + window.clearTimeout(commitTimer); + commitTimer = null; + } + const raw = String(input.value || "").trim(); + if (!raw || raw === "-" || raw === ".") return; + onChange(input.value, input); + }; + input.addEventListener("input", () => { + if (commitTimer) window.clearTimeout(commitTimer); + commitTimer = window.setTimeout(commit, 520); + }); + input.addEventListener("change", commit); + input.addEventListener("keydown", evt => { + if (evt.key === "Enter") commit(); + }); + return input; + } + + createPanelRange(parent, key, label, value, attr, onInput) { + const field = parent.createEl("label", { cls: "mwm-panel-field mwm-panel-range-field" }); + const header = field.createDiv({ cls: "mwm-panel-field-header" }); + header.createSpan({ cls: "mwm-panel-label", text: label }); + const valueEl = header.createSpan({ cls: "mwm-value", text: `${Math.round(this.state.zoom * 100)}%` }); + const input = field.createEl("input", { + attr: Object.assign({ type: "range", value: String(value) }, attr || {}) + }); + this.registerPanelControl(key, input); + input.addEventListener("input", () => onInput(input.value, input, valueEl)); + return { input, valueEl }; + } + + createPanelToggle(parent, key, label, value, onChange) { + const field = parent.createEl("label", { cls: "mwm-panel-toggle" }); + const input = field.createEl("input", { attr: { type: "checkbox" } }); + input.checked = Boolean(value); + this.registerPanelControl(key, input); + field.createSpan({ text: label }); + input.addEventListener("change", () => onChange(input.checked, input)); + return input; + } + + createPanelSelect(parent, key, label, value, options, onChange) { + const field = parent.createEl("label", { cls: "mwm-panel-field" }); + field.createSpan({ cls: "mwm-panel-label", text: label }); + const select = field.createEl("select", { cls: "mwm-select" }); + for (const [optionValue, optionLabel] of options) { + select.createEl("option", { attr: { value: optionValue }, text: optionLabel }); + } + select.value = value; + this.registerPanelControl(key, select); + select.addEventListener("change", () => onChange(select.value, select)); + return select; + } + + createPanelTextArea(parent, key, label, value, onChange) { + const field = parent.createEl("label", { cls: "mwm-panel-field" }); + field.createSpan({ cls: "mwm-panel-label", text: label }); + const textArea = field.createEl("textarea"); + textArea.value = value; + this.registerPanelControl(key, textArea); + textArea.addEventListener("change", () => onChange(textArea.value, textArea)); + return textArea; + } + + needsCanvasHoverLinks() { + return hoverHighlightsNoteLinks(this.state.hoverHighlightMode) || this.pinnedPathsNeedHoverLinks(); + } + + pinnedPathsNeedHoverLinks() { + return this.pinnedPaths.some(pin => + pin.kind === "link" || (pin.kind === "node" && hoverHighlightsNoteLinks(pin.mode)) + ); + } + + currentHoverPinCandidate(index, graph) { + if (!index || !graph) return null; + if (this.hoverNodeId !== null && this.hoverNodeId !== undefined) { + const node = graphNode(index, graph, this.hoverNodeId); + return node ? this.createNodePinCandidate(node, this.state.hoverHighlightMode) : null; + } + if (this.hoverLink) return this.createLinkPinCandidate(index, graph, this.hoverLink); + return null; + } + + currentSelectedPinCandidate(index, graph) { + if (!index || !graph) return null; + if (this.selectedLink) return this.createLinkPinCandidate(index, graph, this.selectedLink); + if (this.selectedNodeId !== null && this.selectedNodeId !== undefined) { + const node = graphNode(index, graph, this.selectedNodeId); + return node ? this.createNodePinCandidate(node, this.state.hoverHighlightMode) : null; + } + return null; + } + + createNodePinCandidate(node, mode) { + return { + kind: "node", + nodeId: node.id, + mode: normalizeHoverHighlightMode(mode), + title: node.title || node.id || ROOT_TITLE, + path: node.path || "/", + nodeType: node.type + }; + } + + createLinkPinCandidate(index, graph, edge) { + const source = graphNode(index, graph, edge.source); + const target = graphNode(index, graph, edge.target); + const sourceTitle = source ? source.title : edge.source; + const targetTitle = target ? target.title : edge.target; + const sourcePath = source ? source.path : edge.source; + const targetPath = target ? target.path : edge.target; + return { + kind: "link", + edgeId: edge.id || `${edge.source}->${edge.target}`, + source: edge.source, + target: edge.target, + title: `${sourceTitle} -> ${targetTitle}`, + path: `${sourcePath || "/"} -> ${targetPath || "/"}`, + mode: "note-links" + }; + } + + pinCandidateKey(candidate) { + if (!candidate) return ""; + if (candidate.kind === "node") return `node:${candidate.nodeId}:${normalizeHoverHighlightMode(candidate.mode)}`; + return `link:${candidate.edgeId || `${candidate.source}->${candidate.target}`}`; + } + + addPinnedPath(candidate) { + const key = this.pinCandidateKey(candidate); + if (!key) return null; + const existing = this.pinnedPaths.find(pin => pin.key === key); + if (existing) { + this.selectedPinIds.add(existing.id); + new Notice("That path is already pinned."); + return existing; + } + + const pin = Object.assign({}, candidate, { + id: `pin-${this.nextPinId++}`, + key, + groupId: null, + createdAt: Date.now() + }); + this.pinnedPaths.push(pin); + this.selectedPinIds.add(pin.id); + return pin; + } + + pinCurrentHoverPath() { + const { index, graph } = this.lastCanvasBundle || {}; + if (!index || !graph) return; + const previousNeedsHoverLinks = this.needsCanvasHoverLinks(); + const freshCandidate = this.currentHoverPinCandidate(index, graph); + const selectedCandidate = this.currentSelectedPinCandidate(index, graph); + const pin = this.addPinnedPath(selectedCandidate || freshCandidate); + if (!pin) { + new Notice("Select or hover a path before pinning."); + return; + } + if (previousNeedsHoverLinks !== this.needsCanvasHoverLinks()) this.renderMapOnly(); + else this.applyPersistentHighlight(); + if (!this.state.fullscreen && this.state.sidePage === "pins" && this.lastCanvasBundle) { + this.renderSidePanel(this.lastCanvasBundle.index, this.lastCanvasBundle.graph); + } + } + + removePinnedPath(pinId) { + const previousNeedsHoverLinks = this.needsCanvasHoverLinks(); + this.pinnedPaths = this.pinnedPaths.filter(pin => pin.id !== pinId); + this.selectedPinIds.delete(pinId); + this.dropEmptyPinGroups(); + if (previousNeedsHoverLinks !== this.needsCanvasHoverLinks()) this.renderMapOnly(); + else this.requestCanvasDraw("full"); + if (!this.state.fullscreen && this.state.sidePage === "pins" && this.lastCanvasBundle) { + this.renderSidePanel(this.lastCanvasBundle.index, this.lastCanvasBundle.graph); + } + } + + clearPinnedPaths() { + const previousNeedsHoverLinks = this.needsCanvasHoverLinks(); + this.pinnedPaths = []; + this.pinGroups = []; + this.selectedPinIds.clear(); + if (previousNeedsHoverLinks !== this.needsCanvasHoverLinks()) this.renderMapOnly(); + else this.requestCanvasDraw("full"); + if (!this.state.fullscreen && this.state.sidePage === "pins" && this.lastCanvasBundle) { + this.renderSidePanel(this.lastCanvasBundle.index, this.lastCanvasBundle.graph); + } + } + + groupSelectedPinnedPaths() { + const selectedPins = this.pinnedPaths.filter(pin => this.selectedPinIds.has(pin.id)); + if (!selectedPins.length) { + new Notice("Select pinned paths to group."); + return; + } + + const groupId = `pin-group-${this.nextPinGroupId++}`; + const name = this.pinGroupName.trim() || `Group ${this.pinGroups.length + 1}`; + this.pinGroups.push({ id: groupId, name }); + for (const pin of selectedPins) pin.groupId = groupId; + this.dropEmptyPinGroups(); + this.pinGroupName = ""; + this.selectedPinIds.clear(); + + if (!this.state.fullscreen && this.lastCanvasBundle) { + this.renderSidePanel(this.lastCanvasBundle.index, this.lastCanvasBundle.graph); + } + } + + removePinGroup(groupId) { + this.pinGroups = this.pinGroups.filter(group => group.id !== groupId); + for (const pin of this.pinnedPaths) { + if (pin.groupId === groupId) pin.groupId = null; + } + if (!this.state.fullscreen && this.lastCanvasBundle) { + this.renderSidePanel(this.lastCanvasBundle.index, this.lastCanvasBundle.graph); + } + } + + ungroupPinnedPath(pinId) { + const pin = this.pinnedPaths.find(item => item.id === pinId); + if (!pin) return; + pin.groupId = null; + this.dropEmptyPinGroups(); + if (!this.state.fullscreen && this.lastCanvasBundle) { + this.renderSidePanel(this.lastCanvasBundle.index, this.lastCanvasBundle.graph); + } + } + + dropEmptyPinGroups() { + const usedGroups = new Set(this.pinnedPaths.map(pin => pin.groupId).filter(Boolean)); + this.pinGroups = this.pinGroups.filter(group => usedGroups.has(group.id)); + } + + findGraphEdgeForPin(pin, graph) { + if (!pin || pin.kind !== "link" || !graph) return null; + const edges = [ + ...(graph.linkEdges || []), + ...(graph.hoverLinkEdges || []) + ]; + return edges.find(edge => + (pin.edgeId && edge.id === pin.edgeId) + || (edge.source === pin.source && edge.target === pin.target) + ) || null; + } + + findCanvasEdgeForPin(pin) { + if (!pin || pin.kind !== "link" || !this.canvasData) return null; + if (pin.edgeId && this.canvasData.edgesById.has(pin.edgeId)) { + return this.canvasData.edgesById.get(pin.edgeId); + } + const collections = [ + ...(this.canvasData.links || []), + ...(this.canvasData.hoverLinks || []), + ...(this.canvasData.hierarchy || []) + ]; + return collections.find(item => + item && item.source === pin.source && item.target === pin.target + ) || null; + } + + centerPinnedPath(pin) { + if (!pin || !this.canvas || !this.canvasData) return; + if (pin.kind === "node") { + const nodeId = this.plugin.index.visualNodeId(pin.nodeId); + const item = this.canvasData.nodesById.get(nodeId); + if (item) this.centerCanvasOnPoint(item.point); + return; + } + + const edge = this.findCanvasEdgeForPin(pin); + if (!edge || !edge.sourcePoint || !edge.targetPoint) return; + this.centerCanvasOnPoint({ + x: (edge.sourcePoint.x + edge.targetPoint.x) / 2, + y: (edge.sourcePoint.y + edge.targetPoint.y) / 2 + }); + } + + centerCanvasOnPoint(point) { + if (!point || !this.canvas) return; + const viewport = this.canvasViewport(); + const zoom = clampFloat(this.state.zoom, this.canvasMinZoom(viewport), this.canvasMaxZoom(viewport), 1); + this.canvasPanX = viewport.width / 2 - point.x * zoom; + this.canvasPanY = viewport.height / 2 - point.y * zoom; + this.saveViewportState(); + this.requestCanvasDraw("full"); + } + + inspectPinnedPath(pin, index, graph) { + if (!pin) return; + if (pin.kind === "node") { + this.state.sidePage = "inspect"; + this.selectedNodeId = this.plugin.index.visualNodeId(pin.nodeId); + this.selectedLink = null; + this.applyPersistentHighlight(); + if (!this.state.fullscreen) this.renderSidePanel(index, graph, this.selectedNodeId); + return; + } + + this.state.sidePage = "inspect"; + this.selectedLink = this.findGraphEdgeForPin(pin, graph) || null; + this.selectedNodeId = null; + this.applyPersistentHighlight(); + if (!this.state.fullscreen) this.renderSidePanel(index, graph); + } + + pinnedCanvasLabelIds() { + if (!this.canvasData) return []; + const ids = new Set(); + for (const pin of this.pinnedPaths) { + if (pin.kind === "node") { + const nodeId = this.plugin.index.visualNodeId(pin.nodeId); + if (this.canvasData.nodesById.has(nodeId)) ids.add(nodeId); + continue; + } + const edge = this.findCanvasEdgeForPin(pin); + if (!edge) continue; + if (edge.source !== null && edge.source !== undefined && this.canvasData.nodesById.has(edge.source)) ids.add(edge.source); + if (edge.target !== null && edge.target !== undefined && this.canvasData.nodesById.has(edge.target)) ids.add(edge.target); + } + return Array.from(ids); + } + + render(options = {}) { + this.syncColorSchemeClass(); + const preservePanel = Boolean(options.preservePanel); + const index = this.plugin.index; + if (!index.ready) { + this.graphHost.empty(); + this.graphHost.createDiv({ cls: "mwm-empty", text: "Building map..." }); + return; + } + + if (this.state.autoDetail && !this.adaptiveInitialized) { + this.applyInitialAdaptiveDefaults(index); + } + + if (this.searchInput && this.searchInput.value !== this.state.search) { + this.searchInput.value = this.state.search; + } + if (this.depthInput) this.depthInput.value = String(this.state.atlasDepth); + if (this.linkInput) this.linkInput.value = String(this.state.linkLimit); + if (this.nodeInput) this.nodeInput.value = String(this.state.nodeLimit); + if (this.autoToggle) this.autoToggle.checked = this.state.autoDetail; + if (this.linkHoverSelect) this.linkHoverSelect.value = normalizeHoverHighlightMode(this.state.hoverHighlightMode); + if (this.externalToggle) this.externalToggle.checked = this.state.showExternalLinks; + if (this.externalModeSelect) this.externalModeSelect.value = this.state.externalDetailMode; + if (this.externalLimitInput) this.externalLimitInput.value = String(this.state.externalLinkAnchorLimit); + this.syncZoomControls(); + if (this.columnInput) this.columnInput.value = String(this.state.columnSpacing); + if (this.rowInput) this.rowInput.value = String(this.state.rowSpacing); + if (this.labelVisibilitySelect) this.labelVisibilitySelect.value = normalizeLabelVisibility(this.state.labelVisibility); + if (this.swirlInput) { + this.swirlInput.value = String(this.state.swirlStrength); + if (this.swirlLabel) this.swirlLabel.textContent = `${Math.round(this.state.swirlStrength)}%`; + } + if (this.bodyEl) this.bodyEl.style.setProperty("--mwm-side-width", `${this.state.sidePanelWidth}px`); + if (this.contentEl) this.contentEl.classList.toggle("is-fullscreen", this.state.fullscreen); + + const started = performance.now(); + const graphState = Object.assign({}, this.state, { + selectedNodeId: this.selectedNodeId, + selectedLink: this.selectedLink, + pinNeedsHoverLinks: this.pinnedPathsNeedHoverLinks() + }); + const graph = index.buildVisibleGraph(graphState); + if (this.applyPreLayoutPressureGuard(graph)) return; + const layout = layoutVisibleGraph(index, graph, this.state); + this.lastLayout = layout; + this.positions = layout.positions; + + this.renderGraph(index, graph, layout); + if (!this.state.fullscreen && !preservePanel) this.renderSidePanel(index, graph); + this.lastRenderMs = Math.round(performance.now() - started); + this.renderMeta(index, graph, layout); + this.maybeAutoTune(index, graph, this.lastRenderMs); + } + + renderMeta(index, graph, layout) { + this.metaEl.empty(); + const rootNode = index.nodes.get(graph.rootId); + const activeNode = graph.focusId ? index.nodes.get(graph.focusId) : null; + const chips = [ + `${this.state.mode}`, + `${graph.nodes.length} nodes`, + `${graph.linkEdges.length} link overlays`, + `${graph.externalFileCount || 0} outside files`, + `${graph.externalGroupCount || 0} outside groups`, + `outside: ${this.state.externalDetailMode}`, + `${index.stats.notes} notes`, + `${index.stats.folders} folders`, + `${index.stats.scannedMarkdown} scanned md`, + `${this.lastRenderMs || 0} ms` + ]; + + if (this.state.autoDetail) chips.push("auto"); + if (this.state.showCompleteRoot) chips.push("complete root"); + if (rootNode && this.state.mode === "atlas" && rootNode.id !== ROOT_ID) { + chips.push(`root: ${rootNode.title}`); + } + if (activeNode && this.state.mode === "focus") { + chips.push(`focus: ${activeNode.title}`); + } + if (this.state.search) chips.push(`search: ${this.state.search}`); + if (normalizeHoverHighlightMode(this.state.hoverHighlightMode) !== "none") { + chips.push(`hover: ${hoverHighlightModeLabel(this.state.hoverHighlightMode)}`); + } + if (graph.hiddenNodeCount) chips.push(`${graph.hiddenNodeCount} hidden by node limit`); + + for (const chip of chips) { + this.metaEl.createSpan({ cls: "mwm-chip", text: chip }); + } + + if (layout.trimmed) { + this.metaEl.createSpan({ cls: "mwm-chip mwm-chip-warn", text: "large view" }); + } + } + + renderMapOnly() { + this.render({ preservePanel: true }); + } + + scheduleRender(delay = 80, options = {}) { + if (this.renderTimer) window.clearTimeout(this.renderTimer); + this.pendingRenderOptions = Object.assign({}, this.pendingRenderOptions || {}, options || {}); + this.renderTimer = window.setTimeout(() => { + this.renderTimer = null; + const renderOptions = this.pendingRenderOptions || {}; + this.pendingRenderOptions = null; + this.render(renderOptions); + }, delay); + } + + disableAutoDetail() { + if (!this.state.autoDetail) return; + this.state.autoDetail = false; + if (this.autoToggle) this.autoToggle.checked = false; + } + + resetAdaptiveTuning() { + if (!this.state.autoDetail) return; + this.adaptiveInitialized = false; + this.autoTuneCount = 0; + } + + toggleFullscreen() { + this.state.fullscreen = !this.state.fullscreen; + this.render(); + } + + resetToVaultRoot() { + this.state.rootPath = ROOT_ID; + this.state.mode = "atlas"; + this.state.showCompleteRoot = false; + this.viewInitialized = false; + this.resetAdaptiveTuning(); + this.render(); + } + + syncColorSchemeClass() { + if (!this.contentEl) return; + const scheme = normalizeColorScheme(this.state.colorScheme); + this.state.colorScheme = scheme; + this.contentEl.classList.toggle("is-day-scheme", scheme === "day"); + this.contentEl.classList.toggle("is-night-scheme", scheme === "night"); + this.contentEl.setAttribute("data-mwm-color-scheme", scheme); + } + + setColorScheme(scheme, persist = true) { + const next = normalizeColorScheme(scheme); + this.state.colorScheme = next; + this.plugin.settings.colorScheme = next; + this.syncColorSchemeClass(); + this.canvasPalette = this.readCanvasPalette(); + this.requestCanvasDraw("full"); + if (persist) { + for (const leaf of this.plugin.app.workspace.getLeavesOfType(VIEW_TYPE_MINI_WORLD_MAP)) { + const view = leaf.view; + if (view && view !== this && typeof view.setColorScheme === "function") { + view.setColorScheme(next, false); + } + } + void this.plugin.saveSettings({ rebuild: false }); + } + } + + cycleColorScheme() { + this.setColorScheme(nextColorScheme(this.state.colorScheme)); + } + + applyPluginSettings(keys, options = {}) { + const settingKeys = new Set(Array.isArray(keys) ? keys : [keys].filter(Boolean)); + if (!settingKeys.size) return; + + const settings = this.plugin.settings; + const previousNeedsHoverLinks = this.needsCanvasHoverLinks(); + const previousSwirl = this.state.swirlStrength; + let needsRender = false; + let needsDraw = false; + let needsPanel = false; + + const applyBudgetSetting = () => { + this.disableAutoDetail(); + this.disableCompleteRoot(); + needsRender = true; + }; + const applyStructuralSetting = () => { + this.disableCompleteRoot(); + needsRender = true; + }; + + if (settingKeys.has("atlasDepth")) { + this.state.atlasDepth = clampNumber(settings.atlasDepth, 1, MAX_ATLAS_DEPTH, DEFAULT_SETTINGS.atlasDepth); + applyBudgetSetting(); + } + if (settingKeys.has("renderNodeLimit")) { + this.state.nodeLimit = clampNumber(settings.renderNodeLimit, 200, MAX_RENDER_NODE_LIMIT, DEFAULT_SETTINGS.renderNodeLimit); + applyBudgetSetting(); + } + if (settingKeys.has("linkLimit")) { + this.state.linkLimit = clampNumber(settings.linkLimit, 0, MAX_LINK_LIMIT, DEFAULT_SETTINGS.linkLimit); + applyBudgetSetting(); + } + if (settingKeys.has("externalLinkAnchorLimit")) { + this.state.externalLinkAnchorLimit = clampNumber( + settings.externalLinkAnchorLimit, + 0, + MAX_EXTERNAL_LINK_ANCHOR_LIMIT, + DEFAULT_SETTINGS.externalLinkAnchorLimit + ); + applyBudgetSetting(); + } + if (settingKeys.has("adaptiveDetail")) { + this.state.autoDetail = Boolean(settings.adaptiveDetail); + if (this.state.autoDetail) this.disableCompleteRoot(); + this.adaptiveInitialized = false; + this.autoTuneCount = 0; + needsRender = true; + } + if (settingKeys.has("hoverHighlightMode")) { + this.state.hoverHighlightMode = normalizeHoverHighlightMode(settings.hoverHighlightMode); + this.state.enableLinkHover = hoverHighlightsNoteLinks(this.state.hoverHighlightMode); + this.hoverLink = null; + if (previousNeedsHoverLinks !== this.needsCanvasHoverLinks()) needsRender = true; + else needsDraw = true; + } + if (settingKeys.has("showExternalLinks")) { + this.state.showExternalLinks = Boolean(settings.showExternalLinks); + applyStructuralSetting(); + } + if (settingKeys.has("externalDetailMode")) { + this.state.externalDetailMode = settings.externalDetailMode || DEFAULT_SETTINGS.externalDetailMode; + this.resetAdaptiveTuning(); + applyStructuralSetting(); + } + if (settingKeys.has("labelVisibility")) { + this.state.labelVisibility = normalizeLabelVisibility(settings.labelVisibility); + needsDraw = true; + } + if (settingKeys.has("swirlStrength")) { + this.state.swirlStrength = clampNumber(settings.swirlStrength, 0, MAX_SWIRL_STRENGTH, DEFAULT_SWIRL_STRENGTH); + if ((previousSwirl > 0) !== (this.state.swirlStrength > 0)) { + if (this.state.swirlStrength > 0) this.startCanvasSwirlAnimation(); + else { + this.cancelCanvasSwirlAnimation(); + this.resetCanvasSwirlPositions(); + } + needsRender = true; + } else { + needsDraw = true; + } + } + if (settingKeys.has("colorScheme")) { + this.state.colorScheme = normalizeColorScheme(settings.colorScheme); + this.syncColorSchemeClass(); + this.canvasPalette = this.readCanvasPalette(); + needsDraw = true; + } + if (settingKeys.has("hiddenLegendItems")) { + this.state.hiddenLegendItems = Array.isArray(settings.hiddenLegendItems) + ? settings.hiddenLegendItems.slice() + : DEFAULT_SETTINGS.hiddenLegendItems.slice(); + needsRender = true; + } + if (settingKeys.has("includeUnresolvedLinks") || settingKeys.has("ignoreFolders")) { + needsRender = true; + this.viewInitialized = false; + } + + if (options.resetViewport) this.viewInitialized = false; + if (options.render === false) return; + + if (needsRender) { + this.render({ preservePanel: options.preservePanel === true }); + return; + } + + if (needsDraw) this.requestCanvasDraw("full"); + if ((needsDraw || needsPanel) && options.preservePanel !== true && !this.state.fullscreen && this.lastCanvasBundle) { + this.renderSidePanel(this.lastCanvasBundle.index, this.lastCanvasBundle.graph); + } + } + + disableCompleteRoot() { + if (!this.state.showCompleteRoot) return; + this.state.showCompleteRoot = false; + this.preCompleteState = null; + } + + exitCompleteRoot() { + if (!this.state.showCompleteRoot) return; + const previous = this.preCompleteState; + this.state.showCompleteRoot = false; + this.preCompleteState = null; + if (previous) { + Object.assign(this.state, previous); + } + } + + showCompleteCurrentRoot() { + const index = this.plugin.index; + if (!index.ready) return; + + const currentGraphRoot = this.lastCanvasBundle?.graph?.rootId; + const rootId = currentGraphRoot !== null && currentGraphRoot !== undefined && index.nodes.has(currentGraphRoot) + ? currentGraphRoot + : this.state.mode === "atlas" && index.nodes.has(this.state.rootPath) + ? this.state.rootPath + : ROOT_ID; + const profile = this.completeRootProfile(index, rootId); + + this.preCompleteState = { + mode: this.state.mode, + rootPath: this.state.rootPath, + atlasDepth: this.state.atlasDepth, + nodeLimit: this.state.nodeLimit, + linkLimit: this.state.linkLimit, + externalLinkAnchorLimit: this.state.externalLinkAnchorLimit, + autoDetail: this.state.autoDetail, + showExternalLinks: this.state.showExternalLinks, + externalDetailMode: this.state.externalDetailMode + }; + this.state.mode = "atlas"; + this.state.rootPath = rootId; + this.state.showCompleteRoot = true; + this.state.autoDetail = false; + this.state.showExternalLinks = true; + this.state.externalDetailMode = "exact"; + this.state.atlasDepth = profile.depth; + this.state.nodeLimit = profile.nodeLimit; + this.state.linkLimit = profile.linkLimit; + this.state.externalLinkAnchorLimit = profile.externalLimit; + this.viewInitialized = false; + this.adaptiveInitialized = true; + this.render(); + } + + completeRootProfile(index, rootId) { + const root = index.nodes.get(rootId) || index.nodes.get(ROOT_ID); + const rootDepth = root ? root.depth || 0 : 0; + const insideIds = new Set(); + let maxRelDepth = 1; + + for (const node of index.nodes.values()) { + if (!nodeWithinRoot(index, node, rootId)) continue; + insideIds.add(node.id); + maxRelDepth = Math.max(maxRelDepth, Math.max(0, (node.depth || 0) - rootDepth)); + } + + let linkCount = 0; + const exactExternalIds = new Set(); + for (const edge of index.linkEdges) { + const sourceInside = insideIds.has(edge.source); + const targetInside = insideIds.has(edge.target); + if (!sourceInside && !targetInside) continue; + linkCount += 1; + if (sourceInside !== targetInside) exactExternalIds.add(sourceInside ? edge.target : edge.source); + } + + return { + depth: clampNumber(maxRelDepth, 1, MAX_ATLAS_DEPTH, DEFAULT_SETTINGS.atlasDepth), + nodeLimit: clampNumber(insideIds.size + exactExternalIds.size + 64, 200, MAX_RENDER_NODE_LIMIT, DEFAULT_SETTINGS.renderNodeLimit), + linkLimit: clampNumber(linkCount + 64, 0, MAX_LINK_LIMIT, DEFAULT_SETTINGS.linkLimit), + externalLimit: clampNumber(exactExternalIds.size + 64, 0, MAX_EXTERNAL_LINK_ANCHOR_LIMIT, DEFAULT_SETTINGS.externalLinkAnchorLimit) + }; + } + + shouldRerenderForSelection() { + return false; + } + + clearSelection(index, graph) { + const hadSelection = this.selectedNodeId !== null && this.selectedNodeId !== undefined || Boolean(this.selectedLink); + this.selectedNodeId = null; + this.selectedLink = null; + this.hoverNodeId = null; + this.hoverLink = null; + + if (hadSelection && this.shouldRerenderForSelection()) { + this.render(); + return; + } + + this.clearHoverClasses(); + if (this.graphHost) this.graphHost.classList.remove("is-hovering"); + if (this.graphHost) this.graphHost.classList.remove("is-pointing"); + this.requestCanvasDraw("full"); + if (!this.state.fullscreen) this.renderSidePanel(index, graph); + } + + applyInitialAdaptiveDefaults(index) { + const root = index.nodes.get(this.state.mode === "atlas" ? this.state.rootPath : ROOT_ID); + const localNotes = root ? (root.noteCount || root.descendantCount || index.stats.notes) : index.stats.notes; + const visiblePressure = Math.max(1, Math.min(index.stats.notes || 1, localNotes || 1)); + + if (visiblePressure < 1500) { + this.state.atlasDepth = Math.max(this.state.atlasDepth, 8); + this.state.nodeLimit = Math.max(this.state.nodeLimit, 6200); + this.state.linkLimit = Math.max(this.state.linkLimit, 2200); + this.state.externalLinkAnchorLimit = Math.max(this.state.externalLinkAnchorLimit, 900); + } else if (visiblePressure < 4200) { + this.state.atlasDepth = Math.max(this.state.atlasDepth, 7); + this.state.nodeLimit = Math.max(this.state.nodeLimit, 5200); + this.state.linkLimit = Math.max(this.state.linkLimit, 1700); + this.state.externalLinkAnchorLimit = Math.max(this.state.externalLinkAnchorLimit, 750); + } else if (visiblePressure < 9000) { + this.state.atlasDepth = Math.max(this.state.atlasDepth, 5); + this.state.nodeLimit = Math.max(this.state.nodeLimit, 3600); + this.state.linkLimit = Math.max(this.state.linkLimit, 1000); + this.state.externalLinkAnchorLimit = Math.max(this.state.externalLinkAnchorLimit, 500); + } else { + this.state.atlasDepth = Math.max(4, Math.min(this.state.atlasDepth, 6)); + this.state.nodeLimit = Math.max(this.state.nodeLimit, 2600); + this.state.linkLimit = Math.max(this.state.linkLimit, 700); + this.state.externalLinkAnchorLimit = Math.max(this.state.externalLinkAnchorLimit, 300); + } + + this.adaptiveInitialized = true; + } + + applyPreLayoutPressureGuard(graph) { + if (!this.state.autoDetail || this.state.search || this.state.showCompleteRoot) return false; + + const pressure = graph.nodes.length + + graph.linkEdges.length * 1.7 + + (graph.externalFileCount || 0) * 5 + + (graph.externalGroupCount || 0) * 1.4 + + (graph.hiddenNodeCount || 0) * 0.15; + + if (pressure < 9000 && graph.externalFileCount < 520) return false; + + let changed = false; + if (this.state.externalLinkAnchorLimit > 260) { + this.state.externalLinkAnchorLimit = Math.max(220, Math.floor(this.state.externalLinkAnchorLimit * 0.72)); + changed = true; + } + if (this.state.linkLimit > 650) { + this.state.linkLimit = Math.max(560, Math.floor(this.state.linkLimit * 0.74)); + changed = true; + } + if (this.state.nodeLimit > 2400) { + this.state.nodeLimit = Math.max(2200, Math.floor(this.state.nodeLimit * 0.78)); + changed = true; + } + if (pressure > 15000 && this.state.atlasDepth > 3) { + this.state.atlasDepth -= 1; + changed = true; + } + + if (changed) { + this.autoTuneCount += 1; + this.lastAutoTuneAt = performance.now(); + this.graphHost.empty(); + this.graphHost.createDiv({ cls: "mwm-empty", text: "Reducing map detail..." }); + this.scheduleRender(60); + return true; + } + return false; + } + + maybeAutoTune(index, graph, elapsedMs) { + if (!this.state.autoDetail || this.state.search || this.state.showCompleteRoot) return; + if (this.autoTuneCount >= 9) return; + + const now = performance.now(); + if (now - this.lastAutoTuneAt < 900) return; + + const pressure = graph.nodes.length + + graph.linkEdges.length * 1.5 + + (graph.externalFileCount || 0) * 4; + const tooHeavy = elapsedMs > 480 || pressure > 9000 || graph.nodes.length > this.state.nodeLimit * 1.08; + const veryLight = elapsedMs > 0 + && elapsedMs < 70 + && graph.hiddenNodeCount === 0 + && graph.externalFileCount < 80 + && graph.nodes.length > Math.max(80, this.state.nodeLimit * 0.35); + + let changed = false; + if (tooHeavy) { + this.state.linkLimit = Math.max(520, Math.floor(this.state.linkLimit * 0.76)); + this.state.nodeLimit = Math.max(2100, Math.floor(this.state.nodeLimit * 0.8)); + this.state.externalLinkAnchorLimit = Math.max(220, Math.floor(this.state.externalLinkAnchorLimit * 0.74)); + if ((elapsedMs > 850 || pressure > 15000) && this.state.atlasDepth > 3) this.state.atlasDepth -= 1; + changed = true; + } else if (veryLight) { + const maxDepth = Math.min(MAX_ATLAS_DEPTH, index.stats.maxDepth || MAX_ATLAS_DEPTH); + if (this.state.atlasDepth < maxDepth) { + this.state.atlasDepth += 1; + changed = true; + } + if (this.state.nodeLimit < 12000) { + this.state.nodeLimit = Math.min(12000, this.state.nodeLimit + 700); + changed = true; + } + if (this.state.linkLimit < 6000) { + this.state.linkLimit = Math.min(6000, this.state.linkLimit + 260); + changed = true; + } + } + + if (changed) { + this.autoTuneCount += 1; + this.lastAutoTuneAt = now; + this.scheduleRender(120); + } + } + + renderGraph(index, graph, layout) { + this.cancelCanvasPanMomentum(); + this.cancelCanvasZoomAnimation(); + this.cancelCanvasSpringBack(); + this.cancelCanvasSwirlAnimation(); + this.destroyPixiRenderer(); + this.graphHost.empty(); + this.graphHost.classList.remove("is-hovering"); + this.graphHost.classList.remove("is-panning"); + this.graphHost.classList.remove("is-pointing"); + this.activeHighlightElements.clear(); + this.nodeElementsById.clear(); + this.edgeElementsByNode.clear(); + this.edgeElementsById.clear(); + this.hoverNodeId = null; + this.hoverLink = null; + + const pixiRenderer = this.createPixiRenderer(); + const canvas = pixiRenderer + ? pixiRenderer.view + : this.graphHost.createEl("canvas", { + cls: "mwm-canvas", + attr: { + role: "img", + "aria-label": "Mini World Map graph", + tabindex: "0" + } + }); + this.canvas = canvas; + this.renderFloatingCanvasControls(); + this.renderFloatingThemeButton(); + this.canvasPalette = this.readCanvasPalette(); + + const query = normalizedQuery(this.state.search); + this.canvasData = buildCanvasGraphData( + index, + graph, + layout, + query, + this.plugin.nativeGraphSettings || DEFAULT_NATIVE_GRAPH_SETTINGS, + { + includeHoverLinks: this.needsCanvasHoverLinks(), + hiddenLegendItems: this.state.hiddenLegendItems + } + ); + this.lastCanvasBundle = { index, graph, layout }; + + const viewport = this.canvasViewport(); + const viewportStateKey = this.viewportStateKey(graph); + const changingViewport = this.currentViewportStateKey !== viewportStateKey; + if (changingViewport) this.saveViewportState(); + const minZoom = this.canvasMinZoom(viewport, layout); + const maxZoom = this.canvasMaxZoom(viewport, layout); + const requestedWholeMap = this.state.zoom <= minZoom + 0.0005; + this.state.zoom = clampFloat(this.state.zoom, minZoom, maxZoom, 1); + if (pixiRenderer) pixiRenderer.resize(viewport); + else this.sizeCanvasToViewport(canvas, viewport); + const signature = [ + graph.rootId, + graph.focusId || "", + graph.nodes.length, + graph.linkEdges.length, + Math.round(layout.width), + Math.round(layout.height) + ].join("|"); + + if (!this.viewInitialized || this.graphViewSignature !== signature || changingViewport) { + const restored = this.restoreViewportState(viewportStateKey, viewport, layout); + if (!restored) { + this.state.zoom = this.defaultZoomForLayout(layout, viewport); + this.centerCanvasView(layout, viewport, this.state.mode === "focus" ? (graph.focusId || graph.rootId) : null); + } + this.currentViewportStateKey = viewportStateKey; + this.graphViewSignature = signature; + this.viewInitialized = true; + this.canvasVisualNodes.clear(); + this.canvasVisualEdges.clear(); + this.canvasVisualInitialized = false; + } else if (requestedWholeMap) { + this.centerCanvasView(layout, viewport, null); + } + this.syncZoomControls(); + + this.installCanvasEvents(canvas); + this.drawCanvasGraph(); + this.startCanvasSwirlAnimation(); + } + + createPixiRenderer() { + if (this.pixiUnavailable) return null; + const PIXI = loadMiniWorldMapPixiRuntime(); + if (!PIXI) { + this.pixiUnavailable = true; + console.warn("Mini World Map Pixi renderer unavailable; falling back to 2D canvas."); + return null; + } + try { + this.pixiRenderer = new MiniWorldMapPixiRenderer(this.graphHost, PIXI); + return this.pixiRenderer; + } catch (error) { + this.pixiUnavailable = true; + console.error("Mini World Map failed to create Pixi renderer; falling back to 2D canvas.", error); + this.destroyPixiRenderer(); + return null; + } + } + + destroyPixiRenderer() { + if (this.pixiRenderer) { + this.pixiRenderer.destroy(); + this.pixiRenderer = null; + } + } + + renderFloatingThemeButton() { + if (!this.graphHost) return; + const scheme = normalizeColorScheme(this.state.colorScheme); + const next = nextColorScheme(scheme); + const button = this.graphHost.createEl("button", { + cls: `mwm-floating-button mwm-theme-button is-${scheme}`, + attr: { + type: "button", + "aria-label": `Theme: ${colorSchemeLabel(scheme)}`, + title: `Theme: ${colorSchemeLabel(scheme)}. Click for ${colorSchemeLabel(next)}.` + } + }); + setIcon(button, colorSchemeIcon(scheme)); + button.addEventListener("click", evt => { + evt.preventDefault(); + evt.stopPropagation(); + this.cycleColorScheme(); + }); + } + + renderFloatingCanvasControls() { + if (!this.graphHost) return; + const controls = this.graphHost.createDiv({ cls: "mwm-floating-controls" }); + + const rootButton = controls.createEl("button", { + cls: "mwm-floating-button", + attr: { type: "button", "aria-label": "Vault root", title: "Vault root" } + }); + setIcon(rootButton, "home"); + rootButton.addEventListener("click", evt => { + evt.preventDefault(); + evt.stopPropagation(); + this.resetToVaultRoot(); + }); + + const pinButton = controls.createEl("button", { + cls: "mwm-floating-button", + attr: { type: "button", "aria-label": "Pin hover", title: "Pin hover" } + }); + setIcon(pinButton, "pin"); + pinButton.addEventListener("click", evt => { + evt.preventDefault(); + evt.stopPropagation(); + this.pinCurrentHoverPath(); + }); + + const fullscreenButton = controls.createEl("button", { + cls: "mwm-floating-button", + attr: { + type: "button", + "aria-label": this.state.fullscreen ? "Exit full screen" : "Full screen", + title: this.state.fullscreen ? "Exit full screen" : "Full screen" + } + }); + setIcon(fullscreenButton, this.state.fullscreen ? "minimize-2" : "maximize-2"); + fullscreenButton.addEventListener("click", evt => { + evt.preventDefault(); + evt.stopPropagation(); + this.toggleFullscreen(); + }); + } + + readCanvasPalette() { + const fallbackBg = resolveCssColor(this.contentEl, "--mwm-graph-bg", "#1e1e1e"); + const fallbackLine = resolveCssColor(this.contentEl, "--mwm-graph-line", "rgba(128, 128, 128, 0.45)"); + const fallbackNode = resolveCssColor(this.contentEl, "--mwm-node", "#8f8f8f"); + const fallbackFocus = resolveCssColor(this.contentEl, "--mwm-node-focused", "#8b7cf6"); + const fallbackText = resolveCssColor(this.contentEl, "--mwm-text", "#dddddd"); + const graphLine = fallbackLine; + const graphNode = fallbackNode; + const graphFocus = fallbackFocus; + const graphCircle = resolveCssColor(this.contentEl, "--mwm-node-focused", graphFocus); + const graphText = fallbackText; + const graphFillHighlight = resolveCssColor(this.contentEl, "--mwm-node-glow", graphFocus); + const graphLineHighlight = resolveCssColor(this.contentEl, "--mwm-link-highlight", graphFocus); + const graphUnresolved = resolveCssColor(this.contentEl, "--mwm-unresolved", fallbackNode); + return { + bg: fallbackBg, + line: graphLine, + lineHighlight: graphLineHighlight, + node: graphNode, + note: resolveCssColor(this.contentEl, "--mwm-note", graphNode), + folder: resolveCssColor(this.contentEl, "--mwm-folder", graphNode), + folderMeta: resolveCssColor(this.contentEl, "--mwm-folder-meta", graphNode), + folderRing: resolveCssColor(this.contentEl, "--mwm-folder-ring", graphLine), + tree: resolveCssColor(this.contentEl, "--mwm-tree", graphLine), + ringGuide: resolveCssColor(this.contentEl, "--mwm-ring-guide", graphLine), + fileRing: resolveCssColor(this.contentEl, "--mwm-file-ring", graphNode), + focus: graphFocus, + circle: graphCircle, + text: graphText, + labelText: resolveCssColor(this.contentEl, "--mwm-label-text", graphText), + labelStroke: resolveCssColor(this.contentEl, "--mwm-label-stroke", fallbackBg), + link: resolveCssColor(this.contentEl, "--mwm-link", graphLine), + external: resolveCssColor(this.contentEl, "--mwm-external", graphCircle), + externalLink: resolveCssColor(this.contentEl, "--mwm-external-link", graphCircle), + unresolved: graphUnresolved, + muted: resolveCssColor(this.contentEl, "--mwm-muted", "#999999"), + stroke: resolveCssColor(this.contentEl, "--mwm-node-stroke", "#111111"), + glow: graphFillHighlight, + fontFamily: getComputedStyle(this.contentEl).fontFamily || "system-ui, -apple-system, BlinkMacSystemFont, \"Segoe UI\", sans-serif" + }; + } + + canvasViewport() { + return { + width: Math.max(1, Math.floor(this.graphHost?.clientWidth || 900)), + height: Math.max(1, Math.floor(this.graphHost?.clientHeight || 520)) + }; + } + + sizeCanvasToViewport(canvas, viewport) { + const dpr = Math.max(1, Math.min(3, window.devicePixelRatio || 1)); + const width = Math.max(1, viewport.width); + const height = Math.max(1, viewport.height); + if (canvas.width !== Math.floor(width * dpr)) canvas.width = Math.floor(width * dpr); + if (canvas.height !== Math.floor(height * dpr)) canvas.height = Math.floor(height * dpr); + canvas.style.width = `${width}px`; + canvas.style.height = `${height}px`; + this.canvasDpr = dpr; + this.canvasViewportSize = { width, height }; + } + + canvasMinZoom(viewport = this.canvasViewport(), layout = this.lastLayout) { + if (!layout || !viewport) return DEFAULT_MIN_CANVAS_ZOOM; + const fitZoom = fitZoomForLayout(layout, viewport, 32); + return clampFloat(Math.min(DEFAULT_MIN_CANVAS_ZOOM, fitZoom), MIN_CANVAS_ZOOM, DEFAULT_MIN_CANVAS_ZOOM, DEFAULT_MIN_CANVAS_ZOOM); + } + + canvasMaxZoom(viewport = this.canvasViewport(), layout = this.lastLayout) { + return MAX_CANVAS_ZOOM; + } + + viewportStateKey(graph) { + const root = graph?.rootId || ROOT_ID; + const focus = graph?.focusId || ""; + const detail = this.state.showCompleteRoot ? "complete" : "bounded"; + return `${this.state.mode}|${root}|${focus}|${detail}`; + } + + saveViewportState() { + if (!this.currentViewportStateKey) return; + if (!Number.isFinite(this.state.zoom) || !Number.isFinite(this.canvasPanX) || !Number.isFinite(this.canvasPanY)) return; + this.viewportStateByKey.set(this.currentViewportStateKey, { + zoom: this.state.zoom, + panX: this.canvasPanX, + panY: this.canvasPanY + }); + } + + restoreViewportState(key, viewport, layout) { + const saved = this.viewportStateByKey.get(key); + if (!saved) return false; + const minZoom = this.canvasMinZoom(viewport, layout); + const maxZoom = this.canvasMaxZoom(viewport, layout); + this.state.zoom = clampFloat(saved.zoom, minZoom, maxZoom, this.defaultZoomForLayout(layout, viewport)); + this.canvasPanX = Number.isFinite(saved.panX) ? saved.panX : 0; + this.canvasPanY = Number.isFinite(saved.panY) ? saved.panY : 0; + return true; + } + + defaultZoomForLayout(layout, viewport = this.canvasViewport()) { + const minZoom = this.canvasMinZoom(viewport, layout); + const maxZoom = this.canvasMaxZoom(viewport, layout); + const fitZoom = fitZoomForLayout(layout, viewport, 42); + return clampFloat(fitZoom * 1.08, minZoom, maxZoom, minZoom); + } + + centerCanvasView(layout, viewport = this.canvasViewport(), rootId = ROOT_ID) { + const zoom = clampFloat(this.state.zoom, this.canvasMinZoom(viewport, layout), this.canvasMaxZoom(viewport, layout), 1); + const rootPoint = rootId === null ? null : (layout.positions.get(rootId) || layout.positions.get(ROOT_ID)); + const centerX = rootPoint ? rootPoint.x : layout.width / 2; + const centerY = rootPoint ? rootPoint.y : layout.height / 2; + this.canvasPanX = viewport.width / 2 - centerX * zoom; + this.canvasPanY = viewport.height / 2 - centerY * zoom; + this.saveViewportState(); + } + + installCanvasEvents(canvas) { + canvas.addEventListener("pointerdown", evt => { + if (evt.button !== 0) return; + this.cancelCanvasPanMomentum(); + this.cancelCanvasZoomAnimation(); + this.cancelCanvasSpringBack(); + const point = this.canvasEventPoint(evt); + const hit = this.hitTestCanvas(point); + if (hit.node && hit.node.node.id !== this.lastCanvasBundle?.graph?.rootId) { + const world = this.screenToWorld(point); + this.dragState = { + mode: "node", + pointerId: evt.pointerId, + nodeId: hit.node.node.id, + startClientX: evt.clientX, + startClientY: evt.clientY, + startWorldX: world.x, + startWorldY: world.y, + nodeStartX: hit.node.point.x, + nodeStartY: hit.node.point.y, + lastDx: 0, + lastDy: 0, + moved: false, + suppressClick: false + }; + canvas.setPointerCapture?.(evt.pointerId); + this.hoverNodeId = hit.node.node.id; + this.hoverLink = null; + this.graphHost.classList.add("is-panning", "is-pointing"); + this.requestCanvasDraw("interactive"); + return; + } + + this.dragState = { + mode: "pan", + pointerId: evt.pointerId, + startClientX: evt.clientX, + startClientY: evt.clientY, + startPanX: this.canvasPanX, + startPanY: this.canvasPanY, + lastClientX: evt.clientX, + lastClientY: evt.clientY, + lastMoveAt: performance.now(), + velocityX: 0, + velocityY: 0, + moved: false, + suppressClick: false + }; + canvas.setPointerCapture?.(evt.pointerId); + this.graphHost.classList.add("is-panning"); + this.updateCanvasHover(point); + }); + + canvas.addEventListener("pointermove", evt => { + const point = this.canvasEventPoint(evt); + if (this.dragState) { + const dx = evt.clientX - this.dragState.startClientX; + const dy = evt.clientY - this.dragState.startClientY; + this.dragState.moved = this.dragState.moved || Math.hypot(dx, dy) > 3; + this.dragState.suppressClick = this.dragState.moved; + if (this.dragState.mode === "node") { + const world = this.screenToWorld(point); + const worldDx = world.x - this.dragState.startWorldX; + const worldDy = world.y - this.dragState.startWorldY; + const stepDx = worldDx - (this.dragState.lastDx || 0); + const stepDy = worldDy - (this.dragState.lastDy || 0); + this.dragState.lastDx = worldDx; + this.dragState.lastDy = worldDy; + + const item = this.canvasData?.nodesById?.get(this.dragState.nodeId); + if (item) { + item.point.x = this.dragState.nodeStartX + worldDx; + item.point.y = this.dragState.nodeStartY + worldDy; + this.updateCanvasPointPolar(item.point); + this.applyNodeDragGravity(item, stepDx, stepDy); + if (!this.canvasNeedsFastDraw()) this.resolveCanvasNodeOverlapsAround(item.node.id, 2); + this.hoverNodeId = item.node.id; + this.hoverLink = null; + } + this.requestCanvasDraw("interactive"); + return; + } + + this.canvasPanX = this.dragState.startPanX + dx; + this.canvasPanY = this.dragState.startPanY + dy; + const now = performance.now(); + const dt = Math.max(1, now - (this.dragState.lastMoveAt || now)); + const stepX = evt.clientX - (this.dragState.lastClientX ?? evt.clientX); + const stepY = evt.clientY - (this.dragState.lastClientY ?? evt.clientY); + this.dragState.velocityX = this.dragState.velocityX * 0.45 + (stepX / dt) * 0.55; + this.dragState.velocityY = this.dragState.velocityY * 0.45 + (stepY / dt) * 0.55; + this.dragState.lastClientX = evt.clientX; + this.dragState.lastClientY = evt.clientY; + this.dragState.lastMoveAt = now; + this.saveViewportState(); + this.requestCanvasDraw("interactive"); + return; + } + this.updateCanvasHover(point); + }); + + canvas.addEventListener("pointerup", evt => { + const wasDragging = Boolean(this.dragState && this.dragState.suppressClick); + const draggedNodeId = this.dragState && this.dragState.mode === "node" ? this.dragState.nodeId : null; + const panVelocity = this.dragState && this.dragState.mode === "pan" + ? { x: this.dragState.velocityX || 0, y: this.dragState.velocityY || 0 } + : null; + canvas.releasePointerCapture?.(evt.pointerId); + this.dragState = null; + this.graphHost.classList.remove("is-panning"); + if (draggedNodeId && wasDragging) { + if (!this.canvasNeedsFastDraw()) this.resolveCanvasNodeOverlapsAround(draggedNodeId, 3); + this.startCanvasSpringBack(draggedNodeId); + return; + } + if (wasDragging) { + if (panVelocity) { + this.startCanvasPanMomentum(panVelocity.x, panVelocity.y); + } else { + this.requestCanvasDraw("full"); + } + return; + } + if (!wasDragging) this.activateCanvasAt(this.canvasEventPoint(evt), evt); + }); + + canvas.addEventListener("pointerleave", () => { + if (this.dragState) return; + this.hoverNodeId = null; + this.hoverLink = null; + this.graphHost.classList.remove("is-hovering"); + this.graphHost.classList.remove("is-pointing"); + this.requestCanvasDraw("full"); + }); + + canvas.addEventListener("dblclick", evt => { + const hit = this.hitTestCanvas(this.canvasEventPoint(evt)); + if (!hit.node) return; + evt.preventDefault(); + evt.stopPropagation(); + const node = hit.node.node; + if (node.type === "note") { + this.openNode(node.id, evt); + } else if (node.type === "folder") { + this.state.mode = "atlas"; + this.state.rootPath = node.id; + this.state.showCompleteRoot = false; + this.viewInitialized = false; + this.resetAdaptiveTuning(); + this.render(); + } + }); + + canvas.addEventListener("contextmenu", evt => { + const hit = this.hitTestCanvas(this.canvasEventPoint(evt)); + if (!hit.node) return; + evt.preventDefault(); + this.showNodeMenu(evt, hit.node.node); + }); + } + + updateCanvasPointPolar(point) { + if (!point) return; + const centerX = Number.isFinite(point.centerX) ? point.centerX : 0; + const centerY = Number.isFinite(point.centerY) ? point.centerY : 0; + const dx = point.x - centerX; + const dy = point.y - centerY; + point.radius = Math.hypot(dx, dy); + point.angle = Math.atan2(dy, dx); + point.labelSide = labelSideForAngle(point.angle); + } + + applyNodeDragGravity(draggedItem, dx, dy) { + if (!draggedItem || (!dx && !dy) || !this.canvasData) return; + const nodeId = draggedItem.node.id; + const edges = this.canvasData.edgesByNode.get(nodeId) || []; + if (!edges.length) return; + + const gravity = clampFloat(Math.sqrt(Math.max(1, draggedItem.radius)) / 9.2, 0.06, 0.34, 0.12); + const moved = new Set([nodeId]); + for (const edge of edges) { + const otherId = edge.source === nodeId ? edge.target : edge.source; + if (moved.has(otherId)) continue; + const other = this.canvasData.nodesById.get(otherId); + if (!other || other.node.id === this.lastCanvasBundle?.graph?.rootId) continue; + + const hierarchy = edge.edge && edge.edge.type && String(edge.edge.type).includes("hierarchy"); + const strength = gravity * (hierarchy ? 0.42 : 0.18); + other.point.x += dx * strength; + other.point.y += dy * strength; + this.updateCanvasPointPolar(other.point); + moved.add(otherId); + } + } + + startCanvasPanMomentum(velocityX, velocityY) { + this.cancelCanvasPanMomentum(); + const speed = Math.hypot(velocityX, velocityY); + if (!this.canvas || !this.canvasData || speed < 0.06) { + this.requestCanvasDraw("full"); + return; + } + + this.canvasPanVelocityX = clampFloat(velocityX, -2.2, 2.2, 0); + this.canvasPanVelocityY = clampFloat(velocityY, -2.2, 2.2, 0); + let lastFrameAt = performance.now(); + + const tick = now => { + this.canvasPanAnimationFrame = null; + if (!this.canvas || !this.canvasData || this.dragState) { + this.canvasPanVelocityX = 0; + this.canvasPanVelocityY = 0; + return; + } + + const dt = clampFloat(now - lastFrameAt, 1, 40, 16); + lastFrameAt = now; + this.canvasPanX += this.canvasPanVelocityX * dt; + this.canvasPanY += this.canvasPanVelocityY * dt; + const friction = Math.exp(-dt / 185); + this.canvasPanVelocityX *= friction; + this.canvasPanVelocityY *= friction; + this.saveViewportState(); + this.requestCanvasDraw("interactive"); + + if (Math.hypot(this.canvasPanVelocityX, this.canvasPanVelocityY) < 0.018) { + this.canvasPanVelocityX = 0; + this.canvasPanVelocityY = 0; + this.requestCanvasDraw("full"); + return; + } + + this.canvasPanAnimationFrame = window.requestAnimationFrame(tick); + }; + + this.canvasPanAnimationFrame = window.requestAnimationFrame(tick); + } + + cancelCanvasPanMomentum() { + if (this.canvasPanAnimationFrame) { + window.cancelAnimationFrame(this.canvasPanAnimationFrame); + this.canvasPanAnimationFrame = null; + } + this.canvasPanVelocityX = 0; + this.canvasPanVelocityY = 0; + } + + resolveCanvasNodeOverlapsAround(nodeId, iterations = 3) { + if (!this.canvasData || !this.canvasData.nodesById.has(nodeId)) return; + + const dragged = this.canvasData.nodesById.get(nodeId); + const rootId = this.lastCanvasBundle?.graph?.rootId; + const zoom = clampFloat(this.state.zoom, this.canvasMinZoom(), this.canvasMaxZoom(), 1); + const gap = Math.max(8, 14 / zoom); + + for (let pass = 0; pass < iterations; pass += 1) { + for (const other of this.canvasData.nodes) { + if (!other || other.node.id === nodeId) continue; + + let dx = other.point.x - dragged.point.x; + let dy = other.point.y - dragged.point.y; + let distance = Math.hypot(dx, dy); + if (distance < 0.001) { + const angle = deterministicPairAngle(nodeId, other.node.id); + dx = Math.cos(angle); + dy = Math.sin(angle); + distance = 1; + } + + const minDistance = dragged.radius + other.radius + gap; + if (distance >= minDistance) continue; + + const push = (minDistance - distance) * (pass === 0 ? 0.95 : 0.72); + const nx = dx / distance; + const ny = dy / distance; + const otherFixed = other.node.id === rootId; + if (otherFixed) { + dragged.point.x -= nx * push; + dragged.point.y -= ny * push; + this.updateCanvasPointPolar(dragged.point); + } else { + other.point.x += nx * push; + other.point.y += ny * push; + this.updateCanvasPointPolar(other.point); + } + } + } + } + + startCanvasSpringBack(nodeId) { + this.cancelCanvasSpringBack(); + if (!this.canvasData || !this.canvasData.nodes || !this.canvasData.nodes.length) return; + + const rootId = this.lastCanvasBundle?.graph?.rootId; + const items = []; + let maxDistance = 0; + + for (const item of this.canvasData.nodes) { + if (!item || !item.point || item.node.id === rootId) continue; + const point = item.point; + const homeX = Number.isFinite(point.homeX) ? point.homeX : point.x; + const homeY = Number.isFinite(point.homeY) ? point.homeY : point.y; + const distance = Math.hypot(point.x - homeX, point.y - homeY); + if (distance < 0.55) continue; + maxDistance = Math.max(maxDistance, distance); + items.push({ + item, + startX: point.x, + startY: point.y, + homeX, + homeY, + primary: item.node.id === nodeId + }); + } + + if (!items.length) { + this.requestCanvasDraw("full"); + return; + } + + const started = performance.now(); + const duration = clampFloat(300 + Math.sqrt(maxDistance) * 16, 340, 760, 460); + this.canvasSpringState = { items, started, duration }; + + const tick = now => { + const state = this.canvasSpringState; + if (!state || this.dragState) { + this.canvasSpringAnimationFrame = null; + return; + } + + const progress = clampFloat((now - state.started) / state.duration, 0, 1, 1); + const eased = springBackEase(progress); + + for (const entry of state.items) { + const point = entry.item.point; + const strength = entry.primary ? eased : Math.min(1, eased * 0.94); + point.x = entry.startX + (entry.homeX - entry.startX) * strength; + point.y = entry.startY + (entry.homeY - entry.startY) * strength; + this.updateCanvasPointPolar(point); + } + + if (progress >= 1) { + for (const entry of state.items) { + const point = entry.item.point; + point.x = entry.homeX; + point.y = entry.homeY; + this.updateCanvasPointPolar(point); + } + this.canvasSpringAnimationFrame = null; + this.canvasSpringState = null; + this.requestCanvasDraw("full"); + return; + } + + this.canvasInteractionUntil = performance.now() + 120; + this.drawCanvasGraph({ mode: "interactive" }); + this.canvasSpringAnimationFrame = window.requestAnimationFrame(tick); + }; + + this.canvasSpringAnimationFrame = window.requestAnimationFrame(tick); + } + + cancelCanvasSpringBack() { + if (this.canvasSpringAnimationFrame) { + window.cancelAnimationFrame(this.canvasSpringAnimationFrame); + this.canvasSpringAnimationFrame = null; + } + this.canvasSpringState = null; + } + + canvasEventPoint(evt) { + const rect = this.canvas.getBoundingClientRect(); + return { + x: evt.clientX - rect.left, + y: evt.clientY - rect.top + }; + } + + screenToWorld(point) { + const viewport = this.canvasViewport(); + const zoom = clampFloat(this.state.zoom, this.canvasMinZoom(viewport), this.canvasMaxZoom(viewport), 1); + return { + x: (point.x - this.canvasPanX) / zoom, + y: (point.y - this.canvasPanY) / zoom + }; + } + + zoomAtClientPoint(clientX, clientY, deltaY, deltaMode = 0) { + const point = this.canvas ? this.canvasEventPoint({ clientX, clientY }) : null; + const viewport = this.canvasViewport(); + const minZoom = this.canvasMinZoom(viewport); + const maxZoom = this.canvasMaxZoom(viewport); + const previousZoom = clampFloat(this.state.zoom, minZoom, maxZoom, 1); + const normalizedDelta = normalizeWheelDeltaY(deltaY, deltaMode); + const factor = Math.pow(ZOOM_WHEEL_BASE, -normalizedDelta / 120); + const baseZoom = Number.isFinite(this.canvasTargetZoom) ? this.canvasTargetZoom : previousZoom; + this.startCanvasZoomAnimation(baseZoom * factor, point); + } + + startCanvasZoomAnimation(nextZoom, anchorPoint = null) { + const viewport = this.canvasViewport(); + const minZoom = this.canvasMinZoom(viewport); + const maxZoom = this.canvasMaxZoom(viewport); + this.canvasTargetZoom = clampFloat(nextZoom, minZoom, maxZoom, this.state.zoom); + this.canvasZoomAnchorPoint = anchorPoint || { x: viewport.width / 2, y: viewport.height / 2 }; + this.canvasInteractionUntil = performance.now() + 180; + if (!this.canvasZoomAnimationFrame) { + this.canvasZoomAnimationFrame = window.requestAnimationFrame(() => this.stepCanvasZoomAnimation()); + } + } + + stepCanvasZoomAnimation() { + this.canvasZoomAnimationFrame = null; + if (!this.canvas || !this.lastCanvasBundle || !Number.isFinite(this.canvasTargetZoom)) { + this.canvasTargetZoom = null; + this.canvasZoomAnchorPoint = null; + return; + } + + const viewport = this.canvasViewport(); + const minZoom = this.canvasMinZoom(viewport); + const maxZoom = this.canvasMaxZoom(viewport); + const previousZoom = clampFloat(this.state.zoom, minZoom, maxZoom, 1); + const targetZoom = clampFloat(this.canvasTargetZoom, minZoom, maxZoom, previousZoom); + const zoomDelta = (previousZoom > targetZoom ? previousZoom / targetZoom : targetZoom / previousZoom) - 1; + const done = zoomDelta < 0.006; + const zoom = done + ? targetZoom + : previousZoom * ZOOM_ANIMATION_FRICTION + targetZoom * (1 - ZOOM_ANIMATION_FRICTION); + const point = this.canvasZoomAnchorPoint || { x: viewport.width / 2, y: viewport.height / 2 }; + const world = { + x: (point.x - this.canvasPanX) / previousZoom, + y: (point.y - this.canvasPanY) / previousZoom + }; + + this.state.zoom = clampFloat(zoom, minZoom, maxZoom, previousZoom); + this.canvasPanX = point.x - world.x * this.state.zoom; + this.canvasPanY = point.y - world.y * this.state.zoom; + if (done) this.syncZoomControls(); + this.saveViewportState(); + this.canvasInteractionUntil = performance.now() + 120; + this.drawCanvasGraph({ mode: "interactive" }); + + if (done) { + this.canvasTargetZoom = null; + this.canvasZoomAnchorPoint = null; + this.scheduleSettledCanvasDraw(120); + return; + } + + this.canvasZoomAnimationFrame = window.requestAnimationFrame(() => this.stepCanvasZoomAnimation()); + } + + cancelCanvasZoomAnimation() { + if (this.canvasZoomAnimationFrame) { + window.cancelAnimationFrame(this.canvasZoomAnimationFrame); + this.canvasZoomAnimationFrame = null; + } + this.canvasTargetZoom = null; + this.canvasZoomAnchorPoint = null; + } + + syncZoomControls() { + const viewport = this.canvasViewport(); + const minZoom = this.canvasMinZoom(viewport); + const maxZoom = this.canvasMaxZoom(viewport); + this.state.zoom = clampFloat(this.state.zoom, minZoom, maxZoom, 1); + if (this.zoomInput) { + this.zoomInput.min = "0"; + this.zoomInput.max = String(ZOOM_SLIDER_STEPS); + this.zoomInput.value = zoomToSliderValue(this.state.zoom, minZoom, maxZoom); + } + if (this.zoomLabel) this.zoomLabel.textContent = `${Math.round(this.state.zoom * 100)}%`; + } + + setCanvasZoom(nextZoom, anchorPoint = null, redraw = true) { + this.cancelCanvasZoomAnimation(); + const viewport = this.canvasViewport(); + const minZoom = this.canvasMinZoom(viewport); + const maxZoom = this.canvasMaxZoom(viewport); + const previousZoom = clampFloat(this.state.zoom, minZoom, maxZoom, 1); + const zoom = clampFloat(nextZoom, minZoom, maxZoom, previousZoom); + if (Math.abs(zoom - previousZoom) < 0.0001) { + this.syncZoomControls(); + return; + } + + const point = anchorPoint || { x: viewport.width / 2, y: viewport.height / 2 }; + if (this.canvas && this.lastCanvasBundle) { + const world = { + x: (point.x - this.canvasPanX) / previousZoom, + y: (point.y - this.canvasPanY) / previousZoom + }; + this.canvasPanX = point.x - world.x * zoom; + this.canvasPanY = point.y - world.y * zoom; + } + + this.state.zoom = zoom; + this.syncZoomControls(); + this.saveViewportState(); + if (redraw && this.canvas && this.lastCanvasBundle) { + this.requestCanvasDraw("interactive"); + this.scheduleSettledCanvasDraw(); + } + else if (redraw) this.render(); + } + + updateCanvasHover(point) { + const hit = this.hitTestCanvas(point, { includeLinks: hoverHighlightsNoteLinks(this.state.hoverHighlightMode) }); + const nextNodeId = hit.node ? hit.node.node.id : null; + const nextLink = hit.link ? hit.link.edge : null; + const sameLink = (!nextLink && !this.hoverLink) || (nextLink && this.hoverLink && nextLink.id === this.hoverLink.id); + if (nextNodeId === this.hoverNodeId && sameLink) return; + + const hasNodeHover = nextNodeId !== null && nextNodeId !== undefined; + this.hoverNodeId = nextNodeId; + this.hoverLink = hasNodeHover ? null : nextLink; + this.graphHost.classList.toggle("is-hovering", hasNodeHover || Boolean(this.hoverLink)); + this.graphHost.classList.toggle("is-pointing", hasNodeHover || Boolean(this.hoverLink)); + if (this.canvasNeedsFastDraw()) { + this.requestCanvasDraw("interactive"); + this.scheduleSettledCanvasDraw(180); + } else { + this.requestCanvasDraw("full"); + } + } + + activateCanvasAt(point, evt) { + const { index, graph } = this.lastCanvasBundle || {}; + if (!index || !graph) return; + const hit = this.hitTestCanvas(point, { includeLinks: true }); + + if (hit.node) { + evt.preventDefault(); + evt.stopPropagation(); + this.state.sidePage = "inspect"; + this.selectedLink = null; + this.selectedNodeId = hit.node.node.id; + this.applyPersistentHighlight(); + if (!this.state.fullscreen) this.renderSidePanel(index, graph, hit.node.node.id); + return; + } + + if (hit.link) { + evt.preventDefault(); + evt.stopPropagation(); + this.state.sidePage = "inspect"; + this.selectedLink = hit.link.edge; + this.selectedNodeId = null; + this.applyPersistentHighlight(); + if (!this.state.fullscreen) this.renderSidePanel(index, graph); + return; + } + + this.clearSelection(index, graph); + } + + hitTestCanvas(point, options = {}) { + if (!this.canvasData) return { node: null, link: null }; + const world = this.screenToWorld(point); + const viewport = this.canvasViewport(); + const zoom = clampFloat(this.state.zoom, this.canvasMinZoom(viewport), this.canvasMaxZoom(viewport), 1); + const nodePad = Math.max(4, 8 / zoom); + const hitNode = this.canvasSwirlMotionActive() + ? hitTestCanvasNodeLinear(this.canvasData, world, nodePad) + : hitTestCanvasNodeIndex(this.canvasData, world, nodePad); + if (hitNode) return { node: hitNode, link: null }; + + if (options.includeLinks) { + const linkPad = Math.max(4, 7 / zoom); + for (let i = this.canvasData.links.length - 1; i >= 0; i -= 1) { + const item = this.canvasData.links[i]; + const distance = distanceToSegment(world, item.sourcePoint, item.targetPoint); + if (distance <= linkPad + item.width * 0.5) return { node: null, link: item }; + } + } + + return { node: null, link: null }; + } + + drawCanvasGraph(options = {}) { + if (!this.canvas || !this.canvasData || !this.lastCanvasBundle) return; + const viewport = this.canvasViewport(); + if (this.pixiRenderer) this.pixiRenderer.resize(viewport); + else this.sizeCanvasToViewport(this.canvas, viewport); + + const palette = this.canvasPalette || this.readCanvasPalette(); + const zoom = clampFloat(this.state.zoom, this.canvasMinZoom(viewport), this.canvasMaxZoom(viewport), 1); + const frameTime = Number.isFinite(options.now) ? options.now : performance.now(); + this.applyCanvasSwirlFrame(frameTime); + const active = this.canvasActiveState(); + const mode = options.mode || (this.dragState || performance.now() < this.canvasInteractionUntil ? "interactive" : "full"); + const interactive = mode === "interactive"; + const activeKey = this.canvasActiveStateKey(active); + const geometryDirty = Boolean( + (this.dragState && this.dragState.mode === "node") + || this.canvasSpringState + || this.canvasSwirlMotionActive() + ); + if ( + this.pixiRenderer + && interactive + && !geometryDirty + && this.pixiRenderer.transformOnly({ + data: this.canvasData, + bundle: this.lastCanvasBundle, + palette, + active, + activeKey, + viewport, + panX: this.canvasPanX, + panY: this.canvasPanY, + zoom, + geometryDirty, + selectedNodeId: this.selectedNodeId, + labelVisibility: this.state.labelVisibility, + mode, + now: frameTime, + allowZoomTransformOnly: true + }) + ) { + return; + } + + if (this.pixiRenderer) { + if (geometryDirty) { + this.updateCanvasDynamicLinkRoutes({ + enabled: this.canvasSwirlMotionActive(), + includeBaseLinks: true, + highlightedHoverLinks: [] + }); + } + if (this.lastCanvasBundle?.layout) { + this.lastCanvasBundle.layout.spinStartedAt = this.canvasSwirlStartedAt || 0; + this.lastCanvasBundle.layout.spinSpeed = this.canvasSwirlAmount(); + } + this.pixiRenderer.render({ + data: this.canvasData, + bundle: this.lastCanvasBundle, + palette, + active, + activeKey, + viewport, + panX: this.canvasPanX, + panY: this.canvasPanY, + zoom, + mode, + geometryDirty, + selectedNodeId: this.selectedNodeId, + labelVisibility: this.state.labelVisibility, + now: frameTime + }); + return; + } + + const fastInteractive = interactive && this.canvasNeedsFastDraw(); + const visuals = this.updateCanvasVisualState(active, zoom, { immediate: interactive }); + const bounds = canvasWorldBounds( + viewport, + this.canvasPanX, + this.canvasPanY, + zoom, + this.pixiRenderer ? (interactive ? 900 : 520) : (interactive ? 120 : 220) + ); + const highlightedHoverLinks = active.highlightedEdges.size + ? Array.from(active.highlightedEdges) + .map(key => this.canvasData.edgesById.get(key)) + .filter(item => item && item.hoverOnly) + : []; + this.updateCanvasDynamicLinkRoutes({ + enabled: this.canvasSwirlMotionActive(), + includeBaseLinks: !fastInteractive, + highlightedHoverLinks + }); + + const ctx = this.canvas.getContext("2d"); + ctx.setTransform(this.canvasDpr, 0, 0, this.canvasDpr, 0, 0); + ctx.clearRect(0, 0, viewport.width, viewport.height); + this.drawCanvasBackground(ctx, palette, viewport); + + ctx.save(); + ctx.translate(this.canvasPanX, this.canvasPanY); + ctx.scale(zoom, zoom); + this.drawCanvasRings(ctx, this.lastCanvasBundle.layout, palette, zoom, { underlay: true, interactive, now: frameTime }); + if (!fastInteractive) { + this.drawCanvasEdges(ctx, this.canvasData.links, visuals, palette, zoom, false, { bounds, interactive }); + } + this.drawCanvasEdges(ctx, this.canvasData.hierarchy, visuals, palette, zoom, true, { bounds, interactive }); + if (!interactive) this.drawCanvasRings(ctx, this.lastCanvasBundle.layout, palette, zoom, { overlay: true, now: frameTime }); + this.drawCanvasEdges(ctx, highlightedHoverLinks, visuals, palette, zoom, false, { hoverOnly: true, bounds, interactive }); + this.drawCanvasNodes(ctx, this.canvasData.nodes, visuals, palette, zoom, { bounds, interactive }); + this.drawCanvasLabels(ctx, this.canvasData.nodes, visuals, palette, zoom, { bounds, interactive, fastInteractive }); + ctx.restore(); + } + + canvasActiveStateKey(active) { + if (!active) return ""; + return [ + active.hasActive ? 1 : 0, + active.activeNodeId || "", + active.activeLinkId || "", + this.selectedNodeId || "", + this.selectedLink?.id || "", + this.hoverNodeId || "", + this.hoverLink?.id || "", + active.relatedNodes?.size || 0, + active.highlightedEdges?.size || 0, + active.labelNodes?.size || 0, + active.pinnedNodeIds?.size || 0, + this.pinnedPaths.length, + normalizeLabelVisibility(this.state.labelVisibility) + ].join("|"); + } + + canvasNeedsFastDraw() { + if (!this.canvasData) return false; + const edgeCount = (this.canvasData.links?.length || 0) + (this.canvasData.hierarchy?.length || 0); + return this.state.showCompleteRoot + || (this.canvasData.nodes?.length || 0) > FAST_CANVAS_NODE_THRESHOLD + || edgeCount > FAST_CANVAS_EDGE_THRESHOLD; + } + + canvasSwirlAmount() { + return clampFloat( + clampNumber(this.state.swirlStrength, 0, MAX_SWIRL_STRENGTH, DEFAULT_SWIRL_STRENGTH) / MAX_SWIRL_STRENGTH, + 0, + 1, + 0 + ); + } + + canvasSwirlMotionActive() { + return this.canvasSwirlAmount() > 0.001; + } + + setSpinSpeed(value, options = {}) { + const previous = this.state.swirlStrength; + const next = clampNumber(value, 0, MAX_SWIRL_STRENGTH, DEFAULT_SWIRL_STRENGTH); + this.state.swirlStrength = next; + + if (this.canvas && this.canvasData) { + if (next > 0) { + this.startCanvasSwirlAnimation(); + this.requestCanvasDraw("interactive"); + } else { + this.cancelCanvasSwirlAnimation(); + this.resetCanvasSwirlPositions(); + this.requestCanvasDraw("full"); + } + } + + const crossedZero = (previous > 0) !== (next > 0); + if (options.render || crossedZero) this.scheduleRender(options.render ? 0 : 140, { preservePanel: true }); + } + + startCanvasSwirlAnimation() { + this.cancelCanvasSwirlAnimation(false); + if (!this.canvas || !this.canvasData || !this.canvasSwirlMotionActive()) { + this.resetCanvasSwirlPositions(); + return; + } + + if (!this.canvasSwirlStartedAt) this.canvasSwirlStartedAt = performance.now(); + this.canvasSwirlLastFrameAt = 0; + + const tick = now => { + this.canvasSwirlAnimationFrame = null; + if (!this.canvas || !this.canvasData || !this.canvasSwirlMotionActive()) { + this.resetCanvasSwirlPositions(); + return; + } + + const frameInterval = this.canvasNeedsFastDraw() + ? SWIRL_FRAME_INTERVAL_MS * 1.75 + : SWIRL_FRAME_INTERVAL_MS; + if (!this.canvasSwirlLastFrameAt || now - this.canvasSwirlLastFrameAt >= frameInterval) { + this.canvasSwirlLastFrameAt = now; + this.canvasInteractionUntil = Math.max(this.canvasInteractionUntil, performance.now() + 80); + this.drawCanvasGraph({ mode: "spin", now }); + } + + this.canvasSwirlAnimationFrame = window.requestAnimationFrame(tick); + }; + + this.canvasSwirlAnimationFrame = window.requestAnimationFrame(tick); + } + + cancelCanvasSwirlAnimation(reset = true) { + if (this.canvasSwirlAnimationFrame) { + window.cancelAnimationFrame(this.canvasSwirlAnimationFrame); + this.canvasSwirlAnimationFrame = null; + } + this.canvasSwirlLastFrameAt = 0; + if (reset) this.canvasSwirlStartedAt = 0; + } + + applyCanvasSwirlFrame(now = performance.now()) { + if (!this.canvasData || !this.canvasData.nodes || !this.canvasData.nodes.length) return; + const amount = this.canvasSwirlAmount(); + if (amount <= 0.001 || this.dragState || this.canvasSpringState) { + if (amount <= 0.001) this.resetCanvasSwirlPositions(); + return; + } + + if (!this.canvasSwirlStartedAt) this.canvasSwirlStartedAt = now; + const elapsedSeconds = Math.max(0, (now - this.canvasSwirlStartedAt) / 1000); + + for (const item of this.canvasData.nodes) { + if (!item || !item.point) continue; + const point = item.point; + const radius = Number.isFinite(point.homeRadius) ? point.homeRadius : point.radius; + if (!Number.isFinite(radius) || radius <= 0.001) continue; + + const depth = Math.max(0, Math.round(point.depth || 0)); + if (depth === 0) { + point.x = Number.isFinite(point.homeX) ? point.homeX : point.x; + point.y = Number.isFinite(point.homeY) ? point.homeY : point.y; + this.updateCanvasPointPolar(point); + continue; + } + + const centerX = Number.isFinite(point.centerX) ? point.centerX : 0; + const centerY = Number.isFinite(point.centerY) ? point.centerY : 0; + const homeAngle = Number.isFinite(point.homeAngle) ? point.homeAngle : point.angle; + const offset = swirlOrbitAngleForRing(depth, radius, amount, elapsedSeconds); + const angle = normalizeAngle(homeAngle + offset); + point.x = centerX + Math.cos(angle) * radius; + point.y = centerY + Math.sin(angle) * radius; + point.radius = radius; + point.angle = angle; + point.labelSide = labelSideForAngle(angle); + } + } + + resetCanvasSwirlPositions() { + if (!this.canvasData || !this.canvasData.nodes) return; + for (const item of this.canvasData.nodes) { + const point = item?.point; + if (!point || !Number.isFinite(point.homeX) || !Number.isFinite(point.homeY)) continue; + point.x = point.homeX; + point.y = point.homeY; + if (Number.isFinite(point.homeRadius)) point.radius = point.homeRadius; + if (Number.isFinite(point.homeAngle)) point.angle = point.homeAngle; + point.labelSide = labelSideForAngle(point.angle); + } + } + + updateCanvasDynamicLinkRoutes(options = {}) { + if (!this.canvasData) return; + const enabled = Boolean(options.enabled); + const highlightedHoverLinks = Array.isArray(options.highlightedHoverLinks) ? options.highlightedHoverLinks : []; + const items = []; + + if (options.includeBaseLinks !== false) items.push(...(this.canvasData.links || [])); + items.push(...highlightedHoverLinks); + + if (!enabled || !items.length) { + for (const item of items) { + if (item) item.dynamicRoute = null; + } + return; + } + + const layout = this.lastCanvasBundle?.layout; + const ringGap = medianRingGap(layout?.rings || []); + const centerX = Number.isFinite(layout?.centerX) ? layout.centerX : 0; + const centerY = Number.isFinite(layout?.centerY) ? layout.centerY : 0; + const capped = items.length > MAX_DYNAMIC_ROUTE_EDGES + ? items.slice(0, MAX_DYNAMIC_ROUTE_EDGES) + : items; + + for (const item of capped) { + if (!item || !item.sourcePoint || !item.targetPoint) continue; + item.dynamicRoute = dynamicOrbitRouteForEdge(item, centerX, centerY, ringGap); + } + + if (capped.length < items.length) { + for (let index = capped.length; index < items.length; index += 1) { + if (items[index]) items[index].dynamicRoute = null; + } + } + } + + drawCanvasBackground(ctx, palette, viewport) { + ctx.fillStyle = palette.bg; + ctx.fillRect(0, 0, viewport.width, viewport.height); + } + + canvasActiveState() { + const data = this.canvasData; + const rawActiveNodeId = this.hoverNodeId ?? this.selectedNodeId ?? null; + const hoverMode = normalizeHoverHighlightMode(this.state.hoverHighlightMode); + const resolveCanvasNodeId = nodeId => { + if (nodeId === null || nodeId === undefined) return null; + if (data.nodesById.has(nodeId)) return nodeId; + const visualNodeId = this.plugin.index.visualNodeId(nodeId); + return visualNodeId !== null && visualNodeId !== undefined && data.nodesById.has(visualNodeId) + ? visualNodeId + : null; + }; + let activeNodeId = resolveCanvasNodeId(rawActiveNodeId); + let activeLinkId = this.hoverLink?.id || this.selectedLink?.id || null; + if (activeLinkId && !data.edgesById.has(activeLinkId)) activeLinkId = null; + const relatedNodes = new Set(); + const highlightedEdges = new Set(); + const labelNodes = new Set(); + const pinnedNodeIds = new Set(); + let hasPinnedActive = false; + let hasActiveLink = false; + + const addNodeHighlight = (nodeId, mode, pinned = false) => { + const visualNodeId = resolveCanvasNodeId(nodeId); + if (visualNodeId === null || visualNodeId === undefined) return null; + relatedNodes.add(visualNodeId); + labelNodes.add(visualNodeId); + if (pinned) pinnedNodeIds.add(visualNodeId); + if (hoverHighlightsNoteLinks(mode)) { + for (const edge of data.linkEdgesByNode.get(visualNodeId) || []) { + highlightedEdges.add(edge.key); + if (edge.source !== null && edge.source !== undefined) relatedNodes.add(edge.source); + if (edge.target !== null && edge.target !== undefined) relatedNodes.add(edge.target); + if (edge.source !== null && edge.source !== undefined) labelNodes.add(edge.source); + if (edge.target !== null && edge.target !== undefined) labelNodes.add(edge.target); + } + } + addHierarchyHoverHighlights(data, visualNodeId, mode, relatedNodes, highlightedEdges, labelNodes); + return visualNodeId; + }; + + const addLinkHighlight = (edgeId, fallbackEdge) => { + let edge = edgeId ? data.edgesById.get(edgeId) : null; + if (!edge && fallbackEdge) { + edge = this.findCanvasEdgeForPin({ + kind: "link", + edgeId: fallbackEdge.id, + source: fallbackEdge.source, + target: fallbackEdge.target + }); + } + if (!edge) return null; + highlightedEdges.add(edge.key); + if (edge.source !== null && edge.source !== undefined) { + relatedNodes.add(edge.source); + labelNodes.add(edge.source); + pinnedNodeIds.add(edge.source); + } + if (edge.target !== null && edge.target !== undefined) { + relatedNodes.add(edge.target); + labelNodes.add(edge.target); + pinnedNodeIds.add(edge.target); + } + return edge.key; + }; + + if (activeNodeId !== null && activeNodeId !== undefined) { + addNodeHighlight(activeNodeId, hoverMode, false); + } + + if (activeLinkId || this.hoverLink || this.selectedLink) { + hasActiveLink = addLinkHighlight(activeLinkId, this.hoverLink || this.selectedLink) !== null; + } + + for (const pin of this.pinnedPaths) { + if (pin.kind === "node") { + if (addNodeHighlight(pin.nodeId, pin.mode, true) !== null) hasPinnedActive = true; + } else if (pin.kind === "link") { + if (addLinkHighlight(pin.edgeId, pin) !== null) hasPinnedActive = true; + } + } + + for (const node of data.searchMatchItems || []) { + if (node.searchMatch) labelNodes.add(node.node.id); + } + + return { + hasActive: activeNodeId !== null && activeNodeId !== undefined || hasActiveLink || hasPinnedActive, + activeNodeId, + activeLinkId, + relatedNodes, + highlightedEdges, + labelNodes, + pinnedNodeIds + }; + } + + updateCanvasVisualState(active, zoom = 1, options = {}) { + const now = performance.now(); + const delta = this.lastCanvasFrameAt ? Math.min(80, now - this.lastCanvasFrameAt) : 16; + this.lastCanvasFrameAt = now; + const immediate = Boolean(options.immediate); + const step = immediate + ? 1 + : this.canvasVisualInitialized + ? clampFloat(delta / 72, 0.16, 0.86, 0.32) + : 1; + let needsFrame = false; + + const updateValue = (current, target) => { + if (!this.canvasVisualInitialized) return target; + const next = current + (target - current) * step; + if (!immediate && Math.abs(next - target) > 0.012) needsFrame = true; + return Math.abs(next - target) < 0.006 ? target : next; + }; + + const currentNodeIds = new Set(); + const hoverOnlyLabels = normalizeLabelVisibility(this.state.labelVisibility) === "hover"; + for (const item of this.canvasData.nodes) { + const nodeId = item.node.id; + currentNodeIds.add(nodeId); + const isFocused = nodeId === active.activeNodeId + || nodeId === this.selectedNodeId + || active.pinnedNodeIds.has(nodeId) + || nodeId === this.lastCanvasBundle.graph.focusId + || item.searchMatch; + const isRelated = active.relatedNodes.has(nodeId); + const directLabel = nodeId === active.activeNodeId + || nodeId === this.selectedNodeId + || active.pinnedNodeIds.has(nodeId) + || item.searchMatch; + const explicitLabel = directLabel || (!hoverOnlyLabels && active.labelNodes.has(nodeId)); + const target = { + focus: isFocused ? 1 : 0, + related: active.hasActive && isRelated && !isFocused ? 1 : 0, + dim: active.hasActive && !isRelated && !isFocused ? 1 : 0, + label: hoverOnlyLabels + ? (explicitLabel ? 1 : 0) + : Math.max(active.labelNodes.has(nodeId) ? 1 : 0, zoomLabelStrength(item, zoom, this.lastCanvasBundle.graph)) + }; + const current = this.canvasVisualNodes.get(nodeId) || { focus: 0, related: 0, dim: 0, label: 0 }; + this.canvasVisualNodes.set(nodeId, { + focus: updateValue(current.focus, target.focus), + related: updateValue(current.related, target.related), + dim: updateValue(current.dim, target.dim), + label: updateValue(current.label, target.label) + }); + } + + for (const id of Array.from(this.canvasVisualNodes.keys())) { + if (!currentNodeIds.has(id)) this.canvasVisualNodes.delete(id); + } + + const visibleHoverLinks = active.highlightedEdges.size + ? Array.from(active.highlightedEdges) + .map(key => this.canvasData.edgesById.get(key)) + .filter(item => item && item.hoverOnly) + : []; + const currentEdgeKeys = new Set(); + const updateEdge = item => { + if (!item) return; + currentEdgeKeys.add(item.key); + const target = { + highlight: active.highlightedEdges.has(item.key) ? 1 : 0, + dim: active.hasActive && !active.highlightedEdges.has(item.key) ? 1 : 0 + }; + const current = this.canvasVisualEdges.get(item.key) || { highlight: 0, dim: 0 }; + this.canvasVisualEdges.set(item.key, { + highlight: updateValue(current.highlight, target.highlight), + dim: updateValue(current.dim, target.dim) + }); + }; + for (const item of this.canvasData.hierarchy) updateEdge(item); + for (const item of this.canvasData.links) updateEdge(item); + for (const item of visibleHoverLinks) updateEdge(item); + + for (const key of Array.from(this.canvasVisualEdges.keys())) { + if (!currentEdgeKeys.has(key)) this.canvasVisualEdges.delete(key); + } + + this.canvasVisualInitialized = true; + if (needsFrame) this.scheduleCanvasAnimation(); + + return { + nodes: this.canvasVisualNodes, + edges: this.canvasVisualEdges + }; + } + + scheduleCanvasAnimation() { + this.requestCanvasDraw("full"); + } + + requestCanvasDraw(mode = "full") { + if (mode === "interactive") { + this.canvasInteractionUntil = performance.now() + 120; + } + + if (this.canvasAnimationFrame) { + this.nextCanvasDrawMode = mode === "full" ? "full" : "interactive"; + return; + } + + this.nextCanvasDrawMode = mode; + this.canvasAnimationFrame = window.requestAnimationFrame(() => { + const drawMode = this.nextCanvasDrawMode || "full"; + this.canvasAnimationFrame = null; + this.nextCanvasDrawMode = "full"; + this.drawCanvasGraph({ mode: drawMode }); + }); + } + + scheduleSettledCanvasDraw(delay = 140) { + if (this.canvasSettleTimer) window.clearTimeout(this.canvasSettleTimer); + this.canvasSettleTimer = window.setTimeout(() => { + this.canvasSettleTimer = null; + this.requestCanvasDraw("full"); + }, delay); + } + + drawCanvasRings(ctx, layout, palette, zoom, options = {}) { + if (!layout || !Array.isArray(layout.rings) || !layout.rings.length) return; + const centerX = Number.isFinite(layout.centerX) ? layout.centerX : layout.width / 2; + const centerY = Number.isFinite(layout.centerY) ? layout.centerY : layout.height / 2; + const alphaScale = options.overlay ? 1.16 : options.underlay ? 0.58 : 1; + const swirlStrength = clampFloat(layout.swirlStrength, 0, 1, 0); + const spinSpeed = this.canvasSwirlAmount(); + const elapsedSeconds = this.canvasSwirlStartedAt && Number.isFinite(options.now) + ? Math.max(0, (options.now - this.canvasSwirlStartedAt) / 1000) + : 0; + + ctx.save(); + ctx.strokeStyle = palette.ringGuide || palette.folderRing || palette.line; + + for (const ring of layout.rings) { + if (!Number.isFinite(ring.radius) || ring.radius <= 0) continue; + const density = Math.min(1, Math.sqrt(Math.max(1, ring.count || 1)) / 9); + const ringPhase = swirlOrbitAngleForRing(ring.depth, ring.radius, spinSpeed, elapsedSeconds); + if (options.overlay) { + ctx.setLineDash([]); + ctx.globalAlpha = (0.035 + density * 0.025) * alphaScale; + ctx.lineWidth = Math.max(4.5 / zoom, 6.5 / zoom); + ctx.beginPath(); + drawCanvasRingPath(ctx, centerX, centerY, ring.radius, ring.depth, swirlStrength, ringPhase); + ctx.stroke(); + } + + ctx.setLineDash([options.overlay ? 5 / zoom : 3 / zoom, options.overlay ? 6 / zoom : 11 / zoom]); + ctx.globalAlpha = (0.13 + density * 0.09) * alphaScale; + ctx.lineWidth = Math.max((options.overlay ? 1.05 : 0.55) / zoom, (options.overlay ? 1.55 : 0.86) / zoom); + ctx.beginPath(); + drawCanvasRingPath(ctx, centerX, centerY, ring.radius, ring.depth, swirlStrength, ringPhase); + ctx.stroke(); + } + + ctx.restore(); + } + + drawCanvasEdges(ctx, edges, visuals, palette, zoom, hierarchy, options = {}) { + const hoverOnly = Boolean(options.hoverOnly); + const bounds = options.bounds || null; + const interactive = Boolean(options.interactive); + for (const item of edges) { + if (bounds && !edgeItemInBounds(item, bounds)) continue; + const visual = visuals.edges.get(item.key) || { highlight: 0, dim: 0 }; + if (hoverOnly && visual.highlight <= 0.01) continue; + const unresolved = item.edge && item.edge.unresolvedCount; + const external = item.external || (item.edge && item.edge.externalCount); + const externalHierarchy = hierarchy && external; + const outsideLink = !hierarchy && external; + const color = unresolved + ? palette.unresolved + : outsideLink + ? (palette.externalLink || palette.external) + : hierarchy + ? (palette.tree || palette.folderRing || palette.line) + : palette.link; + const spinLinkFade = !hierarchy && !hoverOnly && this.canvasSwirlMotionActive() ? 0.58 : 1; + const baseAlpha = (hierarchy + ? (externalHierarchy ? 0.18 : 0.34) + : (outsideLink ? 0.082 : 0.056)) * spinLinkFade; + const minAlpha = (hierarchy ? 0.09 : outsideLink ? 0.03 : 0.024) * spinLinkFade; + const dimFade = hierarchy ? 0.48 : 0.58; + const width = hierarchy + ? (externalHierarchy ? 1.12 : 1.48) + : (outsideLink ? item.width * 0.72 : item.width * 0.7); + const applyDash = () => { + ctx.setLineDash([]); + if (unresolved) ctx.setLineDash([7 / zoom, 5 / zoom]); + else if (outsideLink) ctx.setLineDash([8 / zoom, 7 / zoom]); + else if (externalHierarchy) ctx.setLineDash([2.5 / zoom, 5 / zoom]); + }; + + ctx.save(); + if (!hoverOnly) { + ctx.globalAlpha = Math.max(minAlpha, baseAlpha * (1 - visual.dim * dimFade)); + ctx.strokeStyle = color; + ctx.lineWidth = Math.max((outsideLink ? 0.58 : 0.45) / zoom, width / zoom); + ctx.lineCap = "round"; + if (!interactive || outsideLink || unresolved) applyDash(); + else ctx.setLineDash([]); + ctx.beginPath(); + this.drawCanvasEdgePath(ctx, item); + ctx.stroke(); + } + if (visual.highlight > 0.01) { + applyDash(); + ctx.globalAlpha = visual.highlight * (hierarchy ? 0.78 : outsideLink ? 0.76 : hoverOnly ? 0.74 : 0.88); + ctx.strokeStyle = outsideLink ? (palette.externalLink || palette.external) : (palette.lineHighlight || palette.focus); + ctx.lineWidth = Math.max(0.7 / zoom, (hierarchy ? 2.2 : outsideLink ? 2.1 : hoverOnly ? 1.9 : 2.35) / zoom); + ctx.shadowColor = outsideLink ? (palette.externalLink || palette.external) : palette.glow; + ctx.shadowBlur = (outsideLink ? 6 : 8) / zoom; + ctx.beginPath(); + this.drawCanvasEdgePath(ctx, item); + ctx.stroke(); + if (!hierarchy && this.canvasData?.showArrow) { + this.drawCanvasArrow(ctx, item, zoom, outsideLink ? (palette.externalLink || palette.external) : (palette.lineHighlight || palette.focus)); + } + } + ctx.restore(); + } + } + + drawCanvasEdgePath(ctx, item) { + const source = item.sourcePoint; + const target = item.targetPoint; + const route = item.dynamicRoute || item.route; + + ctx.moveTo(source.x, source.y); + + if (route && (route.kind === "outer" || route.kind === "external" || route.kind === "dynamic-orbit")) { + const startAngle = route.sourceAngle; + const endAngle = Number.isFinite(route.endAngle) ? route.endAngle : route.targetAngle; + const arcStart = radialPoint(route.centerX, route.centerY, route.radius, startAngle); + const arcEnd = radialPoint(route.centerX, route.centerY, route.radius, endAngle); + ctx.lineTo(arcStart.x, arcStart.y); + ctx.arc(route.centerX, route.centerY, route.radius, startAngle, endAngle, endAngle < startAngle); + ctx.lineTo(arcEnd.x, arcEnd.y); + ctx.lineTo(target.x, target.y); + return; + } + + if (route && route.kind === "curve") { + const centerX = Number.isFinite(source.centerX) ? source.centerX : (source.x + target.x) / 2; + const centerY = Number.isFinite(source.centerY) ? source.centerY : (source.y + target.y) / 2; + const midX = (source.x + target.x) / 2; + const midY = (source.y + target.y) / 2; + const pull = Number.isFinite(route.curveStrength) ? route.curveStrength : (item.external ? 0.28 : 0.16); + ctx.quadraticCurveTo( + midX + (centerX - midX) * pull, + midY + (centerY - midY) * pull, + target.x, + target.y + ); + return; + } + + ctx.lineTo(target.x, target.y); + } + + drawCanvasArrow(ctx, item, zoom, color) { + const source = this.canvasArrowSourcePoint(item); + const target = item.targetPoint; + const dx = target.x - source.x; + const dy = target.y - source.y; + const length = Math.hypot(dx, dy); + if (length < 0.001) return; + + const targetNode = this.canvasData?.nodesById?.get(item.target); + const targetRadius = targetNode ? targetNode.radius : 6; + const angle = Math.atan2(dy, dx); + const tipOffset = targetRadius + 2 / zoom; + const tipX = target.x - Math.cos(angle) * tipOffset; + const tipY = target.y - Math.sin(angle) * tipOffset; + const size = 7 / zoom; + const spread = 0.48; + + ctx.save(); + ctx.fillStyle = color; + ctx.shadowBlur = 0; + ctx.beginPath(); + ctx.moveTo(tipX, tipY); + ctx.lineTo( + tipX - Math.cos(angle - spread) * size, + tipY - Math.sin(angle - spread) * size + ); + ctx.lineTo( + tipX - Math.cos(angle + spread) * size, + tipY - Math.sin(angle + spread) * size + ); + ctx.closePath(); + ctx.fill(); + ctx.restore(); + } + + canvasArrowSourcePoint(item) { + const route = item.dynamicRoute || item.route; + if (route && (route.kind === "outer" || route.kind === "external" || route.kind === "dynamic-orbit")) { + const endAngle = Number.isFinite(route.endAngle) ? route.endAngle : route.targetAngle; + return radialPoint(route.centerX, route.centerY, route.radius, endAngle); + } + + if (route && route.kind === "curve") { + const source = item.sourcePoint; + const target = item.targetPoint; + const centerX = Number.isFinite(source.centerX) ? source.centerX : (source.x + target.x) / 2; + const centerY = Number.isFinite(source.centerY) ? source.centerY : (source.y + target.y) / 2; + const midX = (source.x + target.x) / 2; + const midY = (source.y + target.y) / 2; + const pull = Number.isFinite(route.curveStrength) ? route.curveStrength : (item.external ? 0.28 : 0.16); + return { + x: midX + (centerX - midX) * pull, + y: midY + (centerY - midY) * pull + }; + } + + return item.sourcePoint; + } + + drawCanvasNodes(ctx, nodes, visuals, palette, zoom, options = {}) { + const bounds = options.bounds || null; + for (const item of nodes) { + if (bounds && !circleInBounds(item.point, item.radius + 6 / zoom, bounds)) continue; + const nodeId = item.node.id; + const visual = visuals.nodes.get(nodeId) || { focus: 0, related: 0, dim: 0, label: 0 }; + const rootNode = nodeId === this.lastCanvasBundle.graph.rootId; + const folderNode = item.node.type === "folder"; + const folderWithMeta = folderNode && Boolean(item.node.representativeFile); + const externalGroup = item.node.type === "external" && !item.node.externalProxy; + const externalFile = Boolean(item.node.externalProxy); + const unresolvedNode = item.node.type === "unresolved"; + const alpha = clampFloat(0.9 - visual.dim * 0.58 + visual.related * 0.1, 0.24, 1, 0.9); + const fill = unresolvedNode + ? palette.unresolved + : externalGroup + ? palette.node + : folderWithMeta + ? palette.folderMeta + : folderNode + ? palette.folder + : (palette.note || palette.node); + const stroke = rootNode + ? palette.circle + : unresolvedNode + ? palette.unresolved + : externalGroup || externalFile + ? palette.external + : folderNode + ? palette.folderRing + : palette.fileRing || palette.stroke; + + ctx.save(); + ctx.globalAlpha = externalGroup ? alpha * 0.82 : alpha; + ctx.shadowColor = palette.glow; + ctx.shadowBlur = (visual.focus * 13 + (rootNode ? 3 : 0)) / zoom; + ctx.fillStyle = fill; + ctx.beginPath(); + ctx.arc(item.point.x, item.point.y, item.radius, 0, Math.PI * 2); + ctx.fill(); + if (visual.focus > 0.01) { + ctx.globalAlpha = visual.focus * 0.96; + ctx.fillStyle = palette.focus; + ctx.beginPath(); + ctx.arc(item.point.x, item.point.y, item.radius, 0, Math.PI * 2); + ctx.fill(); + } + ctx.shadowBlur = 0; + ctx.globalAlpha = clampFloat(alpha + visual.focus * 0.28, 0.12, 1, alpha); + ctx.strokeStyle = visual.focus > 0.08 ? palette.circle : stroke; + ctx.lineWidth = (0.85 + visual.focus * 1.35 + (rootNode ? 0.55 : folderNode ? 0.22 : 0)) / zoom; + if (externalGroup || externalFile || unresolvedNode) ctx.setLineDash([3 / zoom, 4 / zoom]); + ctx.stroke(); + ctx.setLineDash([]); + ctx.restore(); + } + } + + drawCanvasLabels(ctx, nodes, visuals, palette, zoom, options = {}) { + const fontSize = 12; + const lineHeight = 14; + const screenScale = labelScreenScale(zoom); + const bounds = options.bounds || null; + const interactive = Boolean(options.interactive); + const fastInteractive = Boolean(options.fastInteractive); + ctx.save(); + ctx.textBaseline = "top"; + ctx.textAlign = "center"; + ctx.lineJoin = "round"; + + let labelItems = interactive + ? (this.canvasData?.interactiveLabelItems || nodes) + : (this.canvasData?.labelItems || nodes); + if (interactive) { + const extraIds = [this.selectedNodeId, this.hoverNodeId, this.lastCanvasBundle?.graph?.focusId, ...this.pinnedCanvasLabelIds()] + .filter(id => id !== null && id !== undefined); + if (extraIds.length) { + const seen = new Set(labelItems.map(item => item.node.id)); + const extras = extraIds + .filter(id => !seen.has(id) && this.canvasData?.nodesById?.has(id)) + .map(id => this.canvasData.nodesById.get(id)); + if (extras.length) labelItems = labelItems.concat(extras); + } + } + + const pinnedLabelIds = new Set(this.pinnedCanvasLabelIds()); + for (const item of labelItems) { + if (bounds && !circleInBounds(item.point, item.radius + 180 / zoom, bounds)) continue; + if ( + interactive + && !item.searchMatch + && item.labelRank > FAST_CANVAS_LABEL_LIMIT + && item.node.id !== this.selectedNodeId + && item.node.id !== this.hoverNodeId + && !pinnedLabelIds.has(item.node.id) + ) continue; + const visual = visuals.nodes.get(item.node.id) || { label: 0, focus: 0 }; + if (visual.label <= 0.02) continue; + if (fastInteractive && visual.focus <= 0.02 && !item.searchMatch && item.labelRank > 36) continue; + const rootNode = item.node.id === this.lastCanvasBundle.graph.rootId; + const folderNode = item.node.type === "folder"; + const leading = Number.isFinite(item.labelRank) && item.labelRank < 36; + const localScale = screenScale * (rootNode ? 1.18 : item.radius >= 24 ? 1.1 : item.radius >= 15 ? 1.04 : 1); + const weight = rootNode || leading || folderNode ? 600 : 400; + ctx.font = `${weight} ${(fontSize * localScale) / zoom}px ${palette.fontFamily}`; + const maxWidth = ((rootNode ? 190 : folderNode ? 156 : leading ? 146 : 124) * localScale) / zoom; + const maxLines = rootNode || visual.focus > 0.5 ? 4 : 3; + const lines = wrapCanvasLabel(ctx, item.label, maxWidth, maxLines); + if (!lines.length) continue; + + const x = item.point.x; + const y = item.point.y + item.radius + (8 * localScale) / zoom; + const lineHeightWorld = (lineHeight * localScale) / zoom; + ctx.globalAlpha = visual.label * (rootNode ? 0.98 : leading ? 0.95 : 0.9); + ctx.lineWidth = ((rootNode ? 6 : 5) * localScale) / zoom; + ctx.strokeStyle = palette.labelStroke || palette.bg; + for (let index = 0; index < lines.length; index += 1) { + ctx.strokeText(lines[index], x, y + index * lineHeightWorld); + } + ctx.fillStyle = rootNode ? (palette.circle || palette.labelText || palette.text) : (palette.labelText || palette.text); + for (let index = 0; index < lines.length; index += 1) { + ctx.fillText(lines[index], x, y + index * lineHeightWorld); + } + } + + ctx.restore(); + } + + installPanelResize() { + if (!this.splitter) return; + this.splitter.addEventListener("mousedown", evt => { + evt.preventDefault(); + const startX = evt.clientX; + const startWidth = this.state.sidePanelWidth; + this.bodyEl.classList.add("is-resizing"); + + const onMove = moveEvt => { + const delta = moveEvt.clientX - startX; + this.state.sidePanelWidth = clampNumber(startWidth - delta, 220, 720, 360); + this.bodyEl.style.setProperty("--mwm-side-width", `${this.state.sidePanelWidth}px`); + }; + + const onUp = () => { + this.bodyEl.classList.remove("is-resizing"); + window.removeEventListener("mousemove", onMove); + window.removeEventListener("mouseup", onUp); + }; + + window.addEventListener("mousemove", onMove); + window.addEventListener("mouseup", onUp); + }); + } + + adjustZoom(delta, rerender = true) { + this.cancelCanvasZoomAnimation(); + const viewport = this.canvasViewport(); + const minZoom = this.canvasMinZoom(viewport); + const maxZoom = this.canvasMaxZoom(viewport); + const previousZoom = clampFloat(this.state.zoom, minZoom, maxZoom, 1); + this.state.zoom = clampFloat(previousZoom * Math.pow(ZOOM_BUTTON_STEP, delta), minZoom, maxZoom, 1); + if (!rerender) return; + if (this.canvas && this.lastCanvasBundle) { + const center = { x: viewport.width / 2, y: viewport.height / 2 }; + const world = { + x: (center.x - this.canvasPanX) / previousZoom, + y: (center.y - this.canvasPanY) / previousZoom + }; + this.canvasPanX = center.x - world.x * this.state.zoom; + this.canvasPanY = center.y - world.y * this.state.zoom; + this.syncZoomControls(); + this.requestCanvasDraw("interactive"); + this.scheduleSettledCanvasDraw(); + return; + } + this.render(); + } + + fitToView() { + this.cancelCanvasZoomAnimation(); + if (!this.lastLayout || !this.graphHost) return; + const viewport = this.canvasViewport(); + this.state.zoom = clampFloat( + fitZoomForLayout(this.lastLayout, viewport, 32), + this.canvasMinZoom(viewport, this.lastLayout), + this.canvasMaxZoom(viewport, this.lastLayout), + 1 + ); + if (this.canvas && this.lastCanvasBundle) { + this.centerCanvasView(this.lastLayout, viewport, null); + this.syncZoomControls(); + this.requestCanvasDraw("full"); + return; + } + this.render(); + } + + resetViewZoom() { + this.cancelCanvasZoomAnimation(); + this.viewInitialized = false; + this.state.zoom = 1; + this.render(); + } + + registerEdgeElement(element, edge) { + if (!element || !edge) return; + if (edge.id) this.edgeElementsById.set(edge.id, element); + for (const id of [edge.source, edge.target]) { + if (!id) continue; + if (!this.edgeElementsByNode.has(id)) this.edgeElementsByNode.set(id, []); + this.edgeElementsByNode.get(id).push(element); + } + } + + markHighlight(element, className) { + if (!element) return; + element.classList.add(className); + this.activeHighlightElements.add(element); + } + + setHoverNode(nodeId) { + this.hoverNodeId = nodeId; + this.hoverLink = null; + if (this.graphHost) this.graphHost.classList.add("is-hovering"); + this.requestCanvasDraw("full"); + } + + highlightNode(nodeId) { + this.setHoverNode(nodeId); + } + + setHoverLink(edge, element) { + this.hoverNodeId = null; + this.hoverLink = edge || null; + if (this.graphHost) this.graphHost.classList.toggle("is-hovering", Boolean(edge)); + this.requestCanvasDraw("full"); + } + + highlightLink(edge, element) { + this.setHoverLink(edge); + } + + clearHover() { + this.clearHoverClasses(); + if (this.graphHost) this.graphHost.classList.remove("is-hovering"); + if (this.graphHost) this.graphHost.classList.remove("is-pointing"); + this.hoverNodeId = null; + this.hoverLink = null; + this.requestCanvasDraw("full"); + } + + clearHoverClasses() { + for (const element of this.activeHighlightElements) { + element.classList.remove("is-highlighted", "is-related", "is-hovered"); + } + this.activeHighlightElements.clear(); + } + + findLinkElement(edge) { + if (!edge) return null; + return this.edgeElementsById.get(edge.id) || null; + } + + applyPersistentHighlight() { + if (!this.graphHost) return; + this.clearHoverClasses(); + this.graphHost.classList.toggle( + "is-hovering", + this.hoverNodeId !== null && this.hoverNodeId !== undefined || Boolean(this.hoverLink) + ); + this.requestCanvasDraw("full"); + } + + showNodeMenu(evt, node) { + const menu = new Menu(); + if (node.type === "note") { + menu.addItem(item => item + .setTitle("Open note") + .setIcon("file-text") + .onClick(() => this.openNode(node.id, evt))); + menu.addItem(item => item + .setTitle("Focus note") + .setIcon("locate-fixed") + .onClick(() => { + this.state.mode = "focus"; + this.state.focusPath = node.id; + this.state.showCompleteRoot = false; + this.viewInitialized = false; + this.resetAdaptiveTuning(); + this.render(); + })); + } + if (node.type === "folder") { + menu.addItem(item => item + .setTitle("Use as atlas root") + .setIcon("folder-open") + .onClick(() => { + this.state.mode = "atlas"; + this.state.rootPath = node.id; + this.state.showCompleteRoot = false; + this.viewInitialized = false; + this.resetAdaptiveTuning(); + this.render(); + })); + if (node.representativeFile) { + menu.addItem(item => item + .setTitle("Open representative note") + .setIcon("file-text") + .onClick(() => this.openNode(node.representativeFile, evt))); + } + } + menu.showAtMouseEvent(evt); + } + + renderSidePanel(index, graph, forcedNodeId) { + const focusState = this.capturePanelFocus(); + this.sidePanel.empty(); + this.clearControlRefs(); + + if (["detail", "links", "layout", "legend"].includes(this.state.sidePage)) { + this.state.sidePage = "controls"; + } + const pages = this.sidePanelPages(); + if (!pages.some(([id]) => id === this.state.sidePage)) this.state.sidePage = "inspect"; + + const header = this.sidePanel.createDiv({ cls: "mwm-panel-header" }); + const titleWrap = header.createDiv({ cls: "mwm-panel-title-wrap" }); + titleWrap.createDiv({ cls: "mwm-panel-title", text: "Mini World Map" }); + titleWrap.createDiv({ + cls: "mwm-panel-subtitle", + text: `${this.state.mode} - ${graph?.nodes?.length || 0} nodes` + }); + + const tabs = this.sidePanel.createDiv({ cls: "mwm-page-tabs" }); + for (const [pageId, label, icon] of pages) { + const active = this.state.sidePage === pageId; + const button = tabs.createEl("button", { + cls: active ? "mwm-page-tab is-active" : "mwm-page-tab", + attr: { type: "button", title: label, "aria-pressed": active ? "true" : "false" } + }); + setIcon(button, icon); + button.createSpan({ text: label }); + button.addEventListener("click", () => { + this.state.sidePage = pageId; + this.renderSidePanel(index, graph, forcedNodeId); + }); + } + + const content = this.sidePanel.createDiv({ cls: `mwm-panel-content mwm-panel-content-${this.state.sidePage}` }); + this.sidePanelContent = content; + try { + if (this.state.sidePage === "view") this.renderViewPage(index, graph); + else if (this.state.sidePage === "pins") this.renderPinsPage(index, graph); + else if (this.state.sidePage === "controls") this.renderControlsPage(index, graph); + else if (this.state.sidePage === "defaults") this.renderDefaultsPage(index, graph); + else this.renderInspectPage(index, graph, forcedNodeId); + } finally { + this.sidePanelContent = null; + } + this.restorePanelFocus(focusState); + } + + renderViewPage(index, graph) { + const panel = this.getSidePanelTarget(); + this.createPanelPageTitle(panel, "View"); + + const search = this.createPanelSection(panel, "Search"); + this.searchInput = this.createPanelText( + search, + "search", + "Search", + this.state.search, + "Notes and folders", + value => { + this.disableCompleteRoot(); + this.state.search = value.trim(); + this.scheduleRender(120, { preservePanel: true }); + } + ); + + const modeActions = this.createPanelSection(panel, "Mode"); + const modeGrid = modeActions.createDiv({ cls: "mwm-panel-action-grid" }); + this.createPanelAction(modeGrid, "network", "Atlas", () => { + this.state.mode = "atlas"; + this.state.showCompleteRoot = false; + this.viewInitialized = false; + this.resetAdaptiveTuning(); + this.render(); + }, this.state.mode === "atlas"); + this.createPanelAction(modeGrid, "locate-fixed", "Focus", () => { + this.state.mode = "focus"; + this.state.focusPath = this.plugin.index.getActiveNotePath(); + this.state.showCompleteRoot = false; + this.viewInitialized = false; + this.resetAdaptiveTuning(); + this.render(); + }, this.state.mode === "focus"); + this.createPanelAction(modeGrid, "home", "Vault root", () => { + this.resetToVaultRoot(); + }); + const currentRoot = graph?.rootId !== null && graph?.rootId !== undefined + ? index.nodes.get(graph.rootId) + : index.nodes.get(this.state.rootPath); + const parentRootId = currentRoot && currentRoot.parentId !== null && currentRoot.parentId !== undefined + ? currentRoot.parentId + : null; + const parentButton = this.createPanelAction(modeGrid, "arrow-up", "Parent root", () => { + if (parentRootId === null) return; + this.state.rootPath = parentRootId; + this.state.mode = "atlas"; + this.state.showCompleteRoot = false; + this.viewInitialized = false; + this.resetAdaptiveTuning(); + this.render(); + }); + if (parentRootId === null) parentButton.disabled = true; + this.createPanelAction( + modeGrid, + this.state.showCompleteRoot ? "x" : "route", + this.state.showCompleteRoot ? "Exit complete" : "Complete root", + () => { + if (this.state.showCompleteRoot) { + this.exitCompleteRoot(); + this.resetAdaptiveTuning(); + this.viewInitialized = false; + this.render(); + } else { + this.showCompleteCurrentRoot(); + } + }, + this.state.showCompleteRoot + ); + + const appearance = this.createPanelSection(panel, "Appearance"); + const appearanceGrid = appearance.createDiv({ cls: "mwm-panel-action-grid mwm-panel-action-grid-three" }); + this.createPanelAction(appearanceGrid, "monitor", "Auto", () => this.setColorScheme("auto"), this.state.colorScheme === "auto"); + this.createPanelAction(appearanceGrid, "sun", "Day", () => this.setColorScheme("day"), this.state.colorScheme === "day"); + this.createPanelAction(appearanceGrid, "moon", "Night", () => this.setColorScheme("night"), this.state.colorScheme === "night"); + + const commands = this.createPanelSection(panel, "Commands"); + const commandGrid = commands.createDiv({ cls: "mwm-panel-action-grid" }); + this.createPanelAction(commandGrid, "refresh-cw", "Rebuild", () => { + this.plugin.rebuildIndex("manual"); + }); + this.createPanelAction(commandGrid, "maximize-2", "Full screen", () => this.toggleFullscreen(), this.state.fullscreen); + } + + renderPinsPage(index, graph) { + const panel = this.getSidePanelTarget(); + this.createPanelPageTitle(panel, "Pins"); + + const actions = this.createPanelSection(panel, "Actions"); + const actionGrid = actions.createDiv({ cls: "mwm-panel-action-grid" }); + this.createPanelAction(actionGrid, "pin", "Pin hover", () => this.pinCurrentHoverPath()); + const clearButton = this.createPanelAction(actionGrid, "trash-2", "Clear pins", () => this.clearPinnedPaths()); + clearButton.disabled = this.pinnedPaths.length === 0; + + const grouping = this.createPanelSection(panel, "Group"); + this.pinGroupInput = this.createPanelText( + grouping, + "pin-group-name", + "Name", + this.pinGroupName, + "Group name", + value => { + this.pinGroupName = value; + } + ); + const groupGrid = grouping.createDiv({ cls: "mwm-panel-action-grid" }); + const groupButton = this.createPanelAction(groupGrid, "folder-plus", "Group selected", () => this.groupSelectedPinnedPaths()); + groupButton.disabled = this.selectedPinIds.size === 0; + + const listSection = this.createPanelSection(panel, `Pinned Paths (${this.pinnedPaths.length})`); + if (!this.pinnedPaths.length) { + listSection.createDiv({ cls: "mwm-side-muted", text: "No pinned paths." }); + return; + } + + const groupsById = new Map(this.pinGroups.map(group => [group.id, group])); + const pinsByGroup = new Map(); + const ungrouped = []; + for (const pin of this.pinnedPaths) { + if (!pin.groupId || !groupsById.has(pin.groupId)) { + ungrouped.push(pin); + continue; + } + if (!pinsByGroup.has(pin.groupId)) pinsByGroup.set(pin.groupId, []); + pinsByGroup.get(pin.groupId).push(pin); + } + + if (ungrouped.length) { + this.renderPinnedPathGroup(listSection, index, graph, null, ungrouped); + } + for (const group of this.pinGroups) { + const pins = pinsByGroup.get(group.id) || []; + if (pins.length) this.renderPinnedPathGroup(listSection, index, graph, group, pins); + } + } + + renderPinnedPathGroup(parent, index, graph, group, pins) { + const wrapper = parent.createDiv({ cls: "mwm-pin-group" }); + const header = wrapper.createDiv({ cls: "mwm-pin-group-header" }); + const title = header.createDiv({ cls: "mwm-pin-group-title" }); + title.createSpan({ text: group ? group.name : "Ungrouped" }); + title.createSpan({ cls: "mwm-pin-count", text: String(pins.length) }); + if (group) { + this.createIconButton(header, "folder-minus", "Ungroup all", () => this.removePinGroup(group.id), "mwm-pin-icon-button"); + } + + const list = wrapper.createDiv({ cls: "mwm-pin-list" }); + for (const pin of pins) { + this.renderPinnedPathItem(list, index, graph, pin); + } + } + + renderPinnedPathItem(parent, index, graph, pin) { + const row = parent.createDiv({ cls: "mwm-pin-row" }); + const checkbox = row.createEl("input", { + cls: "mwm-pin-check", + attr: { type: "checkbox", title: "Select for grouping", "aria-label": "Select for grouping" } + }); + checkbox.checked = this.selectedPinIds.has(pin.id); + checkbox.addEventListener("change", () => { + if (checkbox.checked) this.selectedPinIds.add(pin.id); + else this.selectedPinIds.delete(pin.id); + this.renderSidePanel(index, graph); + }); + + const main = row.createEl("button", { + cls: "mwm-pin-main", + attr: { type: "button", title: pin.path || pin.title } + }); + main.addEventListener("click", () => this.centerPinnedPath(pin)); + main.createDiv({ cls: "mwm-pin-title", text: pin.title || "Pinned path" }); + const kind = pin.kind === "link" ? "link" : hoverHighlightModeLabel(pin.mode); + main.createDiv({ cls: "mwm-pin-meta", text: `${kind} - ${pin.path || "/"}` }); + + const actions = row.createDiv({ cls: "mwm-pin-actions" }); + this.createIconButton(actions, "locate-fixed", "Locate", () => this.centerPinnedPath(pin), "mwm-pin-icon-button"); + this.createIconButton(actions, "info", "Inspect", () => this.inspectPinnedPath(pin, index, graph), "mwm-pin-icon-button"); + if (pin.groupId) { + this.createIconButton(actions, "folder-minus", "Ungroup", () => this.ungroupPinnedPath(pin.id), "mwm-pin-icon-button"); + } + this.createIconButton(actions, "x", "Remove", () => this.removePinnedPath(pin.id), "mwm-pin-icon-button"); + } + + renderControlsPage(index, graph) { + const panel = this.getSidePanelTarget(); + this.createPanelPageTitle(panel, "Controls"); + const createPanelPageTitle = this.createPanelPageTitle; + this.createPanelPageTitle = () => {}; + try { + this.renderDetailPage(index, graph); + this.renderLinksPage(index, graph); + this.renderLayoutPage(index, graph); + } finally { + this.createPanelPageTitle = createPanelPageTitle; + } + this.renderLegendControls(panel); + this.renderLegend(false); + } + + renderDetailPage(index, graph) { + const panel = this.getSidePanelTarget(); + this.createPanelPageTitle(panel, "Detail"); + + const budgets = this.createPanelSection(panel, "Budgets"); + this.depthInput = this.createPanelNumber( + budgets, + "atlas-depth", + "Depth", + this.state.atlasDepth, + { min: "1", max: String(MAX_ATLAS_DEPTH) }, + value => { + this.disableAutoDetail(); + this.disableCompleteRoot(); + this.state.atlasDepth = clampNumber(value, 1, MAX_ATLAS_DEPTH, this.plugin.settings.atlasDepth); + this.renderMapOnly(); + } + ); + this.nodeInput = this.createPanelNumber( + budgets, + "node-limit", + "Nodes", + this.state.nodeLimit, + { min: "200", max: String(MAX_RENDER_NODE_LIMIT), step: "100" }, + value => { + this.disableAutoDetail(); + this.disableCompleteRoot(); + this.state.nodeLimit = clampNumber(value, 200, MAX_RENDER_NODE_LIMIT, this.plugin.settings.renderNodeLimit); + this.renderMapOnly(); + } + ); + this.linkInput = this.createPanelNumber( + budgets, + "link-limit", + "Link overlays", + this.state.linkLimit, + { min: "0", max: String(MAX_LINK_LIMIT) }, + value => { + this.disableAutoDetail(); + this.disableCompleteRoot(); + this.state.linkLimit = clampNumber(value, 0, MAX_LINK_LIMIT, this.plugin.settings.linkLimit); + this.renderMapOnly(); + } + ); + + const automation = this.createPanelSection(panel, "Automation"); + this.autoToggle = this.createPanelToggle(automation, "auto-detail", "Adaptive detail", this.state.autoDetail, checked => { + this.state.autoDetail = checked; + if (this.state.autoDetail) this.disableCompleteRoot(); + this.adaptiveInitialized = false; + this.autoTuneCount = 0; + this.renderMapOnly(); + }); + } + + renderLinksPage(index, graph) { + const panel = this.getSidePanelTarget(); + this.createPanelPageTitle(panel, "Links"); + + const overlays = this.createPanelSection(panel, "Overlay"); + this.linkHoverSelect = this.createPanelSelect( + overlays, + "hover-highlight-mode", + "Hover highlight", + normalizeHoverHighlightMode(this.state.hoverHighlightMode), + HOVER_HIGHLIGHT_MODE_OPTIONS, + value => { + const previousUsesHoverLinks = hoverHighlightsNoteLinks(this.state.hoverHighlightMode); + this.state.hoverHighlightMode = normalizeHoverHighlightMode(value); + this.state.enableLinkHover = hoverHighlightsNoteLinks(this.state.hoverHighlightMode); + this.hoverLink = null; + if (previousUsesHoverLinks !== this.state.enableLinkHover) this.renderMapOnly(); + else this.applyPersistentHighlight(); + } + ); + + const external = this.createPanelSection(panel, "Outside Root"); + this.externalToggle = this.createPanelToggle(external, "show-external-links", "Show external links", this.state.showExternalLinks, checked => { + this.disableAutoDetail(); + this.disableCompleteRoot(); + this.state.showExternalLinks = checked; + this.renderMapOnly(); + }); + this.externalModeSelect = this.createPanelSelect( + external, + "external-detail-mode", + "Outside detail", + this.state.externalDetailMode, + [ + ["grouped", "Groups"], + ["selected", "Selected"], + ["exact", "Exact"] + ], + value => { + this.disableAutoDetail(); + this.disableCompleteRoot(); + this.state.externalDetailMode = value; + this.renderMapOnly(); + } + ); + this.externalLimitInput = this.createPanelNumber( + external, + "external-link-anchor-limit", + "Exact outside files", + this.state.externalLinkAnchorLimit, + { min: "0", max: String(MAX_EXTERNAL_LINK_ANCHOR_LIMIT) }, + value => { + this.disableAutoDetail(); + this.disableCompleteRoot(); + this.state.externalLinkAnchorLimit = clampNumber(value, 0, MAX_EXTERNAL_LINK_ANCHOR_LIMIT, this.plugin.settings.externalLinkAnchorLimit); + this.renderMapOnly(); + } + ); + } + + renderLayoutPage(index, graph) { + const panel = this.getSidePanelTarget(); + this.createPanelPageTitle(panel, "Layout"); + + const zoom = this.createPanelSection(panel, "Zoom"); + const zoomActions = zoom.createDiv({ cls: "mwm-panel-action-row" }); + this.createPanelAction(zoomActions, "zoom-out", "Out", () => this.adjustZoom(-1)); + this.createPanelAction(zoomActions, "zoom-in", "In", () => this.adjustZoom(1)); + this.createPanelAction(zoomActions, "maximize", "Fit", () => this.fitToView()); + this.createPanelAction(zoomActions, "rotate-ccw", "Reset", () => this.resetViewZoom()); + + const range = this.createPanelRange( + zoom, + "zoom", + "Zoom", + "500", + { min: "0", max: String(ZOOM_SLIDER_STEPS), step: "1" }, + value => { + const viewport = this.canvasViewport(); + const minZoom = this.canvasMinZoom(viewport, this.lastLayout); + const maxZoom = this.canvasMaxZoom(viewport, this.lastLayout); + this.setCanvasZoom(sliderValueToZoom(value, minZoom, maxZoom), null, true); + } + ); + this.zoomInput = range.input; + this.zoomLabel = range.valueEl; + this.syncZoomControls(); + + const spacing = this.createPanelSection(panel, "Spacing"); + this.columnInput = this.createPanelNumber( + spacing, + "column-spacing", + "Ring", + this.state.columnSpacing, + { min: String(MIN_RING_SPACING), max: String(MAX_RING_SPACING), step: "10" }, + value => { + this.state.columnSpacing = clampNumber(value, MIN_RING_SPACING, MAX_RING_SPACING, DEFAULT_RING_SPACING); + this.renderMapOnly(); + } + ); + this.rowInput = this.createPanelNumber( + spacing, + "row-spacing", + "Gap", + this.state.rowSpacing, + { min: String(MIN_NODE_SPACING), max: String(MAX_NODE_SPACING), step: "2" }, + value => { + this.state.rowSpacing = clampNumber(value, MIN_NODE_SPACING, MAX_NODE_SPACING, DEFAULT_NODE_SPACING); + this.renderMapOnly(); + } + ); + + const labels = this.createPanelSection(panel, "Labels"); + this.labelVisibilitySelect = this.createPanelSelect( + labels, + "label-visibility", + "Names", + normalizeLabelVisibility(this.state.labelVisibility), + LABEL_VISIBILITY_OPTIONS, + value => { + this.state.labelVisibility = normalizeLabelVisibility(value); + this.requestCanvasDraw("full"); + } + ); + + const motion = this.createPanelSection(panel, "Motion"); + const motionActions = motion.createDiv({ cls: "mwm-panel-action-row" }); + const spinning = this.state.swirlStrength > 0; + this.createPanelAction(motionActions, spinning ? "pause" : "play", spinning ? "Stop" : "Spin", () => { + this.setSpinSpeed(spinning ? 0 : DEFAULT_SWIRL_BUTTON_STRENGTH, { render: true }); + }, spinning); + const swirl = this.createPanelRange( + motion, + "swirl-strength", + "Spin speed", + String(this.state.swirlStrength), + { min: "0", max: String(MAX_SWIRL_STRENGTH), step: "1" }, + (value, input, valueEl) => { + this.setSpinSpeed(value); + valueEl.textContent = `${Math.round(this.state.swirlStrength)}%`; + } + ); + this.swirlInput = swirl.input; + this.swirlLabel = swirl.valueEl; + this.swirlLabel.textContent = `${Math.round(this.state.swirlStrength)}%`; + } + + renderDefaultsPage(index, graph) { + const panel = this.getSidePanelTarget(); + this.createPanelPageTitle(panel, "Defaults"); + const applyLiveDefault = (keys, saveOptions = {}) => { + this.plugin.applySettingsToOpenViews(keys, { preservePanel: true }); + void this.plugin.saveSettings(Object.assign({ rebuild: false, refresh: false }, saveOptions)); + }; + + const budgets = this.createPanelSection(panel, "Default Budgets"); + this.createPanelNumber( + budgets, + "default-atlas-depth", + "Atlas depth", + this.plugin.settings.atlasDepth, + { min: "1", max: String(MAX_ATLAS_DEPTH) }, + value => { + this.plugin.settings.atlasDepth = clampNumber(value, 1, MAX_ATLAS_DEPTH, DEFAULT_SETTINGS.atlasDepth); + applyLiveDefault("atlasDepth"); + } + ); + this.createPanelNumber( + budgets, + "default-render-node-limit", + "Render nodes", + this.plugin.settings.renderNodeLimit, + { min: "200", max: String(MAX_RENDER_NODE_LIMIT), step: "100" }, + value => { + this.plugin.settings.renderNodeLimit = clampNumber(value, 200, MAX_RENDER_NODE_LIMIT, DEFAULT_SETTINGS.renderNodeLimit); + applyLiveDefault("renderNodeLimit"); + } + ); + this.createPanelNumber( + budgets, + "default-link-limit", + "Link overlays", + this.plugin.settings.linkLimit, + { min: "0", max: String(MAX_LINK_LIMIT) }, + value => { + this.plugin.settings.linkLimit = clampNumber(value, 0, MAX_LINK_LIMIT, DEFAULT_SETTINGS.linkLimit); + applyLiveDefault("linkLimit"); + } + ); + this.createPanelNumber( + budgets, + "default-external-link-anchor-limit", + "Exact outside files", + this.plugin.settings.externalLinkAnchorLimit, + { min: "0", max: String(MAX_EXTERNAL_LINK_ANCHOR_LIMIT) }, + value => { + this.plugin.settings.externalLinkAnchorLimit = clampNumber(value, 0, MAX_EXTERNAL_LINK_ANCHOR_LIMIT, DEFAULT_SETTINGS.externalLinkAnchorLimit); + applyLiveDefault("externalLinkAnchorLimit"); + } + ); + + const toggles = this.createPanelSection(panel, "Default Toggles"); + this.createPanelToggle(toggles, "default-adaptive-detail", "Adaptive detail", this.plugin.settings.adaptiveDetail, checked => { + this.plugin.settings.adaptiveDetail = checked; + applyLiveDefault("adaptiveDetail"); + }); + this.createPanelSelect( + toggles, + "default-hover-highlight-mode", + "Hover highlight", + normalizeHoverHighlightMode(this.plugin.settings.hoverHighlightMode), + HOVER_HIGHLIGHT_MODE_OPTIONS, + value => { + this.plugin.settings.hoverHighlightMode = normalizeHoverHighlightMode(value); + this.plugin.settings.enableLinkHover = hoverHighlightsNoteLinks(this.plugin.settings.hoverHighlightMode); + applyLiveDefault("hoverHighlightMode"); + } + ); + this.createPanelToggle(toggles, "default-show-external-links", "External links", this.plugin.settings.showExternalLinks, checked => { + this.plugin.settings.showExternalLinks = checked; + applyLiveDefault("showExternalLinks"); + }); + this.createPanelToggle(toggles, "default-include-unresolved-links", "Unresolved links", this.plugin.settings.includeUnresolvedLinks, checked => { + this.plugin.settings.includeUnresolvedLinks = checked; + void this.plugin.saveSettings({ immediateRebuild: true, preservePanel: true }); + }); + + const layoutDefaults = this.createPanelSection(panel, "Default Layout"); + this.createPanelSelect( + layoutDefaults, + "default-label-visibility", + "Names", + normalizeLabelVisibility(this.plugin.settings.labelVisibility), + LABEL_VISIBILITY_OPTIONS, + value => { + this.plugin.settings.labelVisibility = normalizeLabelVisibility(value); + applyLiveDefault("labelVisibility"); + } + ); + this.createPanelNumber( + layoutDefaults, + "default-swirl-strength", + "Spin speed", + this.plugin.settings.swirlStrength, + { min: "0", max: String(MAX_SWIRL_STRENGTH), step: "1" }, + value => { + this.plugin.settings.swirlStrength = clampNumber(value, 0, MAX_SWIRL_STRENGTH, DEFAULT_SWIRL_STRENGTH); + applyLiveDefault("swirlStrength"); + } + ); + + const appearance = this.createPanelSection(panel, "Default Appearance"); + this.createPanelSelect( + appearance, + "default-color-scheme", + "Color scheme", + this.plugin.settings.colorScheme || DEFAULT_SETTINGS.colorScheme, + [ + ["auto", "Auto"], + ["day", "Day"], + ["night", "Night"] + ], + value => { + const scheme = normalizeColorScheme(value); + this.setColorScheme(scheme, false); + this.plugin.settings.colorScheme = scheme; + for (const leaf of this.plugin.app.workspace.getLeavesOfType(VIEW_TYPE_MINI_WORLD_MAP)) { + const view = leaf.view; + if (view && view !== this && typeof view.setColorScheme === "function") view.setColorScheme(scheme, false); + } + void this.plugin.saveSettings({ rebuild: false }); + } + ); + + const outside = this.createPanelSection(panel, "Outside Root"); + this.createPanelSelect( + outside, + "default-external-detail-mode", + "Outside detail", + this.plugin.settings.externalDetailMode || DEFAULT_SETTINGS.externalDetailMode, + [ + ["grouped", "Groups"], + ["selected", "Selected"], + ["exact", "Exact"] + ], + value => { + this.plugin.settings.externalDetailMode = value; + applyLiveDefault("externalDetailMode"); + } + ); + + const ignores = this.createPanelSection(panel, "Ignored Folders"); + this.createPanelTextArea( + ignores, + "default-ignore-folders", + "Folder paths", + (this.plugin.settings.ignoreFolders || []).join("\n"), + value => { + this.plugin.settings.ignoreFolders = value + .split("\n") + .map(line => line.trim()) + .filter(Boolean); + void this.plugin.saveSettings({ immediateRebuild: true, preservePanel: true }); + } + ); + } + + renderLegendPage() { + const panel = this.getSidePanelTarget(); + this.createPanelPageTitle(panel, "Legend"); + this.renderLegendControls(panel); + this.renderLegend(false); + } + + renderLegendControls(panel) { + const section = this.createPanelSection(panel, "Legend"); + const hidden = this.hiddenLegendItemSet(); + for (const [id, label, text] of LEGEND_ITEM_DEFINITIONS) { + this.createPanelToggle(section, `legend-${id}`, label, !hidden.has(id), checked => { + this.setLegendItemHidden(id, !checked); + }).title = text; + } + } + + hiddenLegendItemSet() { + return new Set(Array.isArray(this.state.hiddenLegendItems) ? this.state.hiddenLegendItems : []); + } + + setLegendItemHidden(id, hidden) { + const validIds = new Set(LEGEND_ITEM_DEFINITIONS.map(([itemId]) => itemId)); + if (!validIds.has(id)) return; + const hiddenSet = this.hiddenLegendItemSet(); + if (hidden) hiddenSet.add(id); + else hiddenSet.delete(id); + const next = Array.from(hiddenSet).filter(itemId => validIds.has(itemId)); + this.state.hiddenLegendItems = next; + this.plugin.settings.hiddenLegendItems = next.slice(); + void this.plugin.saveSettings({ rebuild: false, refresh: false }); + this.renderMapOnly(); + } + + renderInspectPage(index, graph, forcedNodeId) { + const panel = this.getSidePanelTarget(); + + if (this.selectedLink && forcedNodeId === undefined) { + this.renderLinkPanel(index, graph, this.selectedLink); + return; + } + + const nodeId = forcedNodeId ?? this.selectedNodeId ?? graph?.focusId ?? graph?.rootId; + const node = nodeId !== null && nodeId !== undefined ? graphNode(index, graph, nodeId) : null; + + panel.createDiv({ cls: "mwm-side-title", text: node ? node.title : "Mini World Map" }); + + if (!node) { + panel.createDiv({ cls: "mwm-side-muted", text: "Select a node to inspect it." }); + return; + } + + const path = panel.createDiv({ cls: "mwm-side-path", text: node.path || "/" }); + path.setAttribute("title", node.path || "/"); + + const nodeType = node.externalProxy + ? `outside ${node.type}` + : node.type === "folder" && node.representativeFile + ? "folder + meta file" + : node.isRepresentativeFile + ? "merged meta file" + : node.type; + const facts = [ + ["Type", nodeType], + ["Depth", String(node.depth)], + ["Notes", String(node.noteCount || node.descendantCount || 0)], + ["Out", String(node.linkCount || 0)], + ["In", String(node.backlinkCount || 0)] + ]; + + const table = panel.createDiv({ cls: "mwm-facts" }); + for (const [label, value] of facts) { + table.createSpan({ text: label }); + table.createSpan({ text: value }); + } + + const actions = panel.createDiv({ cls: "mwm-side-actions" }); + this.createIconButton(actions, "pin", "Pin highlighted path", () => { + const previousNeedsHoverLinks = this.needsCanvasHoverLinks(); + const pin = this.addPinnedPath(this.createNodePinCandidate(node, this.state.hoverHighlightMode)); + if (!pin) return; + this.state.sidePage = "pins"; + if (previousNeedsHoverLinks !== this.needsCanvasHoverLinks()) this.renderMapOnly(); + else this.applyPersistentHighlight(); + if (!this.state.fullscreen && this.lastCanvasBundle) { + this.renderSidePanel(this.lastCanvasBundle.index, this.lastCanvasBundle.graph); + } + }, ""); + if (node.type === "note") { + this.createIconButton(actions, "file-text", "Open note", () => this.openNode(node.id), ""); + this.createIconButton(actions, "locate-fixed", "Focus note", () => { + this.state.mode = "focus"; + this.state.focusPath = node.id; + this.state.showCompleteRoot = false; + this.viewInitialized = false; + this.resetAdaptiveTuning(); + this.render(); + }, ""); + } + if (node.type === "folder") { + this.createIconButton(actions, "folder-open", "Use as atlas root", () => { + this.state.mode = "atlas"; + this.state.rootPath = node.id; + this.state.showCompleteRoot = false; + this.viewInitialized = false; + this.resetAdaptiveTuning(); + this.render(); + }, ""); + if (node.representativeFile) { + this.createIconButton(actions, "file-text", "Open representative note", () => this.openNode(node.representativeFile), ""); + } + } + if (node.type === "external" && node.externalAnchorPath) { + const anchor = index.nodes.get(node.externalAnchorPath); + if (anchor && anchor.type === "folder") { + this.createIconButton(actions, "folder-open", "Use outside branch as root", () => { + this.state.mode = "atlas"; + this.state.rootPath = anchor.id; + this.state.showCompleteRoot = false; + this.viewInitialized = false; + this.resetAdaptiveTuning(); + this.render(); + }, ""); + } + if (anchor && anchor.type === "note") { + this.createIconButton(actions, "file-text", "Open outside note", () => this.openNode(anchor.id), ""); + } + } + + if (node.type === "external") { + panel.createDiv({ + cls: "mwm-side-muted", + text: "This is a summarized outside branch. It keeps links visible when the atlas root is focused on a smaller subtree." + }); + } else { + this.renderNeighborList(index, node); + } + } + + renderLinkPanel(index, graph, edge) { + const panel = this.getSidePanelTarget(); + const source = graphNode(index, graph, edge.source); + const target = graphNode(index, graph, edge.target); + panel.createDiv({ cls: "mwm-side-title", text: "Link overlay" }); + panel.createDiv({ + cls: "mwm-side-muted", + text: "Aggregated internal Markdown links between visible nodes. Folder nodes can include hidden descendants; outside links point to exact outside files when the current root is narrowed." + }); + + const table = panel.createDiv({ cls: "mwm-facts mwm-link-facts" }); + const facts = [ + ["Source", source ? source.title : edge.source], + ["Target", target ? target.title : edge.target], + ["Weight", String(edge.weight || 0)], + ["Raw edges", String(edge.rawCount || edge.weight || 0)], + ["Unresolved", String(edge.unresolvedCount || 0)], + ["External", String(edge.externalCount || 0)] + ]; + for (const [label, value] of facts) { + table.createSpan({ text: label }); + table.createSpan({ text: value }); + } + + const actions = panel.createDiv({ cls: "mwm-side-actions" }); + this.createIconButton(actions, "pin", "Pin link", () => { + const previousNeedsHoverLinks = this.needsCanvasHoverLinks(); + const pin = this.addPinnedPath(this.createLinkPinCandidate(index, graph, edge)); + if (!pin) return; + this.state.sidePage = "pins"; + if (previousNeedsHoverLinks !== this.needsCanvasHoverLinks()) this.renderMapOnly(); + else this.applyPersistentHighlight(); + if (!this.state.fullscreen && this.lastCanvasBundle) { + this.renderSidePanel(this.lastCanvasBundle.index, this.lastCanvasBundle.graph); + } + }, ""); + if (source) { + const button = actions.createEl("button", { cls: "mwm-text-button", attr: { type: "button" } }); + button.textContent = "Source"; + button.addEventListener("click", () => { + this.state.sidePage = "inspect"; + this.selectedLink = null; + this.selectedNodeId = source.id; + if (this.shouldRerenderForSelection()) this.render(); + else { + this.applyPersistentHighlight(); + if (!this.state.fullscreen) this.renderSidePanel(index, graph, source.id); + } + }); + } + if (target) { + const button = actions.createEl("button", { cls: "mwm-text-button", attr: { type: "button" } }); + button.textContent = "Target"; + button.addEventListener("click", () => { + this.state.sidePage = "inspect"; + this.selectedLink = null; + this.selectedNodeId = target.id; + if (this.shouldRerenderForSelection()) this.render(); + else { + this.applyPersistentHighlight(); + if (!this.state.fullscreen) this.renderSidePanel(index, graph, target.id); + } + }); + } + } + + renderLegend(showHeading = true) { + const panel = this.getSidePanelTarget(); + if (showHeading) panel.createDiv({ cls: "mwm-side-heading", text: "Legend" }); + const legend = panel.createDiv({ cls: "mwm-legend" }); + const hidden = this.hiddenLegendItemSet(); + + for (const [id, label, text, cls] of LEGEND_ITEM_DEFINITIONS) { + if (hidden.has(id)) continue; + const item = legend.createDiv({ cls: "mwm-legend-item" }); + item.createSpan({ cls: `mwm-legend-mark ${cls}` }); + item.createSpan({ cls: "mwm-legend-label", text: label }); + item.createSpan({ cls: "mwm-legend-text", text }); + } + } + + renderNeighborList(index, node) { + const panel = this.getSidePanelTarget(); + const mergedIds = [node.id]; + if (node.type === "folder" && node.representativeFile) mergedIds.push(node.representativeFile); + const outgoing = uniqueEdgesByEndpoint( + mergedIds.flatMap(id => index.linkEdgesBySource.get(id) || []), + "target" + ).sort((a, b) => b.weight - a.weight); + const incoming = uniqueEdgesByEndpoint( + mergedIds.flatMap(id => index.linkEdgesByTarget.get(id) || []), + "source" + ).sort((a, b) => b.weight - a.weight); + + panel.createDiv({ cls: "mwm-side-heading", text: `Outgoing (${outgoing.length})` }); + this.renderEdgeList(index, outgoing, "target"); + + panel.createDiv({ cls: "mwm-side-heading", text: `Backlinks (${incoming.length})` }); + this.renderEdgeList(index, incoming, "source"); + } + + renderEdgeList(index, edges, side) { + const panel = this.getSidePanelTarget(); + if (!edges.length) { + panel.createDiv({ cls: "mwm-side-muted", text: "None" }); + return; + } + + const list = panel.createEl("ul", { cls: "mwm-edge-list" }); + for (const edge of edges) { + const target = index.nodes.get(edge[side]); + if (!target) continue; + const item = list.createEl("li"); + const button = item.createEl("button", { attr: { type: "button", title: target.path || target.title } }); + button.textContent = `${target.title} (${edge.weight})`; + button.addEventListener("click", () => { + this.state.sidePage = "inspect"; + this.selectedLink = null; + this.selectedNodeId = target.id; + this.applyPersistentHighlight(); + const bundle = this.lastCanvasBundle; + if (!this.state.fullscreen && bundle) this.renderSidePanel(bundle.index, bundle.graph, target.id); + }); + } + } + + async openNode(path, evt) { + const file = this.app.vault.getAbstractFileByPath(path); + if (!(file instanceof TFile)) return; + const targetPane = evt && (evt.metaKey || evt.ctrlKey) ? "split" : "tab"; + await this.app.workspace.openLinkText(file.path, "", targetPane, { active: true }); + } +} + +class MiniWorldMapSettingTab extends PluginSettingTab { + constructor(app, plugin) { + super(app, plugin); + this.plugin = plugin; + } + + display() { + const { containerEl } = this; + containerEl.empty(); + containerEl.createEl("h2", { text: "Mini World Map" }); + + new Setting(containerEl) + .setName("Color scheme") + .setDesc("Auto follows Obsidian. Day and Night force Mini World Map's own palette.") + .addDropdown(dropdown => dropdown + .addOption("auto", "Auto") + .addOption("day", "Day") + .addOption("night", "Night") + .setValue(this.plugin.settings.colorScheme || DEFAULT_SETTINGS.colorScheme) + .onChange(async value => { + const scheme = normalizeColorScheme(value); + this.plugin.settings.colorScheme = scheme; + for (const leaf of this.plugin.app.workspace.getLeavesOfType(VIEW_TYPE_MINI_WORLD_MAP)) { + const view = leaf.view; + if (view && typeof view.setColorScheme === "function") view.setColorScheme(scheme, false); + } + await this.plugin.saveSettings({ rebuild: false }); + })); + + new Setting(containerEl) + .setName("Default atlas depth") + .setDesc("How many hierarchy levels to render before deeper nodes are aggregated.") + .addSlider(slider => slider + .setLimits(1, 20, 1) + .setValue(this.plugin.settings.atlasDepth) + .setDynamicTooltip() + .onChange(async value => { + this.plugin.settings.atlasDepth = value; + await this.plugin.saveSettings(); + })); + + new Setting(containerEl) + .setName("Default link overlay limit") + .setDesc("Maximum aggregated cross-links to draw in the map.") + .addText(text => text + .setPlaceholder(String(DEFAULT_SETTINGS.linkLimit)) + .setValue(String(this.plugin.settings.linkLimit)) + .onChange(async value => { + this.plugin.settings.linkLimit = clampNumber(value, 0, MAX_LINK_LIMIT, DEFAULT_SETTINGS.linkLimit); + await this.plugin.saveSettings(); + })); + + new Setting(containerEl) + .setName("Default render node limit") + .setDesc("Maximum visible nodes to draw before lower-priority nodes are summarized.") + .addText(text => text + .setPlaceholder(String(DEFAULT_SETTINGS.renderNodeLimit)) + .setValue(String(this.plugin.settings.renderNodeLimit)) + .onChange(async value => { + this.plugin.settings.renderNodeLimit = clampNumber(value, 200, MAX_RENDER_NODE_LIMIT, DEFAULT_SETTINGS.renderNodeLimit); + await this.plugin.saveSettings(); + })); + + new Setting(containerEl) + .setName("Adaptive detail by default") + .setDesc("Let Mini World Map adjust depth, node budget, and link budget after measuring render cost.") + .addToggle(toggle => toggle + .setValue(this.plugin.settings.adaptiveDetail) + .onChange(async value => { + this.plugin.settings.adaptiveDetail = value; + await this.plugin.saveSettings(); + })); + + new Setting(containerEl) + .setName("Default label visibility") + .setDesc("Auto shows important names by zoom. Hover only keeps names hidden until a node is hovered or selected.") + .addDropdown(dropdown => { + for (const [value, label] of LABEL_VISIBILITY_OPTIONS) { + dropdown.addOption(value, label); + } + dropdown + .setValue(normalizeLabelVisibility(this.plugin.settings.labelVisibility)) + .onChange(async value => { + this.plugin.settings.labelVisibility = normalizeLabelVisibility(value); + await this.plugin.saveSettings({ rebuild: false }); + }); + }); + + new Setting(containerEl) + .setName("Default spin speed") + .setDesc("How fast rings orbit when spin is enabled in the map view.") + .addSlider(slider => slider + .setLimits(0, MAX_SWIRL_STRENGTH, 1) + .setValue(this.plugin.settings.swirlStrength) + .setDynamicTooltip() + .onChange(async value => { + this.plugin.settings.swirlStrength = clampNumber(value, 0, MAX_SWIRL_STRENGTH, DEFAULT_SWIRL_STRENGTH); + await this.plugin.saveSettings({ rebuild: false }); + })); + + new Setting(containerEl) + .setName("Default hover highlight") + .setDesc("Choose what the graph highlights when the cursor is over a node.") + .addDropdown(dropdown => { + for (const [value, label] of HOVER_HIGHLIGHT_MODE_OPTIONS) { + dropdown.addOption(value, label); + } + dropdown + .setValue(normalizeHoverHighlightMode(this.plugin.settings.hoverHighlightMode)) + .onChange(async value => { + this.plugin.settings.hoverHighlightMode = normalizeHoverHighlightMode(value); + this.plugin.settings.enableLinkHover = hoverHighlightsNoteLinks(this.plugin.settings.hoverHighlightMode); + await this.plugin.saveSettings(); + }); + }); + + new Setting(containerEl) + .setName("Show external links by default") + .setDesc("When an atlas root is selected, keep links to exact notes outside that root visible around outside branch anchors.") + .addToggle(toggle => toggle + .setValue(this.plugin.settings.showExternalLinks) + .onChange(async value => { + this.plugin.settings.showExternalLinks = value; + await this.plugin.saveSettings(); + })); + + new Setting(containerEl) + .setName("Outside detail mode") + .setDesc("Groups is calmest. Selected uses selection context on the next render. Exact draws all outside files up to the limit.") + .addDropdown(dropdown => dropdown + .addOption("grouped", "Groups") + .addOption("selected", "Selected") + .addOption("exact", "Exact") + .setValue(this.plugin.settings.externalDetailMode || DEFAULT_SETTINGS.externalDetailMode) + .onChange(async value => { + this.plugin.settings.externalDetailMode = value; + await this.plugin.saveSettings(); + })); + + new Setting(containerEl) + .setName("Exact outside file limit") + .setDesc("Maximum exact outside linked files to draw before the remainder is grouped.") + .addText(text => text + .setPlaceholder(String(DEFAULT_SETTINGS.externalLinkAnchorLimit)) + .setValue(String(this.plugin.settings.externalLinkAnchorLimit)) + .onChange(async value => { + this.plugin.settings.externalLinkAnchorLimit = clampNumber(value, 0, MAX_EXTERNAL_LINK_ANCHOR_LIMIT, DEFAULT_SETTINGS.externalLinkAnchorLimit); + await this.plugin.saveSettings(); + })); + + new Setting(containerEl) + .setName("Include unresolved links") + .setDesc("Represent unresolved internal links as temporary nodes.") + .addToggle(toggle => toggle + .setValue(this.plugin.settings.includeUnresolvedLinks) + .onChange(async value => { + this.plugin.settings.includeUnresolvedLinks = value; + await this.plugin.saveSettings(); + })); + + new Setting(containerEl) + .setName("Ignored folders") + .setDesc("One path per line. Matching folders are excluded from the map.") + .addTextArea(text => text + .setValue((this.plugin.settings.ignoreFolders || []).join("\n")) + .onChange(async value => { + this.plugin.settings.ignoreFolders = value + .split("\n") + .map(line => line.trim()) + .filter(Boolean); + await this.plugin.saveSettings(); + })); + } +} + +function layoutVisibleGraph(index, graph, state = {}) { + const positions = new Map(); + const visible = new Set(graph.nodes.map(node => node.id)); + const baseRingGap = clampNumber(state.columnSpacing, MIN_RING_SPACING, MAX_RING_SPACING, DEFAULT_RING_SPACING); + const baseNodeGap = clampNumber(state.rowSpacing, MIN_NODE_SPACING, MAX_NODE_SPACING, DEFAULT_NODE_SPACING); + const padding = 340; + let trimmed = false; + + const graphNodesById = graph.nodesById || new Map(graph.nodes.map(node => [node.id, node])); + const normalNodes = graph.nodes.filter(node => !node.externalProxy && node.type !== "external"); + const normalNodeIds = new Set(normalNodes.map(node => node.id)); + const spacingProfile = adaptiveLayoutSpacing(graph, normalNodeIds, baseRingGap, baseNodeGap); + const ringGap = spacingProfile.ringGap; + const nodeGap = spacingProfile.nodeGap; + const rootId = graph.rootId && normalNodeIds.has(graph.rootId) + ? graph.rootId + : normalNodeIds.has(ROOT_ID) + ? ROOT_ID + : normalNodes.length + ? normalNodes[0].id + : null; + + const childrenByParent = new Map(); + for (const [parentId, childIds] of index.childrenByParent.entries()) { + if (!normalNodeIds.has(parentId)) continue; + const children = childIds.filter(childId => normalNodeIds.has(childId)); + if (children.length) childrenByParent.set(parentId, children); + } + + const metrics = new Map(); + function measureSubtree(id, depth, visiting = new Set()) { + if (metrics.has(id)) return metrics.get(id); + if (visiting.has(id)) { + const fallback = { weight: 1, maxDepth: depth, count: 1 }; + metrics.set(id, fallback); + return fallback; + } + + visiting.add(id); + const children = childrenByParent.get(id) || []; + const incidentPressure = spacingProfile.incidentPressureByNode.get(id) || 0; + let weight = Math.min(9, incidentPressure * 0.24); + let count = 1; + let maxDepth = depth; + for (const childId of children) { + const childMetrics = measureSubtree(childId, depth + 1, visiting); + weight += childMetrics.weight; + count += childMetrics.count; + maxDepth = Math.max(maxDepth, childMetrics.maxDepth); + } + visiting.delete(id); + + const metricsForNode = { + weight: Math.max(1, weight || 1), + maxDepth, + count + }; + metrics.set(id, metricsForNode); + return metricsForNode; + } + + const reachable = new Set(); + function collectReachable(id) { + if (id === null || id === undefined || reachable.has(id)) return; + reachable.add(id); + for (const childId of childrenByParent.get(id) || []) collectReachable(childId); + } + + if (rootId !== null) { + measureSubtree(rootId, 0); + collectReachable(rootId); + } + + const externalGroups = graph.nodes + .filter(node => node.type === "external" && !node.externalProxy) + .sort(compareNodes); + const externalFiles = graph.nodes + .filter(node => node.externalProxy) + .sort(compareNodes); + const orphanNodes = normalNodes + .filter(node => node.id !== rootId && !reachable.has(node.id)) + .sort(compareNodes); + + const rootMetrics = rootId !== null ? metrics.get(rootId) || { weight: 1, maxDepth: 0, count: 1 } : { weight: 0, maxDepth: 0, count: 0 }; + const maxTreeDepth = Math.max(0, rootMetrics.maxDepth || 0); + const ringSpacing = ringGap; + + if (rootId !== null) { + positions.set(rootId, { + x: 0, + y: 0, + depth: 0, + angle: -Math.PI / 2, + radius: 0, + labelSide: 1, + centerX: 0, + centerY: 0 + }); + + placeRadialChildren( + rootId, + 0, + -Math.PI / 2, + -Math.PI / 2 + Math.PI * 2, + -Math.PI / 2, + childrenByParent, + metrics, + positions, + spacingProfile.branchFanSpan, + spacingProfile, + rootId + ); + } + + let maxPlacedRadius = Math.max(maxRadiusFromPositions(positions), ringGap); + if (orphanNodes.length) { + trimmed = true; + maxPlacedRadius = Math.max( + maxPlacedRadius, + placeOuterCircleNodes(orphanNodes, positions, graph, maxPlacedRadius + ringSpacing * 0.72, nodeGap, -Math.PI / 2, { + depth: maxTreeDepth + 1 + }) + ); + } + + if (externalGroups.length) { + maxPlacedRadius = Math.max( + maxPlacedRadius, + placeOuterCircleNodes(externalGroups, positions, graph, maxPlacedRadius + ringSpacing * 0.62, nodeGap * 1.15, -Math.PI / 3, { + depth: maxTreeDepth + 1, + external: true, + externalGroup: true + }) + ); + } + + if (externalFiles.length) { + maxPlacedRadius = Math.max( + maxPlacedRadius, + placeOuterCircleNodes(externalFiles, positions, graph, maxPlacedRadius + Math.max(220, ringSpacing * 0.52), nodeGap, -Math.PI / 5, { + depth: maxTreeDepth + 2, + external: true, + externalFile: true + }) + ); + } + + const ringTargets = assignDepthRingTargets(positions, graph, spacingProfile, nodeGap); + resolveRadialCollisions(positions, graph, spacingProfile, nodeGap, ringTargets); + enforceDepthRingBands(positions, graph, spacingProfile, nodeGap, ringTargets); + if (spacingProfile.radiusExpansion > 1.001) { + applyAdaptiveRadiusExpansion(positions, ringTargets, spacingProfile); + } + const spinSpeed = clampFloat( + clampNumber(state.swirlStrength, 0, MAX_SWIRL_STRENGTH, DEFAULT_SWIRL_STRENGTH) / MAX_SWIRL_STRENGTH, + 0, + 1, + 0 + ); + const swirlStrength = spinSpeed > 0.001 + ? clampFloat(0.24 + spinSpeed * 0.34, 0.24, 0.58, 0.36) + : 0; + if (swirlStrength > 0.001) applyRadialSwirl(positions, graph, spacingProfile, swirlStrength); + maxPlacedRadius = Math.max(maxPlacedRadius, maxRadiusFromPositions(positions)); + + const routeBundle = computeLinkRoutes( + graph.linkEdges, + positions, + maxTreeDepth + (externalGroups.length || externalFiles.length ? 2 : orphanNodes.length ? 1 : 0), + ringSpacing, + maxPlacedRadius + Math.max(150, ringSpacing * 0.35), + spacingProfile.routeGapFactor + ); + const bounds = radialLayoutBounds(positions, Math.max(routeBundle.maxRadius, maxPlacedRadius + Math.max(160, ringSpacing * 0.34))); + const offsetX = padding - bounds.minX; + const offsetY = padding - bounds.minY; + shiftRadialLayout(positions, routeBundle.routes, offsetX, offsetY); + anchorLayoutHomePositions(positions); + const rings = computeLayoutRings(positions, ringTargets); + + return { + positions, + width: Math.max(900, bounds.maxX - bounds.minX + padding * 2), + height: Math.max(520, bounds.maxY - bounds.minY + padding * 2), + trimmed: trimmed || Boolean(graph.hiddenNodeCount), + linkRoutes: routeBundle.routes, + rings, + radiusExpansion: spacingProfile.radiusExpansion, + swirlStrength, + spinSpeed, + centerX: offsetX, + centerY: offsetY + }; +} + +function adaptiveLayoutSpacing(graph, normalNodeIds, baseRingGap, baseNodeGap) { + const incidentPressureByNode = new Map(); + let totalPressure = 0; + let externalPressure = 0; + + for (const edge of graph.linkEdges || []) { + const weightScore = Math.min(7, Math.log2((edge.weight || 1) + 1)); + const rawScore = Math.min(6, Math.log2((edge.rawCount || edge.weight || 1) + 1)); + const edgePressure = 0.75 + + weightScore * 0.62 + + rawScore * 0.28 + + (edge.unresolvedCount ? 0.35 : 0) + + (edge.externalCount ? 0.85 : 0); + + totalPressure += edgePressure; + if (edge.externalCount) externalPressure += edgePressure; + + for (const id of [edge.source, edge.target]) { + if (!id && id !== ROOT_ID) continue; + incidentPressureByNode.set(id, (incidentPressureByNode.get(id) || 0) + edgePressure); + } + } + + let maxIncidentPressure = 0; + for (const pressure of incidentPressureByNode.values()) { + maxIncidentPressure = Math.max(maxIncidentPressure, pressure); + } + + const visibleNodeCount = Math.max(1, (graph.nodes && graph.nodes.length) || normalNodeIds.size || 1); + const nodeDensity = graphNodeDensityProfile(graph, normalNodeIds); + const averagePressure = totalPressure / visibleNodeCount; + const overlayDensity = ((graph.linkEdges || []).length || 0) / visibleNodeCount; + const hubPressure = maxIncidentPressure / Math.max(1, Math.sqrt(visibleNodeCount) * 1.65); + const combinedPressure = averagePressure + + Math.sqrt(Math.max(0, hubPressure)) * 0.58 + + Math.min(1.6, overlayDensity) * 0.3 + + (externalPressure / visibleNodeCount) * 0.32; + const pressureRoot = Math.sqrt(Math.max(0, combinedPressure)); + + const nodeFactor = clampFloat( + 1.2 + pressureRoot * 0.92 + Math.min(0.86, averagePressure * 0.105), + 1.2, + 5.8, + 1 + ); + const ringFactor = clampFloat( + 1.08 + pressureRoot * 0.34 + Math.min(0.34, averagePressure * 0.044), + 1.08, + 2.15, + 1 + ); + const fanFactor = clampFloat( + 0.62 + pressureRoot * 0.24 + Math.min(0.3, overlayDensity * 0.17), + 0.62, + 1.35, + 0.68 + ); + const routeGapFactor = clampFloat( + 0.9 + pressureRoot * 0.38 + Math.min(0.42, overlayDensity * 0.2), + 0.9, + 2.55, + 1 + ); + const countExpansion = Math.max(0, Math.log2(nodeDensity.normalCount / 520)) * 0.035; + const ringExpansion = Math.max(0, Math.sqrt(nodeDensity.maxRingCount / 96) - 1) * 0.16; + const averageRingExpansion = Math.max(0, Math.sqrt(nodeDensity.averageRingCount / 64) - 1) * 0.1; + const pressureExpansion = Math.max(0, pressureRoot - 0.75) * 0.028; + const radiusExpansion = clampFloat( + 1 + countExpansion + ringExpansion + averageRingExpansion + pressureExpansion, + 1, + 1.56, + 1 + ); + + return { + baseRingGap, + baseNodeGap, + ringGap: clampNumber(baseRingGap * ringFactor, MIN_RING_SPACING, 4200, baseRingGap), + nodeGap: clampNumber(baseNodeGap * nodeFactor, 86, 860, baseNodeGap), + branchFanSpan: Math.PI * fanFactor, + routeGapFactor, + radiusExpansion, + ringCountsByDepth: nodeDensity.countsByDepth, + maxDensityDepth: nodeDensity.maxDepth, + totalPressure, + incidentPressureByNode + }; +} + +function graphNodeDensityProfile(graph, normalNodeIds) { + const normalCount = Math.max(1, normalNodeIds?.size || 0); + const byDepth = new Map(); + + for (const node of graph.nodes || []) { + if (!node || !normalNodeIds.has(node.id)) continue; + const depth = Math.max(0, Math.round(node.depth || 0)); + if (depth <= 0) continue; + byDepth.set(depth, (byDepth.get(depth) || 0) + 1); + } + + let maxRingCount = 0; + let totalRingCount = 0; + for (const count of byDepth.values()) { + maxRingCount = Math.max(maxRingCount, count); + totalRingCount += count; + } + + return { + normalCount, + ringCount: byDepth.size, + maxRingCount, + averageRingCount: byDepth.size ? totalRingCount / byDepth.size : normalCount, + maxDepth: Math.max(...Array.from(byDepth.keys()), 1), + countsByDepth: byDepth + }; +} + +function placeRadialChildren(parentId, depth, sectorStart, sectorEnd, parentAngle, childrenByParent, metrics, positions, branchFanSpan, spacingProfile, rootId) { + const children = childrenByParent.get(parentId) || []; + if (!children.length) return; + + let start = sectorStart; + let end = sectorEnd; + let span = Math.max(0.001, end - start); + const parentPoint = positions.get(parentId) || { radius: 0 }; + let childRadius = parentPoint.radius + localRadialGap(parentId, depth, children, metrics, spacingProfile, rootId); + const nodeGap = localArcGap(parentId, children, metrics, spacingProfile, rootId); + + if (children.length === 1 && parentId === rootId) { + const localSpan = Math.max(Math.PI * 0.36, branchFanSpan * 0.82); + start = parentAngle - localSpan / 2; + end = parentAngle + localSpan / 2; + span = localSpan; + } else if (parentId !== rootId) { + const demandSpan = children.length > 1 + ? ((children.length - 1) * nodeGap) / childRadius + 0.08 + : Math.max(Math.PI * 0.24, branchFanSpan * 0.62); + const cappedFan = Math.min(span, branchFanSpan); + const localSpan = Math.min(span, Math.max(cappedFan, demandSpan)); + start = parentAngle - localSpan / 2; + end = parentAngle + localSpan / 2; + span = localSpan; + } + + if (children.length > 1) { + const requiredRadius = ((children.length - 1) * nodeGap) / Math.max(0.16, span * 0.64); + const maxExtra = spacingProfile.ringGap * (parentId === rootId ? 1.8 : 2.8); + childRadius = Math.max(childRadius, Math.min(parentPoint.radius + maxExtra, requiredRadius)); + } + + const totalWeight = Math.max(1, children.reduce((sum, childId) => { + const childMetrics = metrics.get(childId); + return sum + (childMetrics ? childMetrics.weight : 1); + }, 0)); + const localGap = children.length > 1 + ? Math.min(nodeGap / childRadius, span * 0.38 / (children.length - 1)) + : 0; + const usableSpan = Math.max(0.001, span - localGap * Math.max(0, children.length - 1)); + let cursor = start; + + for (const childId of children) { + const childMetrics = metrics.get(childId) || { weight: 1 }; + const childSpan = children.length === 1 + ? usableSpan + : usableSpan * (childMetrics.weight / totalWeight); + const childStart = cursor; + const childEnd = cursor + childSpan; + const childAngle = childStart + childSpan / 2; + const radius = childRadius; + const point = radialPoint(0, 0, radius, childAngle); + + positions.set(childId, { + x: point.x, + y: point.y, + depth: depth + 1, + angle: childAngle, + radius, + labelSide: labelSideForAngle(childAngle), + centerX: 0, + centerY: 0 + }); + + placeRadialChildren( + childId, + depth + 1, + childStart, + childEnd, + childAngle, + childrenByParent, + metrics, + positions, + branchFanSpan, + spacingProfile, + rootId + ); + cursor = childEnd + localGap; + } +} + +function localRadialGap(parentId, depth, children, metrics, spacingProfile, rootId) { + const childCount = children.length; + const parentMetrics = metrics.get(parentId) || { count: 1, weight: 1 }; + const incidentPressure = spacingProfile.incidentPressureByNode.get(parentId) || 0; + const childRoot = Math.sqrt(Math.max(1, childCount)); + const subtreeSignal = Math.log2(Math.max(1, parentMetrics.count || parentMetrics.weight || 1)); + const pressureSignal = Math.sqrt(Math.max(0, incidentPressure)); + + let factor = 0.62 + + depth * 0.105 + + childRoot * 0.135 + + subtreeSignal * 0.094 + + pressureSignal * 0.086; + + if (parentId === rootId) factor *= 0.96; + if (childCount <= 4) factor = Math.min(factor, parentId === rootId ? 0.94 : 1.12); + if (childCount >= 14) factor = Math.max(factor, 1.24 + Math.min(1.1, childRoot * 0.09)); + if (childCount >= 40) factor = Math.max(factor, 1.56 + Math.min(1.34, childRoot * 0.084)); + + const minGap = parentId === rootId ? 260 : 300; + return clampNumber(spacingProfile.baseRingGap * factor, minGap, 4400, spacingProfile.baseRingGap); +} + +function localArcGap(parentId, children, metrics, spacingProfile, rootId) { + const childCount = children.length; + const parentMetrics = metrics.get(parentId) || { count: 1, weight: 1 }; + const incidentPressure = spacingProfile.incidentPressureByNode.get(parentId) || 0; + const densitySignal = Math.sqrt(Math.max(1, childCount)) * 0.07 + + Math.log2(Math.max(1, parentMetrics.count || 1)) * 0.052 + + Math.sqrt(Math.max(0, incidentPressure)) * 0.05; + let factor = 1.58 + densitySignal * 1.95; + + if (parentId === rootId && childCount <= 6) factor *= 1.08; + if (childCount <= 4) factor = clampFloat(factor, 1.62, 2.08, 1.82); + else if (childCount <= 10) factor = Math.max(factor, 1.86); + if (childCount >= 24) factor = Math.max(factor, 2.12); + if (childCount >= 64) factor = Math.max(factor, 2.58); + + return clampNumber(spacingProfile.baseNodeGap * factor, 132, 920, spacingProfile.baseNodeGap); +} + +function maxRadiusFromPositions(positions) { + let maxRadius = 0; + for (const point of positions.values()) { + if (Number.isFinite(point.radius)) maxRadius = Math.max(maxRadius, point.radius); + } + return maxRadius; +} + +function applyAdaptiveRadiusExpansion(positions, ringTargets, spacingProfile) { + const expansion = clampFloat(spacingProfile?.radiusExpansion, 1, 1.65, 1); + if (!positions || !positions.size || expansion <= 1.001) return; + + const maxDepth = Math.max( + 1, + Number(spacingProfile?.maxDensityDepth) || 1, + ringTargets && ringTargets.size + ? Math.max(...Array.from(ringTargets.keys()).map(depth => Number(depth) || 0), 1) + : 1 + ); + const ringCountsByDepth = spacingProfile?.ringCountsByDepth || new Map(); + const scaleForDepth = depth => { + const normalizedDepth = Math.max(0, Number(depth) || 0); + if (normalizedDepth <= 0) return 1; + + const depthRatio = clampFloat((normalizedDepth - 1) / Math.max(1, maxDepth - 1), 0, 1, 0); + const outerWeight = Math.pow(depthRatio, 1.42); + const ringCount = ringCountsByDepth.get(Math.round(normalizedDepth)) || 0; + const crowdedRingBoost = Math.max(0, Math.sqrt(ringCount / 72) - 1) * 0.13 * outerWeight; + const innerCompression = (1 - outerWeight) * Math.min(0.08, (expansion - 1) * 0.42); + const adaptiveGrowth = (expansion - 1) * (0.04 + outerWeight * 1.06); + + return clampFloat( + 1 - innerCompression + adaptiveGrowth + crowdedRingBoost, + 0.93, + 1.68, + 1 + ); + }; + + if (ringTargets && ringTargets.size) { + for (const [depth, radius] of Array.from(ringTargets.entries())) { + if (depth <= 0 || !Number.isFinite(radius)) continue; + ringTargets.set(depth, radius * scaleForDepth(depth)); + } + } + + for (const point of positions.values()) { + if (!point || !Number.isFinite(point.radius) || point.radius <= 0.001) continue; + const depth = Math.max(0, Math.round(point.depth || 0)); + const depthScale = scaleForDepth(depth); + const angle = Number.isFinite(point.angle) + ? point.angle + : Math.atan2(point.y, point.x); + const radius = point.radius * depthScale; + point.radius = radius; + point.x = Math.cos(angle) * radius; + point.y = Math.sin(angle) * radius; + point.angle = angle; + point.labelSide = labelSideForAngle(angle); + if (Number.isFinite(point.ringRadius)) point.ringRadius *= depthScale; + if (Number.isFinite(point.ringBandMin)) point.ringBandMin *= depthScale; + if (Number.isFinite(point.ringBandMax)) point.ringBandMax *= depthScale; + } +} + +function placeOuterCircleNodes(nodes, positions, graph, radius, nodeGap, fallbackStart, options = {}) { + if (!nodes.length) return radius; + + const slot = (Math.PI * 2) / nodes.length; + const crowding = nodes.length * (nodeGap / Math.max(1, radius)); + const items = nodes.map((node, index) => ({ + node, + preferred: preferredAngleForNode(node, graph, positions, fallbackStart + index * slot) + })).sort((a, b) => normalizeAngle(a.preferred) - normalizeAngle(b.preferred)); + const offset = items.length + ? items[0].preferred - slot * 0.5 + : fallbackStart; + + for (let index = 0; index < items.length; index += 1) { + const item = items[index]; + const evenAngle = offset + index * slot; + const preferredDelta = shortestAngleDelta(evenAngle, item.preferred); + const angle = crowding < Math.PI * 1.35 + ? evenAngle + preferredDelta * 0.55 + : evenAngle; + const point = radialPoint(0, 0, radius, angle); + + positions.set(item.node.id, { + x: point.x, + y: point.y, + depth: options.depth || item.node.depth || 1, + angle, + radius, + labelSide: labelSideForAngle(angle), + centerX: 0, + centerY: 0, + external: Boolean(options.external || item.node.externalProxy), + externalGroup: Boolean(options.externalGroup), + externalFile: Boolean(options.externalFile || item.node.externalProxy), + fixed: Boolean(options.fixed) + }); + } + + return radius; +} + +function assignDepthRingTargets(positions, graph, spacingProfile, nodeGap) { + const ringTargets = new Map([[0, 0]]); + if (!positions || !positions.size) return ringTargets; + + const graphNodesById = graph.nodesById || new Map((graph.nodes || []).map(node => [node.id, node])); + let maxLinkDegree = 1; + for (const node of graph.nodes || []) { + maxLinkDegree = Math.max(maxLinkDegree, (node.linkCount || 0) + (node.backlinkCount || 0)); + } + + const byDepth = new Map(); + for (const [id, point] of positions.entries()) { + if (!point) continue; + const depth = Math.max(0, Math.round(point.depth || 0)); + const node = graphNodesById.get(id); + const visualRadius = node ? nodeRadius(node, point, maxLinkDegree) : 6; + const diameter = visualRadius * 2 + Math.max(18, nodeGap * 0.36); + if (!byDepth.has(depth)) { + byDepth.set(depth, { count: 0, diameterTotal: 0, maxDiameter: 0, external: 0, linkPressure: 0 }); + } + const entry = byDepth.get(depth); + entry.count += 1; + entry.diameterTotal += diameter; + entry.maxDiameter = Math.max(entry.maxDiameter, diameter); + entry.linkPressure += spacingProfile.incidentPressureByNode.get(id) || 0; + if (point.external || node?.externalProxy || node?.type === "external") entry.external += 1; + } + + let previousRadius = 0; + const depths = Array.from(byDepth.keys()).filter(depth => depth > 0).sort((a, b) => a - b); + const maxDepth = Math.max(...depths, 1); + const totalNodes = Array.from(byDepth.values()).reduce((sum, entry) => sum + entry.count, 0); + const globalCompression = clampFloat(1 - Math.log10(Math.max(1, totalNodes)) * 0.085, 0.58, 0.9, 0.75); + for (const depth of depths) { + const entry = byDepth.get(depth); + const avgDiameter = entry.diameterTotal / Math.max(1, entry.count); + const rawCircumferenceDemand = entry.count > 1 + ? (entry.count * avgDiameter) / (Math.PI * 2) + : 0; + const compressedDemand = rawCircumferenceDemand > 0 + ? Math.pow(rawCircumferenceDemand, 0.64) * Math.pow(spacingProfile.ringGap, 0.36) + : 0; + const depthRatio = depth / Math.max(1, maxDepth); + const outerExpansion = 1 + Math.pow(depthRatio, 1.35) * 0.34; + const pressureExpansion = 1 + Math.min(0.28, Math.sqrt(entry.linkPressure / Math.max(1, entry.count)) * 0.036); + const depthCompression = clampFloat(1 - depthRatio * 0.1, 0.84, 0.98, 0.92); + const baseRadius = depth * spacingProfile.ringGap * globalCompression * depthCompression * outerExpansion * pressureExpansion; + const minSeparatedRadius = previousRadius + spacingProfile.ringGap * (entry.external ? 0.54 : 0.62); + const crowdingRadius = compressedDemand * globalCompression * outerExpansion * pressureExpansion + entry.maxDiameter * 1.45; + const maxStepRadius = previousRadius + spacingProfile.ringGap * (entry.external ? 1.45 : 1.72) * outerExpansion; + const radius = Math.min( + maxStepRadius, + Math.max(baseRadius, minSeparatedRadius, crowdingRadius) + ); + ringTargets.set(depth, radius); + previousRadius = radius; + } + + for (const point of positions.values()) { + if (!point) continue; + const depth = Math.max(0, Math.round(point.depth || 0)); + const targetRadius = ringTargets.get(depth); + if (!Number.isFinite(targetRadius)) continue; + const currentAngle = Number.isFinite(point.angle) ? point.angle : Math.atan2(point.y, point.x); + point.ringRadius = targetRadius; + if (depth === 0) { + point.x = 0; + point.y = 0; + point.radius = 0; + point.angle = currentAngle; + continue; + } + point.x = Math.cos(currentAngle) * targetRadius; + point.y = Math.sin(currentAngle) * targetRadius; + point.radius = targetRadius; + point.angle = currentAngle; + point.labelSide = labelSideForAngle(currentAngle); + } + + return ringTargets; +} + +function resolveRadialCollisions(positions, graph, spacingProfile, nodeGap, ringTargets = new Map()) { + if (!positions || positions.size < 2) return; + + const graphNodesById = graph.nodesById || new Map((graph.nodes || []).map(node => [node.id, node])); + let maxLinkDegree = 1; + for (const node of graph.nodes || []) { + maxLinkDegree = Math.max(maxLinkDegree, (node.linkCount || 0) + (node.backlinkCount || 0)); + } + + const basePad = clampNumber(nodeGap * 1.12, 54, 280, 96); + const items = []; + let maxCollisionRadius = 1; + + for (const [id, point] of positions.entries()) { + const node = graphNodesById.get(id); + if (!node || !point) continue; + + const visualRadius = nodeRadius(node, point, maxLinkDegree) * 1.72; + const spacingPad = Math.min(320, basePad + labelCollisionPadding(node)); + const collisionRadius = visualRadius + spacingPad; + const depth = Math.max(0, Math.round(point.depth || 0)); + const ringRadius = Number.isFinite(point.ringRadius) + ? point.ringRadius + : ringTargets.get(depth); + maxCollisionRadius = Math.max(maxCollisionRadius, collisionRadius); + items.push({ + id, + node, + point, + depth, + visualRadius, + collisionRadius, + gravity: clampFloat(Math.sqrt(Math.max(1, visualRadius)) / 3.1, 0.85, 2.55, 1), + fixed: id === graph.rootId || Boolean(point.fixed), + anchorX: point.x, + anchorY: point.y, + anchorRadius: Number.isFinite(point.radius) ? point.radius : Math.hypot(point.x, point.y), + anchorAngle: Number.isFinite(point.angle) ? point.angle : Math.atan2(point.y, point.x), + ringRadius: Number.isFinite(ringRadius) ? ringRadius : null + }); + } + + if (items.length < 2) return; + + const iterations = items.length > 3500 ? 9 : items.length > 1200 ? 11 : 16; + const cellSize = Math.max(112, maxCollisionRadius * 2.48); + const separateItems = (strength, softRepel = 0.16) => { + let moved = false; + const grid = new Map(); + + for (let index = 0; index < items.length; index += 1) { + const item = items[index]; + const gx = Math.floor(item.point.x / cellSize); + const gy = Math.floor(item.point.y / cellSize); + const key = `${gx},${gy}`; + if (!grid.has(key)) grid.set(key, []); + grid.get(key).push(index); + } + + for (let index = 0; index < items.length; index += 1) { + const item = items[index]; + const gx = Math.floor(item.point.x / cellSize); + const gy = Math.floor(item.point.y / cellSize); + + for (let x = gx - 1; x <= gx + 1; x += 1) { + for (let y = gy - 1; y <= gy + 1; y += 1) { + const bucket = grid.get(`${x},${y}`); + if (!bucket) continue; + + for (const otherIndex of bucket) { + if (otherIndex <= index) continue; + const other = items[otherIndex]; + if (item.fixed && other.fixed) continue; + + let dx = item.point.x - other.point.x; + let dy = item.point.y - other.point.y; + let distance = Math.hypot(dx, dy); + + if (distance < 0.001) { + const angle = deterministicPairAngle(item.id, other.id); + dx = Math.cos(angle); + dy = Math.sin(angle); + distance = 1; + } + + const minDistance = item.collisionRadius + other.collisionRadius; + const gravity = Math.sqrt(item.gravity * other.gravity); + const softDistance = minDistance + (item.visualRadius + other.visualRadius) * 1.75 * gravity; + if (distance >= softDistance) continue; + + const overlapPush = distance < minDistance + ? (minDistance - distance) * strength + : 0; + const softPush = distance >= minDistance + ? (softDistance - distance) * softRepel + : (softDistance - minDistance) * softRepel * 0.35; + const push = (overlapPush + softPush) * gravity + 0.01; + const nx = dx / distance; + const ny = dy / distance; + const itemShare = item.fixed ? 0 : other.fixed ? 1 : 0.5; + const otherShare = other.fixed ? 0 : item.fixed ? 1 : 0.5; + + item.point.x += nx * push * itemShare; + item.point.y += ny * push * itemShare; + other.point.x -= nx * push * otherShare; + other.point.y -= ny * push * otherShare; + moved = true; + } + } + } + } + return moved; + }; + const pullItemsToRings = strength => { + for (const item of items) { + if (item.fixed || !Number.isFinite(item.ringRadius)) continue; + const currentRadius = Math.max(0.001, Math.hypot(item.point.x, item.point.y)); + const currentAngle = Math.atan2(item.point.y, item.point.x); + const external = item.node.externalProxy || item.node.type === "external"; + const anglePull = external ? 0.016 : 0.024; + const ringTolerance = Math.max( + item.visualRadius * (external ? 1.9 : 1.45), + spacingProfile.ringGap * (external ? 0.15 : 0.105) + ); + const nextAngle = currentAngle + shortestAngleDelta(currentAngle, item.anchorAngle) * anglePull; + const pulledRadius = currentRadius + (item.ringRadius - currentRadius) * strength; + const nextRadius = clampFloat( + pulledRadius, + Math.max(0, item.ringRadius - ringTolerance), + item.ringRadius + ringTolerance, + item.ringRadius + ); + item.point.x = Math.cos(nextAngle) * nextRadius; + item.point.y = Math.sin(nextAngle) * nextRadius; + } + }; + + for (let pass = 0; pass < iterations; pass += 1) { + separateItems(pass === 0 ? 0.9 : 0.72, pass < 3 ? 0.22 : 0.13); + pullItemsToRings(pass < 3 ? 0.42 : 0.28); + } + + for (let finalPass = 0; finalPass < 5; finalPass += 1) { + pullItemsToRings(finalPass === 0 ? 0.18 : 0.1); + if (!separateItems(finalPass === 0 ? 1 : 0.82, 0.05)) break; + } + + for (const item of items) { + const radius = Math.hypot(item.point.x, item.point.y); + if (radius < 0.001) continue; + item.point.radius = radius; + item.point.angle = Math.atan2(item.point.y, item.point.x); + item.point.labelSide = labelSideForAngle(item.point.angle); + } +} + +function enforceDepthRingBands(positions, graph, spacingProfile, nodeGap, ringTargets = new Map()) { + if (!positions || positions.size < 2) return; + + const graphNodesById = graph.nodesById || new Map((graph.nodes || []).map(node => [node.id, node])); + let maxLinkDegree = 1; + for (const node of graph.nodes || []) { + maxLinkDegree = Math.max(maxLinkDegree, (node.linkCount || 0) + (node.backlinkCount || 0)); + } + + const byDepth = new Map(); + for (const [id, point] of positions.entries()) { + const node = graphNodesById.get(id); + if (!node || !point) continue; + + const depth = Math.max(0, Math.round(point.depth || 0)); + if (depth === 0) { + point.x = 0; + point.y = 0; + point.radius = 0; + point.angle = -Math.PI / 2; + continue; + } + + const visualRadius = nodeRadius(node, point, maxLinkDegree); + const labelDemand = labelCollisionPadding(node) + + labelArcPadding(node) * clampFloat(0.58 + Math.min(1, depth / 4) * 0.42, 0.58, 1, 0.72); + const arcDemand = visualRadius * 2.75 + labelDemand * 2.7 + Math.max(22, nodeGap * 0.22); + const currentAngle = normalizeAngle(Number.isFinite(point.angle) ? point.angle : Math.atan2(point.y, point.x)); + const parentPoint = node.parentId !== null && node.parentId !== undefined + ? positions.get(node.parentId) + : null; + const parentAngle = parentPoint && Number.isFinite(parentPoint.angle) + ? normalizeAngle(parentPoint.angle) + : currentAngle; + const preferred = parentPoint + ? blendAngles(currentAngle, parentAngle, 0.68) + : currentAngle; + if (!byDepth.has(depth)) byDepth.set(depth, []); + byDepth.get(depth).push({ + id, + node, + point, + depth, + parentId: node.parentId, + parentAngle, + visualRadius, + arcDemand, + preferred + }); + } + + let previousOuterRadius = 0; + const depths = Array.from(byDepth.keys()).sort((a, b) => a - b); + const maxDepth = Math.max(...depths, 1); + for (const depth of depths) { + const items = byDepth.get(depth); + if (!items || !items.length) continue; + + const totalArcDemand = items.reduce((sum, item) => sum + item.arcDemand, 0); + const maxVisualRadius = Math.max(...items.map(item => item.visualRadius), 4); + const baseTarget = Number.isFinite(ringTargets.get(depth)) + ? ringTargets.get(depth) + : Math.max(spacingProfile.ringGap * depth, previousOuterRadius + spacingProfile.ringGap * 0.62); + const depthRatio = clampFloat((depth - 1) / Math.max(1, maxDepth - 1), 0, 1, 0); + const outerDensity = Math.pow(depthRatio, 1.35); + const laneUtilization = clampFloat(0.72 - outerDensity * 0.16 - Math.min(0.1, items.length / 4200), 0.5, 0.72, 0.62); + const baseCapacity = Math.max(1, Math.PI * 2 * baseTarget * laneUtilization); + const maxLaneCount = outerRingLaneLimit(items.length, depthRatio); + const laneCount = clampNumber( + Math.ceil(totalArcDemand / baseCapacity), + 1, + maxLaneCount, + 1 + ); + const laneGap = Math.max( + maxVisualRadius * (2.45 + outerDensity * 0.7) + Math.max(12, nodeGap * (0.13 + outerDensity * 0.04)), + spacingProfile.ringGap * (0.11 + outerDensity * 0.045) + ); + const depthJaggedFactor = ringJaggedDepthFactor(depth, baseTarget, spacingProfile.ringGap, items.length, totalArcDemand); + const firstLaneRadius = Math.max( + baseTarget - laneGap * (laneCount - 1) * 0.5, + previousOuterRadius + laneGap * 0.86 + ); + const lanes = Array.from({ length: laneCount }, () => []); + const orderedGroups = orderRingGroupsByParent(items); + for (const group of orderedGroups) { + const groupItems = orderRingItemsByPreferredGap(group.items); + if (laneCount === 1) { + lanes[0].push(...groupItems); + continue; + } + + const laneIndex = chooseRingLaneForGroup(lanes, groupItems, group.parentAngle); + lanes[laneIndex].push(...groupItems); + } + + const laneRadii = []; + const laneOuterRadii = []; + for (let laneIndex = 0; laneIndex < lanes.length; laneIndex += 1) { + const laneItems = orderRingItemsByParentThenPreferred(lanes[laneIndex]); + if (!laneItems.length) continue; + + let laneRadius = firstLaneRadius + laneIndex * laneGap; + const laneArcDemand = laneItems.reduce((sum, item) => sum + item.arcDemand, 0); + const requiredRadius = laneArcDemand / (Math.PI * 2 * laneUtilization); + laneRadius = Math.max(laneRadius, requiredRadius, previousOuterRadius + laneGap * 0.72); + const laneJaggedFactor = depthJaggedFactor * ringJaggedDensityFactor( + laneItems.length, + countRingParents(laneItems), + laneArcDemand, + laneRadius + ); + const candidateJitterBand = Math.min( + spacingProfile.ringGap * RING_JAGGED_BAND_FACTOR * laneJaggedFactor, + laneRadius * RING_JAGGED_MAX_FACTOR * Math.min(1.35, laneJaggedFactor) + ); + laneRadius = Math.max(laneRadius, previousOuterRadius + candidateJitterBand * 0.56 + laneGap * 0.68); + const jitterBand = Math.min( + candidateJitterBand, + Math.max(0, laneRadius - previousOuterRadius - maxVisualRadius * 2.2 - Math.max(12, nodeGap * 0.08)) + ); + const laneOccupancy = laneArcDemand / Math.max(1, Math.PI * 2 * laneRadius); + placeItemsOnRingLane(laneItems, laneRadius, { + jitterBand, + preservePreferred: laneItems.length < 90 || laneOccupancy < 0.38 || outerDensity < 0.28 + }); + laneRadii.push(laneRadius); + laneOuterRadii.push(laneRadius + jitterBand); + } + + if (laneRadii.length) { + const ringRadius = medianNumber(laneRadii, baseTarget); + ringTargets.set(depth, ringRadius); + previousOuterRadius = Math.max(...laneOuterRadii) + maxVisualRadius * 1.55 + Math.max(12, nodeGap * 0.08); + } + } +} + +function orderRingItemsByPreferredGap(items) { + const sorted = (items || []) + .slice() + .sort((a, b) => normalizeAngle(a.preferred) - normalizeAngle(b.preferred)); + if (sorted.length <= 2) return sorted; + + let largestGap = -1; + let largestGapIndex = 0; + for (let index = 0; index < sorted.length; index += 1) { + const current = normalizeAngle(sorted[index].preferred); + const next = normalizeAngle(sorted[(index + 1) % sorted.length].preferred) + (index === sorted.length - 1 ? Math.PI * 2 : 0); + const gap = next - current; + if (gap > largestGap) { + largestGap = gap; + largestGapIndex = index; + } + } + + const start = (largestGapIndex + 1) % sorted.length; + return sorted.slice(start).concat(sorted.slice(0, start)); +} + +function outerRingLaneLimit(itemCount, depthRatio) { + const count = Math.max(0, Number(itemCount) || 0); + const outer = clampFloat(depthRatio, 0, 1, 0); + const base = count > 1600 + ? 14 + : count > 900 + ? 12 + : count > 420 + ? 10 + : count > 180 + ? 8 + : count > 80 + ? 6 + : 4; + const outerBonus = outer > 0.72 + ? 3 + : outer > 0.48 + ? 2 + : outer > 0.28 + ? 1 + : 0; + return clampNumber(base + outerBonus, 4, 16, 6); +} + +function orderRingGroupsByParent(items) { + const groupsByParent = new Map(); + for (const item of items || []) { + const key = item.parentId === null || item.parentId === undefined ? item.id : item.parentId; + if (!groupsByParent.has(key)) { + groupsByParent.set(key, { + parentId: key, + parentAngle: item.parentAngle, + arcDemand: 0, + items: [] + }); + } + const group = groupsByParent.get(key); + group.items.push(item); + group.arcDemand += item.arcDemand || 0; + group.parentAngle = averageAngles(group.items.map(child => child.parentAngle), group.parentAngle); + } + + return Array.from(groupsByParent.values()) + .sort((a, b) => normalizeAngle(a.parentAngle) - normalizeAngle(b.parentAngle)); +} + +function chooseRingLaneForGroup(lanes, groupItems, parentAngle) { + let bestIndex = 0; + let bestScore = Infinity; + const groupArc = groupItems.reduce((sum, item) => sum + (item.arcDemand || 0), 0); + + for (let index = 0; index < lanes.length; index += 1) { + const lane = lanes[index]; + const laneArc = lane.reduce((sum, item) => sum + (item.arcDemand || 0), 0); + const last = lane[lane.length - 1]; + const angleCost = last + ? Math.abs(shortestAngleDelta(last.parentAngle || last.preferred, parentAngle)) + : 0; + const score = laneArc + groupArc * 0.18 + angleCost * 180; + if (score < bestScore) { + bestScore = score; + bestIndex = index; + } + } + + return bestIndex; +} + +function orderRingItemsByParentThenPreferred(items) { + const groups = orderRingGroupsByParent(items); + const ordered = []; + for (const group of groups) { + ordered.push(...orderRingItemsByPreferredGap(group.items)); + } + return ordered; +} + +function placeItemsOnRingLane(items, radius, options = {}) { + if (!items.length || !Number.isFinite(radius) || radius <= 0) return; + + const fullCircle = Math.PI * 2; + const arcs = items.map(item => Math.max(0.003, item.arcDemand / radius)); + const totalArc = arcs.reduce((sum, arc) => sum + arc, 0); + const jitterBand = clampFloat(options.jitterBand, 0, Math.max(0, radius * RING_JAGGED_MAX_FACTOR), 0); + const parentOffsets = parentRadialOffsetsForLane(items, jitterBand); + const minGap = Math.min(0.11, Math.max(0.012, (totalArc / Math.max(1, items.length)) * 0.18)); + const minDemand = totalArc + minGap * Math.max(0, items.length - 1); + const preservePreferred = options.preservePreferred !== false; + const canPreservePreferred = preservePreferred && items.length <= 640 && minDemand < fullCircle * 0.9; + + if (canPreservePreferred && placeItemsNearPreferredAngles(items, arcs, radius, jitterBand, parentOffsets, minGap)) { + return; + } + + const extraGap = Math.max(0, (fullCircle - totalArc) / items.length); + let cursor = normalizeAngle(items[0].preferred) - (arcs[0] + extraGap) * 0.5; + for (let index = 0; index < items.length; index += 1) { + const width = arcs[index] + extraGap; + const angle = cursor + width * 0.5; + setRingLanePoint(items[index], radius, angle, jitterBand, parentOffsets); + cursor += width; + } +} + +function placeItemsNearPreferredAngles(items, arcs, radius, jitterBand, parentOffsets, minGap) { + if (!items.length) return true; + const fullCircle = Math.PI * 2; + const entries = items.map((item, index) => ({ + item, + arc: arcs[index], + preferred: normalizeAngle(Number.isFinite(item.preferred) ? item.preferred : item.parentAngle || 0) + })).sort((a, b) => a.preferred - b.preferred); + + if (entries.length > 1) { + let largestGap = -1; + let largestGapIndex = 0; + for (let index = 0; index < entries.length; index += 1) { + const current = entries[index].preferred; + const next = entries[(index + 1) % entries.length].preferred + (index === entries.length - 1 ? fullCircle : 0); + const gap = next - current; + if (gap > largestGap) { + largestGap = gap; + largestGapIndex = index; + } + } + const start = (largestGapIndex + 1) % entries.length; + const rotated = entries.slice(start).concat(entries.slice(0, start)); + entries.splice(0, entries.length, ...rotated); + } + + const angles = []; + let wrapOffset = 0; + let previous = entries[0].preferred; + angles[0] = previous; + for (let index = 1; index < entries.length; index += 1) { + let angle = entries[index].preferred + wrapOffset; + while (angle <= previous) { + wrapOffset += fullCircle; + angle = entries[index].preferred + wrapOffset; + } + angles[index] = angle; + previous = angle; + } + + const preferredCenter = (angles[0] + angles[angles.length - 1]) * 0.5; + for (let pass = 0; pass < 3; pass += 1) { + for (let index = 1; index < entries.length; index += 1) { + const minDelta = (entries[index - 1].arc + entries[index].arc) * 0.5 + minGap; + if (angles[index] - angles[index - 1] < minDelta) { + angles[index] = angles[index - 1] + minDelta; + } + } + } + + const span = angles[angles.length - 1] + entries[entries.length - 1].arc * 0.5 + - (angles[0] - entries[0].arc * 0.5); + if (span > fullCircle - minGap) return false; + + const currentCenter = (angles[0] + angles[angles.length - 1]) * 0.5; + const centerShift = preferredCenter - currentCenter; + for (let index = 0; index < entries.length; index += 1) { + setRingLanePoint(entries[index].item, radius, normalizeAngle(angles[index] + centerShift), jitterBand, parentOffsets); + } + return true; +} + +function setRingLanePoint(item, radius, angle, jitterBand, parentOffsets) { + const actualRadius = jaggedRingRadius(item, radius, jitterBand, parentOffsets); + const point = item.point; + point.x = Math.cos(angle) * actualRadius; + point.y = Math.sin(angle) * actualRadius; + point.radius = actualRadius; + point.ringRadius = radius; + point.ringBandMin = radius - jitterBand; + point.ringBandMax = radius + jitterBand; + point.angle = angle; + point.labelSide = labelSideForAngle(angle); +} + +function parentRadialOffsetsForLane(items, jitterBand) { + const offsets = new Map(); + if (!Array.isArray(items) || !items.length || !Number.isFinite(jitterBand) || jitterBand <= 0) return offsets; + + const parentOrder = []; + const seen = new Set(); + for (const item of items) { + const key = ringParentKey(item); + if (seen.has(key)) continue; + seen.add(key); + parentOrder.push(key); + } + + const lanePattern = [0, -0.96, 0.96, -0.54, 0.54, -0.78, 0.78, -0.28, 0.28]; + for (let index = 0; index < parentOrder.length; index += 1) { + const key = parentOrder[index]; + const base = lanePattern[index % lanePattern.length]; + const variation = deterministicUnitOffset(key, "ring-parent-variation") * 0.12; + offsets.set(key, clampFloat(base + variation, -1, 1, 0) * jitterBand); + } + + return offsets; +} + +function ringJaggedDepthFactor(depth, radius, ringGap, itemCount, arcDemand) { + const normalizedDepth = Math.max(0, Number(depth) || 0); + const normalizedRadius = Number.isFinite(radius) && Number.isFinite(ringGap) && ringGap > 0 + ? radius / ringGap + : normalizedDepth; + const occupancy = Number.isFinite(radius) && radius > 0 + ? arcDemand / Math.max(1, Math.PI * 2 * radius) + : 0; + const outerFactor = clampFloat(0.82 + normalizedDepth * 0.06 + Math.sqrt(Math.max(0, normalizedRadius)) * 0.13, 0.86, 1.62, 1); + const densityFactor = itemCount <= 5 + ? 0.48 + : itemCount <= 10 + ? 0.68 + : occupancy < 0.12 + ? 0.62 + : occupancy < 0.24 + ? 0.82 + : occupancy > 0.52 + ? 1.16 + : 1; + return clampFloat(outerFactor * densityFactor, 0.42, 1.72, 1); +} + +function ringJaggedDensityFactor(itemCount, parentCount, arcDemand, radius) { + const count = Math.max(0, Number(itemCount) || 0); + const parents = Math.max(1, Number(parentCount) || 1); + const occupancy = Number.isFinite(radius) && radius > 0 + ? arcDemand / Math.max(1, Math.PI * 2 * radius) + : 0; + const childFactor = count <= 3 + ? 0.42 + : count <= 7 + ? 0.66 + : count <= 14 + ? 0.86 + : 1.05; + const parentFactor = parents <= 1 + ? 0.52 + : parents <= 2 + ? 0.72 + : parents <= 4 + ? 0.92 + : 1.08; + const occupancyFactor = occupancy < 0.1 + ? 0.56 + : occupancy < 0.2 + ? 0.78 + : occupancy > 0.55 + ? 1.14 + : 1; + return clampFloat(childFactor * parentFactor * occupancyFactor, 0.32, 1.22, 1); +} + +function countRingParents(items) { + const parents = new Set(); + for (const item of items || []) { + parents.add(ringParentKey(item)); + } + return parents.size; +} + +function jaggedRingRadius(item, radius, jitterBand, parentOffsets = null) { + if (!item || !Number.isFinite(jitterBand) || jitterBand <= 0) return radius; + const parentKey = ringParentKey(item); + const parentOffset = parentOffsets && parentOffsets.has(parentKey) + ? parentOffsets.get(parentKey) + : deterministicUnitOffset(parentKey, "ring-parent") * jitterBand * 0.92; + const childOffset = deterministicUnitOffset(item.id, "ring-node") * jitterBand * 0.08; + return radius + clampFloat(parentOffset + childOffset, -jitterBand, jitterBand, 0); +} + +function ringParentKey(item) { + return item && item.parentId !== null && item.parentId !== undefined ? item.parentId : item?.id; +} + +function blendAngles(from, to, amount) { + return normalizeAngle(from + shortestAngleDelta(from, to) * clampFloat(amount, 0, 1, 0.5)); +} + +function labelCollisionPadding(node) { + const titleLength = Array.from(String(node.title || "")).length; + const typePad = node.type === "folder" ? 13 : node.type === "external" || node.externalProxy ? 9 : 5; + return clampNumber(Math.sqrt(Math.max(1, titleLength)) * 4.2 + typePad, 10, 60, 16); +} + +function labelArcPadding(node) { + const titleLength = Array.from(String(node.title || "")).length; + const folderPad = node.type === "folder" ? 14 : 0; + const externalPad = node.type === "external" || node.externalProxy ? 9 : 0; + return clampNumber( + Math.sqrt(Math.max(1, titleLength)) * 7.5 + Math.min(110, titleLength * 1.25) + folderPad + externalPad, + 24, + 150, + 48 + ); +} + +function deterministicPairAngle(a, b) { + const text = `${a}|${b}`; + let hash = 2166136261; + for (let index = 0; index < text.length; index += 1) { + hash ^= text.charCodeAt(index); + hash = Math.imul(hash, 16777619); + } + return ((hash >>> 0) / 4294967296) * Math.PI * 2; +} + +function deterministicUnitOffset(value, salt) { + return Math.sin(deterministicPairAngle(String(value || ""), String(salt || ""))); +} + +function preferredAngleForNode(node, graph, positions, fallbackAngle) { + const angles = []; + + if (node.externalParentId && positions.has(node.externalParentId)) { + angles.push(positions.get(node.externalParentId).angle); + } + + for (const edge of graph.linkEdges || []) { + const otherId = relatedEndpointForNode(node, edge, graph); + if (!otherId) continue; + const otherPoint = positions.get(otherId); + if (otherPoint && Number.isFinite(otherPoint.angle)) { + angles.push(otherPoint.angle); + } + } + + return averageAngles(angles, fallbackAngle); +} + +function relatedEndpointForNode(node, edge, graph) { + if (edge.source === node.id) return edge.target; + if (edge.target === node.id) return edge.source; + + const nodesById = graph.nodesById; + const sourceNode = nodesById && nodesById.get(edge.source); + const targetNode = nodesById && nodesById.get(edge.target); + if (sourceNode && sourceNode.externalParentId === node.id) return edge.target; + if (targetNode && targetNode.externalParentId === node.id) return edge.source; + return null; +} + +function averageAngles(angles, fallbackAngle) { + if (!angles.length) return fallbackAngle; + const sum = angles.reduce((acc, angle) => { + acc.x += Math.cos(angle); + acc.y += Math.sin(angle); + return acc; + }, { x: 0, y: 0 }); + if (Math.abs(sum.x) < 0.0001 && Math.abs(sum.y) < 0.0001) return fallbackAngle; + return Math.atan2(sum.y, sum.x); +} + +function radialLayoutBounds(positions, routeMaxRadius) { + let minX = -Math.max(1, routeMaxRadius || 1); + let minY = -Math.max(1, routeMaxRadius || 1); + let maxX = Math.max(1, routeMaxRadius || 1); + let maxY = Math.max(1, routeMaxRadius || 1); + + for (const point of positions.values()) { + const labelPadX = 170; + const labelPadTop = 46; + const labelPadBottom = 126; + minX = Math.min(minX, point.x - labelPadX); + minY = Math.min(minY, point.y - labelPadTop); + maxX = Math.max(maxX, point.x + labelPadX); + maxY = Math.max(maxY, point.y + labelPadBottom); + } + + return { minX, minY, maxX, maxY }; +} + +function shiftRadialLayout(positions, routes, offsetX, offsetY) { + for (const point of positions.values()) { + point.x += offsetX; + point.y += offsetY; + point.centerX = offsetX; + point.centerY = offsetY; + } + + for (const route of routes.values()) { + if (!Number.isFinite(route.centerX) || !Number.isFinite(route.centerY)) continue; + route.centerX += offsetX; + route.centerY += offsetY; + } +} + +function anchorLayoutHomePositions(positions) { + for (const point of positions.values()) { + if (!point) continue; + point.homeX = point.x; + point.homeY = point.y; + point.homeRadius = point.radius; + point.homeAngle = point.angle; + } +} + +function applyRadialSwirl(positions, graph, spacingProfile, strength) { + const amount = clampFloat(strength, 0, 1, 0); + if (!positions || !positions.size || amount <= 0.001) return; + + const ringGap = Math.max(1, Number(spacingProfile?.ringGap) || DEFAULT_RING_SPACING); + const rootId = graph?.rootId || ROOT_ID; + const direction = deterministicUnitOffset(rootId || "vault", "swirl-direction") >= 0 ? 1 : -1; + + for (const [id, point] of positions.entries()) { + if (!point || !Number.isFinite(point.radius) || point.radius <= 0.001) continue; + + const depth = Math.max(0, Number(point.depth) || 0); + const baseAngle = Number.isFinite(point.angle) + ? point.angle + : Math.atan2(point.y, point.x); + const radialPhase = Math.sqrt(Math.max(0, point.radius) / ringGap); + const armVariation = deterministicUnitOffset(id, "swirl-arm") * 0.16; + const wave = Math.sin(baseAngle * 2.35 + depth * 0.78) * 0.11; + const turn = direction * amount * (depth * 0.32 + radialPhase * 0.22 + armVariation + wave); + const angle = normalizeAngle(baseAngle + turn); + + point.x = Math.cos(angle) * point.radius; + point.y = Math.sin(angle) * point.radius; + point.angle = angle; + point.swirlOffset = turn; + point.labelSide = labelSideForAngle(angle); + } +} + +function computeLayoutRings(positions, ringTargets = null) { + if (ringTargets && ringTargets.size) { + const ringsByKey = new Map(); + for (const point of positions.values()) { + if (!point || !Number.isFinite(point.depth) || point.depth <= 0) continue; + const depth = Math.max(0, Math.round(point.depth || 0)); + const radius = Number.isFinite(point.ringRadius) + ? point.ringRadius + : ringTargets.get(depth); + if (!Number.isFinite(radius) || radius <= 0) continue; + const key = `${depth}:${Math.round(radius)}`; + const existing = ringsByKey.get(key); + if (existing) { + existing.count += 1; + existing.radiusTotal += radius; + } else { + ringsByKey.set(key, { + depth, + radiusTotal: radius, + count: 1 + }); + } + } + return Array.from(ringsByKey.values()) + .map(ring => ({ + depth: ring.depth, + radius: ring.radiusTotal / Math.max(1, ring.count), + count: ring.count + })) + .sort((a, b) => a.radius - b.radius); + } + + const byDepth = new Map(); + for (const point of positions.values()) { + if (!point || !Number.isFinite(point.depth) || point.depth <= 0) continue; + if (!Number.isFinite(point.radius) || point.radius <= 0) continue; + if (!byDepth.has(point.depth)) byDepth.set(point.depth, []); + byDepth.get(point.depth).push(point.radius); + } + + return Array.from(byDepth.entries()) + .map(([depth, radii]) => { + const sorted = radii.slice().sort((a, b) => a - b); + const middle = Math.floor(sorted.length / 2); + const median = sorted.length % 2 + ? sorted[middle] + : (sorted[middle - 1] + sorted[middle]) / 2; + return { depth, radius: median, count: sorted.length }; + }) + .filter(ring => ring.count >= 2) + .sort((a, b) => a.depth - b.depth); +} + +function radialPoint(centerX, centerY, radius, angle) { + return { + x: centerX + Math.cos(angle) * radius, + y: centerY + Math.sin(angle) * radius + }; +} + +function drawCanvasRingPath(ctx, centerX, centerY, radius, depth = 0, swirlStrength = 0, orbitPhase = 0) { + const strength = clampFloat(swirlStrength, 0, 1, 0); + if (strength <= 0.001) { + ctx.arc(centerX, centerY, radius, 0, Math.PI * 2); + return; + } + + const fullCircle = Math.PI * 2; + const steps = 180; + const amplitude = Math.min(radius * 0.045, 58) * strength; + const phase = depth * 0.74 + strength * 0.9 + orbitPhase; + + for (let step = 0; step <= steps; step += 1) { + const t = step / steps; + const angle = t * fullCircle; + const twist = Math.sin(angle - phase) * 0.08 * strength; + const ripple = Math.sin(angle * 2 + phase) * amplitude + + Math.sin(angle * 5 - phase * 0.7) * amplitude * 0.26; + const r = Math.max(1, radius + ripple); + const x = centerX + Math.cos(angle + twist) * r; + const y = centerY + Math.sin(angle + twist) * r; + if (step === 0) ctx.moveTo(x, y); + else ctx.lineTo(x, y); + } + ctx.closePath(); +} + +function swirlOrbitAngleForRing(depth, radius, strength, elapsedSeconds) { + const amount = clampFloat(strength, 0, 1, 0); + if (amount <= 0.001 || depth <= 0 || !Number.isFinite(elapsedSeconds)) return 0; + + const depthIndex = Math.max(1, Math.round(depth)); + const direction = Math.floor((depthIndex - 1) / 2) % 2 === 0 ? 1 : -1; + const outerSlowdown = clampFloat(1 / Math.sqrt(1 + Math.max(0, depthIndex - 1) * 0.24), 0.46, 1, 1); + const radiusSlowdown = Number.isFinite(radius) && radius > 0 + ? clampFloat(Math.sqrt(DEFAULT_RING_SPACING / Math.max(DEFAULT_RING_SPACING, radius)), 0.56, 1, 1) + : 1; + const subtleVariation = 0.86 + Math.sin(depthIndex * 1.17) * 0.1 + Math.cos(depthIndex * 0.53) * 0.045; + const speed = SWIRL_BASE_SPEED_RAD_PER_SEC * amount * outerSlowdown * radiusSlowdown * subtleVariation; + return direction * speed * elapsedSeconds; +} + +function labelSideForAngle(angle) { + return Math.cos(angle) < -0.18 ? -1 : 1; +} + +function normalizeAngle(angle) { + const full = Math.PI * 2; + return ((angle % full) + full) % full; +} + +function shortestAngleDelta(from, to) { + const full = Math.PI * 2; + return ((to - from + Math.PI) % full + full) % full - Math.PI; +} + +function compareNodes(a, b) { + if (!a || !b) return 0; + if (a.type !== b.type) { + const order = { folder: 0, note: 1, external: 2, unresolved: 3 }; + return (order[a.type] || 9) - (order[b.type] || 9); + } + return a.title.localeCompare(b.title, undefined, { sensitivity: "base" }); +} + +function normalizeSettings(saved) { + const source = saved && typeof saved === "object" ? saved : {}; + const settings = Object.assign({}, DEFAULT_SETTINGS, source); + const hasLegacyBudget = source.atlasDepth === LEGACY_DEFAULT_SETTINGS.atlasDepth + && source.linkLimit === LEGACY_DEFAULT_SETTINGS.linkLimit + && source.renderNodeLimit === LEGACY_DEFAULT_SETTINGS.renderNodeLimit; + + if (hasLegacyBudget) { + settings.atlasDepth = DEFAULT_SETTINGS.atlasDepth; + settings.focusSiblingLimit = DEFAULT_SETTINGS.focusSiblingLimit; + settings.linkLimit = DEFAULT_SETTINGS.linkLimit; + settings.renderNodeLimit = DEFAULT_SETTINGS.renderNodeLimit; + settings.externalLinkAnchorLimit = DEFAULT_SETTINGS.externalLinkAnchorLimit; + if (source.enableLinkHover === LEGACY_DEFAULT_SETTINGS.enableLinkHover) { + settings.enableLinkHover = DEFAULT_SETTINGS.enableLinkHover; + } + } + + settings.atlasDepth = clampNumber(settings.atlasDepth, 1, MAX_ATLAS_DEPTH, DEFAULT_SETTINGS.atlasDepth); + settings.focusSiblingLimit = clampNumber(settings.focusSiblingLimit, 10, 1000, DEFAULT_SETTINGS.focusSiblingLimit); + settings.linkLimit = clampNumber(settings.linkLimit, 0, MAX_LINK_LIMIT, DEFAULT_SETTINGS.linkLimit); + settings.renderNodeLimit = clampNumber(settings.renderNodeLimit, 200, MAX_RENDER_NODE_LIMIT, DEFAULT_SETTINGS.renderNodeLimit); + settings.externalLinkAnchorLimit = clampNumber(settings.externalLinkAnchorLimit, 0, MAX_EXTERNAL_LINK_ANCHOR_LIMIT, DEFAULT_SETTINGS.externalLinkAnchorLimit); + settings.externalDetailMode = ["grouped", "selected", "exact"].includes(settings.externalDetailMode) + ? settings.externalDetailMode + : DEFAULT_SETTINGS.externalDetailMode; + settings.colorScheme = normalizeColorScheme(settings.colorScheme); + settings.labelVisibility = normalizeLabelVisibility(settings.labelVisibility); + settings.hoverHighlightMode = normalizeHoverHighlightMode( + source.hoverHighlightMode !== undefined + ? source.hoverHighlightMode + : settings.enableLinkHover + ? "note-links" + : DEFAULT_SETTINGS.hoverHighlightMode + ); + settings.enableLinkHover = hoverHighlightsNoteLinks(settings.hoverHighlightMode); + settings.swirlStrength = clampNumber(settings.swirlStrength, 0, MAX_SWIRL_STRENGTH, DEFAULT_SETTINGS.swirlStrength); + const legendItemIds = new Set(LEGEND_ITEM_DEFINITIONS.map(([id]) => id)); + settings.hiddenLegendItems = Array.isArray(settings.hiddenLegendItems) + ? settings.hiddenLegendItems.filter(id => legendItemIds.has(id)) + : DEFAULT_SETTINGS.hiddenLegendItems.slice(); + settings.ignoreFolders = Array.isArray(settings.ignoreFolders) + ? settings.ignoreFolders.filter(Boolean) + : DEFAULT_SETTINGS.ignoreFolders.slice(); + + return settings; +} + +function normalizeLabelVisibility(value) { + const normalized = String(value || "").trim(); + return LABEL_VISIBILITY_OPTIONS.some(([optionValue]) => optionValue === normalized) + ? normalized + : DEFAULT_SETTINGS.labelVisibility; +} + +function normalizeHoverHighlightMode(value) { + const normalized = String(value || "").trim(); + return HOVER_HIGHLIGHT_MODE_OPTIONS.some(([optionValue]) => optionValue === normalized) + ? normalized + : DEFAULT_SETTINGS.hoverHighlightMode; +} + +function hoverHighlightsNoteLinks(mode) { + return normalizeHoverHighlightMode(mode) === "note-links"; +} + +function hoverHighlightModeLabel(mode) { + const normalized = normalizeHoverHighlightMode(mode); + const option = HOVER_HIGHLIGHT_MODE_OPTIONS.find(([value]) => value === normalized); + return option ? option[1].toLowerCase() : normalized; +} + +function normalizeColorScheme(value) { + return COLOR_SCHEME_OPTIONS.includes(value) ? value : DEFAULT_SETTINGS.colorScheme; +} + +function nextColorScheme(value) { + const current = normalizeColorScheme(value); + const index = COLOR_SCHEME_OPTIONS.indexOf(current); + return COLOR_SCHEME_OPTIONS[(index + 1) % COLOR_SCHEME_OPTIONS.length]; +} + +function colorSchemeLabel(value) { + const scheme = normalizeColorScheme(value); + if (scheme === "day") return "Day"; + if (scheme === "night") return "Night"; + return "Auto"; +} + +function colorSchemeIcon(value) { + const scheme = normalizeColorScheme(value); + if (scheme === "day") return "sun"; + if (scheme === "night") return "moon"; + return "monitor"; +} + +function nodeWithinRoot(index, node, rootId) { + if (!node) return false; + if (rootId === ROOT_ID || rootId === null || rootId === undefined) return true; + + let current = node; + const seen = new Set(); + while (current && !seen.has(current.id)) { + if (current.id === rootId) return true; + seen.add(current.id); + current = current.parentId === null || current.parentId === undefined + ? null + : index.nodes.get(current.parentId); + } + return false; +} + +function vaultRootTitle(app) { + const name = app?.vault?.getName?.(); + return String(name || ROOT_TITLE).trim() || ROOT_TITLE; +} + +function basename(path) { + if (!path) return ROOT_TITLE; + const parts = path.split("/"); + return parts[parts.length - 1] || ROOT_TITLE; +} + +function depthOfPath(path) { + if (!path) return 0; + return path.split("/").filter(Boolean).length; +} + +function parentPath(path) { + const normalizedPath = normalizeVaultPath(path); + if (!normalizedPath || !normalizedPath.includes("/")) return ROOT_ID; + return normalizedPath.split("/").slice(0, -1).join("/"); +} + +function normalizeVaultPath(path) { + const normalized = String(path || "").trim().replace(/^\/+|\/+$/g, ""); + return normalized === "." ? ROOT_ID : normalized; +} + +function comparableTitle(title) { + return String(title || "") + .toLowerCase() + .normalize("NFKD") + .replace(/\.md$/i, "") + .replace(/^[^\p{Letter}\p{Number}]+/u, "") + .replace(/[^\p{Letter}\p{Number}]+/gu, ""); +} + +function normalizedQuery(query) { + return String(query || "").trim().toLowerCase(); +} + +function nodeMatches(node, query) { + if (!query) return false; + return String(node.title || "").toLowerCase().includes(query) + || String(node.path || "").toLowerCase().includes(query); +} + +function nodeRadius(node, point, maxLinkDegree = 1) { + const degree = Math.max(0, (node.linkCount || 0) + (node.backlinkCount || 0)); + const degreeRatio = Math.log1p(degree) / Math.max(1, Math.log1p(maxLinkDegree || 1)); + const clampedRatio = clampFloat(degreeRatio, 0, 1, 0); + const degreeCurve = Math.pow(clampedRatio, 0.48); + const hubCurve = Math.pow(clampedRatio, 1.32); + const degreeBoost = degreeCurve * 19 + + hubCurve * 20 + + Math.log2(degree + 1) * 1.35 + + Math.sqrt(degree) * 0.32; + + if (node.externalProxy) return node.type === "unresolved" ? 5.4 : Math.min(27, 5.8 + degreeBoost * 0.62); + if (node.type === "folder") { + const noteSignal = Math.log2((node.noteCount || node.descendantCount || 1) + 1); + return Math.min(66, 7 + noteSignal * 1.05 + degreeBoost * 1.18); + } + if (node.type === "external") { + const noteSignal = Math.log2((node.noteCount || 1) + 1); + return Math.min(38, 5.8 + noteSignal * 0.72 + degreeBoost * 0.9); + } + if (node.type === "unresolved") return Math.min(16, 4.2 + degreeBoost * 0.5); + return Math.min(58, 3.6 + degreeBoost * 1.05); +} + +function nodeMetric(node) { + if (node.externalProxy) return node.type === "unresolved" ? "outside unresolved" : "outside note"; + if (node.type === "folder") return node.representativeFile ? `${node.noteCount || 0} notes + meta` : `${node.noteCount || 0} notes`; + if (node.type === "external") return node.noteCount ? `${node.noteCount} outside notes` : "outside"; + if (node.type === "unresolved") return "unresolved"; + const total = (node.linkCount || 0) + (node.backlinkCount || 0); + return total ? `${total} links` : "note"; +} + +function assignCanvasLabelPriority(nodes, graph) { + const scored = nodes + .map(item => ({ item, priority: canvasLabelPriority(item, graph) })) + .sort((a, b) => b.priority - a.priority); + const denominator = Math.max(1, scored.length - 1); + + for (let index = 0; index < scored.length; index += 1) { + scored[index].item.labelRank = index; + scored[index].item.labelPriority = scored[index].priority; + scored[index].item.labelPercentile = 1 - index / denominator; + } +} + +function canvasLabelPriority(item, graph) { + const node = item.node || {}; + const degree = Math.max(0, (node.linkCount || 0) + (node.backlinkCount || 0)); + let score = Math.max(0, item.radius || 0) * 2.8 + + Math.log1p(degree) * 13 + + Math.sqrt(degree) * 1.4 + - Math.min(34, (node.depth || 0) * 3.2); + + if (graph && node.id === graph.rootId) score += 10000; + if (graph && node.id === graph.focusId) score += 9000; + if (item.searchMatch) score += 8000; + + if (node.type === "folder") { + score += 32 + Math.log1p(node.noteCount || node.descendantCount || 0) * 8; + if (node.representativeFile) score += 8; + } else if (node.type === "note") { + score += 10; + } else if (node.type === "external" || node.externalProxy) { + score -= 10; + } else if (node.type === "unresolved") { + score -= 22; + } + + return score; +} + +function zoomLabelStrength(item, zoom, graph) { + if (!item || !item.node) return 0; + if (item.searchMatch) return 1; + + const node = item.node; + const degree = Math.max(0, (node.linkCount || 0) + (node.backlinkCount || 0)); + const folder = node.type === "folder"; + const external = node.type === "external" || node.externalProxy; + const unresolved = node.type === "unresolved"; + const root = graph && node.id === graph.rootId; + if (root) { + return 0.68 + smoothstep(0.04, 0.2, zoom) * 0.32; + } + + const sizeSignal = clampFloat((item.radius - 4) / 42, 0, 1, 0); + const degreeSignal = clampFloat(Math.log1p(degree) / Math.log1p(80), 0, 1, 0); + const nodeCount = Math.max(1, (graph?.nodes?.length || 1)); + const rank = Number.isFinite(item.labelRank) ? item.labelRank : nodeCount; + const rankRatio = nodeCount > 1 ? rank / Math.max(1, nodeCount - 1) : 0; + const salienceSignal = clampFloat(1 - rankRatio, 0, 1, 0); + const leadingCount = Math.max(10, Math.min(58, Math.ceil(nodeCount * 0.05))); + const secondaryCount = Math.max(28, Math.min(170, Math.ceil(nodeCount * 0.18))); + const tertiaryCount = Math.max(80, Math.min(520, Math.ceil(nodeCount * 0.38))); + const leading = rank < leadingCount; + const secondary = rank < secondaryCount; + const tertiary = rank < tertiaryCount; + let threshold = 1.24 - sizeSignal * 0.64 - degreeSignal * 0.22 - salienceSignal * 0.42; + + if (leading) threshold -= 0.28; + else if (secondary) threshold -= 0.2; + else if (tertiary) threshold -= 0.08; + if (folder) threshold -= node.representativeFile ? 0.18 : 0.12; + else if (external) threshold -= 0.04; + if (unresolved) threshold += 0.18; + + threshold = clampFloat(threshold, 0.16, 1.38, 0.96); + const fade = smoothstep(threshold - 0.14, threshold + 0.1, zoom); + const leadingFade = leading + ? smoothstep(0.12, 0.38, zoom) + : secondary + ? smoothstep(0.22, 0.62, zoom) * 0.9 + : tertiary + ? smoothstep(0.46, 0.96, zoom) * 0.7 + : 0; + const largeFade = item.radius >= 24 + ? smoothstep(0.22, 0.56, zoom) * 0.96 + : item.radius >= 15 + ? smoothstep(0.42, 0.86, zoom) * 0.78 + : 0; + const smallFade = !unresolved ? smoothstep(0.86, 1.28, zoom) * 0.82 : 0; + const closeFade = !unresolved ? smoothstep(1.12, 1.46, zoom) * 0.98 : 0; + return clampFloat(Math.max(fade, leadingFade, largeFade, smallFade, closeFade), 0, 1, fade); +} + +function labelScreenScale(zoom) { + const clampedZoom = clampFloat(zoom, MIN_CANVAS_ZOOM, MAX_CANVAS_ZOOM, 1); + return 0.62 + smoothstep(0.06, 1.48, clampedZoom) * 0.88; +} + +function nodeTooltip(node) { + const type = node.externalProxy + ? `outside ${node.type}` + : node.type === "folder" && node.representativeFile + ? "folder + meta file" + : node.isRepresentativeFile + ? "merged meta file" + : node.type; + return [ + node.title, + node.path || "/", + `type: ${type}`, + `notes: ${node.noteCount || 0}`, + `out: ${node.linkCount || 0}`, + `in: ${node.backlinkCount || 0}` + ].join("\n"); +} + +function isRepresentativeEdge(index, edge) { + const source = index.nodes.get(edge.source); + return Boolean(source && source.representativeFile === edge.target); +} + +function graphNode(index, graph, id) { + return (graph && graph.nodesById && graph.nodesById.get(id)) || index.nodes.get(id); +} + +function uniqueEdgesByEndpoint(edges, endpointKey) { + const byEndpoint = new Map(); + for (const edge of edges) { + const endpoint = edge && edge[endpointKey]; + if (!endpoint) continue; + const existing = byEndpoint.get(endpoint); + if (existing) { + existing.weight += edge.weight || 0; + continue; + } + byEndpoint.set(endpoint, Object.assign({}, edge)); + } + return Array.from(byEndpoint.values()); +} + +function buildBudgetChildrenByParent(nodes) { + const childrenByParent = new Map(); + for (const node of nodes || []) { + if (!node || node.parentId === null || node.parentId === undefined) continue; + if (!childrenByParent.has(node.parentId)) childrenByParent.set(node.parentId, []); + childrenByParent.get(node.parentId).push(node.id); + } + + const nodeById = new Map((nodes || []).map(node => [node.id, node])); + for (const children of childrenByParent.values()) { + children.sort((a, b) => compareNodes(nodeById.get(a), nodeById.get(b))); + } + return childrenByParent; +} + +function hiddenLegendSetFromState(state = {}) { + return new Set(Array.isArray(state.hiddenLegendItems) ? state.hiddenLegendItems : []); +} + +function legendHidesNode(node, graph, hiddenLegend) { + if (!node || !hiddenLegend || !hiddenLegend.size) return false; + if (node.id === graph?.rootId) return hiddenLegend.has("root"); + if (node.externalProxy) { + if (hiddenLegend.has("outside-file")) return true; + return node.type === "unresolved" && hiddenLegend.has("missing"); + } + if (node.type === "external") return hiddenLegend.has("outside"); + if (node.type === "unresolved") return hiddenLegend.has("missing"); + if (node.type === "folder") { + if (node.representativeFile) return hiddenLegend.has("folder-meta"); + return hiddenLegend.has("folder"); + } + if (node.type === "note") return hiddenLegend.has("file"); + return false; +} + +function legendHidesHierarchyEdge(edge, hiddenLegend) { + return Boolean(hiddenLegend?.has("tree")); +} + +function legendHidesLinkEdge(edge, hiddenLegend) { + if (!edge || !hiddenLegend || !hiddenLegend.size) return false; + if (edge.externalCount && hiddenLegend.has("outside-link")) return true; + if (edge.unresolvedCount && hiddenLegend.has("dashed-link")) return true; + return !edge.externalCount && hiddenLegend.has("link"); +} + +function buildCanvasGraphData(index, graph, layout, query, graphSettings = DEFAULT_NATIVE_GRAPH_SETTINGS, options = {}) { + const nodeSizeMultiplier = clampFloat(graphSettings.nodeSizeMultiplier, 0.55, 2.8, 1); + const lineSizeMultiplier = clampFloat(graphSettings.lineSizeMultiplier, 0.35, 4, 1); + const includeHoverLinks = options.includeHoverLinks !== false; + const hiddenLegend = hiddenLegendSetFromState(options); + const nodes = []; + const nodesById = new Map(); + const hierarchy = []; + const links = []; + const hoverLinks = []; + const searchMatchItems = []; + const edgesByNode = new Map(); + const linkEdgesByNode = new Map(); + const hierarchyParentByNode = new Map(); + const hierarchyChildrenByNode = new Map(); + const hierarchyChildNodeIdsByNode = new Map(); + const edgesById = new Map(); + let maxLinkDegree = 1; + const graphNodes = (graph.nodes || []).filter(node => !legendHidesNode(node, graph, hiddenLegend)); + for (const node of graphNodes) { + maxLinkDegree = Math.max(maxLinkDegree, (node.linkCount || 0) + (node.backlinkCount || 0)); + } + + for (const node of graphNodes) { + const point = layout.positions.get(node.id); + if (!point) continue; + const item = { + node, + point, + renderIndex: nodes.length, + radius: Math.max(3.5, nodeRadius(node, point, maxLinkDegree) * nodeSizeMultiplier), + label: node.title, + metric: nodeMetric(node), + searchMatch: Boolean(query && nodeMatches(node, query)) + }; + nodes.push(item); + if (item.searchMatch) searchMatchItems.push(item); + nodesById.set(node.id, item); + } + + for (const node of graphNodes) { + if (!node || node.parentId === null || node.parentId === undefined) continue; + const childId = index.visualNodeId(node.id); + const parentId = index.visualNodeId(node.parentId); + if (childId === null || childId === undefined || parentId === null || parentId === undefined) continue; + if (childId === parentId || !nodesById.has(childId) || !nodesById.has(parentId)) continue; + if (!hierarchyChildNodeIdsByNode.has(parentId)) hierarchyChildNodeIdsByNode.set(parentId, []); + const children = hierarchyChildNodeIdsByNode.get(parentId); + if (!children.includes(childId)) children.push(childId); + } + + assignCanvasLabelPriority(nodes, graph); + const labelItems = nodes + .slice() + .sort((a, b) => (a.labelPriority || 0) - (b.labelPriority || 0)); + const interactiveLabelItems = labelItems.filter(item => + item.searchMatch + || item.labelRank <= FAST_CANVAS_LABEL_LIMIT + || item.node.id === graph.rootId + || item.node.id === graph.focusId + ); + + const registerEdge = (collection, edge, sourcePoint, targetPoint, options = {}) => { + const key = options.key || edge.id || `${edge.source}->${edge.target}`; + const item = { + key, + edge, + source: edge.source, + target: edge.target, + sourcePoint, + targetPoint, + width: options.width || 1, + external: Boolean(options.external), + hoverOnly: Boolean(options.hoverOnly), + route: options.route || null + }; + collection.push(item); + if (edge.id) edgesById.set(edge.id, item); + for (const id of [edge.source, edge.target]) { + if (!id && id !== ROOT_ID) continue; + if (!edgesByNode.has(id)) edgesByNode.set(id, []); + edgesByNode.get(id).push(item); + if (options.linkOverlay) { + if (!linkEdgesByNode.has(id)) linkEdgesByNode.set(id, []); + linkEdgesByNode.get(id).push(item); + } + } + if (options.hierarchyTree) { + hierarchyParentByNode.set(edge.target, item); + if (!hierarchyChildrenByNode.has(edge.source)) hierarchyChildrenByNode.set(edge.source, []); + hierarchyChildrenByNode.get(edge.source).push(item); + } + return item; + }; + + for (const edge of graph.hierarchyEdges) { + if (legendHidesHierarchyEdge(edge, hiddenLegend)) continue; + if (!nodesById.has(edge.source) || !nodesById.has(edge.target)) continue; + const sourcePoint = layout.positions.get(edge.source); + const targetPoint = layout.positions.get(edge.target); + if (!sourcePoint || !targetPoint) continue; + registerEdge(hierarchy, edge, sourcePoint, targetPoint, { + width: edge.type === "external-hierarchy" ? 1.05 : 1.12, + external: edge.type === "external-hierarchy", + hierarchyTree: true + }); + } + + for (const edge of graph.linkEdges) { + if (legendHidesLinkEdge(edge, hiddenLegend)) continue; + if (!nodesById.has(edge.source) || !nodesById.has(edge.target)) continue; + const sourcePoint = layout.positions.get(edge.source); + const targetPoint = layout.positions.get(edge.target); + if (!sourcePoint || !targetPoint) continue; + const weightSignal = Math.log2((edge.weight || 1) + 1); + registerEdge(links, edge, sourcePoint, targetPoint, { + width: clampFloat((0.42 + weightSignal * 0.2) * lineSizeMultiplier, 0.48, 3.4, 1), + external: Boolean(edge.externalCount), + linkOverlay: true, + route: layout.linkRoutes ? layout.linkRoutes.get(edge.id) : null + }); + } + + if (includeHoverLinks) { + const renderedLinkKeys = new Set(links.map(item => item.key)); + for (const edge of graph.hoverLinkEdges || []) { + if (legendHidesLinkEdge(edge, hiddenLegend)) continue; + if (!nodesById.has(edge.source) || !nodesById.has(edge.target)) continue; + const key = edge.id || `${edge.source}->${edge.target}`; + if (renderedLinkKeys.has(key)) continue; + const sourcePoint = layout.positions.get(edge.source); + const targetPoint = layout.positions.get(edge.target); + if (!sourcePoint || !targetPoint) continue; + const weightSignal = Math.log2((edge.weight || 1) + 1); + registerEdge(hoverLinks, edge, sourcePoint, targetPoint, { + width: clampFloat((0.42 + weightSignal * 0.2) * lineSizeMultiplier, 0.48, 3.4, 1), + external: Boolean(edge.externalCount), + linkOverlay: true, + hoverOnly: true, + route: layout.linkRoutes ? layout.linkRoutes.get(edge.id) : null + }); + } + } + + return { + nodes, + nodesById, + nodeSpatialIndex: buildCanvasNodeSpatialIndex(nodes), + labelItems, + interactiveLabelItems, + searchMatchItems, + hierarchy, + links, + hoverLinks, + edgesByNode, + linkEdgesByNode, + hierarchyParentByNode, + hierarchyChildrenByNode, + hierarchyChildNodeIdsByNode, + edgesById, + showArrow: graphSettings.showArrow !== false + }; +} + +function addHierarchyHoverHighlights(data, nodeId, mode, relatedNodes, highlightedEdges, labelNodes) { + const normalized = normalizeHoverHighlightMode(mode); + if (!data || normalized === "none" || normalized === "note-links") return; + + if ( + normalized === "hierarchy-parents" + || normalized === "hierarchy-parents-direct" + || normalized === "hierarchy-all" + ) { + addHierarchyAncestorHighlights(data, nodeId, relatedNodes, highlightedEdges, labelNodes); + } + + if ( + normalized === "hierarchy-direct-children" + || normalized === "hierarchy-parents-direct" + ) { + addHierarchyChildHighlights(data, nodeId, relatedNodes, highlightedEdges, labelNodes); + } + + if ( + normalized === "hierarchy-descendants" + || normalized === "hierarchy-all" + ) { + addHierarchyDescendantHighlights(data, nodeId, relatedNodes, highlightedEdges, labelNodes); + } +} + +function addHierarchyAncestorHighlights(data, nodeId, relatedNodes, highlightedEdges, labelNodes) { + const parentByNode = data.hierarchyParentByNode || new Map(); + const visited = new Set([nodeId]); + let currentId = nodeId; + + while (parentByNode.has(currentId)) { + const edge = parentByNode.get(currentId); + if (!edge || highlightedEdges.has(edge.key)) break; + highlightedEdges.add(edge.key); + addHierarchyEdgeNodes(edge, relatedNodes, labelNodes); + currentId = edge.source; + if (visited.has(currentId)) break; + visited.add(currentId); + } +} + +function addHierarchyChildHighlights(data, nodeId, relatedNodes, highlightedEdges, labelNodes) { + for (const edge of data.hierarchyChildrenByNode?.get(nodeId) || []) { + highlightedEdges.add(edge.key); + addHierarchyEdgeNodes(edge, relatedNodes, labelNodes); + } + addHierarchyChildNodeIds(data, nodeId, relatedNodes, labelNodes); +} + +function addHierarchyDescendantHighlights(data, nodeId, relatedNodes, highlightedEdges, labelNodes) { + const childrenByNode = data.hierarchyChildrenByNode || new Map(); + const childIdsByNode = data.hierarchyChildNodeIdsByNode || new Map(); + const stack = []; + const visited = new Set([nodeId]); + + pushHierarchyChildEntries(stack, childrenByNode, childIdsByNode, nodeId); + + while (stack.length) { + const entry = stack.pop(); + if (!entry || entry.target === null || entry.target === undefined || visited.has(entry.target)) continue; + if (entry.edge) { + highlightedEdges.add(entry.edge.key); + addHierarchyEdgeNodes(entry.edge, relatedNodes, labelNodes); + } else { + relatedNodes.add(entry.target); + labelNodes.add(entry.target); + } + visited.add(entry.target); + pushHierarchyChildEntries(stack, childrenByNode, childIdsByNode, entry.target); + } +} + +function pushHierarchyChildEntries(stack, childrenByNode, childIdsByNode, nodeId) { + const edges = childrenByNode.get(nodeId) || []; + const edgeTargets = new Set(); + for (let index = edges.length - 1; index >= 0; index -= 1) { + const edge = edges[index]; + if (!edge || edge.target === null || edge.target === undefined) continue; + edgeTargets.add(edge.target); + stack.push({ edge, target: edge.target }); + } + + const childIds = childIdsByNode.get(nodeId) || []; + for (let index = childIds.length - 1; index >= 0; index -= 1) { + const childId = childIds[index]; + if (edgeTargets.has(childId)) continue; + stack.push({ edge: null, target: childId }); + } +} + +function addHierarchyChildNodeIds(data, nodeId, relatedNodes, labelNodes) { + for (const childId of data.hierarchyChildNodeIdsByNode?.get(nodeId) || []) { + relatedNodes.add(childId); + labelNodes.add(childId); + } +} + +function addHierarchyEdgeNodes(edge, relatedNodes, labelNodes) { + if (!edge) return; + if (edge.source !== null && edge.source !== undefined) { + relatedNodes.add(edge.source); + labelNodes.add(edge.source); + } + if (edge.target !== null && edge.target !== undefined) { + relatedNodes.add(edge.target); + labelNodes.add(edge.target); + } +} + +function resolveCssColor(root, variableName, fallback) { + if (!root || !root.ownerDocument) return fallback; + const probe = root.ownerDocument.createElement("span"); + probe.style.position = "absolute"; + probe.style.width = "0"; + probe.style.height = "0"; + probe.style.overflow = "hidden"; + probe.style.pointerEvents = "none"; + probe.style.color = `var(${variableName})`; + root.appendChild(probe); + const color = getComputedStyle(probe).color; + probe.remove(); + return color || fallback; +} + +function resolveGraphViewColor(root, colorClass, fallback) { + if (!root || !root.ownerDocument) return fallback; + const wrapper = root.ownerDocument.createElement("span"); + wrapper.style.position = "absolute"; + wrapper.style.width = "0"; + wrapper.style.height = "0"; + wrapper.style.overflow = "hidden"; + wrapper.style.pointerEvents = "none"; + wrapper.style.color = fallback; + const probe = root.ownerDocument.createElement("span"); + probe.className = `graph-view ${colorClass}`; + wrapper.appendChild(probe); + root.appendChild(wrapper); + const color = getComputedStyle(probe).color; + wrapper.remove(); + return color || fallback; +} + +function canvasWorldBounds(viewport, panX, panY, zoom, margin = 160) { + const safeZoom = Math.max(MIN_CANVAS_ZOOM, Number(zoom) || 1); + const minX = (0 - panX) / safeZoom - margin; + const minY = (0 - panY) / safeZoom - margin; + const maxX = ((viewport?.width || 1) - panX) / safeZoom + margin; + const maxY = ((viewport?.height || 1) - panY) / safeZoom + margin; + return { minX, minY, maxX, maxY }; +} + +function circleInBounds(point, radius, bounds) { + if (!point || !bounds) return true; + const r = Math.max(0, Number(radius) || 0); + return point.x + r >= bounds.minX + && point.x - r <= bounds.maxX + && point.y + r >= bounds.minY + && point.y - r <= bounds.maxY; +} + +function buildCanvasNodeSpatialIndex(nodes) { + if (!Array.isArray(nodes) || !nodes.length) return null; + + let maxRadius = 1; + for (const item of nodes) { + maxRadius = Math.max(maxRadius, Number(item.radius) || 1); + } + + const cellSize = clampFloat(maxRadius * 2.6 + 28, 56, 220, 96); + const grid = new Map(); + for (const item of nodes) { + if (!item || !item.point) continue; + const gx = Math.floor(item.point.x / cellSize); + const gy = Math.floor(item.point.y / cellSize); + const key = `${gx},${gy}`; + if (!grid.has(key)) grid.set(key, []); + grid.get(key).push(item); + } + + return { cellSize, grid, maxRadius }; +} + +function hitTestCanvasNodeIndex(data, world, nodePad) { + const index = data && data.nodeSpatialIndex; + if (!index || !index.grid || !Number.isFinite(index.cellSize)) { + return hitTestCanvasNodeLinear(data, world, nodePad); + } + + const reach = Math.max(index.maxRadius || 1, nodePad || 0) + Math.max(1, nodePad || 0); + const span = Math.max(1, Math.ceil(reach / index.cellSize)); + const centerX = Math.floor(world.x / index.cellSize); + const centerY = Math.floor(world.y / index.cellSize); + let best = null; + let bestRenderIndex = -1; + + for (let gx = centerX - span; gx <= centerX + span; gx += 1) { + for (let gy = centerY - span; gy <= centerY + span; gy += 1) { + const bucket = index.grid.get(`${gx},${gy}`); + if (!bucket) continue; + for (const item of bucket) { + const distance = Math.hypot(world.x - item.point.x, world.y - item.point.y); + if (distance > item.radius + nodePad) continue; + const renderIndex = Number.isFinite(item.renderIndex) ? item.renderIndex : 0; + if (renderIndex >= bestRenderIndex) { + best = item; + bestRenderIndex = renderIndex; + } + } + } + } + + return best; +} + +function hitTestCanvasNodeLinear(data, world, nodePad) { + if (!data || !Array.isArray(data.nodes)) return null; + for (let i = data.nodes.length - 1; i >= 0; i -= 1) { + const item = data.nodes[i]; + if (!item || !item.point) continue; + const distance = Math.hypot(world.x - item.point.x, world.y - item.point.y); + if (distance <= item.radius + nodePad) return item; + } + return null; +} + +function edgeItemInBounds(item, bounds) { + if (!item || !bounds) return true; + const source = item.sourcePoint; + const target = item.targetPoint; + if (!source || !target) return true; + const route = item.dynamicRoute || item.route; + + let minX = Math.min(source.x, target.x); + let minY = Math.min(source.y, target.y); + let maxX = Math.max(source.x, target.x); + let maxY = Math.max(source.y, target.y); + + if (route && Number.isFinite(route.radius) && Number.isFinite(route.centerX) && Number.isFinite(route.centerY)) { + minX = Math.min(minX, route.centerX - route.radius); + minY = Math.min(minY, route.centerY - route.radius); + maxX = Math.max(maxX, route.centerX + route.radius); + maxY = Math.max(maxY, route.centerY + route.radius); + } else if (route && route.kind === "curve") { + const centerX = Number.isFinite(source.centerX) ? source.centerX : (source.x + target.x) / 2; + const centerY = Number.isFinite(source.centerY) ? source.centerY : (source.y + target.y) / 2; + const midX = (source.x + target.x) / 2; + const midY = (source.y + target.y) / 2; + const pull = Number.isFinite(route.curveStrength) ? route.curveStrength : (item.external ? 0.28 : 0.16); + const controlX = midX + (centerX - midX) * pull; + const controlY = midY + (centerY - midY) * pull; + minX = Math.min(minX, controlX); + minY = Math.min(minY, controlY); + maxX = Math.max(maxX, controlX); + maxY = Math.max(maxY, controlY); + } + + return maxX >= bounds.minX + && minX <= bounds.maxX + && maxY >= bounds.minY + && minY <= bounds.maxY; +} + +function distanceToSegment(point, source, target) { + const dx = target.x - source.x; + const dy = target.y - source.y; + if (dx === 0 && dy === 0) return Math.hypot(point.x - source.x, point.y - source.y); + const t = Math.max(0, Math.min(1, ((point.x - source.x) * dx + (point.y - source.y) * dy) / (dx * dx + dy * dy))); + const x = source.x + dx * t; + const y = source.y + dy * t; + return Math.hypot(point.x - x, point.y - y); +} + +function dynamicOrbitRouteForEdge(item, centerX, centerY, ringGap) { + const source = item?.sourcePoint; + const target = item?.targetPoint; + if (!source || !target) return null; + + const sx = source.x - centerX; + const sy = source.y - centerY; + const tx = target.x - centerX; + const ty = target.y - centerY; + const sourceRadius = Math.max(1, Math.hypot(sx, sy)); + const targetRadius = Math.max(1, Math.hypot(tx, ty)); + const sourceAngle = Math.atan2(sy, sx); + const targetAngle = Math.atan2(ty, tx); + const delta = shortestAngleDelta(sourceAngle, targetAngle); + const angleDistance = Math.abs(delta); + const safeRingGap = Math.max(120, Number(ringGap) || DEFAULT_RING_SPACING); + const external = item.external || item.edge?.externalCount; + const sameOrNearRing = Math.abs(sourceRadius - targetRadius) < safeRingGap * 0.5; + const laneNoise = (deterministicUnitOffset(item.key || item.edge?.id || `${item.source}->${item.target}`, "dynamic-route") + 1) * 0.5; + const laneOffset = (0.34 + laneNoise * 0.66) * safeRingGap * (external ? 0.16 : 0.095); + const routeRadius = Math.max(sourceRadius, targetRadius) + + Math.max(external ? 96 : 52, safeRingGap * (external ? 0.2 : sameOrNearRing ? 0.13 : 0.09)) + + laneOffset; + + if (!external && !sameOrNearRing && angleDistance < 0.28) { + return { + kind: "curve", + curveStrength: 0.12 + }; + } + + return { + kind: "dynamic-orbit", + centerX, + centerY, + radius: routeRadius, + sourceAngle, + targetAngle, + endAngle: sourceAngle + delta, + sweep: delta >= 0 ? 1 : 0 + }; +} + +function linkTooltip(index, edge, graph) { + const source = graphNode(index, graph, edge.source); + const target = graphNode(index, graph, edge.target); + return [ + "Aggregated internal link overlay", + `${source ? source.title : edge.source} -> ${target ? target.title : edge.target}`, + `weight: ${edge.weight || 0}`, + `raw edges: ${edge.rawCount || edge.weight || 0}`, + `unresolved: ${edge.unresolvedCount || 0}`, + `external: ${edge.externalCount || 0}` + ].join("\n"); +} + +function nodeRenderScore(node, rootId, focusId, query) { + let score = 0; + if (node.id === rootId) score += 1000000; + if (node.id === focusId) score += 900000; + if (query && nodeMatches(node, query)) score += 700000; + score += Math.max(0, 200 - node.depth * 18); + + if (node.type === "folder") { + score += 5000 + Math.min(2500, Math.log2((node.noteCount || 0) + 1) * 280); + } else if (node.type === "note") { + score += 500 + Math.min(2500, Math.log2((node.linkCount || 0) + (node.backlinkCount || 0) + 1) * 420); + } else if (node.type === "unresolved") { + score += 80; + } + + return score; +} + +function linkRenderScore(edge) { + return (edge.externalCount ? 100000 : 0) + + Math.min(60000, (edge.weight || 0) * 100) + + Math.min(10000, (edge.rawCount || 0) * 10); +} + +function externalAnchorPath(node, rootId) { + const nodePath = node.type === "unresolved" ? parentPath(node.path) : node.path; + const pathParts = String(nodePath || "").split("/").filter(Boolean); + if (!pathParts.length) return null; + if (!rootId) return pathParts[0] || nodePath; + + const rootParts = String(rootId || "").split("/").filter(Boolean); + let common = 0; + while ( + common < rootParts.length + && common < pathParts.length + && rootParts[common] === pathParts[common] + ) { + common += 1; + } + + const anchorLength = Math.min(pathParts.length, common + 1); + return pathParts.slice(0, anchorLength).join("/"); +} + +function computeLinkRoutes(linkEdges, positions, maxDepth, ringSpacing, outerRadius, routeGapFactor = 1) { + const routes = new Map(); + let maxRadius = Math.max(outerRadius || 0, 1); + + for (const edge of linkEdges) { + const source = positions.get(edge.source); + const target = positions.get(edge.target); + if (!source || !target) continue; + + const sourceRadius = Number.isFinite(source.radius) ? source.radius : 0; + const targetRadius = Number.isFinite(target.radius) ? target.radius : 0; + const sourceAngle = Number.isFinite(source.angle) ? source.angle : Math.atan2(target.y - source.y, target.x - source.x); + const targetAngle = Number.isFinite(target.angle) ? target.angle : sourceAngle; + const delta = shortestAngleDelta(sourceAngle, targetAngle); + const angleDistance = Math.abs(delta); + const sameOrNearRing = Math.abs(sourceRadius - targetRadius) < ringSpacing * 0.44; + const touchesOuterRing = Math.max(source.depth || 0, target.depth || 0) >= Math.max(1, maxDepth - 1); + const isExternal = Boolean(source.external || target.external || edge.externalCount); + const shouldCurve = isExternal + || sameOrNearRing + || (touchesOuterRing && angleDistance > 0.34) + || angleDistance > Math.PI * 0.42; + + if (!shouldCurve) continue; + + routes.set(edge.id, { + kind: "curve", + lane: 0, + curveStrength: isExternal + ? 0.3 + : sameOrNearRing + ? 0.22 + : angleDistance > Math.PI * 0.72 + ? 0.2 + : 0.15 + }); + } + + return { routes, maxRadius }; +} + +function assignRadialLaneRoutes(items, routes, outerRadius, ringSpacing, routeGapFactor = 1) { + const lanes = []; + let maxRadius = outerRadius || 0; + const sorted = items.slice().sort((a, b) => { + if (a.interval.start !== b.interval.start) return a.interval.start - b.interval.start; + return a.interval.end - b.interval.end; + }); + + for (const item of sorted) { + let lane = lanes.findIndex(endAngle => item.interval.start > endAngle + 0.12); + if (lane === -1) { + lane = lanes.length; + lanes.push(item.interval.end); + } else { + lanes[lane] = item.interval.end; + } + + const cappedLane = Math.min(lane, 14); + const laneStep = Math.max(18, Math.min(44, ringSpacing * 0.075 * routeGapFactor)); + const radius = Math.max( + outerRadius || 0, + item.source.radius || 0, + item.target.radius || 0 + ) + (item.isExternal ? 100 : 48) * routeGapFactor + cappedLane * laneStep; + maxRadius = Math.max(maxRadius, radius); + routes.set(item.edge.id, { + kind: item.isExternal ? "external" : "outer", + lane, + centerX: 0, + centerY: 0, + radius, + sourceAngle: item.sourceAngle, + targetAngle: item.targetAngle, + endAngle: item.sourceAngle + item.delta, + sweep: item.delta >= 0 ? 1 : 0 + }); + } + + return maxRadius; +} + +function radialRouteInterval(sourceAngle, delta) { + let start = normalizeAngle(sourceAngle); + let end = start + delta; + if (end < start) { + const swap = start; + start = end; + end = swap; + } + if (end - start < 0.05) end += 0.05; + return { start, end }; +} + +function wrapCanvasLabel(ctx, label, maxWidth, maxLines) { + const text = String(label || "").replace(/\s+/g, " ").trim(); + if (!text) return []; + + const words = text.split(" "); + const lines = []; + let current = ""; + + for (const word of words) { + const pieces = splitCanvasWord(ctx, word, maxWidth); + + for (const piece of pieces) { + const candidate = current ? `${current} ${piece}` : piece; + if (!current || ctx.measureText(candidate).width <= maxWidth) { + current = candidate; + continue; + } + + lines.push(current); + current = piece; + } + } + + if (current) lines.push(current); + if (lines.length <= maxLines) return lines; + + const clipped = lines.slice(0, Math.max(1, maxLines)); + clipped[clipped.length - 1] = fitCanvasText(ctx, clipped[clipped.length - 1], maxWidth, "..."); + return clipped; +} + +function splitCanvasWord(ctx, word, maxWidth) { + if (ctx.measureText(word).width <= maxWidth) return [word]; + + const pieces = []; + let current = ""; + for (const char of Array.from(word)) { + const candidate = `${current}${char}`; + if (!current || ctx.measureText(candidate).width <= maxWidth) { + current = candidate; + continue; + } + + pieces.push(current); + current = char; + } + if (current) pieces.push(current); + return pieces; +} + +function fitCanvasText(ctx, text, maxWidth, suffix = "...") { + if (ctx.measureText(text).width <= maxWidth) return text; + const chars = Array.from(String(text || "")); + while (chars.length && ctx.measureText(`${chars.join("")}${suffix}`).width > maxWidth) { + chars.pop(); + } + return chars.length ? `${chars.join("")}${suffix}` : suffix; +} + +function fitZoomForLayout(layout, viewport, inset = 32) { + if (!layout || !viewport) return DEFAULT_MIN_CANVAS_ZOOM; + const availableWidth = Math.max(1, (viewport.width || 1) - inset); + const availableHeight = Math.max(1, (viewport.height || 1) - inset); + return Math.min( + availableWidth / Math.max(1, layout.width || 1), + availableHeight / Math.max(1, layout.height || 1) + ); +} + +function adaptiveMaxZoomForLayout(layout, viewport) { + if (!layout || !viewport) return MAX_CANVAS_ZOOM; + + const fitZoom = fitZoomForLayout(layout, viewport, 42); + const viewportMin = Math.max(1, Math.min(viewport.width || 1, viewport.height || 1)); + const nodeCount = Math.max(1, layout.positions ? layout.positions.size : 1); + const area = Math.max(1, (layout.width || 1) * (layout.height || 1)); + const densitySpacing = Math.sqrt(area / nodeCount) * 0.58; + const ringGap = medianRingGap(layout.rings); + const usefulWorldWindow = clampFloat( + Math.max(densitySpacing * 1.65, ringGap * 1.55), + 280, + 1180, + 520 + ); + const densityMax = viewportMin / usefulWorldWindow; + const countFactor = nodeCount < 120 + ? 2.3 + : nodeCount < 700 + ? 3.35 + : nodeCount < 2400 + ? 4.45 + : 5.25; + const fitMax = fitZoom * countFactor; + const minAllowed = Math.min( + MAX_CANVAS_ZOOM, + Math.max(MIN_CANVAS_ZOOM, DEFAULT_MIN_CANVAS_ZOOM * 2.4, fitZoom * 1.22) + ); + const proposed = Math.max(fitZoom * 1.35, densityMax, fitMax); + return clampFloat(proposed, minAllowed, MAX_CANVAS_ZOOM, MAX_CANVAS_ZOOM); +} + +function medianRingGap(rings) { + if (!Array.isArray(rings) || !rings.length) return 420; + + const radii = rings + .map(ring => ring && Number(ring.radius)) + .filter(radius => Number.isFinite(radius) && radius > 0) + .sort((a, b) => a - b); + if (!radii.length) return 420; + if (radii.length === 1) return clampFloat(radii[0], 260, 720, 420); + + const gaps = []; + for (let index = 1; index < radii.length; index += 1) { + const gap = radii[index] - radii[index - 1]; + if (Number.isFinite(gap) && gap > 0) gaps.push(gap); + } + return medianNumber(gaps, 420); +} + +function medianNumber(values, fallback) { + const numbers = (values || []) + .filter(value => Number.isFinite(value)) + .sort((a, b) => a - b); + if (!numbers.length) return fallback; + const middle = Math.floor(numbers.length / 2); + return numbers.length % 2 + ? numbers[middle] + : (numbers[middle - 1] + numbers[middle]) / 2; +} + +function normalizeWheelDeltaY(deltaY, deltaMode = 0) { + let delta = Number(deltaY) || 0; + if (deltaMode === 1) delta *= 40; + else if (deltaMode === 2) delta *= 800; + return delta; +} + +function zoomToSliderValue(zoom, minZoom, maxZoom) { + const min = Math.max(MIN_CANVAS_ZOOM, Number(minZoom) || MIN_CANVAS_ZOOM); + const max = Math.max(min * 1.001, Number(maxZoom) || MAX_CANVAS_ZOOM); + const clamped = clampFloat(zoom, min, max, min); + const ratio = Math.log(clamped / min) / Math.log(max / min); + const sliderRatio = Math.pow(clampFloat(ratio, 0, 1, 0), 1 / ZOOM_SLIDER_CURVE); + return String(Math.round(sliderRatio * ZOOM_SLIDER_STEPS)); +} + +function sliderValueToZoom(value, minZoom, maxZoom) { + const min = Math.max(MIN_CANVAS_ZOOM, Number(minZoom) || MIN_CANVAS_ZOOM); + const max = Math.max(min * 1.001, Number(maxZoom) || MAX_CANVAS_ZOOM); + const sliderRatio = clampFloat(Number(value) / ZOOM_SLIDER_STEPS, 0, 1, 0); + const ratio = Math.pow(sliderRatio, ZOOM_SLIDER_CURVE); + return min * Math.pow(max / min, ratio); +} + +function truncateLabel(label, maxLength) { + const text = String(label || ""); + if (text.length <= maxLength) return text; + return `${text.slice(0, Math.max(1, maxLength - 1))}...`; +} + +function clampNumber(value, min, max, fallback) { + const parsed = Number(value); + if (!Number.isFinite(parsed)) return fallback; + return Math.min(max, Math.max(min, Math.round(parsed))); +} + +function clampFloat(value, min, max, fallback) { + const parsed = Number(value); + if (!Number.isFinite(parsed)) return fallback; + return Math.min(max, Math.max(min, parsed)); +} + +function smoothstep(edge0, edge1, value) { + if (edge0 === edge1) return value >= edge1 ? 1 : 0; + const t = clampFloat((value - edge0) / (edge1 - edge0), 0, 1, 0); + return t * t * (3 - 2 * t); +} + +function springBackEase(value) { + const t = clampFloat(value, 0, 1, 0); + const c1 = 1.28; + const c3 = c1 + 1; + return 1 + c3 * Math.pow(t - 1, 3) + c1 * Math.pow(t - 1, 2); +} + +module.exports = MiniWorldMapPlugin; diff --git a/main.js b/main.js index c1e0809..14c177f 100644 --- a/main.js +++ b/main.js @@ -1,8957 +1,4372 @@ -const { - ItemView, - Menu, - Notice, - Plugin, - PluginSettingTab, - Setting, - TFile, - TFolder, - setIcon -} = require("obsidian"); +/* +THIS IS A GENERATED/BUNDLED FILE BY ESBUILD +if you want to view the source, please visit the github repository of this plugin +*/ -const VIEW_TYPE_MINI_WORLD_MAP = "mini-world-map-view"; -const ROOT_ID = ""; -const ROOT_TITLE = "Vault"; -const MAX_ATLAS_DEPTH = 80; -const MAX_RENDER_NODE_LIMIT = 20000; -const MAX_LINK_LIMIT = 30000; -const MAX_EXTERNAL_LINK_ANCHOR_LIMIT = 20000; -const MIN_CANVAS_ZOOM = 0.01; -const DEFAULT_MIN_CANVAS_ZOOM = 0.04; -const MAX_CANVAS_ZOOM = 2; -const ZOOM_SLIDER_STEPS = 1000; -const ZOOM_SLIDER_CURVE = 1.35; -const ZOOM_WHEEL_BASE = 1.5; -const ZOOM_ANIMATION_FRICTION = 0.85; -const ZOOM_BUTTON_STEP = 1.14; -const COLOR_SCHEME_OPTIONS = ["auto", "day", "night"]; -const LABEL_VISIBILITY_OPTIONS = [ - ["auto", "Auto"], - ["hover", "Hover only"] -]; -const HOVER_HIGHLIGHT_MODE_OPTIONS = [ - ["none", "None"], - ["note-links", "Note links"], - ["hierarchy-parents", "Hierarchy parents"], - ["hierarchy-direct-children", "Hierarchy direct children"], - ["hierarchy-descendants", "Hierarchy all children"], - ["hierarchy-parents-direct", "Hierarchy parents + direct"], - ["hierarchy-all", "Hierarchy parents + all children"] -]; -const LEGEND_ITEM_DEFINITIONS = [ - ["root", "root", "Current atlas root", "mwm-legend-root"], - ["folder", "folder", "Folder / subtree", "mwm-legend-folder"], - ["folder-meta", "folder+", "Folder with merged meta file", "mwm-legend-meta"], - ["file", "file", "Markdown file", "mwm-legend-note"], - ["outside", "outside", "Grouped outside branch", "mwm-legend-external"], - ["outside-file", "outside file", "Exact linked file outside root", "mwm-legend-outside-file"], - ["missing", "missing", "Unresolved internal link", "mwm-legend-unresolved"], - ["tree", "tree", "Parent-child hierarchy", "mwm-legend-tree"], - ["link", "link", "Internal file links", "mwm-legend-link"], - ["outside-link", "outside link", "Crosses current root", "mwm-legend-link-external"], - ["dashed-link", "dashed", "Includes unresolved links", "mwm-legend-link-unresolved"] -]; -const DEFAULT_RING_SPACING = 960; -const MIN_RING_SPACING = 720; -const MAX_RING_SPACING = 2800; -const DEFAULT_NODE_SPACING = 126; -const MIN_NODE_SPACING = 72; -const MAX_NODE_SPACING = 360; -const DEFAULT_SWIRL_STRENGTH = 0; -const DEFAULT_SWIRL_BUTTON_STRENGTH = 32; -const MAX_SWIRL_STRENGTH = 100; -const SWIRL_FRAME_INTERVAL_MS = 42; -const SWIRL_BASE_SPEED_RAD_PER_SEC = 0.072; -const RING_JAGGED_BAND_FACTOR = 0.26; -const RING_JAGGED_MAX_FACTOR = 0.22; -const FAST_CANVAS_NODE_THRESHOLD = 2600; -const FAST_CANVAS_EDGE_THRESHOLD = 6500; -const FAST_CANVAS_LABEL_LIMIT = 90; -const MAX_DYNAMIC_ROUTE_EDGES = 12000; - -const LEGACY_DEFAULT_SETTINGS = { - atlasDepth: 4, - focusSiblingLimit: 80, - linkLimit: 220, - renderNodeLimit: 1400, - externalLinkAnchorLimit: 220, - enableLinkHover: false +"use strict";var ec=Object.defineProperty;var pm=Object.getOwnPropertyDescriptor;var mm=Object.getOwnPropertyNames;var gm=Object.prototype.hasOwnProperty;var xm=(i,e)=>{for(var t in e)ec(i,t,{get:e[t],enumerable:!0})},vm=(i,e,t,n)=>{if(e&&typeof e=="object"||typeof e=="function")for(let s of mm(e))!gm.call(i,s)&&s!==t&&ec(i,s,{get:()=>e[s],enumerable:!(n=pm(e,s))||n.enumerable});return i};var _m=i=>vm(ec({},"__esModule",{value:!0}),i);var iM={};xm(iM,{default:()=>Jl});module.exports=_m(iM);var Et=require("obsidian");var Wn="mini-world-map-view";var ss=2.2,tc=ss*6;var Jr={strength:.6,radius:.4,threshold:.18};var Dn={angularSpeed:.022,elevationDeg:8,elevationPeriodS:90,radiusBreath:.04,radiusPeriodS:60,resumeDelayMs:1e4,rampUpMs:2e3},Xn={distancePerRadius:12,minDistance:40,maxDistance:140,azimuthOffsetRad:15*Math.PI/180,minMs:800,maxMs:1800,msPerWorldUnit:.45},nc=8e-4;var Hs=[["none","None"],["note-links","Note links"],["hierarchy-parents","Hierarchy parents"],["hierarchy-direct-children","Hierarchy direct children"],["hierarchy-descendants","Hierarchy all children"],["hierarchy-parents-direct","Hierarchy parents + direct"],["hierarchy-all","Hierarchy parents + all children"]],jr=[["auto","Auto"],["hover","Hover only"]],Ws=[["nodes","Nodes"],["links","Links"],["both","Nodes + links"]],ic=[["root","legend.root","legend.root.desc","mwm-legend-root"],["folder","legend.folder","legend.folder.desc","mwm-legend-folder"],["folder-meta","legend.folderMeta","legend.folderMeta.desc","mwm-legend-meta"],["file","legend.note","legend.note.desc","mwm-legend-note"],["outside","legend.outsideGroup","legend.outsideGroup.desc","mwm-legend-external"],["outside-file","legend.outsideNote","legend.outsideNote.desc","mwm-legend-outside-file"],["missing","legend.unresolvedNote","legend.unresolvedNote.desc","mwm-legend-unresolved"],["tree","legend.hierarchy","legend.hierarchy.desc","mwm-legend-tree"],["link","legend.noteLinks","legend.noteLinks.desc","mwm-legend-link"],["outside-link","legend.outsideLinks","legend.outsideLinks.desc","mwm-legend-link-external"],["dashed-link","legend.unresolvedLinks","legend.unresolvedLinks.desc","mwm-legend-link-unresolved"]],Ot={atlasDepth:6,focusSiblingLimit:160,linkLimit:1200,renderNodeLimit:4200,externalLinkAnchorLimit:700,adaptiveDetail:!0,includeUnresolvedLinks:!0,showLinkOverlay:!0,showExternalLinks:!0,externalDetailMode:"grouped",colorScheme:"auto",labelVisibility:"auto",hoverHighlightMode:"hierarchy-all",hoverTargetMode:"nodes",swirlStrength:0,hiddenLegendItems:[],ignoreFolders:[".git",".obsidian"]},xn={bloom:{strength:.35,radius:.35,threshold:.22},physics:{repel:200,linkDistance:70,linkStrength:1,centerPull:.04,flatten:.3},look:{nodeSize:1,linkOpacity:.14,twinkle:.5,sizeBy:"degree"},cruise:!0,cruiseSpeed:1,showUnresolved:!1,showOrphans:!0,colorTheme:"imported",qualityOverride:"auto",preset:"adaptive",colorGroups:[],positionCache:{}},uu={language:"en",viewMode:"radial2d",radial:Ot,galaxy3d:xn};function sc(i){let e=Fn(i)?i:{},t=Fn(e.radial),n=t?e.radial:e,s=Fn(e.galaxy3d)?e.galaxy3d:e,r=ym(n);return!t&&e.showLinkOverlay===!1&&(r.showLinkOverlay=Ot.showLinkOverlay),{language:rs(e.language??e.locale??e.lang),viewMode:Qr(e.viewMode),radial:r,galaxy3d:bm(s)}}function ym(i){let e=Fn(i)?i:{},t=Ot,n=new Set(ic.map(([r])=>r)),s=e.hoverHighlightMode??(e.enableLinkHover===!0?"note-links":t.hoverHighlightMode);return{atlasDepth:Ct(e.atlasDepth,1,80,t.atlasDepth),focusSiblingLimit:Ct(e.focusSiblingLimit,10,1e3,t.focusSiblingLimit),linkLimit:Ct(e.linkLimit,0,3e4,t.linkLimit),renderNodeLimit:Ct(e.renderNodeLimit,200,2e4,t.renderNodeLimit),externalLinkAnchorLimit:Ct(e.externalLinkAnchorLimit,0,2e4,t.externalLinkAnchorLimit),adaptiveDetail:typeof e.adaptiveDetail=="boolean"?e.adaptiveDetail:t.adaptiveDetail,includeUnresolvedLinks:typeof e.includeUnresolvedLinks=="boolean"?e.includeUnresolvedLinks:t.includeUnresolvedLinks,showLinkOverlay:typeof e.showLinkOverlay=="boolean"?e.showLinkOverlay:t.showLinkOverlay,showExternalLinks:typeof e.showExternalLinks=="boolean"?e.showExternalLinks:t.showExternalLinks,externalDetailMode:rc(e.externalDetailMode),colorScheme:eo(e.colorScheme),labelVisibility:qs(e.labelVisibility),hoverHighlightMode:zi(s),hoverTargetMode:os(e.hoverTargetMode),swirlStrength:Ct(e.swirlStrength,0,100,t.swirlStrength),hiddenLegendItems:Array.isArray(e.hiddenLegendItems)?e.hiddenLegendItems.filter(r=>typeof r=="string"&&n.has(r)):t.hiddenLegendItems.slice(),ignoreFolders:Array.isArray(e.ignoreFolders)?e.ignoreFolders.filter(r=>typeof r=="string"&&r.trim().length>0):t.ignoreFolders.slice()}}function bm(i){let e=Fn(i)?i:{},t=xn,n=Fn(e.bloom)?e.bloom:{},s=Fn(e.physics)?e.physics:{},r=Fn(e.look)?e.look:{};return{bloom:{strength:on(n.strength,t.bloom.strength),radius:on(n.radius,t.bloom.radius),threshold:on(n.threshold,t.bloom.threshold)},physics:{repel:on(s.repel,t.physics.repel),linkDistance:on(s.linkDistance,t.physics.linkDistance),linkStrength:on(s.linkStrength,t.physics.linkStrength),centerPull:on(s.centerPull,t.physics.centerPull),flatten:on(s.flatten,t.physics.flatten)},look:{nodeSize:on(r.nodeSize,t.look.nodeSize),linkOpacity:on(r.linkOpacity,t.look.linkOpacity),twinkle:on(r.twinkle,t.look.twinkle),sizeBy:["degree","fileSize","uniform"].includes(r.sizeBy)?r.sizeBy:t.look.sizeBy},cruise:typeof e.cruise=="boolean"?e.cruise:t.cruise,cruiseSpeed:on(e.cruiseSpeed,t.cruiseSpeed),showUnresolved:typeof e.showUnresolved=="boolean"?e.showUnresolved:typeof e.includeUnresolvedLinks=="boolean"?e.includeUnresolvedLinks:t.showUnresolved,showOrphans:typeof e.showOrphans=="boolean"?e.showOrphans:t.showOrphans,colorTheme:typeof e.colorTheme=="string"?e.colorTheme:t.colorTheme,qualityOverride:["auto","high","low","mobile"].includes(e.qualityOverride)?e.qualityOverride:t.qualityOverride,preset:e.preset==="deep-space"?"deep-space":"adaptive",colorGroups:Array.isArray(e.colorGroups)?e.colorGroups.filter(o=>Fn(o)&&typeof o.query=="string"&&typeof o.color=="string"):[],positionCache:Fn(e.positionCache)&&!Array.isArray(e.positionCache)?e.positionCache:{}}}function Xs(i){return{charge:-i.repel,linkDistance:i.linkDistance,linkStrength:i.linkStrength,centerPull:i.centerPull,flatten:i.flatten,velocityDecay:.6}}function Qr(i){return i==="map3d"||i==="radial2d"?i:uu.viewMode}function rs(i){return i==="zh"?"zh":uu.language}function eo(i){return i==="auto"||i==="day"||i==="night"?i:Ot.colorScheme}function qs(i){return i==="hover"?"hover":Ot.labelVisibility}function zi(i){return Hs.some(([e])=>e===i)?i:Ot.hoverHighlightMode}function os(i){return Ws.some(([e])=>e===i)?i:Ot.hoverTargetMode}function Ys(i){return zi(i)==="note-links"}function rc(i){return i==="selected"||i==="exact"||i==="grouped"?i:Ot.externalDetailMode}function Ct(i,e,t,n){let s=on(i,n);return Math.min(Math.max(s,e),t)}function on(i,e){return typeof i=="number"&&Number.isFinite(i)?i:Number.parseFloat(String(i))||e}function Fn(i){return typeof i=="object"&&i!==null}var Mm=[["en","English"],["zh","\u4E2D\u6587"]],du={language:{en:"Language",zh:"\u8BED\u8A00"},"language.en":{en:"English",zh:"English"},"language.zh":{en:"Chinese",zh:"\u4E2D\u6587"},"mode.radial2d":{en:"2D radial",zh:"2D \u73AF\u5F62\u56FE"},"mode.map3d":{en:"3D map",zh:"3D \u5730\u56FE"},"stats.counts":{en:"{nodes} nodes / {links} links",zh:"{nodes} \u4E2A\u8282\u70B9 / {links} \u6761\u94FE\u63A5"},"stats.3d":{en:"{fps} fps \xB7 {calls} calls \xB7 {nodes}n/{links}l \xB7 {state}",zh:"{fps} fps \xB7 {calls} calls \xB7 {nodes} \u8282\u70B9/{links} \u94FE\u63A5 \xB7 {state}"},"state.settled":{en:"settled",zh:"\u5DF2\u6C89\u964D"},"state.layout":{en:"layout",zh:"\u5E03\u5C40\u4E2D"},"tab.inspect":{en:"Inspect",zh:"\u68C0\u67E5"},"tab.pins":{en:"Pins",zh:"\u56FA\u5B9A"},"tab.view":{en:"View",zh:"\u89C6\u56FE"},"tab.controls":{en:"Controls",zh:"\u63A7\u5236"},"tab.defaults":{en:"Defaults",zh:"\u9ED8\u8BA4"},"tab.appearance":{en:"Appearance",zh:"\u5916\u89C2"},"tab.physics":{en:"Physics",zh:"\u529B\u5B66"},"tab.motion":{en:"Motion",zh:"\u8FD0\u52A8"},"tab.advanced":{en:"Advanced",zh:"\u9AD8\u7EA7"},"common.search":{en:"Search",zh:"\u641C\u7D22"},"common.recenter":{en:"Center",zh:"\u56DE\u4E2D\u5FC3"},"common.fit":{en:"Fit",zh:"\u9002\u914D"},"common.rebuild":{en:"Rebuild",zh:"\u91CD\u5EFA"},"common.pin":{en:"Pin",zh:"\u56FA\u5B9A"},"common.pinCurrent":{en:"Pin current",zh:"\u56FA\u5B9A\u5F53\u524D"},"common.clear":{en:"Clear",zh:"\u6E05\u7A7A"},"common.group":{en:"Group",zh:"\u5206\u7EC4"},"common.inspect":{en:"Inspect",zh:"\u68C0\u67E5"},"common.open":{en:"Open",zh:"\u6253\u5F00"},"common.focus":{en:"Focus",zh:"\u805A\u7126"},"common.root":{en:"Root",zh:"\u6839\u8282\u70B9"},"common.source":{en:"Source",zh:"\u6765\u6E90"},"common.target":{en:"Target",zh:"\u76EE\u6807"},"common.none":{en:"None",zh:"\u65E0"},"common.default":{en:"Default",zh:"\u9ED8\u8BA4"},"common.resetDefaults":{en:"Reset defaults",zh:"\u91CD\u7F6E\u9ED8\u8BA4"},"view.atlas":{en:"Atlas",zh:"\u56FE\u8C31"},"view.focus":{en:"Focus",zh:"\u805A\u7126"},"view.vaultRoot":{en:"Vault root",zh:"\u5E93\u6839\u76EE\u5F55"},"view.mode":{en:"Map mode",zh:"\u5730\u56FE\u6A21\u5F0F"},"view.theme":{en:"Theme",zh:"\u4E3B\u9898"},"theme.auto":{en:"System",zh:"\u8DDF\u968F\u7CFB\u7EDF"},"theme.radialAuto":{en:"System",zh:"\u8DDF\u968F\u7CFB\u7EDF"},"theme.day":{en:"Light",zh:"\u6D45\u8272"},"theme.night":{en:"Dark",zh:"\u6DF1\u8272"},"theme.deep":{en:"Deep space",zh:"\u6DF1\u7A7A"},"inspect.type":{en:"Type",zh:"\u7C7B\u578B"},"inspect.depth":{en:"Depth",zh:"\u6DF1\u5EA6"},"inspect.notes":{en:"Notes",zh:"\u7B14\u8BB0"},"inspect.out":{en:"Out",zh:"\u51FA\u94FE"},"inspect.in":{en:"In",zh:"\u5165\u94FE"},"inspect.linkOverlay":{en:"Note link",zh:"\u7B14\u8BB0\u94FE\u63A5"},"inspect.weight":{en:"Weight",zh:"\u6743\u91CD"},"inspect.raw":{en:"Raw",zh:"\u539F\u59CB"},"inspect.unresolved":{en:"Unresolved",zh:"\u672A\u89E3\u6790"},"inspect.external":{en:"Outside",zh:"\u5916\u90E8"},"inspect.outgoing":{en:"Outgoing ({count})",zh:"\u51FA\u94FE\uFF08{count}\uFF09"},"inspect.backlinks":{en:"Backlinks ({count})",zh:"\u53CD\u5411\u94FE\u63A5\uFF08{count}\uFF09"},"pins.groupName":{en:"Group name",zh:"\u5206\u7EC4\u540D\u79F0"},"pins.empty":{en:"No pinned paths.",zh:"\u6682\u65E0\u56FA\u5B9A\u8DEF\u5F84\u3002"},"pins.already":{en:"That path is already pinned.",zh:"\u8FD9\u6761\u8DEF\u5F84\u5DF2\u7ECF\u56FA\u5B9A\u3002"},"pins.selectFirst":{en:"Select pinned paths to group.",zh:"\u5148\u9009\u62E9\u8981\u5206\u7EC4\u7684\u56FA\u5B9A\u8DEF\u5F84\u3002"},"pins.selectBeforePin":{en:"Select or hover a path before pinning.",zh:"\u5148\u9009\u62E9\u6216\u60AC\u505C\u4E00\u6761\u8DEF\u5F84\u518D\u56FA\u5B9A\u3002"},"control.depth":{en:"Depth",zh:"\u6DF1\u5EA6"},"control.nodes":{en:"Nodes",zh:"\u8282\u70B9"},"control.noteLinks":{en:"Note links",zh:"\u7B14\u8BB0\u94FE\u63A5"},"control.showNoteLinks":{en:"Show note links",zh:"\u663E\u793A\u7B14\u8BB0\u94FE\u63A5"},"control.hover":{en:"Hover",zh:"\u60AC\u505C"},"control.hoverTargets":{en:"Hover targets",zh:"\u60AC\u505C\u5BF9\u8C61"},"control.labels":{en:"Labels",zh:"\u6807\u7B7E"},"control.spin":{en:"Ring spin",zh:"\u73AF\u5F62\u65CB\u8F6C"},"control.outsideLinks":{en:"Outside links",zh:"\u5916\u90E8\u94FE\u63A5"},"control.outsideDetail":{en:"Outside detail",zh:"\u5916\u90E8\u7EC6\u8282"},"control.exactOutsideFiles":{en:"Exact outside notes",zh:"\u7CBE\u786E\u5916\u90E8\u7B14\u8BB0"},"control.legend":{en:"Legend",zh:"\u56FE\u4F8B"},"control.defaultDepth":{en:"Default depth",zh:"\u9ED8\u8BA4\u6DF1\u5EA6"},"control.defaultNodes":{en:"Default nodes",zh:"\u9ED8\u8BA4\u8282\u70B9\u6570"},"control.defaultNoteLinks":{en:"Default note links",zh:"\u9ED8\u8BA4\u7B14\u8BB0\u94FE\u63A5"},"control.unresolvedLinks":{en:"Unresolved links",zh:"\u672A\u89E3\u6790\u94FE\u63A5"},"control.ignoredFolders":{en:"Ignored folders",zh:"\u5FFD\u7565\u6587\u4EF6\u5939"},"hover.none":{en:"None",zh:"\u65E0"},"hover.note-links":{en:"Note links",zh:"\u7B14\u8BB0\u94FE\u63A5"},"hover.hierarchy-parents":{en:"Parents",zh:"\u7236\u7EA7"},"hover.hierarchy-direct-children":{en:"Direct children",zh:"\u76F4\u63A5\u5B50\u7EA7"},"hover.hierarchy-descendants":{en:"All children",zh:"\u5168\u90E8\u5B50\u7EA7"},"hover.hierarchy-parents-direct":{en:"Parents + direct children",zh:"\u7236\u7EA7 + \u76F4\u63A5\u5B50\u7EA7"},"hover.hierarchy-all":{en:"Parents + all children",zh:"\u7236\u7EA7 + \u5168\u90E8\u5B50\u7EA7"},"hoverTarget.nodes":{en:"Nodes only",zh:"\u4EC5\u8282\u70B9"},"hoverTarget.links":{en:"Links only",zh:"\u4EC5\u94FE\u63A5"},"hoverTarget.both":{en:"Nodes + links",zh:"\u8282\u70B9 + \u94FE\u63A5"},"labels.auto":{en:"Auto",zh:"\u81EA\u52A8"},"labels.hover":{en:"Hover only",zh:"\u4EC5\u60AC\u505C"},"outside.grouped":{en:"Groups",zh:"\u5206\u7EC4"},"outside.selected":{en:"Selected",zh:"\u9009\u4E2D"},"outside.exact":{en:"Exact",zh:"\u7CBE\u786E"},"legend.root":{en:"Root",zh:"\u6839\u8282\u70B9"},"legend.root.desc":{en:"Current atlas root",zh:"\u5F53\u524D\u56FE\u8C31\u6839\u8282\u70B9"},"legend.folder":{en:"Folders",zh:"\u6587\u4EF6\u5939"},"legend.folder.desc":{en:"Folders without a same-name child note",zh:"\u6CA1\u6709\u540C\u540D\u5B50\u7B14\u8BB0\u7684\u6587\u4EF6\u5939"},"legend.folderMeta":{en:"Folder notes",zh:"\u6587\u4EF6\u5939\u7B14\u8BB0"},"legend.folderMeta.desc":{en:"Folder merged with its same-name child note",zh:"\u4E0E\u540C\u540D\u5B50\u7B14\u8BB0\u5408\u5E76\u7684\u6587\u4EF6\u5939"},"legend.note":{en:"Notes",zh:"\u7B14\u8BB0"},"legend.note.desc":{en:"Markdown notes",zh:"Markdown \u7B14\u8BB0"},"legend.outsideGroup":{en:"Outside groups",zh:"\u5916\u90E8\u5206\u7EC4"},"legend.outsideGroup.desc":{en:"Grouped branches outside the root",zh:"\u6839\u8282\u70B9\u5916\u7684\u5206\u7EC4\u5206\u652F"},"legend.outsideNote":{en:"Outside notes",zh:"\u5916\u90E8\u7B14\u8BB0"},"legend.outsideNote.desc":{en:"Exact linked notes outside the root",zh:"\u6839\u8282\u70B9\u5916\u7684\u7CBE\u786E\u94FE\u63A5\u7B14\u8BB0"},"legend.unresolvedNote":{en:"Unresolved notes",zh:"\u672A\u89E3\u6790\u7B14\u8BB0"},"legend.unresolvedNote.desc":{en:"Unresolved internal link targets",zh:"\u672A\u89E3\u6790\u7684\u5185\u90E8\u94FE\u63A5\u76EE\u6807"},"legend.hierarchy":{en:"Hierarchy",zh:"\u5C42\u7EA7"},"legend.hierarchy.desc":{en:"Parent-child hierarchy edges",zh:"\u7236\u5B50\u5C42\u7EA7\u8FB9"},"legend.noteLinks":{en:"Note links",zh:"\u7B14\u8BB0\u94FE\u63A5"},"legend.noteLinks.desc":{en:"Internal markdown links",zh:"\u5185\u90E8 Markdown \u94FE\u63A5"},"legend.outsideLinks":{en:"Outside links",zh:"\u5916\u90E8\u94FE\u63A5"},"legend.outsideLinks.desc":{en:"Links crossing the current root",zh:"\u8DE8\u51FA\u5F53\u524D\u6839\u8282\u70B9\u7684\u94FE\u63A5"},"legend.unresolvedLinks":{en:"Unresolved links",zh:"\u672A\u89E3\u6790\u94FE\u63A5"},"legend.unresolvedLinks.desc":{en:"Links involving unresolved targets",zh:"\u5305\u542B\u672A\u89E3\u6790\u76EE\u6807\u7684\u94FE\u63A5"},"loading.radial":{en:"Building map\u2026",zh:"\u6784\u5EFA\u5730\u56FE\u2026"},"loading.3d":{en:"Building map\u2026",zh:"\u6784\u5EFA\u5730\u56FE\u2026"},"context.openNote":{en:"Open note",zh:"\u6253\u5F00\u7B14\u8BB0"},"context.focusNote":{en:"Focus note",zh:"\u805A\u7126\u7B14\u8BB0"},"context.useAsRoot":{en:"Use as atlas root",zh:"\u8BBE\u4E3A\u56FE\u8C31\u6839\u8282\u70B9"},"context.openRepresentative":{en:"Open representative note",zh:"\u6253\u5F00\u4EE3\u8868\u7B14\u8BB0"},"context.pinPath":{en:"Pin highlighted path",zh:"\u56FA\u5B9A\u9AD8\u4EAE\u8DEF\u5F84"},"3d.cruiseOn":{en:"Cruise: on",zh:"\u5DE1\u822A\uFF1A\u5F00"},"3d.cruiseOff":{en:"Cruise: off",zh:"\u5DE1\u822A\uFF1A\u5173"},"3d.reveal":{en:"Reveal",zh:"\u521B\u4E16\u52A8\u753B"},"3d.glow":{en:"Glow",zh:"\u8F89\u5149"},"3d.glowStrength":{en:"Strength",zh:"\u5F3A\u5EA6"},"3d.glowRadius":{en:"Radius",zh:"\u6269\u6563"},"3d.glowThreshold":{en:"Threshold",zh:"\u9608\u503C"},"3d.repel":{en:"Repel",zh:"\u65A5\u529B"},"3d.linkDistance":{en:"Link distance",zh:"\u94FE\u63A5\u8DDD\u79BB"},"3d.linkStrength":{en:"Link strength",zh:"\u94FE\u63A5\u5F3A\u5EA6"},"3d.centerPull":{en:"Center pull",zh:"\u5411\u5FC3\u529B"},"3d.flatten":{en:"Flatten",zh:"\u6241\u5E73\u5EA6"},"3d.nodeSize":{en:"Node size",zh:"\u8282\u70B9\u5927\u5C0F"},"3d.linkOpacity":{en:"Link opacity",zh:"\u94FE\u63A5\u900F\u660E\u5EA6"},"3d.twinkle":{en:"Twinkle",zh:"\u661F\u661F\u7728\u773C"},"3d.twinkleOff":{en:"Off",zh:"\u5173"},"3d.size.degree":{en:"Size: links",zh:"\u5927\u5C0F\uFF1A\u94FE\u63A5\u6570"},"3d.size.fileSize":{en:"Size: file size",zh:"\u5927\u5C0F\uFF1A\u6587\u6863\u91CF"},"3d.size.uniform":{en:"Size: uniform",zh:"\u5927\u5C0F\uFF1A\u4E00\u81F4"},"3d.colorTheme":{en:"Color theme\u2026",zh:"\u914D\u8272\u4E3B\u9898\u2026"},"3d.importColors":{en:"Import 2D colors",zh:"\u5BFC\u5165\u4E8C\u7EF4\u914D\u8272"},"3d.shuffleColors":{en:"Shuffle colors",zh:"\u914D\u8272\u6D17\u724C"},"3d.speed":{en:"Speed",zh:"\u901F\u5EA6"},"3d.unresolvedShow":{en:"Unresolved: show",zh:"\u672A\u89E3\u6790\uFF1A\u663E\u793A"},"3d.unresolvedHide":{en:"Unresolved: hide",zh:"\u672A\u89E3\u6790\uFF1A\u9690\u85CF"},"3d.orphansShow":{en:"Orphans: show",zh:"\u5B64\u513F\uFF1A\u663E\u793A"},"3d.orphansHide":{en:"Orphans: hide",zh:"\u5B64\u513F\uFF1A\u9690\u85CF"},"3d.quality.auto":{en:"Quality: auto",zh:"\u753B\u8D28\uFF1A\u81EA\u52A8"},"3d.quality.high":{en:"Quality: high",zh:"\u753B\u8D28\uFF1A\u9AD8"},"3d.quality.low":{en:"Quality: low",zh:"\u753B\u8D28\uFF1A\u4F4E"},"3d.quality.mobile":{en:"Quality: mobile",zh:"\u753B\u8D28\uFF1A\u79FB\u52A8\u6A21\u62DF"},"3d.help.drag":{en:"Left drag = orbit \xB7 wheel = zoom",zh:"\u5DE6\u952E\u62D6 = \u73AF\u7ED5 \xB7 \u6EDA\u8F6E = \u7F29\u653E"},"3d.help.pan":{en:"Right drag / Cmd or Shift + left drag = pan",zh:"\u53F3\u952E\u62D6 / \u2318\u6216\u21E7+\u5DE6\u952E\u62D6 = \u5E73\u79FB"},"3d.help.mac":{en:"macOS treats Ctrl+click as right-click",zh:"macOS \u7684 Ctrl+\u70B9\u51FB\u4F1A\u88AB\u7CFB\u7EDF\u5F53\u53F3\u952E"},"3d.help.fly":{en:"WASD = fly \xB7 Q/E = rise/fall \xB7 Shift = fast",zh:"WASD = \u5E73\u98DE \xB7 Q/E = \u5347\u964D \xB7 Shift = \u52A0\u901F"},"3d.help.pick":{en:"Click node = select and fly \xB7 ESC = clear",zh:"\u70B9\u51FB\u8282\u70B9 = \u9009\u4E2D\u98DE\u884C \xB7 ESC = \u53D6\u6D88"},"3d.help.keys":{en:"F = fly to selected \xB7 R = overview",zh:"F = \u98DE\u5411\u9009\u4E2D \xB7 R = \u56DE\u603B\u89C8"},"3d.help.slider":{en:"Double-click a slider to reset it",zh:"\u53CC\u51FB\u6ED1\u6746 = \u56DE\u9ED8\u8BA4\u503C"},"3d.revealWait":{en:"The map is still settling. Try reveal after it settles.",zh:"\u661F\u7CFB\u8FD8\u5728\u6210\u5F62\u4E2D\uFF0C\u6C89\u964D\u540E\u518D\u8BD5\u3002"},"3d.workerFallback":{en:"Mini World Map 3D: background layout worker is unavailable; using the main thread.",zh:"Mini World Map 3D\uFF1A\u540E\u53F0\u7EBF\u7A0B\u4E0D\u53EF\u7528\uFF0C\u5DF2\u56DE\u9000\u4E3B\u7EBF\u7A0B\u5E03\u5C40\u3002"},"3d.mobileCap":{en:"Mobile quality: showing the top {cap} linked nodes out of {total}.",zh:"\u79FB\u52A8\u6863\uFF1A\u5DF2\u663E\u793A\u94FE\u63A5\u6700\u591A\u7684\u524D {cap} \u4E2A\u8282\u70B9\uFF08\u5171 {total}\uFF09\u3002"},"3d.performanceMode":{en:"Mini World Map 3D switched to performance mode. Change quality in Advanced.",zh:"Mini World Map 3D\uFF1A\u5DF2\u81EA\u52A8\u5207\u6362\u5230\u6027\u80FD\u6A21\u5F0F\uFF0C\u53EF\u5728\u9AD8\u7EA7\u9875\u6539\u56DE\u3002"},"3d.importMissing":{en:"No 2D graph color groups found in graph.json.",zh:"\u672A\u627E\u5230\u81EA\u5E26\u56FE\u8C31\u7684\u989C\u8272\u5206\u7EC4\uFF08graph.json\uFF09\u3002"},"3d.importDone":{en:"Imported {count} 2D color groups.",zh:"\u5DF2\u5BFC\u5165 {count} \u7EC4 2D \u56FE\u8C31\u914D\u8272\u3002"},"3d.shuffleMissing":{en:"Import 2D colors before shuffling.",zh:"\u5148\u5BFC\u5165\u4E8C\u7EF4\u56FE\u8C31\u914D\u8272\uFF0C\u624D\u80FD\u6D17\u724C\u3002"},"3d.contextLost":{en:"Rendering context lost. Click to rebuild.",zh:"\u6E32\u67D3\u4E0A\u4E0B\u6587\u4E22\u5931\uFF0C\u70B9\u51FB\u91CD\u5EFA\u3002"},"3d.searchPlaceholder":{en:"Search notes, press Enter to fly\u2026",zh:"\u641C\u7D22\u7B14\u8BB0\uFF0C\u56DE\u8F66\u98DE\u8FC7\u53BB\u2026"},"3d.searchUnresolved":{en:"Unresolved",zh:"\u672A\u89E3\u6790"},"3d.searchLinks":{en:"{count} links",zh:"{count} \u94FE\u63A5"},"3d.card.unresolved":{en:"Unresolved link (note does not exist)",zh:"\u672A\u89E3\u6790\u94FE\u63A5\uFF08\u7B14\u8BB0\u5C1A\u4E0D\u5B58\u5728\uFF09"},"3d.card.root":{en:"Vault root",zh:"\u6839\u76EE\u5F55"},"3d.card.stats":{en:"\u21A9 {in} backlinks \xB7 \u2192 {out} outgoing",zh:"\u21A9 {in} \u53CD\u94FE \xB7 \u2192 {out} \u51FA\u94FE"},"3d.card.modified":{en:" \xB7 modified {date}",zh:" \xB7 \u6539\u4E8E {date}"},"3d.card.empty":{en:"(empty note)",zh:"\uFF08\u7A7A\u7B14\u8BB0\uFF09"},"3d.bench.wait":{en:"{scenario}: waiting for layout to settle\u2026",zh:"{scenario}\uFF1A\u7B49\u5F85\u5E03\u5C40\u6C89\u964D\u2026"},"3d.bench.orbit":{en:"{scenario}: 20s orbit FPS run\u2026",zh:"{scenario}\uFF1A20s \u73AF\u7ED5\u6D4B\u5E27\u7387\u2026"},"3d.bench.done":{en:"{scenario} done: avg {fps} fps \xB7 {calls} calls",zh:"{scenario} \u5B8C\u6210\uFF1Aavg {fps} fps \xB7 {calls} calls"},"3d.bench.s2Start":{en:"S2: cold layout started. The interface should remain responsive.",zh:"S2\uFF1A\u51B7\u5E03\u5C40\u5F00\u59CB\uFF08\u9884\u7B97\u5316 tick\uFF0C\u671F\u95F4\u754C\u9762\u5E94\u4FDD\u6301\u53EF\u7528\uFF09\u2026"},"3d.bench.s2Done":{en:"S2 done: settled in {seconds}s / {ticks} ticks, longest block {longest}ms",zh:"S2 \u5B8C\u6210\uFF1A\u6C89\u964D {seconds}s / {ticks} ticks\uFF0C\u6700\u957F\u963B\u585E {longest}ms"},"style.galaxy":{en:"Galaxy",zh:"\u94F6\u6CB3"},"style.nebula":{en:"Nebula",zh:"\u661F\u4E91"},"style.minimal":{en:"Minimal",zh:"\u6781\u7B80"},"style.fireworks":{en:"Fireworks",zh:"\u70DF\u706B"},"color.hubble":{en:"Hubble deep field",zh:"\u54C8\u52C3\u6DF1\u7A7A"},"color.tiktok":{en:"Neon pop",zh:"\u6296\u97F3\u9713\u8679"},"color.sunset":{en:"Sunset film",zh:"\u843D\u65E5\u80F6\u7247"},"color.cyber":{en:"Cyber city",zh:"\u8D5B\u535A\u90FD\u5E02"},"color.matrix":{en:"Matrix",zh:"\u9ED1\u5BA2\u5E1D\u56FD"},"color.aurora":{en:"Aurora",zh:"\u6781\u5149"},"settings.title":{en:"Mini World Map",zh:"Mini World Map"},"settings.defaultMode":{en:"Default render mode",zh:"\u9ED8\u8BA4\u6E32\u67D3\u6A21\u5F0F"},"settings.defaultModeDesc":{en:"2D radial rings is the hierarchy-first map. 3D map uses the Galaxy renderer.",zh:"2D \u73AF\u5F62\u56FE\u4EE5\u5C42\u7EA7\u4E3A\u4E3B\uFF1B3D \u5730\u56FE\u4F7F\u7528 Galaxy \u6E32\u67D3\u5668\u3002"},"settings.languageDesc":{en:"Language used by both 2D and 3D panels.",zh:"2D \u4E0E 3D \u9762\u677F\u5171\u540C\u4F7F\u7528\u7684\u8BED\u8A00\u3002"},"settings.depthDesc":{en:"How many hierarchy levels to render before deeper nodes are summarized by budgets.",zh:"\u6E32\u67D3\u591A\u5C11\u5C42\u7EA7\u540E\u7531\u9884\u7B97\u6C47\u603B\u66F4\u6DF1\u8282\u70B9\u3002"},"settings.nodeLimitDesc":{en:"Maximum visible nodes in the 2D radial map.",zh:"2D \u73AF\u5F62\u56FE\u6700\u591A\u663E\u793A\u7684\u8282\u70B9\u6570\u3002"},"settings.linkLimitDesc":{en:"Maximum aggregated note links to draw in 2D.",zh:"2D \u4E2D\u6700\u591A\u7ED8\u5236\u7684\u805A\u5408\u7B14\u8BB0\u94FE\u63A5\u6570\u3002"},"settings.showLinksDesc":{en:"Keeps note-link roads visible. Hover/link pinning still works when this is off.",zh:"\u4FDD\u6301\u7B14\u8BB0\u94FE\u63A5\u53EF\u89C1\uFF1B\u5173\u95ED\u540E\u60AC\u505C\u4E0E\u94FE\u63A5\u56FA\u5B9A\u4ECD\u53EF\u7528\u3002"},"settings.hoverDesc":{en:"Choose what 2D hover highlights.",zh:"\u9009\u62E9 2D \u60AC\u505C\u9AD8\u4EAE\u7684\u5185\u5BB9\u3002"},"settings.hoverTargetsDesc":{en:"Choose whether nodes, note-link roads, or both react to hover and click.",zh:"\u9009\u62E9\u8282\u70B9\u3001\u7B14\u8BB0\u94FE\u63A5\u9053\u8DEF\u6216\u4E24\u8005\u662F\u5426\u54CD\u5E94\u60AC\u505C\u548C\u70B9\u51FB\u3002"},"settings.labelsDesc":{en:"Auto shows important names by zoom; hover only keeps the map quieter.",zh:"\u81EA\u52A8\u6309\u7F29\u653E\u663E\u793A\u91CD\u8981\u540D\u79F0\uFF1B\u4EC5\u60AC\u505C\u4F1A\u66F4\u5B89\u9759\u3002"},"settings.spinDesc":{en:"Optional radial ring motion in the 2D map.",zh:"2D \u73AF\u5F62\u56FE\u4E2D\u7684\u53EF\u9009\u73AF\u5F62\u8FD0\u52A8\u3002"},"settings.unresolvedDesc":{en:"Represent unresolved internal links as temporary nodes.",zh:"\u5C06\u672A\u89E3\u6790\u5185\u90E8\u94FE\u63A5\u663E\u793A\u4E3A\u4E34\u65F6\u8282\u70B9\u3002"},"settings.ignoredDesc":{en:"One folder path per line.",zh:"\u6BCF\u884C\u4E00\u4E2A\u6587\u4EF6\u5939\u8DEF\u5F84\u3002"},"notice.rebuilt":{en:"Mini World Map rebuilt",zh:"Mini World Map \u5DF2\u91CD\u5EFA"},"notice.openToBuild":{en:"Open Mini World Map to build the index",zh:"\u6253\u5F00 Mini World Map \u540E\u624D\u80FD\u6784\u5EFA\u7D22\u5F15"},"notice.switchTo3d":{en:"Switch to 3D map to use fly-to search.",zh:"\u5207\u6362\u5230 3D \u5730\u56FE\u540E\u53EF\u4F7F\u7528\u98DE\u884C\u641C\u7D22\u3002"}};function Ce(i,e,t={}){return(du[e]?.[i]??du[e]?.en??e).replace(/\{(\w+)\}/g,(s,r)=>String(t[r]??`{${r}}`))}function ui(i,e){return Ce(i,`mode.${e}`)}function fu(i){return[["auto",Ce(i,"theme.radialAuto")],["day",Ce(i,"theme.day")],["night",Ce(i,"theme.night")]]}function as(i){return Mm.map(([e])=>[e,Ce(i,`language.${e}`)])}function to(i,e){return e.map(([t])=>[t,Ce(i,`hover.${t}`)])}function no(i,e){return e.map(([t])=>[t,Ce(i,`hoverTarget.${t}`)])}function io(i,e){return e.map(([t])=>[t,Ce(i,`labels.${t}`)])}function pu(i){return[["grouped",Ce(i,"outside.grouped")],["selected",Ce(i,"outside.selected")],["exact",Ce(i,"outside.exact")]]}var rm=require("obsidian");var _t=require("obsidian");var da="184",Cn={LEFT:0,MIDDLE:1,RIGHT:2,ROTATE:0,DOLLY:1,PAN:2},Ri={ROTATE:0,PAN:1,DOLLY_PAN:2,DOLLY_ROTATE:3},zu=0,Oc=1,Vu=2;var vr=1,Gu=2,Cs=3,Qn=0,$t=1,zn=2,yn=0,qi=1,_r=2,Uc=3,Bc=4,Hu=5;var _i=100,Wu=101,Xu=102,qu=103,Yu=104,Zu=200,$u=201,Ku=202,Ju=203,Po=204,Io=205,ju=206,Qu=207,ed=208,td=209,nd=210,id=211,sd=212,rd=213,od=214,Lo=0,No=1,Do=2,Yi=3,Fo=4,ko=5,Oo=6,Uo=7,zc=0,ad=1,ld=2,fn=0,yr=1,br=2,Mr=3,Pi=4,Sr=5,wr=6,Er=7;var Vc=300,Ii=301,$i=302,fa=303,pa=304,Tr=306,Bo=1e3,On=1001,zo=1002,Dt=1003,cd=1004;var Ar=1005;var Bt=1006,ma=1007;var Li=1008;var pn=1009,Gc=1010,Hc=1011,Rs=1012,ga=1013,Rn=1014,Pn=1015,qt=1016,xa=1017,va=1018,Ps=1020,Wc=35902,Xc=35899,qc=1021,Yc=1022,bn=1023,Un=1026,Ni=1027,Zc=1028,_a=1029,Di=1030,ya=1031;var ba=1033,Cr=33776,Rr=33777,Pr=33778,Ir=33779,Ma=35840,Sa=35841,wa=35842,Ea=35843,Ta=36196,Aa=37492,Ca=37496,Ra=37488,Pa=37489,Lr=37490,Ia=37491,La=37808,Na=37809,Da=37810,Fa=37811,ka=37812,Oa=37813,Ua=37814,Ba=37815,za=37816,Va=37817,Ga=37818,Ha=37819,Wa=37820,Xa=37821,qa=36492,Ya=36494,Za=36495,$a=36283,Ka=36284,Nr=36285,Ja=36286;var tr=2300,Vo=2301,Ro=2302,Cc=2303,Rc=2400,Pc=2401,Ic=2402;var hd=3200;var $c=0,ud=1,ri="",cn="srgb",nr="srgb-linear",ir="linear",Je="srgb";var Xi=7680;var Lc=519,dd=512,fd=513,pd=514,ja=515,md=516,gd=517,Qa=518,xd=519,Nc=35044;var Kc="300 es",Tn=2e3,sr=2001;function Sm(i){for(let e=i.length-1;e>=0;--e)if(i[e]>=65535)return!0;return!1}function wm(i){return ArrayBuffer.isView(i)&&!(i instanceof DataView)}function rr(i){return document.createElementNS("http://www.w3.org/1999/xhtml",i)}function vd(){let i=rr("canvas");return i.style.display="block",i}var mu={},Ms=null;function Jc(...i){let e="THREE."+i.shift();Ms?Ms("log",e,...i):console.log(e,...i)}function _d(i){let e=i[0];if(typeof e=="string"&&e.startsWith("TSL:")){let t=i[1];t&&t.isStackTrace?i[0]+=" "+t.getLocation():i[1]='Stack trace not available. Enable "THREE.Node.captureStackTrace" to capture stack traces.'}return i}function Te(...i){i=_d(i);let e="THREE."+i.shift();if(Ms)Ms("warn",e,...i);else{let t=i[0];t&&t.isStackTrace?console.warn(t.getError(e)):console.warn(e,...i)}}function Re(...i){i=_d(i);let e="THREE."+i.shift();if(Ms)Ms("error",e,...i);else{let t=i[0];t&&t.isStackTrace?console.error(t.getError(e)):console.error(e,...i)}}function Go(...i){let e=i.join(" ");e in mu||(mu[e]=!0,Te(...i))}function yd(i,e,t){return new Promise(function(n,s){function r(){switch(i.clientWaitSync(e,i.SYNC_FLUSH_COMMANDS_BIT,0)){case i.WAIT_FAILED:s();break;case i.TIMEOUT_EXPIRED:setTimeout(r,t);break;default:n()}}setTimeout(r,t)})}var bd={[Lo]:No,[Do]:Oo,[Fo]:Uo,[Yi]:ko,[No]:Lo,[Oo]:Do,[Uo]:Fo,[ko]:Yi},An=class{addEventListener(e,t){this._listeners===void 0&&(this._listeners={});let n=this._listeners;n[e]===void 0&&(n[e]=[]),n[e].indexOf(t)===-1&&n[e].push(t)}hasEventListener(e,t){let n=this._listeners;return n===void 0?!1:n[e]!==void 0&&n[e].indexOf(t)!==-1}removeEventListener(e,t){let n=this._listeners;if(n===void 0)return;let s=n[e];if(s!==void 0){let r=s.indexOf(t);r!==-1&&s.splice(r,1)}}dispatchEvent(e){let t=this._listeners;if(t===void 0)return;let n=t[e.type];if(n!==void 0){e.target=this;let s=n.slice(0);for(let r=0,o=s.length;r>8&255]+Gt[i>>16&255]+Gt[i>>24&255]+"-"+Gt[e&255]+Gt[e>>8&255]+"-"+Gt[e>>16&15|64]+Gt[e>>24&255]+"-"+Gt[t&63|128]+Gt[t>>8&255]+"-"+Gt[t>>16&255]+Gt[t>>24&255]+Gt[n&255]+Gt[n>>8&255]+Gt[n>>16&255]+Gt[n>>24&255]).toLowerCase()}function Ve(i,e,t){return Math.max(e,Math.min(t,i))}function jc(i,e){return(i%e+e)%e}function Em(i,e,t,n,s){return n+(i-e)*(s-n)/(t-e)}function Tm(i,e,t){return i!==e?(t-i)/(e-i):0}function er(i,e,t){return(1-t)*i+t*e}function Am(i,e,t,n){return er(i,e,1-Math.exp(-t*n))}function Cm(i,e=1){return e-Math.abs(jc(i,e*2)-e)}function Rm(i,e,t){return i<=e?0:i>=t?1:(i=(i-e)/(t-e),i*i*(3-2*i))}function Pm(i,e,t){return i<=e?0:i>=t?1:(i=(i-e)/(t-e),i*i*i*(i*(i*6-15)+10))}function Im(i,e){return i+Math.floor(Math.random()*(e-i+1))}function Lm(i,e){return i+Math.random()*(e-i)}function Nm(i){return i*(.5-Math.random())}function Dm(i){i!==void 0&&(gu=i);let e=gu+=1831565813;return e=Math.imul(e^e>>>15,e|1),e^=e+Math.imul(e^e>>>7,e|61),((e^e>>>14)>>>0)/4294967296}function Fm(i){return i*Qs}function km(i){return i*Ss}function Om(i){return(i&i-1)===0&&i!==0}function Um(i){return Math.pow(2,Math.ceil(Math.log(i)/Math.LN2))}function Bm(i){return Math.pow(2,Math.floor(Math.log(i)/Math.LN2))}function zm(i,e,t,n,s){let r=Math.cos,o=Math.sin,a=r(t/2),l=o(t/2),c=r((e+n)/2),u=o((e+n)/2),d=r((e-n)/2),h=o((e-n)/2),f=r((n-e)/2),g=o((n-e)/2);switch(s){case"XYX":i.set(a*u,l*d,l*h,a*c);break;case"YZY":i.set(l*h,a*u,l*d,a*c);break;case"ZXZ":i.set(l*d,l*h,a*u,a*c);break;case"XZX":i.set(a*u,l*g,l*f,a*c);break;case"YXY":i.set(l*f,a*u,l*g,a*c);break;case"ZYZ":i.set(l*g,l*f,a*u,a*c);break;default:Te("MathUtils: .setQuaternionFromProperEuler() encountered an unknown order: "+s)}}function ys(i,e){switch(e.constructor){case Float32Array:return i;case Uint32Array:return i/4294967295;case Uint16Array:return i/65535;case Uint8Array:return i/255;case Int32Array:return Math.max(i/2147483647,-1);case Int16Array:return Math.max(i/32767,-1);case Int8Array:return Math.max(i/127,-1);default:throw new Error("Invalid component type.")}}function Zt(i,e){switch(e.constructor){case Float32Array:return i;case Uint32Array:return Math.round(i*4294967295);case Uint16Array:return Math.round(i*65535);case Uint8Array:return Math.round(i*255);case Int32Array:return Math.round(i*2147483647);case Int16Array:return Math.round(i*32767);case Int8Array:return Math.round(i*127);default:throw new Error("Invalid component type.")}}var Qc={DEG2RAD:Qs,RAD2DEG:Ss,generateUUID:Is,clamp:Ve,euclideanModulo:jc,mapLinear:Em,inverseLerp:Tm,lerp:er,damp:Am,pingpong:Cm,smoothstep:Rm,smootherstep:Pm,randInt:Im,randFloat:Lm,randFloatSpread:Nm,seededRandom:Dm,degToRad:Fm,radToDeg:km,isPowerOfTwo:Om,ceilPowerOfTwo:Um,floorPowerOfTwo:Bm,setQuaternionFromProperEuler:zm,normalize:Zt,denormalize:ys},sh=class sh{constructor(e=0,t=0){this.x=e,this.y=t}get width(){return this.x}set width(e){this.x=e}get height(){return this.y}set height(e){this.y=e}set(e,t){return this.x=e,this.y=t,this}setScalar(e){return this.x=e,this.y=e,this}setX(e){return this.x=e,this}setY(e){return this.y=e,this}setComponent(e,t){switch(e){case 0:this.x=t;break;case 1:this.y=t;break;default:throw new Error("index is out of range: "+e)}return this}getComponent(e){switch(e){case 0:return this.x;case 1:return this.y;default:throw new Error("index is out of range: "+e)}}clone(){return new this.constructor(this.x,this.y)}copy(e){return this.x=e.x,this.y=e.y,this}add(e){return this.x+=e.x,this.y+=e.y,this}addScalar(e){return this.x+=e,this.y+=e,this}addVectors(e,t){return this.x=e.x+t.x,this.y=e.y+t.y,this}addScaledVector(e,t){return this.x+=e.x*t,this.y+=e.y*t,this}sub(e){return this.x-=e.x,this.y-=e.y,this}subScalar(e){return this.x-=e,this.y-=e,this}subVectors(e,t){return this.x=e.x-t.x,this.y=e.y-t.y,this}multiply(e){return this.x*=e.x,this.y*=e.y,this}multiplyScalar(e){return this.x*=e,this.y*=e,this}divide(e){return this.x/=e.x,this.y/=e.y,this}divideScalar(e){return this.multiplyScalar(1/e)}applyMatrix3(e){let t=this.x,n=this.y,s=e.elements;return this.x=s[0]*t+s[3]*n+s[6],this.y=s[1]*t+s[4]*n+s[7],this}min(e){return this.x=Math.min(this.x,e.x),this.y=Math.min(this.y,e.y),this}max(e){return this.x=Math.max(this.x,e.x),this.y=Math.max(this.y,e.y),this}clamp(e,t){return this.x=Ve(this.x,e.x,t.x),this.y=Ve(this.y,e.y,t.y),this}clampScalar(e,t){return this.x=Ve(this.x,e,t),this.y=Ve(this.y,e,t),this}clampLength(e,t){let n=this.length();return this.divideScalar(n||1).multiplyScalar(Ve(n,e,t))}floor(){return this.x=Math.floor(this.x),this.y=Math.floor(this.y),this}ceil(){return this.x=Math.ceil(this.x),this.y=Math.ceil(this.y),this}round(){return this.x=Math.round(this.x),this.y=Math.round(this.y),this}roundToZero(){return this.x=Math.trunc(this.x),this.y=Math.trunc(this.y),this}negate(){return this.x=-this.x,this.y=-this.y,this}dot(e){return this.x*e.x+this.y*e.y}cross(e){return this.x*e.y-this.y*e.x}lengthSq(){return this.x*this.x+this.y*this.y}length(){return Math.sqrt(this.x*this.x+this.y*this.y)}manhattanLength(){return Math.abs(this.x)+Math.abs(this.y)}normalize(){return this.divideScalar(this.length()||1)}angle(){return Math.atan2(-this.y,-this.x)+Math.PI}angleTo(e){let t=Math.sqrt(this.lengthSq()*e.lengthSq());if(t===0)return Math.PI/2;let n=this.dot(e)/t;return Math.acos(Ve(n,-1,1))}distanceTo(e){return Math.sqrt(this.distanceToSquared(e))}distanceToSquared(e){let t=this.x-e.x,n=this.y-e.y;return t*t+n*n}manhattanDistanceTo(e){return Math.abs(this.x-e.x)+Math.abs(this.y-e.y)}setLength(e){return this.normalize().multiplyScalar(e)}lerp(e,t){return this.x+=(e.x-this.x)*t,this.y+=(e.y-this.y)*t,this}lerpVectors(e,t,n){return this.x=e.x+(t.x-e.x)*n,this.y=e.y+(t.y-e.y)*n,this}equals(e){return e.x===this.x&&e.y===this.y}fromArray(e,t=0){return this.x=e[t],this.y=e[t+1],this}toArray(e=[],t=0){return e[t]=this.x,e[t+1]=this.y,e}fromBufferAttribute(e,t){return this.x=e.getX(t),this.y=e.getY(t),this}rotateAround(e,t){let n=Math.cos(t),s=Math.sin(t),r=this.x-e.x,o=this.y-e.y;return this.x=r*n-o*s+e.x,this.y=r*s+o*n+e.y,this}random(){return this.x=Math.random(),this.y=Math.random(),this}*[Symbol.iterator](){yield this.x,yield this.y}};sh.prototype.isVector2=!0;var Ee=sh,hn=class{constructor(e=0,t=0,n=0,s=1){this.isQuaternion=!0,this._x=e,this._y=t,this._z=n,this._w=s}static slerpFlat(e,t,n,s,r,o,a){let l=n[s+0],c=n[s+1],u=n[s+2],d=n[s+3],h=r[o+0],f=r[o+1],g=r[o+2],x=r[o+3];if(d!==x||l!==h||c!==f||u!==g){let m=l*h+c*f+u*g+d*x;m<0&&(h=-h,f=-f,g=-g,x=-x,m=-m);let p=1-a;if(m<.9995){let v=Math.acos(m),b=Math.sin(v);p=Math.sin(p*v)/b,a=Math.sin(a*v)/b,l=l*p+h*a,c=c*p+f*a,u=u*p+g*a,d=d*p+x*a}else{l=l*p+h*a,c=c*p+f*a,u=u*p+g*a,d=d*p+x*a;let v=1/Math.sqrt(l*l+c*c+u*u+d*d);l*=v,c*=v,u*=v,d*=v}}e[t]=l,e[t+1]=c,e[t+2]=u,e[t+3]=d}static multiplyQuaternionsFlat(e,t,n,s,r,o){let a=n[s],l=n[s+1],c=n[s+2],u=n[s+3],d=r[o],h=r[o+1],f=r[o+2],g=r[o+3];return e[t]=a*g+u*d+l*f-c*h,e[t+1]=l*g+u*h+c*d-a*f,e[t+2]=c*g+u*f+a*h-l*d,e[t+3]=u*g-a*d-l*h-c*f,e}get x(){return this._x}set x(e){this._x=e,this._onChangeCallback()}get y(){return this._y}set y(e){this._y=e,this._onChangeCallback()}get z(){return this._z}set z(e){this._z=e,this._onChangeCallback()}get w(){return this._w}set w(e){this._w=e,this._onChangeCallback()}set(e,t,n,s){return this._x=e,this._y=t,this._z=n,this._w=s,this._onChangeCallback(),this}clone(){return new this.constructor(this._x,this._y,this._z,this._w)}copy(e){return this._x=e.x,this._y=e.y,this._z=e.z,this._w=e.w,this._onChangeCallback(),this}setFromEuler(e,t=!0){let n=e._x,s=e._y,r=e._z,o=e._order,a=Math.cos,l=Math.sin,c=a(n/2),u=a(s/2),d=a(r/2),h=l(n/2),f=l(s/2),g=l(r/2);switch(o){case"XYZ":this._x=h*u*d+c*f*g,this._y=c*f*d-h*u*g,this._z=c*u*g+h*f*d,this._w=c*u*d-h*f*g;break;case"YXZ":this._x=h*u*d+c*f*g,this._y=c*f*d-h*u*g,this._z=c*u*g-h*f*d,this._w=c*u*d+h*f*g;break;case"ZXY":this._x=h*u*d-c*f*g,this._y=c*f*d+h*u*g,this._z=c*u*g+h*f*d,this._w=c*u*d-h*f*g;break;case"ZYX":this._x=h*u*d-c*f*g,this._y=c*f*d+h*u*g,this._z=c*u*g-h*f*d,this._w=c*u*d+h*f*g;break;case"YZX":this._x=h*u*d+c*f*g,this._y=c*f*d+h*u*g,this._z=c*u*g-h*f*d,this._w=c*u*d-h*f*g;break;case"XZY":this._x=h*u*d-c*f*g,this._y=c*f*d-h*u*g,this._z=c*u*g+h*f*d,this._w=c*u*d+h*f*g;break;default:Te("Quaternion: .setFromEuler() encountered an unknown order: "+o)}return t===!0&&this._onChangeCallback(),this}setFromAxisAngle(e,t){let n=t/2,s=Math.sin(n);return this._x=e.x*s,this._y=e.y*s,this._z=e.z*s,this._w=Math.cos(n),this._onChangeCallback(),this}setFromRotationMatrix(e){let t=e.elements,n=t[0],s=t[4],r=t[8],o=t[1],a=t[5],l=t[9],c=t[2],u=t[6],d=t[10],h=n+a+d;if(h>0){let f=.5/Math.sqrt(h+1);this._w=.25/f,this._x=(u-l)*f,this._y=(r-c)*f,this._z=(o-s)*f}else if(n>a&&n>d){let f=2*Math.sqrt(1+n-a-d);this._w=(u-l)/f,this._x=.25*f,this._y=(s+o)/f,this._z=(r+c)/f}else if(a>d){let f=2*Math.sqrt(1+a-n-d);this._w=(r-c)/f,this._x=(s+o)/f,this._y=.25*f,this._z=(l+u)/f}else{let f=2*Math.sqrt(1+d-n-a);this._w=(o-s)/f,this._x=(r+c)/f,this._y=(l+u)/f,this._z=.25*f}return this._onChangeCallback(),this}setFromUnitVectors(e,t){let n=e.dot(t)+1;return n<1e-8?(n=0,Math.abs(e.x)>Math.abs(e.z)?(this._x=-e.y,this._y=e.x,this._z=0,this._w=n):(this._x=0,this._y=-e.z,this._z=e.y,this._w=n)):(this._x=e.y*t.z-e.z*t.y,this._y=e.z*t.x-e.x*t.z,this._z=e.x*t.y-e.y*t.x,this._w=n),this.normalize()}angleTo(e){return 2*Math.acos(Math.abs(Ve(this.dot(e),-1,1)))}rotateTowards(e,t){let n=this.angleTo(e);if(n===0)return this;let s=Math.min(1,t/n);return this.slerp(e,s),this}identity(){return this.set(0,0,0,1)}invert(){return this.conjugate()}conjugate(){return this._x*=-1,this._y*=-1,this._z*=-1,this._onChangeCallback(),this}dot(e){return this._x*e._x+this._y*e._y+this._z*e._z+this._w*e._w}lengthSq(){return this._x*this._x+this._y*this._y+this._z*this._z+this._w*this._w}length(){return Math.sqrt(this._x*this._x+this._y*this._y+this._z*this._z+this._w*this._w)}normalize(){let e=this.length();return e===0?(this._x=0,this._y=0,this._z=0,this._w=1):(e=1/e,this._x=this._x*e,this._y=this._y*e,this._z=this._z*e,this._w=this._w*e),this._onChangeCallback(),this}multiply(e){return this.multiplyQuaternions(this,e)}premultiply(e){return this.multiplyQuaternions(e,this)}multiplyQuaternions(e,t){let n=e._x,s=e._y,r=e._z,o=e._w,a=t._x,l=t._y,c=t._z,u=t._w;return this._x=n*u+o*a+s*c-r*l,this._y=s*u+o*l+r*a-n*c,this._z=r*u+o*c+n*l-s*a,this._w=o*u-n*a-s*l-r*c,this._onChangeCallback(),this}slerp(e,t){let n=e._x,s=e._y,r=e._z,o=e._w,a=this.dot(e);a<0&&(n=-n,s=-s,r=-r,o=-o,a=-a);let l=1-t;if(a<.9995){let c=Math.acos(a),u=Math.sin(c);l=Math.sin(l*c)/u,t=Math.sin(t*c)/u,this._x=this._x*l+n*t,this._y=this._y*l+s*t,this._z=this._z*l+r*t,this._w=this._w*l+o*t,this._onChangeCallback()}else this._x=this._x*l+n*t,this._y=this._y*l+s*t,this._z=this._z*l+r*t,this._w=this._w*l+o*t,this.normalize();return this}slerpQuaternions(e,t,n){return this.copy(e).slerp(t,n)}random(){let e=2*Math.PI*Math.random(),t=2*Math.PI*Math.random(),n=Math.random(),s=Math.sqrt(1-n),r=Math.sqrt(n);return this.set(s*Math.sin(e),s*Math.cos(e),r*Math.sin(t),r*Math.cos(t))}equals(e){return e._x===this._x&&e._y===this._y&&e._z===this._z&&e._w===this._w}fromArray(e,t=0){return this._x=e[t],this._y=e[t+1],this._z=e[t+2],this._w=e[t+3],this._onChangeCallback(),this}toArray(e=[],t=0){return e[t]=this._x,e[t+1]=this._y,e[t+2]=this._z,e[t+3]=this._w,e}fromBufferAttribute(e,t){return this._x=e.getX(t),this._y=e.getY(t),this._z=e.getZ(t),this._w=e.getW(t),this._onChangeCallback(),this}toJSON(){return this.toArray()}_onChange(e){return this._onChangeCallback=e,this}_onChangeCallback(){}*[Symbol.iterator](){yield this._x,yield this._y,yield this._z,yield this._w}},rh=class rh{constructor(e=0,t=0,n=0){this.x=e,this.y=t,this.z=n}set(e,t,n){return n===void 0&&(n=this.z),this.x=e,this.y=t,this.z=n,this}setScalar(e){return this.x=e,this.y=e,this.z=e,this}setX(e){return this.x=e,this}setY(e){return this.y=e,this}setZ(e){return this.z=e,this}setComponent(e,t){switch(e){case 0:this.x=t;break;case 1:this.y=t;break;case 2:this.z=t;break;default:throw new Error("index is out of range: "+e)}return this}getComponent(e){switch(e){case 0:return this.x;case 1:return this.y;case 2:return this.z;default:throw new Error("index is out of range: "+e)}}clone(){return new this.constructor(this.x,this.y,this.z)}copy(e){return this.x=e.x,this.y=e.y,this.z=e.z,this}add(e){return this.x+=e.x,this.y+=e.y,this.z+=e.z,this}addScalar(e){return this.x+=e,this.y+=e,this.z+=e,this}addVectors(e,t){return this.x=e.x+t.x,this.y=e.y+t.y,this.z=e.z+t.z,this}addScaledVector(e,t){return this.x+=e.x*t,this.y+=e.y*t,this.z+=e.z*t,this}sub(e){return this.x-=e.x,this.y-=e.y,this.z-=e.z,this}subScalar(e){return this.x-=e,this.y-=e,this.z-=e,this}subVectors(e,t){return this.x=e.x-t.x,this.y=e.y-t.y,this.z=e.z-t.z,this}multiply(e){return this.x*=e.x,this.y*=e.y,this.z*=e.z,this}multiplyScalar(e){return this.x*=e,this.y*=e,this.z*=e,this}multiplyVectors(e,t){return this.x=e.x*t.x,this.y=e.y*t.y,this.z=e.z*t.z,this}applyEuler(e){return this.applyQuaternion(xu.setFromEuler(e))}applyAxisAngle(e,t){return this.applyQuaternion(xu.setFromAxisAngle(e,t))}applyMatrix3(e){let t=this.x,n=this.y,s=this.z,r=e.elements;return this.x=r[0]*t+r[3]*n+r[6]*s,this.y=r[1]*t+r[4]*n+r[7]*s,this.z=r[2]*t+r[5]*n+r[8]*s,this}applyNormalMatrix(e){return this.applyMatrix3(e).normalize()}applyMatrix4(e){let t=this.x,n=this.y,s=this.z,r=e.elements,o=1/(r[3]*t+r[7]*n+r[11]*s+r[15]);return this.x=(r[0]*t+r[4]*n+r[8]*s+r[12])*o,this.y=(r[1]*t+r[5]*n+r[9]*s+r[13])*o,this.z=(r[2]*t+r[6]*n+r[10]*s+r[14])*o,this}applyQuaternion(e){let t=this.x,n=this.y,s=this.z,r=e.x,o=e.y,a=e.z,l=e.w,c=2*(o*s-a*n),u=2*(a*t-r*s),d=2*(r*n-o*t);return this.x=t+l*c+o*d-a*u,this.y=n+l*u+a*c-r*d,this.z=s+l*d+r*u-o*c,this}project(e){return this.applyMatrix4(e.matrixWorldInverse).applyMatrix4(e.projectionMatrix)}unproject(e){return this.applyMatrix4(e.projectionMatrixInverse).applyMatrix4(e.matrixWorld)}transformDirection(e){let t=this.x,n=this.y,s=this.z,r=e.elements;return this.x=r[0]*t+r[4]*n+r[8]*s,this.y=r[1]*t+r[5]*n+r[9]*s,this.z=r[2]*t+r[6]*n+r[10]*s,this.normalize()}divide(e){return this.x/=e.x,this.y/=e.y,this.z/=e.z,this}divideScalar(e){return this.multiplyScalar(1/e)}min(e){return this.x=Math.min(this.x,e.x),this.y=Math.min(this.y,e.y),this.z=Math.min(this.z,e.z),this}max(e){return this.x=Math.max(this.x,e.x),this.y=Math.max(this.y,e.y),this.z=Math.max(this.z,e.z),this}clamp(e,t){return this.x=Ve(this.x,e.x,t.x),this.y=Ve(this.y,e.y,t.y),this.z=Ve(this.z,e.z,t.z),this}clampScalar(e,t){return this.x=Ve(this.x,e,t),this.y=Ve(this.y,e,t),this.z=Ve(this.z,e,t),this}clampLength(e,t){let n=this.length();return this.divideScalar(n||1).multiplyScalar(Ve(n,e,t))}floor(){return this.x=Math.floor(this.x),this.y=Math.floor(this.y),this.z=Math.floor(this.z),this}ceil(){return this.x=Math.ceil(this.x),this.y=Math.ceil(this.y),this.z=Math.ceil(this.z),this}round(){return this.x=Math.round(this.x),this.y=Math.round(this.y),this.z=Math.round(this.z),this}roundToZero(){return this.x=Math.trunc(this.x),this.y=Math.trunc(this.y),this.z=Math.trunc(this.z),this}negate(){return this.x=-this.x,this.y=-this.y,this.z=-this.z,this}dot(e){return this.x*e.x+this.y*e.y+this.z*e.z}lengthSq(){return this.x*this.x+this.y*this.y+this.z*this.z}length(){return Math.sqrt(this.x*this.x+this.y*this.y+this.z*this.z)}manhattanLength(){return Math.abs(this.x)+Math.abs(this.y)+Math.abs(this.z)}normalize(){return this.divideScalar(this.length()||1)}setLength(e){return this.normalize().multiplyScalar(e)}lerp(e,t){return this.x+=(e.x-this.x)*t,this.y+=(e.y-this.y)*t,this.z+=(e.z-this.z)*t,this}lerpVectors(e,t,n){return this.x=e.x+(t.x-e.x)*n,this.y=e.y+(t.y-e.y)*n,this.z=e.z+(t.z-e.z)*n,this}cross(e){return this.crossVectors(this,e)}crossVectors(e,t){let n=e.x,s=e.y,r=e.z,o=t.x,a=t.y,l=t.z;return this.x=s*l-r*a,this.y=r*o-n*l,this.z=n*a-s*o,this}projectOnVector(e){let t=e.lengthSq();if(t===0)return this.set(0,0,0);let n=e.dot(this)/t;return this.copy(e).multiplyScalar(n)}projectOnPlane(e){return oc.copy(this).projectOnVector(e),this.sub(oc)}reflect(e){return this.sub(oc.copy(e).multiplyScalar(2*this.dot(e)))}angleTo(e){let t=Math.sqrt(this.lengthSq()*e.lengthSq());if(t===0)return Math.PI/2;let n=this.dot(e)/t;return Math.acos(Ve(n,-1,1))}distanceTo(e){return Math.sqrt(this.distanceToSquared(e))}distanceToSquared(e){let t=this.x-e.x,n=this.y-e.y,s=this.z-e.z;return t*t+n*n+s*s}manhattanDistanceTo(e){return Math.abs(this.x-e.x)+Math.abs(this.y-e.y)+Math.abs(this.z-e.z)}setFromSpherical(e){return this.setFromSphericalCoords(e.radius,e.phi,e.theta)}setFromSphericalCoords(e,t,n){let s=Math.sin(t)*e;return this.x=s*Math.sin(n),this.y=Math.cos(t)*e,this.z=s*Math.cos(n),this}setFromCylindrical(e){return this.setFromCylindricalCoords(e.radius,e.theta,e.y)}setFromCylindricalCoords(e,t,n){return this.x=e*Math.sin(t),this.y=n,this.z=e*Math.cos(t),this}setFromMatrixPosition(e){let t=e.elements;return this.x=t[12],this.y=t[13],this.z=t[14],this}setFromMatrixScale(e){let t=this.setFromMatrixColumn(e,0).length(),n=this.setFromMatrixColumn(e,1).length(),s=this.setFromMatrixColumn(e,2).length();return this.x=t,this.y=n,this.z=s,this}setFromMatrixColumn(e,t){return this.fromArray(e.elements,t*4)}setFromMatrix3Column(e,t){return this.fromArray(e.elements,t*3)}setFromEuler(e){return this.x=e._x,this.y=e._y,this.z=e._z,this}setFromColor(e){return this.x=e.r,this.y=e.g,this.z=e.b,this}equals(e){return e.x===this.x&&e.y===this.y&&e.z===this.z}fromArray(e,t=0){return this.x=e[t],this.y=e[t+1],this.z=e[t+2],this}toArray(e=[],t=0){return e[t]=this.x,e[t+1]=this.y,e[t+2]=this.z,e}fromBufferAttribute(e,t){return this.x=e.getX(t),this.y=e.getY(t),this.z=e.getZ(t),this}random(){return this.x=Math.random(),this.y=Math.random(),this.z=Math.random(),this}randomDirection(){let e=Math.random()*Math.PI*2,t=Math.random()*2-1,n=Math.sqrt(1-t*t);return this.x=n*Math.cos(e),this.y=t,this.z=n*Math.sin(e),this}*[Symbol.iterator](){yield this.x,yield this.y,yield this.z}};rh.prototype.isVector3=!0;var N=rh,oc=new N,xu=new hn,oh=class oh{constructor(e,t,n,s,r,o,a,l,c){this.elements=[1,0,0,0,1,0,0,0,1],e!==void 0&&this.set(e,t,n,s,r,o,a,l,c)}set(e,t,n,s,r,o,a,l,c){let u=this.elements;return u[0]=e,u[1]=s,u[2]=a,u[3]=t,u[4]=r,u[5]=l,u[6]=n,u[7]=o,u[8]=c,this}identity(){return this.set(1,0,0,0,1,0,0,0,1),this}copy(e){let t=this.elements,n=e.elements;return t[0]=n[0],t[1]=n[1],t[2]=n[2],t[3]=n[3],t[4]=n[4],t[5]=n[5],t[6]=n[6],t[7]=n[7],t[8]=n[8],this}extractBasis(e,t,n){return e.setFromMatrix3Column(this,0),t.setFromMatrix3Column(this,1),n.setFromMatrix3Column(this,2),this}setFromMatrix4(e){let t=e.elements;return this.set(t[0],t[4],t[8],t[1],t[5],t[9],t[2],t[6],t[10]),this}multiply(e){return this.multiplyMatrices(this,e)}premultiply(e){return this.multiplyMatrices(e,this)}multiplyMatrices(e,t){let n=e.elements,s=t.elements,r=this.elements,o=n[0],a=n[3],l=n[6],c=n[1],u=n[4],d=n[7],h=n[2],f=n[5],g=n[8],x=s[0],m=s[3],p=s[6],v=s[1],b=s[4],M=s[7],E=s[2],w=s[5],C=s[8];return r[0]=o*x+a*v+l*E,r[3]=o*m+a*b+l*w,r[6]=o*p+a*M+l*C,r[1]=c*x+u*v+d*E,r[4]=c*m+u*b+d*w,r[7]=c*p+u*M+d*C,r[2]=h*x+f*v+g*E,r[5]=h*m+f*b+g*w,r[8]=h*p+f*M+g*C,this}multiplyScalar(e){let t=this.elements;return t[0]*=e,t[3]*=e,t[6]*=e,t[1]*=e,t[4]*=e,t[7]*=e,t[2]*=e,t[5]*=e,t[8]*=e,this}determinant(){let e=this.elements,t=e[0],n=e[1],s=e[2],r=e[3],o=e[4],a=e[5],l=e[6],c=e[7],u=e[8];return t*o*u-t*a*c-n*r*u+n*a*l+s*r*c-s*o*l}invert(){let e=this.elements,t=e[0],n=e[1],s=e[2],r=e[3],o=e[4],a=e[5],l=e[6],c=e[7],u=e[8],d=u*o-a*c,h=a*l-u*r,f=c*r-o*l,g=t*d+n*h+s*f;if(g===0)return this.set(0,0,0,0,0,0,0,0,0);let x=1/g;return e[0]=d*x,e[1]=(s*c-u*n)*x,e[2]=(a*n-s*o)*x,e[3]=h*x,e[4]=(u*t-s*l)*x,e[5]=(s*r-a*t)*x,e[6]=f*x,e[7]=(n*l-c*t)*x,e[8]=(o*t-n*r)*x,this}transpose(){let e,t=this.elements;return e=t[1],t[1]=t[3],t[3]=e,e=t[2],t[2]=t[6],t[6]=e,e=t[5],t[5]=t[7],t[7]=e,this}getNormalMatrix(e){return this.setFromMatrix4(e).invert().transpose()}transposeIntoArray(e){let t=this.elements;return e[0]=t[0],e[1]=t[3],e[2]=t[6],e[3]=t[1],e[4]=t[4],e[5]=t[7],e[6]=t[2],e[7]=t[5],e[8]=t[8],this}setUvTransform(e,t,n,s,r,o,a){let l=Math.cos(r),c=Math.sin(r);return this.set(n*l,n*c,-n*(l*o+c*a)+o+e,-s*c,s*l,-s*(-c*o+l*a)+a+t,0,0,1),this}scale(e,t){return this.premultiply(ac.makeScale(e,t)),this}rotate(e){return this.premultiply(ac.makeRotation(-e)),this}translate(e,t){return this.premultiply(ac.makeTranslation(e,t)),this}makeTranslation(e,t){return e.isVector2?this.set(1,0,e.x,0,1,e.y,0,0,1):this.set(1,0,e,0,1,t,0,0,1),this}makeRotation(e){let t=Math.cos(e),n=Math.sin(e);return this.set(t,-n,0,n,t,0,0,0,1),this}makeScale(e,t){return this.set(e,0,0,0,t,0,0,0,1),this}equals(e){let t=this.elements,n=e.elements;for(let s=0;s<9;s++)if(t[s]!==n[s])return!1;return!0}fromArray(e,t=0){for(let n=0;n<9;n++)this.elements[n]=e[n+t];return this}toArray(e=[],t=0){let n=this.elements;return e[t]=n[0],e[t+1]=n[1],e[t+2]=n[2],e[t+3]=n[3],e[t+4]=n[4],e[t+5]=n[5],e[t+6]=n[6],e[t+7]=n[7],e[t+8]=n[8],e}clone(){return new this.constructor().fromArray(this.elements)}};oh.prototype.isMatrix3=!0;var Le=oh,ac=new Le,vu=new Le().set(.4123908,.3575843,.1804808,.212639,.7151687,.0721923,.0193308,.1191948,.9505322),_u=new Le().set(3.2409699,-1.5373832,-.4986108,-.9692436,1.8759675,.0415551,.0556301,-.203977,1.0569715);function Vm(){let i={enabled:!0,workingColorSpace:nr,spaces:{},convert:function(s,r,o){return this.enabled===!1||r===o||!r||!o||(this.spaces[r].transfer===Je&&(s.r=jn(s.r),s.g=jn(s.g),s.b=jn(s.b)),this.spaces[r].primaries!==this.spaces[o].primaries&&(s.applyMatrix3(this.spaces[r].toXYZ),s.applyMatrix3(this.spaces[o].fromXYZ)),this.spaces[o].transfer===Je&&(s.r=bs(s.r),s.g=bs(s.g),s.b=bs(s.b))),s},workingToColorSpace:function(s,r){return this.convert(s,this.workingColorSpace,r)},colorSpaceToWorking:function(s,r){return this.convert(s,r,this.workingColorSpace)},getPrimaries:function(s){return this.spaces[s].primaries},getTransfer:function(s){return s===ri?ir:this.spaces[s].transfer},getToneMappingMode:function(s){return this.spaces[s].outputColorSpaceConfig.toneMappingMode||"standard"},getLuminanceCoefficients:function(s,r=this.workingColorSpace){return s.fromArray(this.spaces[r].luminanceCoefficients)},define:function(s){Object.assign(this.spaces,s)},_getMatrix:function(s,r,o){return s.copy(this.spaces[r].toXYZ).multiply(this.spaces[o].fromXYZ)},_getDrawingBufferColorSpace:function(s){return this.spaces[s].outputColorSpaceConfig.drawingBufferColorSpace},_getUnpackColorSpace:function(s=this.workingColorSpace){return this.spaces[s].workingColorSpaceConfig.unpackColorSpace},fromWorkingColorSpace:function(s,r){return Go("ColorManagement: .fromWorkingColorSpace() has been renamed to .workingToColorSpace()."),i.workingToColorSpace(s,r)},toWorkingColorSpace:function(s,r){return Go("ColorManagement: .toWorkingColorSpace() has been renamed to .colorSpaceToWorking()."),i.colorSpaceToWorking(s,r)}},e=[.64,.33,.3,.6,.15,.06],t=[.2126,.7152,.0722],n=[.3127,.329];return i.define({[nr]:{primaries:e,whitePoint:n,transfer:ir,toXYZ:vu,fromXYZ:_u,luminanceCoefficients:t,workingColorSpaceConfig:{unpackColorSpace:cn},outputColorSpaceConfig:{drawingBufferColorSpace:cn}},[cn]:{primaries:e,whitePoint:n,transfer:Je,toXYZ:vu,fromXYZ:_u,luminanceCoefficients:t,outputColorSpaceConfig:{drawingBufferColorSpace:cn}}}),i}var ze=Vm();function jn(i){return i<.04045?i*.0773993808:Math.pow(i*.9478672986+.0521327014,2.4)}function bs(i){return i<.0031308?i*12.92:1.055*Math.pow(i,.41666)-.055}var ls,Ho=class{static getDataURL(e,t="image/png"){if(/^data:/i.test(e.src)||typeof HTMLCanvasElement>"u")return e.src;let n;if(e instanceof HTMLCanvasElement)n=e;else{ls===void 0&&(ls=rr("canvas")),ls.width=e.width,ls.height=e.height;let s=ls.getContext("2d");e instanceof ImageData?s.putImageData(e,0,0):s.drawImage(e,0,0,e.width,e.height),n=ls}return n.toDataURL(t)}static sRGBToLinear(e){if(typeof HTMLImageElement<"u"&&e instanceof HTMLImageElement||typeof HTMLCanvasElement<"u"&&e instanceof HTMLCanvasElement||typeof ImageBitmap<"u"&&e instanceof ImageBitmap){let t=rr("canvas");t.width=e.width,t.height=e.height;let n=t.getContext("2d");n.drawImage(e,0,0,e.width,e.height);let s=n.getImageData(0,0,e.width,e.height),r=s.data;for(let o=0;o1),this.pmremVersion=0,this.normalized=!1}get width(){return this.source.getSize(cc).x}get height(){return this.source.getSize(cc).y}get depth(){return this.source.getSize(cc).z}get image(){return this.source.data}set image(e){this.source.data=e}updateMatrix(){this.matrix.setUvTransform(this.offset.x,this.offset.y,this.repeat.x,this.repeat.y,this.rotation,this.center.x,this.center.y)}addUpdateRange(e,t){this.updateRanges.push({start:e,count:t})}clearUpdateRanges(){this.updateRanges.length=0}clone(){return new this.constructor().copy(this)}copy(e){return this.name=e.name,this.source=e.source,this.mipmaps=e.mipmaps.slice(0),this.mapping=e.mapping,this.channel=e.channel,this.wrapS=e.wrapS,this.wrapT=e.wrapT,this.magFilter=e.magFilter,this.minFilter=e.minFilter,this.anisotropy=e.anisotropy,this.format=e.format,this.internalFormat=e.internalFormat,this.type=e.type,this.normalized=e.normalized,this.offset.copy(e.offset),this.repeat.copy(e.repeat),this.center.copy(e.center),this.rotation=e.rotation,this.matrixAutoUpdate=e.matrixAutoUpdate,this.matrix.copy(e.matrix),this.generateMipmaps=e.generateMipmaps,this.premultiplyAlpha=e.premultiplyAlpha,this.flipY=e.flipY,this.unpackAlignment=e.unpackAlignment,this.colorSpace=e.colorSpace,this.renderTarget=e.renderTarget,this.isRenderTargetTexture=e.isRenderTargetTexture,this.isArrayTexture=e.isArrayTexture,this.userData=JSON.parse(JSON.stringify(e.userData)),this.needsUpdate=!0,this}setValues(e){for(let t in e){let n=e[t];if(n===void 0){Te(`Texture.setValues(): parameter '${t}' has value of undefined.`);continue}let s=this[t];if(s===void 0){Te(`Texture.setValues(): property '${t}' does not exist.`);continue}s&&n&&s.isVector2&&n.isVector2||s&&n&&s.isVector3&&n.isVector3||s&&n&&s.isMatrix3&&n.isMatrix3?s.copy(n):this[t]=n}}toJSON(e){let t=e===void 0||typeof e=="string";if(!t&&e.textures[this.uuid]!==void 0)return e.textures[this.uuid];let n={metadata:{version:4.7,type:"Texture",generator:"Texture.toJSON"},uuid:this.uuid,name:this.name,image:this.source.toJSON(e).uuid,mapping:this.mapping,channel:this.channel,repeat:[this.repeat.x,this.repeat.y],offset:[this.offset.x,this.offset.y],center:[this.center.x,this.center.y],rotation:this.rotation,wrap:[this.wrapS,this.wrapT],format:this.format,internalFormat:this.internalFormat,type:this.type,normalized:this.normalized,colorSpace:this.colorSpace,minFilter:this.minFilter,magFilter:this.magFilter,anisotropy:this.anisotropy,flipY:this.flipY,generateMipmaps:this.generateMipmaps,premultiplyAlpha:this.premultiplyAlpha,unpackAlignment:this.unpackAlignment};return Object.keys(this.userData).length>0&&(n.userData=this.userData),t||(e.textures[this.uuid]=n),n}dispose(){this.dispatchEvent({type:"dispose"})}transformUv(e){if(this.mapping!==Vc)return e;if(e.applyMatrix3(this.matrix),e.x<0||e.x>1)switch(this.wrapS){case Bo:e.x=e.x-Math.floor(e.x);break;case On:e.x=e.x<0?0:1;break;case zo:Math.abs(Math.floor(e.x)%2)===1?e.x=Math.ceil(e.x)-e.x:e.x=e.x-Math.floor(e.x);break}if(e.y<0||e.y>1)switch(this.wrapT){case Bo:e.y=e.y-Math.floor(e.y);break;case On:e.y=e.y<0?0:1;break;case zo:Math.abs(Math.floor(e.y)%2)===1?e.y=Math.ceil(e.y)-e.y:e.y=e.y-Math.floor(e.y);break}return this.flipY&&(e.y=1-e.y),e}set needsUpdate(e){e===!0&&(this.version++,this.source.needsUpdate=!0)}set needsPMREMUpdate(e){e===!0&&this.pmremVersion++}};jt.DEFAULT_IMAGE=null;jt.DEFAULT_MAPPING=Vc;jt.DEFAULT_ANISOTROPY=1;var ah=class ah{constructor(e=0,t=0,n=0,s=1){this.x=e,this.y=t,this.z=n,this.w=s}get width(){return this.z}set width(e){this.z=e}get height(){return this.w}set height(e){this.w=e}set(e,t,n,s){return this.x=e,this.y=t,this.z=n,this.w=s,this}setScalar(e){return this.x=e,this.y=e,this.z=e,this.w=e,this}setX(e){return this.x=e,this}setY(e){return this.y=e,this}setZ(e){return this.z=e,this}setW(e){return this.w=e,this}setComponent(e,t){switch(e){case 0:this.x=t;break;case 1:this.y=t;break;case 2:this.z=t;break;case 3:this.w=t;break;default:throw new Error("index is out of range: "+e)}return this}getComponent(e){switch(e){case 0:return this.x;case 1:return this.y;case 2:return this.z;case 3:return this.w;default:throw new Error("index is out of range: "+e)}}clone(){return new this.constructor(this.x,this.y,this.z,this.w)}copy(e){return this.x=e.x,this.y=e.y,this.z=e.z,this.w=e.w!==void 0?e.w:1,this}add(e){return this.x+=e.x,this.y+=e.y,this.z+=e.z,this.w+=e.w,this}addScalar(e){return this.x+=e,this.y+=e,this.z+=e,this.w+=e,this}addVectors(e,t){return this.x=e.x+t.x,this.y=e.y+t.y,this.z=e.z+t.z,this.w=e.w+t.w,this}addScaledVector(e,t){return this.x+=e.x*t,this.y+=e.y*t,this.z+=e.z*t,this.w+=e.w*t,this}sub(e){return this.x-=e.x,this.y-=e.y,this.z-=e.z,this.w-=e.w,this}subScalar(e){return this.x-=e,this.y-=e,this.z-=e,this.w-=e,this}subVectors(e,t){return this.x=e.x-t.x,this.y=e.y-t.y,this.z=e.z-t.z,this.w=e.w-t.w,this}multiply(e){return this.x*=e.x,this.y*=e.y,this.z*=e.z,this.w*=e.w,this}multiplyScalar(e){return this.x*=e,this.y*=e,this.z*=e,this.w*=e,this}applyMatrix4(e){let t=this.x,n=this.y,s=this.z,r=this.w,o=e.elements;return this.x=o[0]*t+o[4]*n+o[8]*s+o[12]*r,this.y=o[1]*t+o[5]*n+o[9]*s+o[13]*r,this.z=o[2]*t+o[6]*n+o[10]*s+o[14]*r,this.w=o[3]*t+o[7]*n+o[11]*s+o[15]*r,this}divide(e){return this.x/=e.x,this.y/=e.y,this.z/=e.z,this.w/=e.w,this}divideScalar(e){return this.multiplyScalar(1/e)}setAxisAngleFromQuaternion(e){this.w=2*Math.acos(e.w);let t=Math.sqrt(1-e.w*e.w);return t<1e-4?(this.x=1,this.y=0,this.z=0):(this.x=e.x/t,this.y=e.y/t,this.z=e.z/t),this}setAxisAngleFromRotationMatrix(e){let t,n,s,r,l=e.elements,c=l[0],u=l[4],d=l[8],h=l[1],f=l[5],g=l[9],x=l[2],m=l[6],p=l[10];if(Math.abs(u-h)<.01&&Math.abs(d-x)<.01&&Math.abs(g-m)<.01){if(Math.abs(u+h)<.1&&Math.abs(d+x)<.1&&Math.abs(g+m)<.1&&Math.abs(c+f+p-3)<.1)return this.set(1,0,0,0),this;t=Math.PI;let b=(c+1)/2,M=(f+1)/2,E=(p+1)/2,w=(u+h)/4,C=(d+x)/4,_=(g+m)/4;return b>M&&b>E?b<.01?(n=0,s=.707106781,r=.707106781):(n=Math.sqrt(b),s=w/n,r=C/n):M>E?M<.01?(n=.707106781,s=0,r=.707106781):(s=Math.sqrt(M),n=w/s,r=_/s):E<.01?(n=.707106781,s=.707106781,r=0):(r=Math.sqrt(E),n=C/r,s=_/r),this.set(n,s,r,t),this}let v=Math.sqrt((m-g)*(m-g)+(d-x)*(d-x)+(h-u)*(h-u));return Math.abs(v)<.001&&(v=1),this.x=(m-g)/v,this.y=(d-x)/v,this.z=(h-u)/v,this.w=Math.acos((c+f+p-1)/2),this}setFromMatrixPosition(e){let t=e.elements;return this.x=t[12],this.y=t[13],this.z=t[14],this.w=t[15],this}min(e){return this.x=Math.min(this.x,e.x),this.y=Math.min(this.y,e.y),this.z=Math.min(this.z,e.z),this.w=Math.min(this.w,e.w),this}max(e){return this.x=Math.max(this.x,e.x),this.y=Math.max(this.y,e.y),this.z=Math.max(this.z,e.z),this.w=Math.max(this.w,e.w),this}clamp(e,t){return this.x=Ve(this.x,e.x,t.x),this.y=Ve(this.y,e.y,t.y),this.z=Ve(this.z,e.z,t.z),this.w=Ve(this.w,e.w,t.w),this}clampScalar(e,t){return this.x=Ve(this.x,e,t),this.y=Ve(this.y,e,t),this.z=Ve(this.z,e,t),this.w=Ve(this.w,e,t),this}clampLength(e,t){let n=this.length();return this.divideScalar(n||1).multiplyScalar(Ve(n,e,t))}floor(){return this.x=Math.floor(this.x),this.y=Math.floor(this.y),this.z=Math.floor(this.z),this.w=Math.floor(this.w),this}ceil(){return this.x=Math.ceil(this.x),this.y=Math.ceil(this.y),this.z=Math.ceil(this.z),this.w=Math.ceil(this.w),this}round(){return this.x=Math.round(this.x),this.y=Math.round(this.y),this.z=Math.round(this.z),this.w=Math.round(this.w),this}roundToZero(){return this.x=Math.trunc(this.x),this.y=Math.trunc(this.y),this.z=Math.trunc(this.z),this.w=Math.trunc(this.w),this}negate(){return this.x=-this.x,this.y=-this.y,this.z=-this.z,this.w=-this.w,this}dot(e){return this.x*e.x+this.y*e.y+this.z*e.z+this.w*e.w}lengthSq(){return this.x*this.x+this.y*this.y+this.z*this.z+this.w*this.w}length(){return Math.sqrt(this.x*this.x+this.y*this.y+this.z*this.z+this.w*this.w)}manhattanLength(){return Math.abs(this.x)+Math.abs(this.y)+Math.abs(this.z)+Math.abs(this.w)}normalize(){return this.divideScalar(this.length()||1)}setLength(e){return this.normalize().multiplyScalar(e)}lerp(e,t){return this.x+=(e.x-this.x)*t,this.y+=(e.y-this.y)*t,this.z+=(e.z-this.z)*t,this.w+=(e.w-this.w)*t,this}lerpVectors(e,t,n){return this.x=e.x+(t.x-e.x)*n,this.y=e.y+(t.y-e.y)*n,this.z=e.z+(t.z-e.z)*n,this.w=e.w+(t.w-e.w)*n,this}equals(e){return e.x===this.x&&e.y===this.y&&e.z===this.z&&e.w===this.w}fromArray(e,t=0){return this.x=e[t],this.y=e[t+1],this.z=e[t+2],this.w=e[t+3],this}toArray(e=[],t=0){return e[t]=this.x,e[t+1]=this.y,e[t+2]=this.z,e[t+3]=this.w,e}fromBufferAttribute(e,t){return this.x=e.getX(t),this.y=e.getY(t),this.z=e.getZ(t),this.w=e.getW(t),this}random(){return this.x=Math.random(),this.y=Math.random(),this.z=Math.random(),this.w=Math.random(),this}*[Symbol.iterator](){yield this.x,yield this.y,yield this.z,yield this.w}};ah.prototype.isVector4=!0;var xt=ah,Wo=class extends An{constructor(e=1,t=1,n={}){super(),n=Object.assign({generateMipmaps:!1,internalFormat:null,minFilter:Bt,depthBuffer:!0,stencilBuffer:!1,resolveDepthBuffer:!0,resolveStencilBuffer:!0,depthTexture:null,samples:0,count:1,depth:1,multiview:!1},n),this.isRenderTarget=!0,this.width=e,this.height=t,this.depth=n.depth,this.scissor=new xt(0,0,e,t),this.scissorTest=!1,this.viewport=new xt(0,0,e,t),this.textures=[];let s={width:e,height:t,depth:n.depth},r=new jt(s),o=n.count;for(let a=0;a1);this.dispose()}this.viewport.set(0,0,e,t),this.scissor.set(0,0,e,t)}clone(){return new this.constructor().copy(this)}copy(e){this.width=e.width,this.height=e.height,this.depth=e.depth,this.scissor.copy(e.scissor),this.scissorTest=e.scissorTest,this.viewport.copy(e.viewport),this.textures.length=0;for(let t=0,n=e.textures.length;t>>0}enable(e){this.mask|=1<1){for(let t=0;t1){for(let n=0;n0&&(s.userData=this.userData),s.layers=this.layers.mask,s.matrix=this.matrix.toArray(),s.up=this.up.toArray(),this.pivot!==null&&(s.pivot=this.pivot.toArray()),this.matrixAutoUpdate===!1&&(s.matrixAutoUpdate=!1),this.morphTargetDictionary!==void 0&&(s.morphTargetDictionary=Object.assign({},this.morphTargetDictionary)),this.morphTargetInfluences!==void 0&&(s.morphTargetInfluences=this.morphTargetInfluences.slice()),this.isInstancedMesh&&(s.type="InstancedMesh",s.count=this.count,s.instanceMatrix=this.instanceMatrix.toJSON(),this.instanceColor!==null&&(s.instanceColor=this.instanceColor.toJSON())),this.isBatchedMesh&&(s.type="BatchedMesh",s.perObjectFrustumCulled=this.perObjectFrustumCulled,s.sortObjects=this.sortObjects,s.drawRanges=this._drawRanges,s.reservedRanges=this._reservedRanges,s.geometryInfo=this._geometryInfo.map(a=>({...a,boundingBox:a.boundingBox?a.boundingBox.toJSON():void 0,boundingSphere:a.boundingSphere?a.boundingSphere.toJSON():void 0})),s.instanceInfo=this._instanceInfo.map(a=>({...a})),s.availableInstanceIds=this._availableInstanceIds.slice(),s.availableGeometryIds=this._availableGeometryIds.slice(),s.nextIndexStart=this._nextIndexStart,s.nextVertexStart=this._nextVertexStart,s.geometryCount=this._geometryCount,s.maxInstanceCount=this._maxInstanceCount,s.maxVertexCount=this._maxVertexCount,s.maxIndexCount=this._maxIndexCount,s.geometryInitialized=this._geometryInitialized,s.matricesTexture=this._matricesTexture.toJSON(e),s.indirectTexture=this._indirectTexture.toJSON(e),this._colorsTexture!==null&&(s.colorsTexture=this._colorsTexture.toJSON(e)),this.boundingSphere!==null&&(s.boundingSphere=this.boundingSphere.toJSON()),this.boundingBox!==null&&(s.boundingBox=this.boundingBox.toJSON()));function r(a,l){return a[l.uuid]===void 0&&(a[l.uuid]=l.toJSON(e)),l.uuid}if(this.isScene)this.background&&(this.background.isColor?s.background=this.background.toJSON():this.background.isTexture&&(s.background=this.background.toJSON(e).uuid)),this.environment&&this.environment.isTexture&&this.environment.isRenderTargetTexture!==!0&&(s.environment=this.environment.toJSON(e).uuid);else if(this.isMesh||this.isLine||this.isPoints){s.geometry=r(e.geometries,this.geometry);let a=this.geometry.parameters;if(a!==void 0&&a.shapes!==void 0){let l=a.shapes;if(Array.isArray(l))for(let c=0,u=l.length;c0){s.children=[];for(let a=0;a0){s.animations=[];for(let a=0;a0&&(n.geometries=a),l.length>0&&(n.materials=l),c.length>0&&(n.textures=c),u.length>0&&(n.images=u),d.length>0&&(n.shapes=d),h.length>0&&(n.skeletons=h),f.length>0&&(n.animations=f),g.length>0&&(n.nodes=g)}return n.object=s,n;function o(a){let l=[];for(let c in a){let u=a[c];delete u.metadata,l.push(u)}return l}}clone(e){return new this.constructor().copy(this,e)}copy(e,t=!0){if(this.name=e.name,this.up.copy(e.up),this.position.copy(e.position),this.rotation.order=e.rotation.order,this.quaternion.copy(e.quaternion),this.scale.copy(e.scale),this.pivot=e.pivot!==null?e.pivot.clone():null,this.matrix.copy(e.matrix),this.matrixWorld.copy(e.matrixWorld),this.matrixAutoUpdate=e.matrixAutoUpdate,this.matrixWorldAutoUpdate=e.matrixWorldAutoUpdate,this.matrixWorldNeedsUpdate=e.matrixWorldNeedsUpdate,this.layers.mask=e.layers.mask,this.visible=e.visible,this.castShadow=e.castShadow,this.receiveShadow=e.receiveShadow,this.frustumCulled=e.frustumCulled,this.renderOrder=e.renderOrder,this.static=e.static,this.animations=e.animations.slice(),this.userData=JSON.parse(JSON.stringify(e.userData)),t===!0)for(let n=0;nf+g?(c.inputState.pinching=!1,this.dispatchEvent({type:"pinchend",handedness:e.handedness,target:this})):!c.inputState.pinching&&h<=f-g&&(c.inputState.pinching=!0,this.dispatchEvent({type:"pinchstart",handedness:e.handedness,target:this}))}else l!==null&&e.gripSpace&&(r=t.getPose(e.gripSpace,n),r!==null&&(l.matrix.fromArray(r.transform.matrix),l.matrix.decompose(l.position,l.rotation,l.scale),l.matrixWorldNeedsUpdate=!0,r.linearVelocity?(l.hasLinearVelocity=!0,l.linearVelocity.copy(r.linearVelocity)):l.hasLinearVelocity=!1,r.angularVelocity?(l.hasAngularVelocity=!0,l.angularVelocity.copy(r.angularVelocity)):l.hasAngularVelocity=!1,l.eventsEnabled&&l.dispatchEvent({type:"gripUpdated",data:e,target:this})));a!==null&&(s=t.getPose(e.targetRaySpace,n),s===null&&r!==null&&(s=r),s!==null&&(a.matrix.fromArray(s.transform.matrix),a.matrix.decompose(a.position,a.rotation,a.scale),a.matrixWorldNeedsUpdate=!0,s.linearVelocity?(a.hasLinearVelocity=!0,a.linearVelocity.copy(s.linearVelocity)):a.hasLinearVelocity=!1,s.angularVelocity?(a.hasAngularVelocity=!0,a.angularVelocity.copy(s.angularVelocity)):a.hasAngularVelocity=!1,this.dispatchEvent(Km)))}return a!==null&&(a.visible=s!==null),l!==null&&(l.visible=r!==null),c!==null&&(c.visible=o!==null),this}_getHandJoint(e,t){if(e.joints[t.jointName]===void 0){let n=new Jn;n.matrixAutoUpdate=!1,n.visible=!1,e.joints[t.jointName]=n,e.add(n)}return e.joints[t.jointName]}},Md={aliceblue:15792383,antiquewhite:16444375,aqua:65535,aquamarine:8388564,azure:15794175,beige:16119260,bisque:16770244,black:0,blanchedalmond:16772045,blue:255,blueviolet:9055202,brown:10824234,burlywood:14596231,cadetblue:6266528,chartreuse:8388352,chocolate:13789470,coral:16744272,cornflowerblue:6591981,cornsilk:16775388,crimson:14423100,cyan:65535,darkblue:139,darkcyan:35723,darkgoldenrod:12092939,darkgray:11119017,darkgreen:25600,darkgrey:11119017,darkkhaki:12433259,darkmagenta:9109643,darkolivegreen:5597999,darkorange:16747520,darkorchid:10040012,darkred:9109504,darksalmon:15308410,darkseagreen:9419919,darkslateblue:4734347,darkslategray:3100495,darkslategrey:3100495,darkturquoise:52945,darkviolet:9699539,deeppink:16716947,deepskyblue:49151,dimgray:6908265,dimgrey:6908265,dodgerblue:2003199,firebrick:11674146,floralwhite:16775920,forestgreen:2263842,fuchsia:16711935,gainsboro:14474460,ghostwhite:16316671,gold:16766720,goldenrod:14329120,gray:8421504,green:32768,greenyellow:11403055,grey:8421504,honeydew:15794160,hotpink:16738740,indianred:13458524,indigo:4915330,ivory:16777200,khaki:15787660,lavender:15132410,lavenderblush:16773365,lawngreen:8190976,lemonchiffon:16775885,lightblue:11393254,lightcoral:15761536,lightcyan:14745599,lightgoldenrodyellow:16448210,lightgray:13882323,lightgreen:9498256,lightgrey:13882323,lightpink:16758465,lightsalmon:16752762,lightseagreen:2142890,lightskyblue:8900346,lightslategray:7833753,lightslategrey:7833753,lightsteelblue:11584734,lightyellow:16777184,lime:65280,limegreen:3329330,linen:16445670,magenta:16711935,maroon:8388608,mediumaquamarine:6737322,mediumblue:205,mediumorchid:12211667,mediumpurple:9662683,mediumseagreen:3978097,mediumslateblue:8087790,mediumspringgreen:64154,mediumturquoise:4772300,mediumvioletred:13047173,midnightblue:1644912,mintcream:16121850,mistyrose:16770273,moccasin:16770229,navajowhite:16768685,navy:128,oldlace:16643558,olive:8421376,olivedrab:7048739,orange:16753920,orangered:16729344,orchid:14315734,palegoldenrod:15657130,palegreen:10025880,paleturquoise:11529966,palevioletred:14381203,papayawhip:16773077,peachpuff:16767673,peru:13468991,pink:16761035,plum:14524637,powderblue:11591910,purple:8388736,rebeccapurple:6697881,red:16711680,rosybrown:12357519,royalblue:4286945,saddlebrown:9127187,salmon:16416882,sandybrown:16032864,seagreen:3050327,seashell:16774638,sienna:10506797,silver:12632256,skyblue:8900331,slateblue:6970061,slategray:7372944,slategrey:7372944,snow:16775930,springgreen:65407,steelblue:4620980,tan:13808780,teal:32896,thistle:14204888,tomato:16737095,turquoise:4251856,violet:15631086,wheat:16113331,white:16777215,whitesmoke:16119285,yellow:16776960,yellowgreen:10145074},fi={h:0,s:0,l:0},oo={h:0,s:0,l:0};function uc(i,e,t){return t<0&&(t+=1),t>1&&(t-=1),t<1/6?i+(e-i)*6*t:t<1/2?e:t<2/3?i+(e-i)*6*(2/3-t):i}var me=class{constructor(e,t,n){return this.isColor=!0,this.r=1,this.g=1,this.b=1,this.set(e,t,n)}set(e,t,n){if(t===void 0&&n===void 0){let s=e;s&&s.isColor?this.copy(s):typeof s=="number"?this.setHex(s):typeof s=="string"&&this.setStyle(s)}else this.setRGB(e,t,n);return this}setScalar(e){return this.r=e,this.g=e,this.b=e,this}setHex(e,t=cn){return e=Math.floor(e),this.r=(e>>16&255)/255,this.g=(e>>8&255)/255,this.b=(e&255)/255,ze.colorSpaceToWorking(this,t),this}setRGB(e,t,n,s=ze.workingColorSpace){return this.r=e,this.g=t,this.b=n,ze.colorSpaceToWorking(this,s),this}setHSL(e,t,n,s=ze.workingColorSpace){if(e=jc(e,1),t=Ve(t,0,1),n=Ve(n,0,1),t===0)this.r=this.g=this.b=n;else{let r=n<=.5?n*(1+t):n+t-n*t,o=2*n-r;this.r=uc(o,r,e+1/3),this.g=uc(o,r,e),this.b=uc(o,r,e-1/3)}return ze.colorSpaceToWorking(this,s),this}setStyle(e,t=cn){function n(r){r!==void 0&&parseFloat(r)<1&&Te("Color: Alpha component of "+e+" will be ignored.")}let s;if(s=/^(\w+)\(([^\)]*)\)/.exec(e)){let r,o=s[1],a=s[2];switch(o){case"rgb":case"rgba":if(r=/^\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*(?:,\s*(\d*\.?\d+)\s*)?$/.exec(a))return n(r[4]),this.setRGB(Math.min(255,parseInt(r[1],10))/255,Math.min(255,parseInt(r[2],10))/255,Math.min(255,parseInt(r[3],10))/255,t);if(r=/^\s*(\d+)\%\s*,\s*(\d+)\%\s*,\s*(\d+)\%\s*(?:,\s*(\d*\.?\d+)\s*)?$/.exec(a))return n(r[4]),this.setRGB(Math.min(100,parseInt(r[1],10))/100,Math.min(100,parseInt(r[2],10))/100,Math.min(100,parseInt(r[3],10))/100,t);break;case"hsl":case"hsla":if(r=/^\s*(\d*\.?\d+)\s*,\s*(\d*\.?\d+)\%\s*,\s*(\d*\.?\d+)\%\s*(?:,\s*(\d*\.?\d+)\s*)?$/.exec(a))return n(r[4]),this.setHSL(parseFloat(r[1])/360,parseFloat(r[2])/100,parseFloat(r[3])/100,t);break;default:Te("Color: Unknown color model "+e)}}else if(s=/^\#([A-Fa-f\d]+)$/.exec(e)){let r=s[1],o=r.length;if(o===3)return this.setRGB(parseInt(r.charAt(0),16)/15,parseInt(r.charAt(1),16)/15,parseInt(r.charAt(2),16)/15,t);if(o===6)return this.setHex(parseInt(r,16),t);Te("Color: Invalid hex color "+e)}else if(e&&e.length>0)return this.setColorName(e,t);return this}setColorName(e,t=cn){let n=Md[e.toLowerCase()];return n!==void 0?this.setHex(n,t):Te("Color: Unknown color "+e),this}clone(){return new this.constructor(this.r,this.g,this.b)}copy(e){return this.r=e.r,this.g=e.g,this.b=e.b,this}copySRGBToLinear(e){return this.r=jn(e.r),this.g=jn(e.g),this.b=jn(e.b),this}copyLinearToSRGB(e){return this.r=bs(e.r),this.g=bs(e.g),this.b=bs(e.b),this}convertSRGBToLinear(){return this.copySRGBToLinear(this),this}convertLinearToSRGB(){return this.copyLinearToSRGB(this),this}getHex(e=cn){return ze.workingToColorSpace(Ht.copy(this),e),Math.round(Ve(Ht.r*255,0,255))*65536+Math.round(Ve(Ht.g*255,0,255))*256+Math.round(Ve(Ht.b*255,0,255))}getHexString(e=cn){return("000000"+this.getHex(e).toString(16)).slice(-6)}getHSL(e,t=ze.workingColorSpace){ze.workingToColorSpace(Ht.copy(this),t);let n=Ht.r,s=Ht.g,r=Ht.b,o=Math.max(n,s,r),a=Math.min(n,s,r),l,c,u=(a+o)/2;if(a===o)l=0,c=0;else{let d=o-a;switch(c=u<=.5?d/(o+a):d/(2-o-a),o){case n:l=(s-r)/d+(s0&&(t.object.backgroundBlurriness=this.backgroundBlurriness),this.backgroundIntensity!==1&&(t.object.backgroundIntensity=this.backgroundIntensity),t.object.backgroundRotation=this.backgroundRotation.toArray(),this.environmentIntensity!==1&&(t.object.environmentIntensity=this.environmentIntensity),t.object.environmentRotation=this.environmentRotation.toArray(),t}},wn=new N,Yn=new N,dc=new N,Zn=new N,ds=new N,fs=new N,Au=new N,fc=new N,pc=new N,mc=new N,gc=new xt,xc=new xt,vc=new xt,vi=class i{constructor(e=new N,t=new N,n=new N){this.a=e,this.b=t,this.c=n}static getNormal(e,t,n,s){s.subVectors(n,t),wn.subVectors(e,t),s.cross(wn);let r=s.lengthSq();return r>0?s.multiplyScalar(1/Math.sqrt(r)):s.set(0,0,0)}static getBarycoord(e,t,n,s,r){wn.subVectors(s,t),Yn.subVectors(n,t),dc.subVectors(e,t);let o=wn.dot(wn),a=wn.dot(Yn),l=wn.dot(dc),c=Yn.dot(Yn),u=Yn.dot(dc),d=o*c-a*a;if(d===0)return r.set(0,0,0),null;let h=1/d,f=(c*l-a*u)*h,g=(o*u-a*l)*h;return r.set(1-f-g,g,f)}static containsPoint(e,t,n,s){return this.getBarycoord(e,t,n,s,Zn)===null?!1:Zn.x>=0&&Zn.y>=0&&Zn.x+Zn.y<=1}static getInterpolation(e,t,n,s,r,o,a,l){return this.getBarycoord(e,t,n,s,Zn)===null?(l.x=0,l.y=0,"z"in l&&(l.z=0),"w"in l&&(l.w=0),null):(l.setScalar(0),l.addScaledVector(r,Zn.x),l.addScaledVector(o,Zn.y),l.addScaledVector(a,Zn.z),l)}static getInterpolatedAttribute(e,t,n,s,r,o){return gc.setScalar(0),xc.setScalar(0),vc.setScalar(0),gc.fromBufferAttribute(e,t),xc.fromBufferAttribute(e,n),vc.fromBufferAttribute(e,s),o.setScalar(0),o.addScaledVector(gc,r.x),o.addScaledVector(xc,r.y),o.addScaledVector(vc,r.z),o}static isFrontFacing(e,t,n,s){return wn.subVectors(n,t),Yn.subVectors(e,t),wn.cross(Yn).dot(s)<0}set(e,t,n){return this.a.copy(e),this.b.copy(t),this.c.copy(n),this}setFromPointsAndIndices(e,t,n,s){return this.a.copy(e[t]),this.b.copy(e[n]),this.c.copy(e[s]),this}setFromAttributeAndIndices(e,t,n,s){return this.a.fromBufferAttribute(e,t),this.b.fromBufferAttribute(e,n),this.c.fromBufferAttribute(e,s),this}clone(){return new this.constructor().copy(this)}copy(e){return this.a.copy(e.a),this.b.copy(e.b),this.c.copy(e.c),this}getArea(){return wn.subVectors(this.c,this.b),Yn.subVectors(this.a,this.b),wn.cross(Yn).length()*.5}getMidpoint(e){return e.addVectors(this.a,this.b).add(this.c).multiplyScalar(1/3)}getNormal(e){return i.getNormal(this.a,this.b,this.c,e)}getPlane(e){return e.setFromCoplanarPoints(this.a,this.b,this.c)}getBarycoord(e,t){return i.getBarycoord(e,this.a,this.b,this.c,t)}getInterpolation(e,t,n,s,r){return i.getInterpolation(e,this.a,this.b,this.c,t,n,s,r)}containsPoint(e){return i.containsPoint(e,this.a,this.b,this.c)}isFrontFacing(e){return i.isFrontFacing(this.a,this.b,this.c,e)}intersectsBox(e){return e.intersectsTriangle(this)}closestPointToPoint(e,t){let n=this.a,s=this.b,r=this.c,o,a;ds.subVectors(s,n),fs.subVectors(r,n),fc.subVectors(e,n);let l=ds.dot(fc),c=fs.dot(fc);if(l<=0&&c<=0)return t.copy(n);pc.subVectors(e,s);let u=ds.dot(pc),d=fs.dot(pc);if(u>=0&&d<=u)return t.copy(s);let h=l*d-u*c;if(h<=0&&l>=0&&u<=0)return o=l/(l-u),t.copy(n).addScaledVector(ds,o);mc.subVectors(e,r);let f=ds.dot(mc),g=fs.dot(mc);if(g>=0&&f<=g)return t.copy(r);let x=f*c-l*g;if(x<=0&&c>=0&&g<=0)return a=c/(c-g),t.copy(n).addScaledVector(fs,a);let m=u*g-f*d;if(m<=0&&d-u>=0&&f-g>=0)return Au.subVectors(r,s),a=(d-u)/(d-u+(f-g)),t.copy(s).addScaledVector(Au,a);let p=1/(m+x+h);return o=x*p,a=h*p,t.copy(n).addScaledVector(ds,o).addScaledVector(fs,a)}equals(e){return e.a.equals(this.a)&&e.b.equals(this.b)&&e.c.equals(this.c)}},bi=class{constructor(e=new N(1/0,1/0,1/0),t=new N(-1/0,-1/0,-1/0)){this.isBox3=!0,this.min=e,this.max=t}set(e,t){return this.min.copy(e),this.max.copy(t),this}setFromArray(e){this.makeEmpty();for(let t=0,n=e.length;t=this.min.x&&e.x<=this.max.x&&e.y>=this.min.y&&e.y<=this.max.y&&e.z>=this.min.z&&e.z<=this.max.z}containsBox(e){return this.min.x<=e.min.x&&e.max.x<=this.max.x&&this.min.y<=e.min.y&&e.max.y<=this.max.y&&this.min.z<=e.min.z&&e.max.z<=this.max.z}getParameter(e,t){return t.set((e.x-this.min.x)/(this.max.x-this.min.x),(e.y-this.min.y)/(this.max.y-this.min.y),(e.z-this.min.z)/(this.max.z-this.min.z))}intersectsBox(e){return e.max.x>=this.min.x&&e.min.x<=this.max.x&&e.max.y>=this.min.y&&e.min.y<=this.max.y&&e.max.z>=this.min.z&&e.min.z<=this.max.z}intersectsSphere(e){return this.clampPoint(e.center,En),En.distanceToSquared(e.center)<=e.radius*e.radius}intersectsPlane(e){let t,n;return e.normal.x>0?(t=e.normal.x*this.min.x,n=e.normal.x*this.max.x):(t=e.normal.x*this.max.x,n=e.normal.x*this.min.x),e.normal.y>0?(t+=e.normal.y*this.min.y,n+=e.normal.y*this.max.y):(t+=e.normal.y*this.max.y,n+=e.normal.y*this.min.y),e.normal.z>0?(t+=e.normal.z*this.min.z,n+=e.normal.z*this.max.z):(t+=e.normal.z*this.max.z,n+=e.normal.z*this.min.z),t<=-e.constant&&n>=-e.constant}intersectsTriangle(e){if(this.isEmpty())return!1;this.getCenter($s),lo.subVectors(this.max,$s),ps.subVectors(e.a,$s),ms.subVectors(e.b,$s),gs.subVectors(e.c,$s),pi.subVectors(ms,ps),mi.subVectors(gs,ms),Vi.subVectors(ps,gs);let t=[0,-pi.z,pi.y,0,-mi.z,mi.y,0,-Vi.z,Vi.y,pi.z,0,-pi.x,mi.z,0,-mi.x,Vi.z,0,-Vi.x,-pi.y,pi.x,0,-mi.y,mi.x,0,-Vi.y,Vi.x,0];return!_c(t,ps,ms,gs,lo)||(t=[1,0,0,0,1,0,0,0,1],!_c(t,ps,ms,gs,lo))?!1:(co.crossVectors(pi,mi),t=[co.x,co.y,co.z],_c(t,ps,ms,gs,lo))}clampPoint(e,t){return t.copy(e).clamp(this.min,this.max)}distanceToPoint(e){return this.clampPoint(e,En).distanceTo(e)}getBoundingSphere(e){return this.isEmpty()?e.makeEmpty():(this.getCenter(e.center),e.radius=this.getSize(En).length()*.5),e}intersect(e){return this.min.max(e.min),this.max.min(e.max),this.isEmpty()&&this.makeEmpty(),this}union(e){return this.min.min(e.min),this.max.max(e.max),this}applyMatrix4(e){return this.isEmpty()?this:($n[0].set(this.min.x,this.min.y,this.min.z).applyMatrix4(e),$n[1].set(this.min.x,this.min.y,this.max.z).applyMatrix4(e),$n[2].set(this.min.x,this.max.y,this.min.z).applyMatrix4(e),$n[3].set(this.min.x,this.max.y,this.max.z).applyMatrix4(e),$n[4].set(this.max.x,this.min.y,this.min.z).applyMatrix4(e),$n[5].set(this.max.x,this.min.y,this.max.z).applyMatrix4(e),$n[6].set(this.max.x,this.max.y,this.min.z).applyMatrix4(e),$n[7].set(this.max.x,this.max.y,this.max.z).applyMatrix4(e),this.setFromPoints($n),this)}translate(e){return this.min.add(e),this.max.add(e),this}equals(e){return e.min.equals(this.min)&&e.max.equals(this.max)}toJSON(){return{min:this.min.toArray(),max:this.max.toArray()}}fromJSON(e){return this.min.fromArray(e.min),this.max.fromArray(e.max),this}},$n=[new N,new N,new N,new N,new N,new N,new N,new N],En=new N,ao=new bi,ps=new N,ms=new N,gs=new N,pi=new N,mi=new N,Vi=new N,$s=new N,lo=new N,co=new N,Gi=new N;function _c(i,e,t,n,s){for(let r=0,o=i.length-3;r<=o;r+=3){Gi.fromArray(i,r);let a=s.x*Math.abs(Gi.x)+s.y*Math.abs(Gi.y)+s.z*Math.abs(Gi.z),l=e.dot(Gi),c=t.dot(Gi),u=n.dot(Gi);if(Math.max(-Math.max(l,c,u),Math.min(l,c,u))>a)return!1}return!0}var wt=new N,ho=new Ee,Jm=0,Ye=class extends An{constructor(e,t,n=!1){if(super(),Array.isArray(e))throw new TypeError("THREE.BufferAttribute: array should be a Typed Array.");this.isBufferAttribute=!0,Object.defineProperty(this,"id",{value:Jm++}),this.name="",this.array=e,this.itemSize=t,this.count=e!==void 0?e.length/t:0,this.normalized=n,this.usage=Nc,this.updateRanges=[],this.gpuType=Pn,this.version=0}onUploadCallback(){}set needsUpdate(e){e===!0&&this.version++}setUsage(e){return this.usage=e,this}addUpdateRange(e,t){this.updateRanges.push({start:e,count:t})}clearUpdateRanges(){this.updateRanges.length=0}copy(e){return this.name=e.name,this.array=new e.array.constructor(e.array),this.itemSize=e.itemSize,this.count=e.count,this.normalized=e.normalized,this.usage=e.usage,this.gpuType=e.gpuType,this}copyAt(e,t,n){e*=this.itemSize,n*=t.itemSize;for(let s=0,r=this.itemSize;sthis.radius*this.radius&&(t.sub(this.center).normalize(),t.multiplyScalar(this.radius).add(this.center)),t}getBoundingBox(e){return this.isEmpty()?(e.makeEmpty(),e):(e.set(this.center,this.center),e.expandByScalar(this.radius),e)}applyMatrix4(e){return this.center.applyMatrix4(e),this.radius=this.radius*e.getMaxScaleOnAxis(),this}translate(e){return this.center.add(e),this}expandByPoint(e){if(this.isEmpty())return this.center.copy(e),this.radius=0,this;Ks.subVectors(e,this.center);let t=Ks.lengthSq();if(t>this.radius*this.radius){let n=Math.sqrt(t),s=(n-this.radius)*.5;this.center.addScaledVector(Ks,s/n),this.radius+=s}return this}union(e){return e.isEmpty()?this:this.isEmpty()?(this.copy(e),this):(this.center.equals(e.center)===!0?this.radius=Math.max(this.radius,e.radius):(yc.subVectors(e.center,this.center).setLength(e.radius),this.expandByPoint(Ks.copy(e.center).add(yc)),this.expandByPoint(Ks.copy(e.center).sub(yc))),this)}equals(e){return e.center.equals(this.center)&&e.radius===this.radius}clone(){return new this.constructor().copy(this)}toJSON(){return{radius:this.radius,center:this.center.toArray()}}fromJSON(e){return this.radius=e.radius,this.center.fromArray(e.center),this}},Qm=0,vn=new gt,bc=new Qt,xs=new N,ln=new bi,Js=new bi,Nt=new N,ht=class i extends An{constructor(){super(),this.isBufferGeometry=!0,Object.defineProperty(this,"id",{value:Qm++}),this.uuid=Is(),this.name="",this.type="BufferGeometry",this.index=null,this.indirect=null,this.indirectOffset=0,this.attributes={},this.morphAttributes={},this.morphTargetsRelative=!1,this.groups=[],this.boundingBox=null,this.boundingSphere=null,this.drawRange={start:0,count:1/0},this.userData={}}getIndex(){return this.index}setIndex(e){return Array.isArray(e)?this.index=new(Sm(e)?cr:lr)(e,1):this.index=e,this}setIndirect(e,t=0){return this.indirect=e,this.indirectOffset=t,this}getIndirect(){return this.indirect}getAttribute(e){return this.attributes[e]}setAttribute(e,t){return this.attributes[e]=t,this}deleteAttribute(e){return delete this.attributes[e],this}hasAttribute(e){return this.attributes[e]!==void 0}addGroup(e,t,n=0){this.groups.push({start:e,count:t,materialIndex:n})}clearGroups(){this.groups=[]}setDrawRange(e,t){this.drawRange.start=e,this.drawRange.count=t}applyMatrix4(e){let t=this.attributes.position;t!==void 0&&(t.applyMatrix4(e),t.needsUpdate=!0);let n=this.attributes.normal;if(n!==void 0){let r=new Le().getNormalMatrix(e);n.applyNormalMatrix(r),n.needsUpdate=!0}let s=this.attributes.tangent;return s!==void 0&&(s.transformDirection(e),s.needsUpdate=!0),this.boundingBox!==null&&this.computeBoundingBox(),this.boundingSphere!==null&&this.computeBoundingSphere(),this}applyQuaternion(e){return vn.makeRotationFromQuaternion(e),this.applyMatrix4(vn),this}rotateX(e){return vn.makeRotationX(e),this.applyMatrix4(vn),this}rotateY(e){return vn.makeRotationY(e),this.applyMatrix4(vn),this}rotateZ(e){return vn.makeRotationZ(e),this.applyMatrix4(vn),this}translate(e,t,n){return vn.makeTranslation(e,t,n),this.applyMatrix4(vn),this}scale(e,t,n){return vn.makeScale(e,t,n),this.applyMatrix4(vn),this}lookAt(e){return bc.lookAt(e),bc.updateMatrix(),this.applyMatrix4(bc.matrix),this}center(){return this.computeBoundingBox(),this.boundingBox.getCenter(xs).negate(),this.translate(xs.x,xs.y,xs.z),this}setFromPoints(e){let t=this.getAttribute("position");if(t===void 0){let n=[];for(let s=0,r=e.length;st.count&&Te("BufferGeometry: Buffer size too small for points data. Use .dispose() and create a new geometry."),t.needsUpdate=!0}return this}computeBoundingBox(){this.boundingBox===null&&(this.boundingBox=new bi);let e=this.attributes.position,t=this.morphAttributes.position;if(e&&e.isGLBufferAttribute){Re("BufferGeometry.computeBoundingBox(): GLBufferAttribute requires a manual bounding box.",this),this.boundingBox.set(new N(-1/0,-1/0,-1/0),new N(1/0,1/0,1/0));return}if(e!==void 0){if(this.boundingBox.setFromBufferAttribute(e),t)for(let n=0,s=t.length;n0&&(e.userData=this.userData),this.parameters!==void 0){let l=this.parameters;for(let c in l)l[c]!==void 0&&(e[c]=l[c]);return e}e.data={attributes:{}};let t=this.index;t!==null&&(e.data.index={type:t.array.constructor.name,array:Array.prototype.slice.call(t.array)});let n=this.attributes;for(let l in n){let c=n[l];e.data.attributes[l]=c.toJSON(e.data)}let s={},r=!1;for(let l in this.morphAttributes){let c=this.morphAttributes[l],u=[];for(let d=0,h=c.length;d0&&(s[l]=u,r=!0)}r&&(e.data.morphAttributes=s,e.data.morphTargetsRelative=this.morphTargetsRelative);let o=this.groups;o.length>0&&(e.data.groups=JSON.parse(JSON.stringify(o)));let a=this.boundingSphere;return a!==null&&(e.data.boundingSphere=a.toJSON()),e}clone(){return new this.constructor().copy(this)}copy(e){this.index=null,this.attributes={},this.morphAttributes={},this.groups=[],this.boundingBox=null,this.boundingSphere=null;let t={};this.name=e.name;let n=e.index;n!==null&&this.setIndex(n.clone());let s=e.attributes;for(let c in s){let u=s[c];this.setAttribute(c,u.clone(t))}let r=e.morphAttributes;for(let c in r){let u=[],d=r[c];for(let h=0,f=d.length;h0!=e>0&&this.version++,this._alphaTest=e}onBeforeRender(){}onBeforeCompile(){}customProgramCacheKey(){return this.onBeforeCompile.toString()}setValues(e){if(e!==void 0)for(let t in e){let n=e[t];if(n===void 0){Te(`Material: parameter '${t}' has value of undefined.`);continue}let s=this[t];if(s===void 0){Te(`Material: '${t}' is not a property of THREE.${this.type}.`);continue}s&&s.isColor?s.set(n):s&&s.isVector3&&n&&n.isVector3?s.copy(n):this[t]=n}}toJSON(e){let t=e===void 0||typeof e=="string";t&&(e={textures:{},images:{}});let n={metadata:{version:4.7,type:"Material",generator:"Material.toJSON"}};n.uuid=this.uuid,n.type=this.type,this.name!==""&&(n.name=this.name),this.color&&this.color.isColor&&(n.color=this.color.getHex()),this.roughness!==void 0&&(n.roughness=this.roughness),this.metalness!==void 0&&(n.metalness=this.metalness),this.sheen!==void 0&&(n.sheen=this.sheen),this.sheenColor&&this.sheenColor.isColor&&(n.sheenColor=this.sheenColor.getHex()),this.sheenRoughness!==void 0&&(n.sheenRoughness=this.sheenRoughness),this.emissive&&this.emissive.isColor&&(n.emissive=this.emissive.getHex()),this.emissiveIntensity!==void 0&&this.emissiveIntensity!==1&&(n.emissiveIntensity=this.emissiveIntensity),this.specular&&this.specular.isColor&&(n.specular=this.specular.getHex()),this.specularIntensity!==void 0&&(n.specularIntensity=this.specularIntensity),this.specularColor&&this.specularColor.isColor&&(n.specularColor=this.specularColor.getHex()),this.shininess!==void 0&&(n.shininess=this.shininess),this.clearcoat!==void 0&&(n.clearcoat=this.clearcoat),this.clearcoatRoughness!==void 0&&(n.clearcoatRoughness=this.clearcoatRoughness),this.clearcoatMap&&this.clearcoatMap.isTexture&&(n.clearcoatMap=this.clearcoatMap.toJSON(e).uuid),this.clearcoatRoughnessMap&&this.clearcoatRoughnessMap.isTexture&&(n.clearcoatRoughnessMap=this.clearcoatRoughnessMap.toJSON(e).uuid),this.clearcoatNormalMap&&this.clearcoatNormalMap.isTexture&&(n.clearcoatNormalMap=this.clearcoatNormalMap.toJSON(e).uuid,n.clearcoatNormalScale=this.clearcoatNormalScale.toArray()),this.sheenColorMap&&this.sheenColorMap.isTexture&&(n.sheenColorMap=this.sheenColorMap.toJSON(e).uuid),this.sheenRoughnessMap&&this.sheenRoughnessMap.isTexture&&(n.sheenRoughnessMap=this.sheenRoughnessMap.toJSON(e).uuid),this.dispersion!==void 0&&(n.dispersion=this.dispersion),this.iridescence!==void 0&&(n.iridescence=this.iridescence),this.iridescenceIOR!==void 0&&(n.iridescenceIOR=this.iridescenceIOR),this.iridescenceThicknessRange!==void 0&&(n.iridescenceThicknessRange=this.iridescenceThicknessRange),this.iridescenceMap&&this.iridescenceMap.isTexture&&(n.iridescenceMap=this.iridescenceMap.toJSON(e).uuid),this.iridescenceThicknessMap&&this.iridescenceThicknessMap.isTexture&&(n.iridescenceThicknessMap=this.iridescenceThicknessMap.toJSON(e).uuid),this.anisotropy!==void 0&&(n.anisotropy=this.anisotropy),this.anisotropyRotation!==void 0&&(n.anisotropyRotation=this.anisotropyRotation),this.anisotropyMap&&this.anisotropyMap.isTexture&&(n.anisotropyMap=this.anisotropyMap.toJSON(e).uuid),this.map&&this.map.isTexture&&(n.map=this.map.toJSON(e).uuid),this.matcap&&this.matcap.isTexture&&(n.matcap=this.matcap.toJSON(e).uuid),this.alphaMap&&this.alphaMap.isTexture&&(n.alphaMap=this.alphaMap.toJSON(e).uuid),this.lightMap&&this.lightMap.isTexture&&(n.lightMap=this.lightMap.toJSON(e).uuid,n.lightMapIntensity=this.lightMapIntensity),this.aoMap&&this.aoMap.isTexture&&(n.aoMap=this.aoMap.toJSON(e).uuid,n.aoMapIntensity=this.aoMapIntensity),this.bumpMap&&this.bumpMap.isTexture&&(n.bumpMap=this.bumpMap.toJSON(e).uuid,n.bumpScale=this.bumpScale),this.normalMap&&this.normalMap.isTexture&&(n.normalMap=this.normalMap.toJSON(e).uuid,n.normalMapType=this.normalMapType,n.normalScale=this.normalScale.toArray()),this.displacementMap&&this.displacementMap.isTexture&&(n.displacementMap=this.displacementMap.toJSON(e).uuid,n.displacementScale=this.displacementScale,n.displacementBias=this.displacementBias),this.roughnessMap&&this.roughnessMap.isTexture&&(n.roughnessMap=this.roughnessMap.toJSON(e).uuid),this.metalnessMap&&this.metalnessMap.isTexture&&(n.metalnessMap=this.metalnessMap.toJSON(e).uuid),this.emissiveMap&&this.emissiveMap.isTexture&&(n.emissiveMap=this.emissiveMap.toJSON(e).uuid),this.specularMap&&this.specularMap.isTexture&&(n.specularMap=this.specularMap.toJSON(e).uuid),this.specularIntensityMap&&this.specularIntensityMap.isTexture&&(n.specularIntensityMap=this.specularIntensityMap.toJSON(e).uuid),this.specularColorMap&&this.specularColorMap.isTexture&&(n.specularColorMap=this.specularColorMap.toJSON(e).uuid),this.envMap&&this.envMap.isTexture&&(n.envMap=this.envMap.toJSON(e).uuid,this.combine!==void 0&&(n.combine=this.combine)),this.envMapRotation!==void 0&&(n.envMapRotation=this.envMapRotation.toArray()),this.envMapIntensity!==void 0&&(n.envMapIntensity=this.envMapIntensity),this.reflectivity!==void 0&&(n.reflectivity=this.reflectivity),this.refractionRatio!==void 0&&(n.refractionRatio=this.refractionRatio),this.gradientMap&&this.gradientMap.isTexture&&(n.gradientMap=this.gradientMap.toJSON(e).uuid),this.transmission!==void 0&&(n.transmission=this.transmission),this.transmissionMap&&this.transmissionMap.isTexture&&(n.transmissionMap=this.transmissionMap.toJSON(e).uuid),this.thickness!==void 0&&(n.thickness=this.thickness),this.thicknessMap&&this.thicknessMap.isTexture&&(n.thicknessMap=this.thicknessMap.toJSON(e).uuid),this.attenuationDistance!==void 0&&this.attenuationDistance!==1/0&&(n.attenuationDistance=this.attenuationDistance),this.attenuationColor!==void 0&&(n.attenuationColor=this.attenuationColor.getHex()),this.size!==void 0&&(n.size=this.size),this.shadowSide!==null&&(n.shadowSide=this.shadowSide),this.sizeAttenuation!==void 0&&(n.sizeAttenuation=this.sizeAttenuation),this.blending!==qi&&(n.blending=this.blending),this.side!==Qn&&(n.side=this.side),this.vertexColors===!0&&(n.vertexColors=!0),this.opacity<1&&(n.opacity=this.opacity),this.transparent===!0&&(n.transparent=!0),this.blendSrc!==Po&&(n.blendSrc=this.blendSrc),this.blendDst!==Io&&(n.blendDst=this.blendDst),this.blendEquation!==_i&&(n.blendEquation=this.blendEquation),this.blendSrcAlpha!==null&&(n.blendSrcAlpha=this.blendSrcAlpha),this.blendDstAlpha!==null&&(n.blendDstAlpha=this.blendDstAlpha),this.blendEquationAlpha!==null&&(n.blendEquationAlpha=this.blendEquationAlpha),this.blendColor&&this.blendColor.isColor&&(n.blendColor=this.blendColor.getHex()),this.blendAlpha!==0&&(n.blendAlpha=this.blendAlpha),this.depthFunc!==Yi&&(n.depthFunc=this.depthFunc),this.depthTest===!1&&(n.depthTest=this.depthTest),this.depthWrite===!1&&(n.depthWrite=this.depthWrite),this.colorWrite===!1&&(n.colorWrite=this.colorWrite),this.stencilWriteMask!==255&&(n.stencilWriteMask=this.stencilWriteMask),this.stencilFunc!==Lc&&(n.stencilFunc=this.stencilFunc),this.stencilRef!==0&&(n.stencilRef=this.stencilRef),this.stencilFuncMask!==255&&(n.stencilFuncMask=this.stencilFuncMask),this.stencilFail!==Xi&&(n.stencilFail=this.stencilFail),this.stencilZFail!==Xi&&(n.stencilZFail=this.stencilZFail),this.stencilZPass!==Xi&&(n.stencilZPass=this.stencilZPass),this.stencilWrite===!0&&(n.stencilWrite=this.stencilWrite),this.rotation!==void 0&&this.rotation!==0&&(n.rotation=this.rotation),this.polygonOffset===!0&&(n.polygonOffset=!0),this.polygonOffsetFactor!==0&&(n.polygonOffsetFactor=this.polygonOffsetFactor),this.polygonOffsetUnits!==0&&(n.polygonOffsetUnits=this.polygonOffsetUnits),this.linewidth!==void 0&&this.linewidth!==1&&(n.linewidth=this.linewidth),this.dashSize!==void 0&&(n.dashSize=this.dashSize),this.gapSize!==void 0&&(n.gapSize=this.gapSize),this.scale!==void 0&&(n.scale=this.scale),this.dithering===!0&&(n.dithering=!0),this.alphaTest>0&&(n.alphaTest=this.alphaTest),this.alphaHash===!0&&(n.alphaHash=!0),this.alphaToCoverage===!0&&(n.alphaToCoverage=!0),this.premultipliedAlpha===!0&&(n.premultipliedAlpha=!0),this.forceSinglePass===!0&&(n.forceSinglePass=!0),this.allowOverride===!1&&(n.allowOverride=!1),this.wireframe===!0&&(n.wireframe=!0),this.wireframeLinewidth>1&&(n.wireframeLinewidth=this.wireframeLinewidth),this.wireframeLinecap!=="round"&&(n.wireframeLinecap=this.wireframeLinecap),this.wireframeLinejoin!=="round"&&(n.wireframeLinejoin=this.wireframeLinejoin),this.flatShading===!0&&(n.flatShading=!0),this.visible===!1&&(n.visible=!1),this.toneMapped===!1&&(n.toneMapped=!1),this.fog===!1&&(n.fog=!1),Object.keys(this.userData).length>0&&(n.userData=this.userData);function s(r){let o=[];for(let a in r){let l=r[a];delete l.metadata,o.push(l)}return o}if(t){let r=s(e.textures),o=s(e.images);r.length>0&&(n.textures=r),o.length>0&&(n.images=o)}return n}clone(){return new this.constructor().copy(this)}copy(e){this.name=e.name,this.blending=e.blending,this.side=e.side,this.vertexColors=e.vertexColors,this.opacity=e.opacity,this.transparent=e.transparent,this.blendSrc=e.blendSrc,this.blendDst=e.blendDst,this.blendEquation=e.blendEquation,this.blendSrcAlpha=e.blendSrcAlpha,this.blendDstAlpha=e.blendDstAlpha,this.blendEquationAlpha=e.blendEquationAlpha,this.blendColor.copy(e.blendColor),this.blendAlpha=e.blendAlpha,this.depthFunc=e.depthFunc,this.depthTest=e.depthTest,this.depthWrite=e.depthWrite,this.stencilWriteMask=e.stencilWriteMask,this.stencilFunc=e.stencilFunc,this.stencilRef=e.stencilRef,this.stencilFuncMask=e.stencilFuncMask,this.stencilFail=e.stencilFail,this.stencilZFail=e.stencilZFail,this.stencilZPass=e.stencilZPass,this.stencilWrite=e.stencilWrite;let t=e.clippingPlanes,n=null;if(t!==null){let s=t.length;n=new Array(s);for(let r=0;r!==s;++r)n[r]=t[r].clone()}return this.clippingPlanes=n,this.clipIntersection=e.clipIntersection,this.clipShadows=e.clipShadows,this.shadowSide=e.shadowSide,this.colorWrite=e.colorWrite,this.precision=e.precision,this.polygonOffset=e.polygonOffset,this.polygonOffsetFactor=e.polygonOffsetFactor,this.polygonOffsetUnits=e.polygonOffsetUnits,this.dithering=e.dithering,this.alphaTest=e.alphaTest,this.alphaHash=e.alphaHash,this.alphaToCoverage=e.alphaToCoverage,this.premultipliedAlpha=e.premultipliedAlpha,this.forceSinglePass=e.forceSinglePass,this.allowOverride=e.allowOverride,this.visible=e.visible,this.toneMapped=e.toneMapped,this.userData=JSON.parse(JSON.stringify(e.userData)),this}dispose(){this.dispatchEvent({type:"dispose"})}set needsUpdate(e){e===!0&&this.version++}};var Kn=new N,Mc=new N,uo=new N,gi=new N,Sc=new N,fo=new N,wc=new N,Si=class{constructor(e=new N,t=new N(0,0,-1)){this.origin=e,this.direction=t}set(e,t){return this.origin.copy(e),this.direction.copy(t),this}copy(e){return this.origin.copy(e.origin),this.direction.copy(e.direction),this}at(e,t){return t.copy(this.origin).addScaledVector(this.direction,e)}lookAt(e){return this.direction.copy(e).sub(this.origin).normalize(),this}recast(e){return this.origin.copy(this.at(e,Kn)),this}closestPointToPoint(e,t){t.subVectors(e,this.origin);let n=t.dot(this.direction);return n<0?t.copy(this.origin):t.copy(this.origin).addScaledVector(this.direction,n)}distanceToPoint(e){return Math.sqrt(this.distanceSqToPoint(e))}distanceSqToPoint(e){let t=Kn.subVectors(e,this.origin).dot(this.direction);return t<0?this.origin.distanceToSquared(e):(Kn.copy(this.origin).addScaledVector(this.direction,t),Kn.distanceToSquared(e))}distanceSqToSegment(e,t,n,s){Mc.copy(e).add(t).multiplyScalar(.5),uo.copy(t).sub(e).normalize(),gi.copy(this.origin).sub(Mc);let r=e.distanceTo(t)*.5,o=-this.direction.dot(uo),a=gi.dot(this.direction),l=-gi.dot(uo),c=gi.lengthSq(),u=Math.abs(1-o*o),d,h,f,g;if(u>0)if(d=o*l-a,h=o*a-l,g=r*u,d>=0)if(h>=-g)if(h<=g){let x=1/u;d*=x,h*=x,f=d*(d+o*h+2*a)+h*(o*d+h+2*l)+c}else h=r,d=Math.max(0,-(o*h+a)),f=-d*d+h*(h+2*l)+c;else h=-r,d=Math.max(0,-(o*h+a)),f=-d*d+h*(h+2*l)+c;else h<=-g?(d=Math.max(0,-(-o*r+a)),h=d>0?-r:Math.min(Math.max(-r,-l),r),f=-d*d+h*(h+2*l)+c):h<=g?(d=0,h=Math.min(Math.max(-r,-l),r),f=h*(h+2*l)+c):(d=Math.max(0,-(o*r+a)),h=d>0?r:Math.min(Math.max(-r,-l),r),f=-d*d+h*(h+2*l)+c);else h=o>0?-r:r,d=Math.max(0,-(o*h+a)),f=-d*d+h*(h+2*l)+c;return n&&n.copy(this.origin).addScaledVector(this.direction,d),s&&s.copy(Mc).addScaledVector(uo,h),f}intersectSphere(e,t){Kn.subVectors(e.center,this.origin);let n=Kn.dot(this.direction),s=Kn.dot(Kn)-n*n,r=e.radius*e.radius;if(s>r)return null;let o=Math.sqrt(r-s),a=n-o,l=n+o;return l<0?null:a<0?this.at(l,t):this.at(a,t)}intersectsSphere(e){return e.radius<0?!1:this.distanceSqToPoint(e.center)<=e.radius*e.radius}distanceToPlane(e){let t=e.normal.dot(this.direction);if(t===0)return e.distanceToPoint(this.origin)===0?0:null;let n=-(this.origin.dot(e.normal)+e.constant)/t;return n>=0?n:null}intersectPlane(e,t){let n=this.distanceToPlane(e);return n===null?null:this.at(n,t)}intersectsPlane(e){let t=e.distanceToPoint(this.origin);return t===0||e.normal.dot(this.direction)*t<0}intersectBox(e,t){let n,s,r,o,a,l,c=1/this.direction.x,u=1/this.direction.y,d=1/this.direction.z,h=this.origin;return c>=0?(n=(e.min.x-h.x)*c,s=(e.max.x-h.x)*c):(n=(e.max.x-h.x)*c,s=(e.min.x-h.x)*c),u>=0?(r=(e.min.y-h.y)*u,o=(e.max.y-h.y)*u):(r=(e.max.y-h.y)*u,o=(e.min.y-h.y)*u),n>o||r>s||((r>n||isNaN(n))&&(n=r),(o=0?(a=(e.min.z-h.z)*d,l=(e.max.z-h.z)*d):(a=(e.max.z-h.z)*d,l=(e.min.z-h.z)*d),n>l||a>s)||((a>n||n!==n)&&(n=a),(l=0?n:s,t)}intersectsBox(e){return this.intersectBox(e,Kn)!==null}intersectTriangle(e,t,n,s,r){Sc.subVectors(t,e),fo.subVectors(n,e),wc.crossVectors(Sc,fo);let o=this.direction.dot(wc),a;if(o>0){if(s)return null;a=1}else if(o<0)a=-1,o=-o;else return null;gi.subVectors(this.origin,e);let l=a*this.direction.dot(fo.crossVectors(gi,fo));if(l<0)return null;let c=a*this.direction.dot(Sc.cross(gi));if(c<0||l+c>o)return null;let u=-a*gi.dot(wc);return u<0?null:this.at(u/o,r)}applyMatrix4(e){return this.origin.applyMatrix4(e),this.direction.transformDirection(e),this}equals(e){return e.origin.equals(this.origin)&&e.direction.equals(this.direction)}clone(){return new this.constructor().copy(this)}},ti=class extends ei{constructor(e){super(),this.isMeshBasicMaterial=!0,this.type="MeshBasicMaterial",this.color=new me(16777215),this.map=null,this.lightMap=null,this.lightMapIntensity=1,this.aoMap=null,this.aoMapIntensity=1,this.specularMap=null,this.alphaMap=null,this.envMap=null,this.envMapRotation=new yi,this.combine=zc,this.reflectivity=1,this.refractionRatio=.98,this.wireframe=!1,this.wireframeLinewidth=1,this.wireframeLinecap="round",this.wireframeLinejoin="round",this.fog=!0,this.setValues(e)}copy(e){return super.copy(e),this.color.copy(e.color),this.map=e.map,this.lightMap=e.lightMap,this.lightMapIntensity=e.lightMapIntensity,this.aoMap=e.aoMap,this.aoMapIntensity=e.aoMapIntensity,this.specularMap=e.specularMap,this.alphaMap=e.alphaMap,this.envMap=e.envMap,this.envMapRotation.copy(e.envMapRotation),this.combine=e.combine,this.reflectivity=e.reflectivity,this.refractionRatio=e.refractionRatio,this.wireframe=e.wireframe,this.wireframeLinewidth=e.wireframeLinewidth,this.wireframeLinecap=e.wireframeLinecap,this.wireframeLinejoin=e.wireframeLinejoin,this.fog=e.fog,this}},Cu=new gt,Hi=new Si,po=new Mi,Ru=new N,mo=new N,go=new N,xo=new N,Ec=new N,vo=new N,Pu=new N,_o=new N,Xt=class extends Qt{constructor(e=new ht,t=new ti){super(),this.isMesh=!0,this.type="Mesh",this.geometry=e,this.material=t,this.morphTargetDictionary=void 0,this.morphTargetInfluences=void 0,this.count=1,this.updateMorphTargets()}copy(e,t){return super.copy(e,t),e.morphTargetInfluences!==void 0&&(this.morphTargetInfluences=e.morphTargetInfluences.slice()),e.morphTargetDictionary!==void 0&&(this.morphTargetDictionary=Object.assign({},e.morphTargetDictionary)),this.material=Array.isArray(e.material)?e.material.slice():e.material,this.geometry=e.geometry,this}updateMorphTargets(){let t=this.geometry.morphAttributes,n=Object.keys(t);if(n.length>0){let s=t[n[0]];if(s!==void 0){this.morphTargetInfluences=[],this.morphTargetDictionary={};for(let r=0,o=s.length;r(e.far-e.near)**2))&&(Cu.copy(r).invert(),Hi.copy(e.ray).applyMatrix4(Cu),!(n.boundingBox!==null&&Hi.intersectsBox(n.boundingBox)===!1)&&this._computeIntersections(e,t,Hi)))}_computeIntersections(e,t,n){let s,r=this.geometry,o=this.material,a=r.index,l=r.attributes.position,c=r.attributes.uv,u=r.attributes.uv1,d=r.attributes.normal,h=r.groups,f=r.drawRange;if(a!==null)if(Array.isArray(o))for(let g=0,x=h.length;gt.far?null:{distance:c,point:_o.clone(),object:i}}function yo(i,e,t,n,s,r,o,a,l,c){i.getVertexPosition(a,mo),i.getVertexPosition(l,go),i.getVertexPosition(c,xo);let u=tg(i,e,t,n,mo,go,xo,Pu);if(u){let d=new N;vi.getBarycoord(Pu,mo,go,xo,d),s&&(u.uv=vi.getInterpolatedAttribute(s,a,l,c,d,new Ee)),r&&(u.uv1=vi.getInterpolatedAttribute(r,a,l,c,d,new Ee)),o&&(u.normal=vi.getInterpolatedAttribute(o,a,l,c,d,new N),u.normal.dot(n.direction)>0&&u.normal.multiplyScalar(-1));let h={a,b:l,c,normal:new N,materialIndex:0};vi.getNormal(mo,go,xo,h.normal),u.face=h,u.barycoord=d}return u}var qo=class extends jt{constructor(e=null,t=1,n=1,s,r,o,a,l,c=Dt,u=Dt,d,h){super(null,o,a,l,c,u,s,r,d,h),this.isDataTexture=!0,this.image={data:e,width:t,height:n},this.generateMipmaps=!1,this.flipY=!1,this.unpackAlignment=1}};var Tc=new N,ng=new N,ig=new Le,_n=class{constructor(e=new N(1,0,0),t=0){this.isPlane=!0,this.normal=e,this.constant=t}set(e,t){return this.normal.copy(e),this.constant=t,this}setComponents(e,t,n,s){return this.normal.set(e,t,n),this.constant=s,this}setFromNormalAndCoplanarPoint(e,t){return this.normal.copy(e),this.constant=-t.dot(this.normal),this}setFromCoplanarPoints(e,t,n){let s=Tc.subVectors(n,t).cross(ng.subVectors(e,t)).normalize();return this.setFromNormalAndCoplanarPoint(s,e),this}copy(e){return this.normal.copy(e.normal),this.constant=e.constant,this}normalize(){let e=1/this.normal.length();return this.normal.multiplyScalar(e),this.constant*=e,this}negate(){return this.constant*=-1,this.normal.negate(),this}distanceToPoint(e){return this.normal.dot(e)+this.constant}distanceToSphere(e){return this.distanceToPoint(e.center)-e.radius}projectPoint(e,t){return t.copy(e).addScaledVector(this.normal,-this.distanceToPoint(e))}intersectLine(e,t,n=!0){let s=e.delta(Tc),r=this.normal.dot(s);if(r===0)return this.distanceToPoint(e.start)===0?t.copy(e.start):null;let o=-(e.start.dot(this.normal)+this.constant)/r;return n===!0&&(o<0||o>1)?null:t.copy(e.start).addScaledVector(s,o)}intersectsLine(e){let t=this.distanceToPoint(e.start),n=this.distanceToPoint(e.end);return t<0&&n>0||n<0&&t>0}intersectsBox(e){return e.intersectsPlane(this)}intersectsSphere(e){return e.intersectsPlane(this)}coplanarPoint(e){return e.copy(this.normal).multiplyScalar(-this.constant)}applyMatrix4(e,t){let n=t||ig.getNormalMatrix(e),s=this.coplanarPoint(Tc).applyMatrix4(e),r=this.normal.applyMatrix3(n).normalize();return this.constant=-s.dot(r),this}translate(e){return this.constant-=e.dot(this.normal),this}equals(e){return e.normal.equals(this.normal)&&e.constant===this.constant}clone(){return new this.constructor().copy(this)}},Wi=new Mi,sg=new Ee(.5,.5),bo=new N,hr=class{constructor(e=new _n,t=new _n,n=new _n,s=new _n,r=new _n,o=new _n){this.planes=[e,t,n,s,r,o]}set(e,t,n,s,r,o){let a=this.planes;return a[0].copy(e),a[1].copy(t),a[2].copy(n),a[3].copy(s),a[4].copy(r),a[5].copy(o),this}copy(e){let t=this.planes;for(let n=0;n<6;n++)t[n].copy(e.planes[n]);return this}setFromProjectionMatrix(e,t=Tn,n=!1){let s=this.planes,r=e.elements,o=r[0],a=r[1],l=r[2],c=r[3],u=r[4],d=r[5],h=r[6],f=r[7],g=r[8],x=r[9],m=r[10],p=r[11],v=r[12],b=r[13],M=r[14],E=r[15];if(s[0].setComponents(c-o,f-u,p-g,E-v).normalize(),s[1].setComponents(c+o,f+u,p+g,E+v).normalize(),s[2].setComponents(c+a,f+d,p+x,E+b).normalize(),s[3].setComponents(c-a,f-d,p-x,E-b).normalize(),n)s[4].setComponents(l,h,m,M).normalize(),s[5].setComponents(c-l,f-h,p-m,E-M).normalize();else if(s[4].setComponents(c-l,f-h,p-m,E-M).normalize(),t===Tn)s[5].setComponents(c+l,f+h,p+m,E+M).normalize();else if(t===sr)s[5].setComponents(l,h,m,M).normalize();else throw new Error("THREE.Frustum.setFromProjectionMatrix(): Invalid coordinate system: "+t);return this}intersectsObject(e){if(e.boundingSphere!==void 0)e.boundingSphere===null&&e.computeBoundingSphere(),Wi.copy(e.boundingSphere).applyMatrix4(e.matrixWorld);else{let t=e.geometry;t.boundingSphere===null&&t.computeBoundingSphere(),Wi.copy(t.boundingSphere).applyMatrix4(e.matrixWorld)}return this.intersectsSphere(Wi)}intersectsSprite(e){Wi.center.set(0,0,0);let t=sg.distanceTo(e.center);return Wi.radius=.7071067811865476+t,Wi.applyMatrix4(e.matrixWorld),this.intersectsSphere(Wi)}intersectsSphere(e){let t=this.planes,n=e.center,s=-e.radius;for(let r=0;r<6;r++)if(t[r].distanceToPoint(n)0?e.max.x:e.min.x,bo.y=s.normal.y>0?e.max.y:e.min.y,bo.z=s.normal.z>0?e.max.z:e.min.z,s.distanceToPoint(bo)<0)return!1}return!0}containsPoint(e){let t=this.planes;for(let n=0;n<6;n++)if(t[n].distanceToPoint(e)<0)return!1;return!0}clone(){return new this.constructor().copy(this)}};var ni=class extends ei{constructor(e){super(),this.isLineBasicMaterial=!0,this.type="LineBasicMaterial",this.color=new me(16777215),this.map=null,this.linewidth=1,this.linecap="round",this.linejoin="round",this.fog=!0,this.setValues(e)}copy(e){return super.copy(e),this.color.copy(e.color),this.map=e.map,this.linewidth=e.linewidth,this.linecap=e.linecap,this.linejoin=e.linejoin,this.fog=e.fog,this}},Yo=new N,Zo=new N,Iu=new gt,js=new Si,Mo=new Mi,Ac=new N,Lu=new N,$o=class extends Qt{constructor(e=new ht,t=new ni){super(),this.isLine=!0,this.type="Line",this.geometry=e,this.material=t,this.morphTargetDictionary=void 0,this.morphTargetInfluences=void 0,this.updateMorphTargets()}copy(e,t){return super.copy(e,t),this.material=Array.isArray(e.material)?e.material.slice():e.material,this.geometry=e.geometry,this}computeLineDistances(){let e=this.geometry;if(e.index===null){let t=e.attributes.position,n=[0];for(let s=1,r=t.count;s0){let s=t[n[0]];if(s!==void 0){this.morphTargetInfluences=[],this.morphTargetDictionary={};for(let r=0,o=s.length;rn)return;Ac.applyMatrix4(i.matrixWorld);let c=e.ray.origin.distanceTo(Ac);if(!(ce.far))return{distance:c,point:Lu.clone().applyMatrix4(i.matrixWorld),index:o,face:null,faceIndex:null,barycoord:null,object:i}}var Nu=new N,Du=new N,wi=class extends $o{constructor(e,t){super(e,t),this.isLineSegments=!0,this.type="LineSegments"}computeLineDistances(){let e=this.geometry;if(e.index===null){let t=e.attributes.position,n=[];for(let s=0,r=t.count;s0){let s=t[n[0]];if(s!==void 0){this.morphTargetInfluences=[],this.morphTargetDictionary={};for(let r=0,o=s.length;rs.far)return;r.push({distance:c,distanceToRay:Math.sqrt(a),point:l,index:e,face:null,faceIndex:null,barycoord:null,object:o})}}var ur=class extends jt{constructor(e=[],t=Ii,n,s,r,o,a,l,c,u){super(e,t,n,s,r,o,a,l,c,u),this.isCubeTexture=!0,this.flipY=!1}get images(){return this.image}set images(e){this.image=e}};var ii=class extends jt{constructor(e,t,n=Rn,s,r,o,a=Dt,l=Dt,c,u=Un,d=1){if(u!==Un&&u!==Ni)throw new Error("DepthTexture format must be either THREE.DepthFormat or THREE.DepthStencilFormat");let h={width:e,height:t,depth:d};super(h,s,r,o,a,l,u,n,c),this.isDepthTexture=!0,this.flipY=!1,this.generateMipmaps=!1,this.compareFunction=null}copy(e){return super.copy(e),this.source=new ws(Object.assign({},e.image)),this.compareFunction=e.compareFunction,this}toJSON(e){let t=super.toJSON(e);return this.compareFunction!==null&&(t.compareFunction=this.compareFunction),t}},Ko=class extends ii{constructor(e,t=Rn,n=Ii,s,r,o=Dt,a=Dt,l,c=Un){let u={width:e,height:e,depth:1},d=[u,u,u,u,u,u];super(e,e,t,n,s,r,o,a,l,c),this.image=d,this.isCubeDepthTexture=!0,this.isCubeTexture=!0}get images(){return this.image}set images(e){this.image=e}},dr=class extends jt{constructor(e=null){super(),this.sourceTexture=e,this.isExternalTexture=!0}copy(e){return super.copy(e),this.sourceTexture=e.sourceTexture,this}},Ts=class i extends ht{constructor(e=1,t=1,n=1,s=1,r=1,o=1){super(),this.type="BoxGeometry",this.parameters={width:e,height:t,depth:n,widthSegments:s,heightSegments:r,depthSegments:o};let a=this;s=Math.floor(s),r=Math.floor(r),o=Math.floor(o);let l=[],c=[],u=[],d=[],h=0,f=0;g("z","y","x",-1,-1,n,t,e,o,r,0),g("z","y","x",1,-1,n,t,-e,o,r,1),g("x","z","y",1,1,e,n,t,s,o,2),g("x","z","y",1,-1,e,n,-t,s,o,3),g("x","y","z",1,-1,e,t,n,s,r,4),g("x","y","z",-1,-1,e,t,-n,s,r,5),this.setIndex(l),this.setAttribute("position",new Ut(c,3)),this.setAttribute("normal",new Ut(u,3)),this.setAttribute("uv",new Ut(d,2));function g(x,m,p,v,b,M,E,w,C,_,T){let P=M/C,R=E/_,I=M/2,H=E/2,W=w/2,k=C+1,z=_+1,V=0,K=0,Q=new N;for(let ae=0;ae0?1:-1,u.push(Q.x,Q.y,Q.z),d.push(be/C),d.push(1-ae/_),V+=1}}for(let ae=0;ae<_;ae++)for(let _e=0;_e0&&(t.defines=this.defines),t.vertexShader=this.vertexShader,t.fragmentShader=this.fragmentShader,t.lights=this.lights,t.clipping=this.clipping;let n={};for(let s in this.extensions)this.extensions[s]===!0&&(n[s]=!0);return Object.keys(n).length>0&&(t.extensions=n),t}},As=class extends ut{constructor(e){super(e),this.isRawShaderMaterial=!0,this.type="RawShaderMaterial"}};var Jo=class extends ei{constructor(e){super(),this.isMeshDepthMaterial=!0,this.type="MeshDepthMaterial",this.depthPacking=hd,this.map=null,this.alphaMap=null,this.displacementMap=null,this.displacementScale=1,this.displacementBias=0,this.wireframe=!1,this.wireframeLinewidth=1,this.setValues(e)}copy(e){return super.copy(e),this.depthPacking=e.depthPacking,this.map=e.map,this.alphaMap=e.alphaMap,this.displacementMap=e.displacementMap,this.displacementScale=e.displacementScale,this.displacementBias=e.displacementBias,this.wireframe=e.wireframe,this.wireframeLinewidth=e.wireframeLinewidth,this}},jo=class extends ei{constructor(e){super(),this.isMeshDistanceMaterial=!0,this.type="MeshDistanceMaterial",this.map=null,this.alphaMap=null,this.displacementMap=null,this.displacementScale=1,this.displacementBias=0,this.setValues(e)}copy(e){return super.copy(e),this.map=e.map,this.alphaMap=e.alphaMap,this.displacementMap=e.displacementMap,this.displacementScale=e.displacementScale,this.displacementBias=e.displacementBias,this}};function To(i,e){return!i||i.constructor===e?i:typeof e.BYTES_PER_ELEMENT=="number"?new e(i):Array.prototype.slice.call(i)}var Ti=class{constructor(e,t,n,s){this.parameterPositions=e,this._cachedIndex=0,this.resultBuffer=s!==void 0?s:new t.constructor(n),this.sampleValues=t,this.valueSize=n,this.settings=null,this.DefaultSettings_={}}evaluate(e){let t=this.parameterPositions,n=this._cachedIndex,s=t[n],r=t[n-1];n:{e:{let o;t:{i:if(!(e=r)){let a=t[1];e=r)break e}o=n,n=0;break t}break n}for(;n>>1;et;)--o;if(++o,r!==0||o!==s){r>=o&&(o=Math.max(o,1),r=o-1);let a=this.getValueSize();this.times=n.slice(r,o),this.values=this.values.slice(r*a,o*a)}return this}validate(){let e=!0,t=this.getValueSize();t-Math.floor(t)!==0&&(Re("KeyframeTrack: Invalid value size in track.",this),e=!1);let n=this.times,s=this.values,r=n.length;r===0&&(Re("KeyframeTrack: Track is empty.",this),e=!1);let o=null;for(let a=0;a!==r;a++){let l=n[a];if(typeof l=="number"&&isNaN(l)){Re("KeyframeTrack: Time is not a valid number.",this,a,l),e=!1;break}if(o!==null&&o>l){Re("KeyframeTrack: Out of order keys.",this,a,l,o),e=!1;break}o=l}if(s!==void 0&&wm(s))for(let a=0,l=s.length;a!==l;++a){let c=s[a];if(isNaN(c)){Re("KeyframeTrack: Value is not a valid number.",this,a,c),e=!1;break}}return e}optimize(){let e=this.times.slice(),t=this.values.slice(),n=this.getValueSize(),s=this.getInterpolation()===Ro,r=e.length-1,o=1;for(let a=1;a0){e[o]=e[r];for(let a=r*n,l=o*n,c=0;c!==n;++c)t[l+c]=t[a+c];++o}return o!==e.length?(this.times=e.slice(0,o),this.values=t.slice(0,o*n)):(this.times=e,this.values=t),this}clone(){let e=this.times.slice(),t=this.values.slice(),n=this.constructor,s=new n(this.name,e,t);return s.createInterpolant=this.createInterpolant,s}};un.prototype.ValueTypeName="";un.prototype.TimeBufferType=Float32Array;un.prototype.ValueBufferType=Float32Array;un.prototype.DefaultInterpolation=Vo;var Ai=class extends un{constructor(e,t,n){super(e,t,n)}};Ai.prototype.ValueTypeName="bool";Ai.prototype.ValueBufferType=Array;Ai.prototype.DefaultInterpolation=tr;Ai.prototype.InterpolantFactoryMethodLinear=void 0;Ai.prototype.InterpolantFactoryMethodSmooth=void 0;var ia=class extends un{constructor(e,t,n,s){super(e,t,n,s)}};ia.prototype.ValueTypeName="color";var sa=class extends un{constructor(e,t,n,s){super(e,t,n,s)}};sa.prototype.ValueTypeName="number";var ra=class extends Ti{constructor(e,t,n,s){super(e,t,n,s)}interpolate_(e,t,n,s){let r=this.resultBuffer,o=this.sampleValues,a=this.valueSize,l=(n-t)/(s-t),c=e*a;for(let u=c+a;c!==u;c+=4)hn.slerpFlat(r,0,o,c-a,o,c,l);return r}},pr=class extends un{constructor(e,t,n,s){super(e,t,n,s)}InterpolantFactoryMethodLinear(e){return new ra(this.times,this.values,this.getValueSize(),e)}};pr.prototype.ValueTypeName="quaternion";pr.prototype.InterpolantFactoryMethodSmooth=void 0;var Ci=class extends un{constructor(e,t,n){super(e,t,n)}};Ci.prototype.ValueTypeName="string";Ci.prototype.ValueBufferType=Array;Ci.prototype.DefaultInterpolation=tr;Ci.prototype.InterpolantFactoryMethodLinear=void 0;Ci.prototype.InterpolantFactoryMethodSmooth=void 0;var oa=class extends un{constructor(e,t,n,s){super(e,t,n,s)}};oa.prototype.ValueTypeName="vector";var aa=class{constructor(e,t,n){let s=this,r=!1,o=0,a=0,l,c=[];this.onStart=void 0,this.onLoad=e,this.onProgress=t,this.onError=n,this._abortController=null,this.itemStart=function(u){a++,r===!1&&s.onStart!==void 0&&s.onStart(u,o,a),r=!0},this.itemEnd=function(u){o++,s.onProgress!==void 0&&s.onProgress(u,o,a),o===a&&(r=!1,s.onLoad!==void 0&&s.onLoad())},this.itemError=function(u){s.onError!==void 0&&s.onError(u)},this.resolveURL=function(u){return l?l(u):u},this.setURLModifier=function(u){return l=u,this},this.addHandler=function(u,d){return c.push(u,d),this},this.removeHandler=function(u){let d=c.indexOf(u);return d!==-1&&c.splice(d,2),this},this.getHandler=function(u){for(let d=0,h=c.length;df.start-g.start);let h=0;for(let f=1;f 0 + vec4 plane; + #ifdef ALPHA_TO_COVERAGE + float distanceToPlane, distanceGradient; + float clipOpacity = 1.0; + #pragma unroll_loop_start + for ( int i = 0; i < UNION_CLIPPING_PLANES; i ++ ) { + plane = clippingPlanes[ i ]; + distanceToPlane = - dot( vClipPosition, plane.xyz ) + plane.w; + distanceGradient = fwidth( distanceToPlane ) / 2.0; + clipOpacity *= smoothstep( - distanceGradient, distanceGradient, distanceToPlane ); + if ( clipOpacity == 0.0 ) discard; + } + #pragma unroll_loop_end + #if UNION_CLIPPING_PLANES < NUM_CLIPPING_PLANES + float unionClipOpacity = 1.0; + #pragma unroll_loop_start + for ( int i = UNION_CLIPPING_PLANES; i < NUM_CLIPPING_PLANES; i ++ ) { + plane = clippingPlanes[ i ]; + distanceToPlane = - dot( vClipPosition, plane.xyz ) + plane.w; + distanceGradient = fwidth( distanceToPlane ) / 2.0; + unionClipOpacity *= 1.0 - smoothstep( - distanceGradient, distanceGradient, distanceToPlane ); + } + #pragma unroll_loop_end + clipOpacity *= 1.0 - unionClipOpacity; + #endif + diffuseColor.a *= clipOpacity; + if ( diffuseColor.a == 0.0 ) discard; + #else + #pragma unroll_loop_start + for ( int i = 0; i < UNION_CLIPPING_PLANES; i ++ ) { + plane = clippingPlanes[ i ]; + if ( dot( vClipPosition, plane.xyz ) > plane.w ) discard; + } + #pragma unroll_loop_end + #if UNION_CLIPPING_PLANES < NUM_CLIPPING_PLANES + bool clipped = true; + #pragma unroll_loop_start + for ( int i = UNION_CLIPPING_PLANES; i < NUM_CLIPPING_PLANES; i ++ ) { + plane = clippingPlanes[ i ]; + clipped = ( dot( vClipPosition, plane.xyz ) > plane.w ) && clipped; + } + #pragma unroll_loop_end + if ( clipped ) discard; + #endif + #endif +#endif`,Fg=`#if NUM_CLIPPING_PLANES > 0 + varying vec3 vClipPosition; + uniform vec4 clippingPlanes[ NUM_CLIPPING_PLANES ]; +#endif`,kg=`#if NUM_CLIPPING_PLANES > 0 + varying vec3 vClipPosition; +#endif`,Og=`#if NUM_CLIPPING_PLANES > 0 + vClipPosition = - mvPosition.xyz; +#endif`,Ug=`#if defined( USE_COLOR ) || defined( USE_COLOR_ALPHA ) + diffuseColor *= vColor; +#endif`,Bg=`#if defined( USE_COLOR ) || defined( USE_COLOR_ALPHA ) + varying vec4 vColor; +#endif`,zg=`#if defined( USE_COLOR ) || defined( USE_COLOR_ALPHA ) || defined( USE_INSTANCING_COLOR ) || defined( USE_BATCHING_COLOR ) + varying vec4 vColor; +#endif`,Vg=`#if defined( USE_COLOR ) || defined( USE_COLOR_ALPHA ) || defined( USE_INSTANCING_COLOR ) || defined( USE_BATCHING_COLOR ) + vColor = vec4( 1.0 ); +#endif +#ifdef USE_COLOR_ALPHA + vColor *= color; +#elif defined( USE_COLOR ) + vColor.rgb *= color; +#endif +#ifdef USE_INSTANCING_COLOR + vColor.rgb *= instanceColor.rgb; +#endif +#ifdef USE_BATCHING_COLOR + vColor *= getBatchingColor( getIndirectIndex( gl_DrawID ) ); +#endif`,Gg=`#define PI 3.141592653589793 +#define PI2 6.283185307179586 +#define PI_HALF 1.5707963267948966 +#define RECIPROCAL_PI 0.3183098861837907 +#define RECIPROCAL_PI2 0.15915494309189535 +#define EPSILON 1e-6 +#ifndef saturate +#define saturate( a ) clamp( a, 0.0, 1.0 ) +#endif +#define whiteComplement( a ) ( 1.0 - saturate( a ) ) +float pow2( const in float x ) { return x*x; } +vec3 pow2( const in vec3 x ) { return x*x; } +float pow3( const in float x ) { return x*x*x; } +float pow4( const in float x ) { float x2 = x*x; return x2*x2; } +float max3( const in vec3 v ) { return max( max( v.x, v.y ), v.z ); } +float average( const in vec3 v ) { return dot( v, vec3( 0.3333333 ) ); } +highp float rand( const in vec2 uv ) { + const highp float a = 12.9898, b = 78.233, c = 43758.5453; + highp float dt = dot( uv.xy, vec2( a,b ) ), sn = mod( dt, PI ); + return fract( sin( sn ) * c ); +} +#ifdef HIGH_PRECISION + float precisionSafeLength( vec3 v ) { return length( v ); } +#else + float precisionSafeLength( vec3 v ) { + float maxComponent = max3( abs( v ) ); + return length( v / maxComponent ) * maxComponent; + } +#endif +struct IncidentLight { + vec3 color; + vec3 direction; + bool visible; }; - -const DEFAULT_SETTINGS = { - atlasDepth: 6, - focusSiblingLimit: 160, - linkLimit: 1200, - renderNodeLimit: 4200, - externalLinkAnchorLimit: 700, - adaptiveDetail: true, - includeUnresolvedLinks: true, - showLinkOverlay: false, - enableLinkHover: false, - hoverHighlightMode: "hierarchy-all", - showExternalLinks: true, - externalDetailMode: "grouped", - colorScheme: "auto", - labelVisibility: "auto", - swirlStrength: DEFAULT_SWIRL_STRENGTH, - hiddenLegendItems: [], - ignoreFolders: [ - ".git", - ".obsidian" - ] +struct ReflectedLight { + vec3 directDiffuse; + vec3 directSpecular; + vec3 indirectDiffuse; + vec3 indirectSpecular; }; - -const DEFAULT_NATIVE_GRAPH_SETTINGS = { - showArrow: true, - textFadeMultiplier: 0, - nodeSizeMultiplier: 1, - lineSizeMultiplier: 1, - repelStrength: 10, - linkStrength: 1, - linkDistance: 250, - scale: 1 +#ifdef USE_ALPHAHASH + varying vec3 vPosition; +#endif +vec3 transformDirection( in vec3 dir, in mat4 matrix ) { + return normalize( ( matrix * vec4( dir, 0.0 ) ).xyz ); +} +vec3 inverseTransformDirection( in vec3 dir, in mat4 matrix ) { + return normalize( ( vec4( dir, 0.0 ) * matrix ).xyz ); +} +bool isPerspectiveMatrix( mat4 m ) { + return m[ 2 ][ 3 ] == - 1.0; +} +vec2 equirectUv( in vec3 dir ) { + float u = atan( dir.z, dir.x ) * RECIPROCAL_PI2 + 0.5; + float v = asin( clamp( dir.y, - 1.0, 1.0 ) ) * RECIPROCAL_PI + 0.5; + return vec2( u, v ); +} +vec3 BRDF_Lambert( const in vec3 diffuseColor ) { + return RECIPROCAL_PI * diffuseColor; +} +vec3 F_Schlick( const in vec3 f0, const in float f90, const in float dotVH ) { + float fresnel = exp2( ( - 5.55473 * dotVH - 6.98316 ) * dotVH ); + return f0 * ( 1.0 - fresnel ) + ( f90 * fresnel ); +} +float F_Schlick( const in float f0, const in float f90, const in float dotVH ) { + float fresnel = exp2( ( - 5.55473 * dotVH - 6.98316 ) * dotVH ); + return f0 * ( 1.0 - fresnel ) + ( f90 * fresnel ); +} // validated`,Hg=`#ifdef ENVMAP_TYPE_CUBE_UV + #define cubeUV_minMipLevel 4.0 + #define cubeUV_minTileSize 16.0 + float getFace( vec3 direction ) { + vec3 absDirection = abs( direction ); + float face = - 1.0; + if ( absDirection.x > absDirection.z ) { + if ( absDirection.x > absDirection.y ) + face = direction.x > 0.0 ? 0.0 : 3.0; + else + face = direction.y > 0.0 ? 1.0 : 4.0; + } else { + if ( absDirection.z > absDirection.y ) + face = direction.z > 0.0 ? 2.0 : 5.0; + else + face = direction.y > 0.0 ? 1.0 : 4.0; + } + return face; + } + vec2 getUV( vec3 direction, float face ) { + vec2 uv; + if ( face == 0.0 ) { + uv = vec2( direction.z, direction.y ) / abs( direction.x ); + } else if ( face == 1.0 ) { + uv = vec2( - direction.x, - direction.z ) / abs( direction.y ); + } else if ( face == 2.0 ) { + uv = vec2( - direction.x, direction.y ) / abs( direction.z ); + } else if ( face == 3.0 ) { + uv = vec2( - direction.z, direction.y ) / abs( direction.x ); + } else if ( face == 4.0 ) { + uv = vec2( - direction.x, direction.z ) / abs( direction.y ); + } else { + uv = vec2( direction.x, direction.y ) / abs( direction.z ); + } + return 0.5 * ( uv + 1.0 ); + } + vec3 bilinearCubeUV( sampler2D envMap, vec3 direction, float mipInt ) { + float face = getFace( direction ); + float filterInt = max( cubeUV_minMipLevel - mipInt, 0.0 ); + mipInt = max( mipInt, cubeUV_minMipLevel ); + float faceSize = exp2( mipInt ); + highp vec2 uv = getUV( direction, face ) * ( faceSize - 2.0 ) + 1.0; + if ( face > 2.0 ) { + uv.y += faceSize; + face -= 3.0; + } + uv.x += face * faceSize; + uv.x += filterInt * 3.0 * cubeUV_minTileSize; + uv.y += 4.0 * ( exp2( CUBEUV_MAX_MIP ) - faceSize ); + uv.x *= CUBEUV_TEXEL_WIDTH; + uv.y *= CUBEUV_TEXEL_HEIGHT; + #ifdef texture2DGradEXT + return texture2DGradEXT( envMap, uv, vec2( 0.0 ), vec2( 0.0 ) ).rgb; + #else + return texture2D( envMap, uv ).rgb; + #endif + } + #define cubeUV_r0 1.0 + #define cubeUV_m0 - 2.0 + #define cubeUV_r1 0.8 + #define cubeUV_m1 - 1.0 + #define cubeUV_r4 0.4 + #define cubeUV_m4 2.0 + #define cubeUV_r5 0.305 + #define cubeUV_m5 3.0 + #define cubeUV_r6 0.21 + #define cubeUV_m6 4.0 + float roughnessToMip( float roughness ) { + float mip = 0.0; + if ( roughness >= cubeUV_r1 ) { + mip = ( cubeUV_r0 - roughness ) * ( cubeUV_m1 - cubeUV_m0 ) / ( cubeUV_r0 - cubeUV_r1 ) + cubeUV_m0; + } else if ( roughness >= cubeUV_r4 ) { + mip = ( cubeUV_r1 - roughness ) * ( cubeUV_m4 - cubeUV_m1 ) / ( cubeUV_r1 - cubeUV_r4 ) + cubeUV_m1; + } else if ( roughness >= cubeUV_r5 ) { + mip = ( cubeUV_r4 - roughness ) * ( cubeUV_m5 - cubeUV_m4 ) / ( cubeUV_r4 - cubeUV_r5 ) + cubeUV_m4; + } else if ( roughness >= cubeUV_r6 ) { + mip = ( cubeUV_r5 - roughness ) * ( cubeUV_m6 - cubeUV_m5 ) / ( cubeUV_r5 - cubeUV_r6 ) + cubeUV_m5; + } else { + mip = - 2.0 * log2( 1.16 * roughness ); } + return mip; + } + vec4 textureCubeUV( sampler2D envMap, vec3 sampleDir, float roughness ) { + float mip = clamp( roughnessToMip( roughness ), cubeUV_m0, CUBEUV_MAX_MIP ); + float mipF = fract( mip ); + float mipInt = floor( mip ); + vec3 color0 = bilinearCubeUV( envMap, sampleDir, mipInt ); + if ( mipF == 0.0 ) { + return vec4( color0, 1.0 ); + } else { + vec3 color1 = bilinearCubeUV( envMap, sampleDir, mipInt + 1.0 ); + return vec4( mix( color0, color1, mipF ), 1.0 ); + } + } +#endif`,Wg=`vec3 transformedNormal = objectNormal; +#ifdef USE_TANGENT + vec3 transformedTangent = objectTangent; +#endif +#ifdef USE_BATCHING + mat3 bm = mat3( batchingMatrix ); + transformedNormal /= vec3( dot( bm[ 0 ], bm[ 0 ] ), dot( bm[ 1 ], bm[ 1 ] ), dot( bm[ 2 ], bm[ 2 ] ) ); + transformedNormal = bm * transformedNormal; + #ifdef USE_TANGENT + transformedTangent = bm * transformedTangent; + #endif +#endif +#ifdef USE_INSTANCING + mat3 im = mat3( instanceMatrix ); + transformedNormal /= vec3( dot( im[ 0 ], im[ 0 ] ), dot( im[ 1 ], im[ 1 ] ), dot( im[ 2 ], im[ 2 ] ) ); + transformedNormal = im * transformedNormal; + #ifdef USE_TANGENT + transformedTangent = im * transformedTangent; + #endif +#endif +transformedNormal = normalMatrix * transformedNormal; +#ifdef FLIP_SIDED + transformedNormal = - transformedNormal; +#endif +#ifdef USE_TANGENT + transformedTangent = ( modelViewMatrix * vec4( transformedTangent, 0.0 ) ).xyz; + #ifdef FLIP_SIDED + transformedTangent = - transformedTangent; + #endif +#endif`,Xg=`#ifdef USE_DISPLACEMENTMAP + uniform sampler2D displacementMap; + uniform float displacementScale; + uniform float displacementBias; +#endif`,qg=`#ifdef USE_DISPLACEMENTMAP + transformed += normalize( objectNormal ) * ( texture2D( displacementMap, vDisplacementMapUv ).x * displacementScale + displacementBias ); +#endif`,Yg=`#ifdef USE_EMISSIVEMAP + vec4 emissiveColor = texture2D( emissiveMap, vEmissiveMapUv ); + #ifdef DECODE_VIDEO_TEXTURE_EMISSIVE + emissiveColor = sRGBTransferEOTF( emissiveColor ); + #endif + totalEmissiveRadiance *= emissiveColor.rgb; +#endif`,Zg=`#ifdef USE_EMISSIVEMAP + uniform sampler2D emissiveMap; +#endif`,$g="gl_FragColor = linearToOutputTexel( gl_FragColor );",Kg=`vec4 LinearTransferOETF( in vec4 value ) { + return value; +} +vec4 sRGBTransferEOTF( in vec4 value ) { + return vec4( mix( pow( value.rgb * 0.9478672986 + vec3( 0.0521327014 ), vec3( 2.4 ) ), value.rgb * 0.0773993808, vec3( lessThanEqual( value.rgb, vec3( 0.04045 ) ) ) ), value.a ); +} +vec4 sRGBTransferOETF( in vec4 value ) { + return vec4( mix( pow( value.rgb, vec3( 0.41666 ) ) * 1.055 - vec3( 0.055 ), value.rgb * 12.92, vec3( lessThanEqual( value.rgb, vec3( 0.0031308 ) ) ) ), value.a ); +}`,Jg=`#ifdef USE_ENVMAP + #ifdef ENV_WORLDPOS + vec3 cameraToFrag; + if ( isOrthographic ) { + cameraToFrag = normalize( vec3( - viewMatrix[ 0 ][ 2 ], - viewMatrix[ 1 ][ 2 ], - viewMatrix[ 2 ][ 2 ] ) ); + } else { + cameraToFrag = normalize( vWorldPosition - cameraPosition ); + } + vec3 worldNormal = inverseTransformDirection( normal, viewMatrix ); + #ifdef ENVMAP_MODE_REFLECTION + vec3 reflectVec = reflect( cameraToFrag, worldNormal ); + #else + vec3 reflectVec = refract( cameraToFrag, worldNormal, refractionRatio ); + #endif + #else + vec3 reflectVec = vReflect; + #endif + #ifdef ENVMAP_TYPE_CUBE + vec4 envColor = textureCube( envMap, envMapRotation * reflectVec ); + #ifdef ENVMAP_BLENDING_MULTIPLY + outgoingLight = mix( outgoingLight, outgoingLight * envColor.xyz, specularStrength * reflectivity ); + #elif defined( ENVMAP_BLENDING_MIX ) + outgoingLight = mix( outgoingLight, envColor.xyz, specularStrength * reflectivity ); + #elif defined( ENVMAP_BLENDING_ADD ) + outgoingLight += envColor.xyz * specularStrength * reflectivity; + #endif + #endif +#endif`,jg=`#ifdef USE_ENVMAP + uniform float envMapIntensity; + uniform mat3 envMapRotation; + #ifdef ENVMAP_TYPE_CUBE + uniform samplerCube envMap; + #else + uniform sampler2D envMap; + #endif +#endif`,Qg=`#ifdef USE_ENVMAP + uniform float reflectivity; + #if defined( USE_BUMPMAP ) || defined( USE_NORMALMAP ) || defined( PHONG ) || defined( LAMBERT ) + #define ENV_WORLDPOS + #endif + #ifdef ENV_WORLDPOS + varying vec3 vWorldPosition; + uniform float refractionRatio; + #else + varying vec3 vReflect; + #endif +#endif`,e0=`#ifdef USE_ENVMAP + #if defined( USE_BUMPMAP ) || defined( USE_NORMALMAP ) || defined( PHONG ) || defined( LAMBERT ) + #define ENV_WORLDPOS + #endif + #ifdef ENV_WORLDPOS + + varying vec3 vWorldPosition; + #else + varying vec3 vReflect; + uniform float refractionRatio; + #endif +#endif`,t0=`#ifdef USE_ENVMAP + #ifdef ENV_WORLDPOS + vWorldPosition = worldPosition.xyz; + #else + vec3 cameraToVertex; + if ( isOrthographic ) { + cameraToVertex = normalize( vec3( - viewMatrix[ 0 ][ 2 ], - viewMatrix[ 1 ][ 2 ], - viewMatrix[ 2 ][ 2 ] ) ); + } else { + cameraToVertex = normalize( worldPosition.xyz - cameraPosition ); + } + vec3 worldNormal = inverseTransformDirection( transformedNormal, viewMatrix ); + #ifdef ENVMAP_MODE_REFLECTION + vReflect = reflect( cameraToVertex, worldNormal ); + #else + vReflect = refract( cameraToVertex, worldNormal, refractionRatio ); + #endif + #endif +#endif`,n0=`#ifdef USE_FOG + vFogDepth = - mvPosition.z; +#endif`,i0=`#ifdef USE_FOG + varying float vFogDepth; +#endif`,s0=`#ifdef USE_FOG + #ifdef FOG_EXP2 + float fogFactor = 1.0 - exp( - fogDensity * fogDensity * vFogDepth * vFogDepth ); + #else + float fogFactor = smoothstep( fogNear, fogFar, vFogDepth ); + #endif + gl_FragColor.rgb = mix( gl_FragColor.rgb, fogColor, fogFactor ); +#endif`,r0=`#ifdef USE_FOG + uniform vec3 fogColor; + varying float vFogDepth; + #ifdef FOG_EXP2 + uniform float fogDensity; + #else + uniform float fogNear; + uniform float fogFar; + #endif +#endif`,o0=`#ifdef USE_GRADIENTMAP + uniform sampler2D gradientMap; +#endif +vec3 getGradientIrradiance( vec3 normal, vec3 lightDirection ) { + float dotNL = dot( normal, lightDirection ); + vec2 coord = vec2( dotNL * 0.5 + 0.5, 0.0 ); + #ifdef USE_GRADIENTMAP + return vec3( texture2D( gradientMap, coord ).r ); + #else + vec2 fw = fwidth( coord ) * 0.5; + return mix( vec3( 0.7 ), vec3( 1.0 ), smoothstep( 0.7 - fw.x, 0.7 + fw.x, coord.x ) ); + #endif +}`,a0=`#ifdef USE_LIGHTMAP + uniform sampler2D lightMap; + uniform float lightMapIntensity; +#endif`,l0=`LambertMaterial material; +material.diffuseColor = diffuseColor.rgb; +material.specularStrength = specularStrength;`,c0=`varying vec3 vViewPosition; +struct LambertMaterial { + vec3 diffuseColor; + float specularStrength; }; +void RE_Direct_Lambert( const in IncidentLight directLight, const in vec3 geometryPosition, const in vec3 geometryNormal, const in vec3 geometryViewDir, const in vec3 geometryClearcoatNormal, const in LambertMaterial material, inout ReflectedLight reflectedLight ) { + float dotNL = saturate( dot( geometryNormal, directLight.direction ) ); + vec3 irradiance = dotNL * directLight.color; + reflectedLight.directDiffuse += irradiance * BRDF_Lambert( material.diffuseColor ); +} +void RE_IndirectDiffuse_Lambert( const in vec3 irradiance, const in vec3 geometryPosition, const in vec3 geometryNormal, const in vec3 geometryViewDir, const in vec3 geometryClearcoatNormal, const in LambertMaterial material, inout ReflectedLight reflectedLight ) { + reflectedLight.indirectDiffuse += irradiance * BRDF_Lambert( material.diffuseColor ); +} +#define RE_Direct RE_Direct_Lambert +#define RE_IndirectDiffuse RE_IndirectDiffuse_Lambert`,h0=`uniform bool receiveShadow; +uniform vec3 ambientLightColor; +#if defined( USE_LIGHT_PROBES ) + uniform vec3 lightProbe[ 9 ]; +#endif +vec3 shGetIrradianceAt( in vec3 normal, in vec3 shCoefficients[ 9 ] ) { + float x = normal.x, y = normal.y, z = normal.z; + vec3 result = shCoefficients[ 0 ] * 0.886227; + result += shCoefficients[ 1 ] * 2.0 * 0.511664 * y; + result += shCoefficients[ 2 ] * 2.0 * 0.511664 * z; + result += shCoefficients[ 3 ] * 2.0 * 0.511664 * x; + result += shCoefficients[ 4 ] * 2.0 * 0.429043 * x * y; + result += shCoefficients[ 5 ] * 2.0 * 0.429043 * y * z; + result += shCoefficients[ 6 ] * ( 0.743125 * z * z - 0.247708 ); + result += shCoefficients[ 7 ] * 2.0 * 0.429043 * x * z; + result += shCoefficients[ 8 ] * 0.429043 * ( x * x - y * y ); + return result; +} +vec3 getLightProbeIrradiance( const in vec3 lightProbe[ 9 ], const in vec3 normal ) { + vec3 worldNormal = inverseTransformDirection( normal, viewMatrix ); + vec3 irradiance = shGetIrradianceAt( worldNormal, lightProbe ); + return irradiance; +} +vec3 getAmbientLightIrradiance( const in vec3 ambientLightColor ) { + vec3 irradiance = ambientLightColor; + return irradiance; +} +float getDistanceAttenuation( const in float lightDistance, const in float cutoffDistance, const in float decayExponent ) { + float distanceFalloff = 1.0 / max( pow( lightDistance, decayExponent ), 0.01 ); + if ( cutoffDistance > 0.0 ) { + distanceFalloff *= pow2( saturate( 1.0 - pow4( lightDistance / cutoffDistance ) ) ); + } + return distanceFalloff; +} +float getSpotAttenuation( const in float coneCosine, const in float penumbraCosine, const in float angleCosine ) { + return smoothstep( coneCosine, penumbraCosine, angleCosine ); +} +#if NUM_DIR_LIGHTS > 0 + struct DirectionalLight { + vec3 direction; + vec3 color; + }; + uniform DirectionalLight directionalLights[ NUM_DIR_LIGHTS ]; + void getDirectionalLightInfo( const in DirectionalLight directionalLight, out IncidentLight light ) { + light.color = directionalLight.color; + light.direction = directionalLight.direction; + light.visible = true; + } +#endif +#if NUM_POINT_LIGHTS > 0 + struct PointLight { + vec3 position; + vec3 color; + float distance; + float decay; + }; + uniform PointLight pointLights[ NUM_POINT_LIGHTS ]; + void getPointLightInfo( const in PointLight pointLight, const in vec3 geometryPosition, out IncidentLight light ) { + vec3 lVector = pointLight.position - geometryPosition; + light.direction = normalize( lVector ); + float lightDistance = length( lVector ); + light.color = pointLight.color; + light.color *= getDistanceAttenuation( lightDistance, pointLight.distance, pointLight.decay ); + light.visible = ( light.color != vec3( 0.0 ) ); + } +#endif +#if NUM_SPOT_LIGHTS > 0 + struct SpotLight { + vec3 position; + vec3 direction; + vec3 color; + float distance; + float decay; + float coneCos; + float penumbraCos; + }; + uniform SpotLight spotLights[ NUM_SPOT_LIGHTS ]; + void getSpotLightInfo( const in SpotLight spotLight, const in vec3 geometryPosition, out IncidentLight light ) { + vec3 lVector = spotLight.position - geometryPosition; + light.direction = normalize( lVector ); + float angleCos = dot( light.direction, spotLight.direction ); + float spotAttenuation = getSpotAttenuation( spotLight.coneCos, spotLight.penumbraCos, angleCos ); + if ( spotAttenuation > 0.0 ) { + float lightDistance = length( lVector ); + light.color = spotLight.color * spotAttenuation; + light.color *= getDistanceAttenuation( lightDistance, spotLight.distance, spotLight.decay ); + light.visible = ( light.color != vec3( 0.0 ) ); + } else { + light.color = vec3( 0.0 ); + light.visible = false; + } + } +#endif +#if NUM_RECT_AREA_LIGHTS > 0 + struct RectAreaLight { + vec3 color; + vec3 position; + vec3 halfWidth; + vec3 halfHeight; + }; + uniform sampler2D ltc_1; uniform sampler2D ltc_2; + uniform RectAreaLight rectAreaLights[ NUM_RECT_AREA_LIGHTS ]; +#endif +#if NUM_HEMI_LIGHTS > 0 + struct HemisphereLight { + vec3 direction; + vec3 skyColor; + vec3 groundColor; + }; + uniform HemisphereLight hemisphereLights[ NUM_HEMI_LIGHTS ]; + vec3 getHemisphereLightIrradiance( const in HemisphereLight hemiLight, const in vec3 normal ) { + float dotNL = dot( normal, hemiLight.direction ); + float hemiDiffuseWeight = 0.5 * dotNL + 0.5; + vec3 irradiance = mix( hemiLight.groundColor, hemiLight.skyColor, hemiDiffuseWeight ); + return irradiance; + } +#endif +#include `,u0=`#ifdef USE_ENVMAP + vec3 getIBLIrradiance( const in vec3 normal ) { + #ifdef ENVMAP_TYPE_CUBE_UV + vec3 worldNormal = inverseTransformDirection( normal, viewMatrix ); + vec4 envMapColor = textureCubeUV( envMap, envMapRotation * worldNormal, 1.0 ); + return PI * envMapColor.rgb * envMapIntensity; + #else + return vec3( 0.0 ); + #endif + } + vec3 getIBLRadiance( const in vec3 viewDir, const in vec3 normal, const in float roughness ) { + #ifdef ENVMAP_TYPE_CUBE_UV + vec3 reflectVec = reflect( - viewDir, normal ); + reflectVec = normalize( mix( reflectVec, normal, pow4( roughness ) ) ); + reflectVec = inverseTransformDirection( reflectVec, viewMatrix ); + vec4 envMapColor = textureCubeUV( envMap, envMapRotation * reflectVec, roughness ); + return envMapColor.rgb * envMapIntensity; + #else + return vec3( 0.0 ); + #endif + } + #ifdef USE_ANISOTROPY + vec3 getIBLAnisotropyRadiance( const in vec3 viewDir, const in vec3 normal, const in float roughness, const in vec3 bitangent, const in float anisotropy ) { + #ifdef ENVMAP_TYPE_CUBE_UV + vec3 bentNormal = cross( bitangent, viewDir ); + bentNormal = normalize( cross( bentNormal, bitangent ) ); + bentNormal = normalize( mix( bentNormal, normal, pow2( pow2( 1.0 - anisotropy * ( 1.0 - roughness ) ) ) ) ); + return getIBLRadiance( viewDir, bentNormal, roughness ); + #else + return vec3( 0.0 ); + #endif + } + #endif +#endif`,d0=`ToonMaterial material; +material.diffuseColor = diffuseColor.rgb;`,f0=`varying vec3 vViewPosition; +struct ToonMaterial { + vec3 diffuseColor; +}; +void RE_Direct_Toon( const in IncidentLight directLight, const in vec3 geometryPosition, const in vec3 geometryNormal, const in vec3 geometryViewDir, const in vec3 geometryClearcoatNormal, const in ToonMaterial material, inout ReflectedLight reflectedLight ) { + vec3 irradiance = getGradientIrradiance( geometryNormal, directLight.direction ) * directLight.color; + reflectedLight.directDiffuse += irradiance * BRDF_Lambert( material.diffuseColor ); +} +void RE_IndirectDiffuse_Toon( const in vec3 irradiance, const in vec3 geometryPosition, const in vec3 geometryNormal, const in vec3 geometryViewDir, const in vec3 geometryClearcoatNormal, const in ToonMaterial material, inout ReflectedLight reflectedLight ) { + reflectedLight.indirectDiffuse += irradiance * BRDF_Lambert( material.diffuseColor ); +} +#define RE_Direct RE_Direct_Toon +#define RE_IndirectDiffuse RE_IndirectDiffuse_Toon`,p0=`BlinnPhongMaterial material; +material.diffuseColor = diffuseColor.rgb; +material.specularColor = specular; +material.specularShininess = shininess; +material.specularStrength = specularStrength;`,m0=`varying vec3 vViewPosition; +struct BlinnPhongMaterial { + vec3 diffuseColor; + vec3 specularColor; + float specularShininess; + float specularStrength; +}; +void RE_Direct_BlinnPhong( const in IncidentLight directLight, const in vec3 geometryPosition, const in vec3 geometryNormal, const in vec3 geometryViewDir, const in vec3 geometryClearcoatNormal, const in BlinnPhongMaterial material, inout ReflectedLight reflectedLight ) { + float dotNL = saturate( dot( geometryNormal, directLight.direction ) ); + vec3 irradiance = dotNL * directLight.color; + reflectedLight.directDiffuse += irradiance * BRDF_Lambert( material.diffuseColor ); + reflectedLight.directSpecular += irradiance * BRDF_BlinnPhong( directLight.direction, geometryViewDir, geometryNormal, material.specularColor, material.specularShininess ) * material.specularStrength; +} +void RE_IndirectDiffuse_BlinnPhong( const in vec3 irradiance, const in vec3 geometryPosition, const in vec3 geometryNormal, const in vec3 geometryViewDir, const in vec3 geometryClearcoatNormal, const in BlinnPhongMaterial material, inout ReflectedLight reflectedLight ) { + reflectedLight.indirectDiffuse += irradiance * BRDF_Lambert( material.diffuseColor ); +} +#define RE_Direct RE_Direct_BlinnPhong +#define RE_IndirectDiffuse RE_IndirectDiffuse_BlinnPhong`,g0=`PhysicalMaterial material; +material.diffuseColor = diffuseColor.rgb; +material.diffuseContribution = diffuseColor.rgb * ( 1.0 - metalnessFactor ); +material.metalness = metalnessFactor; +vec3 dxy = max( abs( dFdx( nonPerturbedNormal ) ), abs( dFdy( nonPerturbedNormal ) ) ); +float geometryRoughness = max( max( dxy.x, dxy.y ), dxy.z ); +material.roughness = max( roughnessFactor, 0.0525 );material.roughness += geometryRoughness; +material.roughness = min( material.roughness, 1.0 ); +#ifdef IOR + material.ior = ior; + #ifdef USE_SPECULAR + float specularIntensityFactor = specularIntensity; + vec3 specularColorFactor = specularColor; + #ifdef USE_SPECULAR_COLORMAP + specularColorFactor *= texture2D( specularColorMap, vSpecularColorMapUv ).rgb; + #endif + #ifdef USE_SPECULAR_INTENSITYMAP + specularIntensityFactor *= texture2D( specularIntensityMap, vSpecularIntensityMapUv ).a; + #endif + material.specularF90 = mix( specularIntensityFactor, 1.0, metalnessFactor ); + #else + float specularIntensityFactor = 1.0; + vec3 specularColorFactor = vec3( 1.0 ); + material.specularF90 = 1.0; + #endif + material.specularColor = min( pow2( ( material.ior - 1.0 ) / ( material.ior + 1.0 ) ) * specularColorFactor, vec3( 1.0 ) ) * specularIntensityFactor; + material.specularColorBlended = mix( material.specularColor, diffuseColor.rgb, metalnessFactor ); +#else + material.specularColor = vec3( 0.04 ); + material.specularColorBlended = mix( material.specularColor, diffuseColor.rgb, metalnessFactor ); + material.specularF90 = 1.0; +#endif +#ifdef USE_CLEARCOAT + material.clearcoat = clearcoat; + material.clearcoatRoughness = clearcoatRoughness; + material.clearcoatF0 = vec3( 0.04 ); + material.clearcoatF90 = 1.0; + #ifdef USE_CLEARCOATMAP + material.clearcoat *= texture2D( clearcoatMap, vClearcoatMapUv ).x; + #endif + #ifdef USE_CLEARCOAT_ROUGHNESSMAP + material.clearcoatRoughness *= texture2D( clearcoatRoughnessMap, vClearcoatRoughnessMapUv ).y; + #endif + material.clearcoat = saturate( material.clearcoat ); material.clearcoatRoughness = max( material.clearcoatRoughness, 0.0525 ); + material.clearcoatRoughness += geometryRoughness; + material.clearcoatRoughness = min( material.clearcoatRoughness, 1.0 ); +#endif +#ifdef USE_DISPERSION + material.dispersion = dispersion; +#endif +#ifdef USE_IRIDESCENCE + material.iridescence = iridescence; + material.iridescenceIOR = iridescenceIOR; + #ifdef USE_IRIDESCENCEMAP + material.iridescence *= texture2D( iridescenceMap, vIridescenceMapUv ).r; + #endif + #ifdef USE_IRIDESCENCE_THICKNESSMAP + material.iridescenceThickness = (iridescenceThicknessMaximum - iridescenceThicknessMinimum) * texture2D( iridescenceThicknessMap, vIridescenceThicknessMapUv ).g + iridescenceThicknessMinimum; + #else + material.iridescenceThickness = iridescenceThicknessMaximum; + #endif +#endif +#ifdef USE_SHEEN + material.sheenColor = sheenColor; + #ifdef USE_SHEEN_COLORMAP + material.sheenColor *= texture2D( sheenColorMap, vSheenColorMapUv ).rgb; + #endif + material.sheenRoughness = clamp( sheenRoughness, 0.0001, 1.0 ); + #ifdef USE_SHEEN_ROUGHNESSMAP + material.sheenRoughness *= texture2D( sheenRoughnessMap, vSheenRoughnessMapUv ).a; + #endif +#endif +#ifdef USE_ANISOTROPY + #ifdef USE_ANISOTROPYMAP + mat2 anisotropyMat = mat2( anisotropyVector.x, anisotropyVector.y, - anisotropyVector.y, anisotropyVector.x ); + vec3 anisotropyPolar = texture2D( anisotropyMap, vAnisotropyMapUv ).rgb; + vec2 anisotropyV = anisotropyMat * normalize( 2.0 * anisotropyPolar.rg - vec2( 1.0 ) ) * anisotropyPolar.b; + #else + vec2 anisotropyV = anisotropyVector; + #endif + material.anisotropy = length( anisotropyV ); + if( material.anisotropy == 0.0 ) { + anisotropyV = vec2( 1.0, 0.0 ); + } else { + anisotropyV /= material.anisotropy; + material.anisotropy = saturate( material.anisotropy ); + } + material.alphaT = mix( pow2( material.roughness ), 1.0, pow2( material.anisotropy ) ); + material.anisotropyT = tbn[ 0 ] * anisotropyV.x + tbn[ 1 ] * anisotropyV.y; + material.anisotropyB = tbn[ 1 ] * anisotropyV.x - tbn[ 0 ] * anisotropyV.y; +#endif`,x0=`uniform sampler2D dfgLUT; +struct PhysicalMaterial { + vec3 diffuseColor; + vec3 diffuseContribution; + vec3 specularColor; + vec3 specularColorBlended; + float roughness; + float metalness; + float specularF90; + float dispersion; + #ifdef USE_CLEARCOAT + float clearcoat; + float clearcoatRoughness; + vec3 clearcoatF0; + float clearcoatF90; + #endif + #ifdef USE_IRIDESCENCE + float iridescence; + float iridescenceIOR; + float iridescenceThickness; + vec3 iridescenceFresnel; + vec3 iridescenceF0; + vec3 iridescenceFresnelDielectric; + vec3 iridescenceFresnelMetallic; + #endif + #ifdef USE_SHEEN + vec3 sheenColor; + float sheenRoughness; + #endif + #ifdef IOR + float ior; + #endif + #ifdef USE_TRANSMISSION + float transmission; + float transmissionAlpha; + float thickness; + float attenuationDistance; + vec3 attenuationColor; + #endif + #ifdef USE_ANISOTROPY + float anisotropy; + float alphaT; + vec3 anisotropyT; + vec3 anisotropyB; + #endif +}; +vec3 clearcoatSpecularDirect = vec3( 0.0 ); +vec3 clearcoatSpecularIndirect = vec3( 0.0 ); +vec3 sheenSpecularDirect = vec3( 0.0 ); +vec3 sheenSpecularIndirect = vec3(0.0 ); +vec3 Schlick_to_F0( const in vec3 f, const in float f90, const in float dotVH ) { + float x = clamp( 1.0 - dotVH, 0.0, 1.0 ); + float x2 = x * x; + float x5 = clamp( x * x2 * x2, 0.0, 0.9999 ); + return ( f - vec3( f90 ) * x5 ) / ( 1.0 - x5 ); +} +float V_GGX_SmithCorrelated( const in float alpha, const in float dotNL, const in float dotNV ) { + float a2 = pow2( alpha ); + float gv = dotNL * sqrt( a2 + ( 1.0 - a2 ) * pow2( dotNV ) ); + float gl = dotNV * sqrt( a2 + ( 1.0 - a2 ) * pow2( dotNL ) ); + return 0.5 / max( gv + gl, EPSILON ); +} +float D_GGX( const in float alpha, const in float dotNH ) { + float a2 = pow2( alpha ); + float denom = pow2( dotNH ) * ( a2 - 1.0 ) + 1.0; + return RECIPROCAL_PI * a2 / pow2( denom ); +} +#ifdef USE_ANISOTROPY + float V_GGX_SmithCorrelated_Anisotropic( const in float alphaT, const in float alphaB, const in float dotTV, const in float dotBV, const in float dotTL, const in float dotBL, const in float dotNV, const in float dotNL ) { + float gv = dotNL * length( vec3( alphaT * dotTV, alphaB * dotBV, dotNV ) ); + float gl = dotNV * length( vec3( alphaT * dotTL, alphaB * dotBL, dotNL ) ); + return 0.5 / max( gv + gl, EPSILON ); + } + float D_GGX_Anisotropic( const in float alphaT, const in float alphaB, const in float dotNH, const in float dotTH, const in float dotBH ) { + float a2 = alphaT * alphaB; + highp vec3 v = vec3( alphaB * dotTH, alphaT * dotBH, a2 * dotNH ); + highp float v2 = dot( v, v ); + float w2 = a2 / v2; + return RECIPROCAL_PI * a2 * pow2 ( w2 ); + } +#endif +#ifdef USE_CLEARCOAT + vec3 BRDF_GGX_Clearcoat( const in vec3 lightDir, const in vec3 viewDir, const in vec3 normal, const in PhysicalMaterial material) { + vec3 f0 = material.clearcoatF0; + float f90 = material.clearcoatF90; + float roughness = material.clearcoatRoughness; + float alpha = pow2( roughness ); + vec3 halfDir = normalize( lightDir + viewDir ); + float dotNL = saturate( dot( normal, lightDir ) ); + float dotNV = saturate( dot( normal, viewDir ) ); + float dotNH = saturate( dot( normal, halfDir ) ); + float dotVH = saturate( dot( viewDir, halfDir ) ); + vec3 F = F_Schlick( f0, f90, dotVH ); + float V = V_GGX_SmithCorrelated( alpha, dotNL, dotNV ); + float D = D_GGX( alpha, dotNH ); + return F * ( V * D ); + } +#endif +vec3 BRDF_GGX( const in vec3 lightDir, const in vec3 viewDir, const in vec3 normal, const in PhysicalMaterial material ) { + vec3 f0 = material.specularColorBlended; + float f90 = material.specularF90; + float roughness = material.roughness; + float alpha = pow2( roughness ); + vec3 halfDir = normalize( lightDir + viewDir ); + float dotNL = saturate( dot( normal, lightDir ) ); + float dotNV = saturate( dot( normal, viewDir ) ); + float dotNH = saturate( dot( normal, halfDir ) ); + float dotVH = saturate( dot( viewDir, halfDir ) ); + vec3 F = F_Schlick( f0, f90, dotVH ); + #ifdef USE_IRIDESCENCE + F = mix( F, material.iridescenceFresnel, material.iridescence ); + #endif + #ifdef USE_ANISOTROPY + float dotTL = dot( material.anisotropyT, lightDir ); + float dotTV = dot( material.anisotropyT, viewDir ); + float dotTH = dot( material.anisotropyT, halfDir ); + float dotBL = dot( material.anisotropyB, lightDir ); + float dotBV = dot( material.anisotropyB, viewDir ); + float dotBH = dot( material.anisotropyB, halfDir ); + float V = V_GGX_SmithCorrelated_Anisotropic( material.alphaT, alpha, dotTV, dotBV, dotTL, dotBL, dotNV, dotNL ); + float D = D_GGX_Anisotropic( material.alphaT, alpha, dotNH, dotTH, dotBH ); + #else + float V = V_GGX_SmithCorrelated( alpha, dotNL, dotNV ); + float D = D_GGX( alpha, dotNH ); + #endif + return F * ( V * D ); +} +vec2 LTC_Uv( const in vec3 N, const in vec3 V, const in float roughness ) { + const float LUT_SIZE = 64.0; + const float LUT_SCALE = ( LUT_SIZE - 1.0 ) / LUT_SIZE; + const float LUT_BIAS = 0.5 / LUT_SIZE; + float dotNV = saturate( dot( N, V ) ); + vec2 uv = vec2( roughness, sqrt( 1.0 - dotNV ) ); + uv = uv * LUT_SCALE + LUT_BIAS; + return uv; +} +float LTC_ClippedSphereFormFactor( const in vec3 f ) { + float l = length( f ); + return max( ( l * l + f.z ) / ( l + 1.0 ), 0.0 ); +} +vec3 LTC_EdgeVectorFormFactor( const in vec3 v1, const in vec3 v2 ) { + float x = dot( v1, v2 ); + float y = abs( x ); + float a = 0.8543985 + ( 0.4965155 + 0.0145206 * y ) * y; + float b = 3.4175940 + ( 4.1616724 + y ) * y; + float v = a / b; + float theta_sintheta = ( x > 0.0 ) ? v : 0.5 * inversesqrt( max( 1.0 - x * x, 1e-7 ) ) - v; + return cross( v1, v2 ) * theta_sintheta; +} +vec3 LTC_Evaluate( const in vec3 N, const in vec3 V, const in vec3 P, const in mat3 mInv, const in vec3 rectCoords[ 4 ] ) { + vec3 v1 = rectCoords[ 1 ] - rectCoords[ 0 ]; + vec3 v2 = rectCoords[ 3 ] - rectCoords[ 0 ]; + vec3 lightNormal = cross( v1, v2 ); + if( dot( lightNormal, P - rectCoords[ 0 ] ) < 0.0 ) return vec3( 0.0 ); + vec3 T1, T2; + T1 = normalize( V - N * dot( V, N ) ); + T2 = - cross( N, T1 ); + mat3 mat = mInv * transpose( mat3( T1, T2, N ) ); + vec3 coords[ 4 ]; + coords[ 0 ] = mat * ( rectCoords[ 0 ] - P ); + coords[ 1 ] = mat * ( rectCoords[ 1 ] - P ); + coords[ 2 ] = mat * ( rectCoords[ 2 ] - P ); + coords[ 3 ] = mat * ( rectCoords[ 3 ] - P ); + coords[ 0 ] = normalize( coords[ 0 ] ); + coords[ 1 ] = normalize( coords[ 1 ] ); + coords[ 2 ] = normalize( coords[ 2 ] ); + coords[ 3 ] = normalize( coords[ 3 ] ); + vec3 vectorFormFactor = vec3( 0.0 ); + vectorFormFactor += LTC_EdgeVectorFormFactor( coords[ 0 ], coords[ 1 ] ); + vectorFormFactor += LTC_EdgeVectorFormFactor( coords[ 1 ], coords[ 2 ] ); + vectorFormFactor += LTC_EdgeVectorFormFactor( coords[ 2 ], coords[ 3 ] ); + vectorFormFactor += LTC_EdgeVectorFormFactor( coords[ 3 ], coords[ 0 ] ); + float result = LTC_ClippedSphereFormFactor( vectorFormFactor ); + return vec3( result ); +} +#if defined( USE_SHEEN ) +float D_Charlie( float roughness, float dotNH ) { + float alpha = pow2( roughness ); + float invAlpha = 1.0 / alpha; + float cos2h = dotNH * dotNH; + float sin2h = max( 1.0 - cos2h, 0.0078125 ); + return ( 2.0 + invAlpha ) * pow( sin2h, invAlpha * 0.5 ) / ( 2.0 * PI ); +} +float V_Neubelt( float dotNV, float dotNL ) { + return saturate( 1.0 / ( 4.0 * ( dotNL + dotNV - dotNL * dotNV ) ) ); +} +vec3 BRDF_Sheen( const in vec3 lightDir, const in vec3 viewDir, const in vec3 normal, vec3 sheenColor, const in float sheenRoughness ) { + vec3 halfDir = normalize( lightDir + viewDir ); + float dotNL = saturate( dot( normal, lightDir ) ); + float dotNV = saturate( dot( normal, viewDir ) ); + float dotNH = saturate( dot( normal, halfDir ) ); + float D = D_Charlie( sheenRoughness, dotNH ); + float V = V_Neubelt( dotNV, dotNL ); + return sheenColor * ( D * V ); +} +#endif +float IBLSheenBRDF( const in vec3 normal, const in vec3 viewDir, const in float roughness ) { + float dotNV = saturate( dot( normal, viewDir ) ); + float r2 = roughness * roughness; + float rInv = 1.0 / ( roughness + 0.1 ); + float a = -1.9362 + 1.0678 * roughness + 0.4573 * r2 - 0.8469 * rInv; + float b = -0.6014 + 0.5538 * roughness - 0.4670 * r2 - 0.1255 * rInv; + float DG = exp( a * dotNV + b ); + return saturate( DG ); +} +vec3 EnvironmentBRDF( const in vec3 normal, const in vec3 viewDir, const in vec3 specularColor, const in float specularF90, const in float roughness ) { + float dotNV = saturate( dot( normal, viewDir ) ); + vec2 fab = texture2D( dfgLUT, vec2( roughness, dotNV ) ).rg; + return specularColor * fab.x + specularF90 * fab.y; +} +#ifdef USE_IRIDESCENCE +void computeMultiscatteringIridescence( const in vec3 normal, const in vec3 viewDir, const in vec3 specularColor, const in float specularF90, const in float iridescence, const in vec3 iridescenceF0, const in float roughness, inout vec3 singleScatter, inout vec3 multiScatter ) { +#else +void computeMultiscattering( const in vec3 normal, const in vec3 viewDir, const in vec3 specularColor, const in float specularF90, const in float roughness, inout vec3 singleScatter, inout vec3 multiScatter ) { +#endif + float dotNV = saturate( dot( normal, viewDir ) ); + vec2 fab = texture2D( dfgLUT, vec2( roughness, dotNV ) ).rg; + #ifdef USE_IRIDESCENCE + vec3 Fr = mix( specularColor, iridescenceF0, iridescence ); + #else + vec3 Fr = specularColor; + #endif + vec3 FssEss = Fr * fab.x + specularF90 * fab.y; + float Ess = fab.x + fab.y; + float Ems = 1.0 - Ess; + vec3 Favg = Fr + ( 1.0 - Fr ) * 0.047619; vec3 Fms = FssEss * Favg / ( 1.0 - Ems * Favg ); + singleScatter += FssEss; + multiScatter += Fms * Ems; +} +vec3 BRDF_GGX_Multiscatter( const in vec3 lightDir, const in vec3 viewDir, const in vec3 normal, const in PhysicalMaterial material ) { + vec3 singleScatter = BRDF_GGX( lightDir, viewDir, normal, material ); + float dotNL = saturate( dot( normal, lightDir ) ); + float dotNV = saturate( dot( normal, viewDir ) ); + vec2 dfgV = texture2D( dfgLUT, vec2( material.roughness, dotNV ) ).rg; + vec2 dfgL = texture2D( dfgLUT, vec2( material.roughness, dotNL ) ).rg; + vec3 FssEss_V = material.specularColorBlended * dfgV.x + material.specularF90 * dfgV.y; + vec3 FssEss_L = material.specularColorBlended * dfgL.x + material.specularF90 * dfgL.y; + float Ess_V = dfgV.x + dfgV.y; + float Ess_L = dfgL.x + dfgL.y; + float Ems_V = 1.0 - Ess_V; + float Ems_L = 1.0 - Ess_L; + vec3 Favg = material.specularColorBlended + ( 1.0 - material.specularColorBlended ) * 0.047619; + vec3 Fms = FssEss_V * FssEss_L * Favg / ( 1.0 - Ems_V * Ems_L * Favg + EPSILON ); + float compensationFactor = Ems_V * Ems_L; + vec3 multiScatter = Fms * compensationFactor; + return singleScatter + multiScatter; +} +#if NUM_RECT_AREA_LIGHTS > 0 + void RE_Direct_RectArea_Physical( const in RectAreaLight rectAreaLight, const in vec3 geometryPosition, const in vec3 geometryNormal, const in vec3 geometryViewDir, const in vec3 geometryClearcoatNormal, const in PhysicalMaterial material, inout ReflectedLight reflectedLight ) { + vec3 normal = geometryNormal; + vec3 viewDir = geometryViewDir; + vec3 position = geometryPosition; + vec3 lightPos = rectAreaLight.position; + vec3 halfWidth = rectAreaLight.halfWidth; + vec3 halfHeight = rectAreaLight.halfHeight; + vec3 lightColor = rectAreaLight.color; + float roughness = material.roughness; + vec3 rectCoords[ 4 ]; + rectCoords[ 0 ] = lightPos + halfWidth - halfHeight; rectCoords[ 1 ] = lightPos - halfWidth - halfHeight; + rectCoords[ 2 ] = lightPos - halfWidth + halfHeight; + rectCoords[ 3 ] = lightPos + halfWidth + halfHeight; + vec2 uv = LTC_Uv( normal, viewDir, roughness ); + vec4 t1 = texture2D( ltc_1, uv ); + vec4 t2 = texture2D( ltc_2, uv ); + mat3 mInv = mat3( + vec3( t1.x, 0, t1.y ), + vec3( 0, 1, 0 ), + vec3( t1.z, 0, t1.w ) + ); + vec3 fresnel = ( material.specularColorBlended * t2.x + ( material.specularF90 - material.specularColorBlended ) * t2.y ); + reflectedLight.directSpecular += lightColor * fresnel * LTC_Evaluate( normal, viewDir, position, mInv, rectCoords ); + reflectedLight.directDiffuse += lightColor * material.diffuseContribution * LTC_Evaluate( normal, viewDir, position, mat3( 1.0 ), rectCoords ); + #ifdef USE_CLEARCOAT + vec3 Ncc = geometryClearcoatNormal; + vec2 uvClearcoat = LTC_Uv( Ncc, viewDir, material.clearcoatRoughness ); + vec4 t1Clearcoat = texture2D( ltc_1, uvClearcoat ); + vec4 t2Clearcoat = texture2D( ltc_2, uvClearcoat ); + mat3 mInvClearcoat = mat3( + vec3( t1Clearcoat.x, 0, t1Clearcoat.y ), + vec3( 0, 1, 0 ), + vec3( t1Clearcoat.z, 0, t1Clearcoat.w ) + ); + vec3 fresnelClearcoat = material.clearcoatF0 * t2Clearcoat.x + ( material.clearcoatF90 - material.clearcoatF0 ) * t2Clearcoat.y; + clearcoatSpecularDirect += lightColor * fresnelClearcoat * LTC_Evaluate( Ncc, viewDir, position, mInvClearcoat, rectCoords ); + #endif + } +#endif +void RE_Direct_Physical( const in IncidentLight directLight, const in vec3 geometryPosition, const in vec3 geometryNormal, const in vec3 geometryViewDir, const in vec3 geometryClearcoatNormal, const in PhysicalMaterial material, inout ReflectedLight reflectedLight ) { + float dotNL = saturate( dot( geometryNormal, directLight.direction ) ); + vec3 irradiance = dotNL * directLight.color; + #ifdef USE_CLEARCOAT + float dotNLcc = saturate( dot( geometryClearcoatNormal, directLight.direction ) ); + vec3 ccIrradiance = dotNLcc * directLight.color; + clearcoatSpecularDirect += ccIrradiance * BRDF_GGX_Clearcoat( directLight.direction, geometryViewDir, geometryClearcoatNormal, material ); + #endif + #ifdef USE_SHEEN + + sheenSpecularDirect += irradiance * BRDF_Sheen( directLight.direction, geometryViewDir, geometryNormal, material.sheenColor, material.sheenRoughness ); + + float sheenAlbedoV = IBLSheenBRDF( geometryNormal, geometryViewDir, material.sheenRoughness ); + float sheenAlbedoL = IBLSheenBRDF( geometryNormal, directLight.direction, material.sheenRoughness ); + + float sheenEnergyComp = 1.0 - max3( material.sheenColor ) * max( sheenAlbedoV, sheenAlbedoL ); + + irradiance *= sheenEnergyComp; + + #endif + reflectedLight.directSpecular += irradiance * BRDF_GGX_Multiscatter( directLight.direction, geometryViewDir, geometryNormal, material ); + reflectedLight.directDiffuse += irradiance * BRDF_Lambert( material.diffuseContribution ); +} +void RE_IndirectDiffuse_Physical( const in vec3 irradiance, const in vec3 geometryPosition, const in vec3 geometryNormal, const in vec3 geometryViewDir, const in vec3 geometryClearcoatNormal, const in PhysicalMaterial material, inout ReflectedLight reflectedLight ) { + vec3 diffuse = irradiance * BRDF_Lambert( material.diffuseContribution ); + #ifdef USE_SHEEN + float sheenAlbedo = IBLSheenBRDF( geometryNormal, geometryViewDir, material.sheenRoughness ); + float sheenEnergyComp = 1.0 - max3( material.sheenColor ) * sheenAlbedo; + diffuse *= sheenEnergyComp; + #endif + reflectedLight.indirectDiffuse += diffuse; +} +void RE_IndirectSpecular_Physical( const in vec3 radiance, const in vec3 irradiance, const in vec3 clearcoatRadiance, const in vec3 geometryPosition, const in vec3 geometryNormal, const in vec3 geometryViewDir, const in vec3 geometryClearcoatNormal, const in PhysicalMaterial material, inout ReflectedLight reflectedLight) { + #ifdef USE_CLEARCOAT + clearcoatSpecularIndirect += clearcoatRadiance * EnvironmentBRDF( geometryClearcoatNormal, geometryViewDir, material.clearcoatF0, material.clearcoatF90, material.clearcoatRoughness ); + #endif + #ifdef USE_SHEEN + sheenSpecularIndirect += irradiance * material.sheenColor * IBLSheenBRDF( geometryNormal, geometryViewDir, material.sheenRoughness ) * RECIPROCAL_PI; + #endif + vec3 singleScatteringDielectric = vec3( 0.0 ); + vec3 multiScatteringDielectric = vec3( 0.0 ); + vec3 singleScatteringMetallic = vec3( 0.0 ); + vec3 multiScatteringMetallic = vec3( 0.0 ); + #ifdef USE_IRIDESCENCE + computeMultiscatteringIridescence( geometryNormal, geometryViewDir, material.specularColor, material.specularF90, material.iridescence, material.iridescenceFresnelDielectric, material.roughness, singleScatteringDielectric, multiScatteringDielectric ); + computeMultiscatteringIridescence( geometryNormal, geometryViewDir, material.diffuseColor, material.specularF90, material.iridescence, material.iridescenceFresnelMetallic, material.roughness, singleScatteringMetallic, multiScatteringMetallic ); + #else + computeMultiscattering( geometryNormal, geometryViewDir, material.specularColor, material.specularF90, material.roughness, singleScatteringDielectric, multiScatteringDielectric ); + computeMultiscattering( geometryNormal, geometryViewDir, material.diffuseColor, material.specularF90, material.roughness, singleScatteringMetallic, multiScatteringMetallic ); + #endif + vec3 singleScattering = mix( singleScatteringDielectric, singleScatteringMetallic, material.metalness ); + vec3 multiScattering = mix( multiScatteringDielectric, multiScatteringMetallic, material.metalness ); + vec3 totalScatteringDielectric = singleScatteringDielectric + multiScatteringDielectric; + vec3 diffuse = material.diffuseContribution * ( 1.0 - totalScatteringDielectric ); + vec3 cosineWeightedIrradiance = irradiance * RECIPROCAL_PI; + vec3 indirectSpecular = radiance * singleScattering; + indirectSpecular += multiScattering * cosineWeightedIrradiance; + vec3 indirectDiffuse = diffuse * cosineWeightedIrradiance; + #ifdef USE_SHEEN + float sheenAlbedo = IBLSheenBRDF( geometryNormal, geometryViewDir, material.sheenRoughness ); + float sheenEnergyComp = 1.0 - max3( material.sheenColor ) * sheenAlbedo; + indirectSpecular *= sheenEnergyComp; + indirectDiffuse *= sheenEnergyComp; + #endif + reflectedLight.indirectSpecular += indirectSpecular; + reflectedLight.indirectDiffuse += indirectDiffuse; +} +#define RE_Direct RE_Direct_Physical +#define RE_Direct_RectArea RE_Direct_RectArea_Physical +#define RE_IndirectDiffuse RE_IndirectDiffuse_Physical +#define RE_IndirectSpecular RE_IndirectSpecular_Physical +float computeSpecularOcclusion( const in float dotNV, const in float ambientOcclusion, const in float roughness ) { + return saturate( pow( dotNV + ambientOcclusion, exp2( - 16.0 * roughness - 1.0 ) ) - 1.0 + ambientOcclusion ); +}`,v0=` +vec3 geometryPosition = - vViewPosition; +vec3 geometryNormal = normal; +vec3 geometryViewDir = ( isOrthographic ) ? vec3( 0, 0, 1 ) : normalize( vViewPosition ); +vec3 geometryClearcoatNormal = vec3( 0.0 ); +#ifdef USE_CLEARCOAT + geometryClearcoatNormal = clearcoatNormal; +#endif +#ifdef USE_IRIDESCENCE + float dotNVi = saturate( dot( normal, geometryViewDir ) ); + if ( material.iridescenceThickness == 0.0 ) { + material.iridescence = 0.0; + } else { + material.iridescence = saturate( material.iridescence ); + } + if ( material.iridescence > 0.0 ) { + material.iridescenceFresnelDielectric = evalIridescence( 1.0, material.iridescenceIOR, dotNVi, material.iridescenceThickness, material.specularColor ); + material.iridescenceFresnelMetallic = evalIridescence( 1.0, material.iridescenceIOR, dotNVi, material.iridescenceThickness, material.diffuseColor ); + material.iridescenceFresnel = mix( material.iridescenceFresnelDielectric, material.iridescenceFresnelMetallic, material.metalness ); + material.iridescenceF0 = Schlick_to_F0( material.iridescenceFresnel, 1.0, dotNVi ); + } +#endif +IncidentLight directLight; +#if ( NUM_POINT_LIGHTS > 0 ) && defined( RE_Direct ) + PointLight pointLight; + #if defined( USE_SHADOWMAP ) && NUM_POINT_LIGHT_SHADOWS > 0 + PointLightShadow pointLightShadow; + #endif + #pragma unroll_loop_start + for ( int i = 0; i < NUM_POINT_LIGHTS; i ++ ) { + pointLight = pointLights[ i ]; + getPointLightInfo( pointLight, geometryPosition, directLight ); + #if defined( USE_SHADOWMAP ) && ( UNROLLED_LOOP_INDEX < NUM_POINT_LIGHT_SHADOWS ) && ( defined( SHADOWMAP_TYPE_PCF ) || defined( SHADOWMAP_TYPE_BASIC ) ) + pointLightShadow = pointLightShadows[ i ]; + directLight.color *= ( directLight.visible && receiveShadow ) ? getPointShadow( pointShadowMap[ i ], pointLightShadow.shadowMapSize, pointLightShadow.shadowIntensity, pointLightShadow.shadowBias, pointLightShadow.shadowRadius, vPointShadowCoord[ i ], pointLightShadow.shadowCameraNear, pointLightShadow.shadowCameraFar ) : 1.0; + #endif + RE_Direct( directLight, geometryPosition, geometryNormal, geometryViewDir, geometryClearcoatNormal, material, reflectedLight ); + } + #pragma unroll_loop_end +#endif +#if ( NUM_SPOT_LIGHTS > 0 ) && defined( RE_Direct ) + SpotLight spotLight; + vec4 spotColor; + vec3 spotLightCoord; + bool inSpotLightMap; + #if defined( USE_SHADOWMAP ) && NUM_SPOT_LIGHT_SHADOWS > 0 + SpotLightShadow spotLightShadow; + #endif + #pragma unroll_loop_start + for ( int i = 0; i < NUM_SPOT_LIGHTS; i ++ ) { + spotLight = spotLights[ i ]; + getSpotLightInfo( spotLight, geometryPosition, directLight ); + #if ( UNROLLED_LOOP_INDEX < NUM_SPOT_LIGHT_SHADOWS_WITH_MAPS ) + #define SPOT_LIGHT_MAP_INDEX UNROLLED_LOOP_INDEX + #elif ( UNROLLED_LOOP_INDEX < NUM_SPOT_LIGHT_SHADOWS ) + #define SPOT_LIGHT_MAP_INDEX NUM_SPOT_LIGHT_MAPS + #else + #define SPOT_LIGHT_MAP_INDEX ( UNROLLED_LOOP_INDEX - NUM_SPOT_LIGHT_SHADOWS + NUM_SPOT_LIGHT_SHADOWS_WITH_MAPS ) + #endif + #if ( SPOT_LIGHT_MAP_INDEX < NUM_SPOT_LIGHT_MAPS ) + spotLightCoord = vSpotLightCoord[ i ].xyz / vSpotLightCoord[ i ].w; + inSpotLightMap = all( lessThan( abs( spotLightCoord * 2. - 1. ), vec3( 1.0 ) ) ); + spotColor = texture2D( spotLightMap[ SPOT_LIGHT_MAP_INDEX ], spotLightCoord.xy ); + directLight.color = inSpotLightMap ? directLight.color * spotColor.rgb : directLight.color; + #endif + #undef SPOT_LIGHT_MAP_INDEX + #if defined( USE_SHADOWMAP ) && ( UNROLLED_LOOP_INDEX < NUM_SPOT_LIGHT_SHADOWS ) + spotLightShadow = spotLightShadows[ i ]; + directLight.color *= ( directLight.visible && receiveShadow ) ? getShadow( spotShadowMap[ i ], spotLightShadow.shadowMapSize, spotLightShadow.shadowIntensity, spotLightShadow.shadowBias, spotLightShadow.shadowRadius, vSpotLightCoord[ i ] ) : 1.0; + #endif + RE_Direct( directLight, geometryPosition, geometryNormal, geometryViewDir, geometryClearcoatNormal, material, reflectedLight ); + } + #pragma unroll_loop_end +#endif +#if ( NUM_DIR_LIGHTS > 0 ) && defined( RE_Direct ) + DirectionalLight directionalLight; + #if defined( USE_SHADOWMAP ) && NUM_DIR_LIGHT_SHADOWS > 0 + DirectionalLightShadow directionalLightShadow; + #endif + #pragma unroll_loop_start + for ( int i = 0; i < NUM_DIR_LIGHTS; i ++ ) { + directionalLight = directionalLights[ i ]; + getDirectionalLightInfo( directionalLight, directLight ); + #if defined( USE_SHADOWMAP ) && ( UNROLLED_LOOP_INDEX < NUM_DIR_LIGHT_SHADOWS ) + directionalLightShadow = directionalLightShadows[ i ]; + directLight.color *= ( directLight.visible && receiveShadow ) ? getShadow( directionalShadowMap[ i ], directionalLightShadow.shadowMapSize, directionalLightShadow.shadowIntensity, directionalLightShadow.shadowBias, directionalLightShadow.shadowRadius, vDirectionalShadowCoord[ i ] ) : 1.0; + #endif + RE_Direct( directLight, geometryPosition, geometryNormal, geometryViewDir, geometryClearcoatNormal, material, reflectedLight ); + } + #pragma unroll_loop_end +#endif +#if ( NUM_RECT_AREA_LIGHTS > 0 ) && defined( RE_Direct_RectArea ) + RectAreaLight rectAreaLight; + #pragma unroll_loop_start + for ( int i = 0; i < NUM_RECT_AREA_LIGHTS; i ++ ) { + rectAreaLight = rectAreaLights[ i ]; + RE_Direct_RectArea( rectAreaLight, geometryPosition, geometryNormal, geometryViewDir, geometryClearcoatNormal, material, reflectedLight ); + } + #pragma unroll_loop_end +#endif +#if defined( RE_IndirectDiffuse ) + vec3 iblIrradiance = vec3( 0.0 ); + vec3 irradiance = getAmbientLightIrradiance( ambientLightColor ); + #if defined( USE_LIGHT_PROBES ) + irradiance += getLightProbeIrradiance( lightProbe, geometryNormal ); + #endif + #if ( NUM_HEMI_LIGHTS > 0 ) + #pragma unroll_loop_start + for ( int i = 0; i < NUM_HEMI_LIGHTS; i ++ ) { + irradiance += getHemisphereLightIrradiance( hemisphereLights[ i ], geometryNormal ); + } + #pragma unroll_loop_end + #endif + #ifdef USE_LIGHT_PROBES_GRID + vec3 probeWorldPos = ( ( vec4( geometryPosition, 1.0 ) - viewMatrix[ 3 ] ) * viewMatrix ).xyz; + vec3 probeWorldNormal = inverseTransformDirection( geometryNormal, viewMatrix ); + irradiance += getLightProbeGridIrradiance( probeWorldPos, probeWorldNormal ); + #endif +#endif +#if defined( RE_IndirectSpecular ) + vec3 radiance = vec3( 0.0 ); + vec3 clearcoatRadiance = vec3( 0.0 ); +#endif`,_0=`#if defined( RE_IndirectDiffuse ) + #ifdef USE_LIGHTMAP + vec4 lightMapTexel = texture2D( lightMap, vLightMapUv ); + vec3 lightMapIrradiance = lightMapTexel.rgb * lightMapIntensity; + irradiance += lightMapIrradiance; + #endif + #if defined( USE_ENVMAP ) && defined( ENVMAP_TYPE_CUBE_UV ) + #if defined( STANDARD ) || defined( LAMBERT ) || defined( PHONG ) + iblIrradiance += getIBLIrradiance( geometryNormal ); + #endif + #endif +#endif +#if defined( USE_ENVMAP ) && defined( RE_IndirectSpecular ) + #ifdef USE_ANISOTROPY + radiance += getIBLAnisotropyRadiance( geometryViewDir, geometryNormal, material.roughness, material.anisotropyB, material.anisotropy ); + #else + radiance += getIBLRadiance( geometryViewDir, geometryNormal, material.roughness ); + #endif + #ifdef USE_CLEARCOAT + clearcoatRadiance += getIBLRadiance( geometryViewDir, geometryClearcoatNormal, material.clearcoatRoughness ); + #endif +#endif`,y0=`#if defined( RE_IndirectDiffuse ) + #if defined( LAMBERT ) || defined( PHONG ) + irradiance += iblIrradiance; + #endif + RE_IndirectDiffuse( irradiance, geometryPosition, geometryNormal, geometryViewDir, geometryClearcoatNormal, material, reflectedLight ); +#endif +#if defined( RE_IndirectSpecular ) + RE_IndirectSpecular( radiance, iblIrradiance, clearcoatRadiance, geometryPosition, geometryNormal, geometryViewDir, geometryClearcoatNormal, material, reflectedLight ); +#endif`,b0=`#ifdef USE_LIGHT_PROBES_GRID +uniform highp sampler3D probesSH; +uniform vec3 probesMin; +uniform vec3 probesMax; +uniform vec3 probesResolution; +vec3 getLightProbeGridIrradiance( vec3 worldPos, vec3 worldNormal ) { + vec3 res = probesResolution; + vec3 gridRange = probesMax - probesMin; + vec3 resMinusOne = res - 1.0; + vec3 probeSpacing = gridRange / resMinusOne; + vec3 samplePos = worldPos + worldNormal * probeSpacing * 0.5; + vec3 uvw = clamp( ( samplePos - probesMin ) / gridRange, 0.0, 1.0 ); + uvw = uvw * resMinusOne / res + 0.5 / res; + float nz = res.z; + float paddedSlices = nz + 2.0; + float atlasDepth = 7.0 * paddedSlices; + float uvZBase = uvw.z * nz + 1.0; + vec4 s0 = texture( probesSH, vec3( uvw.xy, ( uvZBase ) / atlasDepth ) ); + vec4 s1 = texture( probesSH, vec3( uvw.xy, ( uvZBase + paddedSlices ) / atlasDepth ) ); + vec4 s2 = texture( probesSH, vec3( uvw.xy, ( uvZBase + 2.0 * paddedSlices ) / atlasDepth ) ); + vec4 s3 = texture( probesSH, vec3( uvw.xy, ( uvZBase + 3.0 * paddedSlices ) / atlasDepth ) ); + vec4 s4 = texture( probesSH, vec3( uvw.xy, ( uvZBase + 4.0 * paddedSlices ) / atlasDepth ) ); + vec4 s5 = texture( probesSH, vec3( uvw.xy, ( uvZBase + 5.0 * paddedSlices ) / atlasDepth ) ); + vec4 s6 = texture( probesSH, vec3( uvw.xy, ( uvZBase + 6.0 * paddedSlices ) / atlasDepth ) ); + vec3 c0 = s0.xyz; + vec3 c1 = vec3( s0.w, s1.xy ); + vec3 c2 = vec3( s1.zw, s2.x ); + vec3 c3 = s2.yzw; + vec3 c4 = s3.xyz; + vec3 c5 = vec3( s3.w, s4.xy ); + vec3 c6 = vec3( s4.zw, s5.x ); + vec3 c7 = s5.yzw; + vec3 c8 = s6.xyz; + float x = worldNormal.x, y = worldNormal.y, z = worldNormal.z; + vec3 result = c0 * 0.886227; + result += c1 * 2.0 * 0.511664 * y; + result += c2 * 2.0 * 0.511664 * z; + result += c3 * 2.0 * 0.511664 * x; + result += c4 * 2.0 * 0.429043 * x * y; + result += c5 * 2.0 * 0.429043 * y * z; + result += c6 * ( 0.743125 * z * z - 0.247708 ); + result += c7 * 2.0 * 0.429043 * x * z; + result += c8 * 0.429043 * ( x * x - y * y ); + return max( result, vec3( 0.0 ) ); +} +#endif`,M0=`#if defined( USE_LOGARITHMIC_DEPTH_BUFFER ) + gl_FragDepth = vIsPerspective == 0.0 ? gl_FragCoord.z : log2( vFragDepth ) * logDepthBufFC * 0.5; +#endif`,S0=`#if defined( USE_LOGARITHMIC_DEPTH_BUFFER ) + uniform float logDepthBufFC; + varying float vFragDepth; + varying float vIsPerspective; +#endif`,w0=`#ifdef USE_LOGARITHMIC_DEPTH_BUFFER + varying float vFragDepth; + varying float vIsPerspective; +#endif`,E0=`#ifdef USE_LOGARITHMIC_DEPTH_BUFFER + vFragDepth = 1.0 + gl_Position.w; + vIsPerspective = float( isPerspectiveMatrix( projectionMatrix ) ); +#endif`,T0=`#ifdef USE_MAP + vec4 sampledDiffuseColor = texture2D( map, vMapUv ); + #ifdef DECODE_VIDEO_TEXTURE + sampledDiffuseColor = sRGBTransferEOTF( sampledDiffuseColor ); + #endif + diffuseColor *= sampledDiffuseColor; +#endif`,A0=`#ifdef USE_MAP + uniform sampler2D map; +#endif`,C0=`#if defined( USE_MAP ) || defined( USE_ALPHAMAP ) + #if defined( USE_POINTS_UV ) + vec2 uv = vUv; + #else + vec2 uv = ( uvTransform * vec3( gl_PointCoord.x, 1.0 - gl_PointCoord.y, 1 ) ).xy; + #endif +#endif +#ifdef USE_MAP + diffuseColor *= texture2D( map, uv ); +#endif +#ifdef USE_ALPHAMAP + diffuseColor.a *= texture2D( alphaMap, uv ).g; +#endif`,R0=`#if defined( USE_POINTS_UV ) + varying vec2 vUv; +#else + #if defined( USE_MAP ) || defined( USE_ALPHAMAP ) + uniform mat3 uvTransform; + #endif +#endif +#ifdef USE_MAP + uniform sampler2D map; +#endif +#ifdef USE_ALPHAMAP + uniform sampler2D alphaMap; +#endif`,P0=`float metalnessFactor = metalness; +#ifdef USE_METALNESSMAP + vec4 texelMetalness = texture2D( metalnessMap, vMetalnessMapUv ); + metalnessFactor *= texelMetalness.b; +#endif`,I0=`#ifdef USE_METALNESSMAP + uniform sampler2D metalnessMap; +#endif`,L0=`#ifdef USE_INSTANCING_MORPH + float morphTargetInfluences[ MORPHTARGETS_COUNT ]; + float morphTargetBaseInfluence = texelFetch( morphTexture, ivec2( 0, gl_InstanceID ), 0 ).r; + for ( int i = 0; i < MORPHTARGETS_COUNT; i ++ ) { + morphTargetInfluences[i] = texelFetch( morphTexture, ivec2( i + 1, gl_InstanceID ), 0 ).r; + } +#endif`,N0=`#if defined( USE_MORPHCOLORS ) + vColor *= morphTargetBaseInfluence; + for ( int i = 0; i < MORPHTARGETS_COUNT; i ++ ) { + #if defined( USE_COLOR_ALPHA ) + if ( morphTargetInfluences[ i ] != 0.0 ) vColor += getMorph( gl_VertexID, i, 2 ) * morphTargetInfluences[ i ]; + #elif defined( USE_COLOR ) + if ( morphTargetInfluences[ i ] != 0.0 ) vColor += getMorph( gl_VertexID, i, 2 ).rgb * morphTargetInfluences[ i ]; + #endif + } +#endif`,D0=`#ifdef USE_MORPHNORMALS + objectNormal *= morphTargetBaseInfluence; + for ( int i = 0; i < MORPHTARGETS_COUNT; i ++ ) { + if ( morphTargetInfluences[ i ] != 0.0 ) objectNormal += getMorph( gl_VertexID, i, 1 ).xyz * morphTargetInfluences[ i ]; + } +#endif`,F0=`#ifdef USE_MORPHTARGETS + #ifndef USE_INSTANCING_MORPH + uniform float morphTargetBaseInfluence; + uniform float morphTargetInfluences[ MORPHTARGETS_COUNT ]; + #endif + uniform sampler2DArray morphTargetsTexture; + uniform ivec2 morphTargetsTextureSize; + vec4 getMorph( const in int vertexIndex, const in int morphTargetIndex, const in int offset ) { + int texelIndex = vertexIndex * MORPHTARGETS_TEXTURE_STRIDE + offset; + int y = texelIndex / morphTargetsTextureSize.x; + int x = texelIndex - y * morphTargetsTextureSize.x; + ivec3 morphUV = ivec3( x, y, morphTargetIndex ); + return texelFetch( morphTargetsTexture, morphUV, 0 ); + } +#endif`,k0=`#ifdef USE_MORPHTARGETS + transformed *= morphTargetBaseInfluence; + for ( int i = 0; i < MORPHTARGETS_COUNT; i ++ ) { + if ( morphTargetInfluences[ i ] != 0.0 ) transformed += getMorph( gl_VertexID, i, 0 ).xyz * morphTargetInfluences[ i ]; + } +#endif`,O0=`float faceDirection = gl_FrontFacing ? 1.0 : - 1.0; +#ifdef FLAT_SHADED + vec3 fdx = dFdx( vViewPosition ); + vec3 fdy = dFdy( vViewPosition ); + vec3 normal = normalize( cross( fdx, fdy ) ); +#else + vec3 normal = normalize( vNormal ); + #ifdef DOUBLE_SIDED + normal *= faceDirection; + #endif +#endif +#if defined( USE_NORMALMAP_TANGENTSPACE ) || defined( USE_CLEARCOAT_NORMALMAP ) || defined( USE_ANISOTROPY ) + #ifdef USE_TANGENT + mat3 tbn = mat3( normalize( vTangent ), normalize( vBitangent ), normal ); + #else + mat3 tbn = getTangentFrame( - vViewPosition, normal, + #if defined( USE_NORMALMAP ) + vNormalMapUv + #elif defined( USE_CLEARCOAT_NORMALMAP ) + vClearcoatNormalMapUv + #else + vUv + #endif + ); + #endif + #if defined( DOUBLE_SIDED ) && ! defined( FLAT_SHADED ) + tbn[0] *= faceDirection; + tbn[1] *= faceDirection; + #endif +#endif +#ifdef USE_CLEARCOAT_NORMALMAP + #ifdef USE_TANGENT + mat3 tbn2 = mat3( normalize( vTangent ), normalize( vBitangent ), normal ); + #else + mat3 tbn2 = getTangentFrame( - vViewPosition, normal, vClearcoatNormalMapUv ); + #endif + #if defined( DOUBLE_SIDED ) && ! defined( FLAT_SHADED ) + tbn2[0] *= faceDirection; + tbn2[1] *= faceDirection; + #endif +#endif +vec3 nonPerturbedNormal = normal;`,U0=`#ifdef USE_NORMALMAP_OBJECTSPACE + normal = texture2D( normalMap, vNormalMapUv ).xyz * 2.0 - 1.0; + #ifdef FLIP_SIDED + normal = - normal; + #endif + #ifdef DOUBLE_SIDED + normal = normal * faceDirection; + #endif + normal = normalize( normalMatrix * normal ); +#elif defined( USE_NORMALMAP_TANGENTSPACE ) + vec3 mapN = texture2D( normalMap, vNormalMapUv ).xyz * 2.0 - 1.0; + #if defined( USE_PACKED_NORMALMAP ) + mapN = vec3( mapN.xy, sqrt( saturate( 1.0 - dot( mapN.xy, mapN.xy ) ) ) ); + #endif + mapN.xy *= normalScale; + normal = normalize( tbn * mapN ); +#elif defined( USE_BUMPMAP ) + normal = perturbNormalArb( - vViewPosition, normal, dHdxy_fwd(), faceDirection ); +#endif`,B0=`#ifndef FLAT_SHADED + varying vec3 vNormal; + #ifdef USE_TANGENT + varying vec3 vTangent; + varying vec3 vBitangent; + #endif +#endif`,z0=`#ifndef FLAT_SHADED + varying vec3 vNormal; + #ifdef USE_TANGENT + varying vec3 vTangent; + varying vec3 vBitangent; + #endif +#endif`,V0=`#ifndef FLAT_SHADED + vNormal = normalize( transformedNormal ); + #ifdef USE_TANGENT + vTangent = normalize( transformedTangent ); + vBitangent = normalize( cross( vNormal, vTangent ) * tangent.w ); + #endif +#endif`,G0=`#ifdef USE_NORMALMAP + uniform sampler2D normalMap; + uniform vec2 normalScale; +#endif +#ifdef USE_NORMALMAP_OBJECTSPACE + uniform mat3 normalMatrix; +#endif +#if ! defined ( USE_TANGENT ) && ( defined ( USE_NORMALMAP_TANGENTSPACE ) || defined ( USE_CLEARCOAT_NORMALMAP ) || defined( USE_ANISOTROPY ) ) + mat3 getTangentFrame( vec3 eye_pos, vec3 surf_norm, vec2 uv ) { + vec3 q0 = dFdx( eye_pos.xyz ); + vec3 q1 = dFdy( eye_pos.xyz ); + vec2 st0 = dFdx( uv.st ); + vec2 st1 = dFdy( uv.st ); + vec3 N = surf_norm; + vec3 q1perp = cross( q1, N ); + vec3 q0perp = cross( N, q0 ); + vec3 T = q1perp * st0.x + q0perp * st1.x; + vec3 B = q1perp * st0.y + q0perp * st1.y; + float det = max( dot( T, T ), dot( B, B ) ); + float scale = ( det == 0.0 ) ? 0.0 : inversesqrt( det ); + return mat3( T * scale, B * scale, N ); + } +#endif`,H0=`#ifdef USE_CLEARCOAT + vec3 clearcoatNormal = nonPerturbedNormal; +#endif`,W0=`#ifdef USE_CLEARCOAT_NORMALMAP + vec3 clearcoatMapN = texture2D( clearcoatNormalMap, vClearcoatNormalMapUv ).xyz * 2.0 - 1.0; + clearcoatMapN.xy *= clearcoatNormalScale; + clearcoatNormal = normalize( tbn2 * clearcoatMapN ); +#endif`,X0=`#ifdef USE_CLEARCOATMAP + uniform sampler2D clearcoatMap; +#endif +#ifdef USE_CLEARCOAT_NORMALMAP + uniform sampler2D clearcoatNormalMap; + uniform vec2 clearcoatNormalScale; +#endif +#ifdef USE_CLEARCOAT_ROUGHNESSMAP + uniform sampler2D clearcoatRoughnessMap; +#endif`,q0=`#ifdef USE_IRIDESCENCEMAP + uniform sampler2D iridescenceMap; +#endif +#ifdef USE_IRIDESCENCE_THICKNESSMAP + uniform sampler2D iridescenceThicknessMap; +#endif`,Y0=`#ifdef OPAQUE +diffuseColor.a = 1.0; +#endif +#ifdef USE_TRANSMISSION +diffuseColor.a *= material.transmissionAlpha; +#endif +gl_FragColor = vec4( outgoingLight, diffuseColor.a );`,Z0=`vec3 packNormalToRGB( const in vec3 normal ) { + return normalize( normal ) * 0.5 + 0.5; +} +vec3 unpackRGBToNormal( const in vec3 rgb ) { + return 2.0 * rgb.xyz - 1.0; +} +const float PackUpscale = 256. / 255.;const float UnpackDownscale = 255. / 256.;const float ShiftRight8 = 1. / 256.; +const float Inv255 = 1. / 255.; +const vec4 PackFactors = vec4( 1.0, 256.0, 256.0 * 256.0, 256.0 * 256.0 * 256.0 ); +const vec2 UnpackFactors2 = vec2( UnpackDownscale, 1.0 / PackFactors.g ); +const vec3 UnpackFactors3 = vec3( UnpackDownscale / PackFactors.rg, 1.0 / PackFactors.b ); +const vec4 UnpackFactors4 = vec4( UnpackDownscale / PackFactors.rgb, 1.0 / PackFactors.a ); +vec4 packDepthToRGBA( const in float v ) { + if( v <= 0.0 ) + return vec4( 0., 0., 0., 0. ); + if( v >= 1.0 ) + return vec4( 1., 1., 1., 1. ); + float vuf; + float af = modf( v * PackFactors.a, vuf ); + float bf = modf( vuf * ShiftRight8, vuf ); + float gf = modf( vuf * ShiftRight8, vuf ); + return vec4( vuf * Inv255, gf * PackUpscale, bf * PackUpscale, af ); +} +vec3 packDepthToRGB( const in float v ) { + if( v <= 0.0 ) + return vec3( 0., 0., 0. ); + if( v >= 1.0 ) + return vec3( 1., 1., 1. ); + float vuf; + float bf = modf( v * PackFactors.b, vuf ); + float gf = modf( vuf * ShiftRight8, vuf ); + return vec3( vuf * Inv255, gf * PackUpscale, bf ); +} +vec2 packDepthToRG( const in float v ) { + if( v <= 0.0 ) + return vec2( 0., 0. ); + if( v >= 1.0 ) + return vec2( 1., 1. ); + float vuf; + float gf = modf( v * 256., vuf ); + return vec2( vuf * Inv255, gf ); +} +float unpackRGBAToDepth( const in vec4 v ) { + return dot( v, UnpackFactors4 ); +} +float unpackRGBToDepth( const in vec3 v ) { + return dot( v, UnpackFactors3 ); +} +float unpackRGToDepth( const in vec2 v ) { + return v.r * UnpackFactors2.r + v.g * UnpackFactors2.g; +} +vec4 pack2HalfToRGBA( const in vec2 v ) { + vec4 r = vec4( v.x, fract( v.x * 255.0 ), v.y, fract( v.y * 255.0 ) ); + return vec4( r.x - r.y / 255.0, r.y, r.z - r.w / 255.0, r.w ); +} +vec2 unpackRGBATo2Half( const in vec4 v ) { + return vec2( v.x + ( v.y / 255.0 ), v.z + ( v.w / 255.0 ) ); +} +float viewZToOrthographicDepth( const in float viewZ, const in float near, const in float far ) { + return ( viewZ + near ) / ( near - far ); +} +float orthographicDepthToViewZ( const in float depth, const in float near, const in float far ) { + #ifdef USE_REVERSED_DEPTH_BUFFER + + return depth * ( far - near ) - far; + #else + return depth * ( near - far ) - near; + #endif +} +float viewZToPerspectiveDepth( const in float viewZ, const in float near, const in float far ) { + return ( ( near + viewZ ) * far ) / ( ( far - near ) * viewZ ); +} +float perspectiveDepthToViewZ( const in float depth, const in float near, const in float far ) { + + #ifdef USE_REVERSED_DEPTH_BUFFER + return ( near * far ) / ( ( near - far ) * depth - near ); + #else + return ( near * far ) / ( ( far - near ) * depth - far ); + #endif +}`,$0=`#ifdef PREMULTIPLIED_ALPHA + gl_FragColor.rgb *= gl_FragColor.a; +#endif`,K0=`vec4 mvPosition = vec4( transformed, 1.0 ); +#ifdef USE_BATCHING + mvPosition = batchingMatrix * mvPosition; +#endif +#ifdef USE_INSTANCING + mvPosition = instanceMatrix * mvPosition; +#endif +mvPosition = modelViewMatrix * mvPosition; +gl_Position = projectionMatrix * mvPosition;`,J0=`#ifdef DITHERING + gl_FragColor.rgb = dithering( gl_FragColor.rgb ); +#endif`,j0=`#ifdef DITHERING + vec3 dithering( vec3 color ) { + float grid_position = rand( gl_FragCoord.xy ); + vec3 dither_shift_RGB = vec3( 0.25 / 255.0, -0.25 / 255.0, 0.25 / 255.0 ); + dither_shift_RGB = mix( 2.0 * dither_shift_RGB, -2.0 * dither_shift_RGB, grid_position ); + return color + dither_shift_RGB; + } +#endif`,Q0=`float roughnessFactor = roughness; +#ifdef USE_ROUGHNESSMAP + vec4 texelRoughness = texture2D( roughnessMap, vRoughnessMapUv ); + roughnessFactor *= texelRoughness.g; +#endif`,ex=`#ifdef USE_ROUGHNESSMAP + uniform sampler2D roughnessMap; +#endif`,tx=`#if NUM_SPOT_LIGHT_COORDS > 0 + varying vec4 vSpotLightCoord[ NUM_SPOT_LIGHT_COORDS ]; +#endif +#if NUM_SPOT_LIGHT_MAPS > 0 + uniform sampler2D spotLightMap[ NUM_SPOT_LIGHT_MAPS ]; +#endif +#ifdef USE_SHADOWMAP + #if NUM_DIR_LIGHT_SHADOWS > 0 + #if defined( SHADOWMAP_TYPE_PCF ) + uniform sampler2DShadow directionalShadowMap[ NUM_DIR_LIGHT_SHADOWS ]; + #else + uniform sampler2D directionalShadowMap[ NUM_DIR_LIGHT_SHADOWS ]; + #endif + varying vec4 vDirectionalShadowCoord[ NUM_DIR_LIGHT_SHADOWS ]; + struct DirectionalLightShadow { + float shadowIntensity; + float shadowBias; + float shadowNormalBias; + float shadowRadius; + vec2 shadowMapSize; + }; + uniform DirectionalLightShadow directionalLightShadows[ NUM_DIR_LIGHT_SHADOWS ]; + #endif + #if NUM_SPOT_LIGHT_SHADOWS > 0 + #if defined( SHADOWMAP_TYPE_PCF ) + uniform sampler2DShadow spotShadowMap[ NUM_SPOT_LIGHT_SHADOWS ]; + #else + uniform sampler2D spotShadowMap[ NUM_SPOT_LIGHT_SHADOWS ]; + #endif + struct SpotLightShadow { + float shadowIntensity; + float shadowBias; + float shadowNormalBias; + float shadowRadius; + vec2 shadowMapSize; + }; + uniform SpotLightShadow spotLightShadows[ NUM_SPOT_LIGHT_SHADOWS ]; + #endif + #if NUM_POINT_LIGHT_SHADOWS > 0 + #if defined( SHADOWMAP_TYPE_PCF ) + uniform samplerCubeShadow pointShadowMap[ NUM_POINT_LIGHT_SHADOWS ]; + #elif defined( SHADOWMAP_TYPE_BASIC ) + uniform samplerCube pointShadowMap[ NUM_POINT_LIGHT_SHADOWS ]; + #endif + varying vec4 vPointShadowCoord[ NUM_POINT_LIGHT_SHADOWS ]; + struct PointLightShadow { + float shadowIntensity; + float shadowBias; + float shadowNormalBias; + float shadowRadius; + vec2 shadowMapSize; + float shadowCameraNear; + float shadowCameraFar; + }; + uniform PointLightShadow pointLightShadows[ NUM_POINT_LIGHT_SHADOWS ]; + #endif + #if defined( SHADOWMAP_TYPE_PCF ) + float interleavedGradientNoise( vec2 position ) { + return fract( 52.9829189 * fract( dot( position, vec2( 0.06711056, 0.00583715 ) ) ) ); + } + vec2 vogelDiskSample( int sampleIndex, int samplesCount, float phi ) { + const float goldenAngle = 2.399963229728653; + float r = sqrt( ( float( sampleIndex ) + 0.5 ) / float( samplesCount ) ); + float theta = float( sampleIndex ) * goldenAngle + phi; + return vec2( cos( theta ), sin( theta ) ) * r; + } + #endif + #if defined( SHADOWMAP_TYPE_PCF ) + float getShadow( sampler2DShadow shadowMap, vec2 shadowMapSize, float shadowIntensity, float shadowBias, float shadowRadius, vec4 shadowCoord ) { + float shadow = 1.0; + shadowCoord.xyz /= shadowCoord.w; + shadowCoord.z += shadowBias; + bool inFrustum = shadowCoord.x >= 0.0 && shadowCoord.x <= 1.0 && shadowCoord.y >= 0.0 && shadowCoord.y <= 1.0; + bool frustumTest = inFrustum && shadowCoord.z <= 1.0; + if ( frustumTest ) { + vec2 texelSize = vec2( 1.0 ) / shadowMapSize; + float radius = shadowRadius * texelSize.x; + float phi = interleavedGradientNoise( gl_FragCoord.xy ) * PI2; + shadow = ( + texture( shadowMap, vec3( shadowCoord.xy + vogelDiskSample( 0, 5, phi ) * radius, shadowCoord.z ) ) + + texture( shadowMap, vec3( shadowCoord.xy + vogelDiskSample( 1, 5, phi ) * radius, shadowCoord.z ) ) + + texture( shadowMap, vec3( shadowCoord.xy + vogelDiskSample( 2, 5, phi ) * radius, shadowCoord.z ) ) + + texture( shadowMap, vec3( shadowCoord.xy + vogelDiskSample( 3, 5, phi ) * radius, shadowCoord.z ) ) + + texture( shadowMap, vec3( shadowCoord.xy + vogelDiskSample( 4, 5, phi ) * radius, shadowCoord.z ) ) + ) * 0.2; + } + return mix( 1.0, shadow, shadowIntensity ); + } + #elif defined( SHADOWMAP_TYPE_VSM ) + float getShadow( sampler2D shadowMap, vec2 shadowMapSize, float shadowIntensity, float shadowBias, float shadowRadius, vec4 shadowCoord ) { + float shadow = 1.0; + shadowCoord.xyz /= shadowCoord.w; + #ifdef USE_REVERSED_DEPTH_BUFFER + shadowCoord.z -= shadowBias; + #else + shadowCoord.z += shadowBias; + #endif + bool inFrustum = shadowCoord.x >= 0.0 && shadowCoord.x <= 1.0 && shadowCoord.y >= 0.0 && shadowCoord.y <= 1.0; + bool frustumTest = inFrustum && shadowCoord.z <= 1.0; + if ( frustumTest ) { + vec2 distribution = texture2D( shadowMap, shadowCoord.xy ).rg; + float mean = distribution.x; + float variance = distribution.y * distribution.y; + #ifdef USE_REVERSED_DEPTH_BUFFER + float hard_shadow = step( mean, shadowCoord.z ); + #else + float hard_shadow = step( shadowCoord.z, mean ); + #endif + + if ( hard_shadow == 1.0 ) { + shadow = 1.0; + } else { + variance = max( variance, 0.0000001 ); + float d = shadowCoord.z - mean; + float p_max = variance / ( variance + d * d ); + p_max = clamp( ( p_max - 0.3 ) / 0.65, 0.0, 1.0 ); + shadow = max( hard_shadow, p_max ); + } + } + return mix( 1.0, shadow, shadowIntensity ); + } + #else + float getShadow( sampler2D shadowMap, vec2 shadowMapSize, float shadowIntensity, float shadowBias, float shadowRadius, vec4 shadowCoord ) { + float shadow = 1.0; + shadowCoord.xyz /= shadowCoord.w; + #ifdef USE_REVERSED_DEPTH_BUFFER + shadowCoord.z -= shadowBias; + #else + shadowCoord.z += shadowBias; + #endif + bool inFrustum = shadowCoord.x >= 0.0 && shadowCoord.x <= 1.0 && shadowCoord.y >= 0.0 && shadowCoord.y <= 1.0; + bool frustumTest = inFrustum && shadowCoord.z <= 1.0; + if ( frustumTest ) { + float depth = texture2D( shadowMap, shadowCoord.xy ).r; + #ifdef USE_REVERSED_DEPTH_BUFFER + shadow = step( depth, shadowCoord.z ); + #else + shadow = step( shadowCoord.z, depth ); + #endif + } + return mix( 1.0, shadow, shadowIntensity ); + } + #endif + #if NUM_POINT_LIGHT_SHADOWS > 0 + #if defined( SHADOWMAP_TYPE_PCF ) + float getPointShadow( samplerCubeShadow shadowMap, vec2 shadowMapSize, float shadowIntensity, float shadowBias, float shadowRadius, vec4 shadowCoord, float shadowCameraNear, float shadowCameraFar ) { + float shadow = 1.0; + vec3 lightToPosition = shadowCoord.xyz; + vec3 bd3D = normalize( lightToPosition ); + vec3 absVec = abs( lightToPosition ); + float viewSpaceZ = max( max( absVec.x, absVec.y ), absVec.z ); + if ( viewSpaceZ - shadowCameraFar <= 0.0 && viewSpaceZ - shadowCameraNear >= 0.0 ) { + #ifdef USE_REVERSED_DEPTH_BUFFER + float dp = ( shadowCameraNear * ( shadowCameraFar - viewSpaceZ ) ) / ( viewSpaceZ * ( shadowCameraFar - shadowCameraNear ) ); + dp -= shadowBias; + #else + float dp = ( shadowCameraFar * ( viewSpaceZ - shadowCameraNear ) ) / ( viewSpaceZ * ( shadowCameraFar - shadowCameraNear ) ); + dp += shadowBias; + #endif + float texelSize = shadowRadius / shadowMapSize.x; + vec3 absDir = abs( bd3D ); + vec3 tangent = absDir.x > absDir.z ? vec3( 0.0, 1.0, 0.0 ) : vec3( 1.0, 0.0, 0.0 ); + tangent = normalize( cross( bd3D, tangent ) ); + vec3 bitangent = cross( bd3D, tangent ); + float phi = interleavedGradientNoise( gl_FragCoord.xy ) * PI2; + vec2 sample0 = vogelDiskSample( 0, 5, phi ); + vec2 sample1 = vogelDiskSample( 1, 5, phi ); + vec2 sample2 = vogelDiskSample( 2, 5, phi ); + vec2 sample3 = vogelDiskSample( 3, 5, phi ); + vec2 sample4 = vogelDiskSample( 4, 5, phi ); + shadow = ( + texture( shadowMap, vec4( bd3D + ( tangent * sample0.x + bitangent * sample0.y ) * texelSize, dp ) ) + + texture( shadowMap, vec4( bd3D + ( tangent * sample1.x + bitangent * sample1.y ) * texelSize, dp ) ) + + texture( shadowMap, vec4( bd3D + ( tangent * sample2.x + bitangent * sample2.y ) * texelSize, dp ) ) + + texture( shadowMap, vec4( bd3D + ( tangent * sample3.x + bitangent * sample3.y ) * texelSize, dp ) ) + + texture( shadowMap, vec4( bd3D + ( tangent * sample4.x + bitangent * sample4.y ) * texelSize, dp ) ) + ) * 0.2; + } + return mix( 1.0, shadow, shadowIntensity ); + } + #elif defined( SHADOWMAP_TYPE_BASIC ) + float getPointShadow( samplerCube shadowMap, vec2 shadowMapSize, float shadowIntensity, float shadowBias, float shadowRadius, vec4 shadowCoord, float shadowCameraNear, float shadowCameraFar ) { + float shadow = 1.0; + vec3 lightToPosition = shadowCoord.xyz; + vec3 absVec = abs( lightToPosition ); + float viewSpaceZ = max( max( absVec.x, absVec.y ), absVec.z ); + if ( viewSpaceZ - shadowCameraFar <= 0.0 && viewSpaceZ - shadowCameraNear >= 0.0 ) { + float dp = ( shadowCameraFar * ( viewSpaceZ - shadowCameraNear ) ) / ( viewSpaceZ * ( shadowCameraFar - shadowCameraNear ) ); + dp += shadowBias; + vec3 bd3D = normalize( lightToPosition ); + float depth = textureCube( shadowMap, bd3D ).r; + #ifdef USE_REVERSED_DEPTH_BUFFER + depth = 1.0 - depth; + #endif + shadow = step( dp, depth ); + } + return mix( 1.0, shadow, shadowIntensity ); + } + #endif + #endif +#endif`,nx=`#if NUM_SPOT_LIGHT_COORDS > 0 + uniform mat4 spotLightMatrix[ NUM_SPOT_LIGHT_COORDS ]; + varying vec4 vSpotLightCoord[ NUM_SPOT_LIGHT_COORDS ]; +#endif +#ifdef USE_SHADOWMAP + #if NUM_DIR_LIGHT_SHADOWS > 0 + uniform mat4 directionalShadowMatrix[ NUM_DIR_LIGHT_SHADOWS ]; + varying vec4 vDirectionalShadowCoord[ NUM_DIR_LIGHT_SHADOWS ]; + struct DirectionalLightShadow { + float shadowIntensity; + float shadowBias; + float shadowNormalBias; + float shadowRadius; + vec2 shadowMapSize; + }; + uniform DirectionalLightShadow directionalLightShadows[ NUM_DIR_LIGHT_SHADOWS ]; + #endif + #if NUM_SPOT_LIGHT_SHADOWS > 0 + struct SpotLightShadow { + float shadowIntensity; + float shadowBias; + float shadowNormalBias; + float shadowRadius; + vec2 shadowMapSize; + }; + uniform SpotLightShadow spotLightShadows[ NUM_SPOT_LIGHT_SHADOWS ]; + #endif + #if NUM_POINT_LIGHT_SHADOWS > 0 + uniform mat4 pointShadowMatrix[ NUM_POINT_LIGHT_SHADOWS ]; + varying vec4 vPointShadowCoord[ NUM_POINT_LIGHT_SHADOWS ]; + struct PointLightShadow { + float shadowIntensity; + float shadowBias; + float shadowNormalBias; + float shadowRadius; + vec2 shadowMapSize; + float shadowCameraNear; + float shadowCameraFar; + }; + uniform PointLightShadow pointLightShadows[ NUM_POINT_LIGHT_SHADOWS ]; + #endif +#endif`,ix=`#if ( defined( USE_SHADOWMAP ) && ( NUM_DIR_LIGHT_SHADOWS > 0 || NUM_POINT_LIGHT_SHADOWS > 0 ) ) || ( NUM_SPOT_LIGHT_COORDS > 0 ) + #ifdef HAS_NORMAL + vec3 shadowWorldNormal = inverseTransformDirection( transformedNormal, viewMatrix ); + #else + vec3 shadowWorldNormal = vec3( 0.0 ); + #endif + vec4 shadowWorldPosition; +#endif +#if defined( USE_SHADOWMAP ) + #if NUM_DIR_LIGHT_SHADOWS > 0 + #pragma unroll_loop_start + for ( int i = 0; i < NUM_DIR_LIGHT_SHADOWS; i ++ ) { + shadowWorldPosition = worldPosition + vec4( shadowWorldNormal * directionalLightShadows[ i ].shadowNormalBias, 0 ); + vDirectionalShadowCoord[ i ] = directionalShadowMatrix[ i ] * shadowWorldPosition; + } + #pragma unroll_loop_end + #endif + #if NUM_POINT_LIGHT_SHADOWS > 0 + #pragma unroll_loop_start + for ( int i = 0; i < NUM_POINT_LIGHT_SHADOWS; i ++ ) { + shadowWorldPosition = worldPosition + vec4( shadowWorldNormal * pointLightShadows[ i ].shadowNormalBias, 0 ); + vPointShadowCoord[ i ] = pointShadowMatrix[ i ] * shadowWorldPosition; + } + #pragma unroll_loop_end + #endif +#endif +#if NUM_SPOT_LIGHT_COORDS > 0 + #pragma unroll_loop_start + for ( int i = 0; i < NUM_SPOT_LIGHT_COORDS; i ++ ) { + shadowWorldPosition = worldPosition; + #if ( defined( USE_SHADOWMAP ) && UNROLLED_LOOP_INDEX < NUM_SPOT_LIGHT_SHADOWS ) + shadowWorldPosition.xyz += shadowWorldNormal * spotLightShadows[ i ].shadowNormalBias; + #endif + vSpotLightCoord[ i ] = spotLightMatrix[ i ] * shadowWorldPosition; + } + #pragma unroll_loop_end +#endif`,sx=`float getShadowMask() { + float shadow = 1.0; + #ifdef USE_SHADOWMAP + #if NUM_DIR_LIGHT_SHADOWS > 0 + DirectionalLightShadow directionalLight; + #pragma unroll_loop_start + for ( int i = 0; i < NUM_DIR_LIGHT_SHADOWS; i ++ ) { + directionalLight = directionalLightShadows[ i ]; + shadow *= receiveShadow ? getShadow( directionalShadowMap[ i ], directionalLight.shadowMapSize, directionalLight.shadowIntensity, directionalLight.shadowBias, directionalLight.shadowRadius, vDirectionalShadowCoord[ i ] ) : 1.0; + } + #pragma unroll_loop_end + #endif + #if NUM_SPOT_LIGHT_SHADOWS > 0 + SpotLightShadow spotLight; + #pragma unroll_loop_start + for ( int i = 0; i < NUM_SPOT_LIGHT_SHADOWS; i ++ ) { + spotLight = spotLightShadows[ i ]; + shadow *= receiveShadow ? getShadow( spotShadowMap[ i ], spotLight.shadowMapSize, spotLight.shadowIntensity, spotLight.shadowBias, spotLight.shadowRadius, vSpotLightCoord[ i ] ) : 1.0; + } + #pragma unroll_loop_end + #endif + #if NUM_POINT_LIGHT_SHADOWS > 0 && ( defined( SHADOWMAP_TYPE_PCF ) || defined( SHADOWMAP_TYPE_BASIC ) ) + PointLightShadow pointLight; + #pragma unroll_loop_start + for ( int i = 0; i < NUM_POINT_LIGHT_SHADOWS; i ++ ) { + pointLight = pointLightShadows[ i ]; + shadow *= receiveShadow ? getPointShadow( pointShadowMap[ i ], pointLight.shadowMapSize, pointLight.shadowIntensity, pointLight.shadowBias, pointLight.shadowRadius, vPointShadowCoord[ i ], pointLight.shadowCameraNear, pointLight.shadowCameraFar ) : 1.0; + } + #pragma unroll_loop_end + #endif + #endif + return shadow; +}`,rx=`#ifdef USE_SKINNING + mat4 boneMatX = getBoneMatrix( skinIndex.x ); + mat4 boneMatY = getBoneMatrix( skinIndex.y ); + mat4 boneMatZ = getBoneMatrix( skinIndex.z ); + mat4 boneMatW = getBoneMatrix( skinIndex.w ); +#endif`,ox=`#ifdef USE_SKINNING + uniform mat4 bindMatrix; + uniform mat4 bindMatrixInverse; + uniform highp sampler2D boneTexture; + mat4 getBoneMatrix( const in float i ) { + int size = textureSize( boneTexture, 0 ).x; + int j = int( i ) * 4; + int x = j % size; + int y = j / size; + vec4 v1 = texelFetch( boneTexture, ivec2( x, y ), 0 ); + vec4 v2 = texelFetch( boneTexture, ivec2( x + 1, y ), 0 ); + vec4 v3 = texelFetch( boneTexture, ivec2( x + 2, y ), 0 ); + vec4 v4 = texelFetch( boneTexture, ivec2( x + 3, y ), 0 ); + return mat4( v1, v2, v3, v4 ); + } +#endif`,ax=`#ifdef USE_SKINNING + vec4 skinVertex = bindMatrix * vec4( transformed, 1.0 ); + vec4 skinned = vec4( 0.0 ); + skinned += boneMatX * skinVertex * skinWeight.x; + skinned += boneMatY * skinVertex * skinWeight.y; + skinned += boneMatZ * skinVertex * skinWeight.z; + skinned += boneMatW * skinVertex * skinWeight.w; + transformed = ( bindMatrixInverse * skinned ).xyz; +#endif`,lx=`#ifdef USE_SKINNING + mat4 skinMatrix = mat4( 0.0 ); + skinMatrix += skinWeight.x * boneMatX; + skinMatrix += skinWeight.y * boneMatY; + skinMatrix += skinWeight.z * boneMatZ; + skinMatrix += skinWeight.w * boneMatW; + skinMatrix = bindMatrixInverse * skinMatrix * bindMatrix; + objectNormal = vec4( skinMatrix * vec4( objectNormal, 0.0 ) ).xyz; + #ifdef USE_TANGENT + objectTangent = vec4( skinMatrix * vec4( objectTangent, 0.0 ) ).xyz; + #endif +#endif`,cx=`float specularStrength; +#ifdef USE_SPECULARMAP + vec4 texelSpecular = texture2D( specularMap, vSpecularMapUv ); + specularStrength = texelSpecular.r; +#else + specularStrength = 1.0; +#endif`,hx=`#ifdef USE_SPECULARMAP + uniform sampler2D specularMap; +#endif`,ux=`#if defined( TONE_MAPPING ) + gl_FragColor.rgb = toneMapping( gl_FragColor.rgb ); +#endif`,dx=`#ifndef saturate +#define saturate( a ) clamp( a, 0.0, 1.0 ) +#endif +uniform float toneMappingExposure; +vec3 LinearToneMapping( vec3 color ) { + return saturate( toneMappingExposure * color ); +} +vec3 ReinhardToneMapping( vec3 color ) { + color *= toneMappingExposure; + return saturate( color / ( vec3( 1.0 ) + color ) ); +} +vec3 CineonToneMapping( vec3 color ) { + color *= toneMappingExposure; + color = max( vec3( 0.0 ), color - 0.004 ); + return pow( ( color * ( 6.2 * color + 0.5 ) ) / ( color * ( 6.2 * color + 1.7 ) + 0.06 ), vec3( 2.2 ) ); +} +vec3 RRTAndODTFit( vec3 v ) { + vec3 a = v * ( v + 0.0245786 ) - 0.000090537; + vec3 b = v * ( 0.983729 * v + 0.4329510 ) + 0.238081; + return a / b; +} +vec3 ACESFilmicToneMapping( vec3 color ) { + const mat3 ACESInputMat = mat3( + vec3( 0.59719, 0.07600, 0.02840 ), vec3( 0.35458, 0.90834, 0.13383 ), + vec3( 0.04823, 0.01566, 0.83777 ) + ); + const mat3 ACESOutputMat = mat3( + vec3( 1.60475, -0.10208, -0.00327 ), vec3( -0.53108, 1.10813, -0.07276 ), + vec3( -0.07367, -0.00605, 1.07602 ) + ); + color *= toneMappingExposure / 0.6; + color = ACESInputMat * color; + color = RRTAndODTFit( color ); + color = ACESOutputMat * color; + return saturate( color ); +} +const mat3 LINEAR_REC2020_TO_LINEAR_SRGB = mat3( + vec3( 1.6605, - 0.1246, - 0.0182 ), + vec3( - 0.5876, 1.1329, - 0.1006 ), + vec3( - 0.0728, - 0.0083, 1.1187 ) +); +const mat3 LINEAR_SRGB_TO_LINEAR_REC2020 = mat3( + vec3( 0.6274, 0.0691, 0.0164 ), + vec3( 0.3293, 0.9195, 0.0880 ), + vec3( 0.0433, 0.0113, 0.8956 ) +); +vec3 agxDefaultContrastApprox( vec3 x ) { + vec3 x2 = x * x; + vec3 x4 = x2 * x2; + return + 15.5 * x4 * x2 + - 40.14 * x4 * x + + 31.96 * x4 + - 6.868 * x2 * x + + 0.4298 * x2 + + 0.1191 * x + - 0.00232; +} +vec3 AgXToneMapping( vec3 color ) { + const mat3 AgXInsetMatrix = mat3( + vec3( 0.856627153315983, 0.137318972929847, 0.11189821299995 ), + vec3( 0.0951212405381588, 0.761241990602591, 0.0767994186031903 ), + vec3( 0.0482516061458583, 0.101439036467562, 0.811302368396859 ) + ); + const mat3 AgXOutsetMatrix = mat3( + vec3( 1.1271005818144368, - 0.1413297634984383, - 0.14132976349843826 ), + vec3( - 0.11060664309660323, 1.157823702216272, - 0.11060664309660294 ), + vec3( - 0.016493938717834573, - 0.016493938717834257, 1.2519364065950405 ) + ); + const float AgxMinEv = - 12.47393; const float AgxMaxEv = 4.026069; + color *= toneMappingExposure; + color = LINEAR_SRGB_TO_LINEAR_REC2020 * color; + color = AgXInsetMatrix * color; + color = max( color, 1e-10 ); color = log2( color ); + color = ( color - AgxMinEv ) / ( AgxMaxEv - AgxMinEv ); + color = clamp( color, 0.0, 1.0 ); + color = agxDefaultContrastApprox( color ); + color = AgXOutsetMatrix * color; + color = pow( max( vec3( 0.0 ), color ), vec3( 2.2 ) ); + color = LINEAR_REC2020_TO_LINEAR_SRGB * color; + color = clamp( color, 0.0, 1.0 ); + return color; +} +vec3 NeutralToneMapping( vec3 color ) { + const float StartCompression = 0.8 - 0.04; + const float Desaturation = 0.15; + color *= toneMappingExposure; + float x = min( color.r, min( color.g, color.b ) ); + float offset = x < 0.08 ? x - 6.25 * x * x : 0.04; + color -= offset; + float peak = max( color.r, max( color.g, color.b ) ); + if ( peak < StartCompression ) return color; + float d = 1. - StartCompression; + float newPeak = 1. - d * d / ( peak + d - StartCompression ); + color *= newPeak / peak; + float g = 1. - 1. / ( Desaturation * ( peak - newPeak ) + 1. ); + return mix( color, vec3( newPeak ), g ); +} +vec3 CustomToneMapping( vec3 color ) { return color; }`,fx=`#ifdef USE_TRANSMISSION + material.transmission = transmission; + material.transmissionAlpha = 1.0; + material.thickness = thickness; + material.attenuationDistance = attenuationDistance; + material.attenuationColor = attenuationColor; + #ifdef USE_TRANSMISSIONMAP + material.transmission *= texture2D( transmissionMap, vTransmissionMapUv ).r; + #endif + #ifdef USE_THICKNESSMAP + material.thickness *= texture2D( thicknessMap, vThicknessMapUv ).g; + #endif + vec3 pos = vWorldPosition; + vec3 v = normalize( cameraPosition - pos ); + vec3 n = inverseTransformDirection( normal, viewMatrix ); + vec4 transmitted = getIBLVolumeRefraction( + n, v, material.roughness, material.diffuseContribution, material.specularColorBlended, material.specularF90, + pos, modelMatrix, viewMatrix, projectionMatrix, material.dispersion, material.ior, material.thickness, + material.attenuationColor, material.attenuationDistance ); + material.transmissionAlpha = mix( material.transmissionAlpha, transmitted.a, material.transmission ); + totalDiffuse = mix( totalDiffuse, transmitted.rgb, material.transmission ); +#endif`,px=`#ifdef USE_TRANSMISSION + uniform float transmission; + uniform float thickness; + uniform float attenuationDistance; + uniform vec3 attenuationColor; + #ifdef USE_TRANSMISSIONMAP + uniform sampler2D transmissionMap; + #endif + #ifdef USE_THICKNESSMAP + uniform sampler2D thicknessMap; + #endif + uniform vec2 transmissionSamplerSize; + uniform sampler2D transmissionSamplerMap; + uniform mat4 modelMatrix; + uniform mat4 projectionMatrix; + varying vec3 vWorldPosition; + float w0( float a ) { + return ( 1.0 / 6.0 ) * ( a * ( a * ( - a + 3.0 ) - 3.0 ) + 1.0 ); + } + float w1( float a ) { + return ( 1.0 / 6.0 ) * ( a * a * ( 3.0 * a - 6.0 ) + 4.0 ); + } + float w2( float a ){ + return ( 1.0 / 6.0 ) * ( a * ( a * ( - 3.0 * a + 3.0 ) + 3.0 ) + 1.0 ); + } + float w3( float a ) { + return ( 1.0 / 6.0 ) * ( a * a * a ); + } + float g0( float a ) { + return w0( a ) + w1( a ); + } + float g1( float a ) { + return w2( a ) + w3( a ); + } + float h0( float a ) { + return - 1.0 + w1( a ) / ( w0( a ) + w1( a ) ); + } + float h1( float a ) { + return 1.0 + w3( a ) / ( w2( a ) + w3( a ) ); + } + vec4 bicubic( sampler2D tex, vec2 uv, vec4 texelSize, float lod ) { + uv = uv * texelSize.zw + 0.5; + vec2 iuv = floor( uv ); + vec2 fuv = fract( uv ); + float g0x = g0( fuv.x ); + float g1x = g1( fuv.x ); + float h0x = h0( fuv.x ); + float h1x = h1( fuv.x ); + float h0y = h0( fuv.y ); + float h1y = h1( fuv.y ); + vec2 p0 = ( vec2( iuv.x + h0x, iuv.y + h0y ) - 0.5 ) * texelSize.xy; + vec2 p1 = ( vec2( iuv.x + h1x, iuv.y + h0y ) - 0.5 ) * texelSize.xy; + vec2 p2 = ( vec2( iuv.x + h0x, iuv.y + h1y ) - 0.5 ) * texelSize.xy; + vec2 p3 = ( vec2( iuv.x + h1x, iuv.y + h1y ) - 0.5 ) * texelSize.xy; + return g0( fuv.y ) * ( g0x * textureLod( tex, p0, lod ) + g1x * textureLod( tex, p1, lod ) ) + + g1( fuv.y ) * ( g0x * textureLod( tex, p2, lod ) + g1x * textureLod( tex, p3, lod ) ); + } + vec4 textureBicubic( sampler2D sampler, vec2 uv, float lod ) { + vec2 fLodSize = vec2( textureSize( sampler, int( lod ) ) ); + vec2 cLodSize = vec2( textureSize( sampler, int( lod + 1.0 ) ) ); + vec2 fLodSizeInv = 1.0 / fLodSize; + vec2 cLodSizeInv = 1.0 / cLodSize; + vec4 fSample = bicubic( sampler, uv, vec4( fLodSizeInv, fLodSize ), floor( lod ) ); + vec4 cSample = bicubic( sampler, uv, vec4( cLodSizeInv, cLodSize ), ceil( lod ) ); + return mix( fSample, cSample, fract( lod ) ); + } + vec3 getVolumeTransmissionRay( const in vec3 n, const in vec3 v, const in float thickness, const in float ior, const in mat4 modelMatrix ) { + vec3 refractionVector = refract( - v, normalize( n ), 1.0 / ior ); + vec3 modelScale; + modelScale.x = length( vec3( modelMatrix[ 0 ].xyz ) ); + modelScale.y = length( vec3( modelMatrix[ 1 ].xyz ) ); + modelScale.z = length( vec3( modelMatrix[ 2 ].xyz ) ); + return normalize( refractionVector ) * thickness * modelScale; + } + float applyIorToRoughness( const in float roughness, const in float ior ) { + return roughness * clamp( ior * 2.0 - 2.0, 0.0, 1.0 ); + } + vec4 getTransmissionSample( const in vec2 fragCoord, const in float roughness, const in float ior ) { + float lod = log2( transmissionSamplerSize.x ) * applyIorToRoughness( roughness, ior ); + return textureBicubic( transmissionSamplerMap, fragCoord.xy, lod ); + } + vec3 volumeAttenuation( const in float transmissionDistance, const in vec3 attenuationColor, const in float attenuationDistance ) { + if ( isinf( attenuationDistance ) ) { + return vec3( 1.0 ); + } else { + vec3 attenuationCoefficient = -log( attenuationColor ) / attenuationDistance; + vec3 transmittance = exp( - attenuationCoefficient * transmissionDistance ); return transmittance; + } + } + vec4 getIBLVolumeRefraction( const in vec3 n, const in vec3 v, const in float roughness, const in vec3 diffuseColor, + const in vec3 specularColor, const in float specularF90, const in vec3 position, const in mat4 modelMatrix, + const in mat4 viewMatrix, const in mat4 projMatrix, const in float dispersion, const in float ior, const in float thickness, + const in vec3 attenuationColor, const in float attenuationDistance ) { + vec4 transmittedLight; + vec3 transmittance; + #ifdef USE_DISPERSION + float halfSpread = ( ior - 1.0 ) * 0.025 * dispersion; + vec3 iors = vec3( ior - halfSpread, ior, ior + halfSpread ); + for ( int i = 0; i < 3; i ++ ) { + vec3 transmissionRay = getVolumeTransmissionRay( n, v, thickness, iors[ i ], modelMatrix ); + vec3 refractedRayExit = position + transmissionRay; + vec4 ndcPos = projMatrix * viewMatrix * vec4( refractedRayExit, 1.0 ); + vec2 refractionCoords = ndcPos.xy / ndcPos.w; + refractionCoords += 1.0; + refractionCoords /= 2.0; + vec4 transmissionSample = getTransmissionSample( refractionCoords, roughness, iors[ i ] ); + transmittedLight[ i ] = transmissionSample[ i ]; + transmittedLight.a += transmissionSample.a; + transmittance[ i ] = diffuseColor[ i ] * volumeAttenuation( length( transmissionRay ), attenuationColor, attenuationDistance )[ i ]; + } + transmittedLight.a /= 3.0; + #else + vec3 transmissionRay = getVolumeTransmissionRay( n, v, thickness, ior, modelMatrix ); + vec3 refractedRayExit = position + transmissionRay; + vec4 ndcPos = projMatrix * viewMatrix * vec4( refractedRayExit, 1.0 ); + vec2 refractionCoords = ndcPos.xy / ndcPos.w; + refractionCoords += 1.0; + refractionCoords /= 2.0; + transmittedLight = getTransmissionSample( refractionCoords, roughness, ior ); + transmittance = diffuseColor * volumeAttenuation( length( transmissionRay ), attenuationColor, attenuationDistance ); + #endif + vec3 attenuatedColor = transmittance * transmittedLight.rgb; + vec3 F = EnvironmentBRDF( n, v, specularColor, specularF90, roughness ); + float transmittanceFactor = ( transmittance.r + transmittance.g + transmittance.b ) / 3.0; + return vec4( ( 1.0 - F ) * attenuatedColor, 1.0 - ( 1.0 - transmittedLight.a ) * transmittanceFactor ); + } +#endif`,mx=`#if defined( USE_UV ) || defined( USE_ANISOTROPY ) + varying vec2 vUv; +#endif +#ifdef USE_MAP + varying vec2 vMapUv; +#endif +#ifdef USE_ALPHAMAP + varying vec2 vAlphaMapUv; +#endif +#ifdef USE_LIGHTMAP + varying vec2 vLightMapUv; +#endif +#ifdef USE_AOMAP + varying vec2 vAoMapUv; +#endif +#ifdef USE_BUMPMAP + varying vec2 vBumpMapUv; +#endif +#ifdef USE_NORMALMAP + varying vec2 vNormalMapUv; +#endif +#ifdef USE_EMISSIVEMAP + varying vec2 vEmissiveMapUv; +#endif +#ifdef USE_METALNESSMAP + varying vec2 vMetalnessMapUv; +#endif +#ifdef USE_ROUGHNESSMAP + varying vec2 vRoughnessMapUv; +#endif +#ifdef USE_ANISOTROPYMAP + varying vec2 vAnisotropyMapUv; +#endif +#ifdef USE_CLEARCOATMAP + varying vec2 vClearcoatMapUv; +#endif +#ifdef USE_CLEARCOAT_NORMALMAP + varying vec2 vClearcoatNormalMapUv; +#endif +#ifdef USE_CLEARCOAT_ROUGHNESSMAP + varying vec2 vClearcoatRoughnessMapUv; +#endif +#ifdef USE_IRIDESCENCEMAP + varying vec2 vIridescenceMapUv; +#endif +#ifdef USE_IRIDESCENCE_THICKNESSMAP + varying vec2 vIridescenceThicknessMapUv; +#endif +#ifdef USE_SHEEN_COLORMAP + varying vec2 vSheenColorMapUv; +#endif +#ifdef USE_SHEEN_ROUGHNESSMAP + varying vec2 vSheenRoughnessMapUv; +#endif +#ifdef USE_SPECULARMAP + varying vec2 vSpecularMapUv; +#endif +#ifdef USE_SPECULAR_COLORMAP + varying vec2 vSpecularColorMapUv; +#endif +#ifdef USE_SPECULAR_INTENSITYMAP + varying vec2 vSpecularIntensityMapUv; +#endif +#ifdef USE_TRANSMISSIONMAP + uniform mat3 transmissionMapTransform; + varying vec2 vTransmissionMapUv; +#endif +#ifdef USE_THICKNESSMAP + uniform mat3 thicknessMapTransform; + varying vec2 vThicknessMapUv; +#endif`,gx=`#if defined( USE_UV ) || defined( USE_ANISOTROPY ) + varying vec2 vUv; +#endif +#ifdef USE_MAP + uniform mat3 mapTransform; + varying vec2 vMapUv; +#endif +#ifdef USE_ALPHAMAP + uniform mat3 alphaMapTransform; + varying vec2 vAlphaMapUv; +#endif +#ifdef USE_LIGHTMAP + uniform mat3 lightMapTransform; + varying vec2 vLightMapUv; +#endif +#ifdef USE_AOMAP + uniform mat3 aoMapTransform; + varying vec2 vAoMapUv; +#endif +#ifdef USE_BUMPMAP + uniform mat3 bumpMapTransform; + varying vec2 vBumpMapUv; +#endif +#ifdef USE_NORMALMAP + uniform mat3 normalMapTransform; + varying vec2 vNormalMapUv; +#endif +#ifdef USE_DISPLACEMENTMAP + uniform mat3 displacementMapTransform; + varying vec2 vDisplacementMapUv; +#endif +#ifdef USE_EMISSIVEMAP + uniform mat3 emissiveMapTransform; + varying vec2 vEmissiveMapUv; +#endif +#ifdef USE_METALNESSMAP + uniform mat3 metalnessMapTransform; + varying vec2 vMetalnessMapUv; +#endif +#ifdef USE_ROUGHNESSMAP + uniform mat3 roughnessMapTransform; + varying vec2 vRoughnessMapUv; +#endif +#ifdef USE_ANISOTROPYMAP + uniform mat3 anisotropyMapTransform; + varying vec2 vAnisotropyMapUv; +#endif +#ifdef USE_CLEARCOATMAP + uniform mat3 clearcoatMapTransform; + varying vec2 vClearcoatMapUv; +#endif +#ifdef USE_CLEARCOAT_NORMALMAP + uniform mat3 clearcoatNormalMapTransform; + varying vec2 vClearcoatNormalMapUv; +#endif +#ifdef USE_CLEARCOAT_ROUGHNESSMAP + uniform mat3 clearcoatRoughnessMapTransform; + varying vec2 vClearcoatRoughnessMapUv; +#endif +#ifdef USE_SHEEN_COLORMAP + uniform mat3 sheenColorMapTransform; + varying vec2 vSheenColorMapUv; +#endif +#ifdef USE_SHEEN_ROUGHNESSMAP + uniform mat3 sheenRoughnessMapTransform; + varying vec2 vSheenRoughnessMapUv; +#endif +#ifdef USE_IRIDESCENCEMAP + uniform mat3 iridescenceMapTransform; + varying vec2 vIridescenceMapUv; +#endif +#ifdef USE_IRIDESCENCE_THICKNESSMAP + uniform mat3 iridescenceThicknessMapTransform; + varying vec2 vIridescenceThicknessMapUv; +#endif +#ifdef USE_SPECULARMAP + uniform mat3 specularMapTransform; + varying vec2 vSpecularMapUv; +#endif +#ifdef USE_SPECULAR_COLORMAP + uniform mat3 specularColorMapTransform; + varying vec2 vSpecularColorMapUv; +#endif +#ifdef USE_SPECULAR_INTENSITYMAP + uniform mat3 specularIntensityMapTransform; + varying vec2 vSpecularIntensityMapUv; +#endif +#ifdef USE_TRANSMISSIONMAP + uniform mat3 transmissionMapTransform; + varying vec2 vTransmissionMapUv; +#endif +#ifdef USE_THICKNESSMAP + uniform mat3 thicknessMapTransform; + varying vec2 vThicknessMapUv; +#endif`,xx=`#if defined( USE_UV ) || defined( USE_ANISOTROPY ) + vUv = vec3( uv, 1 ).xy; +#endif +#ifdef USE_MAP + vMapUv = ( mapTransform * vec3( MAP_UV, 1 ) ).xy; +#endif +#ifdef USE_ALPHAMAP + vAlphaMapUv = ( alphaMapTransform * vec3( ALPHAMAP_UV, 1 ) ).xy; +#endif +#ifdef USE_LIGHTMAP + vLightMapUv = ( lightMapTransform * vec3( LIGHTMAP_UV, 1 ) ).xy; +#endif +#ifdef USE_AOMAP + vAoMapUv = ( aoMapTransform * vec3( AOMAP_UV, 1 ) ).xy; +#endif +#ifdef USE_BUMPMAP + vBumpMapUv = ( bumpMapTransform * vec3( BUMPMAP_UV, 1 ) ).xy; +#endif +#ifdef USE_NORMALMAP + vNormalMapUv = ( normalMapTransform * vec3( NORMALMAP_UV, 1 ) ).xy; +#endif +#ifdef USE_DISPLACEMENTMAP + vDisplacementMapUv = ( displacementMapTransform * vec3( DISPLACEMENTMAP_UV, 1 ) ).xy; +#endif +#ifdef USE_EMISSIVEMAP + vEmissiveMapUv = ( emissiveMapTransform * vec3( EMISSIVEMAP_UV, 1 ) ).xy; +#endif +#ifdef USE_METALNESSMAP + vMetalnessMapUv = ( metalnessMapTransform * vec3( METALNESSMAP_UV, 1 ) ).xy; +#endif +#ifdef USE_ROUGHNESSMAP + vRoughnessMapUv = ( roughnessMapTransform * vec3( ROUGHNESSMAP_UV, 1 ) ).xy; +#endif +#ifdef USE_ANISOTROPYMAP + vAnisotropyMapUv = ( anisotropyMapTransform * vec3( ANISOTROPYMAP_UV, 1 ) ).xy; +#endif +#ifdef USE_CLEARCOATMAP + vClearcoatMapUv = ( clearcoatMapTransform * vec3( CLEARCOATMAP_UV, 1 ) ).xy; +#endif +#ifdef USE_CLEARCOAT_NORMALMAP + vClearcoatNormalMapUv = ( clearcoatNormalMapTransform * vec3( CLEARCOAT_NORMALMAP_UV, 1 ) ).xy; +#endif +#ifdef USE_CLEARCOAT_ROUGHNESSMAP + vClearcoatRoughnessMapUv = ( clearcoatRoughnessMapTransform * vec3( CLEARCOAT_ROUGHNESSMAP_UV, 1 ) ).xy; +#endif +#ifdef USE_IRIDESCENCEMAP + vIridescenceMapUv = ( iridescenceMapTransform * vec3( IRIDESCENCEMAP_UV, 1 ) ).xy; +#endif +#ifdef USE_IRIDESCENCE_THICKNESSMAP + vIridescenceThicknessMapUv = ( iridescenceThicknessMapTransform * vec3( IRIDESCENCE_THICKNESSMAP_UV, 1 ) ).xy; +#endif +#ifdef USE_SHEEN_COLORMAP + vSheenColorMapUv = ( sheenColorMapTransform * vec3( SHEEN_COLORMAP_UV, 1 ) ).xy; +#endif +#ifdef USE_SHEEN_ROUGHNESSMAP + vSheenRoughnessMapUv = ( sheenRoughnessMapTransform * vec3( SHEEN_ROUGHNESSMAP_UV, 1 ) ).xy; +#endif +#ifdef USE_SPECULARMAP + vSpecularMapUv = ( specularMapTransform * vec3( SPECULARMAP_UV, 1 ) ).xy; +#endif +#ifdef USE_SPECULAR_COLORMAP + vSpecularColorMapUv = ( specularColorMapTransform * vec3( SPECULAR_COLORMAP_UV, 1 ) ).xy; +#endif +#ifdef USE_SPECULAR_INTENSITYMAP + vSpecularIntensityMapUv = ( specularIntensityMapTransform * vec3( SPECULAR_INTENSITYMAP_UV, 1 ) ).xy; +#endif +#ifdef USE_TRANSMISSIONMAP + vTransmissionMapUv = ( transmissionMapTransform * vec3( TRANSMISSIONMAP_UV, 1 ) ).xy; +#endif +#ifdef USE_THICKNESSMAP + vThicknessMapUv = ( thicknessMapTransform * vec3( THICKNESSMAP_UV, 1 ) ).xy; +#endif`,vx=`#if defined( USE_ENVMAP ) || defined( DISTANCE ) || defined ( USE_SHADOWMAP ) || defined ( USE_TRANSMISSION ) || NUM_SPOT_LIGHT_COORDS > 0 + vec4 worldPosition = vec4( transformed, 1.0 ); + #ifdef USE_BATCHING + worldPosition = batchingMatrix * worldPosition; + #endif + #ifdef USE_INSTANCING + worldPosition = instanceMatrix * worldPosition; + #endif + worldPosition = modelMatrix * worldPosition; +#endif`,_x=`varying vec2 vUv; +uniform mat3 uvTransform; +void main() { + vUv = ( uvTransform * vec3( uv, 1 ) ).xy; + gl_Position = vec4( position.xy, 1.0, 1.0 ); +}`,yx=`uniform sampler2D t2D; +uniform float backgroundIntensity; +varying vec2 vUv; +void main() { + vec4 texColor = texture2D( t2D, vUv ); + #ifdef DECODE_VIDEO_TEXTURE + texColor = vec4( mix( pow( texColor.rgb * 0.9478672986 + vec3( 0.0521327014 ), vec3( 2.4 ) ), texColor.rgb * 0.0773993808, vec3( lessThanEqual( texColor.rgb, vec3( 0.04045 ) ) ) ), texColor.w ); + #endif + texColor.rgb *= backgroundIntensity; + gl_FragColor = texColor; + #include + #include +}`,bx=`varying vec3 vWorldDirection; +#include +void main() { + vWorldDirection = transformDirection( position, modelMatrix ); + #include + #include + gl_Position.z = gl_Position.w; +}`,Mx=`#ifdef ENVMAP_TYPE_CUBE + uniform samplerCube envMap; +#elif defined( ENVMAP_TYPE_CUBE_UV ) + uniform sampler2D envMap; +#endif +uniform float backgroundBlurriness; +uniform float backgroundIntensity; +uniform mat3 backgroundRotation; +varying vec3 vWorldDirection; +#include +void main() { + #ifdef ENVMAP_TYPE_CUBE + vec4 texColor = textureCube( envMap, backgroundRotation * vWorldDirection ); + #elif defined( ENVMAP_TYPE_CUBE_UV ) + vec4 texColor = textureCubeUV( envMap, backgroundRotation * vWorldDirection, backgroundBlurriness ); + #else + vec4 texColor = vec4( 0.0, 0.0, 0.0, 1.0 ); + #endif + texColor.rgb *= backgroundIntensity; + gl_FragColor = texColor; + #include + #include +}`,Sx=`varying vec3 vWorldDirection; +#include +void main() { + vWorldDirection = transformDirection( position, modelMatrix ); + #include + #include + gl_Position.z = gl_Position.w; +}`,wx=`uniform samplerCube tCube; +uniform float tFlip; +uniform float opacity; +varying vec3 vWorldDirection; +void main() { + vec4 texColor = textureCube( tCube, vec3( tFlip * vWorldDirection.x, vWorldDirection.yz ) ); + gl_FragColor = texColor; + gl_FragColor.a *= opacity; + #include + #include +}`,Ex=`#include +#include +#include +#include +#include +#include +#include +#include +varying vec2 vHighPrecisionZW; +void main() { + #include + #include + #include + #include + #ifdef USE_DISPLACEMENTMAP + #include + #include + #include + #endif + #include + #include + #include + #include + #include + #include + #include + vHighPrecisionZW = gl_Position.zw; +}`,Tx=`#if DEPTH_PACKING == 3200 + uniform float opacity; +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +varying vec2 vHighPrecisionZW; +void main() { + vec4 diffuseColor = vec4( 1.0 ); + #include + #if DEPTH_PACKING == 3200 + diffuseColor.a = opacity; + #endif + #include + #include + #include + #include + #include + #ifdef USE_REVERSED_DEPTH_BUFFER + float fragCoordZ = vHighPrecisionZW[ 0 ] / vHighPrecisionZW[ 1 ]; + #else + float fragCoordZ = 0.5 * vHighPrecisionZW[ 0 ] / vHighPrecisionZW[ 1 ] + 0.5; + #endif + #if DEPTH_PACKING == 3200 + gl_FragColor = vec4( vec3( 1.0 - fragCoordZ ), opacity ); + #elif DEPTH_PACKING == 3201 + gl_FragColor = packDepthToRGBA( fragCoordZ ); + #elif DEPTH_PACKING == 3202 + gl_FragColor = vec4( packDepthToRGB( fragCoordZ ), 1.0 ); + #elif DEPTH_PACKING == 3203 + gl_FragColor = vec4( packDepthToRG( fragCoordZ ), 0.0, 1.0 ); + #endif +}`,Ax=`#define DISTANCE +varying vec3 vWorldPosition; +#include +#include +#include +#include +#include +#include +#include +void main() { + #include + #include + #include + #include + #ifdef USE_DISPLACEMENTMAP + #include + #include + #include + #endif + #include + #include + #include + #include + #include + #include + #include + vWorldPosition = worldPosition.xyz; +}`,Cx=`#define DISTANCE +uniform vec3 referencePosition; +uniform float nearDistance; +uniform float farDistance; +varying vec3 vWorldPosition; +#include +#include +#include +#include +#include +#include +#include +void main () { + vec4 diffuseColor = vec4( 1.0 ); + #include + #include + #include + #include + #include + float dist = length( vWorldPosition - referencePosition ); + dist = ( dist - nearDistance ) / ( farDistance - nearDistance ); + dist = saturate( dist ); + gl_FragColor = vec4( dist, 0.0, 0.0, 1.0 ); +}`,Rx=`varying vec3 vWorldDirection; +#include +void main() { + vWorldDirection = transformDirection( position, modelMatrix ); + #include + #include +}`,Px=`uniform sampler2D tEquirect; +varying vec3 vWorldDirection; +#include +void main() { + vec3 direction = normalize( vWorldDirection ); + vec2 sampleUV = equirectUv( direction ); + gl_FragColor = texture2D( tEquirect, sampleUV ); + #include + #include +}`,Ix=`uniform float scale; +attribute float lineDistance; +varying float vLineDistance; +#include +#include +#include +#include +#include +#include +#include +void main() { + vLineDistance = scale * lineDistance; + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include +}`,Lx=`uniform vec3 diffuse; +uniform float opacity; +uniform float dashSize; +uniform float totalSize; +varying float vLineDistance; +#include +#include +#include +#include +#include +#include +#include +void main() { + vec4 diffuseColor = vec4( diffuse, opacity ); + #include + if ( mod( vLineDistance, totalSize ) > dashSize ) { + discard; + } + vec3 outgoingLight = vec3( 0.0 ); + #include + #include + #include + outgoingLight = diffuseColor.rgb; + #include + #include + #include + #include + #include +}`,Nx=`#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +void main() { + #include + #include + #include + #include + #include + #if defined ( USE_ENVMAP ) || defined ( USE_SKINNING ) + #include + #include + #include + #include + #include + #endif + #include + #include + #include + #include + #include + #include + #include + #include + #include +}`,Dx=`uniform vec3 diffuse; +uniform float opacity; +#ifndef FLAT_SHADED + varying vec3 vNormal; +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +void main() { + vec4 diffuseColor = vec4( diffuse, opacity ); + #include + #include + #include + #include + #include + #include + #include + #include + ReflectedLight reflectedLight = ReflectedLight( vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ) ); + #ifdef USE_LIGHTMAP + vec4 lightMapTexel = texture2D( lightMap, vLightMapUv ); + reflectedLight.indirectDiffuse += lightMapTexel.rgb * lightMapIntensity * RECIPROCAL_PI; + #else + reflectedLight.indirectDiffuse += vec3( 1.0 ); + #endif + #include + reflectedLight.indirectDiffuse *= diffuseColor.rgb; + vec3 outgoingLight = reflectedLight.indirectDiffuse; + #include + #include + #include + #include + #include + #include + #include +}`,Fx=`#define LAMBERT +varying vec3 vViewPosition; +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +void main() { + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + vViewPosition = - mvPosition.xyz; + #include + #include + #include + #include +}`,kx=`#define LAMBERT +uniform vec3 diffuse; +uniform vec3 emissive; +uniform float opacity; +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +void main() { + vec4 diffuseColor = vec4( diffuse, opacity ); + #include + ReflectedLight reflectedLight = ReflectedLight( vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ) ); + vec3 totalEmissiveRadiance = emissive; + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + vec3 outgoingLight = reflectedLight.directDiffuse + reflectedLight.indirectDiffuse + totalEmissiveRadiance; + #include + #include + #include + #include + #include + #include + #include +}`,Ox=`#define MATCAP +varying vec3 vViewPosition; +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +void main() { + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + vViewPosition = - mvPosition.xyz; +}`,Ux=`#define MATCAP +uniform vec3 diffuse; +uniform float opacity; +uniform sampler2D matcap; +varying vec3 vViewPosition; +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +void main() { + vec4 diffuseColor = vec4( diffuse, opacity ); + #include + #include + #include + #include + #include + #include + #include + #include + #include + vec3 viewDir = normalize( vViewPosition ); + vec3 x = normalize( vec3( viewDir.z, 0.0, - viewDir.x ) ); + vec3 y = cross( viewDir, x ); + vec2 uv = vec2( dot( x, normal ), dot( y, normal ) ) * 0.495 + 0.5; + #ifdef USE_MATCAP + vec4 matcapColor = texture2D( matcap, uv ); + #else + vec4 matcapColor = vec4( vec3( mix( 0.2, 0.8, uv.y ) ), 1.0 ); + #endif + vec3 outgoingLight = diffuseColor.rgb * matcapColor.rgb; + #include + #include + #include + #include + #include + #include +}`,Bx=`#define NORMAL +#if defined( FLAT_SHADED ) || defined( USE_BUMPMAP ) || defined( USE_NORMALMAP_TANGENTSPACE ) + varying vec3 vViewPosition; +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +void main() { + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include +#if defined( FLAT_SHADED ) || defined( USE_BUMPMAP ) || defined( USE_NORMALMAP_TANGENTSPACE ) + vViewPosition = - mvPosition.xyz; +#endif +}`,zx=`#define NORMAL +uniform float opacity; +#if defined( FLAT_SHADED ) || defined( USE_BUMPMAP ) || defined( USE_NORMALMAP_TANGENTSPACE ) + varying vec3 vViewPosition; +#endif +#include +#include +#include +#include +#include +#include +void main() { + vec4 diffuseColor = vec4( 0.0, 0.0, 0.0, opacity ); + #include + #include + #include + #include + gl_FragColor = vec4( normalize( normal ) * 0.5 + 0.5, diffuseColor.a ); + #ifdef OPAQUE + gl_FragColor.a = 1.0; + #endif +}`,Vx=`#define PHONG +varying vec3 vViewPosition; +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +void main() { + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + vViewPosition = - mvPosition.xyz; + #include + #include + #include + #include +}`,Gx=`#define PHONG +uniform vec3 diffuse; +uniform vec3 emissive; +uniform vec3 specular; +uniform float shininess; +uniform float opacity; +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +void main() { + vec4 diffuseColor = vec4( diffuse, opacity ); + #include + ReflectedLight reflectedLight = ReflectedLight( vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ) ); + vec3 totalEmissiveRadiance = emissive; + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + vec3 outgoingLight = reflectedLight.directDiffuse + reflectedLight.indirectDiffuse + reflectedLight.directSpecular + reflectedLight.indirectSpecular + totalEmissiveRadiance; + #include + #include + #include + #include + #include + #include + #include +}`,Hx=`#define STANDARD +varying vec3 vViewPosition; +#ifdef USE_TRANSMISSION + varying vec3 vWorldPosition; +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +void main() { + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + vViewPosition = - mvPosition.xyz; + #include + #include + #include +#ifdef USE_TRANSMISSION + vWorldPosition = worldPosition.xyz; +#endif +}`,Wx=`#define STANDARD +#ifdef PHYSICAL + #define IOR + #define USE_SPECULAR +#endif +uniform vec3 diffuse; +uniform vec3 emissive; +uniform float roughness; +uniform float metalness; +uniform float opacity; +#ifdef IOR + uniform float ior; +#endif +#ifdef USE_SPECULAR + uniform float specularIntensity; + uniform vec3 specularColor; + #ifdef USE_SPECULAR_COLORMAP + uniform sampler2D specularColorMap; + #endif + #ifdef USE_SPECULAR_INTENSITYMAP + uniform sampler2D specularIntensityMap; + #endif +#endif +#ifdef USE_CLEARCOAT + uniform float clearcoat; + uniform float clearcoatRoughness; +#endif +#ifdef USE_DISPERSION + uniform float dispersion; +#endif +#ifdef USE_IRIDESCENCE + uniform float iridescence; + uniform float iridescenceIOR; + uniform float iridescenceThicknessMinimum; + uniform float iridescenceThicknessMaximum; +#endif +#ifdef USE_SHEEN + uniform vec3 sheenColor; + uniform float sheenRoughness; + #ifdef USE_SHEEN_COLORMAP + uniform sampler2D sheenColorMap; + #endif + #ifdef USE_SHEEN_ROUGHNESSMAP + uniform sampler2D sheenRoughnessMap; + #endif +#endif +#ifdef USE_ANISOTROPY + uniform vec2 anisotropyVector; + #ifdef USE_ANISOTROPYMAP + uniform sampler2D anisotropyMap; + #endif +#endif +varying vec3 vViewPosition; +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +void main() { + vec4 diffuseColor = vec4( diffuse, opacity ); + #include + ReflectedLight reflectedLight = ReflectedLight( vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ) ); + vec3 totalEmissiveRadiance = emissive; + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + vec3 totalDiffuse = reflectedLight.directDiffuse + reflectedLight.indirectDiffuse; + vec3 totalSpecular = reflectedLight.directSpecular + reflectedLight.indirectSpecular; + #include + vec3 outgoingLight = totalDiffuse + totalSpecular + totalEmissiveRadiance; + #ifdef USE_SHEEN + + outgoingLight = outgoingLight + sheenSpecularDirect + sheenSpecularIndirect; + + #endif + #ifdef USE_CLEARCOAT + float dotNVcc = saturate( dot( geometryClearcoatNormal, geometryViewDir ) ); + vec3 Fcc = F_Schlick( material.clearcoatF0, material.clearcoatF90, dotNVcc ); + outgoingLight = outgoingLight * ( 1.0 - material.clearcoat * Fcc ) + ( clearcoatSpecularDirect + clearcoatSpecularIndirect ) * material.clearcoat; + #endif + #include + #include + #include + #include + #include + #include +}`,Xx=`#define TOON +varying vec3 vViewPosition; +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +void main() { + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + vViewPosition = - mvPosition.xyz; + #include + #include + #include +}`,qx=`#define TOON +uniform vec3 diffuse; +uniform vec3 emissive; +uniform float opacity; +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +void main() { + vec4 diffuseColor = vec4( diffuse, opacity ); + #include + ReflectedLight reflectedLight = ReflectedLight( vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ) ); + vec3 totalEmissiveRadiance = emissive; + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + vec3 outgoingLight = reflectedLight.directDiffuse + reflectedLight.indirectDiffuse + totalEmissiveRadiance; + #include + #include + #include + #include + #include + #include +}`,Yx=`uniform float size; +uniform float scale; +#include +#include +#include +#include +#include +#include +#ifdef USE_POINTS_UV + varying vec2 vUv; + uniform mat3 uvTransform; +#endif +void main() { + #ifdef USE_POINTS_UV + vUv = ( uvTransform * vec3( uv, 1 ) ).xy; + #endif + #include + #include + #include + #include + #include + #include + gl_PointSize = size; + #ifdef USE_SIZEATTENUATION + bool isPerspective = isPerspectiveMatrix( projectionMatrix ); + if ( isPerspective ) gl_PointSize *= ( scale / - mvPosition.z ); + #endif + #include + #include + #include + #include +}`,Zx=`uniform vec3 diffuse; +uniform float opacity; +#include +#include +#include +#include +#include +#include +#include +#include +void main() { + vec4 diffuseColor = vec4( diffuse, opacity ); + #include + vec3 outgoingLight = vec3( 0.0 ); + #include + #include + #include + #include + #include + outgoingLight = diffuseColor.rgb; + #include + #include + #include + #include + #include +}`,$x=`#include +#include +#include +#include +#include +#include +#include +void main() { + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include +}`,Kx=`uniform vec3 color; +uniform float opacity; +#include +#include +#include +#include +#include +#include +#include +void main() { + #include + gl_FragColor = vec4( color, opacity * ( 1.0 - getShadowMask() ) ); + #include + #include + #include + #include +}`,Jx=`uniform float rotation; +uniform vec2 center; +#include +#include +#include +#include +#include +void main() { + #include + vec4 mvPosition = modelViewMatrix[ 3 ]; + vec2 scale = vec2( length( modelMatrix[ 0 ].xyz ), length( modelMatrix[ 1 ].xyz ) ); + #ifndef USE_SIZEATTENUATION + bool isPerspective = isPerspectiveMatrix( projectionMatrix ); + if ( isPerspective ) scale *= - mvPosition.z; + #endif + vec2 alignedPosition = ( position.xy - ( center - vec2( 0.5 ) ) ) * scale; + vec2 rotatedPosition; + rotatedPosition.x = cos( rotation ) * alignedPosition.x - sin( rotation ) * alignedPosition.y; + rotatedPosition.y = sin( rotation ) * alignedPosition.x + cos( rotation ) * alignedPosition.y; + mvPosition.xy += rotatedPosition; + gl_Position = projectionMatrix * mvPosition; + #include + #include + #include +}`,jx=`uniform vec3 diffuse; +uniform float opacity; +#include +#include +#include +#include +#include +#include +#include +#include +#include +void main() { + vec4 diffuseColor = vec4( diffuse, opacity ); + #include + vec3 outgoingLight = vec3( 0.0 ); + #include + #include + #include + #include + #include + outgoingLight = diffuseColor.rgb; + #include + #include + #include + #include +}`,Ue={alphahash_fragment:_g,alphahash_pars_fragment:yg,alphamap_fragment:bg,alphamap_pars_fragment:Mg,alphatest_fragment:Sg,alphatest_pars_fragment:wg,aomap_fragment:Eg,aomap_pars_fragment:Tg,batching_pars_vertex:Ag,batching_vertex:Cg,begin_vertex:Rg,beginnormal_vertex:Pg,bsdfs:Ig,iridescence_fragment:Lg,bumpmap_pars_fragment:Ng,clipping_planes_fragment:Dg,clipping_planes_pars_fragment:Fg,clipping_planes_pars_vertex:kg,clipping_planes_vertex:Og,color_fragment:Ug,color_pars_fragment:Bg,color_pars_vertex:zg,color_vertex:Vg,common:Gg,cube_uv_reflection_fragment:Hg,defaultnormal_vertex:Wg,displacementmap_pars_vertex:Xg,displacementmap_vertex:qg,emissivemap_fragment:Yg,emissivemap_pars_fragment:Zg,colorspace_fragment:$g,colorspace_pars_fragment:Kg,envmap_fragment:Jg,envmap_common_pars_fragment:jg,envmap_pars_fragment:Qg,envmap_pars_vertex:e0,envmap_physical_pars_fragment:u0,envmap_vertex:t0,fog_vertex:n0,fog_pars_vertex:i0,fog_fragment:s0,fog_pars_fragment:r0,gradientmap_pars_fragment:o0,lightmap_pars_fragment:a0,lights_lambert_fragment:l0,lights_lambert_pars_fragment:c0,lights_pars_begin:h0,lights_toon_fragment:d0,lights_toon_pars_fragment:f0,lights_phong_fragment:p0,lights_phong_pars_fragment:m0,lights_physical_fragment:g0,lights_physical_pars_fragment:x0,lights_fragment_begin:v0,lights_fragment_maps:_0,lights_fragment_end:y0,lightprobes_pars_fragment:b0,logdepthbuf_fragment:M0,logdepthbuf_pars_fragment:S0,logdepthbuf_pars_vertex:w0,logdepthbuf_vertex:E0,map_fragment:T0,map_pars_fragment:A0,map_particle_fragment:C0,map_particle_pars_fragment:R0,metalnessmap_fragment:P0,metalnessmap_pars_fragment:I0,morphinstance_vertex:L0,morphcolor_vertex:N0,morphnormal_vertex:D0,morphtarget_pars_vertex:F0,morphtarget_vertex:k0,normal_fragment_begin:O0,normal_fragment_maps:U0,normal_pars_fragment:B0,normal_pars_vertex:z0,normal_vertex:V0,normalmap_pars_fragment:G0,clearcoat_normal_fragment_begin:H0,clearcoat_normal_fragment_maps:W0,clearcoat_pars_fragment:X0,iridescence_pars_fragment:q0,opaque_fragment:Y0,packing:Z0,premultiplied_alpha_fragment:$0,project_vertex:K0,dithering_fragment:J0,dithering_pars_fragment:j0,roughnessmap_fragment:Q0,roughnessmap_pars_fragment:ex,shadowmap_pars_fragment:tx,shadowmap_pars_vertex:nx,shadowmap_vertex:ix,shadowmask_pars_fragment:sx,skinbase_vertex:rx,skinning_pars_vertex:ox,skinning_vertex:ax,skinnormal_vertex:lx,specularmap_fragment:cx,specularmap_pars_fragment:hx,tonemapping_fragment:ux,tonemapping_pars_fragment:dx,transmission_fragment:fx,transmission_pars_fragment:px,uv_pars_fragment:mx,uv_pars_vertex:gx,uv_vertex:xx,worldpos_vertex:vx,background_vert:_x,background_frag:yx,backgroundCube_vert:bx,backgroundCube_frag:Mx,cube_vert:Sx,cube_frag:wx,depth_vert:Ex,depth_frag:Tx,distance_vert:Ax,distance_frag:Cx,equirect_vert:Rx,equirect_frag:Px,linedashed_vert:Ix,linedashed_frag:Lx,meshbasic_vert:Nx,meshbasic_frag:Dx,meshlambert_vert:Fx,meshlambert_frag:kx,meshmatcap_vert:Ox,meshmatcap_frag:Ux,meshnormal_vert:Bx,meshnormal_frag:zx,meshphong_vert:Vx,meshphong_frag:Gx,meshphysical_vert:Hx,meshphysical_frag:Wx,meshtoon_vert:Xx,meshtoon_frag:qx,points_vert:Yx,points_frag:Zx,shadow_vert:$x,shadow_frag:Kx,sprite_vert:Jx,sprite_frag:jx},ce={common:{diffuse:{value:new me(16777215)},opacity:{value:1},map:{value:null},mapTransform:{value:new Le},alphaMap:{value:null},alphaMapTransform:{value:new Le},alphaTest:{value:0}},specularmap:{specularMap:{value:null},specularMapTransform:{value:new Le}},envmap:{envMap:{value:null},envMapRotation:{value:new Le},reflectivity:{value:1},ior:{value:1.5},refractionRatio:{value:.98},dfgLUT:{value:null}},aomap:{aoMap:{value:null},aoMapIntensity:{value:1},aoMapTransform:{value:new Le}},lightmap:{lightMap:{value:null},lightMapIntensity:{value:1},lightMapTransform:{value:new Le}},bumpmap:{bumpMap:{value:null},bumpMapTransform:{value:new Le},bumpScale:{value:1}},normalmap:{normalMap:{value:null},normalMapTransform:{value:new Le},normalScale:{value:new Ee(1,1)}},displacementmap:{displacementMap:{value:null},displacementMapTransform:{value:new Le},displacementScale:{value:1},displacementBias:{value:0}},emissivemap:{emissiveMap:{value:null},emissiveMapTransform:{value:new Le}},metalnessmap:{metalnessMap:{value:null},metalnessMapTransform:{value:new Le}},roughnessmap:{roughnessMap:{value:null},roughnessMapTransform:{value:new Le}},gradientmap:{gradientMap:{value:null}},fog:{fogDensity:{value:25e-5},fogNear:{value:1},fogFar:{value:2e3},fogColor:{value:new me(16777215)}},lights:{ambientLightColor:{value:[]},lightProbe:{value:[]},directionalLights:{value:[],properties:{direction:{},color:{}}},directionalLightShadows:{value:[],properties:{shadowIntensity:1,shadowBias:{},shadowNormalBias:{},shadowRadius:{},shadowMapSize:{}}},directionalShadowMatrix:{value:[]},spotLights:{value:[],properties:{color:{},position:{},direction:{},distance:{},coneCos:{},penumbraCos:{},decay:{}}},spotLightShadows:{value:[],properties:{shadowIntensity:1,shadowBias:{},shadowNormalBias:{},shadowRadius:{},shadowMapSize:{}}},spotLightMap:{value:[]},spotLightMatrix:{value:[]},pointLights:{value:[],properties:{color:{},position:{},decay:{},distance:{}}},pointLightShadows:{value:[],properties:{shadowIntensity:1,shadowBias:{},shadowNormalBias:{},shadowRadius:{},shadowMapSize:{},shadowCameraNear:{},shadowCameraFar:{}}},pointShadowMatrix:{value:[]},hemisphereLights:{value:[],properties:{direction:{},skyColor:{},groundColor:{}}},rectAreaLights:{value:[],properties:{color:{},position:{},width:{},height:{}}},ltc_1:{value:null},ltc_2:{value:null},probesSH:{value:null},probesMin:{value:new N},probesMax:{value:new N},probesResolution:{value:new N}},points:{diffuse:{value:new me(16777215)},opacity:{value:1},size:{value:1},scale:{value:1},map:{value:null},alphaMap:{value:null},alphaMapTransform:{value:new Le},alphaTest:{value:0},uvTransform:{value:new Le}},sprite:{diffuse:{value:new me(16777215)},opacity:{value:1},center:{value:new Ee(.5,.5)},rotation:{value:0},map:{value:null},mapTransform:{value:new Le},alphaMap:{value:null},alphaMapTransform:{value:new Le},alphaTest:{value:0}}},Gn={basic:{uniforms:Yt([ce.common,ce.specularmap,ce.envmap,ce.aomap,ce.lightmap,ce.fog]),vertexShader:Ue.meshbasic_vert,fragmentShader:Ue.meshbasic_frag},lambert:{uniforms:Yt([ce.common,ce.specularmap,ce.envmap,ce.aomap,ce.lightmap,ce.emissivemap,ce.bumpmap,ce.normalmap,ce.displacementmap,ce.fog,ce.lights,{emissive:{value:new me(0)},envMapIntensity:{value:1}}]),vertexShader:Ue.meshlambert_vert,fragmentShader:Ue.meshlambert_frag},phong:{uniforms:Yt([ce.common,ce.specularmap,ce.envmap,ce.aomap,ce.lightmap,ce.emissivemap,ce.bumpmap,ce.normalmap,ce.displacementmap,ce.fog,ce.lights,{emissive:{value:new me(0)},specular:{value:new me(1118481)},shininess:{value:30},envMapIntensity:{value:1}}]),vertexShader:Ue.meshphong_vert,fragmentShader:Ue.meshphong_frag},standard:{uniforms:Yt([ce.common,ce.envmap,ce.aomap,ce.lightmap,ce.emissivemap,ce.bumpmap,ce.normalmap,ce.displacementmap,ce.roughnessmap,ce.metalnessmap,ce.fog,ce.lights,{emissive:{value:new me(0)},roughness:{value:1},metalness:{value:0},envMapIntensity:{value:1}}]),vertexShader:Ue.meshphysical_vert,fragmentShader:Ue.meshphysical_frag},toon:{uniforms:Yt([ce.common,ce.aomap,ce.lightmap,ce.emissivemap,ce.bumpmap,ce.normalmap,ce.displacementmap,ce.gradientmap,ce.fog,ce.lights,{emissive:{value:new me(0)}}]),vertexShader:Ue.meshtoon_vert,fragmentShader:Ue.meshtoon_frag},matcap:{uniforms:Yt([ce.common,ce.bumpmap,ce.normalmap,ce.displacementmap,ce.fog,{matcap:{value:null}}]),vertexShader:Ue.meshmatcap_vert,fragmentShader:Ue.meshmatcap_frag},points:{uniforms:Yt([ce.points,ce.fog]),vertexShader:Ue.points_vert,fragmentShader:Ue.points_frag},dashed:{uniforms:Yt([ce.common,ce.fog,{scale:{value:1},dashSize:{value:1},totalSize:{value:2}}]),vertexShader:Ue.linedashed_vert,fragmentShader:Ue.linedashed_frag},depth:{uniforms:Yt([ce.common,ce.displacementmap]),vertexShader:Ue.depth_vert,fragmentShader:Ue.depth_frag},normal:{uniforms:Yt([ce.common,ce.bumpmap,ce.normalmap,ce.displacementmap,{opacity:{value:1}}]),vertexShader:Ue.meshnormal_vert,fragmentShader:Ue.meshnormal_frag},sprite:{uniforms:Yt([ce.sprite,ce.fog]),vertexShader:Ue.sprite_vert,fragmentShader:Ue.sprite_frag},background:{uniforms:{uvTransform:{value:new Le},t2D:{value:null},backgroundIntensity:{value:1}},vertexShader:Ue.background_vert,fragmentShader:Ue.background_frag},backgroundCube:{uniforms:{envMap:{value:null},backgroundBlurriness:{value:0},backgroundIntensity:{value:1},backgroundRotation:{value:new Le}},vertexShader:Ue.backgroundCube_vert,fragmentShader:Ue.backgroundCube_frag},cube:{uniforms:{tCube:{value:null},tFlip:{value:-1},opacity:{value:1}},vertexShader:Ue.cube_vert,fragmentShader:Ue.cube_frag},equirect:{uniforms:{tEquirect:{value:null}},vertexShader:Ue.equirect_vert,fragmentShader:Ue.equirect_frag},distance:{uniforms:Yt([ce.common,ce.displacementmap,{referencePosition:{value:new N},nearDistance:{value:1},farDistance:{value:1e3}}]),vertexShader:Ue.distance_vert,fragmentShader:Ue.distance_frag},shadow:{uniforms:Yt([ce.lights,ce.fog,{color:{value:new me(0)},opacity:{value:1}}]),vertexShader:Ue.shadow_vert,fragmentShader:Ue.shadow_frag}};Gn.physical={uniforms:Yt([Gn.standard.uniforms,{clearcoat:{value:0},clearcoatMap:{value:null},clearcoatMapTransform:{value:new Le},clearcoatNormalMap:{value:null},clearcoatNormalMapTransform:{value:new Le},clearcoatNormalScale:{value:new Ee(1,1)},clearcoatRoughness:{value:0},clearcoatRoughnessMap:{value:null},clearcoatRoughnessMapTransform:{value:new Le},dispersion:{value:0},iridescence:{value:0},iridescenceMap:{value:null},iridescenceMapTransform:{value:new Le},iridescenceIOR:{value:1.3},iridescenceThicknessMinimum:{value:100},iridescenceThicknessMaximum:{value:400},iridescenceThicknessMap:{value:null},iridescenceThicknessMapTransform:{value:new Le},sheen:{value:0},sheenColor:{value:new me(0)},sheenColorMap:{value:null},sheenColorMapTransform:{value:new Le},sheenRoughness:{value:1},sheenRoughnessMap:{value:null},sheenRoughnessMapTransform:{value:new Le},transmission:{value:0},transmissionMap:{value:null},transmissionMapTransform:{value:new Le},transmissionSamplerSize:{value:new Ee},transmissionSamplerMap:{value:null},thickness:{value:0},thicknessMap:{value:null},thicknessMapTransform:{value:new Le},attenuationDistance:{value:0},attenuationColor:{value:new me(0)},specularColor:{value:new me(1,1,1)},specularColorMap:{value:null},specularColorMapTransform:{value:new Le},specularIntensity:{value:1},specularIntensityMap:{value:null},specularIntensityMapTransform:{value:new Le},anisotropyVector:{value:new Ee},anisotropyMap:{value:null},anisotropyMapTransform:{value:new Le}}]),vertexShader:Ue.meshphysical_vert,fragmentShader:Ue.meshphysical_frag};var el={r:0,b:0,g:0},Qx=new gt,Yd=new Le;Yd.set(-1,0,0,0,1,0,0,0,1);function ev(i,e,t,n,s,r){let o=new me(0),a=s===!0?0:1,l,c,u=null,d=0,h=null;function f(v){let b=v.isScene===!0?v.background:null;if(b&&b.isTexture){let M=v.backgroundBlurriness>0;b=e.get(b,M)}return b}function g(v){let b=!1,M=f(v);M===null?m(o,a):M&&M.isColor&&(m(M,1),b=!0);let E=i.xr.getEnvironmentBlendMode();E==="additive"?t.buffers.color.setClear(0,0,0,1,r):E==="alpha-blend"&&t.buffers.color.setClear(0,0,0,0,r),(i.autoClear||b)&&(t.buffers.depth.setTest(!0),t.buffers.depth.setMask(!0),t.buffers.color.setMask(!0),i.clear(i.autoClearColor,i.autoClearDepth,i.autoClearStencil))}function x(v,b){let M=f(b);M&&(M.isCubeTexture||M.mapping===Tr)?(c===void 0&&(c=new Xt(new Ts(1,1,1),new ut({name:"BackgroundCubeMaterial",uniforms:Ki(Gn.backgroundCube.uniforms),vertexShader:Gn.backgroundCube.vertexShader,fragmentShader:Gn.backgroundCube.fragmentShader,side:$t,depthTest:!1,depthWrite:!1,fog:!1,allowOverride:!1})),c.geometry.deleteAttribute("normal"),c.geometry.deleteAttribute("uv"),c.onBeforeRender=function(E,w,C){this.matrixWorld.copyPosition(C.matrixWorld)},Object.defineProperty(c.material,"envMap",{get:function(){return this.uniforms.envMap.value}}),n.update(c)),c.material.uniforms.envMap.value=M,c.material.uniforms.backgroundBlurriness.value=b.backgroundBlurriness,c.material.uniforms.backgroundIntensity.value=b.backgroundIntensity,c.material.uniforms.backgroundRotation.value.setFromMatrix4(Qx.makeRotationFromEuler(b.backgroundRotation)).transpose(),M.isCubeTexture&&M.isRenderTargetTexture===!1&&c.material.uniforms.backgroundRotation.value.premultiply(Yd),c.material.toneMapped=ze.getTransfer(M.colorSpace)!==Je,(u!==M||d!==M.version||h!==i.toneMapping)&&(c.material.needsUpdate=!0,u=M,d=M.version,h=i.toneMapping),c.layers.enableAll(),v.unshift(c,c.geometry,c.material,0,0,null)):M&&M.isTexture&&(l===void 0&&(l=new Xt(new fr(2,2),new ut({name:"BackgroundMaterial",uniforms:Ki(Gn.background.uniforms),vertexShader:Gn.background.vertexShader,fragmentShader:Gn.background.fragmentShader,side:Qn,depthTest:!1,depthWrite:!1,fog:!1,allowOverride:!1})),l.geometry.deleteAttribute("normal"),Object.defineProperty(l.material,"map",{get:function(){return this.uniforms.t2D.value}}),n.update(l)),l.material.uniforms.t2D.value=M,l.material.uniforms.backgroundIntensity.value=b.backgroundIntensity,l.material.toneMapped=ze.getTransfer(M.colorSpace)!==Je,M.matrixAutoUpdate===!0&&M.updateMatrix(),l.material.uniforms.uvTransform.value.copy(M.matrix),(u!==M||d!==M.version||h!==i.toneMapping)&&(l.material.needsUpdate=!0,u=M,d=M.version,h=i.toneMapping),l.layers.enableAll(),v.unshift(l,l.geometry,l.material,0,0,null))}function m(v,b){v.getRGB(el,eh(i)),t.buffers.color.setClear(el.r,el.g,el.b,b,r)}function p(){c!==void 0&&(c.geometry.dispose(),c.material.dispose(),c=void 0),l!==void 0&&(l.geometry.dispose(),l.material.dispose(),l=void 0)}return{getClearColor:function(){return o},setClearColor:function(v,b=1){o.set(v),a=b,m(o,a)},getClearAlpha:function(){return a},setClearAlpha:function(v){a=v,m(o,a)},render:g,addToRenderList:x,dispose:p}}function tv(i,e){let t=i.getParameter(i.MAX_VERTEX_ATTRIBS),n={},s=h(null),r=s,o=!1;function a(R,I,H,W,k){let z=!1,V=d(R,W,H,I);r!==V&&(r=V,c(r.object)),z=f(R,W,H,k),z&&g(R,W,H,k),k!==null&&e.update(k,i.ELEMENT_ARRAY_BUFFER),(z||o)&&(o=!1,M(R,I,H,W),k!==null&&i.bindBuffer(i.ELEMENT_ARRAY_BUFFER,e.get(k).buffer))}function l(){return i.createVertexArray()}function c(R){return i.bindVertexArray(R)}function u(R){return i.deleteVertexArray(R)}function d(R,I,H,W){let k=W.wireframe===!0,z=n[I.id];z===void 0&&(z={},n[I.id]=z);let V=R.isInstancedMesh===!0?R.id:0,K=z[V];K===void 0&&(K={},z[V]=K);let Q=K[H.id];Q===void 0&&(Q={},K[H.id]=Q);let ae=Q[k];return ae===void 0&&(ae=h(l()),Q[k]=ae),ae}function h(R){let I=[],H=[],W=[];for(let k=0;k=0){let _e=k[Q],be=z[Q];if(be===void 0&&(Q==="instanceMatrix"&&R.instanceMatrix&&(be=R.instanceMatrix),Q==="instanceColor"&&R.instanceColor&&(be=R.instanceColor)),_e===void 0||_e.attribute!==be||be&&_e.data!==be.data)return!0;V++}return r.attributesNum!==V||r.index!==W}function g(R,I,H,W){let k={},z=I.attributes,V=0,K=H.getAttributes();for(let Q in K)if(K[Q].location>=0){let _e=z[Q];_e===void 0&&(Q==="instanceMatrix"&&R.instanceMatrix&&(_e=R.instanceMatrix),Q==="instanceColor"&&R.instanceColor&&(_e=R.instanceColor));let be={};be.attribute=_e,_e&&_e.data&&(be.data=_e.data),k[Q]=be,V++}r.attributes=k,r.attributesNum=V,r.index=W}function x(){let R=r.newAttributes;for(let I=0,H=R.length;I=0){let ae=k[K];if(ae===void 0&&(K==="instanceMatrix"&&R.instanceMatrix&&(ae=R.instanceMatrix),K==="instanceColor"&&R.instanceColor&&(ae=R.instanceColor)),ae!==void 0){let _e=ae.normalized,be=ae.itemSize,We=e.get(ae);if(We===void 0)continue;let Qe=We.buffer,ke=We.type,$=We.bytesPerElement,de=ke===i.INT||ke===i.UNSIGNED_INT||ae.gpuType===ga;if(ae.isInterleavedBufferAttribute){let ie=ae.data,Ae=ie.stride,Ne=ae.offset;if(ie.isInstancedInterleavedBuffer){for(let Pe=0;Pe0&&i.getShaderPrecisionFormat(i.FRAGMENT_SHADER,i.HIGH_FLOAT).precision>0)return"highp";C="mediump"}return C==="mediump"&&i.getShaderPrecisionFormat(i.VERTEX_SHADER,i.MEDIUM_FLOAT).precision>0&&i.getShaderPrecisionFormat(i.FRAGMENT_SHADER,i.MEDIUM_FLOAT).precision>0?"mediump":"lowp"}let c=t.precision!==void 0?t.precision:"highp",u=l(c);u!==c&&(Te("WebGLRenderer:",c,"not supported, using",u,"instead."),c=u);let d=t.logarithmicDepthBuffer===!0,h=t.reversedDepthBuffer===!0&&e.has("EXT_clip_control");t.reversedDepthBuffer===!0&&h===!1&&Te("WebGLRenderer: Unable to use reversed depth buffer due to missing EXT_clip_control extension. Fallback to default depth buffer.");let f=i.getParameter(i.MAX_TEXTURE_IMAGE_UNITS),g=i.getParameter(i.MAX_VERTEX_TEXTURE_IMAGE_UNITS),x=i.getParameter(i.MAX_TEXTURE_SIZE),m=i.getParameter(i.MAX_CUBE_MAP_TEXTURE_SIZE),p=i.getParameter(i.MAX_VERTEX_ATTRIBS),v=i.getParameter(i.MAX_VERTEX_UNIFORM_VECTORS),b=i.getParameter(i.MAX_VARYING_VECTORS),M=i.getParameter(i.MAX_FRAGMENT_UNIFORM_VECTORS),E=i.getParameter(i.MAX_SAMPLES),w=i.getParameter(i.SAMPLES);return{isWebGL2:!0,getMaxAnisotropy:r,getMaxPrecision:l,textureFormatReadable:o,textureTypeReadable:a,precision:c,logarithmicDepthBuffer:d,reversedDepthBuffer:h,maxTextures:f,maxVertexTextures:g,maxTextureSize:x,maxCubemapSize:m,maxAttributes:p,maxVertexUniforms:v,maxVaryings:b,maxFragmentUniforms:M,maxSamples:E,samples:w}}function sv(i){let e=this,t=null,n=0,s=!1,r=!1,o=new _n,a=new Le,l={value:null,needsUpdate:!1};this.uniform=l,this.numPlanes=0,this.numIntersection=0,this.init=function(d,h){let f=d.length!==0||h||n!==0||s;return s=h,n=d.length,f},this.beginShadows=function(){r=!0,u(null)},this.endShadows=function(){r=!1},this.setGlobalState=function(d,h){t=u(d,h,0)},this.setState=function(d,h,f){let g=d.clippingPlanes,x=d.clipIntersection,m=d.clipShadows,p=i.get(d);if(!s||g===null||g.length===0||r&&!m)r?u(null):c();else{let v=r?0:n,b=v*4,M=p.clippingState||null;l.value=M,M=u(g,h,b,f);for(let E=0;E!==b;++E)M[E]=t[E];p.clippingState=M,this.numIntersection=x?this.numPlanes:0,this.numPlanes+=v}};function c(){l.value!==t&&(l.value=t,l.needsUpdate=n>0),e.numPlanes=n,e.numIntersection=0}function u(d,h,f,g){let x=d!==null?d.length:0,m=null;if(x!==0){if(m=l.value,g!==!0||m===null){let p=f+x*4,v=h.matrixWorldInverse;a.getNormalMatrix(v),(m===null||m.length0&&this._blur(l,0,0,t),this._applyPMREM(l),this._cleanup(l),l}fromEquirectangular(e,t=null){return this._fromTexture(e,t)}fromCubemap(e,t=null){return this._fromTexture(e,t)}compileCubemapShader(){this._cubemapMaterial===null&&(this._cubemapMaterial=Cd(),this._compileMaterial(this._cubemapMaterial))}compileEquirectangularShader(){this._equirectMaterial===null&&(this._equirectMaterial=Ad(),this._compileMaterial(this._equirectMaterial))}dispose(){this._dispose(),this._cubemapMaterial!==null&&this._cubemapMaterial.dispose(),this._equirectMaterial!==null&&this._equirectMaterial.dispose(),this._backgroundBox!==null&&(this._backgroundBox.geometry.dispose(),this._backgroundBox.material.dispose())}_setSize(e){this._lodMax=Math.floor(Math.log2(e)),this._cubeSize=Math.pow(2,this._lodMax)}_dispose(){this._blurMaterial!==null&&this._blurMaterial.dispose(),this._ggxMaterial!==null&&this._ggxMaterial.dispose(),this._pingPongRenderTarget!==null&&this._pingPongRenderTarget.dispose();for(let e=0;e2?E:0,E,E),d.setRenderTarget(s),p&&d.render(x,l),d.render(e,l)}d.toneMapping=f,d.autoClear=h,e.background=v}_textureToCubeUV(e,t){let n=this._renderer,s=e.mapping===Ii||e.mapping===$i;s?(this._cubemapMaterial===null&&(this._cubemapMaterial=Cd()),this._cubemapMaterial.uniforms.flipEnvMap.value=e.isRenderTargetTexture===!1?-1:1):this._equirectMaterial===null&&(this._equirectMaterial=Ad());let r=s?this._cubemapMaterial:this._equirectMaterial,o=this._lodMeshes[0];o.material=r;let a=r.uniforms;a.envMap.value=e;let l=this._cubeSize;Ls(t,0,0,3*l,2*l),n.setRenderTarget(t),n.render(o,Dr)}_applyPMREM(e){let t=this._renderer,n=t.autoClear;t.autoClear=!1;let s=this._lodMeshes.length;for(let r=1;rg-Fi?n-g+Fi:0),p=4*(this._cubeSize-x);l.envMap.value=e.texture,l.roughness.value=f,l.mipInt.value=g-t,Ls(r,m,p,3*x,2*x),s.setRenderTarget(r),s.render(a,Dr),l.envMap.value=r.texture,l.roughness.value=0,l.mipInt.value=g-n,Ls(e,m,p,3*x,2*x),s.setRenderTarget(e),s.render(a,Dr)}_blur(e,t,n,s,r){let o=this._pingPongRenderTarget;this._halfBlur(e,o,t,n,s,"latitudinal",r),this._halfBlur(o,e,n,n,s,"longitudinal",r)}_halfBlur(e,t,n,s,r,o,a){let l=this._renderer,c=this._blurMaterial;o!=="latitudinal"&&o!=="longitudinal"&&Re("blur direction must be either latitudinal or longitudinal!");let u=3,d=this._lodMeshes[s];d.material=c;let h=c.uniforms,f=this._sizeLods[n]-1,g=isFinite(r)?Math.PI/(2*f):2*Math.PI/(2*Ji-1),x=r/g,m=isFinite(r)?1+Math.floor(u*x):Ji;m>Ji&&Te(`sigmaRadians, ${r}, is too large and will clip, as it requested ${m} samples when the maximum is set to ${Ji}`);let p=[],v=0;for(let C=0;Cb-Fi?s-b+Fi:0),w=4*(this._cubeSize-M);Ls(t,E,w,3*M,2*M),l.setRenderTarget(t),l.render(d,Dr)}};function av(i){let e=[],t=[],n=[],s=i,r=i-Fi+1+wd.length;for(let o=0;oi-Fi?l=wd[o-i+Fi-1]:o===0&&(l=0),t.push(l);let c=1/(a-2),u=-c,d=1+c,h=[u,u,d,u,d,d,u,u,d,d,u,d],f=6,g=6,x=3,m=2,p=1,v=new Float32Array(x*g*f),b=new Float32Array(m*g*f),M=new Float32Array(p*g*f);for(let w=0;w2?0:-1,T=[C,_,0,C+2/3,_,0,C+2/3,_+1,0,C,_,0,C+2/3,_+1,0,C,_+1,0];v.set(T,x*g*w),b.set(h,m*g*w);let P=[w,w,w,w,w,w];M.set(P,p*g*w)}let E=new ht;E.setAttribute("position",new Ye(v,x)),E.setAttribute("uv",new Ye(b,m)),E.setAttribute("faceIndex",new Ye(M,p)),n.push(new Xt(E,null)),s>Fi&&s--}return{lodMeshes:n,sizeLods:e,sigmas:t}}function Td(i,e,t){let n=new Rt(i,e,t);return n.texture.mapping=Tr,n.texture.name="PMREM.cubeUv",n.scissorTest=!0,n}function Ls(i,e,t,n,s){i.viewport.set(e,t,n,s),i.scissor.set(e,t,n,s)}function lv(i,e,t){return new ut({name:"PMREMGGXConvolution",defines:{GGX_SAMPLES:rv,CUBEUV_TEXEL_WIDTH:1/e,CUBEUV_TEXEL_HEIGHT:1/t,CUBEUV_MAX_MIP:`${i}.0`},uniforms:{envMap:{value:null},roughness:{value:0},mipInt:{value:0}},vertexShader:sl(),fragmentShader:` + + precision highp float; + precision highp int; + + varying vec3 vOutputDirection; + + uniform sampler2D envMap; + uniform float roughness; + uniform float mipInt; + + #define ENVMAP_TYPE_CUBE_UV + #include + + #define PI 3.14159265359 + + // Van der Corput radical inverse + float radicalInverse_VdC(uint bits) { + bits = (bits << 16u) | (bits >> 16u); + bits = ((bits & 0x55555555u) << 1u) | ((bits & 0xAAAAAAAAu) >> 1u); + bits = ((bits & 0x33333333u) << 2u) | ((bits & 0xCCCCCCCCu) >> 2u); + bits = ((bits & 0x0F0F0F0Fu) << 4u) | ((bits & 0xF0F0F0F0u) >> 4u); + bits = ((bits & 0x00FF00FFu) << 8u) | ((bits & 0xFF00FF00u) >> 8u); + return float(bits) * 2.3283064365386963e-10; // / 0x100000000 + } + + // Hammersley sequence + vec2 hammersley(uint i, uint N) { + return vec2(float(i) / float(N), radicalInverse_VdC(i)); + } + + // GGX VNDF importance sampling (Eric Heitz 2018) + // "Sampling the GGX Distribution of Visible Normals" + // https://jcgt.org/published/0007/04/01/ + vec3 importanceSampleGGX_VNDF(vec2 Xi, vec3 V, float roughness) { + float alpha = roughness * roughness; + + // Section 4.1: Orthonormal basis + vec3 T1 = vec3(1.0, 0.0, 0.0); + vec3 T2 = cross(V, T1); + + // Section 4.2: Parameterization of projected area + float r = sqrt(Xi.x); + float phi = 2.0 * PI * Xi.y; + float t1 = r * cos(phi); + float t2 = r * sin(phi); + float s = 0.5 * (1.0 + V.z); + t2 = (1.0 - s) * sqrt(1.0 - t1 * t1) + s * t2; + + // Section 4.3: Reprojection onto hemisphere + vec3 Nh = t1 * T1 + t2 * T2 + sqrt(max(0.0, 1.0 - t1 * t1 - t2 * t2)) * V; + + // Section 3.4: Transform back to ellipsoid configuration + return normalize(vec3(alpha * Nh.x, alpha * Nh.y, max(0.0, Nh.z))); + } + + void main() { + vec3 N = normalize(vOutputDirection); + vec3 V = N; // Assume view direction equals normal for pre-filtering + + vec3 prefilteredColor = vec3(0.0); + float totalWeight = 0.0; + + // For very low roughness, just sample the environment directly + if (roughness < 0.001) { + gl_FragColor = vec4(bilinearCubeUV(envMap, N, mipInt), 1.0); + return; + } + + // Tangent space basis for VNDF sampling + vec3 up = abs(N.z) < 0.999 ? vec3(0.0, 0.0, 1.0) : vec3(1.0, 0.0, 0.0); + vec3 tangent = normalize(cross(up, N)); + vec3 bitangent = cross(N, tangent); -const OBSIDIAN_PIXI_ASSET_PATH = "lib/pixi.min.js"; -let miniWorldMapPixiRuntime = null; -let miniWorldMapAsarCandidatesCache = null; + for(uint i = 0u; i < uint(GGX_SAMPLES); i++) { + vec2 Xi = hammersley(i, uint(GGX_SAMPLES)); -function miniWorldMapNodeRequire(moduleName) { - try { - return require(moduleName); - } catch (error) { - return null; - } -} + // For PMREM, V = N, so in tangent space V is always (0, 0, 1) + vec3 H_tangent = importanceSampleGGX_VNDF(Xi, vec3(0.0, 0.0, 1.0), roughness); -function miniWorldMapAsarCandidates() { - if (miniWorldMapAsarCandidatesCache) return miniWorldMapAsarCandidatesCache; - const fs = miniWorldMapNodeRequire("fs"); - const path = miniWorldMapNodeRequire("path"); - const os = miniWorldMapNodeRequire("os"); - const processRef = typeof process !== "undefined" ? process : null; - if (!fs || !path) { - miniWorldMapAsarCandidatesCache = []; - return miniWorldMapAsarCandidatesCache; - } - - const candidates = []; - const add = filePath => { - if (!filePath || candidates.includes(filePath)) return; - try { - if (fs.existsSync(filePath)) candidates.push(filePath); - } catch (error) { - // Ignore unreadable install locations; another candidate may work. - } - }; - const addVersioned = directory => { - if (!directory) return; - try { - const files = fs.readdirSync(directory) - .filter(file => /^obsidian-.+\.asar$/i.test(file)) - .map(file => { - const filePath = path.join(directory, file); - let mtime = 0; - try { - mtime = fs.statSync(filePath).mtimeMs; - } catch (error) { - // Keep a neutral sort value if stat is unavailable. - } - return { filePath, mtime }; - }) - .sort((a, b) => b.mtime - a.mtime); - for (const item of files) add(item.filePath); - } catch (error) { - // Not every install keeps versioned ASARs in userData. - } - }; - - if (processRef?.resourcesPath) add(path.join(processRef.resourcesPath, "obsidian.asar")); - - const electron = miniWorldMapNodeRequire("electron"); - const electronApp = electron?.remote?.app || electron?.app; - try { - const userData = electronApp?.getPath?.("userData"); - const version = electronApp?.getVersion?.(); - if (userData && version) add(path.join(userData, `obsidian-${version}.asar`)); - addVersioned(userData); - } catch (error) { - // Obsidian may not expose electron.remote from the renderer. - } - - const home = os?.homedir?.(); - if (home) { - if (processRef?.platform === "darwin") { - addVersioned(path.join(home, "Library", "Application Support", "obsidian")); - add("/Applications/Obsidian.app/Contents/Resources/obsidian.asar"); - } else if (processRef?.platform === "win32") { - addVersioned(path.join(processRef.env?.APPDATA || path.join(home, "AppData", "Roaming"), "obsidian")); - } else { - addVersioned(path.join(processRef?.env?.XDG_CONFIG_HOME || path.join(home, ".config"), "obsidian")); - } - } - - miniWorldMapAsarCandidatesCache = candidates; - return candidates; -} + // Transform H back to world space + vec3 H = normalize(tangent * H_tangent.x + bitangent * H_tangent.y + N * H_tangent.z); + vec3 L = normalize(2.0 * dot(V, H) * H - V); -function findMiniWorldMapAsarEntry(files, assetPath) { - const parts = String(assetPath || "").replace(/^\/+/, "").split("/").filter(Boolean); - let entry = { files }; - for (const part of parts) { - entry = entry?.files?.[part]; - if (!entry) return null; - } - return entry; -} + float NdotL = max(dot(N, L), 0.0); -function readMiniWorldMapAsarFile(asarPath, assetPath) { - const fs = miniWorldMapNodeRequire("fs"); - const path = miniWorldMapNodeRequire("path"); - if (!fs || !path) return null; - let fd = null; - try { - fd = fs.openSync(asarPath, "r"); - const sizeHeader = Buffer.alloc(16); - fs.readSync(fd, sizeHeader, 0, sizeHeader.length, 0); - const headerSize = sizeHeader.readUInt32LE(8); - const jsonSize = sizeHeader.readUInt32LE(12); - if (!Number.isFinite(headerSize) || !Number.isFinite(jsonSize) || jsonSize <= 0 || jsonSize > headerSize) return null; - - const headerBuffer = Buffer.alloc(jsonSize); - fs.readSync(fd, headerBuffer, 0, jsonSize, 16); - const header = JSON.parse(headerBuffer.toString("utf8")); - const normalizedAssetPath = String(assetPath || "").replace(/^\/+/, ""); - const entry = findMiniWorldMapAsarEntry(header.files, normalizedAssetPath); - if (!entry) return null; - if (entry.unpacked) { - return fs.readFileSync(path.join(`${asarPath}.unpacked`, normalizedAssetPath)).toString("utf8"); - } - if (!Number.isFinite(entry.size) || entry.offset === undefined) return null; - const start = 12 + headerSize + Number(entry.offset); - const content = Buffer.alloc(entry.size); - fs.readSync(fd, content, 0, entry.size, start); - return content.toString("utf8"); - } catch (error) { - return null; - } finally { - if (fd !== null) { - try { - fs.closeSync(fd); - } catch (error) { - // Ignore close failures; the renderer can fall back to canvas. - } - } - } -} + if(NdotL > 0.0) { + // Sample environment at fixed mip level + // VNDF importance sampling handles the distribution filtering + vec3 sampleColor = bilinearCubeUV(envMap, L, mipInt); -function loadMiniWorldMapPixiRuntime() { - if (typeof window !== "undefined" && window.PIXI) return window.PIXI; - if (miniWorldMapPixiRuntime) return miniWorldMapPixiRuntime; - - for (const asarPath of miniWorldMapAsarCandidates()) { - const source = readMiniWorldMapAsarFile(asarPath, OBSIDIAN_PIXI_ASSET_PATH); - if (!source) continue; - try { - const PIXI = new Function(`${source}\nreturn PIXI;`)(); - if (PIXI) { - miniWorldMapPixiRuntime = PIXI; - if (typeof window !== "undefined") window.PIXI = PIXI; - return PIXI; - } - } catch (error) { - console.error("Mini World Map failed to initialize Pixi from Obsidian bundle", error); - } - } - - return null; -} + // Weight by NdotL for the split-sum approximation + // VNDF PDF naturally accounts for the visible microfacet distribution + prefilteredColor += sampleColor * NdotL; + totalWeight += NdotL; + } + } -class MiniWorldMapPixiRenderer { - constructor(containerEl, PIXI) { - this.containerEl = containerEl; - this.PIXI = PIXI; - this.view = containerEl.createEl("canvas", { - cls: "mwm-canvas mwm-pixi-canvas", - attr: { - role: "img", - "aria-label": "Mini World Map graph", - tabindex: "0" - } - }); - this.app = new PIXI.Application({ - view: this.view, - antialias: true, - backgroundAlpha: 0, - autoStart: false, - resolution: window.devicePixelRatio || 1, - autoDensity: true - }); - this.background = new PIXI.Graphics(); - this.world = new PIXI.Container(); - this.rings = new PIXI.Graphics(); - this.baseEdges = new PIXI.Graphics(); - this.baseEdgeSprites = new PIXI.Container(); - this.highlightEdges = new PIXI.Graphics(); - this.focusNodes = new PIXI.Graphics(); - this.nodes = new PIXI.Container(); - this.labels = new PIXI.Container(); - this.measureCanvas = document.createElement("canvas"); - this.measureCtx = this.measureCanvas.getContext("2d"); - this.labelPool = []; - this.activeLabels = 0; - this.nodeSprites = new Map(); - this.nodeTextureCache = new Map(); - this.hasFrame = false; - this.lastActiveKey = ""; - this.lastViewportWidth = 0; - this.lastViewportHeight = 0; - this.rendererWidth = 0; - this.rendererHeight = 0; - this.rendererDpr = 0; - this.textStyleCache = new Map(); - this.baseEdgeSpritePool = []; - this.activeBaseEdgeSprites = 0; - this.edgeTexture = null; - this.backgroundKey = ""; - this.sceneSignature = ""; - this.dataRef = null; - this.layoutRef = null; - this.lastPaletteKey = ""; - this.lastZoomLayerBucket = ""; - this.lastLabelZoomBucket = ""; - this.lastTransform = { panX: 0, panY: 0, zoom: 1 }; - this.currentZoom = 1; - - this.app.stage.addChild(this.background); - this.app.stage.addChild(this.world); - this.world.addChild(this.rings); - this.world.addChild(this.baseEdges); - this.world.addChild(this.baseEdgeSprites); - this.world.addChild(this.highlightEdges); - this.world.addChild(this.focusNodes); - this.world.addChild(this.nodes); - this.app.stage.addChild(this.labels); - } - - destroy() { - this.nodeTextureCache.clear(); - if (this.app) { - this.app.destroy(true, { children: true, texture: true, baseTexture: true }); - this.app = null; - } - this.edgeTexture = null; - this.view?.remove(); - this.view = null; - } - - resize(viewport) { - if (!this.app || !viewport) return; - const width = Math.max(1, Math.floor(viewport.width || 1)); - const height = Math.max(1, Math.floor(viewport.height || 1)); - const dpr = Math.max(1, Math.min(3, window.devicePixelRatio || 1)); - if (this.rendererWidth === width && this.rendererHeight === height && this.rendererDpr === dpr) return; - this.rendererWidth = width; - this.rendererHeight = height; - this.rendererDpr = dpr; - if (this.app.renderer.resolution !== dpr) this.app.renderer.resolution = dpr; - this.app.renderer.resize(width, height); - this.view.style.width = `${width}px`; - this.view.style.height = `${height}px`; - } - - render(frame) { - if (!this.app || !frame || !frame.data || !frame.bundle) return; - const { data, bundle, palette, viewport, panX, panY, zoom } = frame; - this.currentZoom = clampFloat(zoom, MIN_CANVAS_ZOOM, MAX_CANVAS_ZOOM, 1); - const zoomLayerBucket = this.zoomLayerBucket(this.currentZoom); - const labelZoomBucket = this.labelZoomBucket(this.currentZoom); - const sceneSignature = this.createSceneSignature(frame); - const geometryDirty = Boolean(frame.geometryDirty); - const activeKey = frame.activeKey || ""; - const zoomLayerChanged = this.lastZoomLayerBucket !== zoomLayerBucket; - this.showArrow = data.showArrow !== false; - this.hasFrame = true; - this.lastViewportWidth = Math.max(1, Math.floor(viewport.width || 1)); - this.lastViewportHeight = Math.max(1, Math.floor(viewport.height || 1)); - - this.resize(viewport); - this.drawBackground(palette, viewport); - this.applyTransform(panX, panY, zoom); - - const sceneChanged = geometryDirty - || this.dataRef !== data - || this.layoutRef !== bundle.layout - || this.sceneSignature !== sceneSignature; - if (sceneChanged) { - this.rebuildScene(frame); - this.sceneSignature = sceneSignature; - this.dataRef = data; - this.layoutRef = bundle.layout; - this.lastZoomLayerBucket = zoomLayerBucket; - this.lastLabelZoomBucket = ""; - this.lastActiveKey = ""; - } else if (zoomLayerChanged) { - this.redrawZoomLayers(frame, { refreshRoutedEdges: frame.mode !== "interactive" }); - } - - const needsLabelRefresh = sceneChanged - || this.lastActiveKey !== activeKey - || this.lastLabelZoomBucket !== labelZoomBucket; - if (sceneChanged || this.lastActiveKey !== activeKey || zoomLayerChanged || needsLabelRefresh) { - this.applyActiveState(frame, { updateLabels: needsLabelRefresh }); - this.lastActiveKey = activeKey; - if (needsLabelRefresh) this.lastLabelZoomBucket = labelZoomBucket; - } else { - this.updateLabelPositions(); - } - - this.app.render(); - } - - canTransformOnly(frame) { - if (!this.hasFrame || !frame || !frame.viewport || frame.geometryDirty) return false; - const width = Math.max(1, Math.floor(frame.viewport.width || 1)); - const height = Math.max(1, Math.floor(frame.viewport.height || 1)); - const zoom = clampFloat(frame.zoom, MIN_CANVAS_ZOOM, MAX_CANVAS_ZOOM, 1); - const previousZoom = clampFloat(this.lastTransform?.zoom, MIN_CANVAS_ZOOM, MAX_CANVAS_ZOOM, 1); - const zoomMatches = Math.abs(previousZoom - zoom) < 0.0005; - return this.lastActiveKey === (frame.activeKey || "") - && this.lastViewportWidth === width - && this.lastViewportHeight === height - && (zoomMatches || frame.allowZoomTransformOnly); - } - - transformOnly(frame) { - if (!this.app || !frame || !this.canTransformOnly(frame)) return false; - const previousZoom = clampFloat(this.lastTransform?.zoom, MIN_CANVAS_ZOOM, MAX_CANVAS_ZOOM, 1); - this.currentZoom = clampFloat(frame.zoom, MIN_CANVAS_ZOOM, MAX_CANVAS_ZOOM, 1); - this.applyTransform(frame.panX, frame.panY, frame.zoom); - const zoomChanged = Math.abs(previousZoom - this.currentZoom) >= 0.0005; - if (zoomChanged && frame.allowZoomTransformOnly && frame.data && frame.bundle && frame.palette) { - this.redrawZoomLayers(frame); - this.applyActiveState(frame, { updateLabels: false }); - } else { - this.updateLabelPositions(); - } - this.app.render(); - return true; - } - - createSceneSignature(frame) { - const data = frame.data || {}; - const bundle = frame.bundle || {}; - const graph = bundle.graph || {}; - const layout = bundle.layout || {}; - const paletteKey = this.paletteSignature(frame.palette); - return [ - graph.rootId || "", - graph.focusId || "", - data.nodes?.length || 0, - data.hierarchy?.length || 0, - data.links?.length || 0, - data.hoverLinks?.length || 0, - Math.round(layout.width || 0), - Math.round(layout.height || 0), - Math.round((layout.centerX || 0) * 10), - Math.round((layout.centerY || 0) * 10), - Math.round((layout.swirlStrength || 0) * 1000), - data.showArrow !== false ? 1 : 0, - paletteKey - ].join("|"); - } - - zoomLayerBucket(zoom) { - return String(Math.round(clampFloat(zoom, MIN_CANVAS_ZOOM, MAX_CANVAS_ZOOM, 1) * 1000)); - } - - labelZoomBucket(zoom) { - return String(Math.round(clampFloat(zoom, MIN_CANVAS_ZOOM, MAX_CANVAS_ZOOM, 1) * 120)); - } - - paletteSignature(palette = {}) { - return [ - palette.bg, - palette.line, - palette.lineHighlight, - palette.node, - palette.note, - palette.folder, - palette.folderMeta, - palette.folderRing, - palette.tree, - palette.ringGuide, - palette.fileRing, - palette.focus, - palette.circle, - palette.labelText, - palette.labelStroke, - palette.link, - palette.external, - palette.externalLink, - palette.unresolved, - palette.stroke, - palette.glow, - palette.fontFamily - ].join("|"); - } - - drawBackground(palette, viewport) { - const key = `${palette.bg}|${Math.round(viewport.width)}|${Math.round(viewport.height)}`; - if (this.backgroundKey === key) return; - this.backgroundKey = key; - this.background.clear(); - this.fillRect(this.background, palette.bg, 0, 0, viewport.width, viewport.height, 1); - } - - applyTransform(panX, panY, zoom) { - this.world.position.set(panX, panY); - this.world.scale.set(zoom); - this.lastTransform = { panX, panY, zoom }; - } - - rebuildScene(frame) { - const { data, bundle, palette } = frame; - const paletteKey = this.paletteSignature(palette); - const paletteChanged = paletteKey !== this.lastPaletteKey; - this.rings.clear(); - this.baseEdges.clear(); - this.resetBaseEdgeSprites(); - this.highlightEdges.clear(); - this.focusNodes.clear(); - this.clearNodeSprites(); - if (paletteChanged) { - this.clearNodeTextureCache(); - this.lastPaletteKey = paletteKey; - } - this.hideLabels(); - this.drawRings(bundle.layout, palette, { underlay: true, now: frame.now }); - this.drawBaseEdges(data.links, palette, false); - this.drawBaseEdges(data.hierarchy, palette, true); - this.drawRings(bundle.layout, palette, { overlay: true, now: frame.now }); - this.buildNodeSprites(data.nodes, palette, bundle.graph || {}); - } - - redrawZoomLayers(frame, options = {}) { - const { data, bundle, palette } = frame; - if (!bundle || !bundle.layout || !palette) return; - this.rings.clear(); - if (options.refreshRoutedEdges) { - this.baseEdges.clear(); - this.drawRoutedBaseEdges(data?.links, palette, false); - this.drawRoutedBaseEdges(data?.hierarchy, palette, true); - } - this.updateBaseEdgeSpriteWidths(); - this.drawRings(bundle.layout, palette, { underlay: true, now: frame.now }); - this.drawRings(bundle.layout, palette, { overlay: true, now: frame.now }); - this.lastZoomLayerBucket = this.zoomLayerBucket(this.currentZoom); - } - - color(value, fallback = "#888888") { - return pixiParseColor(value) || pixiParseColor(fallback) || { rgb: 0x888888, alpha: 1 }; - } - - fillRect(graphics, colorValue, x, y, width, height, alpha = 1) { - const color = this.color(colorValue); - graphics.beginFill(color.rgb, color.alpha * alpha); - graphics.drawRect(x, y, width, height); - graphics.endFill(); - } - - lineStyle(graphics, width, colorValue, alpha = 1) { - const color = this.color(colorValue); - graphics.lineStyle(width, color.rgb, color.alpha * alpha); - } - - screenScaledWidth(width) { - const zoom = Math.max(MIN_CANVAS_ZOOM, this.currentZoom || 1); - return Math.max(0.01, width / zoom); - } - - nodeZoomScale() { - const zoom = Math.max(MIN_CANVAS_ZOOM, this.currentZoom || 1); - return clampFloat(Math.pow(1 / zoom, 0.3), 0.78, 1.34, 1); - } - - drawRings(layout, palette, options = {}) { - if (!layout || !Array.isArray(layout.rings) || !layout.rings.length) return; - const centerX = Number.isFinite(layout.centerX) ? layout.centerX : layout.width / 2; - const centerY = Number.isFinite(layout.centerY) ? layout.centerY : layout.height / 2; - const alphaScale = options.overlay ? 1.16 : options.underlay ? 0.58 : 1; - const swirlStrength = clampFloat(layout.swirlStrength, 0, 1, 0); - const spinSpeed = clampFloat(layout.spinSpeed || 0, 0, 1, 0); - const elapsedSeconds = Number.isFinite(options.now) && layout.spinStartedAt - ? Math.max(0, (options.now - layout.spinStartedAt) / 1000) - : 0; - - for (const ring of layout.rings) { - if (!Number.isFinite(ring.radius) || ring.radius <= 0) continue; - const density = Math.min(1, Math.sqrt(Math.max(1, ring.count || 1)) / 9); - const phase = swirlOrbitAngleForRing(ring.depth, ring.radius, spinSpeed, elapsedSeconds); - if (options.overlay) { - this.lineStyle(this.rings, this.screenScaledWidth(5.5), palette.ringGuide || palette.folderRing || palette.line, (0.032 + density * 0.022) * alphaScale); - pixiDrawRingPath(this.rings, centerX, centerY, ring.radius, ring.depth, swirlStrength, phase); - } - this.lineStyle(this.rings, this.screenScaledWidth(options.overlay ? 1.35 : 0.78), palette.ringGuide || palette.folderRing || palette.line, (0.13 + density * 0.09) * alphaScale); - pixiDrawRingPath(this.rings, centerX, centerY, ring.radius, ring.depth, swirlStrength, phase); - } - } - - drawBaseEdges(edges, palette, hierarchy) { - for (const item of edges || []) { - if (item.hoverOnly) continue; - const style = this.baseEdgeStyle(item, palette, hierarchy); - if (this.canDrawEdgeSprite(item)) this.drawBaseEdgeSprite(item, style); - else this.drawBaseEdgePath(item, style); - } - } - - drawRoutedBaseEdges(edges, palette, hierarchy) { - for (const item of edges || []) { - if (item.hoverOnly || this.canDrawEdgeSprite(item)) continue; - this.drawBaseEdgePath(item, this.baseEdgeStyle(item, palette, hierarchy)); - } - } - - baseEdgeStyle(item, palette, hierarchy) { - const unresolved = item.edge && item.edge.unresolvedCount; - const external = item.external || (item.edge && item.edge.externalCount); - const externalHierarchy = hierarchy && external; - const outsideLink = !hierarchy && external; - const color = unresolved - ? palette.unresolved - : outsideLink - ? (palette.externalLink || palette.external) - : hierarchy - ? (palette.tree || palette.folderRing || palette.line) - : palette.link; - const baseAlpha = (hierarchy - ? (externalHierarchy ? 0.18 : 0.34) - : (outsideLink ? 0.082 : 0.056)); - const minAlpha = hierarchy ? 0.09 : outsideLink ? 0.03 : 0.024; - const width = hierarchy - ? (externalHierarchy ? 1.12 : 1.48) - : (outsideLink ? item.width * 0.72 : item.width * 0.7); - return { - color, - alpha: Math.max(minAlpha, baseAlpha), - width: Math.max(outsideLink ? 0.58 : 0.45, width) - }; - } - - drawBaseEdgePath(item, style) { - this.lineStyle(this.baseEdges, this.screenScaledWidth(style.width), style.color, style.alpha); - this.drawEdgePath(this.baseEdges, item); - } - - canDrawEdgeSprite(item) { - if (!item || item.hoverOnly) return false; - if (item.dynamicRoute || item.route) return false; - const source = item.sourcePoint; - const target = item.targetPoint; - return Boolean(source && target && Math.hypot(target.x - source.x, target.y - source.y) > 0.5); - } - - drawBaseEdgeSprite(item, style) { - const source = item.sourcePoint; - const target = item.targetPoint; - const dx = target.x - source.x; - const dy = target.y - source.y; - const length = Math.hypot(dx, dy); - if (length <= 0.5) return; - const color = this.color(style.color); - const sprite = this.nextBaseEdgeSprite(); - sprite.visible = true; - sprite.position.set(source.x, source.y); - sprite.rotation = Math.atan2(dy, dx); - sprite.tint = color.rgb; - sprite.alpha = color.alpha * style.alpha; - sprite.width = length; - sprite.__mwmScreenWidth = style.width; - this.setBaseEdgeSpriteWidth(sprite); - } - - nextBaseEdgeSprite() { - let sprite = this.baseEdgeSpritePool[this.activeBaseEdgeSprites]; - if (!sprite) { - sprite = new this.PIXI.Sprite(this.edgeLineTexture()); - if (sprite.anchor && typeof sprite.anchor.set === "function") sprite.anchor.set(0, 0.5); - sprite.eventMode = "none"; - sprite.interactive = false; - sprite.roundPixels = false; - this.baseEdgeSpritePool.push(sprite); - this.baseEdgeSprites.addChild(sprite); - } - this.activeBaseEdgeSprites += 1; - return sprite; - } - - edgeLineTexture() { - if (this.edgeTexture) return this.edgeTexture; - const graphics = new this.PIXI.Graphics(); - graphics.beginFill(0xffffff, 1); - graphics.drawRect(0, 0, 1, 1); - graphics.endFill(); - this.edgeTexture = this.app.renderer.generateTexture(graphics); - graphics.destroy(); - return this.edgeTexture; - } - - resetBaseEdgeSprites() { - this.activeBaseEdgeSprites = 0; - for (const sprite of this.baseEdgeSpritePool) { - sprite.visible = false; - } - } - - updateBaseEdgeSpriteWidths() { - for (let index = 0; index < this.activeBaseEdgeSprites; index += 1) { - this.setBaseEdgeSpriteWidth(this.baseEdgeSpritePool[index]); - } - } - - setBaseEdgeSpriteWidth(sprite) { - if (!sprite) return; - sprite.height = this.screenScaledWidth(sprite.__mwmScreenWidth || 1); - } - - drawHighlightedEdges(frame) { - const data = frame.data; - const active = frame.active || {}; - const palette = frame.palette; - this.highlightEdges.clear(); - if (!active.highlightedEdges || !active.highlightedEdges.size) return; - for (const key of active.highlightedEdges) { - const item = data.edgesById.get(key); - if (!item) continue; - const hierarchy = data.hierarchyParentByNode.get(item.target) === item - || data.hierarchyChildrenByNode.get(item.source)?.includes(item); - const outsideLink = !hierarchy && (item.external || (item.edge && item.edge.externalCount)); - const color = outsideLink ? (palette.externalLink || palette.external) : (palette.lineHighlight || palette.focus); - this.lineStyle(this.highlightEdges, this.screenScaledWidth(hierarchy ? 2.35 : outsideLink ? 2.2 : 2.45), color, hierarchy ? 0.78 : outsideLink ? 0.76 : 0.88); - this.drawEdgePath(this.highlightEdges, item); - if (!hierarchy && this.showArrow) this.drawArrow(this.highlightEdges, item, color, 0.95); - } - } - - drawEdgePath(graphics, item) { - const source = item.sourcePoint; - const target = item.targetPoint; - const route = item.dynamicRoute || item.route; - if (!source || !target) return; - graphics.moveTo(source.x, source.y); - if (route && (route.kind === "outer" || route.kind === "external" || route.kind === "dynamic-orbit")) { - const startAngle = route.sourceAngle; - const endAngle = Number.isFinite(route.endAngle) ? route.endAngle : route.targetAngle; - const arcStart = radialPoint(route.centerX, route.centerY, route.radius, startAngle); - const arcEnd = radialPoint(route.centerX, route.centerY, route.radius, endAngle); - graphics.lineTo(arcStart.x, arcStart.y); - graphics.arc(route.centerX, route.centerY, route.radius, startAngle, endAngle); - graphics.lineTo(arcEnd.x, arcEnd.y); - graphics.lineTo(target.x, target.y); - return; - } - if (route && route.kind === "curve") { - const centerX = Number.isFinite(source.centerX) ? source.centerX : (source.x + target.x) / 2; - const centerY = Number.isFinite(source.centerY) ? source.centerY : (source.y + target.y) / 2; - const midX = (source.x + target.x) / 2; - const midY = (source.y + target.y) / 2; - const pull = Number.isFinite(route.curveStrength) ? route.curveStrength : (item.external ? 0.28 : 0.16); - graphics.quadraticCurveTo(midX + (centerX - midX) * pull, midY + (centerY - midY) * pull, target.x, target.y); - return; - } - graphics.lineTo(target.x, target.y); - } - - drawArrow(graphics, item, colorValue, alpha = 1) { - const source = this.arrowSourcePoint(item); - const target = item.targetPoint; - if (!source || !target) return; - const dx = target.x - source.x; - const dy = target.y - source.y; - const length = Math.hypot(dx, dy); - if (length < 0.001) return; - const angle = Math.atan2(dy, dx); - const targetRadius = Math.max(4, item.targetPoint?.nodeRadius || 6); - const zoom = Math.max(MIN_CANVAS_ZOOM, this.currentZoom || 1); - const size = 7 / zoom; - const spread = 0.48; - const color = this.color(colorValue); - const tipX = target.x - Math.cos(angle) * (targetRadius + 2 / zoom); - const tipY = target.y - Math.sin(angle) * (targetRadius + 2 / zoom); - graphics.beginFill(color.rgb, color.alpha * alpha); - graphics.moveTo(tipX, tipY); - graphics.lineTo(tipX - Math.cos(angle - spread) * size, tipY - Math.sin(angle - spread) * size); - graphics.lineTo(tipX - Math.cos(angle + spread) * size, tipY - Math.sin(angle + spread) * size); - graphics.closePath(); - graphics.endFill(); - } - - arrowSourcePoint(item) { - const route = item.dynamicRoute || item.route; - if (route && (route.kind === "outer" || route.kind === "external" || route.kind === "dynamic-orbit")) { - const endAngle = Number.isFinite(route.endAngle) ? route.endAngle : route.targetAngle; - return radialPoint(route.centerX, route.centerY, route.radius, endAngle); - } - if (route && route.kind === "curve") { - const source = item.sourcePoint; - const target = item.targetPoint; - const centerX = Number.isFinite(source.centerX) ? source.centerX : (source.x + target.x) / 2; - const centerY = Number.isFinite(source.centerY) ? source.centerY : (source.y + target.y) / 2; - const midX = (source.x + target.x) / 2; - const midY = (source.y + target.y) / 2; - const pull = Number.isFinite(route.curveStrength) ? route.curveStrength : (item.external ? 0.28 : 0.16); - return { - x: midX + (centerX - midX) * pull, - y: midY + (centerY - midY) * pull - }; - } - return item.sourcePoint; - } - - buildNodeSprites(nodes, palette, graph = {}) { - for (const item of nodes || []) { - const texture = this.getNodeTexture(item, palette, graph); - const sprite = new this.PIXI.Sprite(texture); - if (sprite.anchor && typeof sprite.anchor.set === "function") sprite.anchor.set(0.5); - sprite.position.set(item.point.x, item.point.y); - sprite.alpha = item.node.type === "external" && !item.node.externalProxy ? 0.78 : 0.92; - this.nodes.addChild(sprite); - this.nodeSprites.set(item.node.id, { - sprite, - item, - baseAlpha: sprite.alpha - }); - } - } - - clearNodeSprites() { - this.nodes.removeChildren(); - this.nodeSprites.clear(); - } - - clearNodeTextureCache() { - for (const texture of this.nodeTextureCache.values()) { - if (texture && typeof texture.destroy === "function") texture.destroy(true); - } - this.nodeTextureCache.clear(); - } - - getNodeTexture(item, palette, graph = {}) { - const colors = this.nodeColors(item, palette, graph); - const radius = Math.max(3, Math.round(item.radius * 2) / 2); - const rootNode = item.node.id === graph.rootId; - const folderNode = item.node.type === "folder"; - const strokeWidth = Math.max(1.1, rootNode ? 1.75 : folderNode ? 1.35 : 1.1); - const fill = this.color(colors.fill); - const stroke = this.color(colors.stroke); - const key = [ - radius, - strokeWidth, - fill.rgb, - Math.round(fill.alpha * 100), - stroke.rgb, - Math.round(stroke.alpha * 100), - item.node.type, - item.node.externalProxy ? 1 : 0 - ].join("|"); - const cached = this.nodeTextureCache.get(key); - if (cached) return cached; - - const pad = Math.ceil(strokeWidth + 4); - const size = Math.ceil((radius + pad) * 2); - const center = size / 2; - const graphics = new this.PIXI.Graphics(); - graphics.beginFill(fill.rgb, fill.alpha); - graphics.drawCircle(center, center, radius); - graphics.endFill(); - graphics.lineStyle(strokeWidth, stroke.rgb, stroke.alpha); - graphics.drawCircle(center, center, radius); - if (item.node.type === "external" || item.node.externalProxy || item.node.type === "unresolved") { - graphics.lineStyle(0.8, stroke.rgb, stroke.alpha * 0.48); - graphics.drawCircle(center, center, Math.max(1, radius + 2.2)); - } - - let texture; - try { - texture = this.app.renderer.generateTexture(graphics, { resolution: 2 }); - } catch (error) { - texture = this.app.renderer.generateTexture(graphics); - } - graphics.destroy(); - this.nodeTextureCache.set(key, texture); - if (this.nodeTextureCache.size > 320) { - const firstKey = this.nodeTextureCache.keys().next().value; - const firstTexture = this.nodeTextureCache.get(firstKey); - if (firstTexture && typeof firstTexture.destroy === "function") firstTexture.destroy(true); - this.nodeTextureCache.delete(firstKey); - } - return texture; - } - - nodeColors(item, palette, graph = {}) { - const nodeId = item.node.id; - const rootNode = nodeId === graph.rootId; - const folderNode = item.node.type === "folder"; - const folderWithMeta = folderNode && Boolean(item.node.representativeFile); - const externalGroup = item.node.type === "external" && !item.node.externalProxy; - const externalFile = Boolean(item.node.externalProxy); - const unresolvedNode = item.node.type === "unresolved"; - const fill = unresolvedNode - ? palette.unresolved - : externalGroup - ? palette.node - : folderWithMeta - ? palette.folderMeta - : folderNode - ? palette.folder - : (palette.note || palette.node); - const stroke = rootNode - ? palette.circle - : unresolvedNode - ? palette.unresolved - : externalGroup || externalFile - ? palette.external - : folderNode - ? palette.folderRing - : palette.fileRing || palette.stroke; - return { fill, stroke }; - } - - applyActiveState(frame, options = {}) { - const active = frame.active || {}; - const data = frame.data; - const palette = frame.palette; - const graph = frame.bundle?.graph || {}; - const related = active.relatedNodes || new Set(); - const pinned = active.pinnedNodeIds || new Set(); - const focusColor = this.color(palette.focus || palette.circle); - const glowColor = this.color(palette.glow || palette.focus || palette.circle); - const nodeZoomScale = this.nodeZoomScale(); - this.focusNodes.clear(); - this.drawHighlightedEdges(frame); - - for (const [nodeId, record] of this.nodeSprites.entries()) { - const item = record.item; - const selected = nodeId === frame.selectedNodeId; - const focused = nodeId === active.activeNodeId - || selected - || pinned.has(nodeId) - || nodeId === graph.focusId - || item.searchMatch; - const isRelated = related.has(nodeId); - const dimmed = active.hasActive && !focused && !isRelated; - record.sprite.alpha = dimmed ? Math.max(0.22, record.baseAlpha * 0.32) : focused ? 1 : isRelated ? Math.min(1, record.baseAlpha + 0.08) : record.baseAlpha; - const scale = focused ? 1.16 : isRelated ? 1.06 : 1; - record.sprite.scale.set(scale * nodeZoomScale); - if (focused || (isRelated && related.size <= 900)) { - const color = focused ? focusColor : glowColor; - const alpha = focused ? 0.24 : 0.1; - this.focusNodes.beginFill(color.rgb, color.alpha * alpha); - this.focusNodes.drawCircle(item.point.x, item.point.y, item.radius * nodeZoomScale * (focused ? 2.05 : 1.55)); - this.focusNodes.endFill(); - } - } - - if (options.updateLabels !== false) this.updateLabels(frame); - this.updateLabelPositions(); - } - - updateLabels(frame) { - const data = frame.data; - const graph = frame.bundle?.graph || {}; - const active = frame.active || {}; - const palette = frame.palette; - const hoverOnlyLabels = normalizeLabelVisibility(frame.labelVisibility) === "hover"; - const interactive = frame.mode === "interactive" || frame.mode === "spin"; - const budget = interactive ? 120 : 220; - const items = []; - const seen = new Set(); - const pushItem = id => { - if (id === null || id === undefined || seen.has(id)) return; - const item = data.nodesById.get(id); - if (!item) return; - seen.add(id); - items.push(item); - }; - - pushItem(active.activeNodeId); - pushItem(frame.selectedNodeId); - pushItem(graph.focusId); - for (const id of active.pinnedNodeIds || []) pushItem(id); - for (const item of data.searchMatchItems || []) pushItem(item.node.id); - for (const id of active.labelNodes || []) { - if (items.length >= budget) break; - pushItem(id); - } - if (!hoverOnlyLabels) { - for (const item of data.labelItems || []) { - if (items.length >= budget) break; - if (seen.has(item.node.id)) continue; - if (zoomLabelStrength(item, frame.zoom, graph) <= 0.08 && !item.searchMatch) continue; - pushItem(item.node.id); - } - } - - this.hideLabels(); - const fontSize = 12; - const lineHeight = 14; - const screenScale = labelScreenScale(frame.zoom); - for (const item of items) { - const labelStrength = this.labelStrengthForItem(item, frame, active, graph, hoverOnlyLabels); - if (labelStrength <= 0.02) continue; - const rootNode = item.node.id === graph.rootId; - const folderNode = item.node.type === "folder"; - const leading = Number.isFinite(item.labelRank) && item.labelRank < 36; - const localScale = screenScale * (rootNode ? 1.18 : item.radius >= 24 ? 1.1 : item.radius >= 15 ? 1.04 : 1); - const weight = rootNode || leading || folderNode ? "600" : "400"; - const maxWidth = (rootNode ? 190 : folderNode ? 156 : leading ? 146 : 124) * localScale; - const maxLines = rootNode || item.node.id === active.activeNodeId || item.node.id === frame.selectedNodeId ? 4 : 3; - this.measureCtx.font = `${weight} ${fontSize * localScale}px ${palette.fontFamily}`; - const lines = wrapCanvasLabel(this.measureCtx, item.label, maxWidth, maxLines); - if (!lines.length) continue; - - const text = this.nextLabel(); - text.text = lines.join("\n"); - text.visible = true; - text.alpha = labelStrength * (rootNode ? 0.98 : leading || active.labelNodes?.has(item.node.id) ? 0.94 : 0.86); - text.anchor.set(0.5, 0); - text.__mwmItem = item; - text.__mwmLocalScale = localScale; - text.style = this.getTextStyle({ - fontFamily: palette.fontFamily, - fontSize: fontSize * localScale, - fontWeight: weight, - lineHeight: lineHeight * localScale, - fill: this.color(rootNode ? (palette.circle || palette.labelText || palette.text) : (palette.labelText || palette.text)).rgb, - stroke: this.color(palette.labelStroke || palette.bg).rgb, - strokeThickness: (rootNode ? 6 : 5) * localScale - }); - } - } - - labelStrengthForItem(item, frame, active, graph, hoverOnlyLabels) { - if (!item || !item.node) return 0; - const nodeId = item.node.id; - const directlyRequested = nodeId === active.activeNodeId - || nodeId === frame.selectedNodeId - || active.pinnedNodeIds?.has(nodeId) - || item.searchMatch; - if (hoverOnlyLabels) return directlyRequested ? 1 : 0; - if (directlyRequested || active.labelNodes?.has(nodeId)) return 1; - return zoomLabelStrength(item, frame.zoom, graph); - } - - updateLabelPositions() { - const { panX, panY, zoom } = this.lastTransform || { panX: 0, panY: 0, zoom: 1 }; - for (let index = 0; index < this.activeLabels; index += 1) { - const text = this.labelPool[index]; - const item = text?.__mwmItem; - if (!text || !item) continue; - const localScale = text.__mwmLocalScale || 1; - const nodeZoomScale = this.nodeZoomScale(); - text.x = panX + item.point.x * zoom; - text.y = panY + item.point.y * zoom + item.radius * nodeZoomScale * zoom + 8 * localScale; - } - } - - nextLabel() { - let text = this.labelPool[this.activeLabels]; - if (!text) { - text = new this.PIXI.Text(""); - text.eventMode = "none"; - text.resolution = 2; - this.labelPool.push(text); - this.labels.addChild(text); - } - this.activeLabels += 1; - return text; - } - - hideLabels() { - for (let index = 0; index < this.labelPool.length; index += 1) { - this.labelPool[index].visible = false; - } - this.activeLabels = 0; - } - - getTextStyle(options) { - const key = [ - options.fontFamily, - Math.round(options.fontSize * 10) / 10, - options.fontWeight, - Math.round(options.lineHeight * 10) / 10, - options.fill, - options.stroke, - Math.round(options.strokeThickness * 10) / 10 - ].join("|"); - let style = this.textStyleCache.get(key); - if (!style) { - style = new this.PIXI.TextStyle({ - fontFamily: options.fontFamily, - fontSize: options.fontSize, - fontWeight: options.fontWeight, - lineHeight: options.lineHeight, - align: "center", - fill: options.fill, - stroke: options.stroke, - strokeThickness: options.strokeThickness, - wordWrap: false - }); - this.textStyleCache.set(key, style); - if (this.textStyleCache.size > 160) { - const firstKey = this.textStyleCache.keys().next().value; - this.textStyleCache.delete(firstKey); - } - } - return style; - } -} + if (totalWeight > 0.0) { + prefilteredColor = prefilteredColor / totalWeight; + } -function pixiParseColor(value) { - if (!value || typeof value !== "string") return null; - const input = value.trim(); - const hex = input.match(/^#([0-9a-f]{3}|[0-9a-f]{6}|[0-9a-f]{8})$/i); - if (hex) { - let raw = hex[1]; - if (raw.length === 3) raw = raw.split("").map(ch => ch + ch).join(""); - const rgb = parseInt(raw.slice(0, 6), 16); - const alpha = raw.length === 8 ? parseInt(raw.slice(6, 8), 16) / 255 : 1; - return { rgb, alpha }; - } - - const rgb = input.match(/^rgba?\((.+)\)$/i); - if (rgb) { - const parts = rgb[1] - .replace(/\s*\/\s*/g, " ") - .replace(/,/g, " ") - .split(/\s+/) - .filter(Boolean); - if (parts.length >= 3) { - const r = parsePixiCssChannel(parts[0]); - const g = parsePixiCssChannel(parts[1]); - const b = parsePixiCssChannel(parts[2]); - const alpha = parts.length >= 4 ? parsePixiCssAlpha(parts[3]) : 1; - if ([r, g, b, alpha].every(Number.isFinite)) return { rgb: (r << 16) | (g << 8) | b, alpha }; - } - } - - const srgb = input.match(/^color\(\s*srgb\s+(.+)\)$/i); - if (srgb) { - const parts = srgb[1] - .replace(/\s*\/\s*/g, " ") - .split(/\s+/) - .filter(Boolean); - if (parts.length >= 3) { - const r = parsePixiCssUnitChannel(parts[0]); - const g = parsePixiCssUnitChannel(parts[1]); - const b = parsePixiCssUnitChannel(parts[2]); - const alpha = parts.length >= 4 ? parsePixiCssAlpha(parts[3]) : 1; - if ([r, g, b, alpha].every(Number.isFinite)) return { rgb: (r << 16) | (g << 8) | b, alpha }; - } - } - - return null; -} + gl_FragColor = vec4(prefilteredColor, 1.0); + } + `,blending:yn,depthTest:!1,depthWrite:!1})}function cv(i,e,t){let n=new Float32Array(Ji),s=new N(0,1,0);return new ut({name:"SphericalGaussianBlur",defines:{n:Ji,CUBEUV_TEXEL_WIDTH:1/e,CUBEUV_TEXEL_HEIGHT:1/t,CUBEUV_MAX_MIP:`${i}.0`},uniforms:{envMap:{value:null},samples:{value:1},weights:{value:n},latitudinal:{value:!1},dTheta:{value:0},mipInt:{value:0},poleAxis:{value:s}},vertexShader:sl(),fragmentShader:` -function parsePixiCssChannel(value) { - if (String(value).endsWith("%")) return clampNumber(parseFloat(value) * 2.55, 0, 255, 0); - return clampNumber(parseFloat(value), 0, 255, 0); -} + precision mediump float; + precision mediump int; -function parsePixiCssUnitChannel(value) { - if (String(value).endsWith("%")) return clampNumber(parseFloat(value) * 2.55, 0, 255, 0); - return clampNumber(parseFloat(value) * 255, 0, 255, 0); -} + varying vec3 vOutputDirection; -function parsePixiCssAlpha(value) { - if (String(value).endsWith("%")) return clampFloat(parseFloat(value) / 100, 0, 1, 1); - return clampFloat(parseFloat(value), 0, 1, 1); -} + uniform sampler2D envMap; + uniform int samples; + uniform float weights[ n ]; + uniform bool latitudinal; + uniform float dTheta; + uniform float mipInt; + uniform vec3 poleAxis; -function pixiDrawRingPath(graphics, centerX, centerY, radius, depth = 0, swirlStrength = 0, orbitPhase = 0) { - const strength = clampFloat(swirlStrength, 0, 1, 0); - if (strength <= 0.001) { - graphics.drawCircle(centerX, centerY, radius); - return; - } - - const fullCircle = Math.PI * 2; - const steps = 180; - const amplitude = Math.min(radius * 0.045, 58) * strength; - const phase = depth * 0.74 + strength * 0.9 + orbitPhase; - - for (let step = 0; step <= steps; step += 1) { - const t = step / steps; - const angle = t * fullCircle; - const twist = Math.sin(angle - phase) * 0.08 * strength; - const ripple = Math.sin(angle * 2 + phase) * amplitude - + Math.sin(angle * 5 - phase * 0.7) * amplitude * 0.26; - const r = Math.max(1, radius + ripple); - const x = centerX + Math.cos(angle + twist) * r; - const y = centerY + Math.sin(angle + twist) * r; - if (step === 0) graphics.moveTo(x, y); - else graphics.lineTo(x, y); - } - graphics.closePath(); -} + #define ENVMAP_TYPE_CUBE_UV + #include -class MiniWorldMapPlugin extends Plugin { - async onload() { - this.settings = normalizeSettings(await this.loadData()); - this.nativeGraphSettings = Object.assign({}, DEFAULT_NATIVE_GRAPH_SETTINGS); - this.index = new WorldMapIndex(this.app, this.settings); - this.rebuildTimer = null; - this.themeRefreshTimer = null; - this.themeObserver = null; - - this.registerView( - VIEW_TYPE_MINI_WORLD_MAP, - leaf => new MiniWorldMapView(leaf, this) - ); - - this.addRibbonIcon("network", "Open Mini World Map", () => { - this.activateView(); - }); - - this.addCommand({ - id: "open-map", - name: "Open Mini World Map", - callback: () => this.activateView() - }); - - this.addCommand({ - id: "rebuild-index", - name: "Rebuild Mini World Map index", - callback: () => this.rebuildIndex("manual") - }); - - this.addSettingTab(new MiniWorldMapSettingTab(this.app, this)); - - this.registerEvent(this.app.vault.on("create", () => this.scheduleRebuild("vault create"))); - this.registerEvent(this.app.vault.on("modify", file => { - if (file instanceof TFile && file.extension === "md") this.scheduleRebuild("vault modify"); - })); - this.registerEvent(this.app.vault.on("delete", () => this.scheduleRebuild("vault delete"))); - this.registerEvent(this.app.vault.on("rename", () => this.scheduleRebuild("vault rename"))); - this.registerEvent(this.app.metadataCache.on("resolved", () => this.scheduleRebuild("metadata resolved"))); - this.installThemeRefresh(); - - this.app.workspace.onLayoutReady(() => { - this.loadNativeGraphSettings(); - }); - } - - onunload() { - if (this.rebuildTimer) window.clearTimeout(this.rebuildTimer); - if (this.themeRefreshTimer) window.clearTimeout(this.themeRefreshTimer); - if (this.themeObserver) this.themeObserver.disconnect(); - } - - async activateView() { - const leaves = this.app.workspace.getLeavesOfType(VIEW_TYPE_MINI_WORLD_MAP); - let leaf = leaves.first(); - - if (!leaf) { - leaf = this.app.workspace.getLeaf("tab"); - await leaf.setViewState({ type: VIEW_TYPE_MINI_WORLD_MAP, active: true }); - } - - this.app.workspace.revealLeaf(leaf); - if (!this.index.ready) await this.rebuildIndex("open"); - } - - scheduleRebuild(reason) { - if (!this.index.ready && !this.hasOpenMapView()) return; - if (this.rebuildTimer) window.clearTimeout(this.rebuildTimer); - this.rebuildTimer = window.setTimeout(() => { - this.rebuildTimer = null; - this.rebuildIndex(reason); - }, 900); - } - - hasOpenMapView() { - return this.app.workspace.getLeavesOfType(VIEW_TYPE_MINI_WORLD_MAP).length > 0; - } - - async rebuildIndex(reason, options = {}) { - try { - const started = performance.now(); - this.index = new WorldMapIndex(this.app, this.settings); - this.index.rebuild(); - const elapsed = Math.round(performance.now() - started); - this.refreshViews(options); - if (reason === "manual") { - new Notice(`Mini World Map rebuilt in ${elapsed} ms`); - } - } catch (error) { - console.error("Mini World Map index rebuild failed", error); - new Notice("Mini World Map index rebuild failed. See developer console."); - } - } - - refreshViews(options = {}) { - for (const leaf of this.app.workspace.getLeavesOfType(VIEW_TYPE_MINI_WORLD_MAP)) { - const view = leaf.view; - if (view && typeof view.refresh === "function") view.refresh(options); - } - } - - installThemeRefresh() { - const refresh = () => this.scheduleThemeRefresh(); - if (this.app.workspace && typeof this.app.workspace.on === "function") { - this.registerEvent(this.app.workspace.on("css-change", refresh)); - } - - if (typeof MutationObserver === "undefined" || typeof document === "undefined" || !document.body) return; - this.themeObserver = new MutationObserver(records => { - if (records.some(record => record.attributeName === "class")) refresh(); - }); - this.themeObserver.observe(document.body, { - attributes: true, - attributeFilter: ["class"] - }); - } - - scheduleThemeRefresh() { - if (this.themeRefreshTimer) window.clearTimeout(this.themeRefreshTimer); - this.themeRefreshTimer = window.setTimeout(() => { - this.themeRefreshTimer = null; - this.refreshViews(); - }, 60); - } - - async saveSettings(options = {}) { - await this.saveData(this.settings); - if (options.rebuild !== false) { - if (options.immediateRebuild) { - if (this.rebuildTimer) { - window.clearTimeout(this.rebuildTimer); - this.rebuildTimer = null; - } - await this.rebuildIndex("settings", { preservePanel: options.preservePanel === true }); - } - else this.scheduleRebuild("settings"); - } else if (options.refresh !== false) this.refreshViews(); - } - - applySettingsToOpenViews(keys, options = {}) { - const settingKeys = Array.isArray(keys) ? keys : [keys].filter(Boolean); - if (!settingKeys.length) return; - for (const leaf of this.app.workspace.getLeavesOfType(VIEW_TYPE_MINI_WORLD_MAP)) { - const view = leaf.view; - if (view && typeof view.applyPluginSettings === "function") { - view.applyPluginSettings(settingKeys, options); - } - } - } - - async loadNativeGraphSettings() { - try { - const raw = await this.app.vault.adapter.read(".obsidian/graph.json"); - this.nativeGraphSettings = Object.assign( - {}, - DEFAULT_NATIVE_GRAPH_SETTINGS, - JSON.parse(raw || "{}") - ); - this.refreshViews(); - } catch (error) { - this.nativeGraphSettings = Object.assign({}, DEFAULT_NATIVE_GRAPH_SETTINGS); - } - } -} + vec3 getSample( float theta, vec3 axis ) { -class WorldMapIndex { - constructor(app, settings) { - this.app = app; - this.settings = settings; - this.nodes = new Map(); - this.hierarchyEdges = []; - this.linkEdges = []; - this.childrenByParent = new Map(); - this.linkEdgesBySource = new Map(); - this.linkEdgesByTarget = new Map(); - this.folderRepresentatives = new Map(); - this.stats = { - loadedEntries: 0, - scannedMarkdown: 0, - folders: 0, - notes: 0, - unresolved: 0, - hierarchyEdges: 0, - linkEdges: 0, - maxDepth: 0 - }; - this.ready = false; - } - - rebuild() { - this.addNode({ - id: ROOT_ID, - path: ROOT_ID, - title: vaultRootTitle(this.app), - type: "folder", - parentId: null, - depth: 0, - noteCount: 0, - linkCount: 0, - backlinkCount: 0, - descendantCount: 0 - }); - - const { folders, markdownFiles, totalEntries } = this.collectVaultEntries(); - this.stats.loadedEntries = totalEntries; - this.stats.scannedMarkdown = markdownFiles.length; - - for (const folder of folders) { - this.addFolder(folder); - } - - for (const file of markdownFiles) { - this.ensureFolderPath(parentPath(file.path)); - this.addNote(file); - } - - this.identifyFolderRepresentatives(); - this.buildHierarchyEdges(); - this.buildLinkEdges(); - this.computeStats(); - this.ready = true; - } - - collectVaultEntries() { - const folders = []; - const markdownFiles = []; - let totalEntries = 0; - const root = typeof this.app.vault.getRoot === "function" - ? this.app.vault.getRoot() - : null; - const stack = root && Array.isArray(root.children) - ? root.children.slice() - : []; - - while (stack.length) { - const entry = stack.pop(); - totalEntries += 1; - - if (entry instanceof TFolder) { - folders.push(entry); - if (Array.isArray(entry.children)) { - for (let index = entry.children.length - 1; index >= 0; index -= 1) { - stack.push(entry.children[index]); - } - } - } else if (entry instanceof TFile && entry.extension === "md") { - markdownFiles.push(entry); - } - } - - return { folders, markdownFiles, totalEntries }; - } - - addFolder(folder) { - this.ensureFolderPath(normalizeVaultPath(folder.path)); - } - - ensureFolderPath(folderPath) { - const normalizedPath = normalizeVaultPath(folderPath); - if (!normalizedPath) return; - const parts = normalizedPath.split("/").filter(Boolean); - let current = ROOT_ID; - - for (const part of parts) { - const next = current ? `${current}/${part}` : part; - if (this.shouldIgnorePath(next)) return; - if (!this.nodes.has(next)) { - this.addNode({ - id: next, - path: next, - title: basename(next), - type: "folder", - parentId: current, - depth: depthOfPath(next), - noteCount: 0, - linkCount: 0, - backlinkCount: 0, - descendantCount: 0 - }); - } - current = next; - } - } - - addNote(file) { - if (this.shouldIgnorePath(file.path)) return; - - const parentId = normalizeVaultPath(parentPath(file.path)); - this.addNode({ - id: file.path, - path: file.path, - title: file.basename, - type: "note", - parentId, - depth: depthOfPath(file.path), - noteCount: 1, - linkCount: 0, - backlinkCount: 0, - descendantCount: 0, - file - }); - } - - addUnresolvedNode(sourcePath, linkText) { - const id = `unresolved:${sourcePath}:${linkText}`; - if (!this.nodes.has(id)) { - const source = this.nodes.get(sourcePath); - this.addNode({ - id, - path: linkText, - title: linkText, - type: "unresolved", - parentId: source ? source.parentId : ROOT_ID, - depth: source ? source.depth + 1 : 1, - noteCount: 0, - linkCount: 0, - backlinkCount: 0, - descendantCount: 0 - }); - } - return id; - } - - addNode(node) { - if (!node || this.nodes.has(node.id)) return; - this.nodes.set(node.id, node); - } - - identifyFolderRepresentatives() { - const notes = Array.from(this.nodes.values()).filter(node => node.type === "note"); - const notesByParent = new Map(); - - for (const note of notes) { - if (!notesByParent.has(note.parentId)) notesByParent.set(note.parentId, []); - notesByParent.get(note.parentId).push(note); - } - - for (const folder of this.nodes.values()) { - if (folder.type !== "folder" || folder.id === ROOT_ID) continue; - const siblings = notesByParent.get(folder.id) || []; - const folderTitle = comparableTitle(folder.title); - const representative = siblings.find(note => comparableTitle(note.title) === folderTitle); - if (representative) { - folder.representativeFile = representative.path; - representative.isRepresentativeFile = true; - representative.representativeFor = folder.id; - this.folderRepresentatives.set(folder.id, representative.path); - } - } - } - - buildHierarchyEdges() { - this.childrenByParent.clear(); - - for (const node of this.nodes.values()) { - if (node.parentId === null) continue; - if (!this.nodes.has(node.parentId)) continue; - this.hierarchyEdges.push({ - id: `hierarchy:${node.parentId}->${node.id}`, - type: "hierarchy", - source: node.parentId, - target: node.id, - weight: 1 - }); - if (!this.childrenByParent.has(node.parentId)) this.childrenByParent.set(node.parentId, []); - this.childrenByParent.get(node.parentId).push(node.id); - } - - for (const children of this.childrenByParent.values()) { - children.sort((a, b) => compareNodes(this.nodes.get(a), this.nodes.get(b))); - } - } - - buildLinkEdges() { - const resolved = this.app.metadataCache.resolvedLinks || {}; - const unresolved = this.app.metadataCache.unresolvedLinks || {}; - - for (const [sourcePath, targets] of Object.entries(resolved)) { - if (!this.nodes.has(sourcePath) || this.shouldIgnorePath(sourcePath)) continue; - for (const [targetPath, count] of Object.entries(targets || {})) { - if (!this.nodes.has(targetPath) || this.shouldIgnorePath(targetPath)) continue; - this.addLinkEdge(sourcePath, targetPath, Math.max(1, Number(count) || 1), "link"); - } - } - - if (this.settings.includeUnresolvedLinks) { - for (const [sourcePath, targets] of Object.entries(unresolved)) { - if (!this.nodes.has(sourcePath) || this.shouldIgnorePath(sourcePath)) continue; - for (const [linkText, count] of Object.entries(targets || {})) { - const targetId = this.addUnresolvedNode(sourcePath, linkText); - this.addLinkEdge(sourcePath, targetId, Math.max(1, Number(count) || 1), "unresolved-link"); - } - } - } - } - - addLinkEdge(source, target, weight, type) { - if (source === target) return; - const edge = { - id: `${type}:${source}->${target}:${this.linkEdges.length}`, - type, - source, - target, - weight - }; - this.linkEdges.push(edge); - - const sourceNode = this.nodes.get(source); - const targetNode = this.nodes.get(target); - if (sourceNode) sourceNode.linkCount += weight; - if (targetNode) targetNode.backlinkCount += weight; - - if (!this.linkEdgesBySource.has(source)) this.linkEdgesBySource.set(source, []); - if (!this.linkEdgesByTarget.has(target)) this.linkEdgesByTarget.set(target, []); - this.linkEdgesBySource.get(source).push(edge); - this.linkEdgesByTarget.get(target).push(edge); - } - - computeStats() { - const sorted = Array.from(this.nodes.values()).sort((a, b) => b.depth - a.depth); - - for (const node of sorted) { - node.descendantCount = node.type === "note" ? 1 : node.noteCount; - if (!node.parentId || !this.nodes.has(node.parentId)) continue; - const parent = this.nodes.get(node.parentId); - const subtreeNoteCount = node.type === "note" ? 1 : node.noteCount; - parent.descendantCount += subtreeNoteCount; - parent.noteCount += subtreeNoteCount; - parent.linkCount += node.linkCount; - parent.backlinkCount += node.backlinkCount; - } - - this.stats = { - loadedEntries: this.stats.loadedEntries, - scannedMarkdown: this.stats.scannedMarkdown, - folders: Array.from(this.nodes.values()).filter(node => node.type === "folder").length, - notes: Array.from(this.nodes.values()).filter(node => node.type === "note").length, - unresolved: Array.from(this.nodes.values()).filter(node => node.type === "unresolved").length, - hierarchyEdges: this.hierarchyEdges.length, - linkEdges: this.linkEdges.length, - maxDepth: Math.max(...Array.from(this.nodes.values()).map(node => node.depth), 0) - }; - } - - shouldIgnorePath(path) { - if (!path) return false; - const ignores = this.settings.ignoreFolders || []; - return ignores.some(prefix => path === prefix || path.startsWith(`${prefix}/`)); - } - - getActiveNotePath() { - const active = this.app.workspace.getActiveFile(); - if (active && this.nodes.has(active.path)) return active.path; - if (this.nodes.has("Universe, Self-Awareness, and Intelligence.md")) { - return "Universe, Self-Awareness, and Intelligence.md"; - } - const firstNote = Array.from(this.nodes.values()).find(node => node.type === "note"); - return firstNote ? firstNote.id : null; - } - - buildVisibleGraph(state) { - if (!this.ready) return { nodes: [], hierarchyEdges: [], linkEdges: [], rootId: ROOT_ID }; - - if (state.mode === "focus") return this.buildFocusGraph(state); - return this.buildAtlasGraph(state); - } - - buildAtlasGraph(state) { - const rootId = this.nodes.has(state.rootPath) ? state.rootPath : ROOT_ID; - const rootDepth = this.nodes.get(rootId).depth; - const maxDepth = clampNumber( - state.atlasDepth === undefined || state.atlasDepth === null ? this.settings.atlasDepth : state.atlasDepth, - 1, - MAX_ATLAS_DEPTH, - this.settings.atlasDepth - ); - const visible = new Set(); - const query = normalizedQuery(state.search); - - const stack = [rootId]; - while (stack.length) { - const id = stack.pop(); - const node = this.nodes.get(id); - if (!node) continue; - const relDepth = Math.max(0, node.depth - rootDepth); - const isWithinDepth = relDepth <= maxDepth; - const matchesSearch = query && nodeMatches(node, query); - - if (isWithinDepth || matchesSearch) { - visible.add(id); - if (matchesSearch) this.addAncestors(id, visible, rootId); - } - - if (isWithinDepth || matchesSearch) { - const children = this.childrenByParent.get(id) || []; - for (let i = children.length - 1; i >= 0; i -= 1) stack.push(children[i]); - } - } - - if (query) { - for (const node of this.nodes.values()) { - if (nodeMatches(node, query)) { - visible.add(node.id); - this.addAncestors(node.id, visible, rootId); - } - } - } - - this.addDirectFilesForVisibleFolders(visible); - return this.materializeVisibleGraph(visible, rootId, state); - } - - buildFocusGraph(state) { - const activePath = state.focusPath && this.nodes.has(state.focusPath) - ? state.focusPath - : this.getActiveNotePath(); - const rootId = activePath || ROOT_ID; - const visible = new Set([ROOT_ID]); - const siblingLimit = Math.max(10, Number(this.settings.focusSiblingLimit) || 80); - - if (activePath) { - visible.add(activePath); - this.addAncestors(activePath, visible, ROOT_ID); - - const active = this.nodes.get(activePath); - if (active && active.parentId) { - const siblings = this.childrenByParent.get(active.parentId) || []; - for (const siblingId of siblings.slice(0, siblingLimit)) visible.add(siblingId); - } - - const connected = [ - ...(this.linkEdgesBySource.get(activePath) || []), - ...(this.linkEdgesByTarget.get(activePath) || []) - ]; - connected - .sort((a, b) => b.weight - a.weight) - .slice(0, Math.max(30, siblingLimit)) - .forEach(edge => { - visible.add(edge.source); - visible.add(edge.target); - this.addAncestors(edge.source, visible, ROOT_ID); - this.addAncestors(edge.target, visible, ROOT_ID); - }); - } - - const query = normalizedQuery(state.search); - if (query) { - for (const node of this.nodes.values()) { - if (nodeMatches(node, query)) { - visible.add(node.id); - this.addAncestors(node.id, visible, ROOT_ID); - } - } - } - - this.addDirectFilesForVisibleFolders(visible); - return this.materializeVisibleGraph(visible, ROOT_ID, Object.assign({}, state, { focusPath: activePath, rootPath: ROOT_ID }), rootId); - } - - addDirectFilesForVisibleFolders(visible) { - for (const id of Array.from(visible)) { - const node = this.nodes.get(id); - if (!node || node.type !== "folder") continue; - for (const childId of this.childrenByParent.get(id) || []) { - const child = this.nodes.get(childId); - if (child && (child.type === "note" || child.type === "unresolved")) { - visible.add(childId); - } - } - } - } - - addAncestors(id, visible, stopId) { - let current = this.nodes.get(id); - while (current && current.parentId !== null) { - visible.add(current.id); - if (current.id === stopId) break; - current = this.nodes.get(current.parentId); - } - visible.add(stopId || ROOT_ID); - } - - materializeVisibleGraph(visible, rootId, state, focusId) { - let nodes = Array.from(visible) - .map(id => this.nodes.get(id)) - .filter(Boolean) - .sort(compareNodes); - - const budget = this.applyNodeBudget(nodes, rootId, state, focusId); - nodes = budget.nodes; - nodes = this.foldRepresentativeNodes(nodes); - - const nodeSet = new Set(nodes.map(node => node.id)); - const hierarchyEdges = this.hierarchyEdges.filter(edge => nodeSet.has(edge.source) && nodeSet.has(edge.target)); - const linkBundle = this.aggregateVisibleLinkEdges(nodeSet, state, rootId); - nodes = nodes.concat(linkBundle.externalNodes); - - const nodesById = new Map(nodes.map(node => [node.id, node])); - - return { - nodes, - nodesById, - hierarchyEdges: hierarchyEdges.concat(linkBundle.externalHierarchyEdges), - linkEdges: linkBundle.linkEdges, - hoverLinkEdges: linkBundle.hoverLinkEdges, - rootId, - focusId: this.visualNodeId(focusId) || null, - hiddenNodeCount: budget.hiddenNodeCount, - externalNodeCount: linkBundle.externalNodes.length, - externalFileCount: linkBundle.externalFileCount || 0, - externalGroupCount: linkBundle.externalGroupCount || 0 - }; - } - - foldRepresentativeNodes(nodes) { - const visibleIds = new Set(nodes.map(node => node.id)); - return nodes.filter(node => { - if (!node || !node.isRepresentativeFile) return true; - return !visibleIds.has(node.representativeFor); - }); - } - - visualNodeId(id) { - const node = this.nodes.get(id); - if (node && node.isRepresentativeFile && node.representativeFor && this.nodes.has(node.representativeFor)) { - return node.representativeFor; - } - return id; - } - - aggregateVisibleLinkEdges(visible, state, rootId) { - const needsHoverLinks = hoverHighlightsNoteLinks(state.hoverHighlightMode) || Boolean(state.pinNeedsHoverLinks); - const hiddenLegend = hiddenLegendSetFromState(state); - const wantsInternalLinkOverlay = !hiddenLegend.has("link"); - const wantsUnresolvedLinkOverlay = wantsInternalLinkOverlay && !hiddenLegend.has("dashed-link"); - const wantsExternalLinkOverlay = !hiddenLegend.has("outside-link"); - const wantsExternalNodes = !hiddenLegend.has("outside") || !hiddenLegend.has("outside-file"); - const showExternalLinks = state.showExternalLinks !== false && rootId !== ROOT_ID && (wantsExternalNodes || wantsExternalLinkOverlay); - if (!wantsInternalLinkOverlay && !wantsUnresolvedLinkOverlay && !needsHoverLinks && !showExternalLinks) return { - linkEdges: [], - hoverLinkEdges: [], - externalNodes: [], - externalHierarchyEdges: [], - externalFileCount: 0, - externalGroupCount: 0 - }; - const aggregate = new Map(); - const externalNodes = new Map(); - const externalHierarchyEdges = []; - const externalDetailMode = state.externalDetailMode || this.settings.externalDetailMode || DEFAULT_SETTINGS.externalDetailMode; - const externalLimit = clampNumber( - state.externalLinkAnchorLimit === undefined || state.externalLinkAnchorLimit === null ? this.settings.externalLinkAnchorLimit : state.externalLinkAnchorLimit, - 0, - MAX_EXTERNAL_LINK_ANCHOR_LIMIT, - DEFAULT_SETTINGS.externalLinkAnchorLimit - ); - const externalContext = { fileCount: 0, groupIds: new Set(), overflowId: null }; - - for (const edge of this.linkEdges) { - let source = this.nearestVisibleAncestor(edge.source, visible, rootId); - let target = this.nearestVisibleAncestor(edge.target, visible, rootId); - let externalCount = 0; - - if (showExternalLinks && rootId !== ROOT_ID) { - if (!source && target) { - source = this.externalEndpointFor( - edge.source, - rootId, - externalNodes, - externalHierarchyEdges, - externalLimit, - externalContext, - this.shouldUseExactExternalEndpoint(edge, state, externalDetailMode, target) - ); - if (source) externalCount = 1; - } - if (source && !target) { - target = this.externalEndpointFor( - edge.target, - rootId, - externalNodes, - externalHierarchyEdges, - externalLimit, - externalContext, - this.shouldUseExactExternalEndpoint(edge, state, externalDetailMode, source) - ); - if (target) externalCount = 1; - } - } - - if (!source || !target || source === target) continue; - const key = `${source}->${target}`; - const existing = aggregate.get(key) || { - id: `visible-link:${key}`, - type: "visible-link", - source, - target, - weight: 0, - rawCount: 0, - unresolvedCount: 0, - externalCount: 0 - }; - existing.weight += edge.weight; - existing.rawCount += 1; - if (edge.type === "unresolved-link") existing.unresolvedCount += 1; - existing.externalCount += externalCount; - aggregate.set(key, existing); - } - - const allLinkEdges = Array.from(aggregate.values()) - .sort((a, b) => linkRenderScore(b) - linkRenderScore(a)); - const linkLimit = clampNumber( - state.linkLimit === undefined || state.linkLimit === null ? this.settings.linkLimit : state.linkLimit, - 0, - MAX_LINK_LIMIT, - DEFAULT_SETTINGS.linkLimit - ); - const showAllLinks = Boolean(state.showCompleteRoot); - const completeLinkLimit = showAllLinks ? MAX_LINK_LIMIT : linkLimit; - const linkEdges = []; - const visibleLinkIds = new Set(); - const allowsVisibleLink = edge => { - if (!edge) return false; - if (edge.externalCount) return wantsExternalLinkOverlay && (!edge.unresolvedCount || wantsUnresolvedLinkOverlay); - if (edge.unresolvedCount) return wantsUnresolvedLinkOverlay; - return wantsInternalLinkOverlay; - }; - const pushVisibleLink = edge => { - if (!allowsVisibleLink(edge) || visibleLinkIds.has(edge.id)) return; - visibleLinkIds.add(edge.id); - linkEdges.push(edge); - }; - const visibleLinkLimit = showAllLinks - ? MAX_LINK_LIMIT - : clampNumber( - Math.max(completeLinkLimit, showExternalLinks ? Math.min(MAX_LINK_LIMIT, Math.max(externalLimit, 120)) : 0), - 0, - MAX_LINK_LIMIT, - DEFAULT_SETTINGS.linkLimit - ); - for (const edge of allLinkEdges) { - if (linkEdges.length >= visibleLinkLimit) break; - pushVisibleLink(edge); - } - const hoverLinkEdges = showAllLinks ? linkEdges : allLinkEdges.filter(allowsVisibleLink); - - const usedExternalIds = new Set(); - const externalNodeEdges = showExternalLinks - ? allLinkEdges.filter(edge => edge.externalCount) - : needsHoverLinks - ? hoverLinkEdges - : []; - for (const edge of externalNodeEdges) { - if (externalNodes.has(edge.source)) usedExternalIds.add(edge.source); - if (externalNodes.has(edge.target)) usedExternalIds.add(edge.target); - } - - const collectedExternalNodes = this.collectUsedExternalNodes(usedExternalIds, externalNodes); - - return { - linkEdges, - hoverLinkEdges, - externalNodes: collectedExternalNodes, - externalHierarchyEdges: externalHierarchyEdges.filter(edge => usedExternalIds.has(edge.target)), - externalFileCount: collectedExternalNodes.filter(node => node.externalProxy).length, - externalGroupCount: collectedExternalNodes.filter(node => node.type === "external" && !node.externalProxy).length - }; - } - - nearestVisibleAncestor(id, visible, rootId) { - let current = this.nodes.get(id); - while (current) { - if (visible.has(current.id)) return current.id; - if (current.id === rootId) return rootId; - current = current.parentId === null ? null : this.nodes.get(current.parentId); - } - return visible.has(ROOT_ID) ? ROOT_ID : null; - } - - collectUsedExternalNodes(usedExternalIds, externalNodes) { - const collected = new Map(); - for (const id of usedExternalIds) { - const node = externalNodes.get(id); - if (!node) continue; - if (node.externalParentId && externalNodes.has(node.externalParentId)) { - collected.set(node.externalParentId, externalNodes.get(node.externalParentId)); - } - collected.set(id, node); - } - return Array.from(collected.values()); - } - - shouldUseExactExternalEndpoint(edge, state, detailMode, visibleEndpoint) { - if (detailMode === "exact") return true; - if (detailMode === "grouped") return false; - - const selectedNodeId = state.selectedNodeId; - if (selectedNodeId !== null && selectedNodeId !== undefined && ( - edge.source === selectedNodeId - || edge.target === selectedNodeId - || visibleEndpoint === selectedNodeId - )) { - return true; - } - - const selectedLink = state.selectedLink; - return Boolean(selectedLink && ( - selectedLink.source === visibleEndpoint - || selectedLink.target === visibleEndpoint - || selectedLink.source === edge.source - || selectedLink.target === edge.target - )); - } - - externalEndpointFor(nodeId, rootId, externalNodes, externalHierarchyEdges, externalLimit, context, exactFiles) { - const node = this.nodes.get(nodeId); - if (!node) return null; - const anchorPath = externalAnchorPath(node, rootId); - if (!anchorPath) return null; - - const groupId = this.externalGroupFor(anchorPath, rootId, externalNodes, context); - if (!groupId) return null; - if (!exactFiles) return groupId; - - if (node.type !== "note" && node.type !== "unresolved") return groupId; - - if (externalNodes.has(node.id)) return node.id; - if (context.fileCount >= externalLimit) return this.externalOverflowFor(rootId, groupId, externalNodes, externalHierarchyEdges, context); - - externalNodes.set(node.id, Object.assign({}, node, { - externalProxy: true, - externalParentId: groupId, - externalAnchorPath: anchorPath - })); - context.fileCount += 1; - externalHierarchyEdges.push({ - id: `external-hierarchy:${groupId}->${node.id}`, - type: "external-hierarchy", - source: groupId, - target: node.id, - weight: 1 - }); - - return node.id; - } - - externalGroupFor(anchorPath, rootId, externalNodes, context) { - const id = `external-group:${rootId}:${anchorPath}`; - if (!externalNodes.has(id)) { - const anchorNode = this.nodes.get(anchorPath); - externalNodes.set(id, { - id, - path: anchorPath, - title: `Outside: ${anchorNode ? anchorNode.title : basename(anchorPath)}`, - type: "external", - parentId: null, - depth: (this.nodes.get(rootId)?.depth || 0) + 1, - noteCount: anchorNode ? (anchorNode.noteCount || anchorNode.descendantCount || 0) : 0, - linkCount: 0, - backlinkCount: 0, - descendantCount: anchorNode ? (anchorNode.descendantCount || anchorNode.noteCount || 0) : 0, - externalAnchorPath: anchorPath - }); - context.groupIds.add(id); - } - return id; - } - - externalOverflowFor(rootId, groupId, externalNodes, externalHierarchyEdges, context) { - if (!context.overflowId) { - context.overflowId = `external-overflow:${rootId}`; - externalNodes.set(context.overflowId, { - id: context.overflowId, - path: "outside current root", - title: "More outside files", - type: "external", - parentId: null, - depth: (this.nodes.get(rootId)?.depth || 0) + 1, - noteCount: 0, - linkCount: 0, - backlinkCount: 0, - descendantCount: 0, - externalProxy: true, - externalParentId: groupId, - externalAnchorPath: null - }); - externalHierarchyEdges.push({ - id: `external-hierarchy:${groupId}->${context.overflowId}`, - type: "external-hierarchy", - source: groupId, - target: context.overflowId, - weight: 1 - }); - } - return context.overflowId; - } - - applyNodeBudget(nodes, rootId, state, focusId) { - if (state.showCompleteRoot && nodes.length <= MAX_RENDER_NODE_LIMIT) { - return { nodes, hiddenNodeCount: 0 }; - } - const limit = clampNumber( - state.nodeLimit === undefined || state.nodeLimit === null ? this.settings.renderNodeLimit : state.nodeLimit, - 200, - MAX_RENDER_NODE_LIMIT, - DEFAULT_SETTINGS.renderNodeLimit - ); - if (!limit || nodes.length <= limit) { - return { nodes, hiddenNodeCount: 0 }; - } - - const nodeById = new Map(nodes.map(node => [node.id, node])); - const childrenByParent = buildBudgetChildrenByParent(nodes); - const query = normalizedQuery(state.search); - const keep = new Set([rootId ?? ROOT_ID, focusId].filter(id => id !== null && id !== undefined)); - const candidates = nodes - .slice() - .sort((a, b) => nodeRenderScore(b, rootId, focusId, query) - nodeRenderScore(a, rootId, focusId, query)); - const fileCandidates = candidates.filter(node => node.type === "note" || node.type === "unresolved"); - const targetFileCount = Math.min( - fileCandidates.length, - Math.max(24, Math.floor(limit * (state.showCompleteRoot ? 0.46 : 0.38))) - ); - let keptFileCount = 0; - this.addDirectFileChildrenToSet( - rootId ?? ROOT_ID, - keep, - nodeById, - childrenByParent, - limit, - state.showCompleteRoot ? 128 : 64 - ); - - for (const node of fileCandidates) { - if (keep.size >= limit || keptFileCount >= targetFileCount) break; - const before = keep.size; - this.addNodeWithAncestorsToSet(node.id, keep, nodeById, limit); - if (keep.has(node.id) && keep.size > before) keptFileCount += 1; - } - - for (const node of candidates) { - if (keep.size >= limit) break; - this.addNodeWithAncestorsToSet(node.id, keep, nodeById, limit); - if (node.type === "folder") { - this.addDirectFileChildrenToSet( - node.id, - keep, - nodeById, - childrenByParent, - limit, - state.showCompleteRoot ? 18 : 8 - ); - } - } - - const keptNodes = nodes.filter(node => keep.has(node.id)); - return { - nodes: keptNodes, - hiddenNodeCount: Math.max(0, nodes.length - keptNodes.length) - }; - } - - addNodeWithAncestorsToSet(id, keep, nodeById, limit) { - let current = nodeById.get(id); - const chain = []; - while (current && !keep.has(current.id)) { - chain.push(current.id); - current = current.parentId === null ? null : nodeById.get(current.parentId); - } - - for (let i = chain.length - 1; i >= 0; i -= 1) { - if (keep.size >= limit) break; - keep.add(chain[i]); - } - } - - addDirectFileChildrenToSet(parentId, keep, nodeById, childrenByParent, limit, perParentLimit) { - const children = childrenByParent.get(parentId) || []; - if (!children.length || keep.size >= limit) return; - - let added = 0; - for (const childId of children) { - if (keep.size >= limit || added >= perParentLimit) break; - if (keep.has(childId)) continue; - const child = nodeById.get(childId); - if (!child || (child.type !== "note" && child.type !== "unresolved")) continue; - keep.add(childId); - added += 1; - } - } -} + float cosTheta = cos( theta ); + // Rodrigues' axis-angle rotation + vec3 sampleDirection = vOutputDirection * cosTheta + + cross( axis, vOutputDirection ) * sin( theta ) + + axis * dot( axis, vOutputDirection ) * ( 1.0 - cosTheta ); -class MiniWorldMapView extends ItemView { - constructor(leaf, plugin) { - super(leaf); - this.plugin = plugin; - const hoverHighlightMode = normalizeHoverHighlightMode(plugin.settings.hoverHighlightMode); - this.state = { - mode: "atlas", - search: "", - rootPath: ROOT_ID, - atlasDepth: plugin.settings.atlasDepth, - linkLimit: plugin.settings.linkLimit, - nodeLimit: plugin.settings.renderNodeLimit, - externalLinkAnchorLimit: plugin.settings.externalLinkAnchorLimit, - autoDetail: plugin.settings.adaptiveDetail, - showLinkOverlay: plugin.settings.showLinkOverlay, - enableLinkHover: hoverHighlightsNoteLinks(hoverHighlightMode), - hoverHighlightMode, - showExternalLinks: plugin.settings.showExternalLinks, - externalDetailMode: plugin.settings.externalDetailMode || DEFAULT_SETTINGS.externalDetailMode, - colorScheme: plugin.settings.colorScheme || DEFAULT_SETTINGS.colorScheme, - labelVisibility: normalizeLabelVisibility(plugin.settings.labelVisibility), - focusPath: null, - showCompleteRoot: false, - zoom: 1, - columnSpacing: DEFAULT_RING_SPACING, - rowSpacing: DEFAULT_NODE_SPACING, - swirlStrength: clampNumber(plugin.settings.swirlStrength, 0, MAX_SWIRL_STRENGTH, DEFAULT_SWIRL_STRENGTH), - hiddenLegendItems: Array.isArray(plugin.settings.hiddenLegendItems) ? plugin.settings.hiddenLegendItems.slice() : [], - sidePanelWidth: 360, - sidePage: "inspect", - fullscreen: false - }; - this.preCompleteState = null; - this.positions = new Map(); - this.selectedNodeId = null; - this.selectedLink = null; - this.pinnedPaths = []; - this.pinGroups = []; - this.selectedPinIds = new Set(); - this.nextPinId = 1; - this.nextPinGroupId = 1; - this.pinGroupName = ""; - this.lastLayout = null; - this.renderTimer = null; - this.activeHighlightElements = new Set(); - this.nodeElementsById = new Map(); - this.edgeElementsByNode = new Map(); - this.edgeElementsById = new Map(); - this.canvas = null; - this.pixiRenderer = null; - this.pixiUnavailable = false; - this.canvasData = null; - this.canvasPalette = null; - this.canvasPanX = 0; - this.canvasPanY = 0; - this.canvasDpr = 1; - this.canvasViewportSize = { width: 0, height: 0 }; - this.dragState = null; - this.hoverNodeId = null; - this.hoverLink = null; - this.graphViewSignature = null; - this.viewInitialized = false; - this.lastCanvasBundle = null; - this.canvasVisualNodes = new Map(); - this.canvasVisualEdges = new Map(); - this.canvasVisualInitialized = false; - this.canvasAnimationFrame = null; - this.canvasSettleTimer = null; - this.canvasPanAnimationFrame = null; - this.canvasPanVelocityX = 0; - this.canvasPanVelocityY = 0; - this.canvasZoomAnimationFrame = null; - this.canvasTargetZoom = null; - this.canvasZoomAnchorPoint = null; - this.canvasSpringAnimationFrame = null; - this.canvasSpringState = null; - this.canvasSwirlAnimationFrame = null; - this.canvasSwirlStartedAt = 0; - this.canvasSwirlLastFrameAt = 0; - this.nextCanvasDrawMode = "full"; - this.canvasInteractionUntil = 0; - this.lastCanvasFrameAt = 0; - this.adaptiveInitialized = false; - this.lastAutoTuneAt = 0; - this.autoTuneCount = 0; - this.lastRenderMs = 0; - this.pendingRenderOptions = null; - this.viewportStateByKey = new Map(); - this.currentViewportStateKey = null; - } - - getViewType() { - return VIEW_TYPE_MINI_WORLD_MAP; - } - - getDisplayText() { - return "Mini World Map"; - } - - getIcon() { - return "network"; - } - - async onOpen() { - this.contentEl.empty(); - this.contentEl.addClass("mini-world-map-view"); - this.syncColorSchemeClass(); - this.renderShell(); - if (!this.plugin.index.ready) await this.plugin.rebuildIndex("open"); - this.refresh(); - } - - async onClose() { - if (this.resizeCleanup) this.resizeCleanup(); - if (this.renderTimer) window.clearTimeout(this.renderTimer); - if (this.canvasAnimationFrame) window.cancelAnimationFrame(this.canvasAnimationFrame); - if (this.canvasSettleTimer) window.clearTimeout(this.canvasSettleTimer); - this.cancelCanvasPanMomentum(); - this.cancelCanvasZoomAnimation(); - this.cancelCanvasSpringBack(); - this.cancelCanvasSwirlAnimation(); - this.destroyPixiRenderer(); - this.contentEl.empty(); - } - - refresh(options = {}) { - if (!this.containerEl || !this.graphHost) return; - this.render(options); - } - - renderShell() { - this.contentEl.empty(); - - this.metaEl = this.contentEl.createDiv({ cls: "mwm-meta" }); - - const body = this.contentEl.createDiv({ cls: "mwm-body" }); - this.bodyEl = body; - this.bodyEl.style.setProperty("--mwm-side-width", `${this.state.sidePanelWidth}px`); - this.graphHost = body.createDiv({ cls: "mwm-graph-host" }); - this.graphHost.addEventListener("wheel", evt => { - evt.preventDefault(); - this.zoomAtClientPoint(evt.clientX, evt.clientY, evt.deltaY, evt.deltaMode); - }, { passive: false }); - this.splitter = body.createDiv({ - cls: "mwm-splitter", - attr: { role: "separator", title: "Drag to resize right panel" } - }); - this.installPanelResize(); - this.sidePanel = body.createDiv({ cls: "mwm-side-panel" }); - - if (typeof ResizeObserver !== "undefined") { - const observer = new ResizeObserver(() => this.requestCanvasDraw("full")); - observer.observe(this.graphHost); - this.resizeCleanup = () => observer.disconnect(); - } - } - - createIconButton(parent, icon, title, onClick, extraClass) { - const button = parent.createEl("button", { - cls: extraClass ? `mwm-icon-button ${extraClass}` : "mwm-icon-button", - attr: { type: "button", "aria-label": title, title } - }); - setIcon(button, icon); - button.addEventListener("click", onClick); - return button; - } - - sidePanelPages() { - return [ - ["inspect", "Inspect", "info"], - ["pins", "Pins", "pin"], - ["view", "View", "navigation"], - ["controls", "Controls", "sliders-horizontal"], - ["defaults", "Defaults", "settings"] - ]; - } - - clearControlRefs() { - this.searchInput = null; - this.depthInput = null; - this.linkInput = null; - this.nodeInput = null; - this.autoToggle = null; - this.linkHoverSelect = null; - this.externalToggle = null; - this.externalModeSelect = null; - this.externalLimitInput = null; - this.zoomInput = null; - this.zoomLabel = null; - this.columnInput = null; - this.rowInput = null; - this.labelVisibilitySelect = null; - this.swirlInput = null; - this.swirlLabel = null; - this.pinGroupInput = null; - this.panelControlRefs = new Map(); - } - - capturePanelFocus() { - const doc = this.contentEl?.ownerDocument; - const active = doc?.activeElement; - if (!active || !this.sidePanel || !this.sidePanel.contains(active)) return null; - const key = active.getAttribute("data-mwm-control"); - if (!key) return null; - return { - key, - start: typeof active.selectionStart === "number" ? active.selectionStart : null, - end: typeof active.selectionEnd === "number" ? active.selectionEnd : null - }; - } - - restorePanelFocus(focusState) { - if (!focusState || !this.panelControlRefs) return; - const element = this.panelControlRefs.get(focusState.key); - if (!element) return; - element.focus({ preventScroll: true }); - if (focusState.start !== null && typeof element.setSelectionRange === "function") { - element.setSelectionRange(focusState.start, focusState.end ?? focusState.start); - } - } - - registerPanelControl(key, element) { - if (!element) return element; - element.setAttribute("data-mwm-control", key); - if (!this.panelControlRefs) this.panelControlRefs = new Map(); - this.panelControlRefs.set(key, element); - return element; - } - - getSidePanelTarget() { - return this.sidePanelContent || this.sidePanel; - } - - createPanelPageTitle(parent, title, desc) { - parent.createDiv({ cls: "mwm-panel-page-title", text: title }); - if (desc) parent.createDiv({ cls: "mwm-panel-page-desc", text: desc }); - } - - createPanelSection(parent, title) { - const section = parent.createDiv({ cls: "mwm-panel-section" }); - if (title) section.createDiv({ cls: "mwm-side-heading", text: title }); - return section; - } - - createPanelAction(parent, icon, label, onClick, active = false) { - const button = parent.createEl("button", { - cls: active ? "mwm-panel-action is-active" : "mwm-panel-action", - attr: { type: "button", title: label } - }); - setIcon(button, icon); - button.createSpan({ text: label }); - button.addEventListener("click", onClick); - return button; - } - - createPanelText(parent, key, label, value, placeholder, onInput) { - const field = parent.createEl("label", { cls: "mwm-panel-field" }); - field.createSpan({ cls: "mwm-panel-label", text: label }); - const input = field.createEl("input", { - attr: { type: "search", placeholder, value } - }); - this.registerPanelControl(key, input); - input.addEventListener("input", () => onInput(input.value, input)); - return input; - } - - createPanelNumber(parent, key, label, value, attr, onChange) { - const field = parent.createEl("label", { cls: "mwm-panel-field" }); - field.createSpan({ cls: "mwm-panel-label", text: label }); - const input = field.createEl("input", { - attr: Object.assign({ type: "number", value: String(value) }, attr || {}) - }); - this.registerPanelControl(key, input); - let commitTimer = null; - const commit = () => { - if (commitTimer) { - window.clearTimeout(commitTimer); - commitTimer = null; - } - const raw = String(input.value || "").trim(); - if (!raw || raw === "-" || raw === ".") return; - onChange(input.value, input); - }; - input.addEventListener("input", () => { - if (commitTimer) window.clearTimeout(commitTimer); - commitTimer = window.setTimeout(commit, 520); - }); - input.addEventListener("change", commit); - input.addEventListener("keydown", evt => { - if (evt.key === "Enter") commit(); - }); - return input; - } - - createPanelRange(parent, key, label, value, attr, onInput) { - const field = parent.createEl("label", { cls: "mwm-panel-field mwm-panel-range-field" }); - const header = field.createDiv({ cls: "mwm-panel-field-header" }); - header.createSpan({ cls: "mwm-panel-label", text: label }); - const valueEl = header.createSpan({ cls: "mwm-value", text: `${Math.round(this.state.zoom * 100)}%` }); - const input = field.createEl("input", { - attr: Object.assign({ type: "range", value: String(value) }, attr || {}) - }); - this.registerPanelControl(key, input); - input.addEventListener("input", () => onInput(input.value, input, valueEl)); - return { input, valueEl }; - } - - createPanelToggle(parent, key, label, value, onChange) { - const field = parent.createEl("label", { cls: "mwm-panel-toggle" }); - const input = field.createEl("input", { attr: { type: "checkbox" } }); - input.checked = Boolean(value); - this.registerPanelControl(key, input); - field.createSpan({ text: label }); - input.addEventListener("change", () => onChange(input.checked, input)); - return input; - } - - createPanelSelect(parent, key, label, value, options, onChange) { - const field = parent.createEl("label", { cls: "mwm-panel-field" }); - field.createSpan({ cls: "mwm-panel-label", text: label }); - const select = field.createEl("select", { cls: "mwm-select" }); - for (const [optionValue, optionLabel] of options) { - select.createEl("option", { attr: { value: optionValue }, text: optionLabel }); - } - select.value = value; - this.registerPanelControl(key, select); - select.addEventListener("change", () => onChange(select.value, select)); - return select; - } - - createPanelTextArea(parent, key, label, value, onChange) { - const field = parent.createEl("label", { cls: "mwm-panel-field" }); - field.createSpan({ cls: "mwm-panel-label", text: label }); - const textArea = field.createEl("textarea"); - textArea.value = value; - this.registerPanelControl(key, textArea); - textArea.addEventListener("change", () => onChange(textArea.value, textArea)); - return textArea; - } - - needsCanvasHoverLinks() { - return hoverHighlightsNoteLinks(this.state.hoverHighlightMode) || this.pinnedPathsNeedHoverLinks(); - } - - pinnedPathsNeedHoverLinks() { - return this.pinnedPaths.some(pin => - pin.kind === "link" || (pin.kind === "node" && hoverHighlightsNoteLinks(pin.mode)) - ); - } - - currentHoverPinCandidate(index, graph) { - if (!index || !graph) return null; - if (this.hoverNodeId !== null && this.hoverNodeId !== undefined) { - const node = graphNode(index, graph, this.hoverNodeId); - return node ? this.createNodePinCandidate(node, this.state.hoverHighlightMode) : null; - } - if (this.hoverLink) return this.createLinkPinCandidate(index, graph, this.hoverLink); - return null; - } - - currentSelectedPinCandidate(index, graph) { - if (!index || !graph) return null; - if (this.selectedLink) return this.createLinkPinCandidate(index, graph, this.selectedLink); - if (this.selectedNodeId !== null && this.selectedNodeId !== undefined) { - const node = graphNode(index, graph, this.selectedNodeId); - return node ? this.createNodePinCandidate(node, this.state.hoverHighlightMode) : null; - } - return null; - } - - createNodePinCandidate(node, mode) { - return { - kind: "node", - nodeId: node.id, - mode: normalizeHoverHighlightMode(mode), - title: node.title || node.id || ROOT_TITLE, - path: node.path || "/", - nodeType: node.type - }; - } - - createLinkPinCandidate(index, graph, edge) { - const source = graphNode(index, graph, edge.source); - const target = graphNode(index, graph, edge.target); - const sourceTitle = source ? source.title : edge.source; - const targetTitle = target ? target.title : edge.target; - const sourcePath = source ? source.path : edge.source; - const targetPath = target ? target.path : edge.target; - return { - kind: "link", - edgeId: edge.id || `${edge.source}->${edge.target}`, - source: edge.source, - target: edge.target, - title: `${sourceTitle} -> ${targetTitle}`, - path: `${sourcePath || "/"} -> ${targetPath || "/"}`, - mode: "note-links" - }; - } - - pinCandidateKey(candidate) { - if (!candidate) return ""; - if (candidate.kind === "node") return `node:${candidate.nodeId}:${normalizeHoverHighlightMode(candidate.mode)}`; - return `link:${candidate.edgeId || `${candidate.source}->${candidate.target}`}`; - } - - addPinnedPath(candidate) { - const key = this.pinCandidateKey(candidate); - if (!key) return null; - const existing = this.pinnedPaths.find(pin => pin.key === key); - if (existing) { - this.selectedPinIds.add(existing.id); - new Notice("That path is already pinned."); - return existing; - } - - const pin = Object.assign({}, candidate, { - id: `pin-${this.nextPinId++}`, - key, - groupId: null, - createdAt: Date.now() - }); - this.pinnedPaths.push(pin); - this.selectedPinIds.add(pin.id); - return pin; - } - - pinCurrentHoverPath() { - const { index, graph } = this.lastCanvasBundle || {}; - if (!index || !graph) return; - const previousNeedsHoverLinks = this.needsCanvasHoverLinks(); - const freshCandidate = this.currentHoverPinCandidate(index, graph); - const selectedCandidate = this.currentSelectedPinCandidate(index, graph); - const pin = this.addPinnedPath(selectedCandidate || freshCandidate); - if (!pin) { - new Notice("Select or hover a path before pinning."); - return; - } - if (previousNeedsHoverLinks !== this.needsCanvasHoverLinks()) this.renderMapOnly(); - else this.applyPersistentHighlight(); - if (!this.state.fullscreen && this.state.sidePage === "pins" && this.lastCanvasBundle) { - this.renderSidePanel(this.lastCanvasBundle.index, this.lastCanvasBundle.graph); - } - } - - removePinnedPath(pinId) { - const previousNeedsHoverLinks = this.needsCanvasHoverLinks(); - this.pinnedPaths = this.pinnedPaths.filter(pin => pin.id !== pinId); - this.selectedPinIds.delete(pinId); - this.dropEmptyPinGroups(); - if (previousNeedsHoverLinks !== this.needsCanvasHoverLinks()) this.renderMapOnly(); - else this.requestCanvasDraw("full"); - if (!this.state.fullscreen && this.state.sidePage === "pins" && this.lastCanvasBundle) { - this.renderSidePanel(this.lastCanvasBundle.index, this.lastCanvasBundle.graph); - } - } - - clearPinnedPaths() { - const previousNeedsHoverLinks = this.needsCanvasHoverLinks(); - this.pinnedPaths = []; - this.pinGroups = []; - this.selectedPinIds.clear(); - if (previousNeedsHoverLinks !== this.needsCanvasHoverLinks()) this.renderMapOnly(); - else this.requestCanvasDraw("full"); - if (!this.state.fullscreen && this.state.sidePage === "pins" && this.lastCanvasBundle) { - this.renderSidePanel(this.lastCanvasBundle.index, this.lastCanvasBundle.graph); - } - } - - groupSelectedPinnedPaths() { - const selectedPins = this.pinnedPaths.filter(pin => this.selectedPinIds.has(pin.id)); - if (!selectedPins.length) { - new Notice("Select pinned paths to group."); - return; - } - - const groupId = `pin-group-${this.nextPinGroupId++}`; - const name = this.pinGroupName.trim() || `Group ${this.pinGroups.length + 1}`; - this.pinGroups.push({ id: groupId, name }); - for (const pin of selectedPins) pin.groupId = groupId; - this.dropEmptyPinGroups(); - this.pinGroupName = ""; - this.selectedPinIds.clear(); - - if (!this.state.fullscreen && this.lastCanvasBundle) { - this.renderSidePanel(this.lastCanvasBundle.index, this.lastCanvasBundle.graph); - } - } - - removePinGroup(groupId) { - this.pinGroups = this.pinGroups.filter(group => group.id !== groupId); - for (const pin of this.pinnedPaths) { - if (pin.groupId === groupId) pin.groupId = null; - } - if (!this.state.fullscreen && this.lastCanvasBundle) { - this.renderSidePanel(this.lastCanvasBundle.index, this.lastCanvasBundle.graph); - } - } - - ungroupPinnedPath(pinId) { - const pin = this.pinnedPaths.find(item => item.id === pinId); - if (!pin) return; - pin.groupId = null; - this.dropEmptyPinGroups(); - if (!this.state.fullscreen && this.lastCanvasBundle) { - this.renderSidePanel(this.lastCanvasBundle.index, this.lastCanvasBundle.graph); - } - } - - dropEmptyPinGroups() { - const usedGroups = new Set(this.pinnedPaths.map(pin => pin.groupId).filter(Boolean)); - this.pinGroups = this.pinGroups.filter(group => usedGroups.has(group.id)); - } - - findGraphEdgeForPin(pin, graph) { - if (!pin || pin.kind !== "link" || !graph) return null; - const edges = [ - ...(graph.linkEdges || []), - ...(graph.hoverLinkEdges || []) - ]; - return edges.find(edge => - (pin.edgeId && edge.id === pin.edgeId) - || (edge.source === pin.source && edge.target === pin.target) - ) || null; - } - - findCanvasEdgeForPin(pin) { - if (!pin || pin.kind !== "link" || !this.canvasData) return null; - if (pin.edgeId && this.canvasData.edgesById.has(pin.edgeId)) { - return this.canvasData.edgesById.get(pin.edgeId); - } - const collections = [ - ...(this.canvasData.links || []), - ...(this.canvasData.hoverLinks || []), - ...(this.canvasData.hierarchy || []) - ]; - return collections.find(item => - item && item.source === pin.source && item.target === pin.target - ) || null; - } - - centerPinnedPath(pin) { - if (!pin || !this.canvas || !this.canvasData) return; - if (pin.kind === "node") { - const nodeId = this.plugin.index.visualNodeId(pin.nodeId); - const item = this.canvasData.nodesById.get(nodeId); - if (item) this.centerCanvasOnPoint(item.point); - return; - } - - const edge = this.findCanvasEdgeForPin(pin); - if (!edge || !edge.sourcePoint || !edge.targetPoint) return; - this.centerCanvasOnPoint({ - x: (edge.sourcePoint.x + edge.targetPoint.x) / 2, - y: (edge.sourcePoint.y + edge.targetPoint.y) / 2 - }); - } - - centerCanvasOnPoint(point) { - if (!point || !this.canvas) return; - const viewport = this.canvasViewport(); - const zoom = clampFloat(this.state.zoom, this.canvasMinZoom(viewport), this.canvasMaxZoom(viewport), 1); - this.canvasPanX = viewport.width / 2 - point.x * zoom; - this.canvasPanY = viewport.height / 2 - point.y * zoom; - this.saveViewportState(); - this.requestCanvasDraw("full"); - } - - inspectPinnedPath(pin, index, graph) { - if (!pin) return; - if (pin.kind === "node") { - this.state.sidePage = "inspect"; - this.selectedNodeId = this.plugin.index.visualNodeId(pin.nodeId); - this.selectedLink = null; - this.applyPersistentHighlight(); - if (!this.state.fullscreen) this.renderSidePanel(index, graph, this.selectedNodeId); - return; - } - - this.state.sidePage = "inspect"; - this.selectedLink = this.findGraphEdgeForPin(pin, graph) || null; - this.selectedNodeId = null; - this.applyPersistentHighlight(); - if (!this.state.fullscreen) this.renderSidePanel(index, graph); - } - - pinnedCanvasLabelIds() { - if (!this.canvasData) return []; - const ids = new Set(); - for (const pin of this.pinnedPaths) { - if (pin.kind === "node") { - const nodeId = this.plugin.index.visualNodeId(pin.nodeId); - if (this.canvasData.nodesById.has(nodeId)) ids.add(nodeId); - continue; - } - const edge = this.findCanvasEdgeForPin(pin); - if (!edge) continue; - if (edge.source !== null && edge.source !== undefined && this.canvasData.nodesById.has(edge.source)) ids.add(edge.source); - if (edge.target !== null && edge.target !== undefined && this.canvasData.nodesById.has(edge.target)) ids.add(edge.target); - } - return Array.from(ids); - } - - render(options = {}) { - this.syncColorSchemeClass(); - const preservePanel = Boolean(options.preservePanel); - const index = this.plugin.index; - if (!index.ready) { - this.graphHost.empty(); - this.graphHost.createDiv({ cls: "mwm-empty", text: "Building map..." }); - return; - } - - if (this.state.autoDetail && !this.adaptiveInitialized) { - this.applyInitialAdaptiveDefaults(index); - } - - if (this.searchInput && this.searchInput.value !== this.state.search) { - this.searchInput.value = this.state.search; - } - if (this.depthInput) this.depthInput.value = String(this.state.atlasDepth); - if (this.linkInput) this.linkInput.value = String(this.state.linkLimit); - if (this.nodeInput) this.nodeInput.value = String(this.state.nodeLimit); - if (this.autoToggle) this.autoToggle.checked = this.state.autoDetail; - if (this.linkHoverSelect) this.linkHoverSelect.value = normalizeHoverHighlightMode(this.state.hoverHighlightMode); - if (this.externalToggle) this.externalToggle.checked = this.state.showExternalLinks; - if (this.externalModeSelect) this.externalModeSelect.value = this.state.externalDetailMode; - if (this.externalLimitInput) this.externalLimitInput.value = String(this.state.externalLinkAnchorLimit); - this.syncZoomControls(); - if (this.columnInput) this.columnInput.value = String(this.state.columnSpacing); - if (this.rowInput) this.rowInput.value = String(this.state.rowSpacing); - if (this.labelVisibilitySelect) this.labelVisibilitySelect.value = normalizeLabelVisibility(this.state.labelVisibility); - if (this.swirlInput) { - this.swirlInput.value = String(this.state.swirlStrength); - if (this.swirlLabel) this.swirlLabel.textContent = `${Math.round(this.state.swirlStrength)}%`; - } - if (this.bodyEl) this.bodyEl.style.setProperty("--mwm-side-width", `${this.state.sidePanelWidth}px`); - if (this.contentEl) this.contentEl.classList.toggle("is-fullscreen", this.state.fullscreen); - - const started = performance.now(); - const graphState = Object.assign({}, this.state, { - selectedNodeId: this.selectedNodeId, - selectedLink: this.selectedLink, - pinNeedsHoverLinks: this.pinnedPathsNeedHoverLinks() - }); - const graph = index.buildVisibleGraph(graphState); - if (this.applyPreLayoutPressureGuard(graph)) return; - const layout = layoutVisibleGraph(index, graph, this.state); - this.lastLayout = layout; - this.positions = layout.positions; - - this.renderGraph(index, graph, layout); - if (!this.state.fullscreen && !preservePanel) this.renderSidePanel(index, graph); - this.lastRenderMs = Math.round(performance.now() - started); - this.renderMeta(index, graph, layout); - this.maybeAutoTune(index, graph, this.lastRenderMs); - } - - renderMeta(index, graph, layout) { - this.metaEl.empty(); - const rootNode = index.nodes.get(graph.rootId); - const activeNode = graph.focusId ? index.nodes.get(graph.focusId) : null; - const chips = [ - `${this.state.mode}`, - `${graph.nodes.length} nodes`, - `${graph.linkEdges.length} link overlays`, - `${graph.externalFileCount || 0} outside files`, - `${graph.externalGroupCount || 0} outside groups`, - `outside: ${this.state.externalDetailMode}`, - `${index.stats.notes} notes`, - `${index.stats.folders} folders`, - `${index.stats.scannedMarkdown} scanned md`, - `${this.lastRenderMs || 0} ms` - ]; - - if (this.state.autoDetail) chips.push("auto"); - if (this.state.showCompleteRoot) chips.push("complete root"); - if (rootNode && this.state.mode === "atlas" && rootNode.id !== ROOT_ID) { - chips.push(`root: ${rootNode.title}`); - } - if (activeNode && this.state.mode === "focus") { - chips.push(`focus: ${activeNode.title}`); - } - if (this.state.search) chips.push(`search: ${this.state.search}`); - if (normalizeHoverHighlightMode(this.state.hoverHighlightMode) !== "none") { - chips.push(`hover: ${hoverHighlightModeLabel(this.state.hoverHighlightMode)}`); - } - if (graph.hiddenNodeCount) chips.push(`${graph.hiddenNodeCount} hidden by node limit`); - - for (const chip of chips) { - this.metaEl.createSpan({ cls: "mwm-chip", text: chip }); - } - - if (layout.trimmed) { - this.metaEl.createSpan({ cls: "mwm-chip mwm-chip-warn", text: "large view" }); - } - } - - renderMapOnly() { - this.render({ preservePanel: true }); - } - - scheduleRender(delay = 80, options = {}) { - if (this.renderTimer) window.clearTimeout(this.renderTimer); - this.pendingRenderOptions = Object.assign({}, this.pendingRenderOptions || {}, options || {}); - this.renderTimer = window.setTimeout(() => { - this.renderTimer = null; - const renderOptions = this.pendingRenderOptions || {}; - this.pendingRenderOptions = null; - this.render(renderOptions); - }, delay); - } - - disableAutoDetail() { - if (!this.state.autoDetail) return; - this.state.autoDetail = false; - if (this.autoToggle) this.autoToggle.checked = false; - } - - resetAdaptiveTuning() { - if (!this.state.autoDetail) return; - this.adaptiveInitialized = false; - this.autoTuneCount = 0; - } - - toggleFullscreen() { - this.state.fullscreen = !this.state.fullscreen; - this.render(); - } - - resetToVaultRoot() { - this.state.rootPath = ROOT_ID; - this.state.mode = "atlas"; - this.state.showCompleteRoot = false; - this.viewInitialized = false; - this.resetAdaptiveTuning(); - this.render(); - } - - syncColorSchemeClass() { - if (!this.contentEl) return; - const scheme = normalizeColorScheme(this.state.colorScheme); - this.state.colorScheme = scheme; - this.contentEl.classList.toggle("is-day-scheme", scheme === "day"); - this.contentEl.classList.toggle("is-night-scheme", scheme === "night"); - this.contentEl.setAttribute("data-mwm-color-scheme", scheme); - } - - setColorScheme(scheme, persist = true) { - const next = normalizeColorScheme(scheme); - this.state.colorScheme = next; - this.plugin.settings.colorScheme = next; - this.syncColorSchemeClass(); - this.canvasPalette = this.readCanvasPalette(); - this.requestCanvasDraw("full"); - if (persist) { - for (const leaf of this.plugin.app.workspace.getLeavesOfType(VIEW_TYPE_MINI_WORLD_MAP)) { - const view = leaf.view; - if (view && view !== this && typeof view.setColorScheme === "function") { - view.setColorScheme(next, false); - } - } - void this.plugin.saveSettings({ rebuild: false }); - } - } - - cycleColorScheme() { - this.setColorScheme(nextColorScheme(this.state.colorScheme)); - } - - applyPluginSettings(keys, options = {}) { - const settingKeys = new Set(Array.isArray(keys) ? keys : [keys].filter(Boolean)); - if (!settingKeys.size) return; - - const settings = this.plugin.settings; - const previousNeedsHoverLinks = this.needsCanvasHoverLinks(); - const previousSwirl = this.state.swirlStrength; - let needsRender = false; - let needsDraw = false; - let needsPanel = false; - - const applyBudgetSetting = () => { - this.disableAutoDetail(); - this.disableCompleteRoot(); - needsRender = true; - }; - const applyStructuralSetting = () => { - this.disableCompleteRoot(); - needsRender = true; - }; - - if (settingKeys.has("atlasDepth")) { - this.state.atlasDepth = clampNumber(settings.atlasDepth, 1, MAX_ATLAS_DEPTH, DEFAULT_SETTINGS.atlasDepth); - applyBudgetSetting(); - } - if (settingKeys.has("renderNodeLimit")) { - this.state.nodeLimit = clampNumber(settings.renderNodeLimit, 200, MAX_RENDER_NODE_LIMIT, DEFAULT_SETTINGS.renderNodeLimit); - applyBudgetSetting(); - } - if (settingKeys.has("linkLimit")) { - this.state.linkLimit = clampNumber(settings.linkLimit, 0, MAX_LINK_LIMIT, DEFAULT_SETTINGS.linkLimit); - applyBudgetSetting(); - } - if (settingKeys.has("externalLinkAnchorLimit")) { - this.state.externalLinkAnchorLimit = clampNumber( - settings.externalLinkAnchorLimit, - 0, - MAX_EXTERNAL_LINK_ANCHOR_LIMIT, - DEFAULT_SETTINGS.externalLinkAnchorLimit - ); - applyBudgetSetting(); - } - if (settingKeys.has("adaptiveDetail")) { - this.state.autoDetail = Boolean(settings.adaptiveDetail); - if (this.state.autoDetail) this.disableCompleteRoot(); - this.adaptiveInitialized = false; - this.autoTuneCount = 0; - needsRender = true; - } - if (settingKeys.has("hoverHighlightMode")) { - this.state.hoverHighlightMode = normalizeHoverHighlightMode(settings.hoverHighlightMode); - this.state.enableLinkHover = hoverHighlightsNoteLinks(this.state.hoverHighlightMode); - this.hoverLink = null; - if (previousNeedsHoverLinks !== this.needsCanvasHoverLinks()) needsRender = true; - else needsDraw = true; - } - if (settingKeys.has("showExternalLinks")) { - this.state.showExternalLinks = Boolean(settings.showExternalLinks); - applyStructuralSetting(); - } - if (settingKeys.has("externalDetailMode")) { - this.state.externalDetailMode = settings.externalDetailMode || DEFAULT_SETTINGS.externalDetailMode; - this.resetAdaptiveTuning(); - applyStructuralSetting(); - } - if (settingKeys.has("labelVisibility")) { - this.state.labelVisibility = normalizeLabelVisibility(settings.labelVisibility); - needsDraw = true; - } - if (settingKeys.has("swirlStrength")) { - this.state.swirlStrength = clampNumber(settings.swirlStrength, 0, MAX_SWIRL_STRENGTH, DEFAULT_SWIRL_STRENGTH); - if ((previousSwirl > 0) !== (this.state.swirlStrength > 0)) { - if (this.state.swirlStrength > 0) this.startCanvasSwirlAnimation(); - else { - this.cancelCanvasSwirlAnimation(); - this.resetCanvasSwirlPositions(); - } - needsRender = true; - } else { - needsDraw = true; - } - } - if (settingKeys.has("colorScheme")) { - this.state.colorScheme = normalizeColorScheme(settings.colorScheme); - this.syncColorSchemeClass(); - this.canvasPalette = this.readCanvasPalette(); - needsDraw = true; - } - if (settingKeys.has("hiddenLegendItems")) { - this.state.hiddenLegendItems = Array.isArray(settings.hiddenLegendItems) - ? settings.hiddenLegendItems.slice() - : DEFAULT_SETTINGS.hiddenLegendItems.slice(); - needsRender = true; - } - if (settingKeys.has("includeUnresolvedLinks") || settingKeys.has("ignoreFolders")) { - needsRender = true; - this.viewInitialized = false; - } - - if (options.resetViewport) this.viewInitialized = false; - if (options.render === false) return; - - if (needsRender) { - this.render({ preservePanel: options.preservePanel === true }); - return; - } - - if (needsDraw) this.requestCanvasDraw("full"); - if ((needsDraw || needsPanel) && options.preservePanel !== true && !this.state.fullscreen && this.lastCanvasBundle) { - this.renderSidePanel(this.lastCanvasBundle.index, this.lastCanvasBundle.graph); - } - } - - disableCompleteRoot() { - if (!this.state.showCompleteRoot) return; - this.state.showCompleteRoot = false; - this.preCompleteState = null; - } - - exitCompleteRoot() { - if (!this.state.showCompleteRoot) return; - const previous = this.preCompleteState; - this.state.showCompleteRoot = false; - this.preCompleteState = null; - if (previous) { - Object.assign(this.state, previous); - } - } - - showCompleteCurrentRoot() { - const index = this.plugin.index; - if (!index.ready) return; - - const currentGraphRoot = this.lastCanvasBundle?.graph?.rootId; - const rootId = currentGraphRoot !== null && currentGraphRoot !== undefined && index.nodes.has(currentGraphRoot) - ? currentGraphRoot - : this.state.mode === "atlas" && index.nodes.has(this.state.rootPath) - ? this.state.rootPath - : ROOT_ID; - const profile = this.completeRootProfile(index, rootId); - - this.preCompleteState = { - mode: this.state.mode, - rootPath: this.state.rootPath, - atlasDepth: this.state.atlasDepth, - nodeLimit: this.state.nodeLimit, - linkLimit: this.state.linkLimit, - externalLinkAnchorLimit: this.state.externalLinkAnchorLimit, - autoDetail: this.state.autoDetail, - showExternalLinks: this.state.showExternalLinks, - externalDetailMode: this.state.externalDetailMode - }; - this.state.mode = "atlas"; - this.state.rootPath = rootId; - this.state.showCompleteRoot = true; - this.state.autoDetail = false; - this.state.showExternalLinks = true; - this.state.externalDetailMode = "exact"; - this.state.atlasDepth = profile.depth; - this.state.nodeLimit = profile.nodeLimit; - this.state.linkLimit = profile.linkLimit; - this.state.externalLinkAnchorLimit = profile.externalLimit; - this.viewInitialized = false; - this.adaptiveInitialized = true; - this.render(); - } - - completeRootProfile(index, rootId) { - const root = index.nodes.get(rootId) || index.nodes.get(ROOT_ID); - const rootDepth = root ? root.depth || 0 : 0; - const insideIds = new Set(); - let maxRelDepth = 1; - - for (const node of index.nodes.values()) { - if (!nodeWithinRoot(index, node, rootId)) continue; - insideIds.add(node.id); - maxRelDepth = Math.max(maxRelDepth, Math.max(0, (node.depth || 0) - rootDepth)); - } - - let linkCount = 0; - const exactExternalIds = new Set(); - for (const edge of index.linkEdges) { - const sourceInside = insideIds.has(edge.source); - const targetInside = insideIds.has(edge.target); - if (!sourceInside && !targetInside) continue; - linkCount += 1; - if (sourceInside !== targetInside) exactExternalIds.add(sourceInside ? edge.target : edge.source); - } - - return { - depth: clampNumber(maxRelDepth, 1, MAX_ATLAS_DEPTH, DEFAULT_SETTINGS.atlasDepth), - nodeLimit: clampNumber(insideIds.size + exactExternalIds.size + 64, 200, MAX_RENDER_NODE_LIMIT, DEFAULT_SETTINGS.renderNodeLimit), - linkLimit: clampNumber(linkCount + 64, 0, MAX_LINK_LIMIT, DEFAULT_SETTINGS.linkLimit), - externalLimit: clampNumber(exactExternalIds.size + 64, 0, MAX_EXTERNAL_LINK_ANCHOR_LIMIT, DEFAULT_SETTINGS.externalLinkAnchorLimit) - }; - } - - shouldRerenderForSelection() { - return false; - } - - clearSelection(index, graph) { - const hadSelection = this.selectedNodeId !== null && this.selectedNodeId !== undefined || Boolean(this.selectedLink); - this.selectedNodeId = null; - this.selectedLink = null; - this.hoverNodeId = null; - this.hoverLink = null; - - if (hadSelection && this.shouldRerenderForSelection()) { - this.render(); - return; - } - - this.clearHoverClasses(); - if (this.graphHost) this.graphHost.classList.remove("is-hovering"); - if (this.graphHost) this.graphHost.classList.remove("is-pointing"); - this.requestCanvasDraw("full"); - if (!this.state.fullscreen) this.renderSidePanel(index, graph); - } - - applyInitialAdaptiveDefaults(index) { - const root = index.nodes.get(this.state.mode === "atlas" ? this.state.rootPath : ROOT_ID); - const localNotes = root ? (root.noteCount || root.descendantCount || index.stats.notes) : index.stats.notes; - const visiblePressure = Math.max(1, Math.min(index.stats.notes || 1, localNotes || 1)); - - if (visiblePressure < 1500) { - this.state.atlasDepth = Math.max(this.state.atlasDepth, 8); - this.state.nodeLimit = Math.max(this.state.nodeLimit, 6200); - this.state.linkLimit = Math.max(this.state.linkLimit, 2200); - this.state.externalLinkAnchorLimit = Math.max(this.state.externalLinkAnchorLimit, 900); - } else if (visiblePressure < 4200) { - this.state.atlasDepth = Math.max(this.state.atlasDepth, 7); - this.state.nodeLimit = Math.max(this.state.nodeLimit, 5200); - this.state.linkLimit = Math.max(this.state.linkLimit, 1700); - this.state.externalLinkAnchorLimit = Math.max(this.state.externalLinkAnchorLimit, 750); - } else if (visiblePressure < 9000) { - this.state.atlasDepth = Math.max(this.state.atlasDepth, 5); - this.state.nodeLimit = Math.max(this.state.nodeLimit, 3600); - this.state.linkLimit = Math.max(this.state.linkLimit, 1000); - this.state.externalLinkAnchorLimit = Math.max(this.state.externalLinkAnchorLimit, 500); - } else { - this.state.atlasDepth = Math.max(4, Math.min(this.state.atlasDepth, 6)); - this.state.nodeLimit = Math.max(this.state.nodeLimit, 2600); - this.state.linkLimit = Math.max(this.state.linkLimit, 700); - this.state.externalLinkAnchorLimit = Math.max(this.state.externalLinkAnchorLimit, 300); - } - - this.adaptiveInitialized = true; - } - - applyPreLayoutPressureGuard(graph) { - if (!this.state.autoDetail || this.state.search || this.state.showCompleteRoot) return false; - - const pressure = graph.nodes.length - + graph.linkEdges.length * 1.7 - + (graph.externalFileCount || 0) * 5 - + (graph.externalGroupCount || 0) * 1.4 - + (graph.hiddenNodeCount || 0) * 0.15; - - if (pressure < 9000 && graph.externalFileCount < 520) return false; - - let changed = false; - if (this.state.externalLinkAnchorLimit > 260) { - this.state.externalLinkAnchorLimit = Math.max(220, Math.floor(this.state.externalLinkAnchorLimit * 0.72)); - changed = true; - } - if (this.state.linkLimit > 650) { - this.state.linkLimit = Math.max(560, Math.floor(this.state.linkLimit * 0.74)); - changed = true; - } - if (this.state.nodeLimit > 2400) { - this.state.nodeLimit = Math.max(2200, Math.floor(this.state.nodeLimit * 0.78)); - changed = true; - } - if (pressure > 15000 && this.state.atlasDepth > 3) { - this.state.atlasDepth -= 1; - changed = true; - } - - if (changed) { - this.autoTuneCount += 1; - this.lastAutoTuneAt = performance.now(); - this.graphHost.empty(); - this.graphHost.createDiv({ cls: "mwm-empty", text: "Reducing map detail..." }); - this.scheduleRender(60); - return true; - } - return false; - } - - maybeAutoTune(index, graph, elapsedMs) { - if (!this.state.autoDetail || this.state.search || this.state.showCompleteRoot) return; - if (this.autoTuneCount >= 9) return; - - const now = performance.now(); - if (now - this.lastAutoTuneAt < 900) return; - - const pressure = graph.nodes.length - + graph.linkEdges.length * 1.5 - + (graph.externalFileCount || 0) * 4; - const tooHeavy = elapsedMs > 480 || pressure > 9000 || graph.nodes.length > this.state.nodeLimit * 1.08; - const veryLight = elapsedMs > 0 - && elapsedMs < 70 - && graph.hiddenNodeCount === 0 - && graph.externalFileCount < 80 - && graph.nodes.length > Math.max(80, this.state.nodeLimit * 0.35); - - let changed = false; - if (tooHeavy) { - this.state.linkLimit = Math.max(520, Math.floor(this.state.linkLimit * 0.76)); - this.state.nodeLimit = Math.max(2100, Math.floor(this.state.nodeLimit * 0.8)); - this.state.externalLinkAnchorLimit = Math.max(220, Math.floor(this.state.externalLinkAnchorLimit * 0.74)); - if ((elapsedMs > 850 || pressure > 15000) && this.state.atlasDepth > 3) this.state.atlasDepth -= 1; - changed = true; - } else if (veryLight) { - const maxDepth = Math.min(MAX_ATLAS_DEPTH, index.stats.maxDepth || MAX_ATLAS_DEPTH); - if (this.state.atlasDepth < maxDepth) { - this.state.atlasDepth += 1; - changed = true; - } - if (this.state.nodeLimit < 12000) { - this.state.nodeLimit = Math.min(12000, this.state.nodeLimit + 700); - changed = true; - } - if (this.state.linkLimit < 6000) { - this.state.linkLimit = Math.min(6000, this.state.linkLimit + 260); - changed = true; - } - } - - if (changed) { - this.autoTuneCount += 1; - this.lastAutoTuneAt = now; - this.scheduleRender(120); - } - } - - renderGraph(index, graph, layout) { - this.cancelCanvasPanMomentum(); - this.cancelCanvasZoomAnimation(); - this.cancelCanvasSpringBack(); - this.cancelCanvasSwirlAnimation(); - this.destroyPixiRenderer(); - this.graphHost.empty(); - this.graphHost.classList.remove("is-hovering"); - this.graphHost.classList.remove("is-panning"); - this.graphHost.classList.remove("is-pointing"); - this.activeHighlightElements.clear(); - this.nodeElementsById.clear(); - this.edgeElementsByNode.clear(); - this.edgeElementsById.clear(); - this.hoverNodeId = null; - this.hoverLink = null; - - const pixiRenderer = this.createPixiRenderer(); - const canvas = pixiRenderer - ? pixiRenderer.view - : this.graphHost.createEl("canvas", { - cls: "mwm-canvas", - attr: { - role: "img", - "aria-label": "Mini World Map graph", - tabindex: "0" - } - }); - this.canvas = canvas; - this.renderFloatingCanvasControls(); - this.renderFloatingThemeButton(); - this.canvasPalette = this.readCanvasPalette(); - - const query = normalizedQuery(this.state.search); - this.canvasData = buildCanvasGraphData( - index, - graph, - layout, - query, - this.plugin.nativeGraphSettings || DEFAULT_NATIVE_GRAPH_SETTINGS, - { - includeHoverLinks: this.needsCanvasHoverLinks(), - hiddenLegendItems: this.state.hiddenLegendItems - } - ); - this.lastCanvasBundle = { index, graph, layout }; - - const viewport = this.canvasViewport(); - const viewportStateKey = this.viewportStateKey(graph); - const changingViewport = this.currentViewportStateKey !== viewportStateKey; - if (changingViewport) this.saveViewportState(); - const minZoom = this.canvasMinZoom(viewport, layout); - const maxZoom = this.canvasMaxZoom(viewport, layout); - const requestedWholeMap = this.state.zoom <= minZoom + 0.0005; - this.state.zoom = clampFloat(this.state.zoom, minZoom, maxZoom, 1); - if (pixiRenderer) pixiRenderer.resize(viewport); - else this.sizeCanvasToViewport(canvas, viewport); - const signature = [ - graph.rootId, - graph.focusId || "", - graph.nodes.length, - graph.linkEdges.length, - Math.round(layout.width), - Math.round(layout.height) - ].join("|"); - - if (!this.viewInitialized || this.graphViewSignature !== signature || changingViewport) { - const restored = this.restoreViewportState(viewportStateKey, viewport, layout); - if (!restored) { - this.state.zoom = this.defaultZoomForLayout(layout, viewport); - this.centerCanvasView(layout, viewport, this.state.mode === "focus" ? (graph.focusId || graph.rootId) : null); - } - this.currentViewportStateKey = viewportStateKey; - this.graphViewSignature = signature; - this.viewInitialized = true; - this.canvasVisualNodes.clear(); - this.canvasVisualEdges.clear(); - this.canvasVisualInitialized = false; - } else if (requestedWholeMap) { - this.centerCanvasView(layout, viewport, null); - } - this.syncZoomControls(); - - this.installCanvasEvents(canvas); - this.drawCanvasGraph(); - this.startCanvasSwirlAnimation(); - } - - createPixiRenderer() { - if (this.pixiUnavailable) return null; - const PIXI = loadMiniWorldMapPixiRuntime(); - if (!PIXI) { - this.pixiUnavailable = true; - console.warn("Mini World Map Pixi renderer unavailable; falling back to 2D canvas."); - return null; - } - try { - this.pixiRenderer = new MiniWorldMapPixiRenderer(this.graphHost, PIXI); - return this.pixiRenderer; - } catch (error) { - this.pixiUnavailable = true; - console.error("Mini World Map failed to create Pixi renderer; falling back to 2D canvas.", error); - this.destroyPixiRenderer(); - return null; - } - } - - destroyPixiRenderer() { - if (this.pixiRenderer) { - this.pixiRenderer.destroy(); - this.pixiRenderer = null; - } - } - - renderFloatingThemeButton() { - if (!this.graphHost) return; - const scheme = normalizeColorScheme(this.state.colorScheme); - const next = nextColorScheme(scheme); - const button = this.graphHost.createEl("button", { - cls: `mwm-floating-button mwm-theme-button is-${scheme}`, - attr: { - type: "button", - "aria-label": `Theme: ${colorSchemeLabel(scheme)}`, - title: `Theme: ${colorSchemeLabel(scheme)}. Click for ${colorSchemeLabel(next)}.` - } - }); - setIcon(button, colorSchemeIcon(scheme)); - button.addEventListener("click", evt => { - evt.preventDefault(); - evt.stopPropagation(); - this.cycleColorScheme(); - }); - } - - renderFloatingCanvasControls() { - if (!this.graphHost) return; - const controls = this.graphHost.createDiv({ cls: "mwm-floating-controls" }); - - const rootButton = controls.createEl("button", { - cls: "mwm-floating-button", - attr: { type: "button", "aria-label": "Vault root", title: "Vault root" } - }); - setIcon(rootButton, "home"); - rootButton.addEventListener("click", evt => { - evt.preventDefault(); - evt.stopPropagation(); - this.resetToVaultRoot(); - }); - - const pinButton = controls.createEl("button", { - cls: "mwm-floating-button", - attr: { type: "button", "aria-label": "Pin hover", title: "Pin hover" } - }); - setIcon(pinButton, "pin"); - pinButton.addEventListener("click", evt => { - evt.preventDefault(); - evt.stopPropagation(); - this.pinCurrentHoverPath(); - }); - - const fullscreenButton = controls.createEl("button", { - cls: "mwm-floating-button", - attr: { - type: "button", - "aria-label": this.state.fullscreen ? "Exit full screen" : "Full screen", - title: this.state.fullscreen ? "Exit full screen" : "Full screen" - } - }); - setIcon(fullscreenButton, this.state.fullscreen ? "minimize-2" : "maximize-2"); - fullscreenButton.addEventListener("click", evt => { - evt.preventDefault(); - evt.stopPropagation(); - this.toggleFullscreen(); - }); - } - - readCanvasPalette() { - const fallbackBg = resolveCssColor(this.contentEl, "--mwm-graph-bg", "#1e1e1e"); - const fallbackLine = resolveCssColor(this.contentEl, "--mwm-graph-line", "rgba(128, 128, 128, 0.45)"); - const fallbackNode = resolveCssColor(this.contentEl, "--mwm-node", "#8f8f8f"); - const fallbackFocus = resolveCssColor(this.contentEl, "--mwm-node-focused", "#8b7cf6"); - const fallbackText = resolveCssColor(this.contentEl, "--mwm-text", "#dddddd"); - const graphLine = fallbackLine; - const graphNode = fallbackNode; - const graphFocus = fallbackFocus; - const graphCircle = resolveCssColor(this.contentEl, "--mwm-node-focused", graphFocus); - const graphText = fallbackText; - const graphFillHighlight = resolveCssColor(this.contentEl, "--mwm-node-glow", graphFocus); - const graphLineHighlight = resolveCssColor(this.contentEl, "--mwm-link-highlight", graphFocus); - const graphUnresolved = resolveCssColor(this.contentEl, "--mwm-unresolved", fallbackNode); - return { - bg: fallbackBg, - line: graphLine, - lineHighlight: graphLineHighlight, - node: graphNode, - note: resolveCssColor(this.contentEl, "--mwm-note", graphNode), - folder: resolveCssColor(this.contentEl, "--mwm-folder", graphNode), - folderMeta: resolveCssColor(this.contentEl, "--mwm-folder-meta", graphNode), - folderRing: resolveCssColor(this.contentEl, "--mwm-folder-ring", graphLine), - tree: resolveCssColor(this.contentEl, "--mwm-tree", graphLine), - ringGuide: resolveCssColor(this.contentEl, "--mwm-ring-guide", graphLine), - fileRing: resolveCssColor(this.contentEl, "--mwm-file-ring", graphNode), - focus: graphFocus, - circle: graphCircle, - text: graphText, - labelText: resolveCssColor(this.contentEl, "--mwm-label-text", graphText), - labelStroke: resolveCssColor(this.contentEl, "--mwm-label-stroke", fallbackBg), - link: resolveCssColor(this.contentEl, "--mwm-link", graphLine), - external: resolveCssColor(this.contentEl, "--mwm-external", graphCircle), - externalLink: resolveCssColor(this.contentEl, "--mwm-external-link", graphCircle), - unresolved: graphUnresolved, - muted: resolveCssColor(this.contentEl, "--mwm-muted", "#999999"), - stroke: resolveCssColor(this.contentEl, "--mwm-node-stroke", "#111111"), - glow: graphFillHighlight, - fontFamily: getComputedStyle(this.contentEl).fontFamily || "system-ui, -apple-system, BlinkMacSystemFont, \"Segoe UI\", sans-serif" - }; - } - - canvasViewport() { - return { - width: Math.max(1, Math.floor(this.graphHost?.clientWidth || 900)), - height: Math.max(1, Math.floor(this.graphHost?.clientHeight || 520)) - }; - } - - sizeCanvasToViewport(canvas, viewport) { - const dpr = Math.max(1, Math.min(3, window.devicePixelRatio || 1)); - const width = Math.max(1, viewport.width); - const height = Math.max(1, viewport.height); - if (canvas.width !== Math.floor(width * dpr)) canvas.width = Math.floor(width * dpr); - if (canvas.height !== Math.floor(height * dpr)) canvas.height = Math.floor(height * dpr); - canvas.style.width = `${width}px`; - canvas.style.height = `${height}px`; - this.canvasDpr = dpr; - this.canvasViewportSize = { width, height }; - } - - canvasMinZoom(viewport = this.canvasViewport(), layout = this.lastLayout) { - if (!layout || !viewport) return DEFAULT_MIN_CANVAS_ZOOM; - const fitZoom = fitZoomForLayout(layout, viewport, 32); - return clampFloat(Math.min(DEFAULT_MIN_CANVAS_ZOOM, fitZoom), MIN_CANVAS_ZOOM, DEFAULT_MIN_CANVAS_ZOOM, DEFAULT_MIN_CANVAS_ZOOM); - } - - canvasMaxZoom(viewport = this.canvasViewport(), layout = this.lastLayout) { - return MAX_CANVAS_ZOOM; - } - - viewportStateKey(graph) { - const root = graph?.rootId || ROOT_ID; - const focus = graph?.focusId || ""; - const detail = this.state.showCompleteRoot ? "complete" : "bounded"; - return `${this.state.mode}|${root}|${focus}|${detail}`; - } - - saveViewportState() { - if (!this.currentViewportStateKey) return; - if (!Number.isFinite(this.state.zoom) || !Number.isFinite(this.canvasPanX) || !Number.isFinite(this.canvasPanY)) return; - this.viewportStateByKey.set(this.currentViewportStateKey, { - zoom: this.state.zoom, - panX: this.canvasPanX, - panY: this.canvasPanY - }); - } - - restoreViewportState(key, viewport, layout) { - const saved = this.viewportStateByKey.get(key); - if (!saved) return false; - const minZoom = this.canvasMinZoom(viewport, layout); - const maxZoom = this.canvasMaxZoom(viewport, layout); - this.state.zoom = clampFloat(saved.zoom, minZoom, maxZoom, this.defaultZoomForLayout(layout, viewport)); - this.canvasPanX = Number.isFinite(saved.panX) ? saved.panX : 0; - this.canvasPanY = Number.isFinite(saved.panY) ? saved.panY : 0; - return true; - } - - defaultZoomForLayout(layout, viewport = this.canvasViewport()) { - const minZoom = this.canvasMinZoom(viewport, layout); - const maxZoom = this.canvasMaxZoom(viewport, layout); - const fitZoom = fitZoomForLayout(layout, viewport, 42); - return clampFloat(fitZoom * 1.08, minZoom, maxZoom, minZoom); - } - - centerCanvasView(layout, viewport = this.canvasViewport(), rootId = ROOT_ID) { - const zoom = clampFloat(this.state.zoom, this.canvasMinZoom(viewport, layout), this.canvasMaxZoom(viewport, layout), 1); - const rootPoint = rootId === null ? null : (layout.positions.get(rootId) || layout.positions.get(ROOT_ID)); - const centerX = rootPoint ? rootPoint.x : layout.width / 2; - const centerY = rootPoint ? rootPoint.y : layout.height / 2; - this.canvasPanX = viewport.width / 2 - centerX * zoom; - this.canvasPanY = viewport.height / 2 - centerY * zoom; - this.saveViewportState(); - } - - installCanvasEvents(canvas) { - canvas.addEventListener("pointerdown", evt => { - if (evt.button !== 0) return; - this.cancelCanvasPanMomentum(); - this.cancelCanvasZoomAnimation(); - this.cancelCanvasSpringBack(); - const point = this.canvasEventPoint(evt); - const hit = this.hitTestCanvas(point); - if (hit.node && hit.node.node.id !== this.lastCanvasBundle?.graph?.rootId) { - const world = this.screenToWorld(point); - this.dragState = { - mode: "node", - pointerId: evt.pointerId, - nodeId: hit.node.node.id, - startClientX: evt.clientX, - startClientY: evt.clientY, - startWorldX: world.x, - startWorldY: world.y, - nodeStartX: hit.node.point.x, - nodeStartY: hit.node.point.y, - lastDx: 0, - lastDy: 0, - moved: false, - suppressClick: false - }; - canvas.setPointerCapture?.(evt.pointerId); - this.hoverNodeId = hit.node.node.id; - this.hoverLink = null; - this.graphHost.classList.add("is-panning", "is-pointing"); - this.requestCanvasDraw("interactive"); - return; - } - - this.dragState = { - mode: "pan", - pointerId: evt.pointerId, - startClientX: evt.clientX, - startClientY: evt.clientY, - startPanX: this.canvasPanX, - startPanY: this.canvasPanY, - lastClientX: evt.clientX, - lastClientY: evt.clientY, - lastMoveAt: performance.now(), - velocityX: 0, - velocityY: 0, - moved: false, - suppressClick: false - }; - canvas.setPointerCapture?.(evt.pointerId); - this.graphHost.classList.add("is-panning"); - this.updateCanvasHover(point); - }); - - canvas.addEventListener("pointermove", evt => { - const point = this.canvasEventPoint(evt); - if (this.dragState) { - const dx = evt.clientX - this.dragState.startClientX; - const dy = evt.clientY - this.dragState.startClientY; - this.dragState.moved = this.dragState.moved || Math.hypot(dx, dy) > 3; - this.dragState.suppressClick = this.dragState.moved; - if (this.dragState.mode === "node") { - const world = this.screenToWorld(point); - const worldDx = world.x - this.dragState.startWorldX; - const worldDy = world.y - this.dragState.startWorldY; - const stepDx = worldDx - (this.dragState.lastDx || 0); - const stepDy = worldDy - (this.dragState.lastDy || 0); - this.dragState.lastDx = worldDx; - this.dragState.lastDy = worldDy; - - const item = this.canvasData?.nodesById?.get(this.dragState.nodeId); - if (item) { - item.point.x = this.dragState.nodeStartX + worldDx; - item.point.y = this.dragState.nodeStartY + worldDy; - this.updateCanvasPointPolar(item.point); - this.applyNodeDragGravity(item, stepDx, stepDy); - if (!this.canvasNeedsFastDraw()) this.resolveCanvasNodeOverlapsAround(item.node.id, 2); - this.hoverNodeId = item.node.id; - this.hoverLink = null; - } - this.requestCanvasDraw("interactive"); - return; - } - - this.canvasPanX = this.dragState.startPanX + dx; - this.canvasPanY = this.dragState.startPanY + dy; - const now = performance.now(); - const dt = Math.max(1, now - (this.dragState.lastMoveAt || now)); - const stepX = evt.clientX - (this.dragState.lastClientX ?? evt.clientX); - const stepY = evt.clientY - (this.dragState.lastClientY ?? evt.clientY); - this.dragState.velocityX = this.dragState.velocityX * 0.45 + (stepX / dt) * 0.55; - this.dragState.velocityY = this.dragState.velocityY * 0.45 + (stepY / dt) * 0.55; - this.dragState.lastClientX = evt.clientX; - this.dragState.lastClientY = evt.clientY; - this.dragState.lastMoveAt = now; - this.saveViewportState(); - this.requestCanvasDraw("interactive"); - return; - } - this.updateCanvasHover(point); - }); - - canvas.addEventListener("pointerup", evt => { - const wasDragging = Boolean(this.dragState && this.dragState.suppressClick); - const draggedNodeId = this.dragState && this.dragState.mode === "node" ? this.dragState.nodeId : null; - const panVelocity = this.dragState && this.dragState.mode === "pan" - ? { x: this.dragState.velocityX || 0, y: this.dragState.velocityY || 0 } - : null; - canvas.releasePointerCapture?.(evt.pointerId); - this.dragState = null; - this.graphHost.classList.remove("is-panning"); - if (draggedNodeId && wasDragging) { - if (!this.canvasNeedsFastDraw()) this.resolveCanvasNodeOverlapsAround(draggedNodeId, 3); - this.startCanvasSpringBack(draggedNodeId); - return; - } - if (wasDragging) { - if (panVelocity) { - this.startCanvasPanMomentum(panVelocity.x, panVelocity.y); - } else { - this.requestCanvasDraw("full"); - } - return; - } - if (!wasDragging) this.activateCanvasAt(this.canvasEventPoint(evt), evt); - }); - - canvas.addEventListener("pointerleave", () => { - if (this.dragState) return; - this.hoverNodeId = null; - this.hoverLink = null; - this.graphHost.classList.remove("is-hovering"); - this.graphHost.classList.remove("is-pointing"); - this.requestCanvasDraw("full"); - }); - - canvas.addEventListener("dblclick", evt => { - const hit = this.hitTestCanvas(this.canvasEventPoint(evt)); - if (!hit.node) return; - evt.preventDefault(); - evt.stopPropagation(); - const node = hit.node.node; - if (node.type === "note") { - this.openNode(node.id, evt); - } else if (node.type === "folder") { - this.state.mode = "atlas"; - this.state.rootPath = node.id; - this.state.showCompleteRoot = false; - this.viewInitialized = false; - this.resetAdaptiveTuning(); - this.render(); - } - }); - - canvas.addEventListener("contextmenu", evt => { - const hit = this.hitTestCanvas(this.canvasEventPoint(evt)); - if (!hit.node) return; - evt.preventDefault(); - this.showNodeMenu(evt, hit.node.node); - }); - } - - updateCanvasPointPolar(point) { - if (!point) return; - const centerX = Number.isFinite(point.centerX) ? point.centerX : 0; - const centerY = Number.isFinite(point.centerY) ? point.centerY : 0; - const dx = point.x - centerX; - const dy = point.y - centerY; - point.radius = Math.hypot(dx, dy); - point.angle = Math.atan2(dy, dx); - point.labelSide = labelSideForAngle(point.angle); - } - - applyNodeDragGravity(draggedItem, dx, dy) { - if (!draggedItem || (!dx && !dy) || !this.canvasData) return; - const nodeId = draggedItem.node.id; - const edges = this.canvasData.edgesByNode.get(nodeId) || []; - if (!edges.length) return; - - const gravity = clampFloat(Math.sqrt(Math.max(1, draggedItem.radius)) / 9.2, 0.06, 0.34, 0.12); - const moved = new Set([nodeId]); - for (const edge of edges) { - const otherId = edge.source === nodeId ? edge.target : edge.source; - if (moved.has(otherId)) continue; - const other = this.canvasData.nodesById.get(otherId); - if (!other || other.node.id === this.lastCanvasBundle?.graph?.rootId) continue; - - const hierarchy = edge.edge && edge.edge.type && String(edge.edge.type).includes("hierarchy"); - const strength = gravity * (hierarchy ? 0.42 : 0.18); - other.point.x += dx * strength; - other.point.y += dy * strength; - this.updateCanvasPointPolar(other.point); - moved.add(otherId); - } - } - - startCanvasPanMomentum(velocityX, velocityY) { - this.cancelCanvasPanMomentum(); - const speed = Math.hypot(velocityX, velocityY); - if (!this.canvas || !this.canvasData || speed < 0.06) { - this.requestCanvasDraw("full"); - return; - } - - this.canvasPanVelocityX = clampFloat(velocityX, -2.2, 2.2, 0); - this.canvasPanVelocityY = clampFloat(velocityY, -2.2, 2.2, 0); - let lastFrameAt = performance.now(); - - const tick = now => { - this.canvasPanAnimationFrame = null; - if (!this.canvas || !this.canvasData || this.dragState) { - this.canvasPanVelocityX = 0; - this.canvasPanVelocityY = 0; - return; - } - - const dt = clampFloat(now - lastFrameAt, 1, 40, 16); - lastFrameAt = now; - this.canvasPanX += this.canvasPanVelocityX * dt; - this.canvasPanY += this.canvasPanVelocityY * dt; - const friction = Math.exp(-dt / 185); - this.canvasPanVelocityX *= friction; - this.canvasPanVelocityY *= friction; - this.saveViewportState(); - this.requestCanvasDraw("interactive"); - - if (Math.hypot(this.canvasPanVelocityX, this.canvasPanVelocityY) < 0.018) { - this.canvasPanVelocityX = 0; - this.canvasPanVelocityY = 0; - this.requestCanvasDraw("full"); - return; - } - - this.canvasPanAnimationFrame = window.requestAnimationFrame(tick); - }; - - this.canvasPanAnimationFrame = window.requestAnimationFrame(tick); - } - - cancelCanvasPanMomentum() { - if (this.canvasPanAnimationFrame) { - window.cancelAnimationFrame(this.canvasPanAnimationFrame); - this.canvasPanAnimationFrame = null; - } - this.canvasPanVelocityX = 0; - this.canvasPanVelocityY = 0; - } - - resolveCanvasNodeOverlapsAround(nodeId, iterations = 3) { - if (!this.canvasData || !this.canvasData.nodesById.has(nodeId)) return; - - const dragged = this.canvasData.nodesById.get(nodeId); - const rootId = this.lastCanvasBundle?.graph?.rootId; - const zoom = clampFloat(this.state.zoom, this.canvasMinZoom(), this.canvasMaxZoom(), 1); - const gap = Math.max(8, 14 / zoom); - - for (let pass = 0; pass < iterations; pass += 1) { - for (const other of this.canvasData.nodes) { - if (!other || other.node.id === nodeId) continue; - - let dx = other.point.x - dragged.point.x; - let dy = other.point.y - dragged.point.y; - let distance = Math.hypot(dx, dy); - if (distance < 0.001) { - const angle = deterministicPairAngle(nodeId, other.node.id); - dx = Math.cos(angle); - dy = Math.sin(angle); - distance = 1; - } - - const minDistance = dragged.radius + other.radius + gap; - if (distance >= minDistance) continue; - - const push = (minDistance - distance) * (pass === 0 ? 0.95 : 0.72); - const nx = dx / distance; - const ny = dy / distance; - const otherFixed = other.node.id === rootId; - if (otherFixed) { - dragged.point.x -= nx * push; - dragged.point.y -= ny * push; - this.updateCanvasPointPolar(dragged.point); - } else { - other.point.x += nx * push; - other.point.y += ny * push; - this.updateCanvasPointPolar(other.point); - } - } - } - } - - startCanvasSpringBack(nodeId) { - this.cancelCanvasSpringBack(); - if (!this.canvasData || !this.canvasData.nodes || !this.canvasData.nodes.length) return; - - const rootId = this.lastCanvasBundle?.graph?.rootId; - const items = []; - let maxDistance = 0; - - for (const item of this.canvasData.nodes) { - if (!item || !item.point || item.node.id === rootId) continue; - const point = item.point; - const homeX = Number.isFinite(point.homeX) ? point.homeX : point.x; - const homeY = Number.isFinite(point.homeY) ? point.homeY : point.y; - const distance = Math.hypot(point.x - homeX, point.y - homeY); - if (distance < 0.55) continue; - maxDistance = Math.max(maxDistance, distance); - items.push({ - item, - startX: point.x, - startY: point.y, - homeX, - homeY, - primary: item.node.id === nodeId - }); - } - - if (!items.length) { - this.requestCanvasDraw("full"); - return; - } - - const started = performance.now(); - const duration = clampFloat(300 + Math.sqrt(maxDistance) * 16, 340, 760, 460); - this.canvasSpringState = { items, started, duration }; - - const tick = now => { - const state = this.canvasSpringState; - if (!state || this.dragState) { - this.canvasSpringAnimationFrame = null; - return; - } - - const progress = clampFloat((now - state.started) / state.duration, 0, 1, 1); - const eased = springBackEase(progress); - - for (const entry of state.items) { - const point = entry.item.point; - const strength = entry.primary ? eased : Math.min(1, eased * 0.94); - point.x = entry.startX + (entry.homeX - entry.startX) * strength; - point.y = entry.startY + (entry.homeY - entry.startY) * strength; - this.updateCanvasPointPolar(point); - } - - if (progress >= 1) { - for (const entry of state.items) { - const point = entry.item.point; - point.x = entry.homeX; - point.y = entry.homeY; - this.updateCanvasPointPolar(point); - } - this.canvasSpringAnimationFrame = null; - this.canvasSpringState = null; - this.requestCanvasDraw("full"); - return; - } - - this.canvasInteractionUntil = performance.now() + 120; - this.drawCanvasGraph({ mode: "interactive" }); - this.canvasSpringAnimationFrame = window.requestAnimationFrame(tick); - }; - - this.canvasSpringAnimationFrame = window.requestAnimationFrame(tick); - } - - cancelCanvasSpringBack() { - if (this.canvasSpringAnimationFrame) { - window.cancelAnimationFrame(this.canvasSpringAnimationFrame); - this.canvasSpringAnimationFrame = null; - } - this.canvasSpringState = null; - } - - canvasEventPoint(evt) { - const rect = this.canvas.getBoundingClientRect(); - return { - x: evt.clientX - rect.left, - y: evt.clientY - rect.top - }; - } - - screenToWorld(point) { - const viewport = this.canvasViewport(); - const zoom = clampFloat(this.state.zoom, this.canvasMinZoom(viewport), this.canvasMaxZoom(viewport), 1); - return { - x: (point.x - this.canvasPanX) / zoom, - y: (point.y - this.canvasPanY) / zoom - }; - } - - zoomAtClientPoint(clientX, clientY, deltaY, deltaMode = 0) { - const point = this.canvas ? this.canvasEventPoint({ clientX, clientY }) : null; - const viewport = this.canvasViewport(); - const minZoom = this.canvasMinZoom(viewport); - const maxZoom = this.canvasMaxZoom(viewport); - const previousZoom = clampFloat(this.state.zoom, minZoom, maxZoom, 1); - const normalizedDelta = normalizeWheelDeltaY(deltaY, deltaMode); - const factor = Math.pow(ZOOM_WHEEL_BASE, -normalizedDelta / 120); - const baseZoom = Number.isFinite(this.canvasTargetZoom) ? this.canvasTargetZoom : previousZoom; - this.startCanvasZoomAnimation(baseZoom * factor, point); - } - - startCanvasZoomAnimation(nextZoom, anchorPoint = null) { - const viewport = this.canvasViewport(); - const minZoom = this.canvasMinZoom(viewport); - const maxZoom = this.canvasMaxZoom(viewport); - this.canvasTargetZoom = clampFloat(nextZoom, minZoom, maxZoom, this.state.zoom); - this.canvasZoomAnchorPoint = anchorPoint || { x: viewport.width / 2, y: viewport.height / 2 }; - this.canvasInteractionUntil = performance.now() + 180; - if (!this.canvasZoomAnimationFrame) { - this.canvasZoomAnimationFrame = window.requestAnimationFrame(() => this.stepCanvasZoomAnimation()); - } - } - - stepCanvasZoomAnimation() { - this.canvasZoomAnimationFrame = null; - if (!this.canvas || !this.lastCanvasBundle || !Number.isFinite(this.canvasTargetZoom)) { - this.canvasTargetZoom = null; - this.canvasZoomAnchorPoint = null; - return; - } - - const viewport = this.canvasViewport(); - const minZoom = this.canvasMinZoom(viewport); - const maxZoom = this.canvasMaxZoom(viewport); - const previousZoom = clampFloat(this.state.zoom, minZoom, maxZoom, 1); - const targetZoom = clampFloat(this.canvasTargetZoom, minZoom, maxZoom, previousZoom); - const zoomDelta = (previousZoom > targetZoom ? previousZoom / targetZoom : targetZoom / previousZoom) - 1; - const done = zoomDelta < 0.006; - const zoom = done - ? targetZoom - : previousZoom * ZOOM_ANIMATION_FRICTION + targetZoom * (1 - ZOOM_ANIMATION_FRICTION); - const point = this.canvasZoomAnchorPoint || { x: viewport.width / 2, y: viewport.height / 2 }; - const world = { - x: (point.x - this.canvasPanX) / previousZoom, - y: (point.y - this.canvasPanY) / previousZoom - }; - - this.state.zoom = clampFloat(zoom, minZoom, maxZoom, previousZoom); - this.canvasPanX = point.x - world.x * this.state.zoom; - this.canvasPanY = point.y - world.y * this.state.zoom; - if (done) this.syncZoomControls(); - this.saveViewportState(); - this.canvasInteractionUntil = performance.now() + 120; - this.drawCanvasGraph({ mode: "interactive" }); - - if (done) { - this.canvasTargetZoom = null; - this.canvasZoomAnchorPoint = null; - this.scheduleSettledCanvasDraw(120); - return; - } - - this.canvasZoomAnimationFrame = window.requestAnimationFrame(() => this.stepCanvasZoomAnimation()); - } - - cancelCanvasZoomAnimation() { - if (this.canvasZoomAnimationFrame) { - window.cancelAnimationFrame(this.canvasZoomAnimationFrame); - this.canvasZoomAnimationFrame = null; - } - this.canvasTargetZoom = null; - this.canvasZoomAnchorPoint = null; - } - - syncZoomControls() { - const viewport = this.canvasViewport(); - const minZoom = this.canvasMinZoom(viewport); - const maxZoom = this.canvasMaxZoom(viewport); - this.state.zoom = clampFloat(this.state.zoom, minZoom, maxZoom, 1); - if (this.zoomInput) { - this.zoomInput.min = "0"; - this.zoomInput.max = String(ZOOM_SLIDER_STEPS); - this.zoomInput.value = zoomToSliderValue(this.state.zoom, minZoom, maxZoom); - } - if (this.zoomLabel) this.zoomLabel.textContent = `${Math.round(this.state.zoom * 100)}%`; - } - - setCanvasZoom(nextZoom, anchorPoint = null, redraw = true) { - this.cancelCanvasZoomAnimation(); - const viewport = this.canvasViewport(); - const minZoom = this.canvasMinZoom(viewport); - const maxZoom = this.canvasMaxZoom(viewport); - const previousZoom = clampFloat(this.state.zoom, minZoom, maxZoom, 1); - const zoom = clampFloat(nextZoom, minZoom, maxZoom, previousZoom); - if (Math.abs(zoom - previousZoom) < 0.0001) { - this.syncZoomControls(); - return; - } - - const point = anchorPoint || { x: viewport.width / 2, y: viewport.height / 2 }; - if (this.canvas && this.lastCanvasBundle) { - const world = { - x: (point.x - this.canvasPanX) / previousZoom, - y: (point.y - this.canvasPanY) / previousZoom - }; - this.canvasPanX = point.x - world.x * zoom; - this.canvasPanY = point.y - world.y * zoom; - } - - this.state.zoom = zoom; - this.syncZoomControls(); - this.saveViewportState(); - if (redraw && this.canvas && this.lastCanvasBundle) { - this.requestCanvasDraw("interactive"); - this.scheduleSettledCanvasDraw(); - } - else if (redraw) this.render(); - } - - updateCanvasHover(point) { - const hit = this.hitTestCanvas(point, { includeLinks: hoverHighlightsNoteLinks(this.state.hoverHighlightMode) }); - const nextNodeId = hit.node ? hit.node.node.id : null; - const nextLink = hit.link ? hit.link.edge : null; - const sameLink = (!nextLink && !this.hoverLink) || (nextLink && this.hoverLink && nextLink.id === this.hoverLink.id); - if (nextNodeId === this.hoverNodeId && sameLink) return; - - const hasNodeHover = nextNodeId !== null && nextNodeId !== undefined; - this.hoverNodeId = nextNodeId; - this.hoverLink = hasNodeHover ? null : nextLink; - this.graphHost.classList.toggle("is-hovering", hasNodeHover || Boolean(this.hoverLink)); - this.graphHost.classList.toggle("is-pointing", hasNodeHover || Boolean(this.hoverLink)); - if (this.canvasNeedsFastDraw()) { - this.requestCanvasDraw("interactive"); - this.scheduleSettledCanvasDraw(180); - } else { - this.requestCanvasDraw("full"); - } - } - - activateCanvasAt(point, evt) { - const { index, graph } = this.lastCanvasBundle || {}; - if (!index || !graph) return; - const hit = this.hitTestCanvas(point, { includeLinks: true }); - - if (hit.node) { - evt.preventDefault(); - evt.stopPropagation(); - this.state.sidePage = "inspect"; - this.selectedLink = null; - this.selectedNodeId = hit.node.node.id; - this.applyPersistentHighlight(); - if (!this.state.fullscreen) this.renderSidePanel(index, graph, hit.node.node.id); - return; - } - - if (hit.link) { - evt.preventDefault(); - evt.stopPropagation(); - this.state.sidePage = "inspect"; - this.selectedLink = hit.link.edge; - this.selectedNodeId = null; - this.applyPersistentHighlight(); - if (!this.state.fullscreen) this.renderSidePanel(index, graph); - return; - } - - this.clearSelection(index, graph); - } - - hitTestCanvas(point, options = {}) { - if (!this.canvasData) return { node: null, link: null }; - const world = this.screenToWorld(point); - const viewport = this.canvasViewport(); - const zoom = clampFloat(this.state.zoom, this.canvasMinZoom(viewport), this.canvasMaxZoom(viewport), 1); - const nodePad = Math.max(4, 8 / zoom); - const hitNode = this.canvasSwirlMotionActive() - ? hitTestCanvasNodeLinear(this.canvasData, world, nodePad) - : hitTestCanvasNodeIndex(this.canvasData, world, nodePad); - if (hitNode) return { node: hitNode, link: null }; - - if (options.includeLinks) { - const linkPad = Math.max(4, 7 / zoom); - for (let i = this.canvasData.links.length - 1; i >= 0; i -= 1) { - const item = this.canvasData.links[i]; - const distance = distanceToSegment(world, item.sourcePoint, item.targetPoint); - if (distance <= linkPad + item.width * 0.5) return { node: null, link: item }; - } - } - - return { node: null, link: null }; - } - - drawCanvasGraph(options = {}) { - if (!this.canvas || !this.canvasData || !this.lastCanvasBundle) return; - const viewport = this.canvasViewport(); - if (this.pixiRenderer) this.pixiRenderer.resize(viewport); - else this.sizeCanvasToViewport(this.canvas, viewport); - - const palette = this.canvasPalette || this.readCanvasPalette(); - const zoom = clampFloat(this.state.zoom, this.canvasMinZoom(viewport), this.canvasMaxZoom(viewport), 1); - const frameTime = Number.isFinite(options.now) ? options.now : performance.now(); - this.applyCanvasSwirlFrame(frameTime); - const active = this.canvasActiveState(); - const mode = options.mode || (this.dragState || performance.now() < this.canvasInteractionUntil ? "interactive" : "full"); - const interactive = mode === "interactive"; - const activeKey = this.canvasActiveStateKey(active); - const geometryDirty = Boolean( - (this.dragState && this.dragState.mode === "node") - || this.canvasSpringState - || this.canvasSwirlMotionActive() - ); - if ( - this.pixiRenderer - && interactive - && !geometryDirty - && this.pixiRenderer.transformOnly({ - data: this.canvasData, - bundle: this.lastCanvasBundle, - palette, - active, - activeKey, - viewport, - panX: this.canvasPanX, - panY: this.canvasPanY, - zoom, - geometryDirty, - selectedNodeId: this.selectedNodeId, - labelVisibility: this.state.labelVisibility, - mode, - now: frameTime, - allowZoomTransformOnly: true - }) - ) { - return; - } - - if (this.pixiRenderer) { - if (geometryDirty) { - this.updateCanvasDynamicLinkRoutes({ - enabled: this.canvasSwirlMotionActive(), - includeBaseLinks: true, - highlightedHoverLinks: [] - }); - } - if (this.lastCanvasBundle?.layout) { - this.lastCanvasBundle.layout.spinStartedAt = this.canvasSwirlStartedAt || 0; - this.lastCanvasBundle.layout.spinSpeed = this.canvasSwirlAmount(); - } - this.pixiRenderer.render({ - data: this.canvasData, - bundle: this.lastCanvasBundle, - palette, - active, - activeKey, - viewport, - panX: this.canvasPanX, - panY: this.canvasPanY, - zoom, - mode, - geometryDirty, - selectedNodeId: this.selectedNodeId, - labelVisibility: this.state.labelVisibility, - now: frameTime - }); - return; - } - - const fastInteractive = interactive && this.canvasNeedsFastDraw(); - const visuals = this.updateCanvasVisualState(active, zoom, { immediate: interactive }); - const bounds = canvasWorldBounds( - viewport, - this.canvasPanX, - this.canvasPanY, - zoom, - this.pixiRenderer ? (interactive ? 900 : 520) : (interactive ? 120 : 220) - ); - const highlightedHoverLinks = active.highlightedEdges.size - ? Array.from(active.highlightedEdges) - .map(key => this.canvasData.edgesById.get(key)) - .filter(item => item && item.hoverOnly) - : []; - this.updateCanvasDynamicLinkRoutes({ - enabled: this.canvasSwirlMotionActive(), - includeBaseLinks: !fastInteractive, - highlightedHoverLinks - }); - - const ctx = this.canvas.getContext("2d"); - ctx.setTransform(this.canvasDpr, 0, 0, this.canvasDpr, 0, 0); - ctx.clearRect(0, 0, viewport.width, viewport.height); - this.drawCanvasBackground(ctx, palette, viewport); - - ctx.save(); - ctx.translate(this.canvasPanX, this.canvasPanY); - ctx.scale(zoom, zoom); - this.drawCanvasRings(ctx, this.lastCanvasBundle.layout, palette, zoom, { underlay: true, interactive, now: frameTime }); - if (!fastInteractive) { - this.drawCanvasEdges(ctx, this.canvasData.links, visuals, palette, zoom, false, { bounds, interactive }); - } - this.drawCanvasEdges(ctx, this.canvasData.hierarchy, visuals, palette, zoom, true, { bounds, interactive }); - if (!interactive) this.drawCanvasRings(ctx, this.lastCanvasBundle.layout, palette, zoom, { overlay: true, now: frameTime }); - this.drawCanvasEdges(ctx, highlightedHoverLinks, visuals, palette, zoom, false, { hoverOnly: true, bounds, interactive }); - this.drawCanvasNodes(ctx, this.canvasData.nodes, visuals, palette, zoom, { bounds, interactive }); - this.drawCanvasLabels(ctx, this.canvasData.nodes, visuals, palette, zoom, { bounds, interactive, fastInteractive }); - ctx.restore(); - } - - canvasActiveStateKey(active) { - if (!active) return ""; - return [ - active.hasActive ? 1 : 0, - active.activeNodeId || "", - active.activeLinkId || "", - this.selectedNodeId || "", - this.selectedLink?.id || "", - this.hoverNodeId || "", - this.hoverLink?.id || "", - active.relatedNodes?.size || 0, - active.highlightedEdges?.size || 0, - active.labelNodes?.size || 0, - active.pinnedNodeIds?.size || 0, - this.pinnedPaths.length, - normalizeLabelVisibility(this.state.labelVisibility) - ].join("|"); - } - - canvasNeedsFastDraw() { - if (!this.canvasData) return false; - const edgeCount = (this.canvasData.links?.length || 0) + (this.canvasData.hierarchy?.length || 0); - return this.state.showCompleteRoot - || (this.canvasData.nodes?.length || 0) > FAST_CANVAS_NODE_THRESHOLD - || edgeCount > FAST_CANVAS_EDGE_THRESHOLD; - } - - canvasSwirlAmount() { - return clampFloat( - clampNumber(this.state.swirlStrength, 0, MAX_SWIRL_STRENGTH, DEFAULT_SWIRL_STRENGTH) / MAX_SWIRL_STRENGTH, - 0, - 1, - 0 - ); - } - - canvasSwirlMotionActive() { - return this.canvasSwirlAmount() > 0.001; - } - - setSpinSpeed(value, options = {}) { - const previous = this.state.swirlStrength; - const next = clampNumber(value, 0, MAX_SWIRL_STRENGTH, DEFAULT_SWIRL_STRENGTH); - this.state.swirlStrength = next; - - if (this.canvas && this.canvasData) { - if (next > 0) { - this.startCanvasSwirlAnimation(); - this.requestCanvasDraw("interactive"); - } else { - this.cancelCanvasSwirlAnimation(); - this.resetCanvasSwirlPositions(); - this.requestCanvasDraw("full"); - } - } - - const crossedZero = (previous > 0) !== (next > 0); - if (options.render || crossedZero) this.scheduleRender(options.render ? 0 : 140, { preservePanel: true }); - } - - startCanvasSwirlAnimation() { - this.cancelCanvasSwirlAnimation(false); - if (!this.canvas || !this.canvasData || !this.canvasSwirlMotionActive()) { - this.resetCanvasSwirlPositions(); - return; - } - - if (!this.canvasSwirlStartedAt) this.canvasSwirlStartedAt = performance.now(); - this.canvasSwirlLastFrameAt = 0; - - const tick = now => { - this.canvasSwirlAnimationFrame = null; - if (!this.canvas || !this.canvasData || !this.canvasSwirlMotionActive()) { - this.resetCanvasSwirlPositions(); - return; - } - - const frameInterval = this.canvasNeedsFastDraw() - ? SWIRL_FRAME_INTERVAL_MS * 1.75 - : SWIRL_FRAME_INTERVAL_MS; - if (!this.canvasSwirlLastFrameAt || now - this.canvasSwirlLastFrameAt >= frameInterval) { - this.canvasSwirlLastFrameAt = now; - this.canvasInteractionUntil = Math.max(this.canvasInteractionUntil, performance.now() + 80); - this.drawCanvasGraph({ mode: "spin", now }); - } - - this.canvasSwirlAnimationFrame = window.requestAnimationFrame(tick); - }; - - this.canvasSwirlAnimationFrame = window.requestAnimationFrame(tick); - } - - cancelCanvasSwirlAnimation(reset = true) { - if (this.canvasSwirlAnimationFrame) { - window.cancelAnimationFrame(this.canvasSwirlAnimationFrame); - this.canvasSwirlAnimationFrame = null; - } - this.canvasSwirlLastFrameAt = 0; - if (reset) this.canvasSwirlStartedAt = 0; - } - - applyCanvasSwirlFrame(now = performance.now()) { - if (!this.canvasData || !this.canvasData.nodes || !this.canvasData.nodes.length) return; - const amount = this.canvasSwirlAmount(); - if (amount <= 0.001 || this.dragState || this.canvasSpringState) { - if (amount <= 0.001) this.resetCanvasSwirlPositions(); - return; - } - - if (!this.canvasSwirlStartedAt) this.canvasSwirlStartedAt = now; - const elapsedSeconds = Math.max(0, (now - this.canvasSwirlStartedAt) / 1000); - - for (const item of this.canvasData.nodes) { - if (!item || !item.point) continue; - const point = item.point; - const radius = Number.isFinite(point.homeRadius) ? point.homeRadius : point.radius; - if (!Number.isFinite(radius) || radius <= 0.001) continue; - - const depth = Math.max(0, Math.round(point.depth || 0)); - if (depth === 0) { - point.x = Number.isFinite(point.homeX) ? point.homeX : point.x; - point.y = Number.isFinite(point.homeY) ? point.homeY : point.y; - this.updateCanvasPointPolar(point); - continue; - } - - const centerX = Number.isFinite(point.centerX) ? point.centerX : 0; - const centerY = Number.isFinite(point.centerY) ? point.centerY : 0; - const homeAngle = Number.isFinite(point.homeAngle) ? point.homeAngle : point.angle; - const offset = swirlOrbitAngleForRing(depth, radius, amount, elapsedSeconds); - const angle = normalizeAngle(homeAngle + offset); - point.x = centerX + Math.cos(angle) * radius; - point.y = centerY + Math.sin(angle) * radius; - point.radius = radius; - point.angle = angle; - point.labelSide = labelSideForAngle(angle); - } - } - - resetCanvasSwirlPositions() { - if (!this.canvasData || !this.canvasData.nodes) return; - for (const item of this.canvasData.nodes) { - const point = item?.point; - if (!point || !Number.isFinite(point.homeX) || !Number.isFinite(point.homeY)) continue; - point.x = point.homeX; - point.y = point.homeY; - if (Number.isFinite(point.homeRadius)) point.radius = point.homeRadius; - if (Number.isFinite(point.homeAngle)) point.angle = point.homeAngle; - point.labelSide = labelSideForAngle(point.angle); - } - } - - updateCanvasDynamicLinkRoutes(options = {}) { - if (!this.canvasData) return; - const enabled = Boolean(options.enabled); - const highlightedHoverLinks = Array.isArray(options.highlightedHoverLinks) ? options.highlightedHoverLinks : []; - const items = []; - - if (options.includeBaseLinks !== false) items.push(...(this.canvasData.links || [])); - items.push(...highlightedHoverLinks); - - if (!enabled || !items.length) { - for (const item of items) { - if (item) item.dynamicRoute = null; - } - return; - } - - const layout = this.lastCanvasBundle?.layout; - const ringGap = medianRingGap(layout?.rings || []); - const centerX = Number.isFinite(layout?.centerX) ? layout.centerX : 0; - const centerY = Number.isFinite(layout?.centerY) ? layout.centerY : 0; - const capped = items.length > MAX_DYNAMIC_ROUTE_EDGES - ? items.slice(0, MAX_DYNAMIC_ROUTE_EDGES) - : items; - - for (const item of capped) { - if (!item || !item.sourcePoint || !item.targetPoint) continue; - item.dynamicRoute = dynamicOrbitRouteForEdge(item, centerX, centerY, ringGap); - } - - if (capped.length < items.length) { - for (let index = capped.length; index < items.length; index += 1) { - if (items[index]) items[index].dynamicRoute = null; - } - } - } - - drawCanvasBackground(ctx, palette, viewport) { - ctx.fillStyle = palette.bg; - ctx.fillRect(0, 0, viewport.width, viewport.height); - } - - canvasActiveState() { - const data = this.canvasData; - const rawActiveNodeId = this.hoverNodeId ?? this.selectedNodeId ?? null; - const hoverMode = normalizeHoverHighlightMode(this.state.hoverHighlightMode); - const resolveCanvasNodeId = nodeId => { - if (nodeId === null || nodeId === undefined) return null; - if (data.nodesById.has(nodeId)) return nodeId; - const visualNodeId = this.plugin.index.visualNodeId(nodeId); - return visualNodeId !== null && visualNodeId !== undefined && data.nodesById.has(visualNodeId) - ? visualNodeId - : null; - }; - let activeNodeId = resolveCanvasNodeId(rawActiveNodeId); - let activeLinkId = this.hoverLink?.id || this.selectedLink?.id || null; - if (activeLinkId && !data.edgesById.has(activeLinkId)) activeLinkId = null; - const relatedNodes = new Set(); - const highlightedEdges = new Set(); - const labelNodes = new Set(); - const pinnedNodeIds = new Set(); - let hasPinnedActive = false; - let hasActiveLink = false; - - const addNodeHighlight = (nodeId, mode, pinned = false) => { - const visualNodeId = resolveCanvasNodeId(nodeId); - if (visualNodeId === null || visualNodeId === undefined) return null; - relatedNodes.add(visualNodeId); - labelNodes.add(visualNodeId); - if (pinned) pinnedNodeIds.add(visualNodeId); - if (hoverHighlightsNoteLinks(mode)) { - for (const edge of data.linkEdgesByNode.get(visualNodeId) || []) { - highlightedEdges.add(edge.key); - if (edge.source !== null && edge.source !== undefined) relatedNodes.add(edge.source); - if (edge.target !== null && edge.target !== undefined) relatedNodes.add(edge.target); - if (edge.source !== null && edge.source !== undefined) labelNodes.add(edge.source); - if (edge.target !== null && edge.target !== undefined) labelNodes.add(edge.target); - } - } - addHierarchyHoverHighlights(data, visualNodeId, mode, relatedNodes, highlightedEdges, labelNodes); - return visualNodeId; - }; - - const addLinkHighlight = (edgeId, fallbackEdge) => { - let edge = edgeId ? data.edgesById.get(edgeId) : null; - if (!edge && fallbackEdge) { - edge = this.findCanvasEdgeForPin({ - kind: "link", - edgeId: fallbackEdge.id, - source: fallbackEdge.source, - target: fallbackEdge.target - }); - } - if (!edge) return null; - highlightedEdges.add(edge.key); - if (edge.source !== null && edge.source !== undefined) { - relatedNodes.add(edge.source); - labelNodes.add(edge.source); - pinnedNodeIds.add(edge.source); - } - if (edge.target !== null && edge.target !== undefined) { - relatedNodes.add(edge.target); - labelNodes.add(edge.target); - pinnedNodeIds.add(edge.target); - } - return edge.key; - }; - - if (activeNodeId !== null && activeNodeId !== undefined) { - addNodeHighlight(activeNodeId, hoverMode, false); - } - - if (activeLinkId || this.hoverLink || this.selectedLink) { - hasActiveLink = addLinkHighlight(activeLinkId, this.hoverLink || this.selectedLink) !== null; - } - - for (const pin of this.pinnedPaths) { - if (pin.kind === "node") { - if (addNodeHighlight(pin.nodeId, pin.mode, true) !== null) hasPinnedActive = true; - } else if (pin.kind === "link") { - if (addLinkHighlight(pin.edgeId, pin) !== null) hasPinnedActive = true; - } - } - - for (const node of data.searchMatchItems || []) { - if (node.searchMatch) labelNodes.add(node.node.id); - } - - return { - hasActive: activeNodeId !== null && activeNodeId !== undefined || hasActiveLink || hasPinnedActive, - activeNodeId, - activeLinkId, - relatedNodes, - highlightedEdges, - labelNodes, - pinnedNodeIds - }; - } - - updateCanvasVisualState(active, zoom = 1, options = {}) { - const now = performance.now(); - const delta = this.lastCanvasFrameAt ? Math.min(80, now - this.lastCanvasFrameAt) : 16; - this.lastCanvasFrameAt = now; - const immediate = Boolean(options.immediate); - const step = immediate - ? 1 - : this.canvasVisualInitialized - ? clampFloat(delta / 72, 0.16, 0.86, 0.32) - : 1; - let needsFrame = false; - - const updateValue = (current, target) => { - if (!this.canvasVisualInitialized) return target; - const next = current + (target - current) * step; - if (!immediate && Math.abs(next - target) > 0.012) needsFrame = true; - return Math.abs(next - target) < 0.006 ? target : next; - }; - - const currentNodeIds = new Set(); - const hoverOnlyLabels = normalizeLabelVisibility(this.state.labelVisibility) === "hover"; - for (const item of this.canvasData.nodes) { - const nodeId = item.node.id; - currentNodeIds.add(nodeId); - const isFocused = nodeId === active.activeNodeId - || nodeId === this.selectedNodeId - || active.pinnedNodeIds.has(nodeId) - || nodeId === this.lastCanvasBundle.graph.focusId - || item.searchMatch; - const isRelated = active.relatedNodes.has(nodeId); - const directLabel = nodeId === active.activeNodeId - || nodeId === this.selectedNodeId - || active.pinnedNodeIds.has(nodeId) - || item.searchMatch; - const explicitLabel = directLabel || (!hoverOnlyLabels && active.labelNodes.has(nodeId)); - const target = { - focus: isFocused ? 1 : 0, - related: active.hasActive && isRelated && !isFocused ? 1 : 0, - dim: active.hasActive && !isRelated && !isFocused ? 1 : 0, - label: hoverOnlyLabels - ? (explicitLabel ? 1 : 0) - : Math.max(active.labelNodes.has(nodeId) ? 1 : 0, zoomLabelStrength(item, zoom, this.lastCanvasBundle.graph)) - }; - const current = this.canvasVisualNodes.get(nodeId) || { focus: 0, related: 0, dim: 0, label: 0 }; - this.canvasVisualNodes.set(nodeId, { - focus: updateValue(current.focus, target.focus), - related: updateValue(current.related, target.related), - dim: updateValue(current.dim, target.dim), - label: updateValue(current.label, target.label) - }); - } - - for (const id of Array.from(this.canvasVisualNodes.keys())) { - if (!currentNodeIds.has(id)) this.canvasVisualNodes.delete(id); - } - - const visibleHoverLinks = active.highlightedEdges.size - ? Array.from(active.highlightedEdges) - .map(key => this.canvasData.edgesById.get(key)) - .filter(item => item && item.hoverOnly) - : []; - const currentEdgeKeys = new Set(); - const updateEdge = item => { - if (!item) return; - currentEdgeKeys.add(item.key); - const target = { - highlight: active.highlightedEdges.has(item.key) ? 1 : 0, - dim: active.hasActive && !active.highlightedEdges.has(item.key) ? 1 : 0 - }; - const current = this.canvasVisualEdges.get(item.key) || { highlight: 0, dim: 0 }; - this.canvasVisualEdges.set(item.key, { - highlight: updateValue(current.highlight, target.highlight), - dim: updateValue(current.dim, target.dim) - }); - }; - for (const item of this.canvasData.hierarchy) updateEdge(item); - for (const item of this.canvasData.links) updateEdge(item); - for (const item of visibleHoverLinks) updateEdge(item); - - for (const key of Array.from(this.canvasVisualEdges.keys())) { - if (!currentEdgeKeys.has(key)) this.canvasVisualEdges.delete(key); - } - - this.canvasVisualInitialized = true; - if (needsFrame) this.scheduleCanvasAnimation(); - - return { - nodes: this.canvasVisualNodes, - edges: this.canvasVisualEdges - }; - } - - scheduleCanvasAnimation() { - this.requestCanvasDraw("full"); - } - - requestCanvasDraw(mode = "full") { - if (mode === "interactive") { - this.canvasInteractionUntil = performance.now() + 120; - } - - if (this.canvasAnimationFrame) { - this.nextCanvasDrawMode = mode === "full" ? "full" : "interactive"; - return; - } - - this.nextCanvasDrawMode = mode; - this.canvasAnimationFrame = window.requestAnimationFrame(() => { - const drawMode = this.nextCanvasDrawMode || "full"; - this.canvasAnimationFrame = null; - this.nextCanvasDrawMode = "full"; - this.drawCanvasGraph({ mode: drawMode }); - }); - } - - scheduleSettledCanvasDraw(delay = 140) { - if (this.canvasSettleTimer) window.clearTimeout(this.canvasSettleTimer); - this.canvasSettleTimer = window.setTimeout(() => { - this.canvasSettleTimer = null; - this.requestCanvasDraw("full"); - }, delay); - } - - drawCanvasRings(ctx, layout, palette, zoom, options = {}) { - if (!layout || !Array.isArray(layout.rings) || !layout.rings.length) return; - const centerX = Number.isFinite(layout.centerX) ? layout.centerX : layout.width / 2; - const centerY = Number.isFinite(layout.centerY) ? layout.centerY : layout.height / 2; - const alphaScale = options.overlay ? 1.16 : options.underlay ? 0.58 : 1; - const swirlStrength = clampFloat(layout.swirlStrength, 0, 1, 0); - const spinSpeed = this.canvasSwirlAmount(); - const elapsedSeconds = this.canvasSwirlStartedAt && Number.isFinite(options.now) - ? Math.max(0, (options.now - this.canvasSwirlStartedAt) / 1000) - : 0; - - ctx.save(); - ctx.strokeStyle = palette.ringGuide || palette.folderRing || palette.line; - - for (const ring of layout.rings) { - if (!Number.isFinite(ring.radius) || ring.radius <= 0) continue; - const density = Math.min(1, Math.sqrt(Math.max(1, ring.count || 1)) / 9); - const ringPhase = swirlOrbitAngleForRing(ring.depth, ring.radius, spinSpeed, elapsedSeconds); - if (options.overlay) { - ctx.setLineDash([]); - ctx.globalAlpha = (0.035 + density * 0.025) * alphaScale; - ctx.lineWidth = Math.max(4.5 / zoom, 6.5 / zoom); - ctx.beginPath(); - drawCanvasRingPath(ctx, centerX, centerY, ring.radius, ring.depth, swirlStrength, ringPhase); - ctx.stroke(); - } - - ctx.setLineDash([options.overlay ? 5 / zoom : 3 / zoom, options.overlay ? 6 / zoom : 11 / zoom]); - ctx.globalAlpha = (0.13 + density * 0.09) * alphaScale; - ctx.lineWidth = Math.max((options.overlay ? 1.05 : 0.55) / zoom, (options.overlay ? 1.55 : 0.86) / zoom); - ctx.beginPath(); - drawCanvasRingPath(ctx, centerX, centerY, ring.radius, ring.depth, swirlStrength, ringPhase); - ctx.stroke(); - } - - ctx.restore(); - } - - drawCanvasEdges(ctx, edges, visuals, palette, zoom, hierarchy, options = {}) { - const hoverOnly = Boolean(options.hoverOnly); - const bounds = options.bounds || null; - const interactive = Boolean(options.interactive); - for (const item of edges) { - if (bounds && !edgeItemInBounds(item, bounds)) continue; - const visual = visuals.edges.get(item.key) || { highlight: 0, dim: 0 }; - if (hoverOnly && visual.highlight <= 0.01) continue; - const unresolved = item.edge && item.edge.unresolvedCount; - const external = item.external || (item.edge && item.edge.externalCount); - const externalHierarchy = hierarchy && external; - const outsideLink = !hierarchy && external; - const color = unresolved - ? palette.unresolved - : outsideLink - ? (palette.externalLink || palette.external) - : hierarchy - ? (palette.tree || palette.folderRing || palette.line) - : palette.link; - const spinLinkFade = !hierarchy && !hoverOnly && this.canvasSwirlMotionActive() ? 0.58 : 1; - const baseAlpha = (hierarchy - ? (externalHierarchy ? 0.18 : 0.34) - : (outsideLink ? 0.082 : 0.056)) * spinLinkFade; - const minAlpha = (hierarchy ? 0.09 : outsideLink ? 0.03 : 0.024) * spinLinkFade; - const dimFade = hierarchy ? 0.48 : 0.58; - const width = hierarchy - ? (externalHierarchy ? 1.12 : 1.48) - : (outsideLink ? item.width * 0.72 : item.width * 0.7); - const applyDash = () => { - ctx.setLineDash([]); - if (unresolved) ctx.setLineDash([7 / zoom, 5 / zoom]); - else if (outsideLink) ctx.setLineDash([8 / zoom, 7 / zoom]); - else if (externalHierarchy) ctx.setLineDash([2.5 / zoom, 5 / zoom]); - }; - - ctx.save(); - if (!hoverOnly) { - ctx.globalAlpha = Math.max(minAlpha, baseAlpha * (1 - visual.dim * dimFade)); - ctx.strokeStyle = color; - ctx.lineWidth = Math.max((outsideLink ? 0.58 : 0.45) / zoom, width / zoom); - ctx.lineCap = "round"; - if (!interactive || outsideLink || unresolved) applyDash(); - else ctx.setLineDash([]); - ctx.beginPath(); - this.drawCanvasEdgePath(ctx, item); - ctx.stroke(); - } - if (visual.highlight > 0.01) { - applyDash(); - ctx.globalAlpha = visual.highlight * (hierarchy ? 0.78 : outsideLink ? 0.76 : hoverOnly ? 0.74 : 0.88); - ctx.strokeStyle = outsideLink ? (palette.externalLink || palette.external) : (palette.lineHighlight || palette.focus); - ctx.lineWidth = Math.max(0.7 / zoom, (hierarchy ? 2.2 : outsideLink ? 2.1 : hoverOnly ? 1.9 : 2.35) / zoom); - ctx.shadowColor = outsideLink ? (palette.externalLink || palette.external) : palette.glow; - ctx.shadowBlur = (outsideLink ? 6 : 8) / zoom; - ctx.beginPath(); - this.drawCanvasEdgePath(ctx, item); - ctx.stroke(); - if (!hierarchy && this.canvasData?.showArrow) { - this.drawCanvasArrow(ctx, item, zoom, outsideLink ? (palette.externalLink || palette.external) : (palette.lineHighlight || palette.focus)); - } - } - ctx.restore(); - } - } - - drawCanvasEdgePath(ctx, item) { - const source = item.sourcePoint; - const target = item.targetPoint; - const route = item.dynamicRoute || item.route; - - ctx.moveTo(source.x, source.y); - - if (route && (route.kind === "outer" || route.kind === "external" || route.kind === "dynamic-orbit")) { - const startAngle = route.sourceAngle; - const endAngle = Number.isFinite(route.endAngle) ? route.endAngle : route.targetAngle; - const arcStart = radialPoint(route.centerX, route.centerY, route.radius, startAngle); - const arcEnd = radialPoint(route.centerX, route.centerY, route.radius, endAngle); - ctx.lineTo(arcStart.x, arcStart.y); - ctx.arc(route.centerX, route.centerY, route.radius, startAngle, endAngle, endAngle < startAngle); - ctx.lineTo(arcEnd.x, arcEnd.y); - ctx.lineTo(target.x, target.y); - return; - } - - if (route && route.kind === "curve") { - const centerX = Number.isFinite(source.centerX) ? source.centerX : (source.x + target.x) / 2; - const centerY = Number.isFinite(source.centerY) ? source.centerY : (source.y + target.y) / 2; - const midX = (source.x + target.x) / 2; - const midY = (source.y + target.y) / 2; - const pull = Number.isFinite(route.curveStrength) ? route.curveStrength : (item.external ? 0.28 : 0.16); - ctx.quadraticCurveTo( - midX + (centerX - midX) * pull, - midY + (centerY - midY) * pull, - target.x, - target.y - ); - return; - } - - ctx.lineTo(target.x, target.y); - } - - drawCanvasArrow(ctx, item, zoom, color) { - const source = this.canvasArrowSourcePoint(item); - const target = item.targetPoint; - const dx = target.x - source.x; - const dy = target.y - source.y; - const length = Math.hypot(dx, dy); - if (length < 0.001) return; - - const targetNode = this.canvasData?.nodesById?.get(item.target); - const targetRadius = targetNode ? targetNode.radius : 6; - const angle = Math.atan2(dy, dx); - const tipOffset = targetRadius + 2 / zoom; - const tipX = target.x - Math.cos(angle) * tipOffset; - const tipY = target.y - Math.sin(angle) * tipOffset; - const size = 7 / zoom; - const spread = 0.48; - - ctx.save(); - ctx.fillStyle = color; - ctx.shadowBlur = 0; - ctx.beginPath(); - ctx.moveTo(tipX, tipY); - ctx.lineTo( - tipX - Math.cos(angle - spread) * size, - tipY - Math.sin(angle - spread) * size - ); - ctx.lineTo( - tipX - Math.cos(angle + spread) * size, - tipY - Math.sin(angle + spread) * size - ); - ctx.closePath(); - ctx.fill(); - ctx.restore(); - } - - canvasArrowSourcePoint(item) { - const route = item.dynamicRoute || item.route; - if (route && (route.kind === "outer" || route.kind === "external" || route.kind === "dynamic-orbit")) { - const endAngle = Number.isFinite(route.endAngle) ? route.endAngle : route.targetAngle; - return radialPoint(route.centerX, route.centerY, route.radius, endAngle); - } - - if (route && route.kind === "curve") { - const source = item.sourcePoint; - const target = item.targetPoint; - const centerX = Number.isFinite(source.centerX) ? source.centerX : (source.x + target.x) / 2; - const centerY = Number.isFinite(source.centerY) ? source.centerY : (source.y + target.y) / 2; - const midX = (source.x + target.x) / 2; - const midY = (source.y + target.y) / 2; - const pull = Number.isFinite(route.curveStrength) ? route.curveStrength : (item.external ? 0.28 : 0.16); - return { - x: midX + (centerX - midX) * pull, - y: midY + (centerY - midY) * pull - }; - } - - return item.sourcePoint; - } - - drawCanvasNodes(ctx, nodes, visuals, palette, zoom, options = {}) { - const bounds = options.bounds || null; - for (const item of nodes) { - if (bounds && !circleInBounds(item.point, item.radius + 6 / zoom, bounds)) continue; - const nodeId = item.node.id; - const visual = visuals.nodes.get(nodeId) || { focus: 0, related: 0, dim: 0, label: 0 }; - const rootNode = nodeId === this.lastCanvasBundle.graph.rootId; - const folderNode = item.node.type === "folder"; - const folderWithMeta = folderNode && Boolean(item.node.representativeFile); - const externalGroup = item.node.type === "external" && !item.node.externalProxy; - const externalFile = Boolean(item.node.externalProxy); - const unresolvedNode = item.node.type === "unresolved"; - const alpha = clampFloat(0.9 - visual.dim * 0.58 + visual.related * 0.1, 0.24, 1, 0.9); - const fill = unresolvedNode - ? palette.unresolved - : externalGroup - ? palette.node - : folderWithMeta - ? palette.folderMeta - : folderNode - ? palette.folder - : (palette.note || palette.node); - const stroke = rootNode - ? palette.circle - : unresolvedNode - ? palette.unresolved - : externalGroup || externalFile - ? palette.external - : folderNode - ? palette.folderRing - : palette.fileRing || palette.stroke; - - ctx.save(); - ctx.globalAlpha = externalGroup ? alpha * 0.82 : alpha; - ctx.shadowColor = palette.glow; - ctx.shadowBlur = (visual.focus * 13 + (rootNode ? 3 : 0)) / zoom; - ctx.fillStyle = fill; - ctx.beginPath(); - ctx.arc(item.point.x, item.point.y, item.radius, 0, Math.PI * 2); - ctx.fill(); - if (visual.focus > 0.01) { - ctx.globalAlpha = visual.focus * 0.96; - ctx.fillStyle = palette.focus; - ctx.beginPath(); - ctx.arc(item.point.x, item.point.y, item.radius, 0, Math.PI * 2); - ctx.fill(); - } - ctx.shadowBlur = 0; - ctx.globalAlpha = clampFloat(alpha + visual.focus * 0.28, 0.12, 1, alpha); - ctx.strokeStyle = visual.focus > 0.08 ? palette.circle : stroke; - ctx.lineWidth = (0.85 + visual.focus * 1.35 + (rootNode ? 0.55 : folderNode ? 0.22 : 0)) / zoom; - if (externalGroup || externalFile || unresolvedNode) ctx.setLineDash([3 / zoom, 4 / zoom]); - ctx.stroke(); - ctx.setLineDash([]); - ctx.restore(); - } - } - - drawCanvasLabels(ctx, nodes, visuals, palette, zoom, options = {}) { - const fontSize = 12; - const lineHeight = 14; - const screenScale = labelScreenScale(zoom); - const bounds = options.bounds || null; - const interactive = Boolean(options.interactive); - const fastInteractive = Boolean(options.fastInteractive); - ctx.save(); - ctx.textBaseline = "top"; - ctx.textAlign = "center"; - ctx.lineJoin = "round"; - - let labelItems = interactive - ? (this.canvasData?.interactiveLabelItems || nodes) - : (this.canvasData?.labelItems || nodes); - if (interactive) { - const extraIds = [this.selectedNodeId, this.hoverNodeId, this.lastCanvasBundle?.graph?.focusId, ...this.pinnedCanvasLabelIds()] - .filter(id => id !== null && id !== undefined); - if (extraIds.length) { - const seen = new Set(labelItems.map(item => item.node.id)); - const extras = extraIds - .filter(id => !seen.has(id) && this.canvasData?.nodesById?.has(id)) - .map(id => this.canvasData.nodesById.get(id)); - if (extras.length) labelItems = labelItems.concat(extras); - } - } - - const pinnedLabelIds = new Set(this.pinnedCanvasLabelIds()); - for (const item of labelItems) { - if (bounds && !circleInBounds(item.point, item.radius + 180 / zoom, bounds)) continue; - if ( - interactive - && !item.searchMatch - && item.labelRank > FAST_CANVAS_LABEL_LIMIT - && item.node.id !== this.selectedNodeId - && item.node.id !== this.hoverNodeId - && !pinnedLabelIds.has(item.node.id) - ) continue; - const visual = visuals.nodes.get(item.node.id) || { label: 0, focus: 0 }; - if (visual.label <= 0.02) continue; - if (fastInteractive && visual.focus <= 0.02 && !item.searchMatch && item.labelRank > 36) continue; - const rootNode = item.node.id === this.lastCanvasBundle.graph.rootId; - const folderNode = item.node.type === "folder"; - const leading = Number.isFinite(item.labelRank) && item.labelRank < 36; - const localScale = screenScale * (rootNode ? 1.18 : item.radius >= 24 ? 1.1 : item.radius >= 15 ? 1.04 : 1); - const weight = rootNode || leading || folderNode ? 600 : 400; - ctx.font = `${weight} ${(fontSize * localScale) / zoom}px ${palette.fontFamily}`; - const maxWidth = ((rootNode ? 190 : folderNode ? 156 : leading ? 146 : 124) * localScale) / zoom; - const maxLines = rootNode || visual.focus > 0.5 ? 4 : 3; - const lines = wrapCanvasLabel(ctx, item.label, maxWidth, maxLines); - if (!lines.length) continue; - - const x = item.point.x; - const y = item.point.y + item.radius + (8 * localScale) / zoom; - const lineHeightWorld = (lineHeight * localScale) / zoom; - ctx.globalAlpha = visual.label * (rootNode ? 0.98 : leading ? 0.95 : 0.9); - ctx.lineWidth = ((rootNode ? 6 : 5) * localScale) / zoom; - ctx.strokeStyle = palette.labelStroke || palette.bg; - for (let index = 0; index < lines.length; index += 1) { - ctx.strokeText(lines[index], x, y + index * lineHeightWorld); - } - ctx.fillStyle = rootNode ? (palette.circle || palette.labelText || palette.text) : (palette.labelText || palette.text); - for (let index = 0; index < lines.length; index += 1) { - ctx.fillText(lines[index], x, y + index * lineHeightWorld); - } - } - - ctx.restore(); - } - - installPanelResize() { - if (!this.splitter) return; - this.splitter.addEventListener("mousedown", evt => { - evt.preventDefault(); - const startX = evt.clientX; - const startWidth = this.state.sidePanelWidth; - this.bodyEl.classList.add("is-resizing"); - - const onMove = moveEvt => { - const delta = moveEvt.clientX - startX; - this.state.sidePanelWidth = clampNumber(startWidth - delta, 220, 720, 360); - this.bodyEl.style.setProperty("--mwm-side-width", `${this.state.sidePanelWidth}px`); - }; - - const onUp = () => { - this.bodyEl.classList.remove("is-resizing"); - window.removeEventListener("mousemove", onMove); - window.removeEventListener("mouseup", onUp); - }; - - window.addEventListener("mousemove", onMove); - window.addEventListener("mouseup", onUp); - }); - } - - adjustZoom(delta, rerender = true) { - this.cancelCanvasZoomAnimation(); - const viewport = this.canvasViewport(); - const minZoom = this.canvasMinZoom(viewport); - const maxZoom = this.canvasMaxZoom(viewport); - const previousZoom = clampFloat(this.state.zoom, minZoom, maxZoom, 1); - this.state.zoom = clampFloat(previousZoom * Math.pow(ZOOM_BUTTON_STEP, delta), minZoom, maxZoom, 1); - if (!rerender) return; - if (this.canvas && this.lastCanvasBundle) { - const center = { x: viewport.width / 2, y: viewport.height / 2 }; - const world = { - x: (center.x - this.canvasPanX) / previousZoom, - y: (center.y - this.canvasPanY) / previousZoom - }; - this.canvasPanX = center.x - world.x * this.state.zoom; - this.canvasPanY = center.y - world.y * this.state.zoom; - this.syncZoomControls(); - this.requestCanvasDraw("interactive"); - this.scheduleSettledCanvasDraw(); - return; - } - this.render(); - } - - fitToView() { - this.cancelCanvasZoomAnimation(); - if (!this.lastLayout || !this.graphHost) return; - const viewport = this.canvasViewport(); - this.state.zoom = clampFloat( - fitZoomForLayout(this.lastLayout, viewport, 32), - this.canvasMinZoom(viewport, this.lastLayout), - this.canvasMaxZoom(viewport, this.lastLayout), - 1 - ); - if (this.canvas && this.lastCanvasBundle) { - this.centerCanvasView(this.lastLayout, viewport, null); - this.syncZoomControls(); - this.requestCanvasDraw("full"); - return; - } - this.render(); - } - - resetViewZoom() { - this.cancelCanvasZoomAnimation(); - this.viewInitialized = false; - this.state.zoom = 1; - this.render(); - } - - registerEdgeElement(element, edge) { - if (!element || !edge) return; - if (edge.id) this.edgeElementsById.set(edge.id, element); - for (const id of [edge.source, edge.target]) { - if (!id) continue; - if (!this.edgeElementsByNode.has(id)) this.edgeElementsByNode.set(id, []); - this.edgeElementsByNode.get(id).push(element); - } - } - - markHighlight(element, className) { - if (!element) return; - element.classList.add(className); - this.activeHighlightElements.add(element); - } - - setHoverNode(nodeId) { - this.hoverNodeId = nodeId; - this.hoverLink = null; - if (this.graphHost) this.graphHost.classList.add("is-hovering"); - this.requestCanvasDraw("full"); - } - - highlightNode(nodeId) { - this.setHoverNode(nodeId); - } - - setHoverLink(edge, element) { - this.hoverNodeId = null; - this.hoverLink = edge || null; - if (this.graphHost) this.graphHost.classList.toggle("is-hovering", Boolean(edge)); - this.requestCanvasDraw("full"); - } - - highlightLink(edge, element) { - this.setHoverLink(edge); - } - - clearHover() { - this.clearHoverClasses(); - if (this.graphHost) this.graphHost.classList.remove("is-hovering"); - if (this.graphHost) this.graphHost.classList.remove("is-pointing"); - this.hoverNodeId = null; - this.hoverLink = null; - this.requestCanvasDraw("full"); - } - - clearHoverClasses() { - for (const element of this.activeHighlightElements) { - element.classList.remove("is-highlighted", "is-related", "is-hovered"); - } - this.activeHighlightElements.clear(); - } - - findLinkElement(edge) { - if (!edge) return null; - return this.edgeElementsById.get(edge.id) || null; - } - - applyPersistentHighlight() { - if (!this.graphHost) return; - this.clearHoverClasses(); - this.graphHost.classList.toggle( - "is-hovering", - this.hoverNodeId !== null && this.hoverNodeId !== undefined || Boolean(this.hoverLink) - ); - this.requestCanvasDraw("full"); - } - - showNodeMenu(evt, node) { - const menu = new Menu(); - if (node.type === "note") { - menu.addItem(item => item - .setTitle("Open note") - .setIcon("file-text") - .onClick(() => this.openNode(node.id, evt))); - menu.addItem(item => item - .setTitle("Focus note") - .setIcon("locate-fixed") - .onClick(() => { - this.state.mode = "focus"; - this.state.focusPath = node.id; - this.state.showCompleteRoot = false; - this.viewInitialized = false; - this.resetAdaptiveTuning(); - this.render(); - })); - } - if (node.type === "folder") { - menu.addItem(item => item - .setTitle("Use as atlas root") - .setIcon("folder-open") - .onClick(() => { - this.state.mode = "atlas"; - this.state.rootPath = node.id; - this.state.showCompleteRoot = false; - this.viewInitialized = false; - this.resetAdaptiveTuning(); - this.render(); - })); - if (node.representativeFile) { - menu.addItem(item => item - .setTitle("Open representative note") - .setIcon("file-text") - .onClick(() => this.openNode(node.representativeFile, evt))); - } - } - menu.showAtMouseEvent(evt); - } - - renderSidePanel(index, graph, forcedNodeId) { - const focusState = this.capturePanelFocus(); - this.sidePanel.empty(); - this.clearControlRefs(); - - if (["detail", "links", "layout", "legend"].includes(this.state.sidePage)) { - this.state.sidePage = "controls"; - } - const pages = this.sidePanelPages(); - if (!pages.some(([id]) => id === this.state.sidePage)) this.state.sidePage = "inspect"; - - const header = this.sidePanel.createDiv({ cls: "mwm-panel-header" }); - const titleWrap = header.createDiv({ cls: "mwm-panel-title-wrap" }); - titleWrap.createDiv({ cls: "mwm-panel-title", text: "Mini World Map" }); - titleWrap.createDiv({ - cls: "mwm-panel-subtitle", - text: `${this.state.mode} - ${graph?.nodes?.length || 0} nodes` - }); - - const tabs = this.sidePanel.createDiv({ cls: "mwm-page-tabs" }); - for (const [pageId, label, icon] of pages) { - const active = this.state.sidePage === pageId; - const button = tabs.createEl("button", { - cls: active ? "mwm-page-tab is-active" : "mwm-page-tab", - attr: { type: "button", title: label, "aria-pressed": active ? "true" : "false" } - }); - setIcon(button, icon); - button.createSpan({ text: label }); - button.addEventListener("click", () => { - this.state.sidePage = pageId; - this.renderSidePanel(index, graph, forcedNodeId); - }); - } - - const content = this.sidePanel.createDiv({ cls: `mwm-panel-content mwm-panel-content-${this.state.sidePage}` }); - this.sidePanelContent = content; - try { - if (this.state.sidePage === "view") this.renderViewPage(index, graph); - else if (this.state.sidePage === "pins") this.renderPinsPage(index, graph); - else if (this.state.sidePage === "controls") this.renderControlsPage(index, graph); - else if (this.state.sidePage === "defaults") this.renderDefaultsPage(index, graph); - else this.renderInspectPage(index, graph, forcedNodeId); - } finally { - this.sidePanelContent = null; - } - this.restorePanelFocus(focusState); - } - - renderViewPage(index, graph) { - const panel = this.getSidePanelTarget(); - this.createPanelPageTitle(panel, "View"); - - const search = this.createPanelSection(panel, "Search"); - this.searchInput = this.createPanelText( - search, - "search", - "Search", - this.state.search, - "Notes and folders", - value => { - this.disableCompleteRoot(); - this.state.search = value.trim(); - this.scheduleRender(120, { preservePanel: true }); - } - ); - - const modeActions = this.createPanelSection(panel, "Mode"); - const modeGrid = modeActions.createDiv({ cls: "mwm-panel-action-grid" }); - this.createPanelAction(modeGrid, "network", "Atlas", () => { - this.state.mode = "atlas"; - this.state.showCompleteRoot = false; - this.viewInitialized = false; - this.resetAdaptiveTuning(); - this.render(); - }, this.state.mode === "atlas"); - this.createPanelAction(modeGrid, "locate-fixed", "Focus", () => { - this.state.mode = "focus"; - this.state.focusPath = this.plugin.index.getActiveNotePath(); - this.state.showCompleteRoot = false; - this.viewInitialized = false; - this.resetAdaptiveTuning(); - this.render(); - }, this.state.mode === "focus"); - this.createPanelAction(modeGrid, "home", "Vault root", () => { - this.resetToVaultRoot(); - }); - const currentRoot = graph?.rootId !== null && graph?.rootId !== undefined - ? index.nodes.get(graph.rootId) - : index.nodes.get(this.state.rootPath); - const parentRootId = currentRoot && currentRoot.parentId !== null && currentRoot.parentId !== undefined - ? currentRoot.parentId - : null; - const parentButton = this.createPanelAction(modeGrid, "arrow-up", "Parent root", () => { - if (parentRootId === null) return; - this.state.rootPath = parentRootId; - this.state.mode = "atlas"; - this.state.showCompleteRoot = false; - this.viewInitialized = false; - this.resetAdaptiveTuning(); - this.render(); - }); - if (parentRootId === null) parentButton.disabled = true; - this.createPanelAction( - modeGrid, - this.state.showCompleteRoot ? "x" : "route", - this.state.showCompleteRoot ? "Exit complete" : "Complete root", - () => { - if (this.state.showCompleteRoot) { - this.exitCompleteRoot(); - this.resetAdaptiveTuning(); - this.viewInitialized = false; - this.render(); - } else { - this.showCompleteCurrentRoot(); - } - }, - this.state.showCompleteRoot - ); - - const appearance = this.createPanelSection(panel, "Appearance"); - const appearanceGrid = appearance.createDiv({ cls: "mwm-panel-action-grid mwm-panel-action-grid-three" }); - this.createPanelAction(appearanceGrid, "monitor", "Auto", () => this.setColorScheme("auto"), this.state.colorScheme === "auto"); - this.createPanelAction(appearanceGrid, "sun", "Day", () => this.setColorScheme("day"), this.state.colorScheme === "day"); - this.createPanelAction(appearanceGrid, "moon", "Night", () => this.setColorScheme("night"), this.state.colorScheme === "night"); - - const commands = this.createPanelSection(panel, "Commands"); - const commandGrid = commands.createDiv({ cls: "mwm-panel-action-grid" }); - this.createPanelAction(commandGrid, "refresh-cw", "Rebuild", () => { - this.plugin.rebuildIndex("manual"); - }); - this.createPanelAction(commandGrid, "maximize-2", "Full screen", () => this.toggleFullscreen(), this.state.fullscreen); - } - - renderPinsPage(index, graph) { - const panel = this.getSidePanelTarget(); - this.createPanelPageTitle(panel, "Pins"); - - const actions = this.createPanelSection(panel, "Actions"); - const actionGrid = actions.createDiv({ cls: "mwm-panel-action-grid" }); - this.createPanelAction(actionGrid, "pin", "Pin hover", () => this.pinCurrentHoverPath()); - const clearButton = this.createPanelAction(actionGrid, "trash-2", "Clear pins", () => this.clearPinnedPaths()); - clearButton.disabled = this.pinnedPaths.length === 0; - - const grouping = this.createPanelSection(panel, "Group"); - this.pinGroupInput = this.createPanelText( - grouping, - "pin-group-name", - "Name", - this.pinGroupName, - "Group name", - value => { - this.pinGroupName = value; - } - ); - const groupGrid = grouping.createDiv({ cls: "mwm-panel-action-grid" }); - const groupButton = this.createPanelAction(groupGrid, "folder-plus", "Group selected", () => this.groupSelectedPinnedPaths()); - groupButton.disabled = this.selectedPinIds.size === 0; - - const listSection = this.createPanelSection(panel, `Pinned Paths (${this.pinnedPaths.length})`); - if (!this.pinnedPaths.length) { - listSection.createDiv({ cls: "mwm-side-muted", text: "No pinned paths." }); - return; - } - - const groupsById = new Map(this.pinGroups.map(group => [group.id, group])); - const pinsByGroup = new Map(); - const ungrouped = []; - for (const pin of this.pinnedPaths) { - if (!pin.groupId || !groupsById.has(pin.groupId)) { - ungrouped.push(pin); - continue; - } - if (!pinsByGroup.has(pin.groupId)) pinsByGroup.set(pin.groupId, []); - pinsByGroup.get(pin.groupId).push(pin); - } - - if (ungrouped.length) { - this.renderPinnedPathGroup(listSection, index, graph, null, ungrouped); - } - for (const group of this.pinGroups) { - const pins = pinsByGroup.get(group.id) || []; - if (pins.length) this.renderPinnedPathGroup(listSection, index, graph, group, pins); - } - } - - renderPinnedPathGroup(parent, index, graph, group, pins) { - const wrapper = parent.createDiv({ cls: "mwm-pin-group" }); - const header = wrapper.createDiv({ cls: "mwm-pin-group-header" }); - const title = header.createDiv({ cls: "mwm-pin-group-title" }); - title.createSpan({ text: group ? group.name : "Ungrouped" }); - title.createSpan({ cls: "mwm-pin-count", text: String(pins.length) }); - if (group) { - this.createIconButton(header, "folder-minus", "Ungroup all", () => this.removePinGroup(group.id), "mwm-pin-icon-button"); - } - - const list = wrapper.createDiv({ cls: "mwm-pin-list" }); - for (const pin of pins) { - this.renderPinnedPathItem(list, index, graph, pin); - } - } - - renderPinnedPathItem(parent, index, graph, pin) { - const row = parent.createDiv({ cls: "mwm-pin-row" }); - const checkbox = row.createEl("input", { - cls: "mwm-pin-check", - attr: { type: "checkbox", title: "Select for grouping", "aria-label": "Select for grouping" } - }); - checkbox.checked = this.selectedPinIds.has(pin.id); - checkbox.addEventListener("change", () => { - if (checkbox.checked) this.selectedPinIds.add(pin.id); - else this.selectedPinIds.delete(pin.id); - this.renderSidePanel(index, graph); - }); - - const main = row.createEl("button", { - cls: "mwm-pin-main", - attr: { type: "button", title: pin.path || pin.title } - }); - main.addEventListener("click", () => this.centerPinnedPath(pin)); - main.createDiv({ cls: "mwm-pin-title", text: pin.title || "Pinned path" }); - const kind = pin.kind === "link" ? "link" : hoverHighlightModeLabel(pin.mode); - main.createDiv({ cls: "mwm-pin-meta", text: `${kind} - ${pin.path || "/"}` }); - - const actions = row.createDiv({ cls: "mwm-pin-actions" }); - this.createIconButton(actions, "locate-fixed", "Locate", () => this.centerPinnedPath(pin), "mwm-pin-icon-button"); - this.createIconButton(actions, "info", "Inspect", () => this.inspectPinnedPath(pin, index, graph), "mwm-pin-icon-button"); - if (pin.groupId) { - this.createIconButton(actions, "folder-minus", "Ungroup", () => this.ungroupPinnedPath(pin.id), "mwm-pin-icon-button"); - } - this.createIconButton(actions, "x", "Remove", () => this.removePinnedPath(pin.id), "mwm-pin-icon-button"); - } - - renderControlsPage(index, graph) { - const panel = this.getSidePanelTarget(); - this.createPanelPageTitle(panel, "Controls"); - const createPanelPageTitle = this.createPanelPageTitle; - this.createPanelPageTitle = () => {}; - try { - this.renderDetailPage(index, graph); - this.renderLinksPage(index, graph); - this.renderLayoutPage(index, graph); - } finally { - this.createPanelPageTitle = createPanelPageTitle; - } - this.renderLegendControls(panel); - this.renderLegend(false); - } - - renderDetailPage(index, graph) { - const panel = this.getSidePanelTarget(); - this.createPanelPageTitle(panel, "Detail"); - - const budgets = this.createPanelSection(panel, "Budgets"); - this.depthInput = this.createPanelNumber( - budgets, - "atlas-depth", - "Depth", - this.state.atlasDepth, - { min: "1", max: String(MAX_ATLAS_DEPTH) }, - value => { - this.disableAutoDetail(); - this.disableCompleteRoot(); - this.state.atlasDepth = clampNumber(value, 1, MAX_ATLAS_DEPTH, this.plugin.settings.atlasDepth); - this.renderMapOnly(); - } - ); - this.nodeInput = this.createPanelNumber( - budgets, - "node-limit", - "Nodes", - this.state.nodeLimit, - { min: "200", max: String(MAX_RENDER_NODE_LIMIT), step: "100" }, - value => { - this.disableAutoDetail(); - this.disableCompleteRoot(); - this.state.nodeLimit = clampNumber(value, 200, MAX_RENDER_NODE_LIMIT, this.plugin.settings.renderNodeLimit); - this.renderMapOnly(); - } - ); - this.linkInput = this.createPanelNumber( - budgets, - "link-limit", - "Link overlays", - this.state.linkLimit, - { min: "0", max: String(MAX_LINK_LIMIT) }, - value => { - this.disableAutoDetail(); - this.disableCompleteRoot(); - this.state.linkLimit = clampNumber(value, 0, MAX_LINK_LIMIT, this.plugin.settings.linkLimit); - this.renderMapOnly(); - } - ); - - const automation = this.createPanelSection(panel, "Automation"); - this.autoToggle = this.createPanelToggle(automation, "auto-detail", "Adaptive detail", this.state.autoDetail, checked => { - this.state.autoDetail = checked; - if (this.state.autoDetail) this.disableCompleteRoot(); - this.adaptiveInitialized = false; - this.autoTuneCount = 0; - this.renderMapOnly(); - }); - } - - renderLinksPage(index, graph) { - const panel = this.getSidePanelTarget(); - this.createPanelPageTitle(panel, "Links"); - - const overlays = this.createPanelSection(panel, "Overlay"); - this.linkHoverSelect = this.createPanelSelect( - overlays, - "hover-highlight-mode", - "Hover highlight", - normalizeHoverHighlightMode(this.state.hoverHighlightMode), - HOVER_HIGHLIGHT_MODE_OPTIONS, - value => { - const previousUsesHoverLinks = hoverHighlightsNoteLinks(this.state.hoverHighlightMode); - this.state.hoverHighlightMode = normalizeHoverHighlightMode(value); - this.state.enableLinkHover = hoverHighlightsNoteLinks(this.state.hoverHighlightMode); - this.hoverLink = null; - if (previousUsesHoverLinks !== this.state.enableLinkHover) this.renderMapOnly(); - else this.applyPersistentHighlight(); - } - ); - - const external = this.createPanelSection(panel, "Outside Root"); - this.externalToggle = this.createPanelToggle(external, "show-external-links", "Show external links", this.state.showExternalLinks, checked => { - this.disableAutoDetail(); - this.disableCompleteRoot(); - this.state.showExternalLinks = checked; - this.renderMapOnly(); - }); - this.externalModeSelect = this.createPanelSelect( - external, - "external-detail-mode", - "Outside detail", - this.state.externalDetailMode, - [ - ["grouped", "Groups"], - ["selected", "Selected"], - ["exact", "Exact"] - ], - value => { - this.disableAutoDetail(); - this.disableCompleteRoot(); - this.state.externalDetailMode = value; - this.renderMapOnly(); - } - ); - this.externalLimitInput = this.createPanelNumber( - external, - "external-link-anchor-limit", - "Exact outside files", - this.state.externalLinkAnchorLimit, - { min: "0", max: String(MAX_EXTERNAL_LINK_ANCHOR_LIMIT) }, - value => { - this.disableAutoDetail(); - this.disableCompleteRoot(); - this.state.externalLinkAnchorLimit = clampNumber(value, 0, MAX_EXTERNAL_LINK_ANCHOR_LIMIT, this.plugin.settings.externalLinkAnchorLimit); - this.renderMapOnly(); - } - ); - } - - renderLayoutPage(index, graph) { - const panel = this.getSidePanelTarget(); - this.createPanelPageTitle(panel, "Layout"); - - const zoom = this.createPanelSection(panel, "Zoom"); - const zoomActions = zoom.createDiv({ cls: "mwm-panel-action-row" }); - this.createPanelAction(zoomActions, "zoom-out", "Out", () => this.adjustZoom(-1)); - this.createPanelAction(zoomActions, "zoom-in", "In", () => this.adjustZoom(1)); - this.createPanelAction(zoomActions, "maximize", "Fit", () => this.fitToView()); - this.createPanelAction(zoomActions, "rotate-ccw", "Reset", () => this.resetViewZoom()); - - const range = this.createPanelRange( - zoom, - "zoom", - "Zoom", - "500", - { min: "0", max: String(ZOOM_SLIDER_STEPS), step: "1" }, - value => { - const viewport = this.canvasViewport(); - const minZoom = this.canvasMinZoom(viewport, this.lastLayout); - const maxZoom = this.canvasMaxZoom(viewport, this.lastLayout); - this.setCanvasZoom(sliderValueToZoom(value, minZoom, maxZoom), null, true); - } - ); - this.zoomInput = range.input; - this.zoomLabel = range.valueEl; - this.syncZoomControls(); - - const spacing = this.createPanelSection(panel, "Spacing"); - this.columnInput = this.createPanelNumber( - spacing, - "column-spacing", - "Ring", - this.state.columnSpacing, - { min: String(MIN_RING_SPACING), max: String(MAX_RING_SPACING), step: "10" }, - value => { - this.state.columnSpacing = clampNumber(value, MIN_RING_SPACING, MAX_RING_SPACING, DEFAULT_RING_SPACING); - this.renderMapOnly(); - } - ); - this.rowInput = this.createPanelNumber( - spacing, - "row-spacing", - "Gap", - this.state.rowSpacing, - { min: String(MIN_NODE_SPACING), max: String(MAX_NODE_SPACING), step: "2" }, - value => { - this.state.rowSpacing = clampNumber(value, MIN_NODE_SPACING, MAX_NODE_SPACING, DEFAULT_NODE_SPACING); - this.renderMapOnly(); - } - ); - - const labels = this.createPanelSection(panel, "Labels"); - this.labelVisibilitySelect = this.createPanelSelect( - labels, - "label-visibility", - "Names", - normalizeLabelVisibility(this.state.labelVisibility), - LABEL_VISIBILITY_OPTIONS, - value => { - this.state.labelVisibility = normalizeLabelVisibility(value); - this.requestCanvasDraw("full"); - } - ); - - const motion = this.createPanelSection(panel, "Motion"); - const motionActions = motion.createDiv({ cls: "mwm-panel-action-row" }); - const spinning = this.state.swirlStrength > 0; - this.createPanelAction(motionActions, spinning ? "pause" : "play", spinning ? "Stop" : "Spin", () => { - this.setSpinSpeed(spinning ? 0 : DEFAULT_SWIRL_BUTTON_STRENGTH, { render: true }); - }, spinning); - const swirl = this.createPanelRange( - motion, - "swirl-strength", - "Spin speed", - String(this.state.swirlStrength), - { min: "0", max: String(MAX_SWIRL_STRENGTH), step: "1" }, - (value, input, valueEl) => { - this.setSpinSpeed(value); - valueEl.textContent = `${Math.round(this.state.swirlStrength)}%`; - } - ); - this.swirlInput = swirl.input; - this.swirlLabel = swirl.valueEl; - this.swirlLabel.textContent = `${Math.round(this.state.swirlStrength)}%`; - } - - renderDefaultsPage(index, graph) { - const panel = this.getSidePanelTarget(); - this.createPanelPageTitle(panel, "Defaults"); - const applyLiveDefault = (keys, saveOptions = {}) => { - this.plugin.applySettingsToOpenViews(keys, { preservePanel: true }); - void this.plugin.saveSettings(Object.assign({ rebuild: false, refresh: false }, saveOptions)); - }; - - const budgets = this.createPanelSection(panel, "Default Budgets"); - this.createPanelNumber( - budgets, - "default-atlas-depth", - "Atlas depth", - this.plugin.settings.atlasDepth, - { min: "1", max: String(MAX_ATLAS_DEPTH) }, - value => { - this.plugin.settings.atlasDepth = clampNumber(value, 1, MAX_ATLAS_DEPTH, DEFAULT_SETTINGS.atlasDepth); - applyLiveDefault("atlasDepth"); - } - ); - this.createPanelNumber( - budgets, - "default-render-node-limit", - "Render nodes", - this.plugin.settings.renderNodeLimit, - { min: "200", max: String(MAX_RENDER_NODE_LIMIT), step: "100" }, - value => { - this.plugin.settings.renderNodeLimit = clampNumber(value, 200, MAX_RENDER_NODE_LIMIT, DEFAULT_SETTINGS.renderNodeLimit); - applyLiveDefault("renderNodeLimit"); - } - ); - this.createPanelNumber( - budgets, - "default-link-limit", - "Link overlays", - this.plugin.settings.linkLimit, - { min: "0", max: String(MAX_LINK_LIMIT) }, - value => { - this.plugin.settings.linkLimit = clampNumber(value, 0, MAX_LINK_LIMIT, DEFAULT_SETTINGS.linkLimit); - applyLiveDefault("linkLimit"); - } - ); - this.createPanelNumber( - budgets, - "default-external-link-anchor-limit", - "Exact outside files", - this.plugin.settings.externalLinkAnchorLimit, - { min: "0", max: String(MAX_EXTERNAL_LINK_ANCHOR_LIMIT) }, - value => { - this.plugin.settings.externalLinkAnchorLimit = clampNumber(value, 0, MAX_EXTERNAL_LINK_ANCHOR_LIMIT, DEFAULT_SETTINGS.externalLinkAnchorLimit); - applyLiveDefault("externalLinkAnchorLimit"); - } - ); - - const toggles = this.createPanelSection(panel, "Default Toggles"); - this.createPanelToggle(toggles, "default-adaptive-detail", "Adaptive detail", this.plugin.settings.adaptiveDetail, checked => { - this.plugin.settings.adaptiveDetail = checked; - applyLiveDefault("adaptiveDetail"); - }); - this.createPanelSelect( - toggles, - "default-hover-highlight-mode", - "Hover highlight", - normalizeHoverHighlightMode(this.plugin.settings.hoverHighlightMode), - HOVER_HIGHLIGHT_MODE_OPTIONS, - value => { - this.plugin.settings.hoverHighlightMode = normalizeHoverHighlightMode(value); - this.plugin.settings.enableLinkHover = hoverHighlightsNoteLinks(this.plugin.settings.hoverHighlightMode); - applyLiveDefault("hoverHighlightMode"); - } - ); - this.createPanelToggle(toggles, "default-show-external-links", "External links", this.plugin.settings.showExternalLinks, checked => { - this.plugin.settings.showExternalLinks = checked; - applyLiveDefault("showExternalLinks"); - }); - this.createPanelToggle(toggles, "default-include-unresolved-links", "Unresolved links", this.plugin.settings.includeUnresolvedLinks, checked => { - this.plugin.settings.includeUnresolvedLinks = checked; - void this.plugin.saveSettings({ immediateRebuild: true, preservePanel: true }); - }); - - const layoutDefaults = this.createPanelSection(panel, "Default Layout"); - this.createPanelSelect( - layoutDefaults, - "default-label-visibility", - "Names", - normalizeLabelVisibility(this.plugin.settings.labelVisibility), - LABEL_VISIBILITY_OPTIONS, - value => { - this.plugin.settings.labelVisibility = normalizeLabelVisibility(value); - applyLiveDefault("labelVisibility"); - } - ); - this.createPanelNumber( - layoutDefaults, - "default-swirl-strength", - "Spin speed", - this.plugin.settings.swirlStrength, - { min: "0", max: String(MAX_SWIRL_STRENGTH), step: "1" }, - value => { - this.plugin.settings.swirlStrength = clampNumber(value, 0, MAX_SWIRL_STRENGTH, DEFAULT_SWIRL_STRENGTH); - applyLiveDefault("swirlStrength"); - } - ); - - const appearance = this.createPanelSection(panel, "Default Appearance"); - this.createPanelSelect( - appearance, - "default-color-scheme", - "Color scheme", - this.plugin.settings.colorScheme || DEFAULT_SETTINGS.colorScheme, - [ - ["auto", "Auto"], - ["day", "Day"], - ["night", "Night"] - ], - value => { - const scheme = normalizeColorScheme(value); - this.setColorScheme(scheme, false); - this.plugin.settings.colorScheme = scheme; - for (const leaf of this.plugin.app.workspace.getLeavesOfType(VIEW_TYPE_MINI_WORLD_MAP)) { - const view = leaf.view; - if (view && view !== this && typeof view.setColorScheme === "function") view.setColorScheme(scheme, false); - } - void this.plugin.saveSettings({ rebuild: false }); - } - ); - - const outside = this.createPanelSection(panel, "Outside Root"); - this.createPanelSelect( - outside, - "default-external-detail-mode", - "Outside detail", - this.plugin.settings.externalDetailMode || DEFAULT_SETTINGS.externalDetailMode, - [ - ["grouped", "Groups"], - ["selected", "Selected"], - ["exact", "Exact"] - ], - value => { - this.plugin.settings.externalDetailMode = value; - applyLiveDefault("externalDetailMode"); - } - ); - - const ignores = this.createPanelSection(panel, "Ignored Folders"); - this.createPanelTextArea( - ignores, - "default-ignore-folders", - "Folder paths", - (this.plugin.settings.ignoreFolders || []).join("\n"), - value => { - this.plugin.settings.ignoreFolders = value - .split("\n") - .map(line => line.trim()) - .filter(Boolean); - void this.plugin.saveSettings({ immediateRebuild: true, preservePanel: true }); - } - ); - } - - renderLegendPage() { - const panel = this.getSidePanelTarget(); - this.createPanelPageTitle(panel, "Legend"); - this.renderLegendControls(panel); - this.renderLegend(false); - } - - renderLegendControls(panel) { - const section = this.createPanelSection(panel, "Legend"); - const hidden = this.hiddenLegendItemSet(); - for (const [id, label, text] of LEGEND_ITEM_DEFINITIONS) { - this.createPanelToggle(section, `legend-${id}`, label, !hidden.has(id), checked => { - this.setLegendItemHidden(id, !checked); - }).title = text; - } - } - - hiddenLegendItemSet() { - return new Set(Array.isArray(this.state.hiddenLegendItems) ? this.state.hiddenLegendItems : []); - } - - setLegendItemHidden(id, hidden) { - const validIds = new Set(LEGEND_ITEM_DEFINITIONS.map(([itemId]) => itemId)); - if (!validIds.has(id)) return; - const hiddenSet = this.hiddenLegendItemSet(); - if (hidden) hiddenSet.add(id); - else hiddenSet.delete(id); - const next = Array.from(hiddenSet).filter(itemId => validIds.has(itemId)); - this.state.hiddenLegendItems = next; - this.plugin.settings.hiddenLegendItems = next.slice(); - void this.plugin.saveSettings({ rebuild: false, refresh: false }); - this.renderMapOnly(); - } - - renderInspectPage(index, graph, forcedNodeId) { - const panel = this.getSidePanelTarget(); - - if (this.selectedLink && forcedNodeId === undefined) { - this.renderLinkPanel(index, graph, this.selectedLink); - return; - } - - const nodeId = forcedNodeId ?? this.selectedNodeId ?? graph?.focusId ?? graph?.rootId; - const node = nodeId !== null && nodeId !== undefined ? graphNode(index, graph, nodeId) : null; - - panel.createDiv({ cls: "mwm-side-title", text: node ? node.title : "Mini World Map" }); - - if (!node) { - panel.createDiv({ cls: "mwm-side-muted", text: "Select a node to inspect it." }); - return; - } - - const path = panel.createDiv({ cls: "mwm-side-path", text: node.path || "/" }); - path.setAttribute("title", node.path || "/"); - - const nodeType = node.externalProxy - ? `outside ${node.type}` - : node.type === "folder" && node.representativeFile - ? "folder + meta file" - : node.isRepresentativeFile - ? "merged meta file" - : node.type; - const facts = [ - ["Type", nodeType], - ["Depth", String(node.depth)], - ["Notes", String(node.noteCount || node.descendantCount || 0)], - ["Out", String(node.linkCount || 0)], - ["In", String(node.backlinkCount || 0)] - ]; - - const table = panel.createDiv({ cls: "mwm-facts" }); - for (const [label, value] of facts) { - table.createSpan({ text: label }); - table.createSpan({ text: value }); - } - - const actions = panel.createDiv({ cls: "mwm-side-actions" }); - this.createIconButton(actions, "pin", "Pin highlighted path", () => { - const previousNeedsHoverLinks = this.needsCanvasHoverLinks(); - const pin = this.addPinnedPath(this.createNodePinCandidate(node, this.state.hoverHighlightMode)); - if (!pin) return; - this.state.sidePage = "pins"; - if (previousNeedsHoverLinks !== this.needsCanvasHoverLinks()) this.renderMapOnly(); - else this.applyPersistentHighlight(); - if (!this.state.fullscreen && this.lastCanvasBundle) { - this.renderSidePanel(this.lastCanvasBundle.index, this.lastCanvasBundle.graph); - } - }, ""); - if (node.type === "note") { - this.createIconButton(actions, "file-text", "Open note", () => this.openNode(node.id), ""); - this.createIconButton(actions, "locate-fixed", "Focus note", () => { - this.state.mode = "focus"; - this.state.focusPath = node.id; - this.state.showCompleteRoot = false; - this.viewInitialized = false; - this.resetAdaptiveTuning(); - this.render(); - }, ""); - } - if (node.type === "folder") { - this.createIconButton(actions, "folder-open", "Use as atlas root", () => { - this.state.mode = "atlas"; - this.state.rootPath = node.id; - this.state.showCompleteRoot = false; - this.viewInitialized = false; - this.resetAdaptiveTuning(); - this.render(); - }, ""); - if (node.representativeFile) { - this.createIconButton(actions, "file-text", "Open representative note", () => this.openNode(node.representativeFile), ""); - } - } - if (node.type === "external" && node.externalAnchorPath) { - const anchor = index.nodes.get(node.externalAnchorPath); - if (anchor && anchor.type === "folder") { - this.createIconButton(actions, "folder-open", "Use outside branch as root", () => { - this.state.mode = "atlas"; - this.state.rootPath = anchor.id; - this.state.showCompleteRoot = false; - this.viewInitialized = false; - this.resetAdaptiveTuning(); - this.render(); - }, ""); - } - if (anchor && anchor.type === "note") { - this.createIconButton(actions, "file-text", "Open outside note", () => this.openNode(anchor.id), ""); - } - } - - if (node.type === "external") { - panel.createDiv({ - cls: "mwm-side-muted", - text: "This is a summarized outside branch. It keeps links visible when the atlas root is focused on a smaller subtree." - }); - } else { - this.renderNeighborList(index, node); - } - } - - renderLinkPanel(index, graph, edge) { - const panel = this.getSidePanelTarget(); - const source = graphNode(index, graph, edge.source); - const target = graphNode(index, graph, edge.target); - panel.createDiv({ cls: "mwm-side-title", text: "Link overlay" }); - panel.createDiv({ - cls: "mwm-side-muted", - text: "Aggregated internal Markdown links between visible nodes. Folder nodes can include hidden descendants; outside links point to exact outside files when the current root is narrowed." - }); - - const table = panel.createDiv({ cls: "mwm-facts mwm-link-facts" }); - const facts = [ - ["Source", source ? source.title : edge.source], - ["Target", target ? target.title : edge.target], - ["Weight", String(edge.weight || 0)], - ["Raw edges", String(edge.rawCount || edge.weight || 0)], - ["Unresolved", String(edge.unresolvedCount || 0)], - ["External", String(edge.externalCount || 0)] - ]; - for (const [label, value] of facts) { - table.createSpan({ text: label }); - table.createSpan({ text: value }); - } - - const actions = panel.createDiv({ cls: "mwm-side-actions" }); - this.createIconButton(actions, "pin", "Pin link", () => { - const previousNeedsHoverLinks = this.needsCanvasHoverLinks(); - const pin = this.addPinnedPath(this.createLinkPinCandidate(index, graph, edge)); - if (!pin) return; - this.state.sidePage = "pins"; - if (previousNeedsHoverLinks !== this.needsCanvasHoverLinks()) this.renderMapOnly(); - else this.applyPersistentHighlight(); - if (!this.state.fullscreen && this.lastCanvasBundle) { - this.renderSidePanel(this.lastCanvasBundle.index, this.lastCanvasBundle.graph); - } - }, ""); - if (source) { - const button = actions.createEl("button", { cls: "mwm-text-button", attr: { type: "button" } }); - button.textContent = "Source"; - button.addEventListener("click", () => { - this.state.sidePage = "inspect"; - this.selectedLink = null; - this.selectedNodeId = source.id; - if (this.shouldRerenderForSelection()) this.render(); - else { - this.applyPersistentHighlight(); - if (!this.state.fullscreen) this.renderSidePanel(index, graph, source.id); - } - }); - } - if (target) { - const button = actions.createEl("button", { cls: "mwm-text-button", attr: { type: "button" } }); - button.textContent = "Target"; - button.addEventListener("click", () => { - this.state.sidePage = "inspect"; - this.selectedLink = null; - this.selectedNodeId = target.id; - if (this.shouldRerenderForSelection()) this.render(); - else { - this.applyPersistentHighlight(); - if (!this.state.fullscreen) this.renderSidePanel(index, graph, target.id); - } - }); - } - } - - renderLegend(showHeading = true) { - const panel = this.getSidePanelTarget(); - if (showHeading) panel.createDiv({ cls: "mwm-side-heading", text: "Legend" }); - const legend = panel.createDiv({ cls: "mwm-legend" }); - const hidden = this.hiddenLegendItemSet(); - - for (const [id, label, text, cls] of LEGEND_ITEM_DEFINITIONS) { - if (hidden.has(id)) continue; - const item = legend.createDiv({ cls: "mwm-legend-item" }); - item.createSpan({ cls: `mwm-legend-mark ${cls}` }); - item.createSpan({ cls: "mwm-legend-label", text: label }); - item.createSpan({ cls: "mwm-legend-text", text }); - } - } - - renderNeighborList(index, node) { - const panel = this.getSidePanelTarget(); - const mergedIds = [node.id]; - if (node.type === "folder" && node.representativeFile) mergedIds.push(node.representativeFile); - const outgoing = uniqueEdgesByEndpoint( - mergedIds.flatMap(id => index.linkEdgesBySource.get(id) || []), - "target" - ).sort((a, b) => b.weight - a.weight); - const incoming = uniqueEdgesByEndpoint( - mergedIds.flatMap(id => index.linkEdgesByTarget.get(id) || []), - "source" - ).sort((a, b) => b.weight - a.weight); - - panel.createDiv({ cls: "mwm-side-heading", text: `Outgoing (${outgoing.length})` }); - this.renderEdgeList(index, outgoing, "target"); - - panel.createDiv({ cls: "mwm-side-heading", text: `Backlinks (${incoming.length})` }); - this.renderEdgeList(index, incoming, "source"); - } - - renderEdgeList(index, edges, side) { - const panel = this.getSidePanelTarget(); - if (!edges.length) { - panel.createDiv({ cls: "mwm-side-muted", text: "None" }); - return; - } - - const list = panel.createEl("ul", { cls: "mwm-edge-list" }); - for (const edge of edges) { - const target = index.nodes.get(edge[side]); - if (!target) continue; - const item = list.createEl("li"); - const button = item.createEl("button", { attr: { type: "button", title: target.path || target.title } }); - button.textContent = `${target.title} (${edge.weight})`; - button.addEventListener("click", () => { - this.state.sidePage = "inspect"; - this.selectedLink = null; - this.selectedNodeId = target.id; - this.applyPersistentHighlight(); - const bundle = this.lastCanvasBundle; - if (!this.state.fullscreen && bundle) this.renderSidePanel(bundle.index, bundle.graph, target.id); - }); - } - } - - async openNode(path, evt) { - const file = this.app.vault.getAbstractFileByPath(path); - if (!(file instanceof TFile)) return; - const targetPane = evt && (evt.metaKey || evt.ctrlKey) ? "split" : "tab"; - await this.app.workspace.openLinkText(file.path, "", targetPane, { active: true }); - } -} + return bilinearCubeUV( envMap, sampleDirection, mipInt ); -class MiniWorldMapSettingTab extends PluginSettingTab { - constructor(app, plugin) { - super(app, plugin); - this.plugin = plugin; - } - - display() { - const { containerEl } = this; - containerEl.empty(); - containerEl.createEl("h2", { text: "Mini World Map" }); - - new Setting(containerEl) - .setName("Color scheme") - .setDesc("Auto follows Obsidian. Day and Night force Mini World Map's own palette.") - .addDropdown(dropdown => dropdown - .addOption("auto", "Auto") - .addOption("day", "Day") - .addOption("night", "Night") - .setValue(this.plugin.settings.colorScheme || DEFAULT_SETTINGS.colorScheme) - .onChange(async value => { - const scheme = normalizeColorScheme(value); - this.plugin.settings.colorScheme = scheme; - for (const leaf of this.plugin.app.workspace.getLeavesOfType(VIEW_TYPE_MINI_WORLD_MAP)) { - const view = leaf.view; - if (view && typeof view.setColorScheme === "function") view.setColorScheme(scheme, false); - } - await this.plugin.saveSettings({ rebuild: false }); - })); - - new Setting(containerEl) - .setName("Default atlas depth") - .setDesc("How many hierarchy levels to render before deeper nodes are aggregated.") - .addSlider(slider => slider - .setLimits(1, 20, 1) - .setValue(this.plugin.settings.atlasDepth) - .setDynamicTooltip() - .onChange(async value => { - this.plugin.settings.atlasDepth = value; - await this.plugin.saveSettings(); - })); - - new Setting(containerEl) - .setName("Default link overlay limit") - .setDesc("Maximum aggregated cross-links to draw in the map.") - .addText(text => text - .setPlaceholder(String(DEFAULT_SETTINGS.linkLimit)) - .setValue(String(this.plugin.settings.linkLimit)) - .onChange(async value => { - this.plugin.settings.linkLimit = clampNumber(value, 0, MAX_LINK_LIMIT, DEFAULT_SETTINGS.linkLimit); - await this.plugin.saveSettings(); - })); - - new Setting(containerEl) - .setName("Default render node limit") - .setDesc("Maximum visible nodes to draw before lower-priority nodes are summarized.") - .addText(text => text - .setPlaceholder(String(DEFAULT_SETTINGS.renderNodeLimit)) - .setValue(String(this.plugin.settings.renderNodeLimit)) - .onChange(async value => { - this.plugin.settings.renderNodeLimit = clampNumber(value, 200, MAX_RENDER_NODE_LIMIT, DEFAULT_SETTINGS.renderNodeLimit); - await this.plugin.saveSettings(); - })); - - new Setting(containerEl) - .setName("Adaptive detail by default") - .setDesc("Let Mini World Map adjust depth, node budget, and link budget after measuring render cost.") - .addToggle(toggle => toggle - .setValue(this.plugin.settings.adaptiveDetail) - .onChange(async value => { - this.plugin.settings.adaptiveDetail = value; - await this.plugin.saveSettings(); - })); - - new Setting(containerEl) - .setName("Default label visibility") - .setDesc("Auto shows important names by zoom. Hover only keeps names hidden until a node is hovered or selected.") - .addDropdown(dropdown => { - for (const [value, label] of LABEL_VISIBILITY_OPTIONS) { - dropdown.addOption(value, label); - } - dropdown - .setValue(normalizeLabelVisibility(this.plugin.settings.labelVisibility)) - .onChange(async value => { - this.plugin.settings.labelVisibility = normalizeLabelVisibility(value); - await this.plugin.saveSettings({ rebuild: false }); - }); - }); - - new Setting(containerEl) - .setName("Default spin speed") - .setDesc("How fast rings orbit when spin is enabled in the map view.") - .addSlider(slider => slider - .setLimits(0, MAX_SWIRL_STRENGTH, 1) - .setValue(this.plugin.settings.swirlStrength) - .setDynamicTooltip() - .onChange(async value => { - this.plugin.settings.swirlStrength = clampNumber(value, 0, MAX_SWIRL_STRENGTH, DEFAULT_SWIRL_STRENGTH); - await this.plugin.saveSettings({ rebuild: false }); - })); - - new Setting(containerEl) - .setName("Default hover highlight") - .setDesc("Choose what the graph highlights when the cursor is over a node.") - .addDropdown(dropdown => { - for (const [value, label] of HOVER_HIGHLIGHT_MODE_OPTIONS) { - dropdown.addOption(value, label); - } - dropdown - .setValue(normalizeHoverHighlightMode(this.plugin.settings.hoverHighlightMode)) - .onChange(async value => { - this.plugin.settings.hoverHighlightMode = normalizeHoverHighlightMode(value); - this.plugin.settings.enableLinkHover = hoverHighlightsNoteLinks(this.plugin.settings.hoverHighlightMode); - await this.plugin.saveSettings(); - }); - }); - - new Setting(containerEl) - .setName("Show external links by default") - .setDesc("When an atlas root is selected, keep links to exact notes outside that root visible around outside branch anchors.") - .addToggle(toggle => toggle - .setValue(this.plugin.settings.showExternalLinks) - .onChange(async value => { - this.plugin.settings.showExternalLinks = value; - await this.plugin.saveSettings(); - })); - - new Setting(containerEl) - .setName("Outside detail mode") - .setDesc("Groups is calmest. Selected uses selection context on the next render. Exact draws all outside files up to the limit.") - .addDropdown(dropdown => dropdown - .addOption("grouped", "Groups") - .addOption("selected", "Selected") - .addOption("exact", "Exact") - .setValue(this.plugin.settings.externalDetailMode || DEFAULT_SETTINGS.externalDetailMode) - .onChange(async value => { - this.plugin.settings.externalDetailMode = value; - await this.plugin.saveSettings(); - })); - - new Setting(containerEl) - .setName("Exact outside file limit") - .setDesc("Maximum exact outside linked files to draw before the remainder is grouped.") - .addText(text => text - .setPlaceholder(String(DEFAULT_SETTINGS.externalLinkAnchorLimit)) - .setValue(String(this.plugin.settings.externalLinkAnchorLimit)) - .onChange(async value => { - this.plugin.settings.externalLinkAnchorLimit = clampNumber(value, 0, MAX_EXTERNAL_LINK_ANCHOR_LIMIT, DEFAULT_SETTINGS.externalLinkAnchorLimit); - await this.plugin.saveSettings(); - })); - - new Setting(containerEl) - .setName("Include unresolved links") - .setDesc("Represent unresolved internal links as temporary nodes.") - .addToggle(toggle => toggle - .setValue(this.plugin.settings.includeUnresolvedLinks) - .onChange(async value => { - this.plugin.settings.includeUnresolvedLinks = value; - await this.plugin.saveSettings(); - })); - - new Setting(containerEl) - .setName("Ignored folders") - .setDesc("One path per line. Matching folders are excluded from the map.") - .addTextArea(text => text - .setValue((this.plugin.settings.ignoreFolders || []).join("\n")) - .onChange(async value => { - this.plugin.settings.ignoreFolders = value - .split("\n") - .map(line => line.trim()) - .filter(Boolean); - await this.plugin.saveSettings(); - })); - } -} + } -function layoutVisibleGraph(index, graph, state = {}) { - const positions = new Map(); - const visible = new Set(graph.nodes.map(node => node.id)); - const baseRingGap = clampNumber(state.columnSpacing, MIN_RING_SPACING, MAX_RING_SPACING, DEFAULT_RING_SPACING); - const baseNodeGap = clampNumber(state.rowSpacing, MIN_NODE_SPACING, MAX_NODE_SPACING, DEFAULT_NODE_SPACING); - const padding = 340; - let trimmed = false; - - const graphNodesById = graph.nodesById || new Map(graph.nodes.map(node => [node.id, node])); - const normalNodes = graph.nodes.filter(node => !node.externalProxy && node.type !== "external"); - const normalNodeIds = new Set(normalNodes.map(node => node.id)); - const spacingProfile = adaptiveLayoutSpacing(graph, normalNodeIds, baseRingGap, baseNodeGap); - const ringGap = spacingProfile.ringGap; - const nodeGap = spacingProfile.nodeGap; - const rootId = graph.rootId && normalNodeIds.has(graph.rootId) - ? graph.rootId - : normalNodeIds.has(ROOT_ID) - ? ROOT_ID - : normalNodes.length - ? normalNodes[0].id - : null; - - const childrenByParent = new Map(); - for (const [parentId, childIds] of index.childrenByParent.entries()) { - if (!normalNodeIds.has(parentId)) continue; - const children = childIds.filter(childId => normalNodeIds.has(childId)); - if (children.length) childrenByParent.set(parentId, children); - } - - const metrics = new Map(); - function measureSubtree(id, depth, visiting = new Set()) { - if (metrics.has(id)) return metrics.get(id); - if (visiting.has(id)) { - const fallback = { weight: 1, maxDepth: depth, count: 1 }; - metrics.set(id, fallback); - return fallback; - } - - visiting.add(id); - const children = childrenByParent.get(id) || []; - const incidentPressure = spacingProfile.incidentPressureByNode.get(id) || 0; - let weight = Math.min(9, incidentPressure * 0.24); - let count = 1; - let maxDepth = depth; - for (const childId of children) { - const childMetrics = measureSubtree(childId, depth + 1, visiting); - weight += childMetrics.weight; - count += childMetrics.count; - maxDepth = Math.max(maxDepth, childMetrics.maxDepth); - } - visiting.delete(id); - - const metricsForNode = { - weight: Math.max(1, weight || 1), - maxDepth, - count - }; - metrics.set(id, metricsForNode); - return metricsForNode; - } - - const reachable = new Set(); - function collectReachable(id) { - if (id === null || id === undefined || reachable.has(id)) return; - reachable.add(id); - for (const childId of childrenByParent.get(id) || []) collectReachable(childId); - } - - if (rootId !== null) { - measureSubtree(rootId, 0); - collectReachable(rootId); - } - - const externalGroups = graph.nodes - .filter(node => node.type === "external" && !node.externalProxy) - .sort(compareNodes); - const externalFiles = graph.nodes - .filter(node => node.externalProxy) - .sort(compareNodes); - const orphanNodes = normalNodes - .filter(node => node.id !== rootId && !reachable.has(node.id)) - .sort(compareNodes); - - const rootMetrics = rootId !== null ? metrics.get(rootId) || { weight: 1, maxDepth: 0, count: 1 } : { weight: 0, maxDepth: 0, count: 0 }; - const maxTreeDepth = Math.max(0, rootMetrics.maxDepth || 0); - const ringSpacing = ringGap; - - if (rootId !== null) { - positions.set(rootId, { - x: 0, - y: 0, - depth: 0, - angle: -Math.PI / 2, - radius: 0, - labelSide: 1, - centerX: 0, - centerY: 0 - }); - - placeRadialChildren( - rootId, - 0, - -Math.PI / 2, - -Math.PI / 2 + Math.PI * 2, - -Math.PI / 2, - childrenByParent, - metrics, - positions, - spacingProfile.branchFanSpan, - spacingProfile, - rootId - ); - } - - let maxPlacedRadius = Math.max(maxRadiusFromPositions(positions), ringGap); - if (orphanNodes.length) { - trimmed = true; - maxPlacedRadius = Math.max( - maxPlacedRadius, - placeOuterCircleNodes(orphanNodes, positions, graph, maxPlacedRadius + ringSpacing * 0.72, nodeGap, -Math.PI / 2, { - depth: maxTreeDepth + 1 - }) - ); - } - - if (externalGroups.length) { - maxPlacedRadius = Math.max( - maxPlacedRadius, - placeOuterCircleNodes(externalGroups, positions, graph, maxPlacedRadius + ringSpacing * 0.62, nodeGap * 1.15, -Math.PI / 3, { - depth: maxTreeDepth + 1, - external: true, - externalGroup: true - }) - ); - } - - if (externalFiles.length) { - maxPlacedRadius = Math.max( - maxPlacedRadius, - placeOuterCircleNodes(externalFiles, positions, graph, maxPlacedRadius + Math.max(220, ringSpacing * 0.52), nodeGap, -Math.PI / 5, { - depth: maxTreeDepth + 2, - external: true, - externalFile: true - }) - ); - } - - const ringTargets = assignDepthRingTargets(positions, graph, spacingProfile, nodeGap); - resolveRadialCollisions(positions, graph, spacingProfile, nodeGap, ringTargets); - enforceDepthRingBands(positions, graph, spacingProfile, nodeGap, ringTargets); - if (spacingProfile.radiusExpansion > 1.001) { - applyAdaptiveRadiusExpansion(positions, ringTargets, spacingProfile); - } - const spinSpeed = clampFloat( - clampNumber(state.swirlStrength, 0, MAX_SWIRL_STRENGTH, DEFAULT_SWIRL_STRENGTH) / MAX_SWIRL_STRENGTH, - 0, - 1, - 0 - ); - const swirlStrength = spinSpeed > 0.001 - ? clampFloat(0.24 + spinSpeed * 0.34, 0.24, 0.58, 0.36) - : 0; - if (swirlStrength > 0.001) applyRadialSwirl(positions, graph, spacingProfile, swirlStrength); - maxPlacedRadius = Math.max(maxPlacedRadius, maxRadiusFromPositions(positions)); - - const routeBundle = computeLinkRoutes( - graph.linkEdges, - positions, - maxTreeDepth + (externalGroups.length || externalFiles.length ? 2 : orphanNodes.length ? 1 : 0), - ringSpacing, - maxPlacedRadius + Math.max(150, ringSpacing * 0.35), - spacingProfile.routeGapFactor - ); - const bounds = radialLayoutBounds(positions, Math.max(routeBundle.maxRadius, maxPlacedRadius + Math.max(160, ringSpacing * 0.34))); - const offsetX = padding - bounds.minX; - const offsetY = padding - bounds.minY; - shiftRadialLayout(positions, routeBundle.routes, offsetX, offsetY); - anchorLayoutHomePositions(positions); - const rings = computeLayoutRings(positions, ringTargets); - - return { - positions, - width: Math.max(900, bounds.maxX - bounds.minX + padding * 2), - height: Math.max(520, bounds.maxY - bounds.minY + padding * 2), - trimmed: trimmed || Boolean(graph.hiddenNodeCount), - linkRoutes: routeBundle.routes, - rings, - radiusExpansion: spacingProfile.radiusExpansion, - swirlStrength, - spinSpeed, - centerX: offsetX, - centerY: offsetY - }; -} + void main() { -function adaptiveLayoutSpacing(graph, normalNodeIds, baseRingGap, baseNodeGap) { - const incidentPressureByNode = new Map(); - let totalPressure = 0; - let externalPressure = 0; - - for (const edge of graph.linkEdges || []) { - const weightScore = Math.min(7, Math.log2((edge.weight || 1) + 1)); - const rawScore = Math.min(6, Math.log2((edge.rawCount || edge.weight || 1) + 1)); - const edgePressure = 0.75 - + weightScore * 0.62 - + rawScore * 0.28 - + (edge.unresolvedCount ? 0.35 : 0) - + (edge.externalCount ? 0.85 : 0); - - totalPressure += edgePressure; - if (edge.externalCount) externalPressure += edgePressure; - - for (const id of [edge.source, edge.target]) { - if (!id && id !== ROOT_ID) continue; - incidentPressureByNode.set(id, (incidentPressureByNode.get(id) || 0) + edgePressure); - } - } - - let maxIncidentPressure = 0; - for (const pressure of incidentPressureByNode.values()) { - maxIncidentPressure = Math.max(maxIncidentPressure, pressure); - } - - const visibleNodeCount = Math.max(1, (graph.nodes && graph.nodes.length) || normalNodeIds.size || 1); - const nodeDensity = graphNodeDensityProfile(graph, normalNodeIds); - const averagePressure = totalPressure / visibleNodeCount; - const overlayDensity = ((graph.linkEdges || []).length || 0) / visibleNodeCount; - const hubPressure = maxIncidentPressure / Math.max(1, Math.sqrt(visibleNodeCount) * 1.65); - const combinedPressure = averagePressure - + Math.sqrt(Math.max(0, hubPressure)) * 0.58 - + Math.min(1.6, overlayDensity) * 0.3 - + (externalPressure / visibleNodeCount) * 0.32; - const pressureRoot = Math.sqrt(Math.max(0, combinedPressure)); - - const nodeFactor = clampFloat( - 1.2 + pressureRoot * 0.92 + Math.min(0.86, averagePressure * 0.105), - 1.2, - 5.8, - 1 - ); - const ringFactor = clampFloat( - 1.08 + pressureRoot * 0.34 + Math.min(0.34, averagePressure * 0.044), - 1.08, - 2.15, - 1 - ); - const fanFactor = clampFloat( - 0.62 + pressureRoot * 0.24 + Math.min(0.3, overlayDensity * 0.17), - 0.62, - 1.35, - 0.68 - ); - const routeGapFactor = clampFloat( - 0.9 + pressureRoot * 0.38 + Math.min(0.42, overlayDensity * 0.2), - 0.9, - 2.55, - 1 - ); - const countExpansion = Math.max(0, Math.log2(nodeDensity.normalCount / 520)) * 0.035; - const ringExpansion = Math.max(0, Math.sqrt(nodeDensity.maxRingCount / 96) - 1) * 0.16; - const averageRingExpansion = Math.max(0, Math.sqrt(nodeDensity.averageRingCount / 64) - 1) * 0.1; - const pressureExpansion = Math.max(0, pressureRoot - 0.75) * 0.028; - const radiusExpansion = clampFloat( - 1 + countExpansion + ringExpansion + averageRingExpansion + pressureExpansion, - 1, - 1.56, - 1 - ); - - return { - baseRingGap, - baseNodeGap, - ringGap: clampNumber(baseRingGap * ringFactor, MIN_RING_SPACING, 4200, baseRingGap), - nodeGap: clampNumber(baseNodeGap * nodeFactor, 86, 860, baseNodeGap), - branchFanSpan: Math.PI * fanFactor, - routeGapFactor, - radiusExpansion, - ringCountsByDepth: nodeDensity.countsByDepth, - maxDensityDepth: nodeDensity.maxDepth, - totalPressure, - incidentPressureByNode - }; -} + vec3 axis = latitudinal ? poleAxis : cross( poleAxis, vOutputDirection ); -function graphNodeDensityProfile(graph, normalNodeIds) { - const normalCount = Math.max(1, normalNodeIds?.size || 0); - const byDepth = new Map(); - - for (const node of graph.nodes || []) { - if (!node || !normalNodeIds.has(node.id)) continue; - const depth = Math.max(0, Math.round(node.depth || 0)); - if (depth <= 0) continue; - byDepth.set(depth, (byDepth.get(depth) || 0) + 1); - } - - let maxRingCount = 0; - let totalRingCount = 0; - for (const count of byDepth.values()) { - maxRingCount = Math.max(maxRingCount, count); - totalRingCount += count; - } - - return { - normalCount, - ringCount: byDepth.size, - maxRingCount, - averageRingCount: byDepth.size ? totalRingCount / byDepth.size : normalCount, - maxDepth: Math.max(...Array.from(byDepth.keys()), 1), - countsByDepth: byDepth - }; -} + if ( all( equal( axis, vec3( 0.0 ) ) ) ) { -function placeRadialChildren(parentId, depth, sectorStart, sectorEnd, parentAngle, childrenByParent, metrics, positions, branchFanSpan, spacingProfile, rootId) { - const children = childrenByParent.get(parentId) || []; - if (!children.length) return; - - let start = sectorStart; - let end = sectorEnd; - let span = Math.max(0.001, end - start); - const parentPoint = positions.get(parentId) || { radius: 0 }; - let childRadius = parentPoint.radius + localRadialGap(parentId, depth, children, metrics, spacingProfile, rootId); - const nodeGap = localArcGap(parentId, children, metrics, spacingProfile, rootId); - - if (children.length === 1 && parentId === rootId) { - const localSpan = Math.max(Math.PI * 0.36, branchFanSpan * 0.82); - start = parentAngle - localSpan / 2; - end = parentAngle + localSpan / 2; - span = localSpan; - } else if (parentId !== rootId) { - const demandSpan = children.length > 1 - ? ((children.length - 1) * nodeGap) / childRadius + 0.08 - : Math.max(Math.PI * 0.24, branchFanSpan * 0.62); - const cappedFan = Math.min(span, branchFanSpan); - const localSpan = Math.min(span, Math.max(cappedFan, demandSpan)); - start = parentAngle - localSpan / 2; - end = parentAngle + localSpan / 2; - span = localSpan; - } - - if (children.length > 1) { - const requiredRadius = ((children.length - 1) * nodeGap) / Math.max(0.16, span * 0.64); - const maxExtra = spacingProfile.ringGap * (parentId === rootId ? 1.8 : 2.8); - childRadius = Math.max(childRadius, Math.min(parentPoint.radius + maxExtra, requiredRadius)); - } - - const totalWeight = Math.max(1, children.reduce((sum, childId) => { - const childMetrics = metrics.get(childId); - return sum + (childMetrics ? childMetrics.weight : 1); - }, 0)); - const localGap = children.length > 1 - ? Math.min(nodeGap / childRadius, span * 0.38 / (children.length - 1)) - : 0; - const usableSpan = Math.max(0.001, span - localGap * Math.max(0, children.length - 1)); - let cursor = start; - - for (const childId of children) { - const childMetrics = metrics.get(childId) || { weight: 1 }; - const childSpan = children.length === 1 - ? usableSpan - : usableSpan * (childMetrics.weight / totalWeight); - const childStart = cursor; - const childEnd = cursor + childSpan; - const childAngle = childStart + childSpan / 2; - const radius = childRadius; - const point = radialPoint(0, 0, radius, childAngle); - - positions.set(childId, { - x: point.x, - y: point.y, - depth: depth + 1, - angle: childAngle, - radius, - labelSide: labelSideForAngle(childAngle), - centerX: 0, - centerY: 0 - }); - - placeRadialChildren( - childId, - depth + 1, - childStart, - childEnd, - childAngle, - childrenByParent, - metrics, - positions, - branchFanSpan, - spacingProfile, - rootId - ); - cursor = childEnd + localGap; - } -} + axis = vec3( vOutputDirection.z, 0.0, - vOutputDirection.x ); -function localRadialGap(parentId, depth, children, metrics, spacingProfile, rootId) { - const childCount = children.length; - const parentMetrics = metrics.get(parentId) || { count: 1, weight: 1 }; - const incidentPressure = spacingProfile.incidentPressureByNode.get(parentId) || 0; - const childRoot = Math.sqrt(Math.max(1, childCount)); - const subtreeSignal = Math.log2(Math.max(1, parentMetrics.count || parentMetrics.weight || 1)); - const pressureSignal = Math.sqrt(Math.max(0, incidentPressure)); - - let factor = 0.62 - + depth * 0.105 - + childRoot * 0.135 - + subtreeSignal * 0.094 - + pressureSignal * 0.086; - - if (parentId === rootId) factor *= 0.96; - if (childCount <= 4) factor = Math.min(factor, parentId === rootId ? 0.94 : 1.12); - if (childCount >= 14) factor = Math.max(factor, 1.24 + Math.min(1.1, childRoot * 0.09)); - if (childCount >= 40) factor = Math.max(factor, 1.56 + Math.min(1.34, childRoot * 0.084)); - - const minGap = parentId === rootId ? 260 : 300; - return clampNumber(spacingProfile.baseRingGap * factor, minGap, 4400, spacingProfile.baseRingGap); -} + } -function localArcGap(parentId, children, metrics, spacingProfile, rootId) { - const childCount = children.length; - const parentMetrics = metrics.get(parentId) || { count: 1, weight: 1 }; - const incidentPressure = spacingProfile.incidentPressureByNode.get(parentId) || 0; - const densitySignal = Math.sqrt(Math.max(1, childCount)) * 0.07 - + Math.log2(Math.max(1, parentMetrics.count || 1)) * 0.052 - + Math.sqrt(Math.max(0, incidentPressure)) * 0.05; - let factor = 1.58 + densitySignal * 1.95; - - if (parentId === rootId && childCount <= 6) factor *= 1.08; - if (childCount <= 4) factor = clampFloat(factor, 1.62, 2.08, 1.82); - else if (childCount <= 10) factor = Math.max(factor, 1.86); - if (childCount >= 24) factor = Math.max(factor, 2.12); - if (childCount >= 64) factor = Math.max(factor, 2.58); - - return clampNumber(spacingProfile.baseNodeGap * factor, 132, 920, spacingProfile.baseNodeGap); -} + axis = normalize( axis ); -function maxRadiusFromPositions(positions) { - let maxRadius = 0; - for (const point of positions.values()) { - if (Number.isFinite(point.radius)) maxRadius = Math.max(maxRadius, point.radius); - } - return maxRadius; -} + gl_FragColor = vec4( 0.0, 0.0, 0.0, 1.0 ); + gl_FragColor.rgb += weights[ 0 ] * getSample( 0.0, axis ); -function applyAdaptiveRadiusExpansion(positions, ringTargets, spacingProfile) { - const expansion = clampFloat(spacingProfile?.radiusExpansion, 1, 1.65, 1); - if (!positions || !positions.size || expansion <= 1.001) return; - - const maxDepth = Math.max( - 1, - Number(spacingProfile?.maxDensityDepth) || 1, - ringTargets && ringTargets.size - ? Math.max(...Array.from(ringTargets.keys()).map(depth => Number(depth) || 0), 1) - : 1 - ); - const ringCountsByDepth = spacingProfile?.ringCountsByDepth || new Map(); - const scaleForDepth = depth => { - const normalizedDepth = Math.max(0, Number(depth) || 0); - if (normalizedDepth <= 0) return 1; - - const depthRatio = clampFloat((normalizedDepth - 1) / Math.max(1, maxDepth - 1), 0, 1, 0); - const outerWeight = Math.pow(depthRatio, 1.42); - const ringCount = ringCountsByDepth.get(Math.round(normalizedDepth)) || 0; - const crowdedRingBoost = Math.max(0, Math.sqrt(ringCount / 72) - 1) * 0.13 * outerWeight; - const innerCompression = (1 - outerWeight) * Math.min(0.08, (expansion - 1) * 0.42); - const adaptiveGrowth = (expansion - 1) * (0.04 + outerWeight * 1.06); - - return clampFloat( - 1 - innerCompression + adaptiveGrowth + crowdedRingBoost, - 0.93, - 1.68, - 1 - ); - }; - - if (ringTargets && ringTargets.size) { - for (const [depth, radius] of Array.from(ringTargets.entries())) { - if (depth <= 0 || !Number.isFinite(radius)) continue; - ringTargets.set(depth, radius * scaleForDepth(depth)); - } - } - - for (const point of positions.values()) { - if (!point || !Number.isFinite(point.radius) || point.radius <= 0.001) continue; - const depth = Math.max(0, Math.round(point.depth || 0)); - const depthScale = scaleForDepth(depth); - const angle = Number.isFinite(point.angle) - ? point.angle - : Math.atan2(point.y, point.x); - const radius = point.radius * depthScale; - point.radius = radius; - point.x = Math.cos(angle) * radius; - point.y = Math.sin(angle) * radius; - point.angle = angle; - point.labelSide = labelSideForAngle(angle); - if (Number.isFinite(point.ringRadius)) point.ringRadius *= depthScale; - if (Number.isFinite(point.ringBandMin)) point.ringBandMin *= depthScale; - if (Number.isFinite(point.ringBandMax)) point.ringBandMax *= depthScale; - } -} + for ( int i = 1; i < n; i++ ) { -function placeOuterCircleNodes(nodes, positions, graph, radius, nodeGap, fallbackStart, options = {}) { - if (!nodes.length) return radius; - - const slot = (Math.PI * 2) / nodes.length; - const crowding = nodes.length * (nodeGap / Math.max(1, radius)); - const items = nodes.map((node, index) => ({ - node, - preferred: preferredAngleForNode(node, graph, positions, fallbackStart + index * slot) - })).sort((a, b) => normalizeAngle(a.preferred) - normalizeAngle(b.preferred)); - const offset = items.length - ? items[0].preferred - slot * 0.5 - : fallbackStart; - - for (let index = 0; index < items.length; index += 1) { - const item = items[index]; - const evenAngle = offset + index * slot; - const preferredDelta = shortestAngleDelta(evenAngle, item.preferred); - const angle = crowding < Math.PI * 1.35 - ? evenAngle + preferredDelta * 0.55 - : evenAngle; - const point = radialPoint(0, 0, radius, angle); - - positions.set(item.node.id, { - x: point.x, - y: point.y, - depth: options.depth || item.node.depth || 1, - angle, - radius, - labelSide: labelSideForAngle(angle), - centerX: 0, - centerY: 0, - external: Boolean(options.external || item.node.externalProxy), - externalGroup: Boolean(options.externalGroup), - externalFile: Boolean(options.externalFile || item.node.externalProxy), - fixed: Boolean(options.fixed) - }); - } - - return radius; -} + if ( i >= samples ) { -function assignDepthRingTargets(positions, graph, spacingProfile, nodeGap) { - const ringTargets = new Map([[0, 0]]); - if (!positions || !positions.size) return ringTargets; - - const graphNodesById = graph.nodesById || new Map((graph.nodes || []).map(node => [node.id, node])); - let maxLinkDegree = 1; - for (const node of graph.nodes || []) { - maxLinkDegree = Math.max(maxLinkDegree, (node.linkCount || 0) + (node.backlinkCount || 0)); - } - - const byDepth = new Map(); - for (const [id, point] of positions.entries()) { - if (!point) continue; - const depth = Math.max(0, Math.round(point.depth || 0)); - const node = graphNodesById.get(id); - const visualRadius = node ? nodeRadius(node, point, maxLinkDegree) : 6; - const diameter = visualRadius * 2 + Math.max(18, nodeGap * 0.36); - if (!byDepth.has(depth)) { - byDepth.set(depth, { count: 0, diameterTotal: 0, maxDiameter: 0, external: 0, linkPressure: 0 }); - } - const entry = byDepth.get(depth); - entry.count += 1; - entry.diameterTotal += diameter; - entry.maxDiameter = Math.max(entry.maxDiameter, diameter); - entry.linkPressure += spacingProfile.incidentPressureByNode.get(id) || 0; - if (point.external || node?.externalProxy || node?.type === "external") entry.external += 1; - } - - let previousRadius = 0; - const depths = Array.from(byDepth.keys()).filter(depth => depth > 0).sort((a, b) => a - b); - const maxDepth = Math.max(...depths, 1); - const totalNodes = Array.from(byDepth.values()).reduce((sum, entry) => sum + entry.count, 0); - const globalCompression = clampFloat(1 - Math.log10(Math.max(1, totalNodes)) * 0.085, 0.58, 0.9, 0.75); - for (const depth of depths) { - const entry = byDepth.get(depth); - const avgDiameter = entry.diameterTotal / Math.max(1, entry.count); - const rawCircumferenceDemand = entry.count > 1 - ? (entry.count * avgDiameter) / (Math.PI * 2) - : 0; - const compressedDemand = rawCircumferenceDemand > 0 - ? Math.pow(rawCircumferenceDemand, 0.64) * Math.pow(spacingProfile.ringGap, 0.36) - : 0; - const depthRatio = depth / Math.max(1, maxDepth); - const outerExpansion = 1 + Math.pow(depthRatio, 1.35) * 0.34; - const pressureExpansion = 1 + Math.min(0.28, Math.sqrt(entry.linkPressure / Math.max(1, entry.count)) * 0.036); - const depthCompression = clampFloat(1 - depthRatio * 0.1, 0.84, 0.98, 0.92); - const baseRadius = depth * spacingProfile.ringGap * globalCompression * depthCompression * outerExpansion * pressureExpansion; - const minSeparatedRadius = previousRadius + spacingProfile.ringGap * (entry.external ? 0.54 : 0.62); - const crowdingRadius = compressedDemand * globalCompression * outerExpansion * pressureExpansion + entry.maxDiameter * 1.45; - const maxStepRadius = previousRadius + spacingProfile.ringGap * (entry.external ? 1.45 : 1.72) * outerExpansion; - const radius = Math.min( - maxStepRadius, - Math.max(baseRadius, minSeparatedRadius, crowdingRadius) - ); - ringTargets.set(depth, radius); - previousRadius = radius; - } - - for (const point of positions.values()) { - if (!point) continue; - const depth = Math.max(0, Math.round(point.depth || 0)); - const targetRadius = ringTargets.get(depth); - if (!Number.isFinite(targetRadius)) continue; - const currentAngle = Number.isFinite(point.angle) ? point.angle : Math.atan2(point.y, point.x); - point.ringRadius = targetRadius; - if (depth === 0) { - point.x = 0; - point.y = 0; - point.radius = 0; - point.angle = currentAngle; - continue; - } - point.x = Math.cos(currentAngle) * targetRadius; - point.y = Math.sin(currentAngle) * targetRadius; - point.radius = targetRadius; - point.angle = currentAngle; - point.labelSide = labelSideForAngle(currentAngle); - } - - return ringTargets; -} + break; -function resolveRadialCollisions(positions, graph, spacingProfile, nodeGap, ringTargets = new Map()) { - if (!positions || positions.size < 2) return; - - const graphNodesById = graph.nodesById || new Map((graph.nodes || []).map(node => [node.id, node])); - let maxLinkDegree = 1; - for (const node of graph.nodes || []) { - maxLinkDegree = Math.max(maxLinkDegree, (node.linkCount || 0) + (node.backlinkCount || 0)); - } - - const basePad = clampNumber(nodeGap * 1.12, 54, 280, 96); - const items = []; - let maxCollisionRadius = 1; - - for (const [id, point] of positions.entries()) { - const node = graphNodesById.get(id); - if (!node || !point) continue; - - const visualRadius = nodeRadius(node, point, maxLinkDegree) * 1.72; - const spacingPad = Math.min(320, basePad + labelCollisionPadding(node)); - const collisionRadius = visualRadius + spacingPad; - const depth = Math.max(0, Math.round(point.depth || 0)); - const ringRadius = Number.isFinite(point.ringRadius) - ? point.ringRadius - : ringTargets.get(depth); - maxCollisionRadius = Math.max(maxCollisionRadius, collisionRadius); - items.push({ - id, - node, - point, - depth, - visualRadius, - collisionRadius, - gravity: clampFloat(Math.sqrt(Math.max(1, visualRadius)) / 3.1, 0.85, 2.55, 1), - fixed: id === graph.rootId || Boolean(point.fixed), - anchorX: point.x, - anchorY: point.y, - anchorRadius: Number.isFinite(point.radius) ? point.radius : Math.hypot(point.x, point.y), - anchorAngle: Number.isFinite(point.angle) ? point.angle : Math.atan2(point.y, point.x), - ringRadius: Number.isFinite(ringRadius) ? ringRadius : null - }); - } - - if (items.length < 2) return; - - const iterations = items.length > 3500 ? 9 : items.length > 1200 ? 11 : 16; - const cellSize = Math.max(112, maxCollisionRadius * 2.48); - const separateItems = (strength, softRepel = 0.16) => { - let moved = false; - const grid = new Map(); - - for (let index = 0; index < items.length; index += 1) { - const item = items[index]; - const gx = Math.floor(item.point.x / cellSize); - const gy = Math.floor(item.point.y / cellSize); - const key = `${gx},${gy}`; - if (!grid.has(key)) grid.set(key, []); - grid.get(key).push(index); - } - - for (let index = 0; index < items.length; index += 1) { - const item = items[index]; - const gx = Math.floor(item.point.x / cellSize); - const gy = Math.floor(item.point.y / cellSize); - - for (let x = gx - 1; x <= gx + 1; x += 1) { - for (let y = gy - 1; y <= gy + 1; y += 1) { - const bucket = grid.get(`${x},${y}`); - if (!bucket) continue; - - for (const otherIndex of bucket) { - if (otherIndex <= index) continue; - const other = items[otherIndex]; - if (item.fixed && other.fixed) continue; - - let dx = item.point.x - other.point.x; - let dy = item.point.y - other.point.y; - let distance = Math.hypot(dx, dy); - - if (distance < 0.001) { - const angle = deterministicPairAngle(item.id, other.id); - dx = Math.cos(angle); - dy = Math.sin(angle); - distance = 1; - } - - const minDistance = item.collisionRadius + other.collisionRadius; - const gravity = Math.sqrt(item.gravity * other.gravity); - const softDistance = minDistance + (item.visualRadius + other.visualRadius) * 1.75 * gravity; - if (distance >= softDistance) continue; - - const overlapPush = distance < minDistance - ? (minDistance - distance) * strength - : 0; - const softPush = distance >= minDistance - ? (softDistance - distance) * softRepel - : (softDistance - minDistance) * softRepel * 0.35; - const push = (overlapPush + softPush) * gravity + 0.01; - const nx = dx / distance; - const ny = dy / distance; - const itemShare = item.fixed ? 0 : other.fixed ? 1 : 0.5; - const otherShare = other.fixed ? 0 : item.fixed ? 1 : 0.5; - - item.point.x += nx * push * itemShare; - item.point.y += ny * push * itemShare; - other.point.x -= nx * push * otherShare; - other.point.y -= ny * push * otherShare; - moved = true; - } - } - } - } - return moved; - }; - const pullItemsToRings = strength => { - for (const item of items) { - if (item.fixed || !Number.isFinite(item.ringRadius)) continue; - const currentRadius = Math.max(0.001, Math.hypot(item.point.x, item.point.y)); - const currentAngle = Math.atan2(item.point.y, item.point.x); - const external = item.node.externalProxy || item.node.type === "external"; - const anglePull = external ? 0.016 : 0.024; - const ringTolerance = Math.max( - item.visualRadius * (external ? 1.9 : 1.45), - spacingProfile.ringGap * (external ? 0.15 : 0.105) - ); - const nextAngle = currentAngle + shortestAngleDelta(currentAngle, item.anchorAngle) * anglePull; - const pulledRadius = currentRadius + (item.ringRadius - currentRadius) * strength; - const nextRadius = clampFloat( - pulledRadius, - Math.max(0, item.ringRadius - ringTolerance), - item.ringRadius + ringTolerance, - item.ringRadius - ); - item.point.x = Math.cos(nextAngle) * nextRadius; - item.point.y = Math.sin(nextAngle) * nextRadius; - } - }; - - for (let pass = 0; pass < iterations; pass += 1) { - separateItems(pass === 0 ? 0.9 : 0.72, pass < 3 ? 0.22 : 0.13); - pullItemsToRings(pass < 3 ? 0.42 : 0.28); - } - - for (let finalPass = 0; finalPass < 5; finalPass += 1) { - pullItemsToRings(finalPass === 0 ? 0.18 : 0.1); - if (!separateItems(finalPass === 0 ? 1 : 0.82, 0.05)) break; - } - - for (const item of items) { - const radius = Math.hypot(item.point.x, item.point.y); - if (radius < 0.001) continue; - item.point.radius = radius; - item.point.angle = Math.atan2(item.point.y, item.point.x); - item.point.labelSide = labelSideForAngle(item.point.angle); - } -} + } -function enforceDepthRingBands(positions, graph, spacingProfile, nodeGap, ringTargets = new Map()) { - if (!positions || positions.size < 2) return; - - const graphNodesById = graph.nodesById || new Map((graph.nodes || []).map(node => [node.id, node])); - let maxLinkDegree = 1; - for (const node of graph.nodes || []) { - maxLinkDegree = Math.max(maxLinkDegree, (node.linkCount || 0) + (node.backlinkCount || 0)); - } - - const byDepth = new Map(); - for (const [id, point] of positions.entries()) { - const node = graphNodesById.get(id); - if (!node || !point) continue; - - const depth = Math.max(0, Math.round(point.depth || 0)); - if (depth === 0) { - point.x = 0; - point.y = 0; - point.radius = 0; - point.angle = -Math.PI / 2; - continue; - } - - const visualRadius = nodeRadius(node, point, maxLinkDegree); - const labelDemand = labelCollisionPadding(node) - + labelArcPadding(node) * clampFloat(0.58 + Math.min(1, depth / 4) * 0.42, 0.58, 1, 0.72); - const arcDemand = visualRadius * 2.75 + labelDemand * 2.7 + Math.max(22, nodeGap * 0.22); - const currentAngle = normalizeAngle(Number.isFinite(point.angle) ? point.angle : Math.atan2(point.y, point.x)); - const parentPoint = node.parentId !== null && node.parentId !== undefined - ? positions.get(node.parentId) - : null; - const parentAngle = parentPoint && Number.isFinite(parentPoint.angle) - ? normalizeAngle(parentPoint.angle) - : currentAngle; - const preferred = parentPoint - ? blendAngles(currentAngle, parentAngle, 0.68) - : currentAngle; - if (!byDepth.has(depth)) byDepth.set(depth, []); - byDepth.get(depth).push({ - id, - node, - point, - depth, - parentId: node.parentId, - parentAngle, - visualRadius, - arcDemand, - preferred - }); - } - - let previousOuterRadius = 0; - const depths = Array.from(byDepth.keys()).sort((a, b) => a - b); - const maxDepth = Math.max(...depths, 1); - for (const depth of depths) { - const items = byDepth.get(depth); - if (!items || !items.length) continue; - - const totalArcDemand = items.reduce((sum, item) => sum + item.arcDemand, 0); - const maxVisualRadius = Math.max(...items.map(item => item.visualRadius), 4); - const baseTarget = Number.isFinite(ringTargets.get(depth)) - ? ringTargets.get(depth) - : Math.max(spacingProfile.ringGap * depth, previousOuterRadius + spacingProfile.ringGap * 0.62); - const depthRatio = clampFloat((depth - 1) / Math.max(1, maxDepth - 1), 0, 1, 0); - const outerDensity = Math.pow(depthRatio, 1.35); - const laneUtilization = clampFloat(0.72 - outerDensity * 0.16 - Math.min(0.1, items.length / 4200), 0.5, 0.72, 0.62); - const baseCapacity = Math.max(1, Math.PI * 2 * baseTarget * laneUtilization); - const maxLaneCount = outerRingLaneLimit(items.length, depthRatio); - const laneCount = clampNumber( - Math.ceil(totalArcDemand / baseCapacity), - 1, - maxLaneCount, - 1 - ); - const laneGap = Math.max( - maxVisualRadius * (2.45 + outerDensity * 0.7) + Math.max(12, nodeGap * (0.13 + outerDensity * 0.04)), - spacingProfile.ringGap * (0.11 + outerDensity * 0.045) - ); - const depthJaggedFactor = ringJaggedDepthFactor(depth, baseTarget, spacingProfile.ringGap, items.length, totalArcDemand); - const firstLaneRadius = Math.max( - baseTarget - laneGap * (laneCount - 1) * 0.5, - previousOuterRadius + laneGap * 0.86 - ); - const lanes = Array.from({ length: laneCount }, () => []); - const orderedGroups = orderRingGroupsByParent(items); - for (const group of orderedGroups) { - const groupItems = orderRingItemsByPreferredGap(group.items); - if (laneCount === 1) { - lanes[0].push(...groupItems); - continue; - } - - const laneIndex = chooseRingLaneForGroup(lanes, groupItems, group.parentAngle); - lanes[laneIndex].push(...groupItems); - } - - const laneRadii = []; - const laneOuterRadii = []; - for (let laneIndex = 0; laneIndex < lanes.length; laneIndex += 1) { - const laneItems = orderRingItemsByParentThenPreferred(lanes[laneIndex]); - if (!laneItems.length) continue; - - let laneRadius = firstLaneRadius + laneIndex * laneGap; - const laneArcDemand = laneItems.reduce((sum, item) => sum + item.arcDemand, 0); - const requiredRadius = laneArcDemand / (Math.PI * 2 * laneUtilization); - laneRadius = Math.max(laneRadius, requiredRadius, previousOuterRadius + laneGap * 0.72); - const laneJaggedFactor = depthJaggedFactor * ringJaggedDensityFactor( - laneItems.length, - countRingParents(laneItems), - laneArcDemand, - laneRadius - ); - const candidateJitterBand = Math.min( - spacingProfile.ringGap * RING_JAGGED_BAND_FACTOR * laneJaggedFactor, - laneRadius * RING_JAGGED_MAX_FACTOR * Math.min(1.35, laneJaggedFactor) - ); - laneRadius = Math.max(laneRadius, previousOuterRadius + candidateJitterBand * 0.56 + laneGap * 0.68); - const jitterBand = Math.min( - candidateJitterBand, - Math.max(0, laneRadius - previousOuterRadius - maxVisualRadius * 2.2 - Math.max(12, nodeGap * 0.08)) - ); - const laneOccupancy = laneArcDemand / Math.max(1, Math.PI * 2 * laneRadius); - placeItemsOnRingLane(laneItems, laneRadius, { - jitterBand, - preservePreferred: laneItems.length < 90 || laneOccupancy < 0.38 || outerDensity < 0.28 - }); - laneRadii.push(laneRadius); - laneOuterRadii.push(laneRadius + jitterBand); - } - - if (laneRadii.length) { - const ringRadius = medianNumber(laneRadii, baseTarget); - ringTargets.set(depth, ringRadius); - previousOuterRadius = Math.max(...laneOuterRadii) + maxVisualRadius * 1.55 + Math.max(12, nodeGap * 0.08); - } - } -} + float theta = dTheta * float( i ); + gl_FragColor.rgb += weights[ i ] * getSample( -1.0 * theta, axis ); + gl_FragColor.rgb += weights[ i ] * getSample( theta, axis ); -function orderRingItemsByPreferredGap(items) { - const sorted = (items || []) - .slice() - .sort((a, b) => normalizeAngle(a.preferred) - normalizeAngle(b.preferred)); - if (sorted.length <= 2) return sorted; - - let largestGap = -1; - let largestGapIndex = 0; - for (let index = 0; index < sorted.length; index += 1) { - const current = normalizeAngle(sorted[index].preferred); - const next = normalizeAngle(sorted[(index + 1) % sorted.length].preferred) + (index === sorted.length - 1 ? Math.PI * 2 : 0); - const gap = next - current; - if (gap > largestGap) { - largestGap = gap; - largestGapIndex = index; - } - } - - const start = (largestGapIndex + 1) % sorted.length; - return sorted.slice(start).concat(sorted.slice(0, start)); -} + } -function outerRingLaneLimit(itemCount, depthRatio) { - const count = Math.max(0, Number(itemCount) || 0); - const outer = clampFloat(depthRatio, 0, 1, 0); - const base = count > 1600 - ? 14 - : count > 900 - ? 12 - : count > 420 - ? 10 - : count > 180 - ? 8 - : count > 80 - ? 6 - : 4; - const outerBonus = outer > 0.72 - ? 3 - : outer > 0.48 - ? 2 - : outer > 0.28 - ? 1 - : 0; - return clampNumber(base + outerBonus, 4, 16, 6); -} + } + `,blending:yn,depthTest:!1,depthWrite:!1})}function Ad(){return new ut({name:"EquirectangularToCubeUV",uniforms:{envMap:{value:null}},vertexShader:sl(),fragmentShader:` -function orderRingGroupsByParent(items) { - const groupsByParent = new Map(); - for (const item of items || []) { - const key = item.parentId === null || item.parentId === undefined ? item.id : item.parentId; - if (!groupsByParent.has(key)) { - groupsByParent.set(key, { - parentId: key, - parentAngle: item.parentAngle, - arcDemand: 0, - items: [] - }); - } - const group = groupsByParent.get(key); - group.items.push(item); - group.arcDemand += item.arcDemand || 0; - group.parentAngle = averageAngles(group.items.map(child => child.parentAngle), group.parentAngle); - } - - return Array.from(groupsByParent.values()) - .sort((a, b) => normalizeAngle(a.parentAngle) - normalizeAngle(b.parentAngle)); -} + precision mediump float; + precision mediump int; -function chooseRingLaneForGroup(lanes, groupItems, parentAngle) { - let bestIndex = 0; - let bestScore = Infinity; - const groupArc = groupItems.reduce((sum, item) => sum + (item.arcDemand || 0), 0); - - for (let index = 0; index < lanes.length; index += 1) { - const lane = lanes[index]; - const laneArc = lane.reduce((sum, item) => sum + (item.arcDemand || 0), 0); - const last = lane[lane.length - 1]; - const angleCost = last - ? Math.abs(shortestAngleDelta(last.parentAngle || last.preferred, parentAngle)) - : 0; - const score = laneArc + groupArc * 0.18 + angleCost * 180; - if (score < bestScore) { - bestScore = score; - bestIndex = index; - } - } - - return bestIndex; -} + varying vec3 vOutputDirection; -function orderRingItemsByParentThenPreferred(items) { - const groups = orderRingGroupsByParent(items); - const ordered = []; - for (const group of groups) { - ordered.push(...orderRingItemsByPreferredGap(group.items)); - } - return ordered; -} + uniform sampler2D envMap; -function placeItemsOnRingLane(items, radius, options = {}) { - if (!items.length || !Number.isFinite(radius) || radius <= 0) return; - - const fullCircle = Math.PI * 2; - const arcs = items.map(item => Math.max(0.003, item.arcDemand / radius)); - const totalArc = arcs.reduce((sum, arc) => sum + arc, 0); - const jitterBand = clampFloat(options.jitterBand, 0, Math.max(0, radius * RING_JAGGED_MAX_FACTOR), 0); - const parentOffsets = parentRadialOffsetsForLane(items, jitterBand); - const minGap = Math.min(0.11, Math.max(0.012, (totalArc / Math.max(1, items.length)) * 0.18)); - const minDemand = totalArc + minGap * Math.max(0, items.length - 1); - const preservePreferred = options.preservePreferred !== false; - const canPreservePreferred = preservePreferred && items.length <= 640 && minDemand < fullCircle * 0.9; - - if (canPreservePreferred && placeItemsNearPreferredAngles(items, arcs, radius, jitterBand, parentOffsets, minGap)) { - return; - } - - const extraGap = Math.max(0, (fullCircle - totalArc) / items.length); - let cursor = normalizeAngle(items[0].preferred) - (arcs[0] + extraGap) * 0.5; - for (let index = 0; index < items.length; index += 1) { - const width = arcs[index] + extraGap; - const angle = cursor + width * 0.5; - setRingLanePoint(items[index], radius, angle, jitterBand, parentOffsets); - cursor += width; - } -} + #include -function placeItemsNearPreferredAngles(items, arcs, radius, jitterBand, parentOffsets, minGap) { - if (!items.length) return true; - const fullCircle = Math.PI * 2; - const entries = items.map((item, index) => ({ - item, - arc: arcs[index], - preferred: normalizeAngle(Number.isFinite(item.preferred) ? item.preferred : item.parentAngle || 0) - })).sort((a, b) => a.preferred - b.preferred); - - if (entries.length > 1) { - let largestGap = -1; - let largestGapIndex = 0; - for (let index = 0; index < entries.length; index += 1) { - const current = entries[index].preferred; - const next = entries[(index + 1) % entries.length].preferred + (index === entries.length - 1 ? fullCircle : 0); - const gap = next - current; - if (gap > largestGap) { - largestGap = gap; - largestGapIndex = index; - } - } - const start = (largestGapIndex + 1) % entries.length; - const rotated = entries.slice(start).concat(entries.slice(0, start)); - entries.splice(0, entries.length, ...rotated); - } - - const angles = []; - let wrapOffset = 0; - let previous = entries[0].preferred; - angles[0] = previous; - for (let index = 1; index < entries.length; index += 1) { - let angle = entries[index].preferred + wrapOffset; - while (angle <= previous) { - wrapOffset += fullCircle; - angle = entries[index].preferred + wrapOffset; - } - angles[index] = angle; - previous = angle; - } - - const preferredCenter = (angles[0] + angles[angles.length - 1]) * 0.5; - for (let pass = 0; pass < 3; pass += 1) { - for (let index = 1; index < entries.length; index += 1) { - const minDelta = (entries[index - 1].arc + entries[index].arc) * 0.5 + minGap; - if (angles[index] - angles[index - 1] < minDelta) { - angles[index] = angles[index - 1] + minDelta; - } - } - } - - const span = angles[angles.length - 1] + entries[entries.length - 1].arc * 0.5 - - (angles[0] - entries[0].arc * 0.5); - if (span > fullCircle - minGap) return false; - - const currentCenter = (angles[0] + angles[angles.length - 1]) * 0.5; - const centerShift = preferredCenter - currentCenter; - for (let index = 0; index < entries.length; index += 1) { - setRingLanePoint(entries[index].item, radius, normalizeAngle(angles[index] + centerShift), jitterBand, parentOffsets); - } - return true; -} + void main() { -function setRingLanePoint(item, radius, angle, jitterBand, parentOffsets) { - const actualRadius = jaggedRingRadius(item, radius, jitterBand, parentOffsets); - const point = item.point; - point.x = Math.cos(angle) * actualRadius; - point.y = Math.sin(angle) * actualRadius; - point.radius = actualRadius; - point.ringRadius = radius; - point.ringBandMin = radius - jitterBand; - point.ringBandMax = radius + jitterBand; - point.angle = angle; - point.labelSide = labelSideForAngle(angle); -} + vec3 outputDirection = normalize( vOutputDirection ); + vec2 uv = equirectUv( outputDirection ); -function parentRadialOffsetsForLane(items, jitterBand) { - const offsets = new Map(); - if (!Array.isArray(items) || !items.length || !Number.isFinite(jitterBand) || jitterBand <= 0) return offsets; - - const parentOrder = []; - const seen = new Set(); - for (const item of items) { - const key = ringParentKey(item); - if (seen.has(key)) continue; - seen.add(key); - parentOrder.push(key); - } - - const lanePattern = [0, -0.96, 0.96, -0.54, 0.54, -0.78, 0.78, -0.28, 0.28]; - for (let index = 0; index < parentOrder.length; index += 1) { - const key = parentOrder[index]; - const base = lanePattern[index % lanePattern.length]; - const variation = deterministicUnitOffset(key, "ring-parent-variation") * 0.12; - offsets.set(key, clampFloat(base + variation, -1, 1, 0) * jitterBand); - } - - return offsets; -} + gl_FragColor = vec4( texture2D ( envMap, uv ).rgb, 1.0 ); -function ringJaggedDepthFactor(depth, radius, ringGap, itemCount, arcDemand) { - const normalizedDepth = Math.max(0, Number(depth) || 0); - const normalizedRadius = Number.isFinite(radius) && Number.isFinite(ringGap) && ringGap > 0 - ? radius / ringGap - : normalizedDepth; - const occupancy = Number.isFinite(radius) && radius > 0 - ? arcDemand / Math.max(1, Math.PI * 2 * radius) - : 0; - const outerFactor = clampFloat(0.82 + normalizedDepth * 0.06 + Math.sqrt(Math.max(0, normalizedRadius)) * 0.13, 0.86, 1.62, 1); - const densityFactor = itemCount <= 5 - ? 0.48 - : itemCount <= 10 - ? 0.68 - : occupancy < 0.12 - ? 0.62 - : occupancy < 0.24 - ? 0.82 - : occupancy > 0.52 - ? 1.16 - : 1; - return clampFloat(outerFactor * densityFactor, 0.42, 1.72, 1); -} + } + `,blending:yn,depthTest:!1,depthWrite:!1})}function Cd(){return new ut({name:"CubemapToCubeUV",uniforms:{envMap:{value:null},flipEnvMap:{value:-1}},vertexShader:sl(),fragmentShader:` -function ringJaggedDensityFactor(itemCount, parentCount, arcDemand, radius) { - const count = Math.max(0, Number(itemCount) || 0); - const parents = Math.max(1, Number(parentCount) || 1); - const occupancy = Number.isFinite(radius) && radius > 0 - ? arcDemand / Math.max(1, Math.PI * 2 * radius) - : 0; - const childFactor = count <= 3 - ? 0.42 - : count <= 7 - ? 0.66 - : count <= 14 - ? 0.86 - : 1.05; - const parentFactor = parents <= 1 - ? 0.52 - : parents <= 2 - ? 0.72 - : parents <= 4 - ? 0.92 - : 1.08; - const occupancyFactor = occupancy < 0.1 - ? 0.56 - : occupancy < 0.2 - ? 0.78 - : occupancy > 0.55 - ? 1.14 - : 1; - return clampFloat(childFactor * parentFactor * occupancyFactor, 0.32, 1.22, 1); -} + precision mediump float; + precision mediump int; -function countRingParents(items) { - const parents = new Set(); - for (const item of items || []) { - parents.add(ringParentKey(item)); - } - return parents.size; -} + uniform float flipEnvMap; -function jaggedRingRadius(item, radius, jitterBand, parentOffsets = null) { - if (!item || !Number.isFinite(jitterBand) || jitterBand <= 0) return radius; - const parentKey = ringParentKey(item); - const parentOffset = parentOffsets && parentOffsets.has(parentKey) - ? parentOffsets.get(parentKey) - : deterministicUnitOffset(parentKey, "ring-parent") * jitterBand * 0.92; - const childOffset = deterministicUnitOffset(item.id, "ring-node") * jitterBand * 0.08; - return radius + clampFloat(parentOffset + childOffset, -jitterBand, jitterBand, 0); -} + varying vec3 vOutputDirection; -function ringParentKey(item) { - return item && item.parentId !== null && item.parentId !== undefined ? item.parentId : item?.id; -} + uniform samplerCube envMap; -function blendAngles(from, to, amount) { - return normalizeAngle(from + shortestAngleDelta(from, to) * clampFloat(amount, 0, 1, 0.5)); -} + void main() { -function labelCollisionPadding(node) { - const titleLength = Array.from(String(node.title || "")).length; - const typePad = node.type === "folder" ? 13 : node.type === "external" || node.externalProxy ? 9 : 5; - return clampNumber(Math.sqrt(Math.max(1, titleLength)) * 4.2 + typePad, 10, 60, 16); -} + gl_FragColor = textureCube( envMap, vec3( flipEnvMap * vOutputDirection.x, vOutputDirection.yz ) ); -function labelArcPadding(node) { - const titleLength = Array.from(String(node.title || "")).length; - const folderPad = node.type === "folder" ? 14 : 0; - const externalPad = node.type === "external" || node.externalProxy ? 9 : 0; - return clampNumber( - Math.sqrt(Math.max(1, titleLength)) * 7.5 + Math.min(110, titleLength * 1.25) + folderPad + externalPad, - 24, - 150, - 48 - ); -} + } + `,blending:yn,depthTest:!1,depthWrite:!1})}function sl(){return` -function deterministicPairAngle(a, b) { - const text = `${a}|${b}`; - let hash = 2166136261; - for (let index = 0; index < text.length; index += 1) { - hash ^= text.charCodeAt(index); - hash = Math.imul(hash, 16777619); - } - return ((hash >>> 0) / 4294967296) * Math.PI * 2; -} + precision mediump float; + precision mediump int; -function deterministicUnitOffset(value, salt) { - return Math.sin(deterministicPairAngle(String(value || ""), String(salt || ""))); -} + attribute float faceIndex; -function preferredAngleForNode(node, graph, positions, fallbackAngle) { - const angles = []; + varying vec3 vOutputDirection; - if (node.externalParentId && positions.has(node.externalParentId)) { - angles.push(positions.get(node.externalParentId).angle); - } + // RH coordinate system; PMREM face-indexing convention + vec3 getDirection( vec2 uv, float face ) { - for (const edge of graph.linkEdges || []) { - const otherId = relatedEndpointForNode(node, edge, graph); - if (!otherId) continue; - const otherPoint = positions.get(otherId); - if (otherPoint && Number.isFinite(otherPoint.angle)) { - angles.push(otherPoint.angle); - } - } + uv = 2.0 * uv - 1.0; - return averageAngles(angles, fallbackAngle); -} + vec3 direction = vec3( uv, 1.0 ); -function relatedEndpointForNode(node, edge, graph) { - if (edge.source === node.id) return edge.target; - if (edge.target === node.id) return edge.source; + if ( face == 0.0 ) { - const nodesById = graph.nodesById; - const sourceNode = nodesById && nodesById.get(edge.source); - const targetNode = nodesById && nodesById.get(edge.target); - if (sourceNode && sourceNode.externalParentId === node.id) return edge.target; - if (targetNode && targetNode.externalParentId === node.id) return edge.source; - return null; -} + direction = direction.zyx; // ( 1, v, u ) pos x -function averageAngles(angles, fallbackAngle) { - if (!angles.length) return fallbackAngle; - const sum = angles.reduce((acc, angle) => { - acc.x += Math.cos(angle); - acc.y += Math.sin(angle); - return acc; - }, { x: 0, y: 0 }); - if (Math.abs(sum.x) < 0.0001 && Math.abs(sum.y) < 0.0001) return fallbackAngle; - return Math.atan2(sum.y, sum.x); -} + } else if ( face == 1.0 ) { -function radialLayoutBounds(positions, routeMaxRadius) { - let minX = -Math.max(1, routeMaxRadius || 1); - let minY = -Math.max(1, routeMaxRadius || 1); - let maxX = Math.max(1, routeMaxRadius || 1); - let maxY = Math.max(1, routeMaxRadius || 1); - - for (const point of positions.values()) { - const labelPadX = 170; - const labelPadTop = 46; - const labelPadBottom = 126; - minX = Math.min(minX, point.x - labelPadX); - minY = Math.min(minY, point.y - labelPadTop); - maxX = Math.max(maxX, point.x + labelPadX); - maxY = Math.max(maxY, point.y + labelPadBottom); - } - - return { minX, minY, maxX, maxY }; -} + direction = direction.xzy; + direction.xz *= -1.0; // ( -u, 1, -v ) pos y -function shiftRadialLayout(positions, routes, offsetX, offsetY) { - for (const point of positions.values()) { - point.x += offsetX; - point.y += offsetY; - point.centerX = offsetX; - point.centerY = offsetY; - } - - for (const route of routes.values()) { - if (!Number.isFinite(route.centerX) || !Number.isFinite(route.centerY)) continue; - route.centerX += offsetX; - route.centerY += offsetY; - } -} + } else if ( face == 2.0 ) { -function anchorLayoutHomePositions(positions) { - for (const point of positions.values()) { - if (!point) continue; - point.homeX = point.x; - point.homeY = point.y; - point.homeRadius = point.radius; - point.homeAngle = point.angle; - } -} + direction.x *= -1.0; // ( -u, v, 1 ) pos z -function applyRadialSwirl(positions, graph, spacingProfile, strength) { - const amount = clampFloat(strength, 0, 1, 0); - if (!positions || !positions.size || amount <= 0.001) return; - - const ringGap = Math.max(1, Number(spacingProfile?.ringGap) || DEFAULT_RING_SPACING); - const rootId = graph?.rootId || ROOT_ID; - const direction = deterministicUnitOffset(rootId || "vault", "swirl-direction") >= 0 ? 1 : -1; - - for (const [id, point] of positions.entries()) { - if (!point || !Number.isFinite(point.radius) || point.radius <= 0.001) continue; - - const depth = Math.max(0, Number(point.depth) || 0); - const baseAngle = Number.isFinite(point.angle) - ? point.angle - : Math.atan2(point.y, point.x); - const radialPhase = Math.sqrt(Math.max(0, point.radius) / ringGap); - const armVariation = deterministicUnitOffset(id, "swirl-arm") * 0.16; - const wave = Math.sin(baseAngle * 2.35 + depth * 0.78) * 0.11; - const turn = direction * amount * (depth * 0.32 + radialPhase * 0.22 + armVariation + wave); - const angle = normalizeAngle(baseAngle + turn); - - point.x = Math.cos(angle) * point.radius; - point.y = Math.sin(angle) * point.radius; - point.angle = angle; - point.swirlOffset = turn; - point.labelSide = labelSideForAngle(angle); - } -} + } else if ( face == 3.0 ) { -function computeLayoutRings(positions, ringTargets = null) { - if (ringTargets && ringTargets.size) { - const ringsByKey = new Map(); - for (const point of positions.values()) { - if (!point || !Number.isFinite(point.depth) || point.depth <= 0) continue; - const depth = Math.max(0, Math.round(point.depth || 0)); - const radius = Number.isFinite(point.ringRadius) - ? point.ringRadius - : ringTargets.get(depth); - if (!Number.isFinite(radius) || radius <= 0) continue; - const key = `${depth}:${Math.round(radius)}`; - const existing = ringsByKey.get(key); - if (existing) { - existing.count += 1; - existing.radiusTotal += radius; - } else { - ringsByKey.set(key, { - depth, - radiusTotal: radius, - count: 1 - }); - } - } - return Array.from(ringsByKey.values()) - .map(ring => ({ - depth: ring.depth, - radius: ring.radiusTotal / Math.max(1, ring.count), - count: ring.count - })) - .sort((a, b) => a.radius - b.radius); - } - - const byDepth = new Map(); - for (const point of positions.values()) { - if (!point || !Number.isFinite(point.depth) || point.depth <= 0) continue; - if (!Number.isFinite(point.radius) || point.radius <= 0) continue; - if (!byDepth.has(point.depth)) byDepth.set(point.depth, []); - byDepth.get(point.depth).push(point.radius); - } - - return Array.from(byDepth.entries()) - .map(([depth, radii]) => { - const sorted = radii.slice().sort((a, b) => a - b); - const middle = Math.floor(sorted.length / 2); - const median = sorted.length % 2 - ? sorted[middle] - : (sorted[middle - 1] + sorted[middle]) / 2; - return { depth, radius: median, count: sorted.length }; - }) - .filter(ring => ring.count >= 2) - .sort((a, b) => a.depth - b.depth); -} + direction = direction.zyx; + direction.xz *= -1.0; // ( -1, v, -u ) neg x -function radialPoint(centerX, centerY, radius, angle) { - return { - x: centerX + Math.cos(angle) * radius, - y: centerY + Math.sin(angle) * radius - }; -} + } else if ( face == 4.0 ) { -function drawCanvasRingPath(ctx, centerX, centerY, radius, depth = 0, swirlStrength = 0, orbitPhase = 0) { - const strength = clampFloat(swirlStrength, 0, 1, 0); - if (strength <= 0.001) { - ctx.arc(centerX, centerY, radius, 0, Math.PI * 2); - return; - } - - const fullCircle = Math.PI * 2; - const steps = 180; - const amplitude = Math.min(radius * 0.045, 58) * strength; - const phase = depth * 0.74 + strength * 0.9 + orbitPhase; - - for (let step = 0; step <= steps; step += 1) { - const t = step / steps; - const angle = t * fullCircle; - const twist = Math.sin(angle - phase) * 0.08 * strength; - const ripple = Math.sin(angle * 2 + phase) * amplitude - + Math.sin(angle * 5 - phase * 0.7) * amplitude * 0.26; - const r = Math.max(1, radius + ripple); - const x = centerX + Math.cos(angle + twist) * r; - const y = centerY + Math.sin(angle + twist) * r; - if (step === 0) ctx.moveTo(x, y); - else ctx.lineTo(x, y); - } - ctx.closePath(); -} + direction = direction.xzy; + direction.xy *= -1.0; // ( -u, -1, v ) neg y -function swirlOrbitAngleForRing(depth, radius, strength, elapsedSeconds) { - const amount = clampFloat(strength, 0, 1, 0); - if (amount <= 0.001 || depth <= 0 || !Number.isFinite(elapsedSeconds)) return 0; - - const depthIndex = Math.max(1, Math.round(depth)); - const direction = Math.floor((depthIndex - 1) / 2) % 2 === 0 ? 1 : -1; - const outerSlowdown = clampFloat(1 / Math.sqrt(1 + Math.max(0, depthIndex - 1) * 0.24), 0.46, 1, 1); - const radiusSlowdown = Number.isFinite(radius) && radius > 0 - ? clampFloat(Math.sqrt(DEFAULT_RING_SPACING / Math.max(DEFAULT_RING_SPACING, radius)), 0.56, 1, 1) - : 1; - const subtleVariation = 0.86 + Math.sin(depthIndex * 1.17) * 0.1 + Math.cos(depthIndex * 0.53) * 0.045; - const speed = SWIRL_BASE_SPEED_RAD_PER_SEC * amount * outerSlowdown * radiusSlowdown * subtleVariation; - return direction * speed * elapsedSeconds; -} + } else if ( face == 5.0 ) { -function labelSideForAngle(angle) { - return Math.cos(angle) < -0.18 ? -1 : 1; -} + direction.z *= -1.0; // ( u, v, -1 ) neg z -function normalizeAngle(angle) { - const full = Math.PI * 2; - return ((angle % full) + full) % full; -} + } -function shortestAngleDelta(from, to) { - const full = Math.PI * 2; - return ((to - from + Math.PI) % full + full) % full - Math.PI; -} + return direction; -function compareNodes(a, b) { - if (!a || !b) return 0; - if (a.type !== b.type) { - const order = { folder: 0, note: 1, external: 2, unresolved: 3 }; - return (order[a.type] || 9) - (order[b.type] || 9); - } - return a.title.localeCompare(b.title, undefined, { sensitivity: "base" }); -} + } -function normalizeSettings(saved) { - const source = saved && typeof saved === "object" ? saved : {}; - const settings = Object.assign({}, DEFAULT_SETTINGS, source); - const hasLegacyBudget = source.atlasDepth === LEGACY_DEFAULT_SETTINGS.atlasDepth - && source.linkLimit === LEGACY_DEFAULT_SETTINGS.linkLimit - && source.renderNodeLimit === LEGACY_DEFAULT_SETTINGS.renderNodeLimit; - - if (hasLegacyBudget) { - settings.atlasDepth = DEFAULT_SETTINGS.atlasDepth; - settings.focusSiblingLimit = DEFAULT_SETTINGS.focusSiblingLimit; - settings.linkLimit = DEFAULT_SETTINGS.linkLimit; - settings.renderNodeLimit = DEFAULT_SETTINGS.renderNodeLimit; - settings.externalLinkAnchorLimit = DEFAULT_SETTINGS.externalLinkAnchorLimit; - if (source.enableLinkHover === LEGACY_DEFAULT_SETTINGS.enableLinkHover) { - settings.enableLinkHover = DEFAULT_SETTINGS.enableLinkHover; - } - } - - settings.atlasDepth = clampNumber(settings.atlasDepth, 1, MAX_ATLAS_DEPTH, DEFAULT_SETTINGS.atlasDepth); - settings.focusSiblingLimit = clampNumber(settings.focusSiblingLimit, 10, 1000, DEFAULT_SETTINGS.focusSiblingLimit); - settings.linkLimit = clampNumber(settings.linkLimit, 0, MAX_LINK_LIMIT, DEFAULT_SETTINGS.linkLimit); - settings.renderNodeLimit = clampNumber(settings.renderNodeLimit, 200, MAX_RENDER_NODE_LIMIT, DEFAULT_SETTINGS.renderNodeLimit); - settings.externalLinkAnchorLimit = clampNumber(settings.externalLinkAnchorLimit, 0, MAX_EXTERNAL_LINK_ANCHOR_LIMIT, DEFAULT_SETTINGS.externalLinkAnchorLimit); - settings.externalDetailMode = ["grouped", "selected", "exact"].includes(settings.externalDetailMode) - ? settings.externalDetailMode - : DEFAULT_SETTINGS.externalDetailMode; - settings.colorScheme = normalizeColorScheme(settings.colorScheme); - settings.labelVisibility = normalizeLabelVisibility(settings.labelVisibility); - settings.hoverHighlightMode = normalizeHoverHighlightMode( - source.hoverHighlightMode !== undefined - ? source.hoverHighlightMode - : settings.enableLinkHover - ? "note-links" - : DEFAULT_SETTINGS.hoverHighlightMode - ); - settings.enableLinkHover = hoverHighlightsNoteLinks(settings.hoverHighlightMode); - settings.swirlStrength = clampNumber(settings.swirlStrength, 0, MAX_SWIRL_STRENGTH, DEFAULT_SETTINGS.swirlStrength); - const legendItemIds = new Set(LEGEND_ITEM_DEFINITIONS.map(([id]) => id)); - settings.hiddenLegendItems = Array.isArray(settings.hiddenLegendItems) - ? settings.hiddenLegendItems.filter(id => legendItemIds.has(id)) - : DEFAULT_SETTINGS.hiddenLegendItems.slice(); - settings.ignoreFolders = Array.isArray(settings.ignoreFolders) - ? settings.ignoreFolders.filter(Boolean) - : DEFAULT_SETTINGS.ignoreFolders.slice(); - - return settings; -} + void main() { -function normalizeLabelVisibility(value) { - const normalized = String(value || "").trim(); - return LABEL_VISIBILITY_OPTIONS.some(([optionValue]) => optionValue === normalized) - ? normalized - : DEFAULT_SETTINGS.labelVisibility; -} + vOutputDirection = getDirection( uv, faceIndex ); + gl_Position = vec4( position, 1.0 ); -function normalizeHoverHighlightMode(value) { - const normalized = String(value || "").trim(); - return HOVER_HIGHLIGHT_MODE_OPTIONS.some(([optionValue]) => optionValue === normalized) - ? normalized - : DEFAULT_SETTINGS.hoverHighlightMode; -} + } + `}var il=class extends Rt{constructor(e=1,t={}){super(e,e,t),this.isWebGLCubeRenderTarget=!0;let n={width:e,height:e,depth:1},s=[n,n,n,n,n,n];this.texture=new ur(s),this._setTextureOptions(t),this.texture.isRenderTargetTexture=!0}fromEquirectangularTexture(e,t){this.texture.type=t.type,this.texture.colorSpace=t.colorSpace,this.texture.generateMipmaps=t.generateMipmaps,this.texture.minFilter=t.minFilter,this.texture.magFilter=t.magFilter;let n={uniforms:{tEquirect:{value:null}},vertexShader:` -function hoverHighlightsNoteLinks(mode) { - return normalizeHoverHighlightMode(mode) === "note-links"; -} + varying vec3 vWorldDirection; -function hoverHighlightModeLabel(mode) { - const normalized = normalizeHoverHighlightMode(mode); - const option = HOVER_HIGHLIGHT_MODE_OPTIONS.find(([value]) => value === normalized); - return option ? option[1].toLowerCase() : normalized; -} + vec3 transformDirection( in vec3 dir, in mat4 matrix ) { -function normalizeColorScheme(value) { - return COLOR_SCHEME_OPTIONS.includes(value) ? value : DEFAULT_SETTINGS.colorScheme; -} + return normalize( ( matrix * vec4( dir, 0.0 ) ).xyz ); -function nextColorScheme(value) { - const current = normalizeColorScheme(value); - const index = COLOR_SCHEME_OPTIONS.indexOf(current); - return COLOR_SCHEME_OPTIONS[(index + 1) % COLOR_SCHEME_OPTIONS.length]; -} + } -function colorSchemeLabel(value) { - const scheme = normalizeColorScheme(value); - if (scheme === "day") return "Day"; - if (scheme === "night") return "Night"; - return "Auto"; -} + void main() { -function colorSchemeIcon(value) { - const scheme = normalizeColorScheme(value); - if (scheme === "day") return "sun"; - if (scheme === "night") return "moon"; - return "monitor"; -} + vWorldDirection = transformDirection( position, modelMatrix ); -function nodeWithinRoot(index, node, rootId) { - if (!node) return false; - if (rootId === ROOT_ID || rootId === null || rootId === undefined) return true; - - let current = node; - const seen = new Set(); - while (current && !seen.has(current.id)) { - if (current.id === rootId) return true; - seen.add(current.id); - current = current.parentId === null || current.parentId === undefined - ? null - : index.nodes.get(current.parentId); - } - return false; -} + #include + #include -function vaultRootTitle(app) { - const name = app?.vault?.getName?.(); - return String(name || ROOT_TITLE).trim() || ROOT_TITLE; -} + } + `,fragmentShader:` -function basename(path) { - if (!path) return ROOT_TITLE; - const parts = path.split("/"); - return parts[parts.length - 1] || ROOT_TITLE; -} + uniform sampler2D tEquirect; -function depthOfPath(path) { - if (!path) return 0; - return path.split("/").filter(Boolean).length; -} + varying vec3 vWorldDirection; -function parentPath(path) { - const normalizedPath = normalizeVaultPath(path); - if (!normalizedPath || !normalizedPath.includes("/")) return ROOT_ID; - return normalizedPath.split("/").slice(0, -1).join("/"); -} + #include -function normalizeVaultPath(path) { - const normalized = String(path || "").trim().replace(/^\/+|\/+$/g, ""); - return normalized === "." ? ROOT_ID : normalized; -} + void main() { -function comparableTitle(title) { - return String(title || "") - .toLowerCase() - .normalize("NFKD") - .replace(/\.md$/i, "") - .replace(/^[^\p{Letter}\p{Number}]+/u, "") - .replace(/[^\p{Letter}\p{Number}]+/gu, ""); -} + vec3 direction = normalize( vWorldDirection ); -function normalizedQuery(query) { - return String(query || "").trim().toLowerCase(); -} + vec2 sampleUV = equirectUv( direction ); -function nodeMatches(node, query) { - if (!query) return false; - return String(node.title || "").toLowerCase().includes(query) - || String(node.path || "").toLowerCase().includes(query); -} + gl_FragColor = texture2D( tEquirect, sampleUV ); -function nodeRadius(node, point, maxLinkDegree = 1) { - const degree = Math.max(0, (node.linkCount || 0) + (node.backlinkCount || 0)); - const degreeRatio = Math.log1p(degree) / Math.max(1, Math.log1p(maxLinkDegree || 1)); - const clampedRatio = clampFloat(degreeRatio, 0, 1, 0); - const degreeCurve = Math.pow(clampedRatio, 0.48); - const hubCurve = Math.pow(clampedRatio, 1.32); - const degreeBoost = degreeCurve * 19 - + hubCurve * 20 - + Math.log2(degree + 1) * 1.35 - + Math.sqrt(degree) * 0.32; - - if (node.externalProxy) return node.type === "unresolved" ? 5.4 : Math.min(27, 5.8 + degreeBoost * 0.62); - if (node.type === "folder") { - const noteSignal = Math.log2((node.noteCount || node.descendantCount || 1) + 1); - return Math.min(66, 7 + noteSignal * 1.05 + degreeBoost * 1.18); - } - if (node.type === "external") { - const noteSignal = Math.log2((node.noteCount || 1) + 1); - return Math.min(38, 5.8 + noteSignal * 0.72 + degreeBoost * 0.9); - } - if (node.type === "unresolved") return Math.min(16, 4.2 + degreeBoost * 0.5); - return Math.min(58, 3.6 + degreeBoost * 1.05); -} + } + `},s=new Ts(5,5,5),r=new ut({name:"CubemapFromEquirect",uniforms:Ki(n.uniforms),vertexShader:n.vertexShader,fragmentShader:n.fragmentShader,side:$t,blending:yn});r.uniforms.tEquirect.value=t;let o=new Xt(s,r),a=t.minFilter;return t.minFilter===Li&&(t.minFilter=Bt),new ca(1,10,this).update(e,o),t.minFilter=a,o.geometry.dispose(),o.material.dispose(),this}clear(e,t=!0,n=!0,s=!0){let r=e.getRenderTarget();for(let o=0;o<6;o++)e.setRenderTarget(this,o),e.clear(t,n,s);e.setRenderTarget(r)}};function hv(i){let e=new WeakMap,t=new WeakMap,n=null;function s(h,f=!1){return h==null?null:f?o(h):r(h)}function r(h){if(h&&h.isTexture){let f=h.mapping;if(f===fa||f===pa)if(e.has(h)){let g=e.get(h).texture;return a(g,h.mapping)}else{let g=h.image;if(g&&g.height>0){let x=new il(g.height);return x.fromEquirectangularTexture(i,h),e.set(h,x),h.addEventListener("dispose",c),a(x.texture,h.mapping)}else return null}}return h}function o(h){if(h&&h.isTexture){let f=h.mapping,g=f===fa||f===pa,x=f===Ii||f===$i;if(g||x){let m=t.get(h),p=m!==void 0?m.texture.pmremVersion:0;if(h.isRenderTargetTexture&&h.pmremVersion!==p)return n===null&&(n=new nl(i)),m=g?n.fromEquirectangular(h,m):n.fromCubemap(h,m),m.texture.pmremVersion=h.pmremVersion,t.set(h,m),m.texture;if(m!==void 0)return m.texture;{let v=h.image;return g&&v&&v.height>0||x&&v&&l(v)?(n===null&&(n=new nl(i)),m=g?n.fromEquirectangular(h):n.fromCubemap(h),m.texture.pmremVersion=h.pmremVersion,t.set(h,m),h.addEventListener("dispose",u),m.texture):null}}}return h}function a(h,f){return f===fa?h.mapping=Ii:f===pa&&(h.mapping=$i),h}function l(h){let f=0,g=6;for(let x=0;x=65535?cr:lr)(h,1);m.version=x;let p=r.get(d);p&&e.remove(p),r.set(d,m)}function u(d){let h=r.get(d);if(h){let f=d.index;f!==null&&h.versione.maxTextureSize&&(E=Math.ceil(M/e.maxTextureSize),M=e.maxTextureSize);let w=new Float32Array(M*E*4*d),C=new or(w,M,E,d);C.type=Pn,C.needsUpdate=!0;let _=b*4;for(let P=0;P ({ item, priority: canvasLabelPriority(item, graph) })) - .sort((a, b) => b.priority - a.priority); - const denominator = Math.max(1, scored.length - 1); - - for (let index = 0; index < scored.length; index += 1) { - scored[index].item.labelRank = index; - scored[index].item.labelPriority = scored[index].priority; - scored[index].item.labelPercentile = 1 - index / denominator; - } -} + attribute vec3 position; + attribute vec2 uv; -function canvasLabelPriority(item, graph) { - const node = item.node || {}; - const degree = Math.max(0, (node.linkCount || 0) + (node.backlinkCount || 0)); - let score = Math.max(0, item.radius || 0) * 2.8 - + Math.log1p(degree) * 13 - + Math.sqrt(degree) * 1.4 - - Math.min(34, (node.depth || 0) * 3.2); - - if (graph && node.id === graph.rootId) score += 10000; - if (graph && node.id === graph.focusId) score += 9000; - if (item.searchMatch) score += 8000; - - if (node.type === "folder") { - score += 32 + Math.log1p(node.noteCount || node.descendantCount || 0) * 8; - if (node.representativeFile) score += 8; - } else if (node.type === "note") { - score += 10; - } else if (node.type === "external" || node.externalProxy) { - score -= 10; - } else if (node.type === "unresolved") { - score -= 22; - } - - return score; -} + varying vec2 vUv; -function zoomLabelStrength(item, zoom, graph) { - if (!item || !item.node) return 0; - if (item.searchMatch) return 1; - - const node = item.node; - const degree = Math.max(0, (node.linkCount || 0) + (node.backlinkCount || 0)); - const folder = node.type === "folder"; - const external = node.type === "external" || node.externalProxy; - const unresolved = node.type === "unresolved"; - const root = graph && node.id === graph.rootId; - if (root) { - return 0.68 + smoothstep(0.04, 0.2, zoom) * 0.32; - } - - const sizeSignal = clampFloat((item.radius - 4) / 42, 0, 1, 0); - const degreeSignal = clampFloat(Math.log1p(degree) / Math.log1p(80), 0, 1, 0); - const nodeCount = Math.max(1, (graph?.nodes?.length || 1)); - const rank = Number.isFinite(item.labelRank) ? item.labelRank : nodeCount; - const rankRatio = nodeCount > 1 ? rank / Math.max(1, nodeCount - 1) : 0; - const salienceSignal = clampFloat(1 - rankRatio, 0, 1, 0); - const leadingCount = Math.max(10, Math.min(58, Math.ceil(nodeCount * 0.05))); - const secondaryCount = Math.max(28, Math.min(170, Math.ceil(nodeCount * 0.18))); - const tertiaryCount = Math.max(80, Math.min(520, Math.ceil(nodeCount * 0.38))); - const leading = rank < leadingCount; - const secondary = rank < secondaryCount; - const tertiary = rank < tertiaryCount; - let threshold = 1.24 - sizeSignal * 0.64 - degreeSignal * 0.22 - salienceSignal * 0.42; - - if (leading) threshold -= 0.28; - else if (secondary) threshold -= 0.2; - else if (tertiary) threshold -= 0.08; - if (folder) threshold -= node.representativeFile ? 0.18 : 0.12; - else if (external) threshold -= 0.04; - if (unresolved) threshold += 0.18; - - threshold = clampFloat(threshold, 0.16, 1.38, 0.96); - const fade = smoothstep(threshold - 0.14, threshold + 0.1, zoom); - const leadingFade = leading - ? smoothstep(0.12, 0.38, zoom) - : secondary - ? smoothstep(0.22, 0.62, zoom) * 0.9 - : tertiary - ? smoothstep(0.46, 0.96, zoom) * 0.7 - : 0; - const largeFade = item.radius >= 24 - ? smoothstep(0.22, 0.56, zoom) * 0.96 - : item.radius >= 15 - ? smoothstep(0.42, 0.86, zoom) * 0.78 - : 0; - const smallFade = !unresolved ? smoothstep(0.86, 1.28, zoom) * 0.82 : 0; - const closeFade = !unresolved ? smoothstep(1.12, 1.46, zoom) * 0.98 : 0; - return clampFloat(Math.max(fade, leadingFade, largeFade, smallFade, closeFade), 0, 1, fade); -} + void main() { + vUv = uv; + gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 ); + }`,fragmentShader:` + precision highp float; -function labelScreenScale(zoom) { - const clampedZoom = clampFloat(zoom, MIN_CANVAS_ZOOM, MAX_CANVAS_ZOOM, 1); - return 0.62 + smoothstep(0.06, 1.48, clampedZoom) * 0.88; -} + uniform sampler2D tDiffuse; -function nodeTooltip(node) { - const type = node.externalProxy - ? `outside ${node.type}` - : node.type === "folder" && node.representativeFile - ? "folder + meta file" - : node.isRepresentativeFile - ? "merged meta file" - : node.type; - return [ - node.title, - node.path || "/", - `type: ${type}`, - `notes: ${node.noteCount || 0}`, - `out: ${node.linkCount || 0}`, - `in: ${node.backlinkCount || 0}` - ].join("\n"); -} + varying vec2 vUv; -function isRepresentativeEdge(index, edge) { - const source = index.nodes.get(edge.source); - return Boolean(source && source.representativeFile === edge.target); -} + #include + #include + + void main() { + gl_FragColor = texture2D( tDiffuse, vUv ); + + #ifdef LINEAR_TONE_MAPPING + gl_FragColor.rgb = LinearToneMapping( gl_FragColor.rgb ); + #elif defined( REINHARD_TONE_MAPPING ) + gl_FragColor.rgb = ReinhardToneMapping( gl_FragColor.rgb ); + #elif defined( CINEON_TONE_MAPPING ) + gl_FragColor.rgb = CineonToneMapping( gl_FragColor.rgb ); + #elif defined( ACES_FILMIC_TONE_MAPPING ) + gl_FragColor.rgb = ACESFilmicToneMapping( gl_FragColor.rgb ); + #elif defined( AGX_TONE_MAPPING ) + gl_FragColor.rgb = AgXToneMapping( gl_FragColor.rgb ); + #elif defined( NEUTRAL_TONE_MAPPING ) + gl_FragColor.rgb = NeutralToneMapping( gl_FragColor.rgb ); + #elif defined( CUSTOM_TONE_MAPPING ) + gl_FragColor.rgb = CustomToneMapping( gl_FragColor.rgb ); + #endif + + #ifdef SRGB_TRANSFER + gl_FragColor = sRGBTransferOETF( gl_FragColor ); + #endif + }`,depthTest:!1,depthWrite:!1}),c=new Xt(a,l),u=new si(-1,1,1,-1,0,1),d=null,h=null,f=!1,g,x=null,m=[],p=!1;this.setSize=function(v,b){r.setSize(v,b),o.setSize(v,b);for(let M=0;M0&&m[0].isRenderPass===!0;let b=r.width,M=r.height;for(let E=0;E0)return i;let s=e*t,r=Rd[s];if(r===void 0&&(r=new Float32Array(s),Rd[s]=r),e!==0){n.toArray(r,0);for(let o=1,a=0;o!==e;++o)a+=t,i[o].toArray(r,a)}return r}function Pt(i,e){if(i.length!==e.length)return!1;for(let t=0,n=i.length;t0&&(this.seq=s.concat(r))}setValue(e,t,n,s){let r=this.map[t];r!==void 0&&r.setValue(e,n,s)}setOptional(e,t,n){let s=t[n];s!==void 0&&this.setValue(e,n,s)}static upload(e,t,n,s){for(let r=0,o=t.length;r!==o;++r){let a=t[r],l=n[a.id];l.needsUpdate!==!1&&a.setValue(e,l.value,s)}}static seqWithValue(e,t){let n=[];for(let s=0,r=e.length;s!==r;++s){let o=e[s];o.id in t&&n.push(o)}return n}};function Fd(i,e,t){let n=i.createShader(e);return i.shaderSource(n,t),i.compileShader(n),n}var o_=37297,a_=0;function l_(i,e){let t=i.split(` +`),n=[],s=Math.max(e-6,0),r=Math.min(e+6,t.length);for(let o=s;o":" "} ${a}: ${t[o]}`)}return n.join(` +`)}var kd=new Le;function c_(i){ze._getMatrix(kd,ze.workingColorSpace,i);let e=`mat3( ${kd.elements.map(t=>t.toFixed(4))} )`;switch(ze.getTransfer(i)){case ir:return[e,"LinearTransferOETF"];case Je:return[e,"sRGBTransferOETF"];default:return Te("WebGLProgram: Unsupported color space: ",i),[e,"LinearTransferOETF"]}}function Od(i,e,t){let n=i.getShaderParameter(e,i.COMPILE_STATUS),r=(i.getShaderInfoLog(e)||"").trim();if(n&&r==="")return"";let o=/ERROR: 0:(\d+)/.exec(r);if(o){let a=parseInt(o[1]);return t.toUpperCase()+` + +`+r+` + +`+l_(i.getShaderSource(e),a)}else return r}function h_(i,e){let t=c_(e);return[`vec4 ${i}( vec4 value ) {`,` return ${t[1]}( vec4( value.rgb * ${t[0]}, value.a ) );`,"}"].join(` +`)}var u_={[yr]:"Linear",[br]:"Reinhard",[Mr]:"Cineon",[Pi]:"ACESFilmic",[wr]:"AgX",[Er]:"Neutral",[Sr]:"Custom"};function d_(i,e){let t=u_[e];return t===void 0?(Te("WebGLProgram: Unsupported toneMapping:",e),"vec3 "+i+"( vec3 color ) { return LinearToneMapping( color ); }"):"vec3 "+i+"( vec3 color ) { return "+t+"ToneMapping( color ); }"}var tl=new N;function f_(){ze.getLuminanceCoefficients(tl);let i=tl.x.toFixed(4),e=tl.y.toFixed(4),t=tl.z.toFixed(4);return["float luminance( const in vec3 rgb ) {",` const vec3 weights = vec3( ${i}, ${e}, ${t} );`," return dot( weights, rgb );","}"].join(` +`)}function p_(i){return[i.extensionClipCullDistance?"#extension GL_ANGLE_clip_cull_distance : require":"",i.extensionMultiDraw?"#extension GL_ANGLE_multi_draw : require":""].filter(kr).join(` +`)}function m_(i){let e=[];for(let t in i){let n=i[t];n!==!1&&e.push("#define "+t+" "+n)}return e.join(` +`)}function g_(i,e){let t={},n=i.getProgramParameter(e,i.ACTIVE_ATTRIBUTES);for(let s=0;s/gm;function _h(i){return i.replace(x_,__)}var v_=new Map;function __(i,e){let t=Ue[e];if(t===void 0){let n=v_.get(e);if(n!==void 0)t=Ue[n],Te('WebGLRenderer: Shader chunk "%s" has been deprecated. Use "%s" instead.',e,n);else throw new Error("Can not resolve #include <"+e+">")}return _h(t)}var y_=/#pragma unroll_loop_start\s+for\s*\(\s*int\s+i\s*=\s*(\d+)\s*;\s*i\s*<\s*(\d+)\s*;\s*i\s*\+\+\s*\)\s*{([\s\S]+?)}\s+#pragma unroll_loop_end/g;function zd(i){return i.replace(y_,b_)}function b_(i,e,t,n){let s="";for(let r=parseInt(e);r0&&(m+=` +`),p=["#define SHADER_TYPE "+t.shaderType,"#define SHADER_NAME "+t.shaderName,g].filter(kr).join(` +`),p.length>0&&(p+=` +`)):(m=[Vd(t),"#define SHADER_TYPE "+t.shaderType,"#define SHADER_NAME "+t.shaderName,g,t.extensionClipCullDistance?"#define USE_CLIP_DISTANCE":"",t.batching?"#define USE_BATCHING":"",t.batchingColor?"#define USE_BATCHING_COLOR":"",t.instancing?"#define USE_INSTANCING":"",t.instancingColor?"#define USE_INSTANCING_COLOR":"",t.instancingMorph?"#define USE_INSTANCING_MORPH":"",t.useFog&&t.fog?"#define USE_FOG":"",t.useFog&&t.fogExp2?"#define FOG_EXP2":"",t.map?"#define USE_MAP":"",t.envMap?"#define USE_ENVMAP":"",t.envMap?"#define "+u:"",t.lightMap?"#define USE_LIGHTMAP":"",t.aoMap?"#define USE_AOMAP":"",t.bumpMap?"#define USE_BUMPMAP":"",t.normalMap?"#define USE_NORMALMAP":"",t.normalMapObjectSpace?"#define USE_NORMALMAP_OBJECTSPACE":"",t.normalMapTangentSpace?"#define USE_NORMALMAP_TANGENTSPACE":"",t.displacementMap?"#define USE_DISPLACEMENTMAP":"",t.emissiveMap?"#define USE_EMISSIVEMAP":"",t.anisotropy?"#define USE_ANISOTROPY":"",t.anisotropyMap?"#define USE_ANISOTROPYMAP":"",t.clearcoatMap?"#define USE_CLEARCOATMAP":"",t.clearcoatRoughnessMap?"#define USE_CLEARCOAT_ROUGHNESSMAP":"",t.clearcoatNormalMap?"#define USE_CLEARCOAT_NORMALMAP":"",t.iridescenceMap?"#define USE_IRIDESCENCEMAP":"",t.iridescenceThicknessMap?"#define USE_IRIDESCENCE_THICKNESSMAP":"",t.specularMap?"#define USE_SPECULARMAP":"",t.specularColorMap?"#define USE_SPECULAR_COLORMAP":"",t.specularIntensityMap?"#define USE_SPECULAR_INTENSITYMAP":"",t.roughnessMap?"#define USE_ROUGHNESSMAP":"",t.metalnessMap?"#define USE_METALNESSMAP":"",t.alphaMap?"#define USE_ALPHAMAP":"",t.alphaHash?"#define USE_ALPHAHASH":"",t.transmission?"#define USE_TRANSMISSION":"",t.transmissionMap?"#define USE_TRANSMISSIONMAP":"",t.thicknessMap?"#define USE_THICKNESSMAP":"",t.sheenColorMap?"#define USE_SHEEN_COLORMAP":"",t.sheenRoughnessMap?"#define USE_SHEEN_ROUGHNESSMAP":"",t.mapUv?"#define MAP_UV "+t.mapUv:"",t.alphaMapUv?"#define ALPHAMAP_UV "+t.alphaMapUv:"",t.lightMapUv?"#define LIGHTMAP_UV "+t.lightMapUv:"",t.aoMapUv?"#define AOMAP_UV "+t.aoMapUv:"",t.emissiveMapUv?"#define EMISSIVEMAP_UV "+t.emissiveMapUv:"",t.bumpMapUv?"#define BUMPMAP_UV "+t.bumpMapUv:"",t.normalMapUv?"#define NORMALMAP_UV "+t.normalMapUv:"",t.displacementMapUv?"#define DISPLACEMENTMAP_UV "+t.displacementMapUv:"",t.metalnessMapUv?"#define METALNESSMAP_UV "+t.metalnessMapUv:"",t.roughnessMapUv?"#define ROUGHNESSMAP_UV "+t.roughnessMapUv:"",t.anisotropyMapUv?"#define ANISOTROPYMAP_UV "+t.anisotropyMapUv:"",t.clearcoatMapUv?"#define CLEARCOATMAP_UV "+t.clearcoatMapUv:"",t.clearcoatNormalMapUv?"#define CLEARCOAT_NORMALMAP_UV "+t.clearcoatNormalMapUv:"",t.clearcoatRoughnessMapUv?"#define CLEARCOAT_ROUGHNESSMAP_UV "+t.clearcoatRoughnessMapUv:"",t.iridescenceMapUv?"#define IRIDESCENCEMAP_UV "+t.iridescenceMapUv:"",t.iridescenceThicknessMapUv?"#define IRIDESCENCE_THICKNESSMAP_UV "+t.iridescenceThicknessMapUv:"",t.sheenColorMapUv?"#define SHEEN_COLORMAP_UV "+t.sheenColorMapUv:"",t.sheenRoughnessMapUv?"#define SHEEN_ROUGHNESSMAP_UV "+t.sheenRoughnessMapUv:"",t.specularMapUv?"#define SPECULARMAP_UV "+t.specularMapUv:"",t.specularColorMapUv?"#define SPECULAR_COLORMAP_UV "+t.specularColorMapUv:"",t.specularIntensityMapUv?"#define SPECULAR_INTENSITYMAP_UV "+t.specularIntensityMapUv:"",t.transmissionMapUv?"#define TRANSMISSIONMAP_UV "+t.transmissionMapUv:"",t.thicknessMapUv?"#define THICKNESSMAP_UV "+t.thicknessMapUv:"",t.vertexTangents&&t.flatShading===!1?"#define USE_TANGENT":"",t.vertexNormals?"#define HAS_NORMAL":"",t.vertexColors?"#define USE_COLOR":"",t.vertexAlphas?"#define USE_COLOR_ALPHA":"",t.vertexUv1s?"#define USE_UV1":"",t.vertexUv2s?"#define USE_UV2":"",t.vertexUv3s?"#define USE_UV3":"",t.pointsUvs?"#define USE_POINTS_UV":"",t.flatShading?"#define FLAT_SHADED":"",t.skinning?"#define USE_SKINNING":"",t.morphTargets?"#define USE_MORPHTARGETS":"",t.morphNormals&&t.flatShading===!1?"#define USE_MORPHNORMALS":"",t.morphColors?"#define USE_MORPHCOLORS":"",t.morphTargetsCount>0?"#define MORPHTARGETS_TEXTURE_STRIDE "+t.morphTextureStride:"",t.morphTargetsCount>0?"#define MORPHTARGETS_COUNT "+t.morphTargetsCount:"",t.doubleSided?"#define DOUBLE_SIDED":"",t.flipSided?"#define FLIP_SIDED":"",t.shadowMapEnabled?"#define USE_SHADOWMAP":"",t.shadowMapEnabled?"#define "+l:"",t.sizeAttenuation?"#define USE_SIZEATTENUATION":"",t.numLightProbes>0?"#define USE_LIGHT_PROBES":"",t.logarithmicDepthBuffer?"#define USE_LOGARITHMIC_DEPTH_BUFFER":"",t.reversedDepthBuffer?"#define USE_REVERSED_DEPTH_BUFFER":"","uniform mat4 modelMatrix;","uniform mat4 modelViewMatrix;","uniform mat4 projectionMatrix;","uniform mat4 viewMatrix;","uniform mat3 normalMatrix;","uniform vec3 cameraPosition;","uniform bool isOrthographic;","#ifdef USE_INSTANCING"," attribute mat4 instanceMatrix;","#endif","#ifdef USE_INSTANCING_COLOR"," attribute vec3 instanceColor;","#endif","#ifdef USE_INSTANCING_MORPH"," uniform sampler2D morphTexture;","#endif","attribute vec3 position;","attribute vec3 normal;","attribute vec2 uv;","#ifdef USE_UV1"," attribute vec2 uv1;","#endif","#ifdef USE_UV2"," attribute vec2 uv2;","#endif","#ifdef USE_UV3"," attribute vec2 uv3;","#endif","#ifdef USE_TANGENT"," attribute vec4 tangent;","#endif","#if defined( USE_COLOR_ALPHA )"," attribute vec4 color;","#elif defined( USE_COLOR )"," attribute vec3 color;","#endif","#ifdef USE_SKINNING"," attribute vec4 skinIndex;"," attribute vec4 skinWeight;","#endif",` +`].filter(kr).join(` +`),p=[Vd(t),"#define SHADER_TYPE "+t.shaderType,"#define SHADER_NAME "+t.shaderName,g,t.useFog&&t.fog?"#define USE_FOG":"",t.useFog&&t.fogExp2?"#define FOG_EXP2":"",t.alphaToCoverage?"#define ALPHA_TO_COVERAGE":"",t.map?"#define USE_MAP":"",t.matcap?"#define USE_MATCAP":"",t.envMap?"#define USE_ENVMAP":"",t.envMap?"#define "+c:"",t.envMap?"#define "+u:"",t.envMap?"#define "+d:"",h?"#define CUBEUV_TEXEL_WIDTH "+h.texelWidth:"",h?"#define CUBEUV_TEXEL_HEIGHT "+h.texelHeight:"",h?"#define CUBEUV_MAX_MIP "+h.maxMip+".0":"",t.lightMap?"#define USE_LIGHTMAP":"",t.aoMap?"#define USE_AOMAP":"",t.bumpMap?"#define USE_BUMPMAP":"",t.normalMap?"#define USE_NORMALMAP":"",t.normalMapObjectSpace?"#define USE_NORMALMAP_OBJECTSPACE":"",t.normalMapTangentSpace?"#define USE_NORMALMAP_TANGENTSPACE":"",t.packedNormalMap?"#define USE_PACKED_NORMALMAP":"",t.emissiveMap?"#define USE_EMISSIVEMAP":"",t.anisotropy?"#define USE_ANISOTROPY":"",t.anisotropyMap?"#define USE_ANISOTROPYMAP":"",t.clearcoat?"#define USE_CLEARCOAT":"",t.clearcoatMap?"#define USE_CLEARCOATMAP":"",t.clearcoatRoughnessMap?"#define USE_CLEARCOAT_ROUGHNESSMAP":"",t.clearcoatNormalMap?"#define USE_CLEARCOAT_NORMALMAP":"",t.dispersion?"#define USE_DISPERSION":"",t.iridescence?"#define USE_IRIDESCENCE":"",t.iridescenceMap?"#define USE_IRIDESCENCEMAP":"",t.iridescenceThicknessMap?"#define USE_IRIDESCENCE_THICKNESSMAP":"",t.specularMap?"#define USE_SPECULARMAP":"",t.specularColorMap?"#define USE_SPECULAR_COLORMAP":"",t.specularIntensityMap?"#define USE_SPECULAR_INTENSITYMAP":"",t.roughnessMap?"#define USE_ROUGHNESSMAP":"",t.metalnessMap?"#define USE_METALNESSMAP":"",t.alphaMap?"#define USE_ALPHAMAP":"",t.alphaTest?"#define USE_ALPHATEST":"",t.alphaHash?"#define USE_ALPHAHASH":"",t.sheen?"#define USE_SHEEN":"",t.sheenColorMap?"#define USE_SHEEN_COLORMAP":"",t.sheenRoughnessMap?"#define USE_SHEEN_ROUGHNESSMAP":"",t.transmission?"#define USE_TRANSMISSION":"",t.transmissionMap?"#define USE_TRANSMISSIONMAP":"",t.thicknessMap?"#define USE_THICKNESSMAP":"",t.vertexTangents&&t.flatShading===!1?"#define USE_TANGENT":"",t.vertexColors||t.instancingColor?"#define USE_COLOR":"",t.vertexAlphas||t.batchingColor?"#define USE_COLOR_ALPHA":"",t.vertexUv1s?"#define USE_UV1":"",t.vertexUv2s?"#define USE_UV2":"",t.vertexUv3s?"#define USE_UV3":"",t.pointsUvs?"#define USE_POINTS_UV":"",t.gradientMap?"#define USE_GRADIENTMAP":"",t.flatShading?"#define FLAT_SHADED":"",t.doubleSided?"#define DOUBLE_SIDED":"",t.flipSided?"#define FLIP_SIDED":"",t.shadowMapEnabled?"#define USE_SHADOWMAP":"",t.shadowMapEnabled?"#define "+l:"",t.premultipliedAlpha?"#define PREMULTIPLIED_ALPHA":"",t.numLightProbes>0?"#define USE_LIGHT_PROBES":"",t.numLightProbeGrids>0?"#define USE_LIGHT_PROBES_GRID":"",t.decodeVideoTexture?"#define DECODE_VIDEO_TEXTURE":"",t.decodeVideoTextureEmissive?"#define DECODE_VIDEO_TEXTURE_EMISSIVE":"",t.logarithmicDepthBuffer?"#define USE_LOGARITHMIC_DEPTH_BUFFER":"",t.reversedDepthBuffer?"#define USE_REVERSED_DEPTH_BUFFER":"","uniform mat4 viewMatrix;","uniform vec3 cameraPosition;","uniform bool isOrthographic;",t.toneMapping!==fn?"#define TONE_MAPPING":"",t.toneMapping!==fn?Ue.tonemapping_pars_fragment:"",t.toneMapping!==fn?d_("toneMapping",t.toneMapping):"",t.dithering?"#define DITHERING":"",t.opaque?"#define OPAQUE":"",Ue.colorspace_pars_fragment,h_("linearToOutputTexel",t.outputColorSpace),f_(),t.useDepthPacking?"#define DEPTH_PACKING "+t.depthPacking:"",` +`].filter(kr).join(` +`)),o=_h(o),o=Ud(o,t),o=Bd(o,t),a=_h(a),a=Ud(a,t),a=Bd(a,t),o=zd(o),a=zd(a),t.isRawShaderMaterial!==!0&&(v=`#version 300 es +`,m=[f,"#define attribute in","#define varying out","#define texture2D texture"].join(` +`)+` +`+m,p=["#define varying in",t.glslVersion===Kc?"":"layout(location = 0) out highp vec4 pc_fragColor;",t.glslVersion===Kc?"":"#define gl_FragColor pc_fragColor","#define gl_FragDepthEXT gl_FragDepth","#define texture2D texture","#define textureCube texture","#define texture2DProj textureProj","#define texture2DLodEXT textureLod","#define texture2DProjLodEXT textureProjLod","#define textureCubeLodEXT textureLod","#define texture2DGradEXT textureGrad","#define texture2DProjGradEXT textureProjGrad","#define textureCubeGradEXT textureGrad"].join(` +`)+` +`+p);let b=v+m+o,M=v+p+a,E=Fd(s,s.VERTEX_SHADER,b),w=Fd(s,s.FRAGMENT_SHADER,M);s.attachShader(x,E),s.attachShader(x,w),t.index0AttributeName!==void 0?s.bindAttribLocation(x,0,t.index0AttributeName):t.morphTargets===!0&&s.bindAttribLocation(x,0,"position"),s.linkProgram(x);function C(R){if(i.debug.checkShaderErrors){let I=s.getProgramInfoLog(x)||"",H=s.getShaderInfoLog(E)||"",W=s.getShaderInfoLog(w)||"",k=I.trim(),z=H.trim(),V=W.trim(),K=!0,Q=!0;if(s.getProgramParameter(x,s.LINK_STATUS)===!1)if(K=!1,typeof i.debug.onShaderError=="function")i.debug.onShaderError(s,x,E,w);else{let ae=Od(s,E,"vertex"),_e=Od(s,w,"fragment");Re("THREE.WebGLProgram: Shader Error "+s.getError()+" - VALIDATE_STATUS "+s.getProgramParameter(x,s.VALIDATE_STATUS)+` + +Material Name: `+R.name+` +Material Type: `+R.type+` + +Program Info Log: `+k+` +`+ae+` +`+_e)}else k!==""?Te("WebGLProgram: Program Info Log:",k):(z===""||V==="")&&(Q=!1);Q&&(R.diagnostics={runnable:K,programLog:k,vertexShader:{log:z,prefix:m},fragmentShader:{log:V,prefix:p}})}s.deleteShader(E),s.deleteShader(w),_=new Ns(s,x),T=g_(s,x)}let _;this.getUniforms=function(){return _===void 0&&C(this),_};let T;this.getAttributes=function(){return T===void 0&&C(this),T};let P=t.rendererExtensionParallelShaderCompile===!1;return this.isReady=function(){return P===!1&&(P=s.getProgramParameter(x,o_)),P},this.destroy=function(){n.releaseStatesOfProgram(this),s.deleteProgram(x),this.program=void 0},this.type=t.shaderType,this.name=t.shaderName,this.id=a_++,this.cacheKey=e,this.usedTimes=1,this.program=x,this.vertexShader=E,this.fragmentShader=w,this}var L_=0,yh=class{constructor(){this.shaderCache=new Map,this.materialCache=new Map}update(e){let t=e.vertexShader,n=e.fragmentShader,s=this._getShaderStage(t),r=this._getShaderStage(n),o=this._getShaderCacheForMaterial(e);return o.has(s)===!1&&(o.add(s),s.usedTimes++),o.has(r)===!1&&(o.add(r),r.usedTimes++),this}remove(e){let t=this.materialCache.get(e);for(let n of t)n.usedTimes--,n.usedTimes===0&&this.shaderCache.delete(n.code);return this.materialCache.delete(e),this}getVertexShaderID(e){return this._getShaderStage(e.vertexShader).id}getFragmentShaderID(e){return this._getShaderStage(e.fragmentShader).id}dispose(){this.shaderCache.clear(),this.materialCache.clear()}_getShaderCacheForMaterial(e){let t=this.materialCache,n=t.get(e);return n===void 0&&(n=new Set,t.set(e,n)),n}_getShaderStage(e){let t=this.shaderCache,n=t.get(e);return n===void 0&&(n=new bh(e),t.set(e,n)),n}},bh=class{constructor(e){this.id=L_++,this.code=e,this.usedTimes=0}};function N_(i){return i===Di||i===Lr||i===Nr}function D_(i,e,t,n,s,r){let o=new ar,a=new yh,l=new Set,c=[],u=new Map,d=n.logarithmicDepthBuffer,h=n.precision,f={MeshDepthMaterial:"depth",MeshDistanceMaterial:"distance",MeshNormalMaterial:"normal",MeshBasicMaterial:"basic",MeshLambertMaterial:"lambert",MeshPhongMaterial:"phong",MeshToonMaterial:"toon",MeshStandardMaterial:"physical",MeshPhysicalMaterial:"physical",MeshMatcapMaterial:"matcap",LineBasicMaterial:"basic",LineDashedMaterial:"dashed",PointsMaterial:"points",ShadowMaterial:"shadow",SpriteMaterial:"sprite"};function g(_){return l.add(_),_===0?"uv":`uv${_}`}function x(_,T,P,R,I,H){let W=R.fog,k=I.geometry,z=_.isMeshStandardMaterial||_.isMeshLambertMaterial||_.isMeshPhongMaterial?R.environment:null,V=_.isMeshStandardMaterial||_.isMeshLambertMaterial&&!_.envMap||_.isMeshPhongMaterial&&!_.envMap,K=e.get(_.envMap||z,V),Q=K&&K.mapping===Tr?K.image.height:null,ae=f[_.type];_.precision!==null&&(h=n.getMaxPrecision(_.precision),h!==_.precision&&Te("WebGLProgram.getParameters:",_.precision,"not supported, using",h,"instead."));let _e=k.morphAttributes.position||k.morphAttributes.normal||k.morphAttributes.color,be=_e!==void 0?_e.length:0,We=0;k.morphAttributes.position!==void 0&&(We=1),k.morphAttributes.normal!==void 0&&(We=2),k.morphAttributes.color!==void 0&&(We=3);let Qe,ke,$,de;if(ae){let De=Gn[ae];Qe=De.vertexShader,ke=De.fragmentShader}else Qe=_.vertexShader,ke=_.fragmentShader,a.update(_),$=a.getVertexShaderID(_),de=a.getFragmentShaderID(_);let ie=i.getRenderTarget(),Ae=i.state.buffers.depth.getReversed(),Ne=I.isInstancedMesh===!0,Pe=I.isBatchedMesh===!0,dt=!!_.map,Xe=!!_.matcap,et=!!K,lt=!!_.aoMap,He=!!_.lightMap,Tt=!!_.bumpMap,ft=!!_.normalMap,sn=!!_.displacementMap,D=!!_.emissiveMap,At=!!_.metalnessMap,qe=!!_.roughnessMap,ot=_.anisotropy>0,le=_.clearcoat>0,pt=_.dispersion>0,A=_.iridescence>0,y=_.sheen>0,O=_.transmission>0,Y=ot&&!!_.anisotropyMap,j=le&&!!_.clearcoatMap,ee=le&&!!_.clearcoatNormalMap,oe=le&&!!_.clearcoatRoughnessMap,X=A&&!!_.iridescenceMap,Z=A&&!!_.iridescenceThicknessMap,fe=y&&!!_.sheenColorMap,xe=y&&!!_.sheenRoughnessMap,se=!!_.specularMap,te=!!_.specularColorMap,Ie=!!_.specularIntensityMap,Oe=O&&!!_.transmissionMap,Ke=O&&!!_.thicknessMap,L=!!_.gradientMap,ne=!!_.alphaMap,q=_.alphaTest>0,pe=!!_.alphaHash,re=!!_.extensions,J=fn;_.toneMapped&&(ie===null||ie.isXRRenderTarget===!0)&&(J=i.toneMapping);let Me={shaderID:ae,shaderType:_.type,shaderName:_.name,vertexShader:Qe,fragmentShader:ke,defines:_.defines,customVertexShaderID:$,customFragmentShaderID:de,isRawShaderMaterial:_.isRawShaderMaterial===!0,glslVersion:_.glslVersion,precision:h,batching:Pe,batchingColor:Pe&&I._colorsTexture!==null,instancing:Ne,instancingColor:Ne&&I.instanceColor!==null,instancingMorph:Ne&&I.morphTexture!==null,outputColorSpace:ie===null?i.outputColorSpace:ie.isXRRenderTarget===!0?ie.texture.colorSpace:ze.workingColorSpace,alphaToCoverage:!!_.alphaToCoverage,map:dt,matcap:Xe,envMap:et,envMapMode:et&&K.mapping,envMapCubeUVHeight:Q,aoMap:lt,lightMap:He,bumpMap:Tt,normalMap:ft,displacementMap:sn,emissiveMap:D,normalMapObjectSpace:ft&&_.normalMapType===ud,normalMapTangentSpace:ft&&_.normalMapType===$c,packedNormalMap:ft&&_.normalMapType===$c&&N_(_.normalMap.format),metalnessMap:At,roughnessMap:qe,anisotropy:ot,anisotropyMap:Y,clearcoat:le,clearcoatMap:j,clearcoatNormalMap:ee,clearcoatRoughnessMap:oe,dispersion:pt,iridescence:A,iridescenceMap:X,iridescenceThicknessMap:Z,sheen:y,sheenColorMap:fe,sheenRoughnessMap:xe,specularMap:se,specularColorMap:te,specularIntensityMap:Ie,transmission:O,transmissionMap:Oe,thicknessMap:Ke,gradientMap:L,opaque:_.transparent===!1&&_.blending===qi&&_.alphaToCoverage===!1,alphaMap:ne,alphaTest:q,alphaHash:pe,combine:_.combine,mapUv:dt&&g(_.map.channel),aoMapUv:lt&&g(_.aoMap.channel),lightMapUv:He&&g(_.lightMap.channel),bumpMapUv:Tt&&g(_.bumpMap.channel),normalMapUv:ft&&g(_.normalMap.channel),displacementMapUv:sn&&g(_.displacementMap.channel),emissiveMapUv:D&&g(_.emissiveMap.channel),metalnessMapUv:At&&g(_.metalnessMap.channel),roughnessMapUv:qe&&g(_.roughnessMap.channel),anisotropyMapUv:Y&&g(_.anisotropyMap.channel),clearcoatMapUv:j&&g(_.clearcoatMap.channel),clearcoatNormalMapUv:ee&&g(_.clearcoatNormalMap.channel),clearcoatRoughnessMapUv:oe&&g(_.clearcoatRoughnessMap.channel),iridescenceMapUv:X&&g(_.iridescenceMap.channel),iridescenceThicknessMapUv:Z&&g(_.iridescenceThicknessMap.channel),sheenColorMapUv:fe&&g(_.sheenColorMap.channel),sheenRoughnessMapUv:xe&&g(_.sheenRoughnessMap.channel),specularMapUv:se&&g(_.specularMap.channel),specularColorMapUv:te&&g(_.specularColorMap.channel),specularIntensityMapUv:Ie&&g(_.specularIntensityMap.channel),transmissionMapUv:Oe&&g(_.transmissionMap.channel),thicknessMapUv:Ke&&g(_.thicknessMap.channel),alphaMapUv:ne&&g(_.alphaMap.channel),vertexTangents:!!k.attributes.tangent&&(ft||ot),vertexNormals:!!k.attributes.normal,vertexColors:_.vertexColors,vertexAlphas:_.vertexColors===!0&&!!k.attributes.color&&k.attributes.color.itemSize===4,pointsUvs:I.isPoints===!0&&!!k.attributes.uv&&(dt||ne),fog:!!W,useFog:_.fog===!0,fogExp2:!!W&&W.isFogExp2,flatShading:_.wireframe===!1&&(_.flatShading===!0||k.attributes.normal===void 0&&ft===!1&&(_.isMeshLambertMaterial||_.isMeshPhongMaterial||_.isMeshStandardMaterial||_.isMeshPhysicalMaterial)),sizeAttenuation:_.sizeAttenuation===!0,logarithmicDepthBuffer:d,reversedDepthBuffer:Ae,skinning:I.isSkinnedMesh===!0,morphTargets:k.morphAttributes.position!==void 0,morphNormals:k.morphAttributes.normal!==void 0,morphColors:k.morphAttributes.color!==void 0,morphTargetsCount:be,morphTextureStride:We,numDirLights:T.directional.length,numPointLights:T.point.length,numSpotLights:T.spot.length,numSpotLightMaps:T.spotLightMap.length,numRectAreaLights:T.rectArea.length,numHemiLights:T.hemi.length,numDirLightShadows:T.directionalShadowMap.length,numPointLightShadows:T.pointShadowMap.length,numSpotLightShadows:T.spotShadowMap.length,numSpotLightShadowsWithMaps:T.numSpotLightShadowsWithMaps,numLightProbes:T.numLightProbes,numLightProbeGrids:H.length,numClippingPlanes:r.numPlanes,numClipIntersection:r.numIntersection,dithering:_.dithering,shadowMapEnabled:i.shadowMap.enabled&&P.length>0,shadowMapType:i.shadowMap.type,toneMapping:J,decodeVideoTexture:dt&&_.map.isVideoTexture===!0&&ze.getTransfer(_.map.colorSpace)===Je,decodeVideoTextureEmissive:D&&_.emissiveMap.isVideoTexture===!0&&ze.getTransfer(_.emissiveMap.colorSpace)===Je,premultipliedAlpha:_.premultipliedAlpha,doubleSided:_.side===zn,flipSided:_.side===$t,useDepthPacking:_.depthPacking>=0,depthPacking:_.depthPacking||0,index0AttributeName:_.index0AttributeName,extensionClipCullDistance:re&&_.extensions.clipCullDistance===!0&&t.has("WEBGL_clip_cull_distance"),extensionMultiDraw:(re&&_.extensions.multiDraw===!0||Pe)&&t.has("WEBGL_multi_draw"),rendererExtensionParallelShaderCompile:t.has("KHR_parallel_shader_compile"),customProgramCacheKey:_.customProgramCacheKey()};return Me.vertexUv1s=l.has(1),Me.vertexUv2s=l.has(2),Me.vertexUv3s=l.has(3),l.clear(),Me}function m(_){let T=[];if(_.shaderID?T.push(_.shaderID):(T.push(_.customVertexShaderID),T.push(_.customFragmentShaderID)),_.defines!==void 0)for(let P in _.defines)T.push(P),T.push(_.defines[P]);return _.isRawShaderMaterial===!1&&(p(T,_),v(T,_),T.push(i.outputColorSpace)),T.push(_.customProgramCacheKey),T.join()}function p(_,T){_.push(T.precision),_.push(T.outputColorSpace),_.push(T.envMapMode),_.push(T.envMapCubeUVHeight),_.push(T.mapUv),_.push(T.alphaMapUv),_.push(T.lightMapUv),_.push(T.aoMapUv),_.push(T.bumpMapUv),_.push(T.normalMapUv),_.push(T.displacementMapUv),_.push(T.emissiveMapUv),_.push(T.metalnessMapUv),_.push(T.roughnessMapUv),_.push(T.anisotropyMapUv),_.push(T.clearcoatMapUv),_.push(T.clearcoatNormalMapUv),_.push(T.clearcoatRoughnessMapUv),_.push(T.iridescenceMapUv),_.push(T.iridescenceThicknessMapUv),_.push(T.sheenColorMapUv),_.push(T.sheenRoughnessMapUv),_.push(T.specularMapUv),_.push(T.specularColorMapUv),_.push(T.specularIntensityMapUv),_.push(T.transmissionMapUv),_.push(T.thicknessMapUv),_.push(T.combine),_.push(T.fogExp2),_.push(T.sizeAttenuation),_.push(T.morphTargetsCount),_.push(T.morphAttributeCount),_.push(T.numDirLights),_.push(T.numPointLights),_.push(T.numSpotLights),_.push(T.numSpotLightMaps),_.push(T.numHemiLights),_.push(T.numRectAreaLights),_.push(T.numDirLightShadows),_.push(T.numPointLightShadows),_.push(T.numSpotLightShadows),_.push(T.numSpotLightShadowsWithMaps),_.push(T.numLightProbes),_.push(T.shadowMapType),_.push(T.toneMapping),_.push(T.numClippingPlanes),_.push(T.numClipIntersection),_.push(T.depthPacking)}function v(_,T){o.disableAll(),T.instancing&&o.enable(0),T.instancingColor&&o.enable(1),T.instancingMorph&&o.enable(2),T.matcap&&o.enable(3),T.envMap&&o.enable(4),T.normalMapObjectSpace&&o.enable(5),T.normalMapTangentSpace&&o.enable(6),T.clearcoat&&o.enable(7),T.iridescence&&o.enable(8),T.alphaTest&&o.enable(9),T.vertexColors&&o.enable(10),T.vertexAlphas&&o.enable(11),T.vertexUv1s&&o.enable(12),T.vertexUv2s&&o.enable(13),T.vertexUv3s&&o.enable(14),T.vertexTangents&&o.enable(15),T.anisotropy&&o.enable(16),T.alphaHash&&o.enable(17),T.batching&&o.enable(18),T.dispersion&&o.enable(19),T.batchingColor&&o.enable(20),T.gradientMap&&o.enable(21),T.packedNormalMap&&o.enable(22),T.vertexNormals&&o.enable(23),_.push(o.mask),o.disableAll(),T.fog&&o.enable(0),T.useFog&&o.enable(1),T.flatShading&&o.enable(2),T.logarithmicDepthBuffer&&o.enable(3),T.reversedDepthBuffer&&o.enable(4),T.skinning&&o.enable(5),T.morphTargets&&o.enable(6),T.morphNormals&&o.enable(7),T.morphColors&&o.enable(8),T.premultipliedAlpha&&o.enable(9),T.shadowMapEnabled&&o.enable(10),T.doubleSided&&o.enable(11),T.flipSided&&o.enable(12),T.useDepthPacking&&o.enable(13),T.dithering&&o.enable(14),T.transmission&&o.enable(15),T.sheen&&o.enable(16),T.opaque&&o.enable(17),T.pointsUvs&&o.enable(18),T.decodeVideoTexture&&o.enable(19),T.decodeVideoTextureEmissive&&o.enable(20),T.alphaToCoverage&&o.enable(21),T.numLightProbeGrids>0&&o.enable(22),_.push(o.mask)}function b(_){let T=f[_.type],P;if(T){let R=Gn[T];P=oi.clone(R.uniforms)}else P=_.uniforms;return P}function M(_,T){let P=u.get(T);return P!==void 0?++P.usedTimes:(P=new I_(i,T,_,s),c.push(P),u.set(T,P)),P}function E(_){if(--_.usedTimes===0){let T=c.indexOf(_);c[T]=c[c.length-1],c.pop(),u.delete(_.cacheKey),_.destroy()}}function w(_){a.remove(_)}function C(){a.dispose()}return{getParameters:x,getProgramCacheKey:m,getUniforms:b,acquireProgram:M,releaseProgram:E,releaseShaderCache:w,programs:c,dispose:C}}function F_(){let i=new WeakMap;function e(o){return i.has(o)}function t(o){let a=i.get(o);return a===void 0&&(a={},i.set(o,a)),a}function n(o){i.delete(o)}function s(o,a,l){i.get(o)[a]=l}function r(){i=new WeakMap}return{has:e,get:t,remove:n,update:s,dispose:r}}function k_(i,e){return i.groupOrder!==e.groupOrder?i.groupOrder-e.groupOrder:i.renderOrder!==e.renderOrder?i.renderOrder-e.renderOrder:i.material.id!==e.material.id?i.material.id-e.material.id:i.materialVariant!==e.materialVariant?i.materialVariant-e.materialVariant:i.z!==e.z?i.z-e.z:i.id-e.id}function Gd(i,e){return i.groupOrder!==e.groupOrder?i.groupOrder-e.groupOrder:i.renderOrder!==e.renderOrder?i.renderOrder-e.renderOrder:i.z!==e.z?e.z-i.z:i.id-e.id}function Hd(){let i=[],e=0,t=[],n=[],s=[];function r(){e=0,t.length=0,n.length=0,s.length=0}function o(h){let f=0;return h.isInstancedMesh&&(f+=2),h.isSkinnedMesh&&(f+=1),f}function a(h,f,g,x,m,p){let v=i[e];return v===void 0?(v={id:h.id,object:h,geometry:f,material:g,materialVariant:o(h),groupOrder:x,renderOrder:h.renderOrder,z:m,group:p},i[e]=v):(v.id=h.id,v.object=h,v.geometry=f,v.material=g,v.materialVariant=o(h),v.groupOrder=x,v.renderOrder=h.renderOrder,v.z=m,v.group=p),e++,v}function l(h,f,g,x,m,p){let v=a(h,f,g,x,m,p);g.transmission>0?n.push(v):g.transparent===!0?s.push(v):t.push(v)}function c(h,f,g,x,m,p){let v=a(h,f,g,x,m,p);g.transmission>0?n.unshift(v):g.transparent===!0?s.unshift(v):t.unshift(v)}function u(h,f){t.length>1&&t.sort(h||k_),n.length>1&&n.sort(f||Gd),s.length>1&&s.sort(f||Gd)}function d(){for(let h=e,f=i.length;h=r.length?(o=new Hd,r.push(o)):o=r[s],o}function t(){i=new WeakMap}return{get:e,dispose:t}}function U_(){let i={};return{get:function(e){if(i[e.id]!==void 0)return i[e.id];let t;switch(e.type){case"DirectionalLight":t={direction:new N,color:new me};break;case"SpotLight":t={position:new N,direction:new N,color:new me,distance:0,coneCos:0,penumbraCos:0,decay:0};break;case"PointLight":t={position:new N,color:new me,distance:0,decay:0};break;case"HemisphereLight":t={direction:new N,skyColor:new me,groundColor:new me};break;case"RectAreaLight":t={color:new me,position:new N,halfWidth:new N,halfHeight:new N};break}return i[e.id]=t,t}}}function B_(){let i={};return{get:function(e){if(i[e.id]!==void 0)return i[e.id];let t;switch(e.type){case"DirectionalLight":t={shadowIntensity:1,shadowBias:0,shadowNormalBias:0,shadowRadius:1,shadowMapSize:new Ee};break;case"SpotLight":t={shadowIntensity:1,shadowBias:0,shadowNormalBias:0,shadowRadius:1,shadowMapSize:new Ee};break;case"PointLight":t={shadowIntensity:1,shadowBias:0,shadowNormalBias:0,shadowRadius:1,shadowMapSize:new Ee,shadowCameraNear:1,shadowCameraFar:1e3};break}return i[e.id]=t,t}}}var z_=0;function V_(i,e){return(e.castShadow?2:0)-(i.castShadow?2:0)+(e.map?1:0)-(i.map?1:0)}function G_(i){let e=new U_,t=B_(),n={version:0,hash:{directionalLength:-1,pointLength:-1,spotLength:-1,rectAreaLength:-1,hemiLength:-1,numDirectionalShadows:-1,numPointShadows:-1,numSpotShadows:-1,numSpotMaps:-1,numLightProbes:-1},ambient:[0,0,0],probe:[],directional:[],directionalShadow:[],directionalShadowMap:[],directionalShadowMatrix:[],spot:[],spotLightMap:[],spotShadow:[],spotShadowMap:[],spotLightMatrix:[],rectArea:[],rectAreaLTC1:null,rectAreaLTC2:null,point:[],pointShadow:[],pointShadowMap:[],pointShadowMatrix:[],hemi:[],numSpotLightShadowsWithMaps:0,numLightProbes:0};for(let c=0;c<9;c++)n.probe.push(new N);let s=new N,r=new gt,o=new gt;function a(c){let u=0,d=0,h=0;for(let T=0;T<9;T++)n.probe[T].set(0,0,0);let f=0,g=0,x=0,m=0,p=0,v=0,b=0,M=0,E=0,w=0,C=0;c.sort(V_);for(let T=0,P=c.length;T0&&(i.has("OES_texture_float_linear")===!0?(n.rectAreaLTC1=ce.LTC_FLOAT_1,n.rectAreaLTC2=ce.LTC_FLOAT_2):(n.rectAreaLTC1=ce.LTC_HALF_1,n.rectAreaLTC2=ce.LTC_HALF_2)),n.ambient[0]=u,n.ambient[1]=d,n.ambient[2]=h;let _=n.hash;(_.directionalLength!==f||_.pointLength!==g||_.spotLength!==x||_.rectAreaLength!==m||_.hemiLength!==p||_.numDirectionalShadows!==v||_.numPointShadows!==b||_.numSpotShadows!==M||_.numSpotMaps!==E||_.numLightProbes!==C)&&(n.directional.length=f,n.spot.length=x,n.rectArea.length=m,n.point.length=g,n.hemi.length=p,n.directionalShadow.length=v,n.directionalShadowMap.length=v,n.pointShadow.length=b,n.pointShadowMap.length=b,n.spotShadow.length=M,n.spotShadowMap.length=M,n.directionalShadowMatrix.length=v,n.pointShadowMatrix.length=b,n.spotLightMatrix.length=M+E-w,n.spotLightMap.length=E,n.numSpotLightShadowsWithMaps=w,n.numLightProbes=C,_.directionalLength=f,_.pointLength=g,_.spotLength=x,_.rectAreaLength=m,_.hemiLength=p,_.numDirectionalShadows=v,_.numPointShadows=b,_.numSpotShadows=M,_.numSpotMaps=E,_.numLightProbes=C,n.version=z_++)}function l(c,u){let d=0,h=0,f=0,g=0,x=0,m=u.matrixWorldInverse;for(let p=0,v=c.length;p=o.length?(a=new Wd(i),o.push(a)):a=o[r],a}function n(){e=new WeakMap}return{get:t,dispose:n}}var W_=`void main() { + gl_Position = vec4( position, 1.0 ); +}`,X_=`uniform sampler2D shadow_pass; +uniform vec2 resolution; +uniform float radius; +void main() { + const float samples = float( VSM_SAMPLES ); + float mean = 0.0; + float squared_mean = 0.0; + float uvStride = samples <= 1.0 ? 0.0 : 2.0 / ( samples - 1.0 ); + float uvStart = samples <= 1.0 ? 0.0 : - 1.0; + for ( float i = 0.0; i < samples; i ++ ) { + float uvOffset = uvStart + i * uvStride; + #ifdef HORIZONTAL_PASS + vec2 distribution = texture2D( shadow_pass, ( gl_FragCoord.xy + vec2( uvOffset, 0.0 ) * radius ) / resolution ).rg; + mean += distribution.x; + squared_mean += distribution.y * distribution.y + distribution.x * distribution.x; + #else + float depth = texture2D( shadow_pass, ( gl_FragCoord.xy + vec2( 0.0, uvOffset ) * radius ) / resolution ).r; + mean += depth; + squared_mean += depth * depth; + #endif + } + mean = mean / samples; + squared_mean = squared_mean / samples; + float std_dev = sqrt( max( 0.0, squared_mean - mean * mean ) ); + gl_FragColor = vec4( mean, std_dev, 0.0, 1.0 ); +}`,q_=[new N(1,0,0),new N(-1,0,0),new N(0,1,0),new N(0,-1,0),new N(0,0,1),new N(0,0,-1)],Y_=[new N(0,-1,0),new N(0,-1,0),new N(0,0,1),new N(0,0,-1),new N(0,-1,0),new N(0,-1,0)],Xd=new gt,Fr=new N,ph=new N;function Z_(i,e,t){let n=new hr,s=new Ee,r=new Ee,o=new xt,a=new Jo,l=new jo,c={},u=t.maxTextureSize,d={[Qn]:$t,[$t]:Qn,[zn]:zn},h=new ut({defines:{VSM_SAMPLES:8},uniforms:{shadow_pass:{value:null},resolution:{value:new Ee},radius:{value:4}},vertexShader:W_,fragmentShader:X_}),f=h.clone();f.defines.HORIZONTAL_PASS=1;let g=new ht;g.setAttribute("position",new Ye(new Float32Array([-1,-1,.5,3,-1,.5,-1,3,.5]),3));let x=new Xt(g,h),m=this;this.enabled=!1,this.autoUpdate=!0,this.needsUpdate=!1,this.type=vr;let p=this.type;this.render=function(w,C,_){if(m.enabled===!1||m.autoUpdate===!1&&m.needsUpdate===!1||w.length===0)return;this.type===Gu&&(Te("WebGLShadowMap: PCFSoftShadowMap has been deprecated. Using PCFShadowMap instead."),this.type=vr);let T=i.getRenderTarget(),P=i.getActiveCubeFace(),R=i.getActiveMipmapLevel(),I=i.state;I.setBlending(yn),I.buffers.depth.getReversed()===!0?I.buffers.color.setClear(0,0,0,0):I.buffers.color.setClear(1,1,1,1),I.buffers.depth.setTest(!0),I.setScissorTest(!1);let H=p!==this.type;H&&C.traverse(function(W){W.material&&(Array.isArray(W.material)?W.material.forEach(k=>k.needsUpdate=!0):W.material.needsUpdate=!0)});for(let W=0,k=w.length;Wu||s.y>u)&&(s.x>u&&(r.x=Math.floor(u/K.x),s.x=r.x*K.x,V.mapSize.x=r.x),s.y>u&&(r.y=Math.floor(u/K.y),s.y=r.y*K.y,V.mapSize.y=r.y));let Q=i.state.buffers.depth.getReversed();if(V.camera._reversedDepth=Q,V.map===null||H===!0){if(V.map!==null&&(V.map.depthTexture!==null&&(V.map.depthTexture.dispose(),V.map.depthTexture=null),V.map.dispose()),this.type===Cs){if(z.isPointLight){Te("WebGLShadowMap: VSM shadow maps are not supported for PointLights. Use PCF or BasicShadowMap instead.");continue}V.map=new Rt(s.x,s.y,{format:Di,type:qt,minFilter:Bt,magFilter:Bt,generateMipmaps:!1}),V.map.texture.name=z.name+".shadowMap",V.map.depthTexture=new ii(s.x,s.y,Pn),V.map.depthTexture.name=z.name+".shadowMapDepth",V.map.depthTexture.format=Un,V.map.depthTexture.compareFunction=null,V.map.depthTexture.minFilter=Dt,V.map.depthTexture.magFilter=Dt}else z.isPointLight?(V.map=new il(s.x),V.map.depthTexture=new Ko(s.x,Rn)):(V.map=new Rt(s.x,s.y),V.map.depthTexture=new ii(s.x,s.y,Rn)),V.map.depthTexture.name=z.name+".shadowMap",V.map.depthTexture.format=Un,this.type===vr?(V.map.depthTexture.compareFunction=Q?Qa:ja,V.map.depthTexture.minFilter=Bt,V.map.depthTexture.magFilter=Bt):(V.map.depthTexture.compareFunction=null,V.map.depthTexture.minFilter=Dt,V.map.depthTexture.magFilter=Dt);V.camera.updateProjectionMatrix()}let ae=V.map.isWebGLCubeRenderTarget?6:1;for(let _e=0;_e0||C.map&&C.alphaTest>0||C.alphaToCoverage===!0){let I=P.uuid,H=C.uuid,W=c[I];W===void 0&&(W={},c[I]=W);let k=W[H];k===void 0&&(k=P.clone(),W[H]=k,C.addEventListener("dispose",E)),P=k}if(P.visible=C.visible,P.wireframe=C.wireframe,T===Cs?P.side=C.shadowSide!==null?C.shadowSide:C.side:P.side=C.shadowSide!==null?C.shadowSide:d[C.side],P.alphaMap=C.alphaMap,P.alphaTest=C.alphaToCoverage===!0?.5:C.alphaTest,P.map=C.map,P.clipShadows=C.clipShadows,P.clippingPlanes=C.clippingPlanes,P.clipIntersection=C.clipIntersection,P.displacementMap=C.displacementMap,P.displacementScale=C.displacementScale,P.displacementBias=C.displacementBias,P.wireframeLinewidth=C.wireframeLinewidth,P.linewidth=C.linewidth,_.isPointLight===!0&&P.isMeshDistanceMaterial===!0){let I=i.properties.get(P);I.light=_}return P}function M(w,C,_,T,P){if(w.visible===!1)return;if(w.layers.test(C.layers)&&(w.isMesh||w.isLine||w.isPoints)&&(w.castShadow||w.receiveShadow&&P===Cs)&&(!w.frustumCulled||n.intersectsObject(w))){w.modelViewMatrix.multiplyMatrices(_.matrixWorldInverse,w.matrixWorld);let H=e.update(w),W=w.material;if(Array.isArray(W)){let k=H.groups;for(let z=0,V=k.length;z=1):Q.indexOf("OpenGL ES")!==-1&&(K=parseFloat(/^OpenGL ES (\d)/.exec(Q)[1]),V=K>=2);let ae=null,_e={},be=i.getParameter(i.SCISSOR_BOX),We=i.getParameter(i.VIEWPORT),Qe=new xt().fromArray(be),ke=new xt().fromArray(We);function $(L,ne,q,pe){let re=new Uint8Array(4),J=i.createTexture();i.bindTexture(L,J),i.texParameteri(L,i.TEXTURE_MIN_FILTER,i.NEAREST),i.texParameteri(L,i.TEXTURE_MAG_FILTER,i.NEAREST);for(let Me=0;Me"u"?!1:/OculusBrowser/g.test(navigator.userAgent),c=new Ee,u=new WeakMap,d=new Set,h,f=new WeakMap,g=!1;try{g=typeof OffscreenCanvas<"u"&&new OffscreenCanvas(1,1).getContext("2d")!==null}catch{}function x(A,y){return g?new OffscreenCanvas(A,y):rr("canvas")}function m(A,y,O){let Y=1,j=pt(A);if((j.width>O||j.height>O)&&(Y=O/Math.max(j.width,j.height)),Y<1)if(typeof HTMLImageElement<"u"&&A instanceof HTMLImageElement||typeof HTMLCanvasElement<"u"&&A instanceof HTMLCanvasElement||typeof ImageBitmap<"u"&&A instanceof ImageBitmap||typeof VideoFrame<"u"&&A instanceof VideoFrame){let ee=Math.floor(Y*j.width),oe=Math.floor(Y*j.height);h===void 0&&(h=x(ee,oe));let X=y?x(ee,oe):h;return X.width=ee,X.height=oe,X.getContext("2d").drawImage(A,0,0,ee,oe),Te("WebGLRenderer: Texture has been resized from ("+j.width+"x"+j.height+") to ("+ee+"x"+oe+")."),X}else return"data"in A&&Te("WebGLRenderer: Image in DataTexture is too big ("+j.width+"x"+j.height+")."),A;return A}function p(A){return A.generateMipmaps}function v(A){i.generateMipmap(A)}function b(A){return A.isWebGLCubeRenderTarget?i.TEXTURE_CUBE_MAP:A.isWebGL3DRenderTarget?i.TEXTURE_3D:A.isWebGLArrayRenderTarget||A.isCompressedArrayTexture?i.TEXTURE_2D_ARRAY:i.TEXTURE_2D}function M(A,y,O,Y,j,ee=!1){if(A!==null){if(i[A]!==void 0)return i[A];Te("WebGLRenderer: Attempt to use non-existing WebGL internal format '"+A+"'")}let oe;Y&&(oe=e.get("EXT_texture_norm16"),oe||Te("WebGLRenderer: Unable to use normalized textures without EXT_texture_norm16 extension"));let X=y;if(y===i.RED&&(O===i.FLOAT&&(X=i.R32F),O===i.HALF_FLOAT&&(X=i.R16F),O===i.UNSIGNED_BYTE&&(X=i.R8),O===i.UNSIGNED_SHORT&&oe&&(X=oe.R16_EXT),O===i.SHORT&&oe&&(X=oe.R16_SNORM_EXT)),y===i.RED_INTEGER&&(O===i.UNSIGNED_BYTE&&(X=i.R8UI),O===i.UNSIGNED_SHORT&&(X=i.R16UI),O===i.UNSIGNED_INT&&(X=i.R32UI),O===i.BYTE&&(X=i.R8I),O===i.SHORT&&(X=i.R16I),O===i.INT&&(X=i.R32I)),y===i.RG&&(O===i.FLOAT&&(X=i.RG32F),O===i.HALF_FLOAT&&(X=i.RG16F),O===i.UNSIGNED_BYTE&&(X=i.RG8),O===i.UNSIGNED_SHORT&&oe&&(X=oe.RG16_EXT),O===i.SHORT&&oe&&(X=oe.RG16_SNORM_EXT)),y===i.RG_INTEGER&&(O===i.UNSIGNED_BYTE&&(X=i.RG8UI),O===i.UNSIGNED_SHORT&&(X=i.RG16UI),O===i.UNSIGNED_INT&&(X=i.RG32UI),O===i.BYTE&&(X=i.RG8I),O===i.SHORT&&(X=i.RG16I),O===i.INT&&(X=i.RG32I)),y===i.RGB_INTEGER&&(O===i.UNSIGNED_BYTE&&(X=i.RGB8UI),O===i.UNSIGNED_SHORT&&(X=i.RGB16UI),O===i.UNSIGNED_INT&&(X=i.RGB32UI),O===i.BYTE&&(X=i.RGB8I),O===i.SHORT&&(X=i.RGB16I),O===i.INT&&(X=i.RGB32I)),y===i.RGBA_INTEGER&&(O===i.UNSIGNED_BYTE&&(X=i.RGBA8UI),O===i.UNSIGNED_SHORT&&(X=i.RGBA16UI),O===i.UNSIGNED_INT&&(X=i.RGBA32UI),O===i.BYTE&&(X=i.RGBA8I),O===i.SHORT&&(X=i.RGBA16I),O===i.INT&&(X=i.RGBA32I)),y===i.RGB&&(O===i.UNSIGNED_SHORT&&oe&&(X=oe.RGB16_EXT),O===i.SHORT&&oe&&(X=oe.RGB16_SNORM_EXT),O===i.UNSIGNED_INT_5_9_9_9_REV&&(X=i.RGB9_E5),O===i.UNSIGNED_INT_10F_11F_11F_REV&&(X=i.R11F_G11F_B10F)),y===i.RGBA){let Z=ee?ir:ze.getTransfer(j);O===i.FLOAT&&(X=i.RGBA32F),O===i.HALF_FLOAT&&(X=i.RGBA16F),O===i.UNSIGNED_BYTE&&(X=Z===Je?i.SRGB8_ALPHA8:i.RGBA8),O===i.UNSIGNED_SHORT&&oe&&(X=oe.RGBA16_EXT),O===i.SHORT&&oe&&(X=oe.RGBA16_SNORM_EXT),O===i.UNSIGNED_SHORT_4_4_4_4&&(X=i.RGBA4),O===i.UNSIGNED_SHORT_5_5_5_1&&(X=i.RGB5_A1)}return(X===i.R16F||X===i.R32F||X===i.RG16F||X===i.RG32F||X===i.RGBA16F||X===i.RGBA32F)&&e.get("EXT_color_buffer_float"),X}function E(A,y){let O;return A?y===null||y===Rn||y===Ps?O=i.DEPTH24_STENCIL8:y===Pn?O=i.DEPTH32F_STENCIL8:y===Rs&&(O=i.DEPTH24_STENCIL8,Te("DepthTexture: 16 bit depth attachment is not supported with stencil. Using 24-bit attachment.")):y===null||y===Rn||y===Ps?O=i.DEPTH_COMPONENT24:y===Pn?O=i.DEPTH_COMPONENT32F:y===Rs&&(O=i.DEPTH_COMPONENT16),O}function w(A,y){return p(A)===!0||A.isFramebufferTexture&&A.minFilter!==Dt&&A.minFilter!==Bt?Math.log2(Math.max(y.width,y.height))+1:A.mipmaps!==void 0&&A.mipmaps.length>0?A.mipmaps.length:A.isCompressedTexture&&Array.isArray(A.image)?y.mipmaps.length:1}function C(A){let y=A.target;y.removeEventListener("dispose",C),T(y),y.isVideoTexture&&u.delete(y),y.isHTMLTexture&&d.delete(y)}function _(A){let y=A.target;y.removeEventListener("dispose",_),R(y)}function T(A){let y=n.get(A);if(y.__webglInit===void 0)return;let O=A.source,Y=f.get(O);if(Y){let j=Y[y.__cacheKey];j.usedTimes--,j.usedTimes===0&&P(A),Object.keys(Y).length===0&&f.delete(O)}n.remove(A)}function P(A){let y=n.get(A);i.deleteTexture(y.__webglTexture);let O=A.source,Y=f.get(O);delete Y[y.__cacheKey],o.memory.textures--}function R(A){let y=n.get(A);if(A.depthTexture&&(A.depthTexture.dispose(),n.remove(A.depthTexture)),A.isWebGLCubeRenderTarget)for(let Y=0;Y<6;Y++){if(Array.isArray(y.__webglFramebuffer[Y]))for(let j=0;j=s.maxTextures&&Te("WebGLTextures: Trying to use "+A+" texture units while this GPU supports only "+s.maxTextures),I+=1,A}function V(A){let y=[];return y.push(A.wrapS),y.push(A.wrapT),y.push(A.wrapR||0),y.push(A.magFilter),y.push(A.minFilter),y.push(A.anisotropy),y.push(A.internalFormat),y.push(A.format),y.push(A.type),y.push(A.generateMipmaps),y.push(A.premultiplyAlpha),y.push(A.flipY),y.push(A.unpackAlignment),y.push(A.colorSpace),y.join()}function K(A,y){let O=n.get(A);if(A.isVideoTexture&&ot(A),A.isRenderTargetTexture===!1&&A.isExternalTexture!==!0&&A.version>0&&O.__version!==A.version){let Y=A.image;if(Y===null)Te("WebGLRenderer: Texture marked for update but no image data found.");else if(Y.complete===!1)Te("WebGLRenderer: Texture marked for update but image is incomplete");else{Ae(O,A,y);return}}else A.isExternalTexture&&(O.__webglTexture=A.sourceTexture?A.sourceTexture:null);t.bindTexture(i.TEXTURE_2D,O.__webglTexture,i.TEXTURE0+y)}function Q(A,y){let O=n.get(A);if(A.isRenderTargetTexture===!1&&A.version>0&&O.__version!==A.version){Ae(O,A,y);return}else A.isExternalTexture&&(O.__webglTexture=A.sourceTexture?A.sourceTexture:null);t.bindTexture(i.TEXTURE_2D_ARRAY,O.__webglTexture,i.TEXTURE0+y)}function ae(A,y){let O=n.get(A);if(A.isRenderTargetTexture===!1&&A.version>0&&O.__version!==A.version){Ae(O,A,y);return}t.bindTexture(i.TEXTURE_3D,O.__webglTexture,i.TEXTURE0+y)}function _e(A,y){let O=n.get(A);if(A.isCubeDepthTexture!==!0&&A.version>0&&O.__version!==A.version){Ne(O,A,y);return}t.bindTexture(i.TEXTURE_CUBE_MAP,O.__webglTexture,i.TEXTURE0+y)}let be={[Bo]:i.REPEAT,[On]:i.CLAMP_TO_EDGE,[zo]:i.MIRRORED_REPEAT},We={[Dt]:i.NEAREST,[cd]:i.NEAREST_MIPMAP_NEAREST,[Ar]:i.NEAREST_MIPMAP_LINEAR,[Bt]:i.LINEAR,[ma]:i.LINEAR_MIPMAP_NEAREST,[Li]:i.LINEAR_MIPMAP_LINEAR},Qe={[dd]:i.NEVER,[xd]:i.ALWAYS,[fd]:i.LESS,[ja]:i.LEQUAL,[pd]:i.EQUAL,[Qa]:i.GEQUAL,[md]:i.GREATER,[gd]:i.NOTEQUAL};function ke(A,y){if(y.type===Pn&&e.has("OES_texture_float_linear")===!1&&(y.magFilter===Bt||y.magFilter===ma||y.magFilter===Ar||y.magFilter===Li||y.minFilter===Bt||y.minFilter===ma||y.minFilter===Ar||y.minFilter===Li)&&Te("WebGLRenderer: Unable to use linear filtering with floating point textures. OES_texture_float_linear not supported on this device."),i.texParameteri(A,i.TEXTURE_WRAP_S,be[y.wrapS]),i.texParameteri(A,i.TEXTURE_WRAP_T,be[y.wrapT]),(A===i.TEXTURE_3D||A===i.TEXTURE_2D_ARRAY)&&i.texParameteri(A,i.TEXTURE_WRAP_R,be[y.wrapR]),i.texParameteri(A,i.TEXTURE_MAG_FILTER,We[y.magFilter]),i.texParameteri(A,i.TEXTURE_MIN_FILTER,We[y.minFilter]),y.compareFunction&&(i.texParameteri(A,i.TEXTURE_COMPARE_MODE,i.COMPARE_REF_TO_TEXTURE),i.texParameteri(A,i.TEXTURE_COMPARE_FUNC,Qe[y.compareFunction])),e.has("EXT_texture_filter_anisotropic")===!0){if(y.magFilter===Dt||y.minFilter!==Ar&&y.minFilter!==Li||y.type===Pn&&e.has("OES_texture_float_linear")===!1)return;if(y.anisotropy>1||n.get(y).__currentAnisotropy){let O=e.get("EXT_texture_filter_anisotropic");i.texParameterf(A,O.TEXTURE_MAX_ANISOTROPY_EXT,Math.min(y.anisotropy,s.getMaxAnisotropy())),n.get(y).__currentAnisotropy=y.anisotropy}}}function $(A,y){let O=!1;A.__webglInit===void 0&&(A.__webglInit=!0,y.addEventListener("dispose",C));let Y=y.source,j=f.get(Y);j===void 0&&(j={},f.set(Y,j));let ee=V(y);if(ee!==A.__cacheKey){j[ee]===void 0&&(j[ee]={texture:i.createTexture(),usedTimes:0},o.memory.textures++,O=!0),j[ee].usedTimes++;let oe=j[A.__cacheKey];oe!==void 0&&(j[A.__cacheKey].usedTimes--,oe.usedTimes===0&&P(y)),A.__cacheKey=ee,A.__webglTexture=j[ee].texture}return O}function de(A,y,O){return Math.floor(Math.floor(A/O)/y)}function ie(A,y,O,Y){let ee=A.updateRanges;if(ee.length===0)t.texSubImage2D(i.TEXTURE_2D,0,0,0,y.width,y.height,O,Y,y.data);else{ee.sort((xe,se)=>xe.start-se.start);let oe=0;for(let xe=1;xe0){Oe&&Ke&&t.texStorage2D(i.TEXTURE_2D,ne,se,Ie[0].width,Ie[0].height);for(let q=0,pe=Ie.length;q0){let re=ih(te.width,te.height,y.format,y.type);for(let J of y.layerUpdates){let Me=te.data.subarray(J*re/te.data.BYTES_PER_ELEMENT,(J+1)*re/te.data.BYTES_PER_ELEMENT);t.compressedTexSubImage3D(i.TEXTURE_2D_ARRAY,q,0,0,J,te.width,te.height,1,fe,Me)}y.clearLayerUpdates()}else t.compressedTexSubImage3D(i.TEXTURE_2D_ARRAY,q,0,0,0,te.width,te.height,Z.depth,fe,te.data)}else t.compressedTexImage3D(i.TEXTURE_2D_ARRAY,q,se,te.width,te.height,Z.depth,0,te.data,0,0);else Te("WebGLRenderer: Attempt to load unsupported compressed texture format in .uploadTexture()");else Oe?L&&t.texSubImage3D(i.TEXTURE_2D_ARRAY,q,0,0,0,te.width,te.height,Z.depth,fe,xe,te.data):t.texImage3D(i.TEXTURE_2D_ARRAY,q,se,te.width,te.height,Z.depth,0,fe,xe,te.data)}else{Oe&&Ke&&t.texStorage2D(i.TEXTURE_2D,ne,se,Ie[0].width,Ie[0].height);for(let q=0,pe=Ie.length;q0){let q=ih(Z.width,Z.height,y.format,y.type);for(let pe of y.layerUpdates){let re=Z.data.subarray(pe*q/Z.data.BYTES_PER_ELEMENT,(pe+1)*q/Z.data.BYTES_PER_ELEMENT);t.texSubImage3D(i.TEXTURE_2D_ARRAY,0,0,0,pe,Z.width,Z.height,1,fe,xe,re)}y.clearLayerUpdates()}else t.texSubImage3D(i.TEXTURE_2D_ARRAY,0,0,0,0,Z.width,Z.height,Z.depth,fe,xe,Z.data)}else t.texImage3D(i.TEXTURE_2D_ARRAY,0,se,Z.width,Z.height,Z.depth,0,fe,xe,Z.data);else if(y.isData3DTexture)Oe?(Ke&&t.texStorage3D(i.TEXTURE_3D,ne,se,Z.width,Z.height,Z.depth),L&&t.texSubImage3D(i.TEXTURE_3D,0,0,0,0,Z.width,Z.height,Z.depth,fe,xe,Z.data)):t.texImage3D(i.TEXTURE_3D,0,se,Z.width,Z.height,Z.depth,0,fe,xe,Z.data);else if(y.isFramebufferTexture){if(Ke)if(Oe)t.texStorage2D(i.TEXTURE_2D,ne,se,Z.width,Z.height);else{let q=Z.width,pe=Z.height;for(let re=0;re>=1,pe>>=1}}else if(y.isHTMLTexture){if("texElementImage2D"in i){let q=i.canvas;if(q.hasAttribute("layoutsubtree")||q.setAttribute("layoutsubtree","true"),Z.parentNode!==q){q.appendChild(Z),d.add(y),q.onpaint=De=>{let yt=De.changedElements;for(let tt of d)yt.includes(tt.image)&&(tt.needsUpdate=!0)},q.requestPaint();return}let pe=0,re=i.RGBA,J=i.RGBA,Me=i.UNSIGNED_BYTE;i.texElementImage2D(i.TEXTURE_2D,pe,re,J,Me,Z),i.texParameteri(i.TEXTURE_2D,i.TEXTURE_MIN_FILTER,i.LINEAR),i.texParameteri(i.TEXTURE_2D,i.TEXTURE_WRAP_S,i.CLAMP_TO_EDGE),i.texParameteri(i.TEXTURE_2D,i.TEXTURE_WRAP_T,i.CLAMP_TO_EDGE)}}else if(Ie.length>0){if(Oe&&Ke){let q=pt(Ie[0]);t.texStorage2D(i.TEXTURE_2D,ne,se,q.width,q.height)}for(let q=0,pe=Ie.length;q0&&pe++;let J=pt(se[0]);t.texStorage2D(i.TEXTURE_CUBE_MAP,pe,Ke,J.width,J.height)}for(let J=0;J<6;J++)if(xe){L?q&&t.texSubImage2D(i.TEXTURE_CUBE_MAP_POSITIVE_X+J,0,0,0,se[J].width,se[J].height,Ie,Oe,se[J].data):t.texImage2D(i.TEXTURE_CUBE_MAP_POSITIVE_X+J,0,Ke,se[J].width,se[J].height,0,Ie,Oe,se[J].data);for(let Me=0;Me>ee),te=Math.max(1,y.height>>ee);j===i.TEXTURE_3D||j===i.TEXTURE_2D_ARRAY?t.texImage3D(j,ee,Z,se,te,y.depth,0,oe,X,null):t.texImage2D(j,ee,Z,se,te,0,oe,X,null)}t.bindFramebuffer(i.FRAMEBUFFER,A),qe(y)?a.framebufferTexture2DMultisampleEXT(i.FRAMEBUFFER,Y,j,xe.__webglTexture,0,At(y)):(j===i.TEXTURE_2D||j>=i.TEXTURE_CUBE_MAP_POSITIVE_X&&j<=i.TEXTURE_CUBE_MAP_NEGATIVE_Z)&&i.framebufferTexture2D(i.FRAMEBUFFER,Y,j,xe.__webglTexture,ee),t.bindFramebuffer(i.FRAMEBUFFER,null)}function dt(A,y,O){if(i.bindRenderbuffer(i.RENDERBUFFER,A),y.depthBuffer){let Y=y.depthTexture,j=Y&&Y.isDepthTexture?Y.type:null,ee=E(y.stencilBuffer,j),oe=y.stencilBuffer?i.DEPTH_STENCIL_ATTACHMENT:i.DEPTH_ATTACHMENT;qe(y)?a.renderbufferStorageMultisampleEXT(i.RENDERBUFFER,At(y),ee,y.width,y.height):O?i.renderbufferStorageMultisample(i.RENDERBUFFER,At(y),ee,y.width,y.height):i.renderbufferStorage(i.RENDERBUFFER,ee,y.width,y.height),i.framebufferRenderbuffer(i.FRAMEBUFFER,oe,i.RENDERBUFFER,A)}else{let Y=y.textures;for(let j=0;j{delete y.__boundDepthTexture,delete y.__depthDisposeCallback,Y.removeEventListener("dispose",j)};Y.addEventListener("dispose",j),y.__depthDisposeCallback=j}y.__boundDepthTexture=Y}if(A.depthTexture&&!y.__autoAllocateDepthBuffer)if(O)for(let Y=0;Y<6;Y++)Xe(y.__webglFramebuffer[Y],A,Y);else{let Y=A.texture.mipmaps;Y&&Y.length>0?Xe(y.__webglFramebuffer[0],A,0):Xe(y.__webglFramebuffer,A,0)}else if(O){y.__webglDepthbuffer=[];for(let Y=0;Y<6;Y++)if(t.bindFramebuffer(i.FRAMEBUFFER,y.__webglFramebuffer[Y]),y.__webglDepthbuffer[Y]===void 0)y.__webglDepthbuffer[Y]=i.createRenderbuffer(),dt(y.__webglDepthbuffer[Y],A,!1);else{let j=A.stencilBuffer?i.DEPTH_STENCIL_ATTACHMENT:i.DEPTH_ATTACHMENT,ee=y.__webglDepthbuffer[Y];i.bindRenderbuffer(i.RENDERBUFFER,ee),i.framebufferRenderbuffer(i.FRAMEBUFFER,j,i.RENDERBUFFER,ee)}}else{let Y=A.texture.mipmaps;if(Y&&Y.length>0?t.bindFramebuffer(i.FRAMEBUFFER,y.__webglFramebuffer[0]):t.bindFramebuffer(i.FRAMEBUFFER,y.__webglFramebuffer),y.__webglDepthbuffer===void 0)y.__webglDepthbuffer=i.createRenderbuffer(),dt(y.__webglDepthbuffer,A,!1);else{let j=A.stencilBuffer?i.DEPTH_STENCIL_ATTACHMENT:i.DEPTH_ATTACHMENT,ee=y.__webglDepthbuffer;i.bindRenderbuffer(i.RENDERBUFFER,ee),i.framebufferRenderbuffer(i.FRAMEBUFFER,j,i.RENDERBUFFER,ee)}}t.bindFramebuffer(i.FRAMEBUFFER,null)}function lt(A,y,O){let Y=n.get(A);y!==void 0&&Pe(Y.__webglFramebuffer,A,A.texture,i.COLOR_ATTACHMENT0,i.TEXTURE_2D,0),O!==void 0&&et(A)}function He(A){let y=A.texture,O=n.get(A),Y=n.get(y);A.addEventListener("dispose",_);let j=A.textures,ee=A.isWebGLCubeRenderTarget===!0,oe=j.length>1;if(oe||(Y.__webglTexture===void 0&&(Y.__webglTexture=i.createTexture()),Y.__version=y.version,o.memory.textures++),ee){O.__webglFramebuffer=[];for(let X=0;X<6;X++)if(y.mipmaps&&y.mipmaps.length>0){O.__webglFramebuffer[X]=[];for(let Z=0;Z0){O.__webglFramebuffer=[];for(let X=0;X0&&qe(A)===!1){O.__webglMultisampledFramebuffer=i.createFramebuffer(),O.__webglColorRenderbuffer=[],t.bindFramebuffer(i.FRAMEBUFFER,O.__webglMultisampledFramebuffer);for(let X=0;X0)for(let Z=0;Z0)for(let Z=0;Z0){if(qe(A)===!1){let y=A.textures,O=A.width,Y=A.height,j=i.COLOR_BUFFER_BIT,ee=A.stencilBuffer?i.DEPTH_STENCIL_ATTACHMENT:i.DEPTH_ATTACHMENT,oe=n.get(A),X=y.length>1;if(X)for(let fe=0;fe0?t.bindFramebuffer(i.DRAW_FRAMEBUFFER,oe.__webglFramebuffer[0]):t.bindFramebuffer(i.DRAW_FRAMEBUFFER,oe.__webglFramebuffer);for(let fe=0;fe0&&e.has("WEBGL_multisampled_render_to_texture")===!0&&y.__useRenderToTexture!==!1}function ot(A){let y=o.render.frame;u.get(A)!==y&&(u.set(A,y),A.update())}function le(A,y){let O=A.colorSpace,Y=A.format,j=A.type;return A.isCompressedTexture===!0||A.isVideoTexture===!0||O!==nr&&O!==ri&&(ze.getTransfer(O)===Je?(Y!==bn||j!==pn)&&Te("WebGLTextures: sRGB encoded textures have to use RGBAFormat and UnsignedByteType."):Re("WebGLTextures: Unsupported texture color space:",O)),y}function pt(A){return typeof HTMLImageElement<"u"&&A instanceof HTMLImageElement?(c.width=A.naturalWidth||A.width,c.height=A.naturalHeight||A.height):typeof VideoFrame<"u"&&A instanceof VideoFrame?(c.width=A.displayWidth,c.height=A.displayHeight):(c.width=A.width,c.height=A.height),c}this.allocateTextureUnit=z,this.resetTextureUnits=H,this.getTextureUnits=W,this.setTextureUnits=k,this.setTexture2D=K,this.setTexture2DArray=Q,this.setTexture3D=ae,this.setTextureCube=_e,this.rebindTextures=lt,this.setupRenderTarget=He,this.updateRenderTargetMipmap=Tt,this.updateMultisampleRenderTarget=D,this.setupDepthRenderbuffer=et,this.setupFrameBufferTexture=Pe,this.useMultisampledRTT=qe,this.isReversedDepthBuffer=function(){return t.buffers.depth.getReversed()}}function J_(i,e){function t(n,s=ri){let r,o=ze.getTransfer(s);if(n===pn)return i.UNSIGNED_BYTE;if(n===xa)return i.UNSIGNED_SHORT_4_4_4_4;if(n===va)return i.UNSIGNED_SHORT_5_5_5_1;if(n===Wc)return i.UNSIGNED_INT_5_9_9_9_REV;if(n===Xc)return i.UNSIGNED_INT_10F_11F_11F_REV;if(n===Gc)return i.BYTE;if(n===Hc)return i.SHORT;if(n===Rs)return i.UNSIGNED_SHORT;if(n===ga)return i.INT;if(n===Rn)return i.UNSIGNED_INT;if(n===Pn)return i.FLOAT;if(n===qt)return i.HALF_FLOAT;if(n===qc)return i.ALPHA;if(n===Yc)return i.RGB;if(n===bn)return i.RGBA;if(n===Un)return i.DEPTH_COMPONENT;if(n===Ni)return i.DEPTH_STENCIL;if(n===Zc)return i.RED;if(n===_a)return i.RED_INTEGER;if(n===Di)return i.RG;if(n===ya)return i.RG_INTEGER;if(n===ba)return i.RGBA_INTEGER;if(n===Cr||n===Rr||n===Pr||n===Ir)if(o===Je)if(r=e.get("WEBGL_compressed_texture_s3tc_srgb"),r!==null){if(n===Cr)return r.COMPRESSED_SRGB_S3TC_DXT1_EXT;if(n===Rr)return r.COMPRESSED_SRGB_ALPHA_S3TC_DXT1_EXT;if(n===Pr)return r.COMPRESSED_SRGB_ALPHA_S3TC_DXT3_EXT;if(n===Ir)return r.COMPRESSED_SRGB_ALPHA_S3TC_DXT5_EXT}else return null;else if(r=e.get("WEBGL_compressed_texture_s3tc"),r!==null){if(n===Cr)return r.COMPRESSED_RGB_S3TC_DXT1_EXT;if(n===Rr)return r.COMPRESSED_RGBA_S3TC_DXT1_EXT;if(n===Pr)return r.COMPRESSED_RGBA_S3TC_DXT3_EXT;if(n===Ir)return r.COMPRESSED_RGBA_S3TC_DXT5_EXT}else return null;if(n===Ma||n===Sa||n===wa||n===Ea)if(r=e.get("WEBGL_compressed_texture_pvrtc"),r!==null){if(n===Ma)return r.COMPRESSED_RGB_PVRTC_4BPPV1_IMG;if(n===Sa)return r.COMPRESSED_RGB_PVRTC_2BPPV1_IMG;if(n===wa)return r.COMPRESSED_RGBA_PVRTC_4BPPV1_IMG;if(n===Ea)return r.COMPRESSED_RGBA_PVRTC_2BPPV1_IMG}else return null;if(n===Ta||n===Aa||n===Ca||n===Ra||n===Pa||n===Lr||n===Ia)if(r=e.get("WEBGL_compressed_texture_etc"),r!==null){if(n===Ta||n===Aa)return o===Je?r.COMPRESSED_SRGB8_ETC2:r.COMPRESSED_RGB8_ETC2;if(n===Ca)return o===Je?r.COMPRESSED_SRGB8_ALPHA8_ETC2_EAC:r.COMPRESSED_RGBA8_ETC2_EAC;if(n===Ra)return r.COMPRESSED_R11_EAC;if(n===Pa)return r.COMPRESSED_SIGNED_R11_EAC;if(n===Lr)return r.COMPRESSED_RG11_EAC;if(n===Ia)return r.COMPRESSED_SIGNED_RG11_EAC}else return null;if(n===La||n===Na||n===Da||n===Fa||n===ka||n===Oa||n===Ua||n===Ba||n===za||n===Va||n===Ga||n===Ha||n===Wa||n===Xa)if(r=e.get("WEBGL_compressed_texture_astc"),r!==null){if(n===La)return o===Je?r.COMPRESSED_SRGB8_ALPHA8_ASTC_4x4_KHR:r.COMPRESSED_RGBA_ASTC_4x4_KHR;if(n===Na)return o===Je?r.COMPRESSED_SRGB8_ALPHA8_ASTC_5x4_KHR:r.COMPRESSED_RGBA_ASTC_5x4_KHR;if(n===Da)return o===Je?r.COMPRESSED_SRGB8_ALPHA8_ASTC_5x5_KHR:r.COMPRESSED_RGBA_ASTC_5x5_KHR;if(n===Fa)return o===Je?r.COMPRESSED_SRGB8_ALPHA8_ASTC_6x5_KHR:r.COMPRESSED_RGBA_ASTC_6x5_KHR;if(n===ka)return o===Je?r.COMPRESSED_SRGB8_ALPHA8_ASTC_6x6_KHR:r.COMPRESSED_RGBA_ASTC_6x6_KHR;if(n===Oa)return o===Je?r.COMPRESSED_SRGB8_ALPHA8_ASTC_8x5_KHR:r.COMPRESSED_RGBA_ASTC_8x5_KHR;if(n===Ua)return o===Je?r.COMPRESSED_SRGB8_ALPHA8_ASTC_8x6_KHR:r.COMPRESSED_RGBA_ASTC_8x6_KHR;if(n===Ba)return o===Je?r.COMPRESSED_SRGB8_ALPHA8_ASTC_8x8_KHR:r.COMPRESSED_RGBA_ASTC_8x8_KHR;if(n===za)return o===Je?r.COMPRESSED_SRGB8_ALPHA8_ASTC_10x5_KHR:r.COMPRESSED_RGBA_ASTC_10x5_KHR;if(n===Va)return o===Je?r.COMPRESSED_SRGB8_ALPHA8_ASTC_10x6_KHR:r.COMPRESSED_RGBA_ASTC_10x6_KHR;if(n===Ga)return o===Je?r.COMPRESSED_SRGB8_ALPHA8_ASTC_10x8_KHR:r.COMPRESSED_RGBA_ASTC_10x8_KHR;if(n===Ha)return o===Je?r.COMPRESSED_SRGB8_ALPHA8_ASTC_10x10_KHR:r.COMPRESSED_RGBA_ASTC_10x10_KHR;if(n===Wa)return o===Je?r.COMPRESSED_SRGB8_ALPHA8_ASTC_12x10_KHR:r.COMPRESSED_RGBA_ASTC_12x10_KHR;if(n===Xa)return o===Je?r.COMPRESSED_SRGB8_ALPHA8_ASTC_12x12_KHR:r.COMPRESSED_RGBA_ASTC_12x12_KHR}else return null;if(n===qa||n===Ya||n===Za)if(r=e.get("EXT_texture_compression_bptc"),r!==null){if(n===qa)return o===Je?r.COMPRESSED_SRGB_ALPHA_BPTC_UNORM_EXT:r.COMPRESSED_RGBA_BPTC_UNORM_EXT;if(n===Ya)return r.COMPRESSED_RGB_BPTC_SIGNED_FLOAT_EXT;if(n===Za)return r.COMPRESSED_RGB_BPTC_UNSIGNED_FLOAT_EXT}else return null;if(n===$a||n===Ka||n===Nr||n===Ja)if(r=e.get("EXT_texture_compression_rgtc"),r!==null){if(n===$a)return r.COMPRESSED_RED_RGTC1_EXT;if(n===Ka)return r.COMPRESSED_SIGNED_RED_RGTC1_EXT;if(n===Nr)return r.COMPRESSED_RED_GREEN_RGTC2_EXT;if(n===Ja)return r.COMPRESSED_SIGNED_RED_GREEN_RGTC2_EXT}else return null;return n===Ps?i.UNSIGNED_INT_24_8:i[n]!==void 0?i[n]:null}return{convert:t}}var j_=` +void main() { -function graphNode(index, graph, id) { - return (graph && graph.nodesById && graph.nodesById.get(id)) || index.nodes.get(id); -} + gl_Position = vec4( position, 1.0 ); -function uniqueEdgesByEndpoint(edges, endpointKey) { - const byEndpoint = new Map(); - for (const edge of edges) { - const endpoint = edge && edge[endpointKey]; - if (!endpoint) continue; - const existing = byEndpoint.get(endpoint); - if (existing) { - existing.weight += edge.weight || 0; - continue; - } - byEndpoint.set(endpoint, Object.assign({}, edge)); - } - return Array.from(byEndpoint.values()); -} +}`,Q_=` +uniform sampler2DArray depthColor; +uniform float depthWidth; +uniform float depthHeight; -function buildBudgetChildrenByParent(nodes) { - const childrenByParent = new Map(); - for (const node of nodes || []) { - if (!node || node.parentId === null || node.parentId === undefined) continue; - if (!childrenByParent.has(node.parentId)) childrenByParent.set(node.parentId, []); - childrenByParent.get(node.parentId).push(node.id); - } - - const nodeById = new Map((nodes || []).map(node => [node.id, node])); - for (const children of childrenByParent.values()) { - children.sort((a, b) => compareNodes(nodeById.get(a), nodeById.get(b))); - } - return childrenByParent; -} +void main() { -function hiddenLegendSetFromState(state = {}) { - return new Set(Array.isArray(state.hiddenLegendItems) ? state.hiddenLegendItems : []); -} + vec2 coord = vec2( gl_FragCoord.x / depthWidth, gl_FragCoord.y / depthHeight ); -function legendHidesNode(node, graph, hiddenLegend) { - if (!node || !hiddenLegend || !hiddenLegend.size) return false; - if (node.id === graph?.rootId) return hiddenLegend.has("root"); - if (node.externalProxy) { - if (hiddenLegend.has("outside-file")) return true; - return node.type === "unresolved" && hiddenLegend.has("missing"); - } - if (node.type === "external") return hiddenLegend.has("outside"); - if (node.type === "unresolved") return hiddenLegend.has("missing"); - if (node.type === "folder") { - if (node.representativeFile) return hiddenLegend.has("folder-meta"); - return hiddenLegend.has("folder"); - } - if (node.type === "note") return hiddenLegend.has("file"); - return false; -} + if ( coord.x >= 1.0 ) { -function legendHidesHierarchyEdge(edge, hiddenLegend) { - return Boolean(hiddenLegend?.has("tree")); -} + gl_FragDepth = texture( depthColor, vec3( coord.x - 1.0, coord.y, 1 ) ).r; -function legendHidesLinkEdge(edge, hiddenLegend) { - if (!edge || !hiddenLegend || !hiddenLegend.size) return false; - if (edge.externalCount && hiddenLegend.has("outside-link")) return true; - if (edge.unresolvedCount && hiddenLegend.has("dashed-link")) return true; - return !edge.externalCount && hiddenLegend.has("link"); -} + } else { -function buildCanvasGraphData(index, graph, layout, query, graphSettings = DEFAULT_NATIVE_GRAPH_SETTINGS, options = {}) { - const nodeSizeMultiplier = clampFloat(graphSettings.nodeSizeMultiplier, 0.55, 2.8, 1); - const lineSizeMultiplier = clampFloat(graphSettings.lineSizeMultiplier, 0.35, 4, 1); - const includeHoverLinks = options.includeHoverLinks !== false; - const hiddenLegend = hiddenLegendSetFromState(options); - const nodes = []; - const nodesById = new Map(); - const hierarchy = []; - const links = []; - const hoverLinks = []; - const searchMatchItems = []; - const edgesByNode = new Map(); - const linkEdgesByNode = new Map(); - const hierarchyParentByNode = new Map(); - const hierarchyChildrenByNode = new Map(); - const hierarchyChildNodeIdsByNode = new Map(); - const edgesById = new Map(); - let maxLinkDegree = 1; - const graphNodes = (graph.nodes || []).filter(node => !legendHidesNode(node, graph, hiddenLegend)); - for (const node of graphNodes) { - maxLinkDegree = Math.max(maxLinkDegree, (node.linkCount || 0) + (node.backlinkCount || 0)); - } - - for (const node of graphNodes) { - const point = layout.positions.get(node.id); - if (!point) continue; - const item = { - node, - point, - renderIndex: nodes.length, - radius: Math.max(3.5, nodeRadius(node, point, maxLinkDegree) * nodeSizeMultiplier), - label: node.title, - metric: nodeMetric(node), - searchMatch: Boolean(query && nodeMatches(node, query)) - }; - nodes.push(item); - if (item.searchMatch) searchMatchItems.push(item); - nodesById.set(node.id, item); - } - - for (const node of graphNodes) { - if (!node || node.parentId === null || node.parentId === undefined) continue; - const childId = index.visualNodeId(node.id); - const parentId = index.visualNodeId(node.parentId); - if (childId === null || childId === undefined || parentId === null || parentId === undefined) continue; - if (childId === parentId || !nodesById.has(childId) || !nodesById.has(parentId)) continue; - if (!hierarchyChildNodeIdsByNode.has(parentId)) hierarchyChildNodeIdsByNode.set(parentId, []); - const children = hierarchyChildNodeIdsByNode.get(parentId); - if (!children.includes(childId)) children.push(childId); - } - - assignCanvasLabelPriority(nodes, graph); - const labelItems = nodes - .slice() - .sort((a, b) => (a.labelPriority || 0) - (b.labelPriority || 0)); - const interactiveLabelItems = labelItems.filter(item => - item.searchMatch - || item.labelRank <= FAST_CANVAS_LABEL_LIMIT - || item.node.id === graph.rootId - || item.node.id === graph.focusId - ); - - const registerEdge = (collection, edge, sourcePoint, targetPoint, options = {}) => { - const key = options.key || edge.id || `${edge.source}->${edge.target}`; - const item = { - key, - edge, - source: edge.source, - target: edge.target, - sourcePoint, - targetPoint, - width: options.width || 1, - external: Boolean(options.external), - hoverOnly: Boolean(options.hoverOnly), - route: options.route || null - }; - collection.push(item); - if (edge.id) edgesById.set(edge.id, item); - for (const id of [edge.source, edge.target]) { - if (!id && id !== ROOT_ID) continue; - if (!edgesByNode.has(id)) edgesByNode.set(id, []); - edgesByNode.get(id).push(item); - if (options.linkOverlay) { - if (!linkEdgesByNode.has(id)) linkEdgesByNode.set(id, []); - linkEdgesByNode.get(id).push(item); - } - } - if (options.hierarchyTree) { - hierarchyParentByNode.set(edge.target, item); - if (!hierarchyChildrenByNode.has(edge.source)) hierarchyChildrenByNode.set(edge.source, []); - hierarchyChildrenByNode.get(edge.source).push(item); - } - return item; - }; - - for (const edge of graph.hierarchyEdges) { - if (legendHidesHierarchyEdge(edge, hiddenLegend)) continue; - if (!nodesById.has(edge.source) || !nodesById.has(edge.target)) continue; - const sourcePoint = layout.positions.get(edge.source); - const targetPoint = layout.positions.get(edge.target); - if (!sourcePoint || !targetPoint) continue; - registerEdge(hierarchy, edge, sourcePoint, targetPoint, { - width: edge.type === "external-hierarchy" ? 1.05 : 1.12, - external: edge.type === "external-hierarchy", - hierarchyTree: true - }); - } - - for (const edge of graph.linkEdges) { - if (legendHidesLinkEdge(edge, hiddenLegend)) continue; - if (!nodesById.has(edge.source) || !nodesById.has(edge.target)) continue; - const sourcePoint = layout.positions.get(edge.source); - const targetPoint = layout.positions.get(edge.target); - if (!sourcePoint || !targetPoint) continue; - const weightSignal = Math.log2((edge.weight || 1) + 1); - registerEdge(links, edge, sourcePoint, targetPoint, { - width: clampFloat((0.42 + weightSignal * 0.2) * lineSizeMultiplier, 0.48, 3.4, 1), - external: Boolean(edge.externalCount), - linkOverlay: true, - route: layout.linkRoutes ? layout.linkRoutes.get(edge.id) : null - }); - } - - if (includeHoverLinks) { - const renderedLinkKeys = new Set(links.map(item => item.key)); - for (const edge of graph.hoverLinkEdges || []) { - if (legendHidesLinkEdge(edge, hiddenLegend)) continue; - if (!nodesById.has(edge.source) || !nodesById.has(edge.target)) continue; - const key = edge.id || `${edge.source}->${edge.target}`; - if (renderedLinkKeys.has(key)) continue; - const sourcePoint = layout.positions.get(edge.source); - const targetPoint = layout.positions.get(edge.target); - if (!sourcePoint || !targetPoint) continue; - const weightSignal = Math.log2((edge.weight || 1) + 1); - registerEdge(hoverLinks, edge, sourcePoint, targetPoint, { - width: clampFloat((0.42 + weightSignal * 0.2) * lineSizeMultiplier, 0.48, 3.4, 1), - external: Boolean(edge.externalCount), - linkOverlay: true, - hoverOnly: true, - route: layout.linkRoutes ? layout.linkRoutes.get(edge.id) : null - }); - } - } - - return { - nodes, - nodesById, - nodeSpatialIndex: buildCanvasNodeSpatialIndex(nodes), - labelItems, - interactiveLabelItems, - searchMatchItems, - hierarchy, - links, - hoverLinks, - edgesByNode, - linkEdgesByNode, - hierarchyParentByNode, - hierarchyChildrenByNode, - hierarchyChildNodeIdsByNode, - edgesById, - showArrow: graphSettings.showArrow !== false - }; -} + gl_FragDepth = texture( depthColor, vec3( coord.x, coord.y, 0 ) ).r; -function addHierarchyHoverHighlights(data, nodeId, mode, relatedNodes, highlightedEdges, labelNodes) { - const normalized = normalizeHoverHighlightMode(mode); - if (!data || normalized === "none" || normalized === "note-links") return; - - if ( - normalized === "hierarchy-parents" - || normalized === "hierarchy-parents-direct" - || normalized === "hierarchy-all" - ) { - addHierarchyAncestorHighlights(data, nodeId, relatedNodes, highlightedEdges, labelNodes); - } - - if ( - normalized === "hierarchy-direct-children" - || normalized === "hierarchy-parents-direct" - ) { - addHierarchyChildHighlights(data, nodeId, relatedNodes, highlightedEdges, labelNodes); - } - - if ( - normalized === "hierarchy-descendants" - || normalized === "hierarchy-all" - ) { - addHierarchyDescendantHighlights(data, nodeId, relatedNodes, highlightedEdges, labelNodes); - } -} + } -function addHierarchyAncestorHighlights(data, nodeId, relatedNodes, highlightedEdges, labelNodes) { - const parentByNode = data.hierarchyParentByNode || new Map(); - const visited = new Set([nodeId]); - let currentId = nodeId; - - while (parentByNode.has(currentId)) { - const edge = parentByNode.get(currentId); - if (!edge || highlightedEdges.has(edge.key)) break; - highlightedEdges.add(edge.key); - addHierarchyEdgeNodes(edge, relatedNodes, labelNodes); - currentId = edge.source; - if (visited.has(currentId)) break; - visited.add(currentId); - } -} +}`,Mh=class{constructor(){this.texture=null,this.mesh=null,this.depthNear=0,this.depthFar=0}init(e,t){if(this.texture===null){let n=new dr(e.texture);(e.depthNear!==t.depthNear||e.depthFar!==t.depthFar)&&(this.depthNear=e.depthNear,this.depthFar=e.depthFar),this.texture=n}}getMesh(e){if(this.texture!==null&&this.mesh===null){let t=e.cameras[0].viewport,n=new ut({vertexShader:j_,fragmentShader:Q_,uniforms:{depthColor:{value:this.texture},depthWidth:{value:t.z},depthHeight:{value:t.w}}});this.mesh=new Xt(new fr(20,20),n)}return this.mesh}reset(){this.texture=null,this.mesh=null}getDepthTexture(){return this.texture}},Sh=class extends An{constructor(e,t){super();let n=this,s=null,r=1,o=null,a="local-floor",l=1,c=null,u=null,d=null,h=null,f=null,g=null,x=typeof XRWebGLBinding<"u",m=new Mh,p={},v=t.getContextAttributes(),b=null,M=null,E=[],w=[],C=new Ee,_=null,T=new Wt;T.viewport=new xt;let P=new Wt;P.viewport=new xt;let R=[T,P],I=new ha,H=null,W=null;this.cameraAutoUpdate=!0,this.enabled=!1,this.isPresenting=!1,this.getController=function($){let de=E[$];return de===void 0&&(de=new Es,E[$]=de),de.getTargetRaySpace()},this.getControllerGrip=function($){let de=E[$];return de===void 0&&(de=new Es,E[$]=de),de.getGripSpace()},this.getHand=function($){let de=E[$];return de===void 0&&(de=new Es,E[$]=de),de.getHandSpace()};function k($){let de=w.indexOf($.inputSource);if(de===-1)return;let ie=E[de];ie!==void 0&&(ie.update($.inputSource,$.frame,c||o),ie.dispatchEvent({type:$.type,data:$.inputSource}))}function z(){s.removeEventListener("select",k),s.removeEventListener("selectstart",k),s.removeEventListener("selectend",k),s.removeEventListener("squeeze",k),s.removeEventListener("squeezestart",k),s.removeEventListener("squeezeend",k),s.removeEventListener("end",z),s.removeEventListener("inputsourceschange",V);for(let $=0;$=0&&(w[Ae]=null,E[Ae].disconnect(ie))}for(let de=0;de<$.added.length;de++){let ie=$.added[de],Ae=w.indexOf(ie);if(Ae===-1){for(let Pe=0;Pe=w.length){w.push(ie),Ae=Pe;break}else if(w[Pe]===null){w[Pe]=ie,Ae=Pe;break}if(Ae===-1)break}let Ne=E[Ae];Ne&&Ne.connect(ie)}}let K=new N,Q=new N;function ae($,de,ie){K.setFromMatrixPosition(de.matrixWorld),Q.setFromMatrixPosition(ie.matrixWorld);let Ae=K.distanceTo(Q),Ne=de.projectionMatrix.elements,Pe=ie.projectionMatrix.elements,dt=Ne[14]/(Ne[10]-1),Xe=Ne[14]/(Ne[10]+1),et=(Ne[9]+1)/Ne[5],lt=(Ne[9]-1)/Ne[5],He=(Ne[8]-1)/Ne[0],Tt=(Pe[8]+1)/Pe[0],ft=dt*He,sn=dt*Tt,D=Ae/(-He+Tt),At=D*-He;if(de.matrixWorld.decompose($.position,$.quaternion,$.scale),$.translateX(At),$.translateZ(D),$.matrixWorld.compose($.position,$.quaternion,$.scale),$.matrixWorldInverse.copy($.matrixWorld).invert(),Ne[10]===-1)$.projectionMatrix.copy(de.projectionMatrix),$.projectionMatrixInverse.copy(de.projectionMatrixInverse);else{let qe=dt+D,ot=Xe+D,le=ft-At,pt=sn+(Ae-At),A=et*Xe/ot*qe,y=lt*Xe/ot*qe;$.projectionMatrix.makePerspective(le,pt,A,y,qe,ot),$.projectionMatrixInverse.copy($.projectionMatrix).invert()}}function _e($,de){de===null?$.matrixWorld.copy($.matrix):$.matrixWorld.multiplyMatrices(de.matrixWorld,$.matrix),$.matrixWorldInverse.copy($.matrixWorld).invert()}this.updateCamera=function($){if(s===null)return;let de=$.near,ie=$.far;m.texture!==null&&(m.depthNear>0&&(de=m.depthNear),m.depthFar>0&&(ie=m.depthFar)),I.near=P.near=T.near=de,I.far=P.far=T.far=ie,(H!==I.near||W!==I.far)&&(s.updateRenderState({depthNear:I.near,depthFar:I.far}),H=I.near,W=I.far),I.layers.mask=$.layers.mask|6,T.layers.mask=I.layers.mask&-5,P.layers.mask=I.layers.mask&-3;let Ae=$.parent,Ne=I.cameras;_e(I,Ae);for(let Pe=0;Pe0&&(m.alphaTest.value=p.alphaTest);let v=e.get(p),b=v.envMap,M=v.envMapRotation;b&&(m.envMap.value=b,m.envMapRotation.value.setFromMatrix4(ey.makeRotationFromEuler(M)).transpose(),b.isCubeTexture&&b.isRenderTargetTexture===!1&&m.envMapRotation.value.premultiply(jd),m.reflectivity.value=p.reflectivity,m.ior.value=p.ior,m.refractionRatio.value=p.refractionRatio),p.lightMap&&(m.lightMap.value=p.lightMap,m.lightMapIntensity.value=p.lightMapIntensity,t(p.lightMap,m.lightMapTransform)),p.aoMap&&(m.aoMap.value=p.aoMap,m.aoMapIntensity.value=p.aoMapIntensity,t(p.aoMap,m.aoMapTransform))}function o(m,p){m.diffuse.value.copy(p.color),m.opacity.value=p.opacity,p.map&&(m.map.value=p.map,t(p.map,m.mapTransform))}function a(m,p){m.dashSize.value=p.dashSize,m.totalSize.value=p.dashSize+p.gapSize,m.scale.value=p.scale}function l(m,p,v,b){m.diffuse.value.copy(p.color),m.opacity.value=p.opacity,m.size.value=p.size*v,m.scale.value=b*.5,p.map&&(m.map.value=p.map,t(p.map,m.uvTransform)),p.alphaMap&&(m.alphaMap.value=p.alphaMap,t(p.alphaMap,m.alphaMapTransform)),p.alphaTest>0&&(m.alphaTest.value=p.alphaTest)}function c(m,p){m.diffuse.value.copy(p.color),m.opacity.value=p.opacity,m.rotation.value=p.rotation,p.map&&(m.map.value=p.map,t(p.map,m.mapTransform)),p.alphaMap&&(m.alphaMap.value=p.alphaMap,t(p.alphaMap,m.alphaMapTransform)),p.alphaTest>0&&(m.alphaTest.value=p.alphaTest)}function u(m,p){m.specular.value.copy(p.specular),m.shininess.value=Math.max(p.shininess,1e-4)}function d(m,p){p.gradientMap&&(m.gradientMap.value=p.gradientMap)}function h(m,p){m.metalness.value=p.metalness,p.metalnessMap&&(m.metalnessMap.value=p.metalnessMap,t(p.metalnessMap,m.metalnessMapTransform)),m.roughness.value=p.roughness,p.roughnessMap&&(m.roughnessMap.value=p.roughnessMap,t(p.roughnessMap,m.roughnessMapTransform)),p.envMap&&(m.envMapIntensity.value=p.envMapIntensity)}function f(m,p,v){m.ior.value=p.ior,p.sheen>0&&(m.sheenColor.value.copy(p.sheenColor).multiplyScalar(p.sheen),m.sheenRoughness.value=p.sheenRoughness,p.sheenColorMap&&(m.sheenColorMap.value=p.sheenColorMap,t(p.sheenColorMap,m.sheenColorMapTransform)),p.sheenRoughnessMap&&(m.sheenRoughnessMap.value=p.sheenRoughnessMap,t(p.sheenRoughnessMap,m.sheenRoughnessMapTransform))),p.clearcoat>0&&(m.clearcoat.value=p.clearcoat,m.clearcoatRoughness.value=p.clearcoatRoughness,p.clearcoatMap&&(m.clearcoatMap.value=p.clearcoatMap,t(p.clearcoatMap,m.clearcoatMapTransform)),p.clearcoatRoughnessMap&&(m.clearcoatRoughnessMap.value=p.clearcoatRoughnessMap,t(p.clearcoatRoughnessMap,m.clearcoatRoughnessMapTransform)),p.clearcoatNormalMap&&(m.clearcoatNormalMap.value=p.clearcoatNormalMap,t(p.clearcoatNormalMap,m.clearcoatNormalMapTransform),m.clearcoatNormalScale.value.copy(p.clearcoatNormalScale),p.side===$t&&m.clearcoatNormalScale.value.negate())),p.dispersion>0&&(m.dispersion.value=p.dispersion),p.iridescence>0&&(m.iridescence.value=p.iridescence,m.iridescenceIOR.value=p.iridescenceIOR,m.iridescenceThicknessMinimum.value=p.iridescenceThicknessRange[0],m.iridescenceThicknessMaximum.value=p.iridescenceThicknessRange[1],p.iridescenceMap&&(m.iridescenceMap.value=p.iridescenceMap,t(p.iridescenceMap,m.iridescenceMapTransform)),p.iridescenceThicknessMap&&(m.iridescenceThicknessMap.value=p.iridescenceThicknessMap,t(p.iridescenceThicknessMap,m.iridescenceThicknessMapTransform))),p.transmission>0&&(m.transmission.value=p.transmission,m.transmissionSamplerMap.value=v.texture,m.transmissionSamplerSize.value.set(v.width,v.height),p.transmissionMap&&(m.transmissionMap.value=p.transmissionMap,t(p.transmissionMap,m.transmissionMapTransform)),m.thickness.value=p.thickness,p.thicknessMap&&(m.thicknessMap.value=p.thicknessMap,t(p.thicknessMap,m.thicknessMapTransform)),m.attenuationDistance.value=p.attenuationDistance,m.attenuationColor.value.copy(p.attenuationColor)),p.anisotropy>0&&(m.anisotropyVector.value.set(p.anisotropy*Math.cos(p.anisotropyRotation),p.anisotropy*Math.sin(p.anisotropyRotation)),p.anisotropyMap&&(m.anisotropyMap.value=p.anisotropyMap,t(p.anisotropyMap,m.anisotropyMapTransform))),m.specularIntensity.value=p.specularIntensity,m.specularColor.value.copy(p.specularColor),p.specularColorMap&&(m.specularColorMap.value=p.specularColorMap,t(p.specularColorMap,m.specularColorMapTransform)),p.specularIntensityMap&&(m.specularIntensityMap.value=p.specularIntensityMap,t(p.specularIntensityMap,m.specularIntensityMapTransform))}function g(m,p){p.matcap&&(m.matcap.value=p.matcap)}function x(m,p){let v=e.get(p).light;m.referencePosition.value.setFromMatrixPosition(v.matrixWorld),m.nearDistance.value=v.shadow.camera.near,m.farDistance.value=v.shadow.camera.far}return{refreshFogUniforms:n,refreshMaterialUniforms:s}}function ny(i,e,t,n){let s={},r={},o=[],a=i.getParameter(i.MAX_UNIFORM_BUFFER_BINDINGS);function l(v,b){let M=b.program;n.uniformBlockBinding(v,M)}function c(v,b){let M=s[v.id];M===void 0&&(g(v),M=u(v),s[v.id]=M,v.addEventListener("dispose",m));let E=b.program;n.updateUBOMapping(v,E);let w=e.render.frame;r[v.id]!==w&&(h(v),r[v.id]=w)}function u(v){let b=d();v.__bindingPointIndex=b;let M=i.createBuffer(),E=v.__size,w=v.usage;return i.bindBuffer(i.UNIFORM_BUFFER,M),i.bufferData(i.UNIFORM_BUFFER,E,w),i.bindBuffer(i.UNIFORM_BUFFER,null),i.bindBufferBase(i.UNIFORM_BUFFER,b,M),M}function d(){for(let v=0;v0&&(M+=E-w),v.__size=M,v.__cache={},this}function x(v){let b={boundary:0,storage:0};return typeof v=="number"||typeof v=="boolean"?(b.boundary=4,b.storage=4):v.isVector2?(b.boundary=8,b.storage=8):v.isVector3||v.isColor?(b.boundary=16,b.storage=12):v.isVector4?(b.boundary=16,b.storage=16):v.isMatrix3?(b.boundary=48,b.storage=48):v.isMatrix4?(b.boundary=64,b.storage=64):v.isTexture?Te("WebGLRenderer: Texture samplers can not be part of an uniforms group."):ArrayBuffer.isView(v)?(b.boundary=16,b.storage=v.byteLength):Te("WebGLRenderer: Unsupported uniform value type.",v),b}function m(v){let b=v.target;b.removeEventListener("dispose",m);let M=o.indexOf(b.__bindingPointIndex);o.splice(M,1),i.deleteBuffer(s[b.id]),delete s[b.id],delete r[b.id]}function p(){for(let v in s)i.deleteBuffer(s[v]);o=[],s={},r={}}return{bind:l,update:c,dispose:p}}var iy=new Uint16Array([12469,15057,12620,14925,13266,14620,13807,14376,14323,13990,14545,13625,14713,13328,14840,12882,14931,12528,14996,12233,15039,11829,15066,11525,15080,11295,15085,10976,15082,10705,15073,10495,13880,14564,13898,14542,13977,14430,14158,14124,14393,13732,14556,13410,14702,12996,14814,12596,14891,12291,14937,11834,14957,11489,14958,11194,14943,10803,14921,10506,14893,10278,14858,9960,14484,14039,14487,14025,14499,13941,14524,13740,14574,13468,14654,13106,14743,12678,14818,12344,14867,11893,14889,11509,14893,11180,14881,10751,14852,10428,14812,10128,14765,9754,14712,9466,14764,13480,14764,13475,14766,13440,14766,13347,14769,13070,14786,12713,14816,12387,14844,11957,14860,11549,14868,11215,14855,10751,14825,10403,14782,10044,14729,9651,14666,9352,14599,9029,14967,12835,14966,12831,14963,12804,14954,12723,14936,12564,14917,12347,14900,11958,14886,11569,14878,11247,14859,10765,14828,10401,14784,10011,14727,9600,14660,9289,14586,8893,14508,8533,15111,12234,15110,12234,15104,12216,15092,12156,15067,12010,15028,11776,14981,11500,14942,11205,14902,10752,14861,10393,14812,9991,14752,9570,14682,9252,14603,8808,14519,8445,14431,8145,15209,11449,15208,11451,15202,11451,15190,11438,15163,11384,15117,11274,15055,10979,14994,10648,14932,10343,14871,9936,14803,9532,14729,9218,14645,8742,14556,8381,14461,8020,14365,7603,15273,10603,15272,10607,15267,10619,15256,10631,15231,10614,15182,10535,15118,10389,15042,10167,14963,9787,14883,9447,14800,9115,14710,8665,14615,8318,14514,7911,14411,7507,14279,7198,15314,9675,15313,9683,15309,9712,15298,9759,15277,9797,15229,9773,15166,9668,15084,9487,14995,9274,14898,8910,14800,8539,14697,8234,14590,7790,14479,7409,14367,7067,14178,6621,15337,8619,15337,8631,15333,8677,15325,8769,15305,8871,15264,8940,15202,8909,15119,8775,15022,8565,14916,8328,14804,8009,14688,7614,14569,7287,14448,6888,14321,6483,14088,6171,15350,7402,15350,7419,15347,7480,15340,7613,15322,7804,15287,7973,15229,8057,15148,8012,15046,7846,14933,7611,14810,7357,14682,7069,14552,6656,14421,6316,14251,5948,14007,5528,15356,5942,15356,5977,15353,6119,15348,6294,15332,6551,15302,6824,15249,7044,15171,7122,15070,7050,14949,6861,14818,6611,14679,6349,14538,6067,14398,5651,14189,5311,13935,4958,15359,4123,15359,4153,15356,4296,15353,4646,15338,5160,15311,5508,15263,5829,15188,6042,15088,6094,14966,6001,14826,5796,14678,5543,14527,5287,14377,4985,14133,4586,13869,4257,15360,1563,15360,1642,15358,2076,15354,2636,15341,3350,15317,4019,15273,4429,15203,4732,15105,4911,14981,4932,14836,4818,14679,4621,14517,4386,14359,4156,14083,3795,13808,3437,15360,122,15360,137,15358,285,15355,636,15344,1274,15322,2177,15281,2765,15215,3223,15120,3451,14995,3569,14846,3567,14681,3466,14511,3305,14344,3121,14037,2800,13753,2467,15360,0,15360,1,15359,21,15355,89,15346,253,15325,479,15287,796,15225,1148,15133,1492,15008,1749,14856,1882,14685,1886,14506,1783,14324,1608,13996,1398,13702,1183]),Vn=null;function sy(){return Vn===null&&(Vn=new qo(iy,16,16,Di,qt),Vn.name="DFG_LUT",Vn.minFilter=Bt,Vn.magFilter=Bt,Vn.wrapS=On,Vn.wrapT=On,Vn.generateMipmaps=!1,Vn.needsUpdate=!0),Vn}var Ds=class{constructor(e={}){let{canvas:t=vd(),context:n=null,depth:s=!0,stencil:r=!1,alpha:o=!1,antialias:a=!1,premultipliedAlpha:l=!0,preserveDrawingBuffer:c=!1,powerPreference:u="default",failIfMajorPerformanceCaveat:d=!1,reversedDepthBuffer:h=!1,outputBufferType:f=pn}=e;this.isWebGLRenderer=!0;let g;if(n!==null){if(typeof WebGLRenderingContext<"u"&&n instanceof WebGLRenderingContext)throw new Error("THREE.WebGLRenderer: WebGL 1 is not supported since r163.");g=n.getContextAttributes().alpha}else g=o;let x=f,m=new Set([ba,ya,_a]),p=new Set([pn,Rn,Rs,Ps,xa,va]),v=new Uint32Array(4),b=new Int32Array(4),M=new N,E=null,w=null,C=[],_=[],T=null;this.domElement=t,this.debug={checkShaderErrors:!0,onShaderError:null},this.autoClear=!0,this.autoClearColor=!0,this.autoClearDepth=!0,this.autoClearStencil=!0,this.sortObjects=!0,this.clippingPlanes=[],this.localClippingEnabled=!1,this.toneMapping=fn,this.toneMappingExposure=1,this.transmissionResolutionScale=1;let P=this,R=!1,I=null;this._outputColorSpace=cn;let H=0,W=0,k=null,z=-1,V=null,K=new xt,Q=new xt,ae=null,_e=new me(0),be=0,We=t.width,Qe=t.height,ke=1,$=null,de=null,ie=new xt(0,0,We,Qe),Ae=new xt(0,0,We,Qe),Ne=!1,Pe=new hr,dt=!1,Xe=!1,et=new gt,lt=new N,He=new xt,Tt={background:null,fog:null,environment:null,overrideMaterial:null,isScene:!0},ft=!1;function sn(){return k===null?ke:1}let D=n;function At(S,F){return t.getContext(S,F)}try{let S={alpha:!0,depth:s,stencil:r,antialias:a,premultipliedAlpha:l,preserveDrawingBuffer:c,powerPreference:u,failIfMajorPerformanceCaveat:d};if("setAttribute"in t&&t.setAttribute("data-engine",`three.js r${da}`),t.addEventListener("webglcontextlost",J,!1),t.addEventListener("webglcontextrestored",Me,!1),t.addEventListener("webglcontextcreationerror",De,!1),D===null){let F="webgl2";if(D=At(F,S),D===null)throw At(F)?new Error("Error creating WebGL context with your selected attributes."):new Error("Error creating WebGL context.")}}catch(S){throw Re("WebGLRenderer: "+S.message),S}let qe,ot,le,pt,A,y,O,Y,j,ee,oe,X,Z,fe,xe,se,te,Ie,Oe,Ke,L,ne,q;function pe(){qe=new uv(D),qe.init(),L=new J_(D,qe),ot=new iv(D,qe,e,L),le=new $_(D,qe),ot.reversedDepthBuffer&&h&&le.buffers.depth.setReversed(!0),pt=new pv(D),A=new F_,y=new K_(D,qe,le,A,ot,L,pt),O=new hv(P),Y=new vg(D),ne=new tv(D,Y),j=new dv(D,Y,pt,ne),ee=new gv(D,j,Y,ne,pt),Ie=new mv(D,ot,y),xe=new sv(A),oe=new D_(P,O,qe,ot,ne,xe),X=new ty(P,A),Z=new O_,fe=new H_(qe),te=new ev(P,O,le,ee,g,l),se=new Z_(P,ee,ot),q=new ny(D,pt,ot,le),Oe=new nv(D,qe,pt),Ke=new fv(D,qe,pt),pt.programs=oe.programs,P.capabilities=ot,P.extensions=qe,P.properties=A,P.renderLists=Z,P.shadowMap=se,P.state=le,P.info=pt}pe(),x!==pn&&(T=new vv(x,t.width,t.height,s,r));let re=new Sh(P,D);this.xr=re,this.getContext=function(){return D},this.getContextAttributes=function(){return D.getContextAttributes()},this.forceContextLoss=function(){let S=qe.get("WEBGL_lose_context");S&&S.loseContext()},this.forceContextRestore=function(){let S=qe.get("WEBGL_lose_context");S&&S.restoreContext()},this.getPixelRatio=function(){return ke},this.setPixelRatio=function(S){S!==void 0&&(ke=S,this.setSize(We,Qe,!1))},this.getSize=function(S){return S.set(We,Qe)},this.setSize=function(S,F,G=!0){if(re.isPresenting){Te("WebGLRenderer: Can't change size while VR device is presenting.");return}We=S,Qe=F,t.width=Math.floor(S*ke),t.height=Math.floor(F*ke),G===!0&&(t.style.width=S+"px",t.style.height=F+"px"),T!==null&&T.setSize(t.width,t.height),this.setViewport(0,0,S,F)},this.getDrawingBufferSize=function(S){return S.set(We*ke,Qe*ke).floor()},this.setDrawingBufferSize=function(S,F,G){We=S,Qe=F,ke=G,t.width=Math.floor(S*G),t.height=Math.floor(F*G),this.setViewport(0,0,S,F)},this.setEffects=function(S){if(x===pn){Re("THREE.WebGLRenderer: setEffects() requires outputBufferType set to HalfFloatType or FloatType.");return}if(S){for(let F=0;F{function ue(){if(U.forEach(function(ve){A.get(ve).currentProgram.isReady()&&U.delete(ve)}),U.size===0){B(S);return}setTimeout(ue,10)}qe.get("KHR_parallel_shader_compile")!==null?ue():setTimeout(ue,10)})};let jl=null;function om(S){jl&&jl(S)}function iu(){Bi.stop()}function su(){Bi.start()}let Bi=new qd;Bi.setAnimationLoop(om),typeof self<"u"&&Bi.setContext(self),this.setAnimationLoop=function(S){jl=S,re.setAnimationLoop(S),S===null?Bi.stop():Bi.start()},re.addEventListener("sessionstart",iu),re.addEventListener("sessionend",su),this.render=function(S,F){if(F!==void 0&&F.isCamera!==!0){Re("WebGLRenderer.render: camera is not an instance of THREE.Camera.");return}if(R===!0)return;I!==null&&I.renderStart(S,F);let G=re.enabled===!0&&re.isPresenting===!0,U=T!==null&&(k===null||G)&&T.begin(P,k);if(S.matrixWorldAutoUpdate===!0&&S.updateMatrixWorld(),F.parent===null&&F.matrixWorldAutoUpdate===!0&&F.updateMatrixWorld(),re.enabled===!0&&re.isPresenting===!0&&(T===null||T.isCompositing()===!1)&&(re.cameraAutoUpdate===!0&&re.updateCamera(F),F=re.getCamera()),S.isScene===!0&&S.onBeforeRender(P,S,F,k),w=fe.get(S,_.length),w.init(F),w.state.textureUnits=y.getTextureUnits(),_.push(w),et.multiplyMatrices(F.projectionMatrix,F.matrixWorldInverse),Pe.setFromProjectionMatrix(et,Tn,F.reversedDepth),Xe=this.localClippingEnabled,dt=xe.init(this.clippingPlanes,Xe),E=Z.get(S,C.length),E.init(),C.push(E),re.enabled===!0&&re.isPresenting===!0){let ve=P.xr.getDepthSensingMesh();ve!==null&&Ql(ve,F,-1/0,P.sortObjects)}Ql(S,F,0,P.sortObjects),E.finish(),P.sortObjects===!0&&E.sort($,de),ft=re.enabled===!1||re.isPresenting===!1||re.hasDepthSensing()===!1,ft&&te.addToRenderList(E,S),this.info.render.frame++,dt===!0&&xe.beginShadows();let B=w.state.shadowsArray;if(se.render(B,S,F),dt===!0&&xe.endShadows(),this.info.autoReset===!0&&this.info.reset(),(U&&T.hasRenderPass())===!1){let ve=E.opaque,he=E.transmissive;if(w.setupLights(),F.isArrayCamera){let ye=F.cameras;if(he.length>0)for(let Se=0,Fe=ye.length;Se0&&ou(ve,he,S,F),ft&&te.render(S),ru(E,S,F)}k!==null&&W===0&&(y.updateMultisampleRenderTarget(k),y.updateRenderTargetMipmap(k)),U&&T.end(P),S.isScene===!0&&S.onAfterRender(P,S,F),ne.resetDefaultState(),z=-1,V=null,_.pop(),_.length>0?(w=_[_.length-1],y.setTextureUnits(w.state.textureUnits),dt===!0&&xe.setGlobalState(P.clippingPlanes,w.state.camera)):w=null,C.pop(),C.length>0?E=C[C.length-1]:E=null,I!==null&&I.renderEnd()};function Ql(S,F,G,U){if(S.visible===!1)return;if(S.layers.test(F.layers)){if(S.isGroup)G=S.renderOrder;else if(S.isLOD)S.autoUpdate===!0&&S.update(F);else if(S.isLightProbeGrid)w.pushLightProbeGrid(S);else if(S.isLight)w.pushLight(S),S.castShadow&&w.pushShadow(S);else if(S.isSprite){if(!S.frustumCulled||Pe.intersectsSprite(S)){U&&He.setFromMatrixPosition(S.matrixWorld).applyMatrix4(et);let ve=ee.update(S),he=S.material;he.visible&&E.push(S,ve,he,G,He.z,null)}}else if((S.isMesh||S.isLine||S.isPoints)&&(!S.frustumCulled||Pe.intersectsObject(S))){let ve=ee.update(S),he=S.material;if(U&&(S.boundingSphere!==void 0?(S.boundingSphere===null&&S.computeBoundingSphere(),He.copy(S.boundingSphere.center)):(ve.boundingSphere===null&&ve.computeBoundingSphere(),He.copy(ve.boundingSphere.center)),He.applyMatrix4(S.matrixWorld).applyMatrix4(et)),Array.isArray(he)){let ye=ve.groups;for(let Se=0,Fe=ye.length;Se0&&$r(B,F,G),ue.length>0&&$r(ue,F,G),ve.length>0&&$r(ve,F,G),le.buffers.depth.setTest(!0),le.buffers.depth.setMask(!0),le.buffers.color.setMask(!0),le.setPolygonOffset(!1)}function ou(S,F,G,U){if((G.isScene===!0?G.overrideMaterial:null)!==null)return;if(w.state.transmissionRenderTarget[U.id]===void 0){let we=qe.has("EXT_color_buffer_half_float")||qe.has("EXT_color_buffer_float");w.state.transmissionRenderTarget[U.id]=new Rt(1,1,{generateMipmaps:!0,type:we?qt:pn,minFilter:Li,samples:Math.max(4,ot.samples),stencilBuffer:r,resolveDepthBuffer:!1,resolveStencilBuffer:!1,colorSpace:ze.workingColorSpace})}let ue=w.state.transmissionRenderTarget[U.id],ve=U.viewport||K;ue.setSize(ve.z*P.transmissionResolutionScale,ve.w*P.transmissionResolutionScale);let he=P.getRenderTarget(),ye=P.getActiveCubeFace(),Se=P.getActiveMipmapLevel();P.setRenderTarget(ue),P.getClearColor(_e),be=P.getClearAlpha(),be<1&&P.setClearColor(16777215,.5),P.clear(),ft&&te.render(G);let Fe=P.toneMapping;P.toneMapping=fn;let Be=U.viewport;if(U.viewport!==void 0&&(U.viewport=void 0),w.setupLightsView(U),dt===!0&&xe.setGlobalState(P.clippingPlanes,U),$r(S,G,U),y.updateMultisampleRenderTarget(ue),y.updateRenderTargetMipmap(ue),qe.has("WEBGL_multisampled_render_to_texture")===!1){let we=!1;for(let nt=0,bt=F.length;nt0,U.currentProgram=Be,U.uniformsList=null,Be}function lu(S){if(S.uniformsList===null){let F=S.currentProgram.getUniforms();S.uniformsList=Ns.seqWithValue(F.seq,S.uniforms)}return S.uniformsList}function cu(S,F){let G=A.get(S);G.outputColorSpace=F.outputColorSpace,G.batching=F.batching,G.batchingColor=F.batchingColor,G.instancing=F.instancing,G.instancingColor=F.instancingColor,G.instancingMorph=F.instancingMorph,G.skinning=F.skinning,G.morphTargets=F.morphTargets,G.morphNormals=F.morphNormals,G.morphColors=F.morphColors,G.morphTargetsCount=F.morphTargetsCount,G.numClippingPlanes=F.numClippingPlanes,G.numIntersection=F.numClipIntersection,G.vertexAlphas=F.vertexAlphas,G.vertexTangents=F.vertexTangents,G.toneMapping=F.toneMapping}function am(S,F){if(S.length===0)return null;if(S.length===1)return S[0].texture!==null?S[0]:null;M.setFromMatrixPosition(F.matrixWorld);for(let G=0,U=S.length;G0),we=!!G.morphAttributes.position,nt=!!G.morphAttributes.normal,bt=!!G.morphAttributes.color,mt=fn;U.toneMapped&&(k===null||k.isXRRenderTarget===!0)&&(mt=P.toneMapping);let st=G.morphAttributes.position||G.morphAttributes.normal||G.morphAttributes.color,Vt=st!==void 0?st.length:0,ge=A.get(U),rn=w.state.lights;if(dt===!0&&(Xe===!0||S!==V)){let at=S===V&&U.id===z;xe.setState(U,S,at)}let $e=!1;U.version===ge.__version?(ge.needsLights&&ge.lightsStateVersion!==rn.state.version||ge.outputColorSpace!==he||B.isBatchedMesh&&ge.batching===!1||!B.isBatchedMesh&&ge.batching===!0||B.isBatchedMesh&&ge.batchingColor===!0&&B.colorTexture===null||B.isBatchedMesh&&ge.batchingColor===!1&&B.colorTexture!==null||B.isInstancedMesh&&ge.instancing===!1||!B.isInstancedMesh&&ge.instancing===!0||B.isSkinnedMesh&&ge.skinning===!1||!B.isSkinnedMesh&&ge.skinning===!0||B.isInstancedMesh&&ge.instancingColor===!0&&B.instanceColor===null||B.isInstancedMesh&&ge.instancingColor===!1&&B.instanceColor!==null||B.isInstancedMesh&&ge.instancingMorph===!0&&B.morphTexture===null||B.isInstancedMesh&&ge.instancingMorph===!1&&B.morphTexture!==null||ge.envMap!==Se||U.fog===!0&&ge.fog!==ue||ge.numClippingPlanes!==void 0&&(ge.numClippingPlanes!==xe.numPlanes||ge.numIntersection!==xe.numIntersection)||ge.vertexAlphas!==Fe||ge.vertexTangents!==Be||ge.morphTargets!==we||ge.morphNormals!==nt||ge.morphColors!==bt||ge.toneMapping!==mt||ge.morphTargetsCount!==Vt||!!ge.lightProbeGrid!=w.state.lightProbeGridArray.length>0)&&($e=!0):($e=!0,ge.__version=U.version);let gn=ge.currentProgram;$e===!0&&(gn=Kr(U,F,B),I&&U.isNodeMaterial&&I.onUpdateProgram(U,gn,ge));let Nn=!1,li=!1,ns=!1,rt=gn.getUniforms(),Mt=ge.uniforms;if(le.useProgram(gn.program)&&(Nn=!0,li=!0,ns=!0),U.id!==z&&(z=U.id,li=!0),ge.needsLights){let at=am(w.state.lightProbeGridArray,B);ge.lightProbeGrid!==at&&(ge.lightProbeGrid=at,li=!0)}if(Nn||V!==S){le.buffers.depth.getReversed()&&S.reversedDepth!==!0&&(S._reversedDepth=!0,S.updateProjectionMatrix()),rt.setValue(D,"projectionMatrix",S.projectionMatrix),rt.setValue(D,"viewMatrix",S.matrixWorldInverse);let hi=rt.map.cameraPosition;hi!==void 0&&hi.setValue(D,lt.setFromMatrixPosition(S.matrixWorld)),ot.logarithmicDepthBuffer&&rt.setValue(D,"logDepthBufFC",2/(Math.log(S.far+1)/Math.LN2)),(U.isMeshPhongMaterial||U.isMeshToonMaterial||U.isMeshLambertMaterial||U.isMeshBasicMaterial||U.isMeshStandardMaterial||U.isShaderMaterial)&&rt.setValue(D,"isOrthographic",S.isOrthographicCamera===!0),V!==S&&(V=S,li=!0,ns=!0)}if(ge.needsLights&&(rn.state.directionalShadowMap.length>0&&rt.setValue(D,"directionalShadowMap",rn.state.directionalShadowMap,y),rn.state.spotShadowMap.length>0&&rt.setValue(D,"spotShadowMap",rn.state.spotShadowMap,y),rn.state.pointShadowMap.length>0&&rt.setValue(D,"pointShadowMap",rn.state.pointShadowMap,y)),B.isSkinnedMesh){rt.setOptional(D,B,"bindMatrix"),rt.setOptional(D,B,"bindMatrixInverse");let at=B.skeleton;at&&(at.boneTexture===null&&at.computeBoneTexture(),rt.setValue(D,"boneTexture",at.boneTexture,y))}B.isBatchedMesh&&(rt.setOptional(D,B,"batchingTexture"),rt.setValue(D,"batchingTexture",B._matricesTexture,y),rt.setOptional(D,B,"batchingIdTexture"),rt.setValue(D,"batchingIdTexture",B._indirectTexture,y),rt.setOptional(D,B,"batchingColorTexture"),B._colorsTexture!==null&&rt.setValue(D,"batchingColorTexture",B._colorsTexture,y));let ci=G.morphAttributes;if((ci.position!==void 0||ci.normal!==void 0||ci.color!==void 0)&&Ie.update(B,G,gn),(li||ge.receiveShadow!==B.receiveShadow)&&(ge.receiveShadow=B.receiveShadow,rt.setValue(D,"receiveShadow",B.receiveShadow)),(U.isMeshStandardMaterial||U.isMeshLambertMaterial||U.isMeshPhongMaterial)&&U.envMap===null&&F.environment!==null&&(Mt.envMapIntensity.value=F.environmentIntensity),Mt.dfgLUT!==void 0&&(Mt.dfgLUT.value=sy()),li){if(rt.setValue(D,"toneMappingExposure",P.toneMappingExposure),ge.needsLights&&cm(Mt,ns),ue&&U.fog===!0&&X.refreshFogUniforms(Mt,ue),X.refreshMaterialUniforms(Mt,U,ke,Qe,w.state.transmissionRenderTarget[S.id]),ge.needsLights&&ge.lightProbeGrid){let at=ge.lightProbeGrid;Mt.probesSH.value=at.texture,Mt.probesMin.value.copy(at.boundingBox.min),Mt.probesMax.value.copy(at.boundingBox.max),Mt.probesResolution.value.copy(at.resolution)}Ns.upload(D,lu(ge),Mt,y)}if(U.isShaderMaterial&&U.uniformsNeedUpdate===!0&&(Ns.upload(D,lu(ge),Mt,y),U.uniformsNeedUpdate=!1),U.isSpriteMaterial&&rt.setValue(D,"center",B.center),rt.setValue(D,"modelViewMatrix",B.modelViewMatrix),rt.setValue(D,"normalMatrix",B.normalMatrix),rt.setValue(D,"modelMatrix",B.matrixWorld),U.uniformsGroups!==void 0){let at=U.uniformsGroups;for(let hi=0,is=at.length;hi0&&y.useMultisampledRTT(S)===!1?U=A.get(S).__webglMultisampledFramebuffer:Array.isArray(Se)?U=Se[G]:U=Se,K.copy(S.viewport),Q.copy(S.scissor),ae=S.scissorTest}else K.copy(ie).multiplyScalar(ke).floor(),Q.copy(Ae).multiplyScalar(ke).floor(),ae=Ne;if(G!==0&&(U=um),le.bindFramebuffer(D.FRAMEBUFFER,U)&&le.drawBuffers(S,U),le.viewport(K),le.scissor(Q),le.setScissorTest(ae),B){let he=A.get(S.texture);D.framebufferTexture2D(D.FRAMEBUFFER,D.COLOR_ATTACHMENT0,D.TEXTURE_CUBE_MAP_POSITIVE_X+F,he.__webglTexture,G)}else if(ue){let he=F;for(let ye=0;ye1&&D.readBuffer(D.COLOR_ATTACHMENT0+he),!ot.textureFormatReadable(Fe)){Re("WebGLRenderer.readRenderTargetPixels: renderTarget is not in RGBA or implementation defined format.");return}if(!ot.textureTypeReadable(Be)){Re("WebGLRenderer.readRenderTargetPixels: renderTarget is not in UnsignedByteType or implementation defined type.");return}F>=0&&F<=S.width-U&&G>=0&&G<=S.height-B&&D.readPixels(F,G,U,B,L.convert(Fe),L.convert(Be),ue)}finally{let Se=k!==null?A.get(k).__webglFramebuffer:null;le.bindFramebuffer(D.FRAMEBUFFER,Se)}}},this.readRenderTargetPixelsAsync=async function(S,F,G,U,B,ue,ve,he=0){if(!(S&&S.isWebGLRenderTarget))throw new Error("THREE.WebGLRenderer.readRenderTargetPixels: renderTarget is not THREE.WebGLRenderTarget.");let ye=A.get(S).__webglFramebuffer;if(S.isWebGLCubeRenderTarget&&ve!==void 0&&(ye=ye[ve]),ye)if(F>=0&&F<=S.width-U&&G>=0&&G<=S.height-B){le.bindFramebuffer(D.FRAMEBUFFER,ye);let Se=S.textures[he],Fe=Se.format,Be=Se.type;if(S.textures.length>1&&D.readBuffer(D.COLOR_ATTACHMENT0+he),!ot.textureFormatReadable(Fe))throw new Error("THREE.WebGLRenderer.readRenderTargetPixelsAsync: renderTarget is not in RGBA or implementation defined format.");if(!ot.textureTypeReadable(Be))throw new Error("THREE.WebGLRenderer.readRenderTargetPixelsAsync: renderTarget is not in UnsignedByteType or implementation defined type.");let we=D.createBuffer();D.bindBuffer(D.PIXEL_PACK_BUFFER,we),D.bufferData(D.PIXEL_PACK_BUFFER,ue.byteLength,D.STREAM_READ),D.readPixels(F,G,U,B,L.convert(Fe),L.convert(Be),0);let nt=k!==null?A.get(k).__webglFramebuffer:null;le.bindFramebuffer(D.FRAMEBUFFER,nt);let bt=D.fenceSync(D.SYNC_GPU_COMMANDS_COMPLETE,0);return D.flush(),await yd(D,bt,4),D.bindBuffer(D.PIXEL_PACK_BUFFER,we),D.getBufferSubData(D.PIXEL_PACK_BUFFER,0,ue),D.deleteBuffer(we),D.deleteSync(bt),ue}else throw new Error("THREE.WebGLRenderer.readRenderTargetPixelsAsync: requested read bounds are out of range.")},this.copyFramebufferToTexture=function(S,F=null,G=0){let U=Math.pow(2,-G),B=Math.floor(S.image.width*U),ue=Math.floor(S.image.height*U),ve=F!==null?F.x:0,he=F!==null?F.y:0;y.setTexture2D(S,0),D.copyTexSubImage2D(D.TEXTURE_2D,G,0,0,ve,he,B,ue),le.unbindTexture()};let dm=D.createFramebuffer(),fm=D.createFramebuffer();this.copyTextureToTexture=function(S,F,G=null,U=null,B=0,ue=0){let ve,he,ye,Se,Fe,Be,we,nt,bt,mt=S.isCompressedTexture?S.mipmaps[ue]:S.image;if(G!==null)ve=G.max.x-G.min.x,he=G.max.y-G.min.y,ye=G.isBox3?G.max.z-G.min.z:1,Se=G.min.x,Fe=G.min.y,Be=G.isBox3?G.min.z:0;else{let Mt=Math.pow(2,-B);ve=Math.floor(mt.width*Mt),he=Math.floor(mt.height*Mt),S.isDataArrayTexture?ye=mt.depth:S.isData3DTexture?ye=Math.floor(mt.depth*Mt):ye=1,Se=0,Fe=0,Be=0}U!==null?(we=U.x,nt=U.y,bt=U.z):(we=0,nt=0,bt=0);let st=L.convert(F.format),Vt=L.convert(F.type),ge;F.isData3DTexture?(y.setTexture3D(F,0),ge=D.TEXTURE_3D):F.isDataArrayTexture||F.isCompressedArrayTexture?(y.setTexture2DArray(F,0),ge=D.TEXTURE_2D_ARRAY):(y.setTexture2D(F,0),ge=D.TEXTURE_2D),le.activeTexture(D.TEXTURE0),le.pixelStorei(D.UNPACK_FLIP_Y_WEBGL,F.flipY),le.pixelStorei(D.UNPACK_PREMULTIPLY_ALPHA_WEBGL,F.premultiplyAlpha),le.pixelStorei(D.UNPACK_ALIGNMENT,F.unpackAlignment);let rn=le.getParameter(D.UNPACK_ROW_LENGTH),$e=le.getParameter(D.UNPACK_IMAGE_HEIGHT),gn=le.getParameter(D.UNPACK_SKIP_PIXELS),Nn=le.getParameter(D.UNPACK_SKIP_ROWS),li=le.getParameter(D.UNPACK_SKIP_IMAGES);le.pixelStorei(D.UNPACK_ROW_LENGTH,mt.width),le.pixelStorei(D.UNPACK_IMAGE_HEIGHT,mt.height),le.pixelStorei(D.UNPACK_SKIP_PIXELS,Se),le.pixelStorei(D.UNPACK_SKIP_ROWS,Fe),le.pixelStorei(D.UNPACK_SKIP_IMAGES,Be);let ns=S.isDataArrayTexture||S.isData3DTexture,rt=F.isDataArrayTexture||F.isData3DTexture;if(S.isDepthTexture){let Mt=A.get(S),ci=A.get(F),at=A.get(Mt.__renderTarget),hi=A.get(ci.__renderTarget);le.bindFramebuffer(D.READ_FRAMEBUFFER,at.__webglFramebuffer),le.bindFramebuffer(D.DRAW_FRAMEBUFFER,hi.__webglFramebuffer);for(let is=0;is{o.push({source:d,target:h});let f=s[d],g=s[h];f&&(f.degree++,f.outDegree++),g&&(g.degree++,g.inDegree++)};for(let d of Object.keys(e)){let h=r.get(d);if(h===void 0)continue;let f=e[d]??{};for(let g of Object.keys(f)){let x=r.get(g);x!==void 0&&a(h,x)}}if(n.includeUnresolved)for(let d of Object.keys(t)){let h=r.get(d);if(h===void 0)continue;let f=t[d]??{};for(let g of Object.keys(f)){let x=`unresolved:${g}`,m=r.get(x);m===void 0&&(m=s.length,r.set(x,m),s.push({id:x,name:g,folderTop:"__unresolved__",degree:0,inDegree:0,outDegree:0,fileSize:0,unresolved:!0})),a(h,m)}}let l={nodes:s,links:o};n.includeOrphans||(l=tf(l,d=>d.degree>0));let c=n.nodeCap??null;if(c!==null&&l.nodes.length>c){let d=[...l.nodes.entries()].sort((f,g)=>g[1].degree-f[1].degree||f[0]-g[0]),h=new Set(d.slice(0,c).map(([f])=>f));l=tf(l,(f,g)=>h.has(g))}let u=n.linkCap??null;if(u!==null&&l.links.length>u){let d=h=>l.nodes[h]?.degree??0;l={nodes:l.nodes,links:[...l.links.entries()].sort((h,f)=>Math.min(d(f[1].source),d(f[1].target))-Math.min(d(h[1].source),d(h[1].target))||h[0]-f[0]).slice(0,u).map(([,h])=>h)}}return l}function tf(i,e){let t=new Map,n=[];i.nodes.forEach((r,o)=>{e(r,o)&&(t.set(o,n.length),n.push(r))});let s=[];for(let r of i.links){let o=t.get(r.source),a=t.get(r.target);o!==void 0&&a!==void 0&&s.push({source:o,target:a})}return{nodes:n,links:s}}function Eh(i){let e=2166136261;for(let t=0;t>>0}function wh(i){return Eh(i)/4294967295}function sf(i,e){let t=wh(i),n=wh(i+":v"),s=wh(i+":w"),r=e*Math.cbrt(t),o=2*Math.PI*n,a=Math.acos(2*s-1);return[r*Math.sin(a)*Math.cos(o),r*Math.sin(a)*Math.sin(o),r*Math.cos(a)]}function ol(i){return 80*Math.cbrt(Math.max(i,1)/1e3)}var al=class extends ll.Component{constructor(t){super();this.app=t;this.data={nodes:[],links:[]};this.positions=new Float32Array(0);this.includeUnresolved=!1;this.includeOrphans=!0;this.nodeCap=null;this.linkCap=null;this.onChanged=null}init(t,n,s){this.includeUnresolved=t,this.includeOrphans=n,this.onChanged=s;let r=(0,ll.debounce)(()=>this.rebuild(!0),800,!0);this.registerEvent(this.app.metadataCache.on("resolved",r)),this.registerEvent(this.app.vault.on("rename",r)),this.registerEvent(this.app.vault.on("delete",r))}async ensureCacheReady(){Object.keys(this.app.metadataCache.resolvedLinks).length>0||await new Promise(t=>{this.registerEvent(this.app.metadataCache.on("resolved",()=>t()))})}setIncludeUnresolved(t){t!==this.includeUnresolved&&(this.includeUnresolved=t,this.rebuild(!0))}getIncludeUnresolved(){return this.includeUnresolved}setCaps(t,n){t===this.nodeCap&&n===this.linkCap||(this.nodeCap=t,this.linkCap=n,this.rebuild(!0))}setIncludeOrphans(t){t!==this.includeOrphans&&(this.includeOrphans=t,this.rebuild(!0))}rebuild(t){let n=this.app.vault.getMarkdownFiles().map(c=>({path:c.path,basename:c.basename,size:c.stat.size})),s=nf(n,this.app.metadataCache.resolvedLinks,this.app.metadataCache.unresolvedLinks,{includeUnresolved:this.includeUnresolved,includeOrphans:this.includeOrphans,nodeCap:this.nodeCap,linkCap:this.linkCap}),r=new Map;t&&this.data.nodes.forEach((c,u)=>r.set(c.id,u));let o=this.positions,a=ol(s.nodes.length),l=new Float32Array(s.nodes.length*3);s.nodes.forEach((c,u)=>{let d=r.get(c.id);if(d!==void 0&&d*3+2=(l=(o+a)/2))?o=l:a=l,n=s,!(s=s[d=+u]))return n[d]=r,i;if(c=+i._x.call(null,s.data),e===c)return r.next=s,n?n[d]=r:i._root=r,i;do n=n?n[d]=new Array(2):i._root=new Array(2),(u=e>=(l=(o+a)/2))?o=l:a=l;while((d=+u)==(h=+(c>=l)));return n[h]=s,n[d]=r,i}function af(i){Array.isArray(i)||(i=Array.from(i));let e=i.length,t=new Float64Array(e),n=1/0,s=-1/0;for(let r=0,o;rs&&(s=o));if(n>s)return this;this.cover(n).cover(s);for(let r=0;ri||i>=t;)switch(o=+(io||(r=c.x1)=d))&&(c=a[a.length-1],a[a.length-1]=a[a.length-1-u],a[a.length-1-u]=c)}else{var h=Math.abs(i-+this._x.call(null,l.data));h=(c=(o+a)/2))?o=c:a=c,e=t,!(t=t[d=+u]))return this;if(!t.length)break;e[d+1&1]&&(n=e,h=d)}for(;t.data!==i;)if(s=t,!(t=t.next))return this;return(r=t.next)&&delete t.next,s?(r?s.next=r:delete s.next,this):e?(r?e[d]=r:delete e[d],(t=e[0]||e[1])&&t===(e[1]||e[0])&&!t.length&&(n?n[h]=t:this._root=t),this):(this._root=r,this)}function ff(i){for(var e=0,t=i.length;e=(d=(a+c)/2))?a=d:c=d,(m=t>=(h=(l+u)/2))?l=h:u=h,s=r,!(r=r[p=m<<1|x]))return s[p]=o,i;if(f=+i._x.call(null,r.data),g=+i._y.call(null,r.data),e===f&&t===g)return o.next=r,s?s[p]=o:i._root=o,i;do s=s?s[p]=new Array(4):i._root=new Array(4),(x=e>=(d=(a+c)/2))?a=d:c=d,(m=t>=(h=(l+u)/2))?l=h:u=h;while((p=m<<1|x)===(v=(g>=h)<<1|f>=d));return s[v]=r,s[p]=o,i}function Sf(i){var e,t,n=i.length,s,r,o=new Array(n),a=new Array(n),l=1/0,c=1/0,u=-1/0,d=-1/0;for(t=0;tu&&(u=s),rd&&(d=r));if(l>u||c>d)return this;for(this.cover(l,c).cover(u,d),t=0;ti||i>=s||n>e||e>=r;)switch(c=(eu||(a=g.y0)>d||(l=g.x1)=p)<<1|i>=m)&&(g=h[h.length-1],h[h.length-1]=h[h.length-1-x],h[h.length-1-x]=g)}else{var v=i-+this._x.call(null,f.data),b=e-+this._y.call(null,f.data),M=v*v+b*b;if(M=(h=(o+l)/2))?o=h:l=h,(x=d>=(f=(a+c)/2))?a=f:c=f,e=t,!(t=t[m=x<<1|g]))return this;if(!t.length)break;(e[m+1&3]||e[m+2&3]||e[m+3&3])&&(n=e,p=m)}for(;t.data!==i;)if(s=t,!(t=t.next))return this;return(r=t.next)&&delete t.next,s?(r?s.next=r:delete s.next,this):e?(r?e[m]=r:delete e[m],(t=e[0]||e[1]||e[2]||e[3])&&t===(e[3]||e[2]||e[1]||e[0])&&!t.length&&(n?n[p]=t:this._root=t),this):(this._root=r,this)}function Rf(i){for(var e=0,t=i.length;e=(g=(l+d)/2))?l=g:d=g,(E=t>=(x=(c+h)/2))?c=x:h=x,(w=n>=(m=(u+f)/2))?u=m:f=m,r=o,!(o=o[C=w<<2|E<<1|M]))return r[C]=a,i;if(p=+i._x.call(null,o.data),v=+i._y.call(null,o.data),b=+i._z.call(null,o.data),e===p&&t===v&&n===b)return a.next=o,r?r[C]=a:i._root=a,i;do r=r?r[C]=new Array(8):i._root=new Array(8),(M=e>=(g=(l+d)/2))?l=g:d=g,(E=t>=(x=(c+h)/2))?c=x:h=x,(w=n>=(m=(u+f)/2))?u=m:f=m;while((C=w<<2|E<<1|M)===(_=(b>=m)<<2|(v>=x)<<1|p>=g));return r[_]=o,r[C]=a,i}function Vf(i){Array.isArray(i)||(i=Array.from(i));let e=i.length,t=new Float64Array(e),n=new Float64Array(e),s=new Float64Array(e),r=1/0,o=1/0,a=1/0,l=-1/0,c=-1/0,u=-1/0;for(let d=0,h,f,g,x;dl&&(l=f),gc&&(c=g),xu&&(u=x));if(r>l||o>c||a>u)return this;this.cover(r,o,a).cover(l,c,u);for(let d=0;di||i>=o||s>e||e>=a||r>t||t>=l;)switch(h=(tg||(c=b.y0)>x||(u=b.z0)>m||(d=b.x1)=C)<<2|(e>=w)<<1|i>=E)&&(b=p[p.length-1],p[p.length-1]=p[p.length-1-M],p[p.length-1-M]=b)}else{var _=i-+this._x.call(null,v.data),T=e-+this._y.call(null,v.data),P=t-+this._z.call(null,v.data),R=_*_+T*T+P*P;if(RMath.sqrt((i-n)**2+(e-s)**2+(t-r)**2);function qf(i,e,t,n){let s=[],r=i-n,o=e-n,a=t-n,l=i+n,c=e+n,u=t+n;return this.visit((d,h,f,g,x,m,p)=>{if(!d.length)do{let v=d.data;oy(i,e,t,this._x(v),this._y(v),this._z(v))<=n&&s.push(v)}while(d=d.next);return h>l||f>c||g>u||x=(x=(o+c)/2))?o=x:c=x,(b=f>=(m=(a+u)/2))?a=m:u=m,(M=g>=(p=(l+d)/2))?l=p:d=p,e=t,!(t=t[E=M<<2|b<<1|v]))return this;if(!t.length)break;(e[E+1&7]||e[E+2&7]||e[E+3&7]||e[E+4&7]||e[E+5&7]||e[E+6&7]||e[E+7&7])&&(n=e,w=E)}for(;t.data!==i;)if(s=t,!(t=t.next))return this;return(r=t.next)&&delete t.next,s?(r?s.next=r:delete s.next,this):e?(r?e[E]=r:delete e[E],(t=e[0]||e[1]||e[2]||e[3]||e[4]||e[5]||e[6]||e[7])&&t===(e[7]||e[6]||e[5]||e[4]||e[3]||e[2]||e[1]||e[0])&&!t.length&&(n?n[w]=t:this._root=t),this):(this._root=r,this)}function Zf(i){for(var e=0,t=i.length;e1&&(T=C.y+C.vy-w.y-w.vy||In(u)),a>2&&(P=C.z+C.vz-w.z-w.vz||In(u)),R=Math.sqrt(_*_+T*T+P*P),R=(R-r[M])/R*p*n[M],_*=R,T*=R,P*=R,C.vx-=_*(I=c[M]),a>1&&(C.vy-=T*I),a>2&&(C.vz-=P*I),w.vx+=_*(I=1-I),a>1&&(w.vy+=T*I),a>2&&(w.vz+=P*I)}function g(){if(o){var p,v=o.length,b=i.length,M=new Map(o.map((w,C)=>[e(w,C,o),w])),E;for(p=0,l=new Array(v);ptypeof b=="function")||Math.random,a=v.find(b=>[1,2,3].includes(b))||2,g()},f.links=function(p){return arguments.length?(i=p,g(),f):i},f.id=function(p){return arguments.length?(e=p,f):e},f.iterations=function(p){return arguments.length?(d=+p,f):d},f.strength=function(p){return arguments.length?(t=typeof p=="function"?p:vt(+p),x(),f):t},f.distance=function(p){return arguments.length?(s=typeof p=="function"?p:vt(+p),m(),f):s},f}var ly={value:()=>{}};function lp(){for(var i=0,e=arguments.length,t={},n;i=0&&(n=t.slice(s+1),t=t.slice(0,s)),t&&!e.hasOwnProperty(t))throw new Error("unknown type: "+t);return{type:t,name:n}})}cl.prototype=lp.prototype={constructor:cl,on:function(i,e){var t=this._,n=cy(i+"",t),s,r=-1,o=n.length;if(arguments.length<2){for(;++r0)for(var t=new Array(s),n=0,s,r;n=0&&i._call.call(void 0,e),i=i._next;--ks}function cp(){ji=(ul=Hr.now())+dl,ks=Vr=0;try{dp()}finally{ks=0,fy(),ji=0}}function dy(){var i=Hr.now(),e=i-ul;e>hp&&(dl-=e,ul=i)}function fy(){for(var i,e=hl,t,n=1/0;e;)e._call?(n>e._time&&(n=e._time),i=e,e=e._next):(t=e._next,e._next=null,e=i?i._next=t:hl=t);Gr=i,Lh(n)}function Lh(i){if(!ks){Vr&&(Vr=clearTimeout(Vr));var e=i-ji;e>24?(i<1/0&&(Vr=setTimeout(cp,i-Hr.now()-dl)),zr&&(zr=clearInterval(zr))):(zr||(ul=Hr.now(),zr=setInterval(dy,hp)),ks=1,up(cp))}}function fp(){let i=1;return()=>(i=(1664525*i+1013904223)%4294967296)/4294967296}var pp=3;function pl(i){return i.x}function Dh(i){return i.y}function mp(i){return i.z}var py=10,my=Math.PI*(3-Math.sqrt(5)),gy=Math.PI*20/(9+Math.sqrt(221));function Fh(i,e){e=e||2;var t=Math.min(pp,Math.max(1,Math.round(e))),n,s=1,r=.001,o=1-Math.pow(r,1/300),a=0,l=.6,c=new Map,u=fl(f),d=Ph("tick","end"),h=fp();i==null&&(i=[]);function f(){g(),d.call("tick",n),s1&&(M.fy==null?M.y+=M.vy*=l:(M.y=M.fy,M.vy=0)),t>2&&(M.fz==null?M.z+=M.vz*=l:(M.z=M.fz,M.vz=0));return n}function x(){for(var p=0,v=i.length,b;p1&&isNaN(b.y)||t>2&&isNaN(b.z)){var M=py*(t>2?Math.cbrt(.5+p):t>1?Math.sqrt(.5+p):p),E=p*my,w=p*gy;t===1?b.x=M:t===2?(b.x=M*Math.cos(E),b.y=M*Math.sin(E)):(b.x=M*Math.sin(E)*Math.cos(w),b.y=M*Math.cos(E),b.z=M*Math.sin(E)*Math.sin(w))}(isNaN(b.vx)||t>1&&isNaN(b.vy)||t>2&&isNaN(b.vz))&&(b.vx=0,t>1&&(b.vy=0),t>2&&(b.vz=0))}}function m(p){return p.initialize&&p.initialize(i,h,t),p}return x(),n={tick:g,restart:function(){return u.restart(f),n},stop:function(){return u.stop(),n},numDimensions:function(p){return arguments.length?(t=Math.min(pp,Math.max(1,Math.round(p))),c.forEach(m),n):t},nodes:function(p){return arguments.length?(i=p,x(),c.forEach(m),n):i},alpha:function(p){return arguments.length?(s=+p,n):s},alphaMin:function(p){return arguments.length?(r=+p,n):r},alphaDecay:function(p){return arguments.length?(o=+p,n):+o},alphaTarget:function(p){return arguments.length?(a=+p,n):a},velocityDecay:function(p){return arguments.length?(l=1-p,n):1-l},randomSource:function(p){return arguments.length?(h=p,c.forEach(m),n):h},force:function(p,v){return arguments.length>1?(v==null?c.delete(p):c.set(p,m(v)),n):c.get(p)},find:function(){var p=Array.prototype.slice.call(arguments),v=p.shift()||0,b=(t>1?p.shift():null)||0,M=(t>2?p.shift():null)||0,E=p.shift()||1/0,w=0,C=i.length,_,T,P,R,I,H;for(E*=E,w=0;w1?(d.on(p,v),n):d.on(p)}}}function kh(){var i,e,t,n,s,r=vt(-30),o,a=1,l=1/0,c=.81;function u(g){var x,m=i.length,p=(e===1?Or(i,pl):e===2?Ur(i,pl,Dh):e===3?Br(i,pl,Dh,mp):null).visitAfter(h);for(s=g,x=0;x1&&(g.y=M/v),e>2&&(g.z=E/v)}else{m=g,m.x=m.data.x,e>1&&(m.y=m.data.y),e>2&&(m.z=m.data.z);do x+=o[m.data.index];while(m=m.next)}g.value=x}function f(g,x,m,p,v){if(!g.value)return!0;var b=[m,p,v][e-1],M=g.x-t.x,E=e>1?g.y-t.y:0,w=e>2?g.z-t.z:0,C=b-x,_=M*M+E*E+w*w;if(C*C/c<_)return _1&&E===0&&(E=In(n),_+=E*E),e>2&&w===0&&(w=In(n),_+=w*w),_1&&(t.vy+=E*g.value*s/_),e>2&&(t.vz+=w*g.value*s/_)),!0;if(g.length||_>=l)return;(g.data!==t||g.next)&&(M===0&&(M=In(n),_+=M*M),e>1&&E===0&&(E=In(n),_+=E*E),e>2&&w===0&&(w=In(n),_+=w*w),_1&&(t.vy+=E*C),e>2&&(t.vz+=w*C));while(g=g.next)}return u.initialize=function(g,...x){i=g,n=x.find(m=>typeof m=="function")||Math.random,e=x.find(m=>[1,2,3].includes(m))||2,d()},u.strength=function(g){return arguments.length?(r=typeof g=="function"?g:vt(+g),d(),u):r},u.distanceMin=function(g){return arguments.length?(a=g*g,u):Math.sqrt(a)},u.distanceMax=function(g){return arguments.length?(l=g*g,u):Math.sqrt(l)},u.theta=function(g){return arguments.length?(c=g*g,u):Math.sqrt(c)},u}function Oh(i){var e=vt(.1),t,n,s;typeof i!="function"&&(i=vt(i==null?0:+i));function r(a){for(var l=0,c=t.length,u;lMath.max(o.degree,1)),this.simNodes=e.nodes.map((o,a)=>({x:t[a*3]??0,y:t[a*3+1]??0,z:t[a*3+2]??0}));let r=e.links.map(o=>({source:o.source,target:o.target}));this.sim=Fh(this.simNodes,3).alphaDecay(1-Math.pow(.001,1/300)).velocityDecay(n.velocityDecay).force("link",Rh(r).distance(n.linkDistance).strength(this.linkStrengthFn(n.linkStrength))).force("charge",kh().strength(n.charge).distanceMax(800)).force("x",Oh(0).strength(n.centerPull)).force("y",Uh(0).strength(n.centerPull+n.flatten)).force("z",Bh(0).strength(n.centerPull)).stop(),this.sim.alpha(s),this._ticks=0,this.settled=!1}linkStrengthFn(e){let t=this.degrees;return n=>{let s=typeof n.source=="number"?n.source:n.source.index??0,r=typeof n.target=="number"?n.target:n.target.index??0,o=1/Math.min(t[s]??1,t[r]??1);return Math.min(o*e,1)}}updateParams(e){let t=this.sim;if(!t)return;t.force("charge")?.strength(e.charge);let n=t.force("link");n?.distance(e.linkDistance),n?.strength(this.linkStrengthFn(e.linkStrength)),t.force("x")?.strength(e.centerPull),t.force("y")?.strength(e.centerPull+e.flatten),t.force("z")?.strength(e.centerPull),this.reheat(.5)}step(){let e=this.sim;if(!e||this.settled)return!1;e.tick(),this._ticks++;let t=this.positions,n=this.simNodes;for(let s=0;s{function Nt(t){let e=+this._x.call(null,t);return At(this.cover(e),e,t)}function At(t,e,r){if(isNaN(e))return t;var n,i=t._root,o={data:r},f=t._x0,a=t._x1,s,l,h,c,x;if(!i)return t._root=o,t;for(;i.length;)if((h=e>=(s=(f+a)/2))?f=s:a=s,n=i,!(i=i[c=+h]))return n[c]=o,t;if(l=+t._x.call(null,i.data),e===l)return o.next=i,n?n[c]=o:t._root=o,t;do n=n?n[c]=new Array(2):t._root=new Array(2),(h=e>=(s=(f+a)/2))?f=s:a=s;while((c=+h)==(x=+(l>=s)));return n[x]=i,n[c]=o,t}function Mt(t){Array.isArray(t)||(t=Array.from(t));let e=t.length,r=new Float64Array(e),n=1/0,i=-1/0;for(let o=0,f;oi&&(i=f));if(n>i)return this;this.cover(n).cover(i);for(let o=0;ot||t>=r;)switch(f=+(tf||(o=l.x1)=c))&&(l=a[a.length-1],a[a.length-1]=a[a.length-1-h],a[a.length-1-h]=l)}else{var x=Math.abs(t-+this._x.call(null,s.data));x=(l=(f+a)/2))?f=l:a=l,e=r,!(r=r[c=+h]))return this;if(!r.length)break;e[c+1&1]&&(n=e,x=c)}for(;r.data!==t;)if(i=r,!(r=r.next))return this;return(o=r.next)&&delete r.next,i?(o?i.next=o:delete i.next,this):e?(o?e[c]=o:delete e[c],(r=e[0]||e[1])&&r===(e[1]||e[0])&&!r.length&&(n?n[x]=r:this._root=r),this):(this._root=o,this)}function Pt(t){for(var e=0,r=t.length;e=(c=(a+l)/2))?a=c:l=c,(y=r>=(x=(s+h)/2))?s=x:h=x,i=o,!(o=o[u=y<<1|w]))return i[u]=f,t;if(v=+t._x.call(null,o.data),p=+t._y.call(null,o.data),e===v&&r===p)return f.next=o,i?i[u]=f:t._root=f,t;do i=i?i[u]=new Array(4):t._root=new Array(4),(w=e>=(c=(a+l)/2))?a=c:l=c,(y=r>=(x=(s+h)/2))?s=x:h=x;while((u=y<<1|w)===(m=(p>=x)<<1|v>=c));return i[m]=o,i[u]=f,t}function Ot(t){var e,r,n=t.length,i,o,f=new Array(n),a=new Array(n),s=1/0,l=1/0,h=-1/0,c=-1/0;for(r=0;rh&&(h=i),oc&&(c=o));if(s>h||l>c)return this;for(this.cover(s,l).cover(h,c),r=0;rt||t>=i||n>e||e>=o;)switch(l=(eh||(a=p.y0)>c||(s=p.x1)=u)<<1|t>=y)&&(p=x[x.length-1],x[x.length-1]=x[x.length-1-w],x[x.length-1-w]=p)}else{var m=t-+this._x.call(null,v.data),d=e-+this._y.call(null,v.data),g=m*m+d*d;if(g=(x=(f+s)/2))?f=x:s=x,(w=c>=(v=(a+l)/2))?a=v:l=v,e=r,!(r=r[y=w<<1|p]))return this;if(!r.length)break;(e[y+1&3]||e[y+2&3]||e[y+3&3])&&(n=e,u=y)}for(;r.data!==t;)if(i=r,!(r=r.next))return this;return(o=r.next)&&delete r.next,i?(o?i.next=o:delete i.next,this):e?(o?e[y]=o:delete e[y],(r=e[0]||e[1]||e[2]||e[3])&&r===(e[3]||e[2]||e[1]||e[0])&&!r.length&&(n?n[u]=r:this._root=r),this):(this._root=o,this)}function Qt(t){for(var e=0,r=t.length;e=(p=(s+c)/2))?s=p:c=p,(_=r>=(w=(l+x)/2))?l=w:x=w,(N=n>=(y=(h+v)/2))?h=y:v=y,o=f,!(f=f[A=N<<2|_<<1|g]))return o[A]=a,t;if(u=+t._x.call(null,f.data),m=+t._y.call(null,f.data),d=+t._z.call(null,f.data),e===u&&r===m&&n===d)return a.next=f,o?o[A]=a:t._root=a,t;do o=o?o[A]=new Array(8):t._root=new Array(8),(g=e>=(p=(s+c)/2))?s=p:c=p,(_=r>=(w=(l+x)/2))?l=w:x=w,(N=n>=(y=(h+v)/2))?h=y:v=y;while((A=N<<2|_<<1|g)===(M=(d>=y)<<2|(m>=w)<<1|u>=p));return o[M]=f,o[A]=a,t}function ie(t){Array.isArray(t)||(t=Array.from(t));let e=t.length,r=new Float64Array(e),n=new Float64Array(e),i=new Float64Array(e),o=1/0,f=1/0,a=1/0,s=-1/0,l=-1/0,h=-1/0;for(let c=0,x,v,p,w;cs&&(s=v),pl&&(l=p),wh&&(h=w));if(o>s||f>l||a>h)return this;this.cover(o,f,a).cover(s,l,h);for(let c=0;ct||t>=f||i>e||e>=a||o>r||r>=s;)switch(x=(rp||(l=d.y0)>w||(h=d.z0)>y||(c=d.x1)=A)<<2|(e>=N)<<1|t>=_)&&(d=u[u.length-1],u[u.length-1]=u[u.length-1-g],u[u.length-1-g]=d)}else{var M=t-+this._x.call(null,m.data),S=e-+this._y.call(null,m.data),B=r-+this._z.call(null,m.data),W=M*M+S*S+B*B;if(WMath.sqrt((t-n)**2+(e-i)**2+(r-o)**2);function ue(t,e,r,n){let i=[],o=t-n,f=e-n,a=r-n,s=t+n,l=e+n,h=r+n;return this.visit((c,x,v,p,w,y,u)=>{if(!c.length)do{let m=c.data;Ee(t,e,r,this._x(m),this._y(m),this._z(m))<=n&&i.push(m)}while(c=c.next);return x>s||v>l||p>h||w=(w=(f+l)/2))?f=w:l=w,(d=v>=(y=(a+h)/2))?a=y:h=y,(g=p>=(u=(s+c)/2))?s=u:c=u,e=r,!(r=r[_=g<<2|d<<1|m]))return this;if(!r.length)break;(e[_+1&7]||e[_+2&7]||e[_+3&7]||e[_+4&7]||e[_+5&7]||e[_+6&7]||e[_+7&7])&&(n=e,N=_)}for(;r.data!==t;)if(i=r,!(r=r.next))return this;return(o=r.next)&&delete r.next,i?(o?i.next=o:delete i.next,this):e?(o?e[_]=o:delete e[_],(r=e[0]||e[1]||e[2]||e[3]||e[4]||e[5]||e[6]||e[7])&&r===(e[7]||e[6]||e[5]||e[4]||e[3]||e[2]||e[1]||e[0])&&!r.length&&(n?n[N]=r:this._root=r),this):(this._root=o,this)}function he(t){for(var e=0,r=t.length;e1&&(S=A.y+A.vy-N.y-N.vy||j(h)),a>2&&(B=A.z+A.vz-N.z-N.vz||j(h)),W=Math.sqrt(M*M+S*S+B*B),W=(W-o[g])/W*u*n[g],M*=W,S*=W,B*=W,A.vx-=M*(k=l[g]),a>1&&(A.vy-=S*k),a>2&&(A.vz-=B*k),N.vx+=M*(k=1-k),a>1&&(N.vy+=S*k),a>2&&(N.vz+=B*k)}function p(){if(f){var u,m=f.length,d=t.length,g=new Map(f.map((N,A)=>[e(N,A,f),N])),_;for(u=0,s=new Array(m);utypeof d=="function")||Math.random,a=m.find(d=>[1,2,3].includes(d))||2,p()},v.links=function(u){return arguments.length?(t=u,p(),v):t},v.id=function(u){return arguments.length?(e=u,v):e},v.iterations=function(u){return arguments.length?(c=+u,v):c},v.strength=function(u){return arguments.length?(r=typeof u=="function"?u:b(+u),w(),v):r},v.distance=function(u){return arguments.length?(i=typeof u=="function"?u:b(+u),y(),v):i},v}var Te={value:()=>{}};function ze(){for(var t=0,e=arguments.length,r={},n;t=0&&(n=r.slice(i+1),r=r.slice(0,i)),r&&!e.hasOwnProperty(r))throw new Error("unknown type: "+r);return{type:r,name:n}})}G.prototype=ze.prototype={constructor:G,on:function(t,e){var r=this._,n=Le(t+"",r),i,o=-1,f=n.length;if(arguments.length<2){for(;++o0)for(var r=new Array(i),n=0,i,o;n=0&&t._call.call(void 0,e),t=t._next;--O}function be(){L=(K=Z.now())+V,O=H=0;try{Ie()}finally{O=0,Ye(),L=0}}function Re(){var t=Z.now(),e=t-K;e>ke&&(V-=e,K=t)}function Ye(){for(var t,e=J,r,n=1/0;e;)e._call?(n>e._time&&(n=e._time),t=e,e=e._next):(r=e._next,e._next=null,e=t?t._next=r:J=r);Q=t,ut(n)}function ut(t){if(!O){H&&(H=clearTimeout(H));var e=t-L;e>24?(t<1/0&&(H=setTimeout(be,t-Z.now()-V)),C&&(C=clearInterval(C))):(C||(K=Z.now(),C=setInterval(Re,ke)),O=1,qe(be))}}function Pe(){let t=1;return()=>(t=(1664525*t+1013904223)%4294967296)/4294967296}var Fe=3;function tt(t){return t.x}function ht(t){return t.y}function We(t){return t.z}var Ce=10,He=Math.PI*(3-Math.sqrt(5)),Qe=Math.PI*20/(9+Math.sqrt(221));function ct(t,e){e=e||2;var r=Math.min(Fe,Math.max(1,Math.round(e))),n,i=1,o=.001,f=1-Math.pow(o,1/300),a=0,s=.6,l=new Map,h=$(v),c=ft("tick","end"),x=Pe();t==null&&(t=[]);function v(){p(),c.call("tick",n),i1&&(g.fy==null?g.y+=g.vy*=s:(g.y=g.fy,g.vy=0)),r>2&&(g.fz==null?g.z+=g.vz*=s:(g.z=g.fz,g.vz=0));return n}function w(){for(var u=0,m=t.length,d;u1&&isNaN(d.y)||r>2&&isNaN(d.z)){var g=Ce*(r>2?Math.cbrt(.5+u):r>1?Math.sqrt(.5+u):u),_=u*He,N=u*Qe;r===1?d.x=g:r===2?(d.x=g*Math.cos(_),d.y=g*Math.sin(_)):(d.x=g*Math.sin(_)*Math.cos(N),d.y=g*Math.cos(_),d.z=g*Math.sin(_)*Math.sin(N))}(isNaN(d.vx)||r>1&&isNaN(d.vy)||r>2&&isNaN(d.vz))&&(d.vx=0,r>1&&(d.vy=0),r>2&&(d.vz=0))}}function y(u){return u.initialize&&u.initialize(t,x,r),u}return w(),n={tick:p,restart:function(){return h.restart(v),n},stop:function(){return h.stop(),n},numDimensions:function(u){return arguments.length?(r=Math.min(Fe,Math.max(1,Math.round(u))),l.forEach(y),n):r},nodes:function(u){return arguments.length?(t=u,w(),l.forEach(y),n):t},alpha:function(u){return arguments.length?(i=+u,n):i},alphaMin:function(u){return arguments.length?(o=+u,n):o},alphaDecay:function(u){return arguments.length?(f=+u,n):+f},alphaTarget:function(u){return arguments.length?(a=+u,n):a},velocityDecay:function(u){return arguments.length?(s=1-u,n):1-s},randomSource:function(u){return arguments.length?(x=u,l.forEach(y),n):x},force:function(u,m){return arguments.length>1?(m==null?l.delete(u):l.set(u,y(m)),n):l.get(u)},find:function(){var u=Array.prototype.slice.call(arguments),m=u.shift()||0,d=(r>1?u.shift():null)||0,g=(r>2?u.shift():null)||0,_=u.shift()||1/0,N=0,A=t.length,M,S,B,W,k,wt;for(_*=_,N=0;N1?(c.on(u,m),n):c.on(u)}}}function pt(){var t,e,r,n,i,o=b(-30),f,a=1,s=1/0,l=.81;function h(p){var w,y=t.length,u=(e===1?X(t,tt):e===2?R(t,tt,ht):e===3?Y(t,tt,ht,We):null).visitAfter(x);for(i=p,w=0;w1&&(p.y=g/m),e>2&&(p.z=_/m)}else{y=p,y.x=y.data.x,e>1&&(y.y=y.data.y),e>2&&(y.z=y.data.z);do w+=f[y.data.index];while(y=y.next)}p.value=w}function v(p,w,y,u,m){if(!p.value)return!0;var d=[y,u,m][e-1],g=p.x-r.x,_=e>1?p.y-r.y:0,N=e>2?p.z-r.z:0,A=d-w,M=g*g+_*_+N*N;if(A*A/l1&&_===0&&(_=j(n),M+=_*_),e>2&&N===0&&(N=j(n),M+=N*N),M1&&(r.vy+=_*p.value*i/M),e>2&&(r.vz+=N*p.value*i/M)),!0;if(p.length||M>=s)return;(p.data!==r||p.next)&&(g===0&&(g=j(n),M+=g*g),e>1&&_===0&&(_=j(n),M+=_*_),e>2&&N===0&&(N=j(n),M+=N*N),M1&&(r.vy+=_*A),e>2&&(r.vz+=N*A));while(p=p.next)}return h.initialize=function(p,...w){t=p,n=w.find(y=>typeof y=="function")||Math.random,e=w.find(y=>[1,2,3].includes(y))||2,c()},h.strength=function(p){return arguments.length?(o=typeof p=="function"?p:b(+p),c(),h):o},h.distanceMin=function(p){return arguments.length?(a=p*p,h):Math.sqrt(a)},h.distanceMax=function(p){return arguments.length?(s=p*p,h):Math.sqrt(s)},h.theta=function(p){return arguments.length?(l=p*p,h):Math.sqrt(l)},h}function xt(t){var e=b(.1),r,n,i;typeof t!="function"&&(t=b(t==null?0:+t));function o(a){for(var s=0,l=r.length,h;s{let r=typeof e.source=="number"?e.source:e.source.index??0,n=typeof e.target=="number"?e.target:e.target.index??0,i=1/Math.min(yt[r]??1,yt[n]??1);return Math.min(i*t,1)}}function Ze(t){if(!q)return;q.force("charge")?.strength(t.charge);let e=q.force("link");e?.distance(t.linkDistance),e?.strength(Se(t.linkStrength)),q.force("x")?.strength(t.centerPull),q.force("y")?.strength(t.centerPull+t.flatten),q.force("z")?.strength(t.centerPull)}function et(){dt||(dt=!0,self.setTimeout(Ue,0))}function Ue(){dt=!1;let t=q;if(!t||T)return;let e=Date.now();for(;Date.now()-e<12;)if(t.tick(),mt++,t.alpha(){let e=t.data;switch(e.type){case"init":{q?.stop();let r=new Float32Array(e.positions);yt=new Float32Array(e.degrees);let n=new Uint32Array(e.links);U=[];for(let o=0;o{let h=d.data;if(h.type!=="tick")return;let f=new Float32Array(h.buffer);this.positions.set(f.subarray(0,this.positions.length)),this._ticks=h.ticks,this.settled=h.settled,this.dirty=!0,this.worker?.postMessage({type:"buffer",buffer:h.buffer},[h.buffer])};let r=e.nodes.length,o=new Float32Array(t),a=new Uint32Array(e.links.length*2);e.links.forEach((d,h)=>{a[h*2]=d.source,a[h*2+1]=d.target});let l=new Float32Array(r);e.nodes.forEach((d,h)=>l[h]=Math.max(d.degree,1));let c=new ArrayBuffer(r*3*4),u=new ArrayBuffer(r*3*4);this.settled=s<.001,this.worker.postMessage({type:"init",count:r,positions:o.buffer,links:a.buffer,degrees:l.buffer,params:n,initialAlpha:s,bufA:c,bufB:u},[o.buffer,a.buffer,l.buffer,c,u])}step(){let e=this.dirty;return this.dirty=!1,e}isSettled(){return this.settled}reheat(e=.3){this.settled=!1,this.worker?.postMessage({type:"reheat",alpha:e})}updateParams(e){this.settled=!1,this.worker?.postMessage({type:"params",params:e})}disposeWorker(){this.worker?.terminate(),this.worker=null,this.url&&(URL.revokeObjectURL(this.url),this.url="")}dispose(){this.disposeWorker(),this.settled=!0,this.dirty=!1}};var Os={name:"CopyShader",uniforms:{tDiffuse:{value:null},opacity:{value:1}},vertexShader:` -function addHierarchyChildHighlights(data, nodeId, relatedNodes, highlightedEdges, labelNodes) { - for (const edge of data.hierarchyChildrenByNode?.get(nodeId) || []) { - highlightedEdges.add(edge.key); - addHierarchyEdgeNodes(edge, relatedNodes, labelNodes); - } - addHierarchyChildNodeIds(data, nodeId, relatedNodes, labelNodes); -} + varying vec2 vUv; -function addHierarchyDescendantHighlights(data, nodeId, relatedNodes, highlightedEdges, labelNodes) { - const childrenByNode = data.hierarchyChildrenByNode || new Map(); - const childIdsByNode = data.hierarchyChildNodeIdsByNode || new Map(); - const stack = []; - const visited = new Set([nodeId]); - - pushHierarchyChildEntries(stack, childrenByNode, childIdsByNode, nodeId); - - while (stack.length) { - const entry = stack.pop(); - if (!entry || entry.target === null || entry.target === undefined || visited.has(entry.target)) continue; - if (entry.edge) { - highlightedEdges.add(entry.edge.key); - addHierarchyEdgeNodes(entry.edge, relatedNodes, labelNodes); - } else { - relatedNodes.add(entry.target); - labelNodes.add(entry.target); - } - visited.add(entry.target); - pushHierarchyChildEntries(stack, childrenByNode, childIdsByNode, entry.target); - } -} + void main() { -function pushHierarchyChildEntries(stack, childrenByNode, childIdsByNode, nodeId) { - const edges = childrenByNode.get(nodeId) || []; - const edgeTargets = new Set(); - for (let index = edges.length - 1; index >= 0; index -= 1) { - const edge = edges[index]; - if (!edge || edge.target === null || edge.target === undefined) continue; - edgeTargets.add(edge.target); - stack.push({ edge, target: edge.target }); - } - - const childIds = childIdsByNode.get(nodeId) || []; - for (let index = childIds.length - 1; index >= 0; index -= 1) { - const childId = childIds[index]; - if (edgeTargets.has(childId)) continue; - stack.push({ edge: null, target: childId }); - } -} + vUv = uv; + gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 ); -function addHierarchyChildNodeIds(data, nodeId, relatedNodes, labelNodes) { - for (const childId of data.hierarchyChildNodeIdsByNode?.get(nodeId) || []) { - relatedNodes.add(childId); - labelNodes.add(childId); - } -} + }`,fragmentShader:` -function addHierarchyEdgeNodes(edge, relatedNodes, labelNodes) { - if (!edge) return; - if (edge.source !== null && edge.source !== undefined) { - relatedNodes.add(edge.source); - labelNodes.add(edge.source); - } - if (edge.target !== null && edge.target !== undefined) { - relatedNodes.add(edge.target); - labelNodes.add(edge.target); - } -} + uniform float opacity; -function resolveCssColor(root, variableName, fallback) { - if (!root || !root.ownerDocument) return fallback; - const probe = root.ownerDocument.createElement("span"); - probe.style.position = "absolute"; - probe.style.width = "0"; - probe.style.height = "0"; - probe.style.overflow = "hidden"; - probe.style.pointerEvents = "none"; - probe.style.color = `var(${variableName})`; - root.appendChild(probe); - const color = getComputedStyle(probe).color; - probe.remove(); - return color || fallback; -} + uniform sampler2D tDiffuse; -function resolveGraphViewColor(root, colorClass, fallback) { - if (!root || !root.ownerDocument) return fallback; - const wrapper = root.ownerDocument.createElement("span"); - wrapper.style.position = "absolute"; - wrapper.style.width = "0"; - wrapper.style.height = "0"; - wrapper.style.overflow = "hidden"; - wrapper.style.pointerEvents = "none"; - wrapper.style.color = fallback; - const probe = root.ownerDocument.createElement("span"); - probe.className = `graph-view ${colorClass}`; - wrapper.appendChild(probe); - root.appendChild(wrapper); - const color = getComputedStyle(probe).color; - wrapper.remove(); - return color || fallback; -} + varying vec2 vUv; -function canvasWorldBounds(viewport, panX, panY, zoom, margin = 160) { - const safeZoom = Math.max(MIN_CANVAS_ZOOM, Number(zoom) || 1); - const minX = (0 - panX) / safeZoom - margin; - const minY = (0 - panY) / safeZoom - margin; - const maxX = ((viewport?.width || 1) - panX) / safeZoom + margin; - const maxY = ((viewport?.height || 1) - panY) / safeZoom + margin; - return { minX, minY, maxX, maxY }; -} + void main() { -function circleInBounds(point, radius, bounds) { - if (!point || !bounds) return true; - const r = Math.max(0, Number(radius) || 0); - return point.x + r >= bounds.minX - && point.x - r <= bounds.maxX - && point.y + r >= bounds.minY - && point.y - r <= bounds.maxY; -} + vec4 texel = texture2D( tDiffuse, vUv ); + gl_FragColor = opacity * texel; -function buildCanvasNodeSpatialIndex(nodes) { - if (!Array.isArray(nodes) || !nodes.length) return null; - - let maxRadius = 1; - for (const item of nodes) { - maxRadius = Math.max(maxRadius, Number(item.radius) || 1); - } - - const cellSize = clampFloat(maxRadius * 2.6 + 28, 56, 220, 96); - const grid = new Map(); - for (const item of nodes) { - if (!item || !item.point) continue; - const gx = Math.floor(item.point.x / cellSize); - const gy = Math.floor(item.point.y / cellSize); - const key = `${gx},${gy}`; - if (!grid.has(key)) grid.set(key, []); - grid.get(key).push(item); - } - - return { cellSize, grid, maxRadius }; -} -function hitTestCanvasNodeIndex(data, world, nodePad) { - const index = data && data.nodeSpatialIndex; - if (!index || !index.grid || !Number.isFinite(index.cellSize)) { - return hitTestCanvasNodeLinear(data, world, nodePad); - } - - const reach = Math.max(index.maxRadius || 1, nodePad || 0) + Math.max(1, nodePad || 0); - const span = Math.max(1, Math.ceil(reach / index.cellSize)); - const centerX = Math.floor(world.x / index.cellSize); - const centerY = Math.floor(world.y / index.cellSize); - let best = null; - let bestRenderIndex = -1; - - for (let gx = centerX - span; gx <= centerX + span; gx += 1) { - for (let gy = centerY - span; gy <= centerY + span; gy += 1) { - const bucket = index.grid.get(`${gx},${gy}`); - if (!bucket) continue; - for (const item of bucket) { - const distance = Math.hypot(world.x - item.point.x, world.y - item.point.y); - if (distance > item.radius + nodePad) continue; - const renderIndex = Number.isFinite(item.renderIndex) ? item.renderIndex : 0; - if (renderIndex >= bestRenderIndex) { - best = item; - bestRenderIndex = renderIndex; - } - } - } - } - - return best; -} + }`};var mn=class{constructor(){this.isPass=!0,this.enabled=!0,this.needsSwap=!0,this.clear=!1,this.renderToScreen=!1}setSize(){}render(){console.error("THREE.Pass: .render() must be implemented in derived pass.")}dispose(){}},vy=new si(-1,1,1,-1,0,1),zh=class extends ht{constructor(){super(),this.setAttribute("position",new Ut([-1,3,0,-1,-1,0,3,-1,0],3)),this.setAttribute("uv",new Ut([0,2,0,0,2,0],2))}},_y=new zh,ki=class{constructor(e){this._mesh=new Xt(_y,e)}dispose(){this._mesh.geometry.dispose()}render(e){e.render(this._mesh,vy)}get material(){return this._mesh.material}set material(e){this._mesh.material=e}};var gl=class extends mn{constructor(e,t="tDiffuse"){super(),this.textureID=t,this.uniforms=null,this.material=null,e instanceof ut?(this.uniforms=e.uniforms,this.material=e):e&&(this.uniforms=oi.clone(e.uniforms),this.material=new ut({name:e.name!==void 0?e.name:"unspecified",defines:Object.assign({},e.defines),uniforms:this.uniforms,vertexShader:e.vertexShader,fragmentShader:e.fragmentShader})),this._fsQuad=new ki(this.material)}render(e,t,n){this.uniforms[this.textureID]&&(this.uniforms[this.textureID].value=n.texture),this._fsQuad.material=this.material,this.renderToScreen?(e.setRenderTarget(null),this._fsQuad.render(e)):(e.setRenderTarget(t),this.clear&&e.clear(e.autoClearColor,e.autoClearDepth,e.autoClearStencil),this._fsQuad.render(e))}dispose(){this.material.dispose(),this._fsQuad.dispose()}};var Xr=class extends mn{constructor(e,t){super(),this.scene=e,this.camera=t,this.clear=!0,this.needsSwap=!1,this.inverse=!1}render(e,t,n){let s=e.getContext(),r=e.state;r.buffers.color.setMask(!1),r.buffers.depth.setMask(!1),r.buffers.color.setLocked(!0),r.buffers.depth.setLocked(!0);let o,a;this.inverse?(o=0,a=1):(o=1,a=0),r.buffers.stencil.setTest(!0),r.buffers.stencil.setOp(s.REPLACE,s.REPLACE,s.REPLACE),r.buffers.stencil.setFunc(s.ALWAYS,o,4294967295),r.buffers.stencil.setClear(a),r.buffers.stencil.setLocked(!0),e.setRenderTarget(n),this.clear&&e.clear(),e.render(this.scene,this.camera),e.setRenderTarget(t),this.clear&&e.clear(),e.render(this.scene,this.camera),r.buffers.color.setLocked(!1),r.buffers.depth.setLocked(!1),r.buffers.color.setMask(!0),r.buffers.depth.setMask(!0),r.buffers.stencil.setLocked(!1),r.buffers.stencil.setFunc(s.EQUAL,1,4294967295),r.buffers.stencil.setOp(s.KEEP,s.KEEP,s.KEEP),r.buffers.stencil.setLocked(!0)}},xl=class extends mn{constructor(){super(),this.needsSwap=!1}render(e){e.state.buffers.stencil.setLocked(!1),e.state.buffers.stencil.setTest(!1)}};var vl=class{constructor(e,t){if(this.renderer=e,this._pixelRatio=e.getPixelRatio(),t===void 0){let n=e.getSize(new Ee);this._width=n.width,this._height=n.height,t=new Rt(this._width*this._pixelRatio,this._height*this._pixelRatio,{type:qt}),t.texture.name="EffectComposer.rt1"}else this._width=t.width,this._height=t.height;this.renderTarget1=t,this.renderTarget2=t.clone(),this.renderTarget2.texture.name="EffectComposer.rt2",this.writeBuffer=this.renderTarget1,this.readBuffer=this.renderTarget2,this.renderToScreen=!0,this.passes=[],this.copyPass=new gl(Os),this.copyPass.material.blending=yn,this.timer=new gr}swapBuffers(){let e=this.readBuffer;this.readBuffer=this.writeBuffer,this.writeBuffer=e}addPass(e){this.passes.push(e),e.setSize(this._width*this._pixelRatio,this._height*this._pixelRatio)}insertPass(e,t){this.passes.splice(t,0,e),e.setSize(this._width*this._pixelRatio,this._height*this._pixelRatio)}removePass(e){let t=this.passes.indexOf(e);t!==-1&&this.passes.splice(t,1)}isLastEnabledPass(e){for(let t=e+1;t= 0; i -= 1) { - const item = data.nodes[i]; - if (!item || !item.point) continue; - const distance = Math.hypot(world.x - item.point.x, world.y - item.point.y); - if (distance <= item.radius + nodePad) return item; - } - return null; -} + varying vec2 vUv; -function edgeItemInBounds(item, bounds) { - if (!item || !bounds) return true; - const source = item.sourcePoint; - const target = item.targetPoint; - if (!source || !target) return true; - const route = item.dynamicRoute || item.route; - - let minX = Math.min(source.x, target.x); - let minY = Math.min(source.y, target.y); - let maxX = Math.max(source.x, target.x); - let maxY = Math.max(source.y, target.y); - - if (route && Number.isFinite(route.radius) && Number.isFinite(route.centerX) && Number.isFinite(route.centerY)) { - minX = Math.min(minX, route.centerX - route.radius); - minY = Math.min(minY, route.centerY - route.radius); - maxX = Math.max(maxX, route.centerX + route.radius); - maxY = Math.max(maxY, route.centerY + route.radius); - } else if (route && route.kind === "curve") { - const centerX = Number.isFinite(source.centerX) ? source.centerX : (source.x + target.x) / 2; - const centerY = Number.isFinite(source.centerY) ? source.centerY : (source.y + target.y) / 2; - const midX = (source.x + target.x) / 2; - const midY = (source.y + target.y) / 2; - const pull = Number.isFinite(route.curveStrength) ? route.curveStrength : (item.external ? 0.28 : 0.16); - const controlX = midX + (centerX - midX) * pull; - const controlY = midY + (centerY - midY) * pull; - minX = Math.min(minX, controlX); - minY = Math.min(minY, controlY); - maxX = Math.max(maxX, controlX); - maxY = Math.max(maxY, controlY); - } - - return maxX >= bounds.minX - && minX <= bounds.maxX - && maxY >= bounds.minY - && minY <= bounds.maxY; -} + void main() { -function distanceToSegment(point, source, target) { - const dx = target.x - source.x; - const dy = target.y - source.y; - if (dx === 0 && dy === 0) return Math.hypot(point.x - source.x, point.y - source.y); - const t = Math.max(0, Math.min(1, ((point.x - source.x) * dx + (point.y - source.y) * dy) / (dx * dx + dy * dy))); - const x = source.x + dx * t; - const y = source.y + dy * t; - return Math.hypot(point.x - x, point.y - y); -} + vUv = uv; -function dynamicOrbitRouteForEdge(item, centerX, centerY, ringGap) { - const source = item?.sourcePoint; - const target = item?.targetPoint; - if (!source || !target) return null; - - const sx = source.x - centerX; - const sy = source.y - centerY; - const tx = target.x - centerX; - const ty = target.y - centerY; - const sourceRadius = Math.max(1, Math.hypot(sx, sy)); - const targetRadius = Math.max(1, Math.hypot(tx, ty)); - const sourceAngle = Math.atan2(sy, sx); - const targetAngle = Math.atan2(ty, tx); - const delta = shortestAngleDelta(sourceAngle, targetAngle); - const angleDistance = Math.abs(delta); - const safeRingGap = Math.max(120, Number(ringGap) || DEFAULT_RING_SPACING); - const external = item.external || item.edge?.externalCount; - const sameOrNearRing = Math.abs(sourceRadius - targetRadius) < safeRingGap * 0.5; - const laneNoise = (deterministicUnitOffset(item.key || item.edge?.id || `${item.source}->${item.target}`, "dynamic-route") + 1) * 0.5; - const laneOffset = (0.34 + laneNoise * 0.66) * safeRingGap * (external ? 0.16 : 0.095); - const routeRadius = Math.max(sourceRadius, targetRadius) - + Math.max(external ? 96 : 52, safeRingGap * (external ? 0.2 : sameOrNearRing ? 0.13 : 0.09)) - + laneOffset; - - if (!external && !sameOrNearRing && angleDistance < 0.28) { - return { - kind: "curve", - curveStrength: 0.12 - }; - } - - return { - kind: "dynamic-orbit", - centerX, - centerY, - radius: routeRadius, - sourceAngle, - targetAngle, - endAngle: sourceAngle + delta, - sweep: delta >= 0 ? 1 : 0 - }; -} + gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 ); -function linkTooltip(index, edge, graph) { - const source = graphNode(index, graph, edge.source); - const target = graphNode(index, graph, edge.target); - return [ - "Aggregated internal link overlay", - `${source ? source.title : edge.source} -> ${target ? target.title : edge.target}`, - `weight: ${edge.weight || 0}`, - `raw edges: ${edge.rawCount || edge.weight || 0}`, - `unresolved: ${edge.unresolvedCount || 0}`, - `external: ${edge.externalCount || 0}` - ].join("\n"); -} + }`,fragmentShader:` -function nodeRenderScore(node, rootId, focusId, query) { - let score = 0; - if (node.id === rootId) score += 1000000; - if (node.id === focusId) score += 900000; - if (query && nodeMatches(node, query)) score += 700000; - score += Math.max(0, 200 - node.depth * 18); - - if (node.type === "folder") { - score += 5000 + Math.min(2500, Math.log2((node.noteCount || 0) + 1) * 280); - } else if (node.type === "note") { - score += 500 + Math.min(2500, Math.log2((node.linkCount || 0) + (node.backlinkCount || 0) + 1) * 420); - } else if (node.type === "unresolved") { - score += 80; - } - - return score; -} + uniform sampler2D tDiffuse; + uniform vec3 defaultColor; + uniform float defaultOpacity; + uniform float luminosityThreshold; + uniform float smoothWidth; -function linkRenderScore(edge) { - return (edge.externalCount ? 100000 : 0) - + Math.min(60000, (edge.weight || 0) * 100) - + Math.min(10000, (edge.rawCount || 0) * 10); -} + varying vec2 vUv; -function externalAnchorPath(node, rootId) { - const nodePath = node.type === "unresolved" ? parentPath(node.path) : node.path; - const pathParts = String(nodePath || "").split("/").filter(Boolean); - if (!pathParts.length) return null; - if (!rootId) return pathParts[0] || nodePath; - - const rootParts = String(rootId || "").split("/").filter(Boolean); - let common = 0; - while ( - common < rootParts.length - && common < pathParts.length - && rootParts[common] === pathParts[common] - ) { - common += 1; - } - - const anchorLength = Math.min(pathParts.length, common + 1); - return pathParts.slice(0, anchorLength).join("/"); -} + void main() { -function computeLinkRoutes(linkEdges, positions, maxDepth, ringSpacing, outerRadius, routeGapFactor = 1) { - const routes = new Map(); - let maxRadius = Math.max(outerRadius || 0, 1); - - for (const edge of linkEdges) { - const source = positions.get(edge.source); - const target = positions.get(edge.target); - if (!source || !target) continue; - - const sourceRadius = Number.isFinite(source.radius) ? source.radius : 0; - const targetRadius = Number.isFinite(target.radius) ? target.radius : 0; - const sourceAngle = Number.isFinite(source.angle) ? source.angle : Math.atan2(target.y - source.y, target.x - source.x); - const targetAngle = Number.isFinite(target.angle) ? target.angle : sourceAngle; - const delta = shortestAngleDelta(sourceAngle, targetAngle); - const angleDistance = Math.abs(delta); - const sameOrNearRing = Math.abs(sourceRadius - targetRadius) < ringSpacing * 0.44; - const touchesOuterRing = Math.max(source.depth || 0, target.depth || 0) >= Math.max(1, maxDepth - 1); - const isExternal = Boolean(source.external || target.external || edge.externalCount); - const shouldCurve = isExternal - || sameOrNearRing - || (touchesOuterRing && angleDistance > 0.34) - || angleDistance > Math.PI * 0.42; - - if (!shouldCurve) continue; - - routes.set(edge.id, { - kind: "curve", - lane: 0, - curveStrength: isExternal - ? 0.3 - : sameOrNearRing - ? 0.22 - : angleDistance > Math.PI * 0.72 - ? 0.2 - : 0.15 - }); - } - - return { routes, maxRadius }; -} + vec4 texel = texture2D( tDiffuse, vUv ); -function assignRadialLaneRoutes(items, routes, outerRadius, ringSpacing, routeGapFactor = 1) { - const lanes = []; - let maxRadius = outerRadius || 0; - const sorted = items.slice().sort((a, b) => { - if (a.interval.start !== b.interval.start) return a.interval.start - b.interval.start; - return a.interval.end - b.interval.end; - }); - - for (const item of sorted) { - let lane = lanes.findIndex(endAngle => item.interval.start > endAngle + 0.12); - if (lane === -1) { - lane = lanes.length; - lanes.push(item.interval.end); - } else { - lanes[lane] = item.interval.end; - } - - const cappedLane = Math.min(lane, 14); - const laneStep = Math.max(18, Math.min(44, ringSpacing * 0.075 * routeGapFactor)); - const radius = Math.max( - outerRadius || 0, - item.source.radius || 0, - item.target.radius || 0 - ) + (item.isExternal ? 100 : 48) * routeGapFactor + cappedLane * laneStep; - maxRadius = Math.max(maxRadius, radius); - routes.set(item.edge.id, { - kind: item.isExternal ? "external" : "outer", - lane, - centerX: 0, - centerY: 0, - radius, - sourceAngle: item.sourceAngle, - targetAngle: item.targetAngle, - endAngle: item.sourceAngle + item.delta, - sweep: item.delta >= 0 ? 1 : 0 - }); - } - - return maxRadius; -} + float v = luminance( texel.xyz ); -function radialRouteInterval(sourceAngle, delta) { - let start = normalizeAngle(sourceAngle); - let end = start + delta; - if (end < start) { - const swap = start; - start = end; - end = swap; - } - if (end - start < 0.05) end += 0.05; - return { start, end }; -} + vec4 outputColor = vec4( defaultColor.rgb, defaultOpacity ); -function wrapCanvasLabel(ctx, label, maxWidth, maxLines) { - const text = String(label || "").replace(/\s+/g, " ").trim(); - if (!text) return []; + float alpha = smoothstep( luminosityThreshold, luminosityThreshold + smoothWidth, v ); - const words = text.split(" "); - const lines = []; - let current = ""; + gl_FragColor = mix( outputColor, texel, alpha ); - for (const word of words) { - const pieces = splitCanvasWord(ctx, word, maxWidth); + }`};var Us=class i extends mn{constructor(e,t=1,n,s){super(),this.strength=t,this.radius=n,this.threshold=s,this.resolution=e!==void 0?new Ee(e.x,e.y):new Ee(256,256),this.clearColor=new me(0,0,0),this.needsSwap=!1,this.renderTargetsHorizontal=[],this.renderTargetsVertical=[],this.nMips=5;let r=Math.round(this.resolution.x/2),o=Math.round(this.resolution.y/2);this.renderTargetBright=new Rt(r,o,{type:qt}),this.renderTargetBright.texture.name="UnrealBloomPass.bright",this.renderTargetBright.texture.generateMipmaps=!1;for(let u=0;u -function fitCanvasText(ctx, text, maxWidth, suffix = "...") { - if (ctx.measureText(text).width <= maxWidth) return text; - const chars = Array.from(String(text || "")); - while (chars.length && ctx.measureText(`${chars.join("")}${suffix}`).width > maxWidth) { - chars.pop(); - } - return chars.length ? `${chars.join("")}${suffix}` : suffix; -} + varying vec2 vUv; -function fitZoomForLayout(layout, viewport, inset = 32) { - if (!layout || !viewport) return DEFAULT_MIN_CANVAS_ZOOM; - const availableWidth = Math.max(1, (viewport.width || 1) - inset); - const availableHeight = Math.max(1, (viewport.height || 1) - inset); - return Math.min( - availableWidth / Math.max(1, layout.width || 1), - availableHeight / Math.max(1, layout.height || 1) - ); -} + uniform sampler2D colorTexture; + uniform vec2 invSize; + uniform vec2 direction; + uniform float gaussianCoefficients[KERNEL_RADIUS]; -function adaptiveMaxZoomForLayout(layout, viewport) { - if (!layout || !viewport) return MAX_CANVAS_ZOOM; - - const fitZoom = fitZoomForLayout(layout, viewport, 42); - const viewportMin = Math.max(1, Math.min(viewport.width || 1, viewport.height || 1)); - const nodeCount = Math.max(1, layout.positions ? layout.positions.size : 1); - const area = Math.max(1, (layout.width || 1) * (layout.height || 1)); - const densitySpacing = Math.sqrt(area / nodeCount) * 0.58; - const ringGap = medianRingGap(layout.rings); - const usefulWorldWindow = clampFloat( - Math.max(densitySpacing * 1.65, ringGap * 1.55), - 280, - 1180, - 520 - ); - const densityMax = viewportMin / usefulWorldWindow; - const countFactor = nodeCount < 120 - ? 2.3 - : nodeCount < 700 - ? 3.35 - : nodeCount < 2400 - ? 4.45 - : 5.25; - const fitMax = fitZoom * countFactor; - const minAllowed = Math.min( - MAX_CANVAS_ZOOM, - Math.max(MIN_CANVAS_ZOOM, DEFAULT_MIN_CANVAS_ZOOM * 2.4, fitZoom * 1.22) - ); - const proposed = Math.max(fitZoom * 1.35, densityMax, fitMax); - return clampFloat(proposed, minAllowed, MAX_CANVAS_ZOOM, MAX_CANVAS_ZOOM); -} + void main() { -function medianRingGap(rings) { - if (!Array.isArray(rings) || !rings.length) return 420; - - const radii = rings - .map(ring => ring && Number(ring.radius)) - .filter(radius => Number.isFinite(radius) && radius > 0) - .sort((a, b) => a - b); - if (!radii.length) return 420; - if (radii.length === 1) return clampFloat(radii[0], 260, 720, 420); - - const gaps = []; - for (let index = 1; index < radii.length; index += 1) { - const gap = radii[index] - radii[index - 1]; - if (Number.isFinite(gap) && gap > 0) gaps.push(gap); - } - return medianNumber(gaps, 420); -} + float weightSum = gaussianCoefficients[0]; + vec3 diffuseSum = texture2D( colorTexture, vUv ).rgb * weightSum; -function medianNumber(values, fallback) { - const numbers = (values || []) - .filter(value => Number.isFinite(value)) - .sort((a, b) => a - b); - if (!numbers.length) return fallback; - const middle = Math.floor(numbers.length / 2); - return numbers.length % 2 - ? numbers[middle] - : (numbers[middle - 1] + numbers[middle]) / 2; -} + for ( int i = 1; i < KERNEL_RADIUS; i ++ ) { -function normalizeWheelDeltaY(deltaY, deltaMode = 0) { - let delta = Number(deltaY) || 0; - if (deltaMode === 1) delta *= 40; - else if (deltaMode === 2) delta *= 800; - return delta; -} + float x = float( i ); + float w = gaussianCoefficients[i]; + vec2 uvOffset = direction * invSize * x; + vec3 sample1 = texture2D( colorTexture, vUv + uvOffset ).rgb; + vec3 sample2 = texture2D( colorTexture, vUv - uvOffset ).rgb; + diffuseSum += ( sample1 + sample2 ) * w; -function zoomToSliderValue(zoom, minZoom, maxZoom) { - const min = Math.max(MIN_CANVAS_ZOOM, Number(minZoom) || MIN_CANVAS_ZOOM); - const max = Math.max(min * 1.001, Number(maxZoom) || MAX_CANVAS_ZOOM); - const clamped = clampFloat(zoom, min, max, min); - const ratio = Math.log(clamped / min) / Math.log(max / min); - const sliderRatio = Math.pow(clampFloat(ratio, 0, 1, 0), 1 / ZOOM_SLIDER_CURVE); - return String(Math.round(sliderRatio * ZOOM_SLIDER_STEPS)); -} + } -function sliderValueToZoom(value, minZoom, maxZoom) { - const min = Math.max(MIN_CANVAS_ZOOM, Number(minZoom) || MIN_CANVAS_ZOOM); - const max = Math.max(min * 1.001, Number(maxZoom) || MAX_CANVAS_ZOOM); - const sliderRatio = clampFloat(Number(value) / ZOOM_SLIDER_STEPS, 0, 1, 0); - const ratio = Math.pow(sliderRatio, ZOOM_SLIDER_CURVE); - return min * Math.pow(max / min, ratio); -} + gl_FragColor = vec4( diffuseSum, 1.0 ); -function truncateLabel(label, maxLength) { - const text = String(label || ""); - if (text.length <= maxLength) return text; - return `${text.slice(0, Math.max(1, maxLength - 1))}...`; -} + }`})}_getCompositeMaterial(e){return new ut({defines:{NUM_MIPS:e},uniforms:{blurTexture1:{value:null},blurTexture2:{value:null},blurTexture3:{value:null},blurTexture4:{value:null},blurTexture5:{value:null},bloomStrength:{value:1},bloomFactors:{value:null},bloomTintColors:{value:null},bloomRadius:{value:0}},vertexShader:` -function clampNumber(value, min, max, fallback) { - const parsed = Number(value); - if (!Number.isFinite(parsed)) return fallback; - return Math.min(max, Math.max(min, Math.round(parsed))); -} + varying vec2 vUv; -function clampFloat(value, min, max, fallback) { - const parsed = Number(value); - if (!Number.isFinite(parsed)) return fallback; - return Math.min(max, Math.max(min, parsed)); -} + void main() { + + vUv = uv; + gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 ); + + }`,fragmentShader:` + + varying vec2 vUv; -function smoothstep(edge0, edge1, value) { - if (edge0 === edge1) return value >= edge1 ? 1 : 0; - const t = clampFloat((value - edge0) / (edge1 - edge0), 0, 1, 0); - return t * t * (3 - 2 * t); + uniform sampler2D blurTexture1; + uniform sampler2D blurTexture2; + uniform sampler2D blurTexture3; + uniform sampler2D blurTexture4; + uniform sampler2D blurTexture5; + uniform float bloomStrength; + uniform float bloomRadius; + uniform float bloomFactors[NUM_MIPS]; + uniform vec3 bloomTintColors[NUM_MIPS]; + + float lerpBloomFactor( const in float factor ) { + + float mirrorFactor = 1.2 - factor; + return mix( factor, mirrorFactor, bloomRadius ); + + } + + void main() { + + // 3.0 for backwards compatibility with previous alpha-based intensity + vec3 bloom = 3.0 * bloomStrength * ( + lerpBloomFactor( bloomFactors[ 0 ] ) * bloomTintColors[ 0 ] * texture2D( blurTexture1, vUv ).rgb + + lerpBloomFactor( bloomFactors[ 1 ] ) * bloomTintColors[ 1 ] * texture2D( blurTexture2, vUv ).rgb + + lerpBloomFactor( bloomFactors[ 2 ] ) * bloomTintColors[ 2 ] * texture2D( blurTexture3, vUv ).rgb + + lerpBloomFactor( bloomFactors[ 3 ] ) * bloomTintColors[ 3 ] * texture2D( blurTexture4, vUv ).rgb + + lerpBloomFactor( bloomFactors[ 4 ] ) * bloomTintColors[ 4 ] * texture2D( blurTexture5, vUv ).rgb + ); + + float bloomAlpha = max( bloom.r, max( bloom.g, bloom.b ) ); + gl_FragColor = vec4( bloom, bloomAlpha ); + + }`})}};Us.BlurDirectionX=new Ee(1,0);Us.BlurDirectionY=new Ee(0,1);var qr={name:"OutputShader",uniforms:{tDiffuse:{value:null},toneMappingExposure:{value:1}},vertexShader:` + precision highp float; + + uniform mat4 modelViewMatrix; + uniform mat4 projectionMatrix; + + attribute vec3 position; + attribute vec2 uv; + + varying vec2 vUv; + + void main() { + + vUv = uv; + gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 ); + + }`,fragmentShader:` + + precision highp float; + + uniform sampler2D tDiffuse; + + #include + #include + + varying vec2 vUv; + + void main() { + + gl_FragColor = texture2D( tDiffuse, vUv ); + + // tone mapping + + #ifdef LINEAR_TONE_MAPPING + + gl_FragColor.rgb = LinearToneMapping( gl_FragColor.rgb ); + + #elif defined( REINHARD_TONE_MAPPING ) + + gl_FragColor.rgb = ReinhardToneMapping( gl_FragColor.rgb ); + + #elif defined( CINEON_TONE_MAPPING ) + + gl_FragColor.rgb = CineonToneMapping( gl_FragColor.rgb ); + + #elif defined( ACES_FILMIC_TONE_MAPPING ) + + gl_FragColor.rgb = ACESFilmicToneMapping( gl_FragColor.rgb ); + + #elif defined( AGX_TONE_MAPPING ) + + gl_FragColor.rgb = AgXToneMapping( gl_FragColor.rgb ); + + #elif defined( NEUTRAL_TONE_MAPPING ) + + gl_FragColor.rgb = NeutralToneMapping( gl_FragColor.rgb ); + + #elif defined( CUSTOM_TONE_MAPPING ) + + gl_FragColor.rgb = CustomToneMapping( gl_FragColor.rgb ); + + #endif + + // color space + + #ifdef SRGB_TRANSFER + + gl_FragColor = sRGBTransferOETF( gl_FragColor ); + + #endif + + }`};var yl=class extends mn{constructor(){super(),this.isOutputPass=!0,this.uniforms=oi.clone(qr.uniforms),this.material=new As({name:qr.name,uniforms:this.uniforms,vertexShader:qr.vertexShader,fragmentShader:qr.fragmentShader}),this._fsQuad=new ki(this.material),this._outputColorSpace=null,this._toneMapping=null}render(e,t,n){this.uniforms.tDiffuse.value=n.texture,this.uniforms.toneMappingExposure.value=e.toneMappingExposure,(this._outputColorSpace!==e.outputColorSpace||this._toneMapping!==e.toneMapping)&&(this._outputColorSpace=e.outputColorSpace,this._toneMapping=e.toneMapping,this.material.defines={},ze.getTransfer(this._outputColorSpace)===Je&&(this.material.defines.SRGB_TRANSFER=""),this._toneMapping===yr?this.material.defines.LINEAR_TONE_MAPPING="":this._toneMapping===br?this.material.defines.REINHARD_TONE_MAPPING="":this._toneMapping===Mr?this.material.defines.CINEON_TONE_MAPPING="":this._toneMapping===Pi?this.material.defines.ACES_FILMIC_TONE_MAPPING="":this._toneMapping===wr?this.material.defines.AGX_TONE_MAPPING="":this._toneMapping===Er?this.material.defines.NEUTRAL_TONE_MAPPING="":this._toneMapping===Sr&&(this.material.defines.CUSTOM_TONE_MAPPING=""),this.material.needsUpdate=!0),this.renderToScreen===!0?(e.setRenderTarget(null),this._fsQuad.render(e)):(e.setRenderTarget(t),this.clear&&e.clear(e.autoClearColor,e.autoClearDepth,e.autoClearStencil),this._fsQuad.render(e))}dispose(){this.material.dispose(),this._fsQuad.dispose()}};var bl=` +attribute float aSize; +attribute float aGhost; +attribute float aDim; +varying vec3 vColor; +varying float vGhost; +varying float vDim; +uniform float uPixelScale; // drawingBufferHeight / (2\xB7tan(fov/2)) +uniform float uSizeMul; // \u63A7\u5236\u9762\u677F\u300C\u8282\u70B9\u5927\u5C0F\u300D\u500D\u7387 +uniform float uMaxPoint; // \u8BBE\u5907\u50CF\u7D20\u94B3\u5236\uFF1A\u7A7F\u884C\u661F\u56E2\u65F6\u9632\u6EE1\u5C4F\u5927\u7CBE\u7075\u6253\u7206\u586B\u5145\u7387\uFF08M3\uFF09 + +void main() { + vColor = color; + vGhost = aGhost; + vDim = aDim; + vec4 mv = modelViewMatrix * vec4(position, 1.0); + gl_PointSize = min(aSize * uSizeMul * uPixelScale / max(-mv.z, 1.0), uMaxPoint); + gl_Position = projectionMatrix * mv; } +`,Ml=` +varying vec3 vColor; +varying float vGhost; +varying float vDim; +uniform float uLightMode; // 0 = \u6DF1\u7A7A\uFF08\u767D\u70ED\u6838\u5FC3\uFF09\uFF0C1 = \u6668\u663C\uFF08\u58A8\u6C34\u5706\u76D8 + rim\uFF09 + +void main() { + vec2 uv = gl_PointCoord - 0.5; + float d = length(uv); + + float core = smoothstep(0.18, 0.0, d) * 0.55 * (1.0 - vGhost) * (1.0 - uLightMode); + vec3 col = mix(vColor, vec3(1.0), core); + + // \u6668\u663C\uFF1A\u5916\u7F18 1px \u6DF1\u8272 rim\uFF0C\u8BA9\u8282\u70B9\u300C\u5750\u5728\u7EB8\u4E0A\u300D + float rim = smoothstep(0.40, 0.46, d) * smoothstep(0.50, 0.46, d); + col = mix(col, col * 0.72, rim * uLightMode); -function springBackEase(value) { - const t = clampFloat(value, 0, 1, 0); - const c1 = 1.28; - const c3 = c1 + 1; - return 1 + c3 * Math.pow(t - 1, 3) + c1 * Math.pow(t - 1, 2); + float alpha = smoothstep(0.5, 0.42, d) * mix(1.0, 0.45, vGhost) * vDim; + if (alpha < 0.01) discard; + gl_FragColor = vec4(col, alpha); } +`;function Yr(i){let e=i.map(t=>({prefix:t.query.startsWith("path:")?t.query.slice(5).trim():null,raw:t.query,color:new me(t.color)}));return t=>{if(t.unresolved)return yp;for(let n of e)if(n.prefix!==null?t.id.startsWith(n.prefix):t.id.includes(n.raw))return n.color;return bp(t.folderTop,!1)}}var Sl=i=>bp(i.folderTop,i.unresolved),vp=[0,40,80,120,160,200,240,280,320],yy=new me("#9aa4b2"),yp=new me("#7a8499"),_p=new Map;function bp(i,e){if(e)return yp;if(i==="")return yy;let t=_p.get(i);if(!t){let n=vp[Eh(i)%vp.length]??0;t=new me().setHSL(n/360,.6,.6),_p.set(i,t)}return t}function Mp(i,e){let t=i.clone().lerp(e,.5),n={h:0,s:0,l:0};return t.getHSL(n),t.setHSL(n.h,n.s*.4,Math.min(n.l,.35)),t}var by=[{count:2600,size:1.2},{count:900,size:2},{count:250,size:3}],My=new me("#9da8c4"),Sy=new me("#ffffff"),wy=new me("#ffe9c9"),Ey=new me("#bfd3ff");function Ty(i){let e=i>>>0;return()=>{e=e+1831565813>>>0;let t=e;return t=Math.imul(t^t>>>15,t|1),t^=t+Math.imul(t^t>>>7,t|61),((t^t>>>14)>>>0)/4294967296}}function Vh(i){for(let e of i.children){let t=e;t.geometry.dispose(),t.material.dispose()}}var wl=class{constructor(e,t){this.geometry=e;this.starCount=t;this.active=null;this.nextIn=3;this.attr=e.getAttribute("color"),this.baseColors=new Float32Array(this.attr.array)}update(e,t){if(this.active){this.active.t+=e;let n=this.active.t,s=1.6,r=this.attr.array,o=this.active.index*3,a=n>=s?1:1+2.2*Math.sin(Math.PI*n/s);r[o]=(this.baseColors[o]??1)*a,r[o+1]=(this.baseColors[o+1]??1)*a,r[o+2]=(this.baseColors[o+2]??1)*a,this.attr.needsUpdate=!0,n>=s&&(this.active=null);return}t<=.01||(this.nextIn-=e,this.nextIn<=0&&(this.active={index:Math.floor(Math.random()*this.starCount),t:0},this.nextIn=Math.min(Math.max(-Math.log(Math.random()+1e-9)*(6/t),1.5),90)))}};function Gh(i,e=1){let t=new Jn,n=Ty(5340353),s=null;for(let r of by){let o={count:Math.max(Math.round(r.count*e),50),size:r.size},a=new Float32Array(o.count*3),l=new Float32Array(o.count*3);for(let h=0;h=3&&n()<.03&&p.multiplyScalar(1.8),l[h*3]=p.r,l[h*3+1]=p.g,l[h*3+2]=p.b}let c=new ht;c.setAttribute("position",new Ye(a,3)),c.setAttribute("color",new Ye(l,3));let u=new Ei({size:o.size,sizeAttenuation:!1,vertexColors:!0,transparent:!0,opacity:.55,depthWrite:!1}),d=new Bn(c,u);d.renderOrder=-1,t.add(d),o.size>=3&&(s=new wl(c,o.count))}return{group:t,twinkler:s??new wl(new ht().setAttribute("color",new Ye(new Float32Array(3),3)),1)}}var El={id:"deep-space",background:3,starfield:!0,motes:!1,bloomEnabled:!0,lightMode:!1,nodeLightness:null,linkInk:null,linkOpacityScale:1,panelClass:"gx-theme-dark"},Sp={id:"daylight",background:16184559,starfield:!1,motes:!0,bloomEnabled:!1,lightMode:!0,nodeLightness:.44,linkInk:"#2e2a24",linkOpacityScale:.65,panelClass:"gx-theme-light"};var Ay=.12,Cy=.28,Tl=class{constructor(e,t){this.scene=new Zi;this.nodePoints=null;this.nodeMaterial=null;this.nodeGeometry=null;this.linkSegments=null;this.linkGeometry=null;this.linkMaterial=null;this.selSegments=null;this.selGeometry=null;this.selMaterial=null;this.selLinkIdx=[];this.twinkleFreq=.5;this.motes=null;this.reveal=null;this.revealBuf=new Float32Array(0);this.data={nodes:[],links:[]};this.positions=new Float32Array(0);this.sizes=new Float32Array(0);this.dimCurrent=new Float32Array(0);this.dimTarget=new Float32Array(0);this.dimAnimating=!1;this.colorFn=Sl;this.tokens=El;this.tierBloomAllowed=!0;this.lastW=2;this.lastH=2;this.baseLinkOpacity=.16;this.focusActive=!1;this.projVec=new N;this.pixelScale=1;this.nodeScale=1;this.sizeMode="degree";this.graphRadiusEstimate=t,this.renderer=new Ds({antialias:!1,alpha:!1}),this.renderer.setPixelRatio(Math.min(window.devicePixelRatio,2)),this.renderer.toneMapping=Pi,this.renderer.toneMappingExposure=1.05,this.renderer.info.autoReset=!1,e.appendChild(this.renderer.domElement),this.scene.background=new me(this.tokens.background),this.camera=new Wt(60,1,.5,5e4);let n=Gh(t*6.5);this.starfield=n.group,this.twinkler=n.twinkler,this.scene.add(this.starfield),this.composer=new vl(this.renderer),this.renderPass=new _l(this.scene,this.camera),this.bloomPass=new Us(new Ee(2,2),Jr.strength,Jr.radius,Jr.threshold),this.outputPass=new yl,this.composer.addPass(this.renderPass),this.composer.addPass(this.bloomPass),this.composer.addPass(this.outputPass)}setColorFn(e){this.colorFn=e}setData(e,t){this.data=e,this.positions=t,this.disposeGraphObjects();let n=e.nodes.length,s=e.links.length,r=new Float32Array(n*3);r.set(t.subarray(0,n*3));let o=new Float32Array(n);this.sizes=new Float32Array(n),this.dimCurrent=new Float32Array(n).fill(1),this.dimTarget=new Float32Array(n).fill(1);for(let a=0;an&&(n=r)}this.revealBuf.length=1){this.reveal=null,this.updatePositions(),this.linkMaterial&&(this.linkMaterial.opacity=this.effectiveLinkOpacity());return}let o=this.data.nodes.length,a=this.revealBuf,l=this.positions;for(let f=0;f.001,e.motes&&!this.motes&&this.buildMotes(),this.motes&&(this.motes.visible=e.motes),this.linkMaterial&&(this.linkMaterial.opacity=this.effectiveLinkOpacity()),this.recolor(),this.setSelectedLinks(this.selLinkIdx)}get currentTokens(){return this.tokens}buildMotes(){let t=new Float32Array(1800),n=this.graphRadiusEstimate*2.2;for(let o=0;o<600;o++)t[o*3]=(Math.random()*2-1)*n,t[o*3+1]=(Math.random()*2-1)*n,t[o*3+2]=(Math.random()*2-1)*n;let s=new ht;s.setAttribute("position",new Ye(t,3));let r=new Ei({color:new me("#d8d4cb"),size:1.6,sizeAttenuation:!0,transparent:!0,opacity:.5,depthWrite:!1});this.motes=new Bn(s,r),this.motes.renderOrder=-1,this.scene.add(this.motes)}render(e){this.starfield.rotation.y+=nc*e,this.starfield.visible&&this.twinkler.update(e,this.twinkleFreq),this.motes?.visible&&(this.motes.rotation.y-=nc*2*e),this.dimAnimating&&this.stepDim(e),this.reveal&&this.stepReveal(performance.now()),this.renderer.info.reset(),this.composer.render()}stepDim(e){let t=Math.min(e/Cy,1),n=!1;for(let s=0;s.001}getBloomStrength(){return this.bloomPass.enabled?this.bloomPass.strength:0}setBloomStrength(e){this.bloomPass.strength=e,this.bloomPass.enabled=this.tokens.bloomEnabled&&this.tierBloomAllowed&&e>.001}applyTier(e,t){this.tierBloomAllowed=e.bloomAllowed,this.renderer.setPixelRatio(Math.min(window.devicePixelRatio,e.pixelRatioCap)),this.bloomPass.enabled=this.tokens.bloomEnabled&&this.tierBloomAllowed&&t>.001;let n=this.starfield.visible,s=this.starfield.rotation.y;Vh(this.starfield),this.scene.remove(this.starfield);let r=Gh(this.graphRadiusEstimate*6.5,e.starScale);this.starfield=r.group,this.twinkler=r.twinkler,this.starfield.visible=n,this.starfield.rotation.y=s,this.scene.add(this.starfield),this.resize(this.lastW,this.lastH);let o=this.nodeMaterial?.uniforms.uMaxPoint;o&&(o.value=110*this.renderer.getPixelRatio())}setLinkOpacity(e){this.baseLinkOpacity=e,this.linkMaterial&&(this.linkMaterial.opacity=this.effectiveLinkOpacity())}setNodeScale(e){this.nodeScale=e;let t=this.nodeMaterial?.uniforms.uSizeMul;t&&(t.value=e)}resize(e,t){if(e<2||t<2)return;this.lastW=e,this.lastH=t,this.camera.aspect=e/t,this.camera.updateProjectionMatrix(),this.renderer.setSize(e,t),this.composer.setSize(e,t),this.bloomPass.resolution.set(e,t);let n=t*this.renderer.getPixelRatio();this.pixelScale=n/(2*Math.tan(this.camera.fov/2*Math.PI/180));let s=this.nodeMaterial?.uniforms.uPixelScale;s&&(s.value=this.pixelScale)}projectNode(e,t,n){return this.projVec.set(this.positions[e*3]??0,this.positions[e*3+1]??0,this.positions[e*3+2]??0),this.projVec.project(this.camera),{x:(this.projVec.x+1)/2*t,y:(1-this.projVec.y)/2*n,behind:this.projVec.z>1}}pickNearest(e,t,n,s,r){let o=-1,a=r;for(let l=0;lMath.PI&&(n-=tn),s<-Math.PI?s+=tn:s>Math.PI&&(s-=tn),n<=s?this._spherical.theta=Math.max(n,Math.min(s,this._spherical.theta)):this._spherical.theta=this._spherical.theta>(n+s)/2?Math.max(n,this._spherical.theta):Math.min(s,this._spherical.theta)),this._spherical.phi=Math.max(this.minPolarAngle,Math.min(this.maxPolarAngle,this._spherical.phi)),this._spherical.makeSafe(),this.enableDamping===!0?this.target.addScaledVector(this._panOffset,this.dampingFactor):this.target.add(this._panOffset),this.target.sub(this.cursor),this.target.clampLength(this.minTargetRadius,this.maxTargetRadius),this.target.add(this.cursor);let r=!1;if(this.zoomToCursor&&this._performCursorZoom||this.object.isOrthographicCamera)this._spherical.radius=this._clampDistance(this._spherical.radius);else{let o=this._spherical.radius;this._spherical.radius=this._clampDistance(this._spherical.radius*this._scale),r=o!=this._spherical.radius}if(Lt.setFromSpherical(this._spherical),Lt.applyQuaternion(this._quatInverse),t.copy(this.target).add(Lt),this.object.lookAt(this.target),this.enableDamping===!0?(this._sphericalDelta.theta*=1-this.dampingFactor,this._sphericalDelta.phi*=1-this.dampingFactor,this._panOffset.multiplyScalar(1-this.dampingFactor)):(this._sphericalDelta.set(0,0,0),this._panOffset.set(0,0,0)),this.zoomToCursor&&this._performCursorZoom){let o=null;if(this.object.isPerspectiveCamera){let a=Lt.length();o=this._clampDistance(a*this._scale);let l=a-o;this.object.position.addScaledVector(this._dollyDirection,l),this.object.updateMatrixWorld(),r=!!l}else if(this.object.isOrthographicCamera){let a=new N(this._mouse.x,this._mouse.y,0);a.unproject(this.object);let l=this.object.zoom;this.object.zoom=Math.max(this.minZoom,Math.min(this.maxZoom,this.object.zoom/this._scale)),this.object.updateProjectionMatrix(),r=l!==this.object.zoom;let c=new N(this._mouse.x,this._mouse.y,0);c.unproject(this.object),this.object.position.sub(c).add(a),this.object.updateMatrixWorld(),o=Lt.length()}else console.warn("WARNING: OrbitControls.js encountered an unknown camera type - zoom to cursor disabled."),this.zoomToCursor=!1;o!==null&&(this.screenSpacePanning?this.target.set(0,0,-1).transformDirection(this.object.matrix).multiplyScalar(o).add(this.object.position):(Al.origin.copy(this.object.position),Al.direction.set(0,0,-1).transformDirection(this.object.matrix),Math.abs(this.object.up.dot(Al.direction))Hh||8*(1-this._lastQuaternion.dot(this.object.quaternion))>Hh||this._lastTargetPosition.distanceToSquared(this.target)>Hh?(this.dispatchEvent(wp),this._lastPosition.copy(this.object.position),this._lastQuaternion.copy(this.object.quaternion),this._lastTargetPosition.copy(this.target),!0):!1}_getAutoRotationAngle(e){return e!==null?tn/60*this.autoRotateSpeed*e:tn/60/60*this.autoRotateSpeed}_getZoomScale(e){let t=Math.abs(e*.01);return Math.pow(.95,this.zoomSpeed*t)}_rotateLeft(e){this._sphericalDelta.theta-=e}_rotateUp(e){this._sphericalDelta.phi-=e}_panLeft(e,t){Lt.setFromMatrixColumn(t,0),Lt.multiplyScalar(-e),this._panOffset.add(Lt)}_panUp(e,t){this.screenSpacePanning===!0?Lt.setFromMatrixColumn(t,1):(Lt.setFromMatrixColumn(t,0),Lt.crossVectors(this.object.up,Lt)),Lt.multiplyScalar(e),this._panOffset.add(Lt)}_pan(e,t){let n=this.domElement;if(this.object.isPerspectiveCamera){let s=this.object.position;Lt.copy(s).sub(this.target);let r=Lt.length();r*=Math.tan(this.object.fov/2*Math.PI/180),this._panLeft(2*e*r/n.clientHeight,this.object.matrix),this._panUp(2*t*r/n.clientHeight,this.object.matrix)}else this.object.isOrthographicCamera?(this._panLeft(e*(this.object.right-this.object.left)/this.object.zoom/n.clientWidth,this.object.matrix),this._panUp(t*(this.object.top-this.object.bottom)/this.object.zoom/n.clientHeight,this.object.matrix)):(console.warn("WARNING: OrbitControls.js encountered an unknown camera type - pan disabled."),this.enablePan=!1)}_dollyOut(e){this.object.isPerspectiveCamera||this.object.isOrthographicCamera?this._scale/=e:(console.warn("WARNING: OrbitControls.js encountered an unknown camera type - dolly/zoom disabled."),this.enableZoom=!1)}_dollyIn(e){this.object.isPerspectiveCamera||this.object.isOrthographicCamera?this._scale*=e:(console.warn("WARNING: OrbitControls.js encountered an unknown camera type - dolly/zoom disabled."),this.enableZoom=!1)}_updateZoomParameters(e,t){if(!this.zoomToCursor)return;this._performCursorZoom=!0;let n=this.domElement.getBoundingClientRect(),s=e-n.left,r=t-n.top,o=n.width,a=n.height;this._mouse.x=s/o*2-1,this._mouse.y=-(r/a)*2+1,this._dollyDirection.set(this._mouse.x,this._mouse.y,1).unproject(this.object).sub(this.object.position).normalize()}_clampDistance(e){return Math.max(this.minDistance,Math.min(this.maxDistance,e))}_handleMouseDownRotate(e){this._rotateStart.set(e.clientX,e.clientY)}_handleMouseDownDolly(e){this._updateZoomParameters(e.clientX,e.clientX),this._dollyStart.set(e.clientX,e.clientY)}_handleMouseDownPan(e){this._panStart.set(e.clientX,e.clientY)}_handleMouseMoveRotate(e){this._rotateEnd.set(e.clientX,e.clientY),this._rotateDelta.subVectors(this._rotateEnd,this._rotateStart).multiplyScalar(this.rotateSpeed);let t=this.domElement;this._rotateLeft(tn*this._rotateDelta.x/t.clientHeight),this._rotateUp(tn*this._rotateDelta.y/t.clientHeight),this._rotateStart.copy(this._rotateEnd),this.update()}_handleMouseMoveDolly(e){this._dollyEnd.set(e.clientX,e.clientY),this._dollyDelta.subVectors(this._dollyEnd,this._dollyStart),this._dollyDelta.y>0?this._dollyOut(this._getZoomScale(this._dollyDelta.y)):this._dollyDelta.y<0&&this._dollyIn(this._getZoomScale(this._dollyDelta.y)),this._dollyStart.copy(this._dollyEnd),this.update()}_handleMouseMovePan(e){this._panEnd.set(e.clientX,e.clientY),this._panDelta.subVectors(this._panEnd,this._panStart).multiplyScalar(this.panSpeed),this._pan(this._panDelta.x,this._panDelta.y),this._panStart.copy(this._panEnd),this.update()}_handleMouseWheel(e){this._updateZoomParameters(e.clientX,e.clientY),e.deltaY<0?this._dollyIn(this._getZoomScale(e.deltaY)):e.deltaY>0&&this._dollyOut(this._getZoomScale(e.deltaY)),this.update()}_handleKeyDown(e){let t=!1;switch(e.code){case this.keys.UP:e.ctrlKey||e.metaKey||e.shiftKey?this.enableRotate&&this._rotateUp(tn*this.keyRotateSpeed/this.domElement.clientHeight):this.enablePan&&this._pan(0,this.keyPanSpeed),t=!0;break;case this.keys.BOTTOM:e.ctrlKey||e.metaKey||e.shiftKey?this.enableRotate&&this._rotateUp(-tn*this.keyRotateSpeed/this.domElement.clientHeight):this.enablePan&&this._pan(0,-this.keyPanSpeed),t=!0;break;case this.keys.LEFT:e.ctrlKey||e.metaKey||e.shiftKey?this.enableRotate&&this._rotateLeft(tn*this.keyRotateSpeed/this.domElement.clientHeight):this.enablePan&&this._pan(this.keyPanSpeed,0),t=!0;break;case this.keys.RIGHT:e.ctrlKey||e.metaKey||e.shiftKey?this.enableRotate&&this._rotateLeft(-tn*this.keyRotateSpeed/this.domElement.clientHeight):this.enablePan&&this._pan(-this.keyPanSpeed,0),t=!0;break}t&&(e.preventDefault(),this.update())}_handleTouchStartRotate(e){if(this._pointers.length===1)this._rotateStart.set(e.pageX,e.pageY);else{let t=this._getSecondPointerPosition(e),n=.5*(e.pageX+t.x),s=.5*(e.pageY+t.y);this._rotateStart.set(n,s)}}_handleTouchStartPan(e){if(this._pointers.length===1)this._panStart.set(e.pageX,e.pageY);else{let t=this._getSecondPointerPosition(e),n=.5*(e.pageX+t.x),s=.5*(e.pageY+t.y);this._panStart.set(n,s)}}_handleTouchStartDolly(e){let t=this._getSecondPointerPosition(e),n=e.pageX-t.x,s=e.pageY-t.y,r=Math.sqrt(n*n+s*s);this._dollyStart.set(0,r)}_handleTouchStartDollyPan(e){this.enableZoom&&this._handleTouchStartDolly(e),this.enablePan&&this._handleTouchStartPan(e)}_handleTouchStartDollyRotate(e){this.enableZoom&&this._handleTouchStartDolly(e),this.enableRotate&&this._handleTouchStartRotate(e)}_handleTouchMoveRotate(e){if(this._pointers.length==1)this._rotateEnd.set(e.pageX,e.pageY);else{let n=this._getSecondPointerPosition(e),s=.5*(e.pageX+n.x),r=.5*(e.pageY+n.y);this._rotateEnd.set(s,r)}this._rotateDelta.subVectors(this._rotateEnd,this._rotateStart).multiplyScalar(this.rotateSpeed);let t=this.domElement;this._rotateLeft(tn*this._rotateDelta.x/t.clientHeight),this._rotateUp(tn*this._rotateDelta.y/t.clientHeight),this._rotateStart.copy(this._rotateEnd)}_handleTouchMovePan(e){if(this._pointers.length===1)this._panEnd.set(e.pageX,e.pageY);else{let t=this._getSecondPointerPosition(e),n=.5*(e.pageX+t.x),s=.5*(e.pageY+t.y);this._panEnd.set(n,s)}this._panDelta.subVectors(this._panEnd,this._panStart).multiplyScalar(this.panSpeed),this._pan(this._panDelta.x,this._panDelta.y),this._panStart.copy(this._panEnd)}_handleTouchMoveDolly(e){let t=this._getSecondPointerPosition(e),n=e.pageX-t.x,s=e.pageY-t.y,r=Math.sqrt(n*n+s*s);this._dollyEnd.set(0,r),this._dollyDelta.set(0,Math.pow(this._dollyEnd.y/this._dollyStart.y,this.zoomSpeed)),this._dollyOut(this._dollyDelta.y),this._dollyStart.copy(this._dollyEnd);let o=(e.pageX+t.x)*.5,a=(e.pageY+t.y)*.5;this._updateZoomParameters(o,a)}_handleTouchMoveDollyPan(e){this.enableZoom&&this._handleTouchMoveDolly(e),this.enablePan&&this._handleTouchMovePan(e)}_handleTouchMoveDollyRotate(e){this.enableZoom&&this._handleTouchMoveDolly(e),this.enableRotate&&this._handleTouchMoveRotate(e)}_addPointer(e){this._pointers.push(e.pointerId)}_removePointer(e){delete this._pointerPositions[e.pointerId];for(let t=0;t{this.dom.focus(),this.controls.mouseButtons.LEFT=n.metaKey||n.shiftKey||n.ctrlKey?Cn.PAN:Cn.ROTATE,this.markInput()},t=()=>this.markInput();this.dom.addEventListener("pointerdown",e,{capture:!0}),this.dom.addEventListener("wheel",t,{passive:!0}),this.dom.addEventListener("touchstart",t,{passive:!0}),this.disposeFns.push(()=>{this.dom.removeEventListener("pointerdown",e,{capture:!0}),this.dom.removeEventListener("wheel",t),this.dom.removeEventListener("touchstart",t)})}bindKeys(){let e=s=>{let r=s.key.toLowerCase();this.shiftHeld=s.shiftKey,Hy.has(r)?(this.pressed.add(r),this.markInput(),s.preventDefault(),s.stopPropagation()):r==="f"?(this.hooks.onFlyToSelected(),s.preventDefault()):r==="r"&&(this.hooks.onResetView(),s.preventDefault())},t=s=>{this.shiftHeld=s.shiftKey,this.pressed.delete(s.key.toLowerCase())},n=()=>this.pressed.clear();this.dom.addEventListener("keydown",e),this.dom.addEventListener("keyup",t),this.dom.addEventListener("blur",n),this.disposeFns.push(()=>{this.dom.removeEventListener("keydown",e),this.dom.removeEventListener("keyup",t),this.dom.removeEventListener("blur",n)})}applyFly(e){if(this.pressed.size===0)return!1;let t=this.camera.position.distanceTo(this.controls.target),n=Math.min(Math.max(t*.8,10),600)*(this.shiftHeld?3:1),s=this.camera.getWorldDirection(this.tmpDir);this.tmpRight.crossVectors(s,this.camera.up).normalize();let r=new N;return this.pressed.has("w")&&r.add(s),this.pressed.has("s")&&r.sub(s),this.pressed.has("d")&&r.add(this.tmpRight),this.pressed.has("a")&&r.sub(this.tmpRight),this.pressed.has("e")&&(r.y+=1),this.pressed.has("q")&&(r.y-=1),r.lengthSq()<1e-8?!1:(r.normalize().multiplyScalar(n*e),this.camera.position.add(r),this.controls.target.add(r),this.lastInputAt=performance.now(),!0)}setInitialFraming(e){this.camera.position.copy(this.framingPosition(e)),this.controls.target.set(0,0,0),this.controls.update()}framingPosition(e){let t=e*2.2,n=18*Math.PI/180;return new N(t*Math.cos(n),t*Math.sin(n),t*.35)}resetView(e,t){this.startTween(this.framingPosition(e),new N(0,0,0),1200,t)}beginFocusOrbit(e){this.pendingDensityDir=e,this.cruiseAnchor=null,this.lastInputAt=performance.now()-Dn.resumeDelayMs-1}flyTo(e,t,n){let s=Math.min(Math.max(t*Xn.distancePerRadius,Xn.minDistance),Xn.maxDistance),r=this.camera.position.clone().sub(e);r.lengthSq()<1e-6&&r.set(0,0,1),this.tmpSph.setFromVector3(r),this.tmpSph.theta+=Xn.azimuthOffsetRad,this.tmpSph.radius=s;let o=e.clone().add(new N().setFromSpherical(this.tmpSph)),a=this.camera.position.distanceTo(o),l=Math.min(Math.max(Xn.minMs+Xn.msPerWorldUnit*a,Xn.minMs),Xn.maxMs);this.startTween(o,e.clone(),l,n)}startTween(e,t,n,s){this.tween={t0:performance.now(),durMs:n,fromPos:this.camera.position.clone(),toPos:e,fromTarget:this.controls.target.clone(),toTarget:t},s&&(this.tween.onDone=s)}update(e,t){if(this.tween){let r=this.tween,o=Math.min((e-r.t0)/r.durMs,1),a=Gy(o);return this.camera.position.lerpVectors(r.fromPos,r.toPos,a),this.controls.target.lerpVectors(r.fromTarget,r.toTarget,a),this.controls.update(),o>=1&&(this.tween=null,r.onDone?.(),this.lastInputAt=e),!1}let n=this.applyFly(t),s=e-this.lastInputAt;if(!n&&this.cruiseEnabled&&s>Dn.resumeDelayMs){let r=Math.min((s-Dn.resumeDelayMs)/Dn.rampUpMs,1);if(!this.cruiseAnchor){if(this.cruiseAnchor=new dn().setFromVector3(this.tmpOffset.copy(this.camera.position).sub(this.controls.target)),this.cruiseT=0,this.cruiseDir=1,this.pendingDensityDir&&this.pendingDensityDir.lengthSq()>1e-6){let d=new dn().setFromVector3(this.pendingDensityDir).theta-this.cruiseAnchor.theta;for(;d>Math.PI;)d-=2*Math.PI;for(;d<-Math.PI;)d+=2*Math.PI;this.cruiseDir=d>=0?1:-1}this.pendingDensityDir=null}this.cruiseT+=t*r;let o=this.cruiseT,a=this.cruiseAnchor,l=Dn.elevationDeg*Math.PI/180*Math.sin(2*Math.PI*o/Dn.elevationPeriodS),c=1+Dn.radiusBreath*Math.sin(2*Math.PI*o/Dn.radiusPeriodS);return this.tmpSph.radius=a.radius*c,this.tmpSph.theta=a.theta+this.cruiseDir*Dn.angularSpeed*this.cruiseSpeed*o,this.tmpSph.phi=Math.min(Math.max(a.phi+l,.05),Math.PI-.05),this.camera.position.setFromSpherical(this.tmpSph).add(this.controls.target),this.camera.lookAt(this.controls.target),!0}return this.controls.update(),!1}dispose(){this.tween=null,this.pressed.clear();for(let e of this.disposeFns)e();this.disposeFns=[],this.controls.dispose()}};var Ap=[{id:"galaxy",name:"\u94F6\u6CB3",bloom:{strength:.35,radius:.35,threshold:.22},physics:{repel:200,linkDistance:70,linkStrength:1,centerPull:.04,flatten:.3},look:{nodeSize:1,linkOpacity:.14,twinkle:.5,sizeBy:"degree"}},{id:"nebula",name:"\u661F\u4E91",bloom:{strength:.6,radius:.4,threshold:.18},physics:{repel:180,linkDistance:80,linkStrength:1,centerPull:.04,flatten:0},look:{nodeSize:1,linkOpacity:.16,twinkle:.5,sizeBy:"degree"}},{id:"minimal",name:"\u6781\u7B80",bloom:{strength:0,radius:.3,threshold:.3},physics:{repel:220,linkDistance:80,linkStrength:1,centerPull:.04,flatten:0},look:{nodeSize:.85,linkOpacity:.08,twinkle:.2,sizeBy:"degree"}},{id:"fireworks",name:"\u70DF\u706B",bloom:{strength:1.2,radius:.6,threshold:.1},physics:{repel:160,linkDistance:60,linkStrength:1.2,centerPull:.05,flatten:0},look:{nodeSize:1.15,linkOpacity:.25,twinkle:1.2,sizeBy:"degree"}}];var Pl=[{id:"hubble",name:"\u54C8\u52C3\u6DF1\u7A7A",colors:["#46d4dc","#ffc35c","#d05a32","#7fd0a0","#e8d9a0","#5a9bd8","#d87fa8","#9a7fe0","#cfd8e8"]},{id:"tiktok",name:"\u6296\u97F3\u9713\u8679",colors:["#25f4ee","#fe2c55","#ffffff","#7ae8e2","#ff7a9c","#19b8b2","#c2244a","#a8f0ec","#ffd0dc"]},{id:"sunset",name:"\u843D\u65E5\u80F6\u7247",colors:["#f58529","#dd2a7b","#8134af","#515bd4","#feda77","#e1306c","#c13584","#fd8d32","#405de6"]},{id:"cyber",name:"\u8D5B\u535A\u90FD\u5E02",colors:["#fcee0a","#00f0ff","#ff003c","#9d00ff","#00ff9f","#ff6ec7","#3df5ff","#ffe600","#c800ff"]},{id:"matrix",name:"\u9ED1\u5BA2\u5E1D\u56FD",colors:["#00ff41","#33ff66","#00cc34","#66ff8c","#00b32d","#80ffa0","#1aff4d","#00e639","#4dff79"]},{id:"aurora",name:"\u6781\u5149",colors:["#1db954","#00d4ff","#7f5fff","#38f0c0","#4fa8ff","#9f7fff","#22e6a8","#66c2ff","#b08fff"]}];var Jt=class{constructor(e,t){this.spec=t;let n=e.createDiv({cls:"gx-slider"}),s=n.createDiv({cls:"gx-slider-head"});s.createSpan({cls:"gx-slider-label",text:t.label}),this.valueEl=s.createSpan({cls:"gx-slider-value"}),this.trackEl=n.createDiv({cls:"gx-slider-track"}),this.trackEl.createDiv({cls:"gx-slider-rail"}),this.fillEl=this.trackEl.createDiv({cls:"gx-slider-fill"}),this.trackEl.createDiv({cls:"gx-slider-notch",attr:{title:`${t.defaultLabel??"Default"} ${this.fmt(t.defaultValue)}`}}),this.thumbEl=this.trackEl.createDiv({cls:"gx-slider-thumb"});let r=n.createDiv({cls:"gx-slider-bounds"});r.createSpan({text:this.fmt(t.min)}),r.createSpan({text:this.fmt(t.max)}),this.bindDrag(),this.trackEl.addEventListener("dblclick",()=>{t.set(t.defaultValue),this.refresh(),t.onInput()}),this.refresh()}fmt(e){return this.spec.fmt?this.spec.fmt(e):e.toFixed(2)}posToValue(e){let{min:t,max:n,defaultValue:s,step:r}=this.spec,o=e<=.5?t+(s-t)*(e/.5):s+(n-s)*((e-.5)/.5),a=Math.round(o/r)*r;return Math.min(Math.max(a,t),n)}valueToPos(e){let{min:t,max:n,defaultValue:s}=this.spec;return e<=s?s===t?0:.5*(e-t)/(s-t):n===s?1:.5+.5*(e-s)/(n-s)}bindDrag(){let e=t=>{let n=this.trackEl.getBoundingClientRect(),s=Math.min(Math.max((t.clientX-n.left)/n.width,0),1);this.spec.set(this.posToValue(s)),this.refresh(),this.spec.onInput()};this.trackEl.addEventListener("pointerdown",t=>{t.preventDefault(),this.trackEl.setPointerCapture(t.pointerId),e(t);let n=r=>e(r),s=r=>{this.trackEl.releasePointerCapture(r.pointerId),this.trackEl.removeEventListener("pointermove",n),this.trackEl.removeEventListener("pointerup",s)};this.trackEl.addEventListener("pointermove",n),this.trackEl.addEventListener("pointerup",s)})}refresh(){let e=this.spec.get(),t=this.valueToPos(e);this.thumbEl.style.left=`${(t*100).toFixed(2)}%`;let n=Math.min(t,.5),s=Math.abs(t-.5);this.fillEl.style.left=`${(n*100).toFixed(2)}%`,this.fillEl.style.width=`${(s*100).toFixed(2)}%`,this.valueEl.setText(this.fmt(e)),this.valueEl.toggleClass("is-default",Math.abs(e-this.spec.defaultValue){let l=this.body.hasClass("is-hidden");this.body.toggleClass("is-hidden",!l),a.setText(l?"-":"+")}),this.render()}render(){this.body.empty(),this.sliders=[],this.styleChips=[],this.cruiseBtn=null,this.presetBtn=null,this.unresolvedBtn=null,this.orphanBtn=null,this.sizeByBtn=null,this.qualityBtn=null;let e=this.body.createDiv({cls:"mwm-mode-switch"});e.createDiv({cls:"mwm-mode-switch-label",text:this.tt("view.mode")});let t=e.createDiv({cls:"galaxy-mode-row mwm-mode-row"});this.modeButton(t,"radial2d"),this.modeButton(t,"map3d");let n=this.body.createDiv({cls:"mwm-panel-settings mwm-3d-settings"}),s=n.createDiv({cls:"mwm-panel-tabs"});for(let[o,a]of[["view",this.tt("tab.view")],["appearance",this.tt("tab.appearance")],["physics",this.tt("tab.physics")],["motion",this.tt("tab.motion")],["advanced",this.tt("tab.advanced")]])s.createEl("button",{cls:this.page===o?"is-active":"",text:a}).addEventListener("click",()=>{this.page=o,this.render()});let r=n.createDiv({cls:"mwm-panel-page"});this.page==="appearance"?this.renderAppearancePage(r):this.page==="physics"?this.renderPhysicsPage(r):this.page==="motion"?this.renderMotionPage(r):this.page==="advanced"?this.renderAdvancedPage(r):this.renderViewPage(r)}renderViewPage(e){let t=e.createDiv({cls:"galaxy-panel-row"});t.createEl("button",{text:this.tt("common.search")}).addEventListener("click",this.cb.onSearch),t.createEl("button",{text:this.tt("common.recenter")}).addEventListener("click",this.cb.onRecenter),t.createEl("button",{text:this.tt("3d.reveal")}).addEventListener("click",this.cb.onReveal)}renderAppearancePage(e){let t=this.settings,n=xn,s=e.createDiv({cls:"galaxy-panel-row"});this.presetBtn=s.createEl("button",{text:this.presetLabel()}),this.presetBtn.addEventListener("click",()=>{t.preset=t.preset==="deep-space"?"adaptive":"deep-space",this.presetBtn?.setText(this.presetLabel()),this.cb.onPreset()});let r=e.createDiv({cls:"gx-chips"});for(let h of Ap){let f=r.createEl("button",{cls:"gx-chip",text:this.tt(`style.${h.id}`)});f.addEventListener("click",()=>{this.cb.onStylePreset(h),this.refreshAll(),this.markActiveChip(h.id)}),f.dataset.presetId=h.id,this.styleChips.push(f)}this.sliders.push(new Jt(e,this.slider("3d.nodeSize",.3,2.5,.05,n.look.nodeSize,()=>t.look.nodeSize,h=>t.look.nodeSize=h,Xh("x"),this.cb.onLook)),new Jt(e,this.slider("3d.linkOpacity",0,.6,.01,n.look.linkOpacity,()=>t.look.linkOpacity,h=>t.look.linkOpacity=h,void 0,this.cb.onLook)),new Jt(e,this.slider("3d.twinkle",0,2,.1,n.look.twinkle,()=>t.look.twinkle,h=>t.look.twinkle=h,h=>h<.05?this.tt("3d.twinkleOff"):h.toFixed(1),this.cb.onLook)),new Jt(e,this.slider("3d.glowStrength",0,2.5,.05,n.bloom.strength,()=>t.bloom.strength,h=>t.bloom.strength=h,void 0,this.cb.onBloom)),new Jt(e,this.slider("3d.glowRadius",0,1.2,.05,n.bloom.radius,()=>t.bloom.radius,h=>t.bloom.radius=h,void 0,this.cb.onBloom)),new Jt(e,this.slider("3d.glowThreshold",0,1,.05,n.bloom.threshold,()=>t.bloom.threshold,h=>t.bloom.threshold=h,void 0,this.cb.onBloom)));let o=e.createDiv({cls:"galaxy-panel-row"});this.sizeByBtn=o.createEl("button",{text:this.sizeByLabel()}),this.sizeByBtn.addEventListener("click",()=>{let h=["degree","fileSize","uniform"];t.look.sizeBy=h[(h.indexOf(t.look.sizeBy)+1)%h.length]??"degree",this.sizeByBtn?.setText(this.sizeByLabel()),this.cb.onSizeBy()});let a=e.createEl("select",{cls:"gx-theme-select"}),l=a.createEl("option",{text:this.tt("3d.colorTheme"),value:""});l.disabled=!0;for(let h of Pl)a.createEl("option",{text:this.tt(`color.${h.id}`),value:h.id});a.value=Pl.some(h=>h.id===t.colorTheme)?t.colorTheme:"",a.value||(l.selected=!0),a.addEventListener("change",()=>{let h=Pl.find(f=>f.id===a.value);h&&this.cb.onColorTheme(h)});let c=e.createDiv({cls:"galaxy-panel-row"});c.createEl("button",{text:this.tt("3d.importColors")}).addEventListener("click",()=>{this.cb.onImportColors(),l.selected=!0}),c.createEl("button",{text:this.tt("3d.shuffleColors")}).addEventListener("click",()=>{this.cb.onShuffleColors(),l.selected=!0})}renderPhysicsPage(e){let t=this.settings,n=xn;this.sliders.push(new Jt(e,this.slider("3d.repel",20,400,5,n.physics.repel,()=>t.physics.repel,s=>t.physics.repel=s,s=>String(Math.round(s)),this.cb.onPhysics)),new Jt(e,this.slider("3d.linkDistance",20,200,5,n.physics.linkDistance,()=>t.physics.linkDistance,s=>t.physics.linkDistance=s,s=>String(Math.round(s)),this.cb.onPhysics)),new Jt(e,this.slider("3d.linkStrength",.1,2,.1,n.physics.linkStrength,()=>t.physics.linkStrength,s=>t.physics.linkStrength=s,Xh("x1"),this.cb.onPhysics)),new Jt(e,this.slider("3d.centerPull",0,.2,.005,n.physics.centerPull,()=>t.physics.centerPull,s=>t.physics.centerPull=s,s=>s.toFixed(3),this.cb.onPhysics)),new Jt(e,this.slider("3d.flatten",0,.8,.02,n.physics.flatten,()=>t.physics.flatten,s=>t.physics.flatten=s,void 0,this.cb.onPhysics)))}renderMotionPage(e){let t=this.settings,n=xn,s=e.createDiv({cls:"galaxy-panel-row"});this.cruiseBtn=s.createEl("button",{text:this.cruiseLabel()}),this.cruiseBtn.addEventListener("click",()=>{t.cruise=!t.cruise,this.cruiseBtn?.setText(this.cruiseLabel()),this.cb.onCruise(t.cruise)}),this.sliders.push(new Jt(e,this.slider("3d.speed",.2,3,.1,n.cruiseSpeed,()=>t.cruiseSpeed,o=>t.cruiseSpeed=o,Xh("x1"),this.cb.onCruiseSpeed)));let r=e.createDiv({cls:"galaxy-panel-help"});for(let o of["3d.help.drag","3d.help.pan","3d.help.mac","3d.help.fly","3d.help.pick","3d.help.keys","3d.help.slider"])r.createDiv({text:this.tt(o)})}renderAdvancedPage(e){let t=this.settings,n=e.createDiv({cls:"galaxy-panel-row"});this.unresolvedBtn=n.createEl("button",{text:this.unresolvedLabel()}),this.unresolvedBtn.addEventListener("click",()=>{t.showUnresolved=!t.showUnresolved,this.unresolvedBtn?.setText(this.unresolvedLabel()),this.cb.onShowUnresolved(t.showUnresolved)}),this.orphanBtn=n.createEl("button",{text:this.orphanLabel()}),this.orphanBtn.addEventListener("click",()=>{t.showOrphans=!t.showOrphans,this.orphanBtn?.setText(this.orphanLabel()),this.cb.onShowOrphans(t.showOrphans)});let s=e.createDiv({cls:"galaxy-panel-row"});this.qualityBtn=s.createEl("button",{text:this.qualityLabel()}),this.qualityBtn.addEventListener("click",()=>{let o=["auto","high","low","mobile"];t.qualityOverride=o[(o.indexOf(t.qualityOverride)+1)%o.length]??"auto",this.qualityBtn?.setText(this.qualityLabel()),this.cb.onQuality()}),s.createEl("button",{text:this.tt("common.resetDefaults")}).addEventListener("click",()=>{this.cb.onReset(),this.refreshAll()})}modeButton(e,t){e.createEl("button",{cls:this.viewMode===t?"is-active":"",text:ui(this.language,t)}).addEventListener("click",()=>this.cb.onViewMode?.(t))}slider(e,t,n,s,r,o,a,l,c){return{label:this.tt(e),min:t,max:n,step:s,defaultValue:r,get:o,set:a,fmt:l,onInput:c,defaultLabel:this.tt("common.default")}}presetLabel(){return`${this.tt("view.theme")}: ${this.settings.preset==="deep-space"?this.tt("theme.deep"):this.tt("theme.auto")}`}cruiseLabel(){return this.settings.cruise?this.tt("3d.cruiseOn"):this.tt("3d.cruiseOff")}unresolvedLabel(){return this.settings.showUnresolved?this.tt("3d.unresolvedShow"):this.tt("3d.unresolvedHide")}orphanLabel(){return this.settings.showOrphans?this.tt("3d.orphansShow"):this.tt("3d.orphansHide")}sizeByLabel(){return this.tt(`3d.size.${this.settings.look.sizeBy}`)}qualityLabel(){return this.tt(`3d.quality.${this.settings.qualityOverride}`)}markActiveChip(e){for(let t of this.styleChips)t.toggleClass("is-active",t.dataset.presetId===e)}refreshAll(){for(let e of this.sliders)e.refresh();this.cruiseBtn?.setText(this.cruiseLabel()),this.presetBtn?.setText(this.presetLabel()),this.unresolvedBtn?.setText(this.unresolvedLabel()),this.orphanBtn?.setText(this.orphanLabel()),this.sizeByBtn?.setText(this.sizeByLabel()),this.qualityBtn?.setText(this.qualityLabel())}setLanguage(e){this.language=e,this.render()}setPanelTheme(e){this.root.removeClass("gx-theme-dark"),this.root.removeClass("gx-theme-light"),this.root.addClass(e)}dispose(){this.root.remove(),this.sliders=[],this.styleChips=[]}tt(e,t={}){return Ce(this.language,e,t)}};function Xh(i){return e=>i==="x1"?`${e.toFixed(1)}x`:`${e.toFixed(2)}x`}var Nl=require("obsidian");var Ll=class{constructor(e,t,n,s,r="en"){this.app=t;this.renderer=n;this.cb=s;this.language=r;this.hubEls=[];this.neighborEls=[];this.hoverIndex=-1;this.cardIndex=-1;this.data={nodes:[],links:[]};this.graphRadius=200;this.snippetToken=0;this.hubBudget=14;this.neighborBudget=20;this.mobileCard=!1;this.root=e.createDiv({cls:"gx-overlay"}),this.hoverEl=this.root.createDiv({cls:"gx-label gx-label-hover"}),this.hoverEl.hide(),this.card=this.root.createDiv({cls:"gx-card"}),this.card.hide()}setLanguage(e){if(this.language=e,this.cardIndex>=0){let t=this.data.nodes[this.cardIndex];t&&this.buildCard(t,this.cardIndex)}}setBudgets(e,t,n){this.hubBudget=e,this.neighborBudget=t,this.mobileCard=n,this.setData(this.data,this.graphRadius)}setData(e,t){this.data=e,this.graphRadius=t;for(let n of this.hubEls)n.el.remove();this.hubEls=[...e.nodes.entries()].filter(([,n])=>!n.unresolved).sort((n,s)=>s[1].degree-n[1].degree).slice(0,this.hubBudget).map(([n,s])=>({index:n,el:this.root.createDiv({cls:"gx-label gx-label-hub",text:s.name})})),this.setHover(-1),this.setSelection(-1,new Set)}setHover(e){if(this.hoverIndex=e,e<0){this.hoverEl.hide();return}let t=this.data.nodes[e];t&&(this.hoverEl.setText(t.name),this.hoverEl.show())}refreshBottomInset(){let e=0,t=activeDocument.querySelector(".mobile-navbar");if(t){let n=t.getBoundingClientRect(),s=this.root.getBoundingClientRect();e=Math.max(0,Math.round(s.bottom-n.top))}this.root.setCssProps({"--gx-bottom-inset":`${e}px`})}setSelection(e,t){for(let r of this.neighborEls)r.el.remove();if(this.neighborEls=[],this.cardIndex=e,e<0){this.card.hide();return}let n=[...t].filter(r=>r!==e).sort((r,o)=>(this.data.nodes[o]?.degree??0)-(this.data.nodes[r]?.degree??0)).slice(0,this.neighborBudget);this.neighborEls=n.map(r=>({index:r,el:this.root.createDiv({cls:"gx-label gx-label-neighbor",text:this.data.nodes[r]?.name??""})}));let s=this.data.nodes[e];s&&(this.mobileCard&&(this.refreshBottomInset(),this.card.style.removeProperty("transform")),this.buildCard(s,e))}buildCard(e,t){this.card.empty(),this.card.show(),this.card.createDiv({cls:"gx-card-title",text:e.name});let n=this.card.createDiv({cls:"gx-card-meta"}),s=n.createSpan({cls:"gx-card-dot"});s.style.background=this.renderer.nodeColorHex(t),n.createSpan({text:e.unresolved?this.tt("3d.card.unresolved"):e.id.includes("/")?e.id.slice(0,e.id.lastIndexOf("/")):this.tt("3d.card.root")});let r=e.unresolved?null:this.app.vault.getAbstractFileByPath(e.id),o=r instanceof Nl.TFile?r:null;if(o){let u=this.app.metadataCache.getFileCache(o),d=u?(0,Nl.getAllTags)(u)??[]:[];if(d.length>0){let h=this.card.createDiv({cls:"gx-card-tags"});for(let f of d.slice(0,5))h.createSpan({cls:"gx-card-tag",text:f})}}if(this.card.createDiv({cls:"gx-card-stats"}).setText(this.tt("3d.card.stats",{in:e.inDegree,out:e.outDegree})+(o?this.tt("3d.card.modified",{date:new Date(o.stat.mtime).toLocaleDateString(this.language==="zh"?"zh-CN":"en-US")}):"")),o){let u=this.card.createDiv({cls:"gx-card-snippet",text:"\u2026"}),d=++this.snippetToken;this.app.vault.cachedRead(o).then(h=>{d===this.snippetToken&&u.setText(Wy(h).slice(0,120)||this.tt("3d.card.empty"))})}let l=this.card.createDiv({cls:"gx-card-actions"});e.unresolved||l.createEl("button",{text:this.tt("context.openNote")}).addEventListener("click",()=>this.cb.openNote(e.id)),l.createEl("button",{text:this.tt("common.focus")}).addEventListener("click",()=>this.cb.focusNode(t))}update(e,t){let n=this.graphRadius*2.6,s=this.graphRadius*1.2;for(let{index:r,el:o}of this.hubEls){let a=this.renderer.projectNode(r,e,t);if(a.behind||a.x<0||a.x>e||a.y<0||a.y>t){o.setCssProps({opacity:"0"});continue}let l=this.renderer.cameraDistanceTo(r),c=Math.min(Math.max((n-l)/(n-s),0),1);o.style.opacity=c.toFixed(2),o.style.transform=`translate3d(${a.x.toFixed(1)}px, ${(a.y-14).toFixed(1)}px, 0)`}for(let{index:r,el:o}of this.neighborEls){let a=this.renderer.projectNode(r,e,t);o.style.opacity=a.behind?"0":"0.85",a.behind||(o.style.transform=`translate3d(${a.x.toFixed(1)}px, ${(a.y-12).toFixed(1)}px, 0)`)}if(this.hoverIndex>=0){let r=this.renderer.projectNode(this.hoverIndex,e,t);r.behind||(this.hoverEl.style.transform=`translate3d(${r.x.toFixed(1)}px, ${(r.y-18).toFixed(1)}px, 0)`)}if(this.cardIndex>=0&&!this.mobileCard){let r=this.renderer.projectNode(this.cardIndex,e,t);if(!r.behind){let a=r.x+296>e?r.x-296:r.x+16,l=Math.min(Math.max(r.y-40,12),Math.max(t-this.card.clientHeight-12,12));this.card.style.transform=`translate3d(${a.toFixed(1)}px, ${l.toFixed(1)}px, 0)`}}}dispose(){this.root.remove(),this.hubEls=[],this.neighborEls=[]}tt(e,t={}){return Ce(this.language,e,t)}};function Wy(i){return i.replace(/^---\n[\s\S]*?\n---\n?/,"").replace(/!?\[\[([^\]|]+)(\|[^\]]+)?\]\]/g,"$1").replace(/\[([^\]]*)\]\([^)]*\)/g,"$1").replace(/[#*`>~_]|---/g,"").replace(/\s+/g," ").trim()}var Fl=require("obsidian");var Dl=class extends Fl.SuggestModal{constructor(t,n,s,r="en"){super(t);this.nodes=n;this.onPick=s;this.language=r;this.setPlaceholder(this.tt("3d.searchPlaceholder"))}getSuggestions(t){let n=t.trim();if(!n)return[...this.nodes.entries()].filter(([,o])=>!o.unresolved).sort((o,a)=>a[1].degree-o[1].degree).slice(0,20).map(([o,a])=>({index:o,node:a,score:0}));let s=(0,Fl.prepareFuzzySearch)(n),r=[];for(let o=0;oa.score-o.score||a.node.degree-o.node.degree).slice(0,50)}renderSuggestion(t,n){n.createDiv({text:t.node.name}),n.createDiv({cls:"gx-search-path",text:`${t.node.unresolved?this.tt("3d.searchUnresolved"):t.node.id} \xB7 ${this.tt("3d.searchLinks",{count:t.node.degree})}`})}onChooseSuggestion(t){this.onPick(t.index)}tt(t,n={}){return Ce(this.language,t,n)}};var qh=require("obsidian"),Cp="_galaxy_bench";function Rp(i,e){return new Promise(t=>{let n=[],s=performance.now(),r=s,o=a=>{n.push(a-s),s=a;let l=a-r;if(e?.(l),lh-f),u=n.reduce((h,f)=>h+f,0),d=c[Math.min(Math.floor(c.length*.95),c.length-1)]??0;t({frames:n.length,avgFps:n.length>0?1e3/(u/n.length):0,p95FrameMs:d,worstFrameMs:c[c.length-1]??0,durationMs:u})}};window.requestAnimationFrame(o)})}function Pp(){let i=0,e=0,t=0,n=null;try{n=new PerformanceObserver(s=>{for(let r of s.getEntries())i++,t+=r.duration,r.duration>e&&(e=r.duration)}),n.observe({entryTypes:["longtask"]})}catch{}return{stop:()=>(n?.disconnect(),{count:i,longestMs:e,totalMs:t})}}async function Yh(i,e){let t=i.vault.adapter,n=(0,qh.normalizePath)(Cp);await t.exists(n)||await t.mkdir(n);let s=(0,qh.normalizePath)(`${Cp}/${e.scenario}-${Date.now()}.json`);return await t.write(s,JSON.stringify(e,null,2)),s}function Zh(i){return new Promise(e=>window.setTimeout(e,i))}var Bs={high:{id:"high",pixelRatioCap:2,bloomAllowed:!0,starScale:1,nodeCap:null,linkCap:null,hubLabels:14,neighborLabels:20,hoverThrottleMs:30},low:{id:"low",pixelRatioCap:1,bloomAllowed:!0,starScale:.4,nodeCap:null,linkCap:null,hubLabels:8,neighborLabels:12,hoverThrottleMs:80},mobile:{id:"mobile",pixelRatioCap:1.5,bloomAllowed:!1,starScale:.32,nodeCap:1500,linkCap:12e3,hubLabels:6,neighborLabels:8,hoverThrottleMs:null}};var Xy=.8,qy=3200,kl=class{constructor(e,t,n,s,r=null,o="en",a=null){this.app=e;this.contentEl=t;this.settings=n;this.onViewMode=r;this.language=o;this.onLanguage=a;this.layout=new ml;this.renderer=null;this.director=null;this.overlay=null;this.panel=null;this.rafId=0;this.lastNow=0;this.paused=!1;this.visible=!0;this.benchMode=!1;this.benchRunning=!1;this.selected=-1;this.graphRadius=200;this.wasSettled=!1;this.shot=null;this.maskEl=null;this.hudFrames=[];this.intersection=null;this.disposeFns=[];this.tier=Bs.high;this.watchdogTripped=!1;this.lowFpsChecks=0;this.lastWatchdogAt=0;this.onContextLost=null;this.store=new al(e),this.saveSoon=(0,_t.debounce)(s,800,!0)}get counts(){return{nodes:this.store.data.nodes.length,links:this.store.data.links.length}}async start(){await this.store.ensureCacheReady(),this.store.init(this.settings.showUnresolved,this.settings.showOrphans,()=>this.onDataChanged()),this.store.rebuild(!1);let t=this.applyPositionCache()>=Xy,n=this.contentEl.createDiv({cls:"galaxy-view-canvas"});this.graphRadius=ol(this.store.data.nodes.length)*1.6;let s=new Tl(n,this.graphRadius);this.renderer=s,s.setColorFn(this.settings.colorGroups.length>0?Yr(this.settings.colorGroups):Sl),s.setData(this.store.data,this.store.positions),this.initLayout(t?.06:1),this.director=new Rl(s.camera,s.renderer.domElement,{onFlyToSelected:()=>this.flyToSelected(),onResetView:()=>this.recenter()}),this.overlay=new Ll(this.contentEl,this.app,s,{openNote:o=>void this.app.workspace.openLinkText(o,"",!0),focusNode:o=>this.selectNode(o,!0)},this.language),this.overlay.setData(this.store.data,this.graphRadius),this.applySettings(),this.applyTier(),this.buildPanel(),this.buildFloatingControls(),this.applyPreset(),this.bindPicking(s.renderer.domElement),this.bindContextLost(s.renderer.domElement),this.bindVisibility(),this.resize(),this.settings.colorGroups.length===0&&this.importColors(!1),t?this.playEstablishing():this.director.setInitialFraming(this.graphRadius),this.lastNow=performance.now();let r=o=>{let a=Math.min((o-this.lastNow)/1e3,.1);if(this.lastNow=o,!this.paused){this.layout.step()&&this.renderer?.updatePositions(),this.checkSettled(),this.benchMode||this.director?.update(o,a),this.stepShot(o),this.renderer?.render(a);let{clientWidth:l,clientHeight:c}=this.contentEl;this.overlay?.update(l,c)}this.updateHud(o),this.watchdog(o),this.rafId=window.requestAnimationFrame(r)};this.rafId=window.requestAnimationFrame(r)}bindContextLost(e){let t=n=>{n.preventDefault(),this.contentEl.createDiv({cls:"gx-mask"}).createEl("button",{cls:"gx-mask-btn",text:this.tt("3d.contextLost")}).addEventListener("click",()=>this.onContextLost?.())};e.addEventListener("webglcontextlost",t),this.disposeFns.push(()=>e.removeEventListener("webglcontextlost",t))}resize(){let{clientWidth:e,clientHeight:t}=this.contentEl;this.renderer?.resize(e,t)}applyPositionCache(){let e=this.settings.positionCache,t=this.store.data.nodes;if(t.length===0)return 0;let n=0;return t.forEach((s,r)=>{let o=e[s.id];o&&(this.store.positions[r*3]=o[0],this.store.positions[r*3+1]=o[1],this.store.positions[r*3+2]=o[2],n++)}),n/t.length}checkSettled(){let e=this.layout.isSettled();if(e&&!this.wasSettled){let t={},n=this.store.positions;this.store.data.nodes.forEach((s,r)=>{t[s.id]=[Math.round((n[r*3]??0)*10)/10,Math.round((n[r*3+1]??0)*10)/10,Math.round((n[r*3+2]??0)*10)/10]}),this.settings.positionCache=t,this.saveSoon()}this.wasSettled=e}playEstablishing(){let e=this.renderer,t=this.director;!e||!t||(this.maskEl=this.contentEl.createDiv({cls:"gx-mask"}),this.maskEl.createDiv({cls:"gx-mask-text",text:this.tt("loading.3d")}),window.setTimeout(()=>{if(!this.maskEl)return;this.maskEl.addClass("is-fading"),window.setTimeout(()=>{this.maskEl?.remove(),this.maskEl=null},650);let n=this.graphRadius*.5,s=10*Math.PI/180;e.camera.position.set(n*Math.cos(s),n*Math.sin(s),n*.2),t.target.set(0,0,0),t.resetView(this.graphRadius,()=>t.beginFocusOrbit(null)),e.playReveal(2600),this.shot={t0:performance.now(),durMs:qy,fromBloom:this.settings.bloom.strength*1.8}},450))}stepShot(e){if(!this.shot||!this.renderer)return;let t=Math.min((e-this.shot.t0)/this.shot.durMs,1),n=this.shot.fromBloom+(this.settings.bloom.strength-this.shot.fromBloom)*t;this.renderer.setBloomStrength(n),t>=1&&(this.shot=null)}onDataChanged(){this.renderer&&(this.clearSelection(),this.renderer.setData(this.store.data,this.store.positions),this.overlay?.setData(this.store.data,this.graphRadius),this.initLayout(.3),this.wasSettled=!1)}initLayout(e){let t=Xs(this.settings.physics);try{this.layout.init(this.store.data,this.store.positions,t,e)}catch{if(this.layout instanceof Wr)throw new Error("layout init failed");this.layout.dispose(),this.layout=new Wr,this.layout.init(this.store.data,this.store.positions,t,e),new _t.Notice(this.tt("3d.workerFallback"))}}pickTier(){if(_t.Platform.isMobile)return Bs.mobile;let e=this.settings.qualityOverride;return e==="high"||e==="low"||e==="mobile"?Bs[e]:this.watchdogTripped?Bs.low:Bs.high}applyTier(){let e=this.tier.id;this.tier=this.pickTier(),this.renderer?.applyTier(this.tier,this.settings.bloom.strength),this.overlay?.setBudgets(this.tier.hubLabels,this.tier.neighborLabels,this.tier.id==="mobile"),this.contentEl.toggleClass("gx-mobile",this.tier.id==="mobile");let t=this.app.vault.getMarkdownFiles().length;this.store.setCaps(this.tier.nodeCap,this.tier.linkCap),this.tier.nodeCap!==null&&t>this.tier.nodeCap&&e!==this.tier.id&&new _t.Notice(this.tt("3d.mobileCap",{cap:this.tier.nodeCap,total:t}))}watchdog(e){this.watchdogTripped||_t.Platform.isMobile||this.settings.qualityOverride!=="auto"||!this.layout.isSettled()||this.benchRunning||e-this.lastWatchdogAt<5e3||(this.lastWatchdogAt=e,this.hudFrames.length>0&&this.hudFrames.length<30&&!this.paused?(this.lowFpsChecks++,this.lowFpsChecks>=3&&(this.watchdogTripped=!0,this.applyTier(),new _t.Notice(this.tt("3d.performanceMode")))):this.lowFpsChecks=0)}applySettings(){let e=this.settings;this.renderer?.setBloomParams(e.bloom),this.renderer?.setNodeScale(e.look.nodeSize),this.renderer?.setLinkOpacity(e.look.linkOpacity),this.renderer?.setSizeMode(e.look.sizeBy),this.renderer&&(this.renderer.twinkleFreq=e.look.twinkle),this.director&&(this.director.cruiseEnabled=e.cruise,this.director.cruiseSpeed=e.cruiseSpeed)}applyStylePreset(e){Object.assign(this.settings.bloom,e.bloom),Object.assign(this.settings.physics,e.physics),Object.assign(this.settings.look,e.look),this.applySettings(),this.layout.updateParams(Xs(this.settings.physics)),this.wasSettled=!1,this.saveSoon()}recenter(){this.clearSelection(),this.director?.resetView(this.graphRadius,()=>this.director?.beginFocusOrbit(null))}applyColorTheme(e){let t=this.settings.colorGroups;if(t.length===0){let n=new Map;for(let s of this.store.data.nodes)s.folderTop&&!s.unresolved&&n.set(s.folderTop,(n.get(s.folderTop)??0)+1);t=[...n.entries()].sort((s,r)=>r[1]-s[1]).slice(0,9).map(([s])=>({query:`path:${s}`,color:"#9aa4b2"})),this.settings.colorGroups=t}t.forEach((n,s)=>n.color=e.colors[s%e.colors.length]??n.color),this.settings.colorTheme=e.id,this.renderer?.setColorFn(Yr(t)),this.renderer?.recolor(),this.saveSoon()}playRevealManually(){if(!this.layout.isSettled()){new _t.Notice(this.tt("3d.revealWait"));return}this.renderer?.playReveal()}shuffleColors(){let e=this.settings.colorGroups;if(e.length<2){new _t.Notice(this.tt("3d.shuffleMissing"));return}let t=e.map(n=>n.color);for(let n=t.length-1;n>0;n--){let s=Math.floor(Math.random()*(n+1));[t[n],t[s]]=[t[s],t[n]]}e.forEach((n,s)=>n.color=t[s]??n.color),this.settings.colorTheme="custom",this.renderer?.setColorFn(Yr(e)),this.renderer?.recolor(),this.saveSoon()}applyPreset(){if(!this.renderer)return;let e=activeDocument.body.hasClass("theme-dark"),t=this.settings.preset==="deep-space"||e?El:Sp;this.renderer.applyTokens(t,this.settings.bloom.strength),this.panel?.setPanelTheme(t.id==="daylight"?"gx-theme-light":"gx-theme-dark"),this.contentEl.toggleClass("gx-daylight",t.id==="daylight")}onCssChange(){this.settings.preset==="adaptive"&&this.applyPreset()}async importColors(e){let t=await ef(this.app);if(!t||t.length===0){e&&new _t.Notice(this.tt("3d.importMissing"));return}this.settings.colorGroups=t,this.renderer?.setColorFn(Yr(t)),this.renderer?.recolor(),this.saveSoon(),e&&new _t.Notice(this.tt("3d.importDone",{count:t.length}))}openSearch(){new Dl(this.app,this.store.data.nodes,e=>this.selectNode(e,!0),this.language).open()}selectNode(e,t){let n=this.renderer,s=this.director;if(!n||!s)return;this.selected=e;let r=new Set,o=[];if(this.store.data.links.forEach((a,l)=>{(a.source===e||a.target===e)&&(r.add(a.source),r.add(a.target),o.push(l))}),n.setFocus(e,r),n.setSelectedLinks(o),this.overlay?.setSelection(e,r),t){let a=n.nodePosition(e,new N),l=new N,c=0,u=new N;for(let h of r)h!==e&&(l.add(n.nodePosition(h,u)),c++);let d=c>0?l.divideScalar(c).sub(a):null;s.flyTo(a,n.nodeRadius(e),()=>s.beginFocusOrbit(d))}}clearSelection(){this.selected=-1,this.renderer?.setFocus(-1,null),this.renderer?.setSelectedLinks([]),this.overlay?.setSelection(-1,new Set)}flyToSelected(){if(this.selected<0||!this.renderer||!this.director)return;let e=this.renderer.nodePosition(this.selected,new N);this.director.flyTo(e,this.renderer.nodeRadius(this.selected))}bindPicking(e){let t=0,n=0,s=c=>{t=c.clientX,n=c.clientY},r=c=>{if(c.button!==0||c.ctrlKey||c.metaKey||Math.hypot(c.clientX-t,c.clientY-n)>5)return;let u=e.getBoundingClientRect(),d=this.renderer?.pickNearest(c.clientX-u.left,c.clientY-u.top,u.width,u.height,14)??-1;d>=0?this.selectNode(d,!0):this.clearSelection()},o=!1,a=c=>{let u=this.tier.hoverThrottleMs;u===null||o||(o=!0,window.setTimeout(()=>{o=!1;let d=this.renderer;if(!d)return;let h=e.getBoundingClientRect(),f=d.pickNearest(c.clientX-h.left,c.clientY-h.top,h.width,h.height,10);this.overlay?.setHover(f),e.style.cursor=f>=0?"pointer":"default"},u))},l=c=>{c.key==="Escape"&&(this.clearSelection(),c.preventDefault())};e.addEventListener("pointerdown",s),e.addEventListener("pointerup",r),e.addEventListener("pointermove",a),e.addEventListener("keydown",l),this.disposeFns.push(()=>{e.removeEventListener("pointerdown",s),e.removeEventListener("pointerup",r),e.removeEventListener("pointermove",a),e.removeEventListener("keydown",l)})}bindVisibility(){let e=()=>{this.paused=activeDocument.hidden||!this.visible};activeDocument.addEventListener("visibilitychange",e),this.disposeFns.push(()=>activeDocument.removeEventListener("visibilitychange",e)),this.intersection=new IntersectionObserver(t=>{this.visible=t[0]?.isIntersecting??!0,e()}),this.intersection.observe(this.contentEl)}buildPanel(){this.panel=new Il(this.contentEl,this.settings,this.language,{onViewMode:e=>this.onViewMode?.(e),onBloom:()=>{this.renderer?.setBloomParams(this.settings.bloom),this.saveSoon()},onPhysics:()=>{this.layout.updateParams(Xs(this.settings.physics)),this.wasSettled=!1,this.saveSoon()},onLook:()=>{this.renderer?.setNodeScale(this.settings.look.nodeSize),this.renderer?.setLinkOpacity(this.settings.look.linkOpacity),this.renderer&&(this.renderer.twinkleFreq=this.settings.look.twinkle),this.saveSoon()},onSizeBy:()=>{this.renderer?.setSizeMode(this.settings.look.sizeBy),this.saveSoon()},onCruise:e=>{this.director&&(this.director.cruiseEnabled=e),this.saveSoon()},onPreset:()=>{this.applyPreset(),this.saveSoon()},onShowUnresolved:e=>{this.store.setIncludeUnresolved(e),this.saveSoon()},onImportColors:()=>void this.importColors(!0),onShuffleColors:()=>this.shuffleColors(),onColorTheme:e=>this.applyColorTheme(e),onRecenter:()=>this.recenter(),onReveal:()=>this.playRevealManually(),onShowOrphans:e=>{this.store.setIncludeOrphans(e),this.saveSoon()},onStylePreset:e=>this.applyStylePreset(e),onCruiseSpeed:()=>{this.director&&(this.director.cruiseSpeed=this.settings.cruiseSpeed),this.saveSoon()},onQuality:()=>{this.watchdogTripped=!1,this.lowFpsChecks=0,this.applyTier(),this.saveSoon()},onSearch:()=>this.openSearch(),onReset:()=>{Object.assign(this.settings.bloom,xn.bloom),Object.assign(this.settings.physics,xn.physics),Object.assign(this.settings.look,xn.look),this.settings.cruise=xn.cruise,this.settings.cruiseSpeed=xn.cruiseSpeed,this.applySettings(),this.layout.updateParams(Xs(this.settings.physics)),this.wasSettled=!1,this.saveSoon()},runScenario:e=>void this.runScenario(e)},"map3d")}buildFloatingControls(){let e=this.contentEl.createDiv({cls:"mwm-floating-controls mwm-3d-floating-controls"}),t=e.createEl("button",{cls:"mwm-floating-button",attr:{type:"button",title:this.tt("language"),"aria-label":this.tt("language")}});(0,_t.setIcon)(t,"languages"),t.addEventListener("click",n=>{n.preventDefault(),n.stopPropagation(),this.openLanguageMenu(t)}),this.disposeFns.push(()=>e.remove())}openLanguageMenu(e){let t=new _t.Menu;for(let[s,r]of as(this.language))t.addItem(o=>{o.setTitle(r),s===this.language&&o.setIcon("check"),o.onClick(()=>{s!==this.language&&(this.language=s,this.overlay?.setLanguage(s),this.panel?.setLanguage(s),this.onLanguage?.(s))})});let n=e.getBoundingClientRect();t.showAtPosition({x:n.right,y:n.bottom})}updateHud(e){for(this.hudFrames.push(e);this.hudFrames.length>0&&e-(this.hudFrames[0]??0)>1e3;)this.hudFrames.shift();let t=this.panel?.statsEl;if(!t||e%500>250)return;let n=this.counts;t.setText(this.tt("stats.3d",{fps:this.hudFrames.length,calls:this.renderer?.drawCalls??0,nodes:n.nodes,links:n.links,state:this.layout.isSettled()?this.tt("state.settled"):this.tt("state.layout")}))}tt(e,t={}){return Ce(this.language,e,t)}waitSettle(e=12e4){return new Promise(t=>{let n=performance.now(),s=()=>{this.layout.isSettled()||performance.now()-n>e?t():window.setTimeout(s,100)};s()})}async runScenario(e){if(this.benchRunning||!this.renderer||!this.director)return null;this.benchRunning=!0;try{return e==="S2"?await this.benchColdLayout():await this.benchOrbit(e)}finally{this.benchRunning=!1}}async benchOrbit(e){let t=this.renderer,n=this.director;if(!t||!n)throw new Error("not ready");let s=e==="S3";this.store.getIncludeUnresolved()!==s&&this.store.setIncludeUnresolved(s),new _t.Notice(this.tt("3d.bench.wait",{scenario:e})),await this.waitSettle(),t.getBloomStrength()<.01&&t.setBloomStrength(.9),await Zh(300),this.benchMode=!0;let r=n.target.clone(),o=new dn().setFromVector3(t.camera.position.clone().sub(r));new _t.Notice(this.tt("3d.bench.orbit",{scenario:e}));let a=await Rp(2e4,c=>{let u=o.theta+c/2e4*Math.PI*2;t.camera.position.setFromSpherical(new dn(o.radius,o.phi,u)).add(r),t.camera.lookAt(r)});this.benchMode=!1;let l={scenario:e,timestamp:new Date().toISOString(),nodes:this.counts.nodes,links:this.counts.links,bloom:t.getBloomStrength()>0,drawCalls:t.drawCalls,renderer:"aggregate",...a};return await Yh(this.app,l),new _t.Notice(this.tt("3d.bench.done",{scenario:e,fps:a.avgFps.toFixed(1),calls:t.drawCalls})),l}async benchColdLayout(){new _t.Notice(this.tt("3d.bench.s2Start")),this.store.getIncludeUnresolved()&&this.store.setIncludeUnresolved(!1),await Zh(300);let e=Pp(),t=performance.now();this.settings.positionCache={},this.store.rebuild(!1),this.initLayout(1),this.wasSettled=!1,await this.waitSettle();let n=performance.now()-t,s=this.layout.ticks,r=e.stop(),o={scenario:"S2",timestamp:new Date().toISOString(),nodes:this.counts.nodes,links:this.counts.links,bloom:(this.renderer?.getBloomStrength()??0)>0,renderer:"aggregate",settleMs:n,ticks:s,avgTickMs:s>0?n/s:-1,longTaskCount:r.count,longestTaskMs:r.longestMs,longTaskTotalMs:r.totalMs};return await Yh(this.app,o),new _t.Notice(this.tt("3d.bench.s2Done",{seconds:(n/1e3).toFixed(1),ticks:s,longest:r.longestMs.toFixed(0)})),o}dispose(){window.cancelAnimationFrame(this.rafId),this.intersection?.disconnect(),this.intersection=null;for(let e of this.disposeFns)e();this.disposeFns=[],this.maskEl?.remove(),this.maskEl=null,this.overlay?.dispose(),this.overlay=null,this.director?.dispose(),this.director=null,this.layout.dispose(),this.renderer?.dispose(),this.renderer=null,this.panel?.dispose(),this.panel=null}};var zs=class extends kl{};var kt=require("obsidian");var Ze="",Ip="Vault";var Lp=960,Np=720,Yy=2800,Dp=126,Zy=72,$y=360,Ky=.36,Fp=.28;function kp(i,e){let t=Ge(e.ringSpacing,Np,Yy),n=Ge(e.nodeSpacing,Zy,$y),s=new Map,r=i.nodesById,o=ub(i.nodes),a=i.nodes.filter(T=>!T.externalProxy&&T.type!=="external"),l=new Set(a.map(T=>T.id)),c=sb(i,l,t,n),u=Jy(i,l,r),d=jy(u,r,c),h=l.has(i.rootId)?i.rootId:l.has(Ze)?Ze:a[0]?.id??null;if(h!==null){let T=Ul(0,-Math.PI/2,0,Vs(r.get(h),o),!1);s.set(h,T),Op(h,0,-Math.PI/2,-Math.PI/2+Math.PI*2,-Math.PI/2,u,d,s,r,c,h,o)}let f=new Set(s.keys()),g=a.filter(T=>T.id!==h&&!f.has(T.id)).sort(Ol),x=h!==null?d.get(h):null,m=Math.max(0,x?.maxDepth??0),p=Math.max(db(s),c.ringGap);g.length>0&&(p=Math.max(p,$h(g,s,i,r,p+c.ringGap*.72,c.nodeGap,-Math.PI/2,m+1,!1,o)));let v=i.nodes.filter(T=>T.type==="external"&&!T.externalProxy).sort(Ol);v.length>0&&(p=Math.max(p,$h(v,s,i,r,p+c.ringGap*.62,c.nodeGap*1.15,-Math.PI/3,m+1,!0,o)));let b=i.nodes.filter(T=>T.externalProxy).sort(Ol);b.length>0&&$h(b,s,i,r,p+Math.max(220,c.ringGap*.52),c.nodeGap,-Math.PI/5,m+2,!0,o);let M=tb(s,i,c,c.nodeGap,o);nb(s,i,c,c.nodeGap,M,o),ib(s,i,c,c.nodeGap,M,o),c.radiusExpansion>1.001&&ob(s,M,c),ab(s,u,i,c),e.swirlStrength>.001&&wb(s,i,c,e.swirlStrength/100),Eb(s);let E=cb(s,M),w=m+(v.length||b.length?2:g.length?1:0),C=lb(i.linkEdges,s,w,c.ringGap),_=hb(s,C);return{positions:s,rings:E,routes:C,bounds:_,width:_.maxX-_.minX,height:_.maxY-_.minY,centerX:0,centerY:0,ringSpacing:c.ringGap,nodeSpacing:c.nodeGap}}function Jy(i,e,t){let n=new Map;for(let s of i.hierarchyEdges){if(!e.has(s.source)||!e.has(s.target))continue;let r=n.get(s.source);r?r.push(s.target):n.set(s.source,[s.target])}for(let s of n.values())s.sort((r,o)=>Ol(t.get(r),t.get(o)));return n}function jy(i,e,t){let n=new Map,s=(r,o,a=new Set)=>{let l=n.get(r);if(l)return l;if(a.has(r))return{weight:1,count:1,maxDepth:o};a.add(r);let c=t.incidentPressureByNode.get(r)??0,u=Math.min(9,c*.24),d=1,h=o;for(let g of i.get(r)??[]){let x=s(g,o+1,a);u+=x.weight,d+=x.count,h=Math.max(h,x.maxDepth)}a.delete(r);let f={weight:Math.max(1,u||1),count:d,maxDepth:h};return n.set(r,f),f};for(let r of i.keys())s(r,0);return n}function Op(i,e,t,n,s,r,o,a,l,c,u,d){let h=r.get(i)??[];if(h.length===0)return;let f=t,g=n,x=Math.max(.001,g-f),m=a.get(i)??Ul(0,s,e,5,!1),p=m.radius+Qy(i,e,h,o,c,u),v=eb(i,h,o,c,u);if(h.length===1&&i===u){let C=Math.max(Math.PI*.36,c.branchFanSpan*.82);f=s-C/2,g=s+C/2,x=C}else if(i!==u){let C=h.length>1?(h.length-1)*v/p+.08:Math.max(Math.PI*.24,c.branchFanSpan*.62),_=Math.min(x,c.branchFanSpan),T=Math.min(x,Math.max(_,C));f=s-T/2,g=s+T/2,x=T}if(h.length>1){let C=(h.length-1)*v/Math.max(.16,x*.64),_=c.ringGap*(i===u?1.8:2.8);p=Math.max(p,Math.min(m.radius+_,C))}let b=Math.max(1,h.reduce((C,_)=>C+(o.get(_)?.weight??1),0)),M=h.length>1?Math.min(v/p,x*.38/(h.length-1)):0,E=Math.max(.001,x-M*Math.max(0,h.length-1)),w=f;for(let C of h){let _=h.length===1?E:E*((o.get(C)?.weight??1)/b),T=w,P=w+_,R=T+_/2,I=l.get(C);a.set(C,Ul(p,R,e+1,Vs(I,d),!1)),Op(C,e+1,T,P,R,r,o,a,l,c,u,d),w=P+M}}function Qy(i,e,t,n,s,r){let o=t.length,a=n.get(i)??{count:1,weight:1,maxDepth:e},l=s.incidentPressureByNode.get(i)??0,c=Math.sqrt(Math.max(1,o)),u=Math.log2(Math.max(1,a.count||a.weight||1)),d=Math.sqrt(Math.max(0,l)),h=.62+e*.105+c*.135+u*.094+d*.086;return i===r&&(h*=.96),o<=4&&(h=Math.min(h,i===r?.94:1.12)),o>=14&&(h=Math.max(h,1.24+Math.min(1.1,c*.09))),o>=40&&(h=Math.max(h,1.56+Math.min(1.34,c*.084))),Ge(s.baseRingGap*h,i===r?260:300,4400)}function eb(i,e,t,n,s){let r=e.length,o=t.get(i)??{count:1,weight:1,maxDepth:1},a=n.incidentPressureByNode.get(i)??0,c=1.58+(Math.sqrt(Math.max(1,r))*.07+Math.log2(Math.max(1,o.count||1))*.052+Math.sqrt(Math.max(0,a))*.05)*1.95;return i===s&&r<=6&&(c*=1.08),r<=4?c=Ge(c,1.62,2.08):r<=10&&(c=Math.max(c,1.86)),r>=24&&(c=Math.max(c,2.12)),r>=64&&(c=Math.max(c,2.58)),Ge(n.baseNodeGap*c,132,920)}function $h(i,e,t,n,s,r,o,a,l,c){if(i.length===0)return s;let u=Math.PI*2/i.length,d=i.length*(r/Math.max(1,s)),h=i.map((g,x)=>({node:g,preferred:fb(g,t,e,o+x*u)})).sort((g,x)=>St(g.preferred)-St(x.preferred)),f=h[0]?h[0].preferred-u*.5:o;for(let g=0;gh>0).sort((h,f)=>h-f),c=Math.max(...l,1),u=[...o.values()].reduce((h,f)=>h+f.count,0),d=Ge(1-Math.log10(Math.max(1,u))*.085,.58,.9);for(let h of l){let f=o.get(h);if(!f)continue;let g=f.diameterTotal/Math.max(1,f.count),x=f.count>1?f.count*g/(Math.PI*2):0,m=x>0?Math.pow(x,.64)*Math.pow(t.ringGap,.36):0,p=h/Math.max(1,c),v=1+Math.pow(p,1.35)*.34,b=1+Math.min(.28,Math.sqrt(f.linkPressure/Math.max(1,f.count))*.036),M=Ge(1-p*.1,.84,.98),E=h*t.ringGap*d*M*v*b,w=a+t.ringGap*(f.external?.54:.62),C=m*d*v*b+f.maxDiameter*1.45,_=a+t.ringGap*(f.external?1.45:1.72)*v,T=Math.min(_,Math.max(E,w,C));r.set(h,T),a=T}for(let h of i.values()){let f=Math.max(0,Math.round(h.depth||0)),g=r.get(f);if(!Number.isFinite(g))continue;let x=g,m=Number.isFinite(h.angle)?h.angle:Math.atan2(h.y,h.x);if(h.ringRadius=x,f===0){h.x=0,h.y=0,h.radius=0,h.angle=-Math.PI/2;continue}h.x=Math.cos(m)*x,h.y=Math.sin(m)*x,h.radius=x,h.angle=St(m)}return r}function nb(i,e,t,n,s,r){if(i.size<2)return;let o=Ge(n*1.12,54,280),a=[],l=1;for(let[f,g]of i.entries()){let x=e.nodesById.get(f);if(!x)continue;let m=Vs(x,r)*1.72,p=Math.min(320,o+Gp(x)),v=m+p,b=Math.max(0,Math.round(g.depth||0)),M=Number.isFinite(g.ringRadius)?g.ringRadius:s.get(b);l=Math.max(l,v),a.push({id:f,node:x,point:g,visualRadius:m,collisionRadius:v,gravity:Ge(Math.sqrt(Math.max(1,m))/3.1,.85,2.55),fixed:f===e.rootId,anchorAngle:Number.isFinite(g.angle)?g.angle:Math.atan2(g.y,g.x),ringRadius:Number.isFinite(M)?M:null})}if(a.length<2)return;let c=a.length>3500?9:a.length>1200?11:16,u=Math.max(112,l*2.48),d=(f,g)=>{let x=!1,m=new Map;for(let p=0;p=k)continue;let z=I=H?(k-I)*g:(k-H)*g*.35,K=(z+V)*W+.01,Q=P/I,ae=R/I,_e=v.fixed?0:T.fixed?1:.5,be=T.fixed?0:v.fixed?1:.5;v.point.x+=Q*K*_e,v.point.y+=ae*K*_e,T.point.x-=Q*K*be,T.point.y-=ae*K*be,x=!0}}}return x},h=f=>{for(let g of a){if(g.fixed||!Number.isFinite(g.ringRadius))continue;let x=Math.max(.001,Math.hypot(g.point.x,g.point.y)),m=Math.atan2(g.point.y,g.point.x),p=g.node.externalProxy||g.node.type==="external",v=p?.016:.024,b=Math.max(g.visualRadius*(p?1.9:1.45),t.ringGap*(p?.15:.105)),M=m+Qi(m,g.anchorAngle)*v,E=x+(g.ringRadius-x)*f,w=Ge(E,Math.max(0,g.ringRadius-b),g.ringRadius+b);g.point.x=Math.cos(M)*w,g.point.y=Math.sin(M)*w}};for(let f=0;fu-d),c=Math.max(...l,1);for(let u of l){let d=o.get(u);if(!d?.length)continue;let h=d.reduce((P,R)=>P+R.arcDemand,0),f=Math.max(...d.map(P=>P.visualRadius),4),g=s.get(u)??Math.max(t.ringGap*u,a+t.ringGap*.62),x=Ge((u-1)/Math.max(1,c-1),0,1),m=Math.pow(x,1.35),p=Ge(.72-m*.16-Math.min(.1,d.length/4200),.5,.72),v=Math.max(1,Math.PI*2*g*p),b=Ge(Math.ceil(h/v),1,pb(d.length,x)),M=Math.max(f*(2.45+m*.7)+Math.max(12,n*(.13+m*.04)),t.ringGap*(.11+m*.045)),E=yb(u,g,t.ringGap,d.length,h),w=Math.max(g-M*(b-1)*.5,a+M*.86),C=Array.from({length:b},()=>[]);for(let P of zp(d)){let R=Bp(P.items);b===1?C[0]?.push(...R):C[mb(C,R,P.parentAngle)]?.push(...R)}let _=[],T=[];for(let P=0;PQ+ae.arcDemand,0),W=H/(Math.PI*2*p);I=Math.max(I,W,a+M*.72);let k=E*bb(R.length,Mb(R),H,I),z=Math.min(t.ringGap*Ky*k,I*Fp*Math.min(1.35,k));I=Math.max(I,a+z*.56+M*.68);let V=Math.min(z,Math.max(0,I-a-f*2.2-Math.max(12,n*.08))),K=H/Math.max(1,Math.PI*2*I);xb(R,I,{jitterBand:V,preservePreferred:R.length<90||K<.38||m<.28}),_.push(I),T.push(I+V)}_.length&&(s.set(u,Ab(_,g)),a=Math.max(...T)+f*1.55+Math.max(12,n*.08))}}function sb(i,e,t,n){let s=new Map,r=0,o=0;for(let C of i.linkEdges){let _=Math.min(7,Math.log2((C.weight||1)+1)),T=Math.min(6,Math.log2((C.rawCount||C.weight||1)+1)),P=.75+_*.62+T*.28+(C.unresolvedCount?.35:0)+(C.externalCount?.85:0);r+=P,C.externalCount&&(o+=P);for(let R of[C.source,C.target])s.set(R,(s.get(R)??0)+P)}let a=0;for(let C of s.values())a=Math.max(a,C);let l=Math.max(1,i.nodes.length||e.size||1),c=rb(i,e),u=r/l,d=i.linkEdges.length/l,h=a/Math.max(1,Math.sqrt(l)*1.65),f=u+Math.sqrt(Math.max(0,h))*.58+Math.min(1.6,d)*.3+o/l*.32,g=Math.sqrt(Math.max(0,f)),x=Ge(1.2+g*.92+Math.min(.86,u*.105),1.2,5.8),m=Ge(1.08+g*.34+Math.min(.34,u*.044),1.08,2.15),p=Ge(.62+g*.24+Math.min(.3,d*.17),.62,1.35),v=Ge(.9+g*.38+Math.min(.42,d*.2),.9,2.55),b=Math.max(0,Math.log2(c.normalCount/520))*.035,M=Math.max(0,Math.sqrt(c.maxRingCount/96)-1)*.16,E=Math.max(0,Math.sqrt(c.averageRingCount/64)-1)*.1,w=Math.max(0,g-.75)*.028;return{baseRingGap:t,baseNodeGap:n,ringGap:Ge(t*m,Np,4200),nodeGap:Ge(n*x,86,860),branchFanSpan:Math.PI*p,routeGapFactor:v,radiusExpansion:Ge(1+b+M+E+w,1,1.56),ringCountsByDepth:c.countsByDepth,maxDensityDepth:c.maxDepth,incidentPressureByNode:s}}function rb(i,e){let t=new Map;for(let r of i.nodes){if(!e.has(r.id))continue;let o=Math.max(0,Math.round(r.depth||0));o<=0||t.set(o,(t.get(o)??0)+1)}let n=0,s=0;for(let r of t.values())n=Math.max(n,r),s+=r;return{normalCount:Math.max(1,e.size||0),maxRingCount:n,averageRingCount:t.size?s/t.size:Math.max(1,e.size||0),maxDepth:Math.max(...t.keys(),1),countsByDepth:t}}function ob(i,e,t){let n=Ge(t.radiusExpansion,1,1.65);if(n<=1.001)return;let s=Math.max(1,t.maxDensityDepth,...e.keys()),r=o=>{if(o<=0)return 1;let a=Ge((o-1)/Math.max(1,s-1),0,1),l=Math.pow(a,1.42),c=t.ringCountsByDepth.get(Math.round(o))??0,u=Math.max(0,Math.sqrt(c/72)-1)*.13*l,d=(1-l)*Math.min(.08,(n-1)*.42),h=(n-1)*(.04+l*1.06);return Ge(1-d+h+u,.93,1.68)};for(let[o,a]of[...e.entries()])o>0&&e.set(o,a*r(o));for(let o of i.values()){if(o.radius<=.001)continue;let a=Math.max(0,Math.round(o.depth||0)),l=r(a),c=Number.isFinite(o.angle)?o.angle:Math.atan2(o.y,o.x),u=o.radius*l;o.radius=u,o.x=Math.cos(c)*u,o.y=Math.sin(c)*u,o.angle=c,Number.isFinite(o.ringRadius)&&(o.ringRadius=o.ringRadius*l),Number.isFinite(o.ringBandMin)&&(o.ringBandMin=o.ringBandMin*l),Number.isFinite(o.ringBandMax)&&(o.ringBandMax=o.ringBandMax*l)}}function ab(i,e,t,n){for(let[s,r]of e.entries()){let o=i.get(s);if(!o||o.radius<=.001||r.length===0)continue;let a=Number.isFinite(o.angle)?o.angle:Math.atan2(o.y,o.x),l=r.map(m=>{let p=i.get(m),v=t.nodesById.get(m);return p&&v&&!p.external?{id:m,point:p,node:v,delta:Qi(a,p.angle)}:null}).filter(m=>!!m).sort((m,p)=>m.delta-p.delta);if(l.length===0)continue;let c=l.reduce((m,p)=>m+Math.max(1,p.point.radius),0)/l.length,u=l.reduce((m,p)=>m+p.point.nodeRadius*2.25+Hp(p.node)*.76,0),d=Math.min(Math.PI*1.1,u/Math.max(1,c)),h=l.length>1?Math.max(...l.map(m=>m.delta))-Math.min(...l.map(m=>m.delta)):0,f=Ge(Math.max(d,h*.58),l.length===1?0:.035,Math.PI*.86),g=Math.min(Math.PI*.18,Math.max(.03,n.branchFanSpan*.08)),x=l.length<=2?.54:l.length<=8?.46:.34;for(let m=0;m=Math.max(1,t-1),m=!!(o.external||a.external||r.externalCount);(m||g||l<=1&&x&&f>.34||f>Math.PI*.42)&&s.set(r.id,{kind:"curve",centerX:0,centerY:0,radius:Math.max(c,u),sourceAngle:d,targetAngle:h,curveStrength:m?.3:g?.22:f>Math.PI*.72?.2:.15})}return s}function cb(i,e){if(e.size>1){let n=new Map;for(let s of i.values()){let r=Math.max(0,Math.round(s.depth||0));if(r<=0)continue;let o=Number.isFinite(s.ringRadius)?s.ringRadius:e.get(r);if(!Number.isFinite(o)||o<=0)continue;let a=`${r}:${Math.round(o)}`,l=n.get(a);l?(l.count++,l.radiusTotal+=o):n.set(a,{depth:r,radiusTotal:o,count:1})}return[...n.values()].map(s=>({depth:s.depth,radius:s.radiusTotal/Math.max(1,s.count),count:s.count})).sort((s,r)=>s.radius-r.radius)}let t=new Map;for(let n of i.values()){if(n.depth<=0)continue;let s=n.ringRadius??n.radius,r=t.get(n.depth)??{radius:0,count:0};r.radius+=s,r.count++,t.set(n.depth,r)}return[...t.entries()].map(([n,s])=>({depth:n,radius:s.radius/Math.max(1,s.count),count:s.count})).sort((n,s)=>n.radius-s.radius)}function hb(i,e){let t=-500,n=-500,s=500,r=500;for(let o of i.values()){let a=o.nodeRadius+180;t=Math.min(t,o.x-a),n=Math.min(n,o.y-a),s=Math.max(s,o.x+a),r=Math.max(r,o.y+a)}for(let o of e.values())o.kind==="outer"&&(t=Math.min(t,-o.radius-160),n=Math.min(n,-o.radius-160),s=Math.max(s,o.radius+160),r=Math.max(r,o.radius+160));return{minX:t,minY:n,maxX:s,maxY:r}}function Ul(i,e,t,n,s){let r=Math.cos(e)*i,o=Math.sin(e)*i;return{x:r,y:o,homeX:r,homeY:o,radius:i,homeRadius:i,angle:e,homeAngle:e,depth:t,nodeRadius:n,centerX:0,centerY:0,external:s}}function Vs(i,e){if(!i)return 3.6;let t=Math.max(0,(i.linkCount||0)+(i.backlinkCount||0)),n=Math.log1p(t)/Math.max(1,Math.log1p(e||1)),s=Ge(n,0,1),r=Math.pow(s,.48),o=Math.pow(s,1.32),a=r*19+o*20+Math.log2(t+1)*1.35+Math.sqrt(t)*.32;if(i.externalProxy)return i.type==="unresolved"?5.4:Math.min(27,5.8+a*.62);if(i.type==="folder"){let l=Math.log2((i.noteCount||i.descendantCount||1)+1);return Math.min(66,7+l*1.05+a*1.18)}if(i.type==="external"){let l=Math.log2((i.noteCount||1)+1);return Math.min(38,5.8+l*.72+a*.9)}return i.type==="unresolved"?Math.min(16,4.2+a*.5):Math.min(58,3.6+a*1.05)}function ub(i){let e=1;for(let t of i)e=Math.max(e,(t.linkCount||0)+(t.backlinkCount||0));return e}function Ol(i,e){if(!i||!e)return i?-1:e?1:0;let t=n=>n.type==="folder"?0:n.type==="note"?1:n.type==="external"?2:3;return t(i)-t(e)||i.title.localeCompare(e.title)||i.id.localeCompare(e.id)}function db(i){let e=0;for(let t of i.values())e=Math.max(e,t.radius);return e}function fb(i,e,t,n){let s=[];for(let r of e.linkEdges){let o=r.source===i.id?r.target:r.target===i.id?r.source:null;if(!o)continue;let a=t.get(o);a&&Number.isFinite(a.angle)&&s.push(a.angle)}return Up(s,n)}function Up(i,e){if(i.length===0)return e;let t=i.reduce((n,s)=>(n.x+=Math.cos(s),n.y+=Math.sin(s),n),{x:0,y:0});return Math.abs(t.x)<1e-4&&Math.abs(t.y)<1e-4?e:Math.atan2(t.y,t.x)}function Bp(i){let e=i.slice().sort((r,o)=>St(r.preferred)-St(o.preferred));if(e.length<=2)return e;let t=-1,n=0;for(let r=0;rt&&(t=l,n=r)}let s=(n+1)%e.length;return e.slice(s).concat(e.slice(0,s))}function pb(i,e){let t=Math.max(0,i||0),n=Ge(e,0,1),s=t>1600?14:t>900?12:t>420?10:t>180?8:t>80?6:4,r=n>.72?3:n>.48?2:n>.28?1:0;return Ge(s+r,4,16)}function zp(i){let e=new Map;for(let t of i){let n=t.parentId??t.id,s=e.get(n)??{parentId:n,parentAngle:t.parentAngle,arcDemand:0,items:[]};s.items.push(t),s.arcDemand+=t.arcDemand||0,s.parentAngle=Up(s.items.map(r=>r.parentAngle),s.parentAngle),e.set(n,s)}return[...e.values()].sort((t,n)=>St(t.parentAngle)-St(n.parentAngle))}function mb(i,e,t){let n=0,s=1/0,r=e.reduce((o,a)=>o+(a.arcDemand||0),0);for(let o=0;oh+(f.arcDemand||0),0),c=a[a.length-1],u=c?Math.abs(Qi(c.parentAngle||c.preferred,t)):0,d=l+r*.18+u*180;dMath.max(.003,f.arcDemand/e)),r=s.reduce((f,g)=>f+g,0),o=Ge(t.jitterBand,0,Math.max(0,e*Fp)),a=_b(i,o),l=Math.min(.11,Math.max(.012,r/Math.max(1,i.length)*.18)),c=r+l*Math.max(0,i.length-1);if(t.preservePreferred&&i.length<=640&&c({item:x,arc:e[m]??.003,preferred:St(x.preferred)})).sort((x,m)=>x.preferred-m.preferred);if(a.length>1){let x=-1,m=0;for(let v=0;vx&&(x=E,m=v)}let p=(m+1)%a.length;a=a.slice(p).concat(a.slice(0,p))}let l=[],c=0,u=a[0]?.preferred??0;l[0]=u;for(let x=1;xo-r)return!1;let f=((l[0]??0)+(l[l.length-1]??0))*.5,g=d-f;for(let x=0;x0?e/t:r,a=Number.isFinite(e)&&e>0?s/Math.max(1,Math.PI*2*e):0,l=Ge(.82+r*.06+Math.sqrt(Math.max(0,o))*.13,.86,1.62),c=n<=5?.48:n<=10?.68:a<.12?.62:a<.24?.82:a>.52?1.16:1;return Ge(l*c,.42,1.72)}function bb(i,e,t,n){let s=Math.max(0,i||0),r=Math.max(1,e||1),o=Number.isFinite(n)&&n>0?t/Math.max(1,Math.PI*2*n):0,a=s<=3?.42:s<=7?.66:s<=14?.86:1.05,l=r<=1?.52:r<=2?.72:r<=4?.92:1.08,c=o<.1?.56:o<.2?.78:o>.55?1.14:1;return Ge(a*l*c,.32,1.22)}function Mb(i){return new Set(i.map(Kh)).size}function Sb(i,e,t,n){if(t<=0)return e;let s=Kh(i),r=n.get(s)??Zr(s,"ring-parent")*t*.92,o=Zr(i.id,"ring-node")*t*.08;return e+Ge(r+o,-t,t)}function Kh(i){return i.parentId??i.id}function Gp(i){let e=Array.from(String(i.title||"")).length,t=i.type==="folder"?13:i.type==="external"||i.externalProxy?9:5;return Ge(Math.sqrt(Math.max(1,e))*4.2+t,10,60)}function Hp(i){let e=Array.from(String(i.title||"")).length,t=i.type==="folder"?14:0,n=i.type==="external"||i.externalProxy?9:0;return Ge(Math.sqrt(Math.max(1,e))*7.5+Math.min(110,e*1.25)+t+n,24,150)}function wb(i,e,t,n){let s=e.rootId||Ze,r=Zr(s||"vault","swirl-direction")>=0?1:-1;for(let[o,a]of i.entries()){if(a.radius<=.001)continue;let l=Math.max(0,a.depth||0),c=Number.isFinite(a.angle)?a.angle:Math.atan2(a.y,a.x),u=Math.sqrt(Math.max(0,a.radius)/Math.max(1,t.ringGap)),d=Zr(o,"swirl-arm")*.16,h=Math.sin(c*2.35+l*.78)*.11,f=r*n*(l*.32+u*.22+d+h),g=St(c+f);a.x=Math.cos(g)*a.radius,a.y=Math.sin(g)*a.radius,a.angle=g}}function Eb(i){for(let e of i.values())e.homeX=e.x,e.homeY=e.y,e.homeRadius=e.radius,e.homeAngle=e.angle}function Tb(i,e,t){return St(i+Qi(i,e)*Ge(t,0,1))}function St(i){let e=Number.isFinite(i)?i:0;return e%=Math.PI*2,e<0&&(e+=Math.PI*2),e}function Qi(i,e){let t=St(e)-St(i);return t>Math.PI&&(t-=Math.PI*2),t<-Math.PI&&(t+=Math.PI*2),t}function Wp(i,e){let t=`${i}|${e}`,n=2166136261;for(let s=0;s>>0)/4294967296*Math.PI*2}function Zr(i,e){return Math.sin(Wp(String(i||""),String(e||"")))}function Ab(i,e){let t=i.filter(Number.isFinite).sort((s,r)=>s-r);if(!t.length)return e;let n=Math.floor(t.length/2);return t.length%2?t[n]??e:((t[n-1]??e)+(t[n]??e))/2}function Ge(i,e,t){return Math.min(Math.max(Number.isFinite(i)?i:e,e),t)}var Cb={day:{bg:"#f7f8fb",ring:"#94a3b8",tree:"#5b677a",link:"#525e70",external:"#5b5fe0",externalLink:"#cb7622",unresolved:"#dc2626",focus:"#5368ee",folder:"#3f4a5a",folderMeta:"#4f5ee8",note:"#536073",root:"#4f5ee8",ringOpacity:.16,treeOpacity:.17,linkOpacity:.06,externalLinkOpacity:.08,highlightOpacity:.9,nodeScale:.19,maxLabels:140},night:{bg:"#0f1117",ring:"#818cf8",tree:"#818cf8",link:"#94a3b8",external:"#c4b5fd",externalLink:"#fb923c",unresolved:"#fb7185",focus:"#aab6ff",folder:"#d7dae2",folderMeta:"#f59e0b",note:"#a1a8b5",root:"#7c9cff",ringOpacity:.15,treeOpacity:.18,linkOpacity:.065,externalLinkOpacity:.09,highlightOpacity:.98,nodeScale:.2,maxLabels:140}},zl=class{constructor(e){this.container=e;this.scene=new Zi;this.ringSegments=null;this.hierarchySegments=null;this.linkSegments=null;this.highlightSegments=null;this.nodePoints=null;this.nodeGeometry=null;this.nodeMaterial=null;this.nodeIds=[];this.edgeVisuals=new Map;this.graph=null;this.layout=null;this.active=Vl();this.width=1;this.height=1;this.centerX=0;this.centerY=0;this.zoom=1;this.scheme="night";this.revealFrame=0;this.revealOverlay=null;this.renderer=new Ds({antialias:!0,alpha:!1}),this.renderer.setPixelRatio(Math.min(window.devicePixelRatio||1,2)),this.renderer.setClearColor(this.palette().bg,1),this.domElement=this.renderer.domElement,this.domElement.classList.add("mwm-radial-canvas"),this.domElement.tabIndex=0,e.appendChild(this.domElement),this.labelRoot=e.createDiv({cls:"mwm-radial-labels"}),this.camera=new si(-1,1,1,-1,.1,5e3),this.camera.position.set(0,0,1e3),this.camera.lookAt(0,0,0)}setTheme(e){if(this.scheme===e)return!1;if(this.scheme=e,this.renderer.setClearColor(this.palette().bg,1),this.nodeMaterial){let t=this.nodeMaterial.uniforms.uLightMode,n=this.nodeMaterial.uniforms.uPixelScale;t&&(t.value=e==="day"?1:0),n&&(n.value=1e3*Math.min(window.devicePixelRatio||1,2)),this.updateNodeScale()}return this.render(),!0}setData(e,t,n){this.graph=e,this.layout=t,this.edgeVisuals.clear(),this.disposeObjects(),this.buildRings(),this.buildEdges(e.hierarchyEdges,t,"hierarchy"),this.buildEdges(e.linkEdges,t,"links");for(let s of e.hoverLinkEdges){if(this.edgeVisuals.has(s.id))continue;let r=this.edgeVisual(s,t);r&&this.edgeVisuals.set(r.key,r)}this.buildNodes(e,t),this.setActive(this.active,n),this.render()}resize(e,t){this.width=Math.max(1,Math.floor(e)),this.height=Math.max(1,Math.floor(t)),this.renderer.setSize(this.width,this.height,!1),this.camera.left=-this.width/2,this.camera.right=this.width/2,this.camera.top=this.height/2,this.camera.bottom=-this.height/2,this.applyCamera(),this.updateLabels()}setView(e,t,n){this.centerX=Number.isFinite(e)?e:0,this.centerY=Number.isFinite(t)?t:0,this.zoom=Math.min(Math.max(Number.isFinite(n)?n:1,.003),6),this.updateNodeScale(),this.applyCamera();let s=this.active.highlightedEdges.size>0;s&&this.rebuildHighlights(),this.updateLabels(),s&&this.render()}getView(){return{centerX:this.centerX,centerY:this.centerY,zoom:this.zoom}}fitToLayout(){if(!this.layout)return;let e=160,t=Math.max(1,this.layout.bounds.maxX-this.layout.bounds.minX+e*2),n=Math.max(1,this.layout.bounds.maxY-this.layout.bounds.minY+e*2),s=Math.min(this.width/t,this.height/n,1.2)*.92;this.setView((this.layout.bounds.minX+this.layout.bounds.maxX)/2,(this.layout.bounds.minY+this.layout.bounds.maxY)/2,s)}playRevealFromRoot(e,t,n=1200){if(!this.layout||this.width<2||this.height<2)return;cancelAnimationFrame(this.revealFrame),this.revealOverlay?.remove();let s=this.layout.positions.get(e)??{x:0,y:0},r=this.worldToScreen(s.x,s.y),o=Math.hypot(Math.max(r.x,this.width-r.x),Math.max(r.y,this.height-r.y))+120,a=performance.now();this.container.style.setProperty("--mwm-reveal-x",`${r.x.toFixed(1)}px`),this.container.style.setProperty("--mwm-reveal-y",`${r.y.toFixed(1)}px`),this.container.style.setProperty("--mwm-reveal-radius","0px"),this.container.addClass("is-radial-revealing"),this.revealOverlay=this.container.createDiv({cls:"mwm-radial-loading gx-mask-text",text:t});let l=c=>{let u=Math.min(Math.max((c-a)/n,0),1),d=1-Math.pow(1-u,3);if(this.container.style.setProperty("--mwm-reveal-radius",`${(o*d).toFixed(1)}px`),u<1){this.revealFrame=window.requestAnimationFrame(l);return}this.container.removeClass("is-radial-revealing"),this.revealOverlay?.addClass("is-fading"),window.setTimeout(()=>{this.revealOverlay?.remove(),this.revealOverlay=null},220)};this.revealFrame=window.requestAnimationFrame(l)}setActive(e,t){this.active=e,this.updateNodeDim(),this.rebuildHighlights(),this.updateLabels(t),this.render()}render(){this.renderer.render(this.scene,this.camera)}worldToScreen(e,t){let n=new N(e,t,0).project(this.camera);return{x:(n.x+1)/2*this.width,y:(1-n.y)/2*this.height}}screenToWorld(e,t){return{x:this.centerX+(e-this.width/2)/this.zoom,y:this.centerY-(t-this.height/2)/this.zoom}}hitTest(e,t,n,s=!0){if(!this.graph||!this.layout)return{nodeId:null,edge:null};let r=this.screenToWorld(e,t),o=null;if(s)for(let l of this.graph.nodes){let c=this.layout.positions.get(l.id);if(!c)continue;let u=Math.hypot(r.x-c.x,r.y-c.y),d=Math.max(4,c.nodeRadius*this.palette().nodeScale*Bl(this.zoom)*.55),h=Math.max(c.nodeRadius*.36,d/Math.max(.003,this.zoom))+Math.max(5,6/this.zoom);u<=h&&(!o||ul.externalCount)?o.externalLinkOpacity:o.linkOpacity,depthWrite:!1}));n==="hierarchy"?this.hierarchySegments=a:this.linkSegments=a,this.scene.add(a)}buildNodes(e,t){let n=this.palette(),s=new Float32Array(e.nodes.length*3),r=new Float32Array(e.nodes.length*3),o=new Float32Array(e.nodes.length),a=new Float32Array(e.nodes.length),l=new Float32Array(e.nodes.length).fill(1);this.nodeIds=e.nodes.map(c=>c.id),e.nodes.forEach((c,u)=>{let d=t.positions.get(c.id),h=Ib(c,e.rootId,n);s[u*3]=d?.x??0,s[u*3+1]=d?.y??0,s[u*3+2]=1,r[u*3]=h.r,r[u*3+1]=h.g,r[u*3+2]=h.b,o[u]=Math.max(2.1,(d?.nodeRadius??8)*n.nodeScale),a[u]=c.type==="unresolved"||c.type==="external"||c.externalProxy?1:0}),this.nodeGeometry=new ht,this.nodeGeometry.setAttribute("position",new Ye(s,3)),this.nodeGeometry.setAttribute("color",new Ye(r,3)),this.nodeGeometry.setAttribute("aSize",new Ye(o,1)),this.nodeGeometry.setAttribute("aGhost",new Ye(a,1)),this.nodeGeometry.setAttribute("aDim",new Ye(l,1)),this.nodeMaterial=new ut({vertexShader:bl,fragmentShader:Ml,vertexColors:!0,transparent:!0,depthWrite:!1,uniforms:{uPixelScale:{value:1e3*Math.min(window.devicePixelRatio||1,2)},uSizeMul:{value:Bl(this.zoom)},uLightMode:{value:this.scheme==="day"?1:0},uMaxPoint:{value:58*Math.min(window.devicePixelRatio||1,2)}}}),this.nodePoints=new Bn(this.nodeGeometry,this.nodeMaterial),this.nodePoints.frustumCulled=!1,this.scene.add(this.nodePoints)}updateNodeDim(){if(!this.nodeGeometry)return;let e=this.nodeGeometry.getAttribute("aDim"),t=e.array;for(let n=0;no.dispose()):this.highlightSegments.material.dispose(),this.highlightSegments=null);let e=[],t=[],n=this.palette(),s=new me(n.focus),r=Math.max(.003,this.zoom||1);for(let o of this.active.highlightedEdges){let a=this.edgeVisuals.get(o);if(a)for(let l=0;l{let l=this.layout?.positions.get(a.id)??null;return l?{node:a,point:l,score:Ob(a,l,this.graph)}:null}).filter(a=>!!a).sort((a,l)=>l.score-a.score),s=Math.max(1,n.length-1),r=e==="auto"&&this.zoom>=.08?Math.round(Math.min(this.palette().maxLabels,Math.max(0,(this.zoom-.08)*84+Math.sqrt(this.zoom)*18))):0,o=0;for(let a=0;a=r)continue;o++}let f=this.worldToScreen(u.x,u.y);if(f.x<-160||f.y<-80||f.x>this.width+160||f.y>this.height+120)continue;let g=this.labelRoot.createDiv({cls:c.id===this.graph.rootId?"mwm-radial-label is-root":"mwm-radial-label"});g.setText(c.title);let x=Bb(this.zoom)*(c.id===this.graph.rootId?1.18:u.nodeRadius>=24?1.1:u.nodeRadius>=15?1.04:1),m=Math.max(c.id===this.graph.rootId?12:9.5,12*x);g.style.fontSize=`${m.toFixed(2)}px`,g.style.maxWidth=`${Math.round((c.id===this.graph.rootId?190:c.type==="folder"?156:u.nodeRadius>=18?146:124)*x)}px`,g.style.opacity=String(d?.96:Math.min(.9,.22+h*.68));let p=Math.max(5,u.nodeRadius*this.palette().nodeScale*Bl(this.zoom)*.62),v=Math.max(9,p+6*x);g.style.transform=`translate3d(${f.x.toFixed(1)}px, ${(f.y+v).toFixed(1)}px, 0)`,this.active.dimOthers&&!t.has(c.id)&&!this.active.relatedNodes.has(c.id)&&(g.style.opacity=String(Math.min(Number(g.style.opacity)||1,.34)),g.addClass("is-dim"))}}disposeObjects(){for(let e of[this.ringSegments,this.hierarchySegments,this.linkSegments,this.highlightSegments,this.nodePoints]){if(!e)continue;this.scene.remove(e),"geometry"in e&&e.geometry.dispose();let t=Array.isArray(e.material)?e.material:[e.material];for(let n of t)n.dispose()}this.ringSegments=null,this.hierarchySegments=null,this.linkSegments=null,this.highlightSegments=null,this.nodePoints=null,this.nodeGeometry=null,this.nodeMaterial=null,this.labelRoot.empty()}};function Vl(){return{hasActive:!1,dimOthers:!1,activeNodeId:null,activeLinkId:null,relatedNodes:new Set,highlightedEdges:new Set,labelNodes:new Set,pinnedNodeIds:new Set}}function Xp(i,e){let t=new ht;return t.setAttribute("position",new Ye(new Float32Array(i),3)),t.setAttribute("color",new Ye(new Float32Array(e),3)),t}function Rb(i,e){let t=Xp(i,e);return t.computeBoundingSphere(),t}function Pb(i,e,t,n,s,r,o){let a=s.x-n.x,l=s.y-n.y,c=Math.hypot(a,l);if(!Number.isFinite(c)||c<=.001)return;let u=Math.max(.1,r*.5),d=-l/c*u,h=a/c*u,f=[[n.x+d,n.y+h,o],[n.x-d,n.y-h,o],[s.x-d,s.y-h,o],[n.x+d,n.y+h,o],[s.x-d,s.y-h,o],[s.x+d,s.y+h,o]];for(let g of f)i.push(g[0],g[1],g[2]),Jh(e,t,1)}function Jh(i,e,t){i.push(e.r,e.g,e.b)}function Ib(i,e,t){return i.id===e?new me(t.root):i.type==="unresolved"?new me(t.unresolved):i.type==="external"||i.externalProxy?new me(t.external):i.type==="folder"&&i.representativeFile?new me(t.folderMeta):i.type==="folder"?new me(t.folder):new me(t.note)}function Lb(i,e,t){return i.unresolvedCount?new me(t.unresolved):i.externalCount?new me(e==="links"?t.externalLink:t.external):new me(e==="hierarchy"?t.tree:t.link)}function Nb(i,e,t){if(!t)return[i,e];if(t.kind==="outer"){let l=[i],c=t.sourceAngle,u=t.endAngle??t.targetAngle,d=u-c;Math.abs(d)>Math.PI&&(d+=d>0?-Math.PI*2:Math.PI*2),u=c+d;let h=Math.max(8,Math.ceil(Math.abs(d)/.18));for(let f=0;f<=h;f++){let g=c+d*f/h;l.push({x:t.centerX+Math.cos(g)*t.radius,y:t.centerY+Math.sin(g)*t.radius})}return l.push(e),l}let n=(i.x+e.x)/2,s=(i.y+e.y)/2,r=t.curveStrength??.16,o={x:n-n*r,y:s-s*r},a=[];for(let l=0;l<=12;l++){let c=l/12,u=(1-c)*(1-c),d=2*(1-c)*c,h=c*c;a.push({x:u*i.x+d*o.x+h*e.x,y:u*i.y+d*o.y+h*e.y})}return a}function Db(i,e){let t=1/0;for(let n=0;n=24?nn(.22,.56,o)*.96:e.nodeRadius>=15?nn(.42,.86,o)*.78:0,C=d?0:nn(.86,1.28,o)*.82,_=d?0:nn(1.12,1.46,o)*.98;return ai(Math.max(M,E,w,C,_),0,1)}function Bb(i){let e=ai(i,.003,6);return .62+nn(.06,1.48,e)*.88}function Bl(i){let e=ai(i,.003,6);return .7+nn(.07,.36,e)*.36+nn(.36,1.32,e)*.82+nn(1.32,3.2,e)*.72+nn(3.2,6,e)*.28}function nn(i,e,t){if(i===e)return t>=e?1:0;let n=ai((t-i)/(e-i),0,1);return n*n*(3-2*n)}function ai(i,e,t){return Number.isFinite(i)?Math.min(t,Math.max(e,i)):e}var Yl=require("obsidian");function Zp(i,e,t,n){let s=new Map,r=[],o=[],a=new Map,l=new Map,c=new Map,u=new Map,d=0,h=0,f=v=>zb(v,n.ignoreFolders),g=v=>{s.has(v.id)||f(v.id)||s.set(v.id,v)};g({id:Ze,path:Ze,title:Ip,type:"folder",parentId:null,depth:0,noteCount:0,linkCount:0,backlinkCount:0,descendantCount:0});let x=v=>{let b=Oi(v);if(!b)return Ze;let M=Ze;for(let E of b.split("/").filter(Boolean)){let w=M?`${M}/${E}`:E;if(f(w))return M;s.has(w)||g({id:w,path:w,title:Gl(w),type:"folder",parentId:M,depth:qp(w),noteCount:0,linkCount:0,backlinkCount:0,descendantCount:0}),M=w}return M};for(let v of i){d++;let b=Oi(v.path);if(!b||f(b))continue;if(v.kind==="folder"){x(b);continue}h++;let M=x(jh(b));g({id:b,path:b,title:v.basename||Gl(b).replace(/\.md$/i,""),type:"note",parentId:M,depth:qp(b),noteCount:1,linkCount:0,backlinkCount:0,descendantCount:0})}Vb(s,u);let m=(v,b,M,E)=>{if(v===b||!s.has(v)||!s.has(b))return;let w={id:`${E}:${v}->${b}:${o.length}`,type:E,source:v,target:b,weight:M};o.push(w);let C=s.get(v),_=s.get(b);C&&(C.linkCount+=M),_&&(_.backlinkCount+=M),Hl(l,v,w),Hl(c,b,w)};for(let[v,b]of Object.entries(e))if(!(!s.has(v)||f(v)))for(let[M,E]of Object.entries(b??{}))!s.has(M)||f(M)||m(v,M,Math.max(1,Number(E)||1),"link");if(n.includeUnresolvedLinks){for(let[v,b]of Object.entries(t))if(!(!s.has(v)||f(v)))for(let[M,E]of Object.entries(b??{})){let w=`unresolved:${v}:${M}`;if(!s.has(w)){let C=s.get(v);g({id:w,path:M,title:M,type:"unresolved",parentId:C?.parentId??Ze,depth:(C?.depth??0)+1,noteCount:0,linkCount:0,backlinkCount:0,descendantCount:0})}m(v,w,Math.max(1,Number(E)||1),"unresolved-link")}}Gb(s,r,a),Hb(s);let p={loadedEntries:d,scannedMarkdown:h,folders:[...s.values()].filter(v=>v.type==="folder").length,notes:[...s.values()].filter(v=>v.type==="note").length,unresolved:[...s.values()].filter(v=>v.type==="unresolved").length,hierarchyEdges:r.length,linkEdges:o.length,maxDepth:Math.max(0,...[...s.values()].map(v=>v.depth))};return{nodes:s,hierarchyEdges:r,linkEdges:o,childrenByParent:a,linkEdgesBySource:l,linkEdgesByTarget:c,folderRepresentatives:u,stats:p}}function Wl(i,e){if(!i||!e)return i?-1:e?1:0;let t=n=>n.type==="folder"?0:n.type==="note"?1:n.type==="external"?2:3;return i.depth-e.depth||t(i)-t(e)||i.title.localeCompare(e.title)||i.id.localeCompare(e.id)}function Oi(i){return String(i||"").trim().replace(/^\/+|\/+$/g,"")}function jh(i){let e=Oi(i),t=e.lastIndexOf("/");return t===-1?Ze:e.slice(0,t)}function Gl(i){let e=Oi(i),t=e.lastIndexOf("/");return t===-1?e:e.slice(t+1)}function qp(i){let e=Oi(i);return e?e.split("/").filter(Boolean).length:0}function zb(i,e){let t=Oi(i);return t?e.some(n=>t===n||t.startsWith(`${n}/`)):!1}function Vb(i,e){let t=new Map;for(let n of i.values())n.type==="note"&&n.parentId!==null&&Hl(t,n.parentId,n);for(let n of i.values()){if(n.type!=="folder"||n.id===Ze)continue;let s=Yp(n.title),r=(t.get(n.id)??[]).find(o=>Yp(o.title)===s);r&&(n.representativeFile=r.id,r.isRepresentativeFile=!0,r.representativeFor=n.id,e.set(n.id,r.id))}}function Gb(i,e,t){for(let n of i.values())n.parentId===null||!i.has(n.parentId)||(e.push({id:`hierarchy:${n.parentId}->${n.id}`,type:"hierarchy",source:n.parentId,target:n.id,weight:1}),Hl(t,n.parentId,n.id));for(let n of t.values())n.sort((s,r)=>Wl(i.get(s),i.get(r)))}function Hb(i){let e=[...i.values()].sort((t,n)=>n.depth-t.depth);for(let t of e){if(t.descendantCount=t.type==="note"?1:t.noteCount,!t.parentId||!i.has(t.parentId))continue;let n=i.get(t.parentId);if(!n)continue;let s=t.type==="note"?1:t.noteCount;n.descendantCount+=s,n.noteCount+=s,n.linkCount+=t.linkCount,n.backlinkCount+=t.backlinkCount}}function Yp(i){return i.replace(/\.md$/i,"").trim().toLowerCase()}function Hl(i,e,t){let n=i.get(e);n?n.push(t):i.set(e,[t])}function tm(i){let e=i.hiddenLegendItems.slice(),t=new Set(e);return{mode:"atlas",rootPath:Ze,focusPath:null,search:"",atlasDepth:i.atlasDepth,focusSiblingLimit:i.focusSiblingLimit,nodeLimit:i.renderNodeLimit,linkLimit:i.linkLimit,externalLinkAnchorLimit:i.externalLinkAnchorLimit,showLinkOverlay:i.showLinkOverlay||!t.has("link"),showExternalLinks:i.showExternalLinks,externalDetailMode:i.externalDetailMode,showCompleteRoot:!1,hoverHighlightMode:i.hoverHighlightMode,pinNeedsHoverLinks:!1,selectedNodeId:null,selectedLink:null,hiddenLegendItems:e,labelVisibility:i.labelVisibility}}function nm(i,e,t=Ot){return e.mode==="focus"?Xb(i,e,t):Wb(i,e,t)}function eu(i,e){if(e==null)return null;let t=i.nodes.get(e);return t?.isRepresentativeFile&&t.representativeFor&&i.nodes.has(t.representativeFor)?t.representativeFor:e}function Wb(i,e,t){let n=i.nodes.has(e.rootPath)?e.rootPath:Ze,s=i.nodes.get(n)?.depth??0,r=Ct(e.atlasDepth,1,80,t.atlasDepth),o=tu(e.search),a=new Set,l=[n];for(;l.length>0;){let c=l.pop(),u=c?i.nodes.get(c):i.nodes.get(Ze);if(!u)continue;let h=Math.max(0,u.depth-s)<=r,f=o.length>0&&Xl(u,o);if((h||f)&&(a.add(u.id),f&&Gs(i,u.id,a,n)),h||f)for(let g of[...i.childrenByParent.get(u.id)??[]].reverse())l.push(g)}if(o)for(let c of i.nodes.values())Xl(c,o)&&(a.add(c.id),Gs(i,c.id,a,n));return sm(i,a),im(i,a,n,e,t,null)}function Xb(i,e,t){let n=e.focusPath&&i.nodes.has(e.focusPath)?e.focusPath:eM(i),s=new Set([Ze]),r=Ct(e.focusSiblingLimit,10,1e3,t.focusSiblingLimit);if(n){s.add(n),Gs(i,n,s,Ze);let a=i.nodes.get(n);if(a?.parentId)for(let c of(i.childrenByParent.get(a.parentId)??[]).slice(0,r))s.add(c);let l=[...i.linkEdgesBySource.get(n)??[],...i.linkEdgesByTarget.get(n)??[]].sort((c,u)=>u.weight-c.weight);for(let c of l.slice(0,Math.max(30,r)))s.add(c.source),s.add(c.target),Gs(i,c.source,s,Ze),Gs(i,c.target,s,Ze)}let o=tu(e.search);if(o)for(let a of i.nodes.values())Xl(a,o)&&(s.add(a.id),Gs(i,a.id,s,Ze));return sm(i,s),im(i,s,Ze,e,t,n)}function im(i,e,t,n,s,r){let o=[...e].map(h=>i.nodes.get(h)).filter(h=>!!h);o.sort(Wl);let a=Yb(i,o,t,n,s,r);o=Zb(i,a.nodes);let l=new Set(o.map(h=>h.id)),c=i.hierarchyEdges.filter(h=>l.has(h.source)&&l.has(h.target)),u=qb(i,l,n,s,t);o=o.concat(u.externalNodes);let d=new Map(o.map(h=>[h.id,h]));return{nodes:o,nodesById:d,hierarchyEdges:c.concat(u.externalHierarchyEdges),linkEdges:u.linkEdges,hoverLinkEdges:u.hoverLinkEdges,rootId:t,focusId:eu(i,r),hiddenNodeCount:a.hiddenNodeCount,externalNodeCount:u.externalNodes.length,externalFileCount:u.externalNodes.filter(h=>h.externalProxy).length,externalGroupCount:u.externalNodes.filter(h=>h.type==="external"&&!h.externalProxy).length}}function qb(i,e,t,n,s){let r=new Set(t.hiddenLegendItems),o=t.showLinkOverlay&&!r.has("link"),a=o&&!r.has("dashed-link"),l=!r.has("outside-link"),c=!r.has("outside")||!r.has("outside-file"),u=Ys(t.hoverHighlightMode)||t.pinNeedsHoverLinks,d=t.showExternalLinks&&s!==Ze&&(c||l);if(!o&&!a&&!u&&!d)return{linkEdges:[],hoverLinkEdges:[],externalNodes:[],externalHierarchyEdges:[]};let h=new Map,f=new Map,g=[],x=Ct(t.externalLinkAnchorLimit,0,2e4,n.externalLinkAnchorLimit),m={fileCount:0,overflowId:null};for(let T of i.linkEdges){let P=Kp(i,T.source,e,s),R=Kp(i,T.target,e,s),I=0;if(d&&(!P&&R&&(P=Jp(i,T.source,s,f,g,x,m,jp(T,t,R)),P&&(I=1)),P&&!R&&(R=Jp(i,T.target,s,f,g,x,m,jp(T,t,P)),R&&(I=1))),!P||!R||P===R)continue;let H=`${P}->${R}`,W=h.get(H)??{id:`visible-link:${H}`,type:"visible-link",source:P,target:R,weight:0,rawCount:0,unresolvedCount:0,externalCount:0};W.weight+=T.weight,W.rawCount=(W.rawCount??0)+1,T.type==="unresolved-link"&&(W.unresolvedCount=(W.unresolvedCount??0)+1),W.externalCount=(W.externalCount??0)+I,h.set(H,W)}let p=[...h.values()].sort((T,P)=>em(P)-em(T)),v=T=>T.externalCount?l&&(!T.unresolvedCount||a):T.unresolvedCount?a:o,b=T=>T.externalCount?d&&l&&(!T.unresolvedCount||!r.has("dashed-link")):T.unresolvedCount?!r.has("dashed-link"):!r.has("link"),M=t.showCompleteRoot?3e4:Ct(t.linkLimit,0,3e4,n.linkLimit),E=p.filter(v).slice(0,M),w=t.showCompleteRoot?E:p.filter(b),C=new Set;for(let T of d?p.filter(P=>P.externalCount):u?w:[])f.has(T.source)&&C.add(T.source),f.has(T.target)&&C.add(T.target);let _=jb(C,f);return{linkEdges:E,hoverLinkEdges:w,externalNodes:_,externalHierarchyEdges:g.filter(T=>C.has(T.target))}}function Yb(i,e,t,n,s,r){if(n.showCompleteRoot&&e.length<=2e4)return{nodes:e,hiddenNodeCount:0};let o=Ct(n.nodeLimit,200,2e4,s.renderNodeLimit);if(e.length<=o)return{nodes:e,hiddenNodeCount:0};let a=e.slice().sort((d,h)=>Qp(h,t,r,n.search)-Qp(d,t,r,n.search)),l=new Map(e.map(d=>[d.id,d])),c=new Set([t,r].filter(d=>d!=null));$p(i,t,c,o,n.showCompleteRoot?128:64);for(let d of a){if(c.size>=o)break;$b(i,d.id,c,l,o),d.type==="folder"&&$p(i,d.id,c,o,n.showCompleteRoot?18:8)}let u=e.filter(d=>c.has(d.id));return{nodes:u,hiddenNodeCount:Math.max(0,e.length-u.length)}}function Zb(i,e){let t=new Set(e.map(n=>n.id));return e.filter(n=>!n.isRepresentativeFile||!n.representativeFor||!t.has(n.representativeFor))}function sm(i,e){for(let t of[...e]){let n=i.nodes.get(t);if(!(!n||n.type!=="folder"))for(let s of i.childrenByParent.get(t)??[]){let r=i.nodes.get(s);(r?.type==="note"||r?.type==="unresolved")&&e.add(s)}}}function Gs(i,e,t,n){let s=i.nodes.get(e);for(;s&&s.parentId!==null&&(t.add(s.id),s.id!==n);)s=i.nodes.get(s.parentId);t.add(n||Ze)}function $b(i,e,t,n,s){let r=[],o=n.get(e);for(;o&&!t.has(o.id);)r.push(o.id),o=o.parentId===null?void 0:n.get(o.parentId);for(let a=r.length-1;a>=0&&t.size=n||r>=s)return;let a=i.nodes.get(o);a?.type!=="note"&&a?.type!=="unresolved"||(t.add(o),r++)}}function Kp(i,e,t,n){let s=i.nodes.get(e);for(;s;){if(t.has(s.id))return s.id;if(s.id===n)return n;s=s.parentId===null?void 0:i.nodes.get(s.parentId)}return t.has(Ze)?Ze:null}function Jp(i,e,t,n,s,r,o,a){let l=i.nodes.get(e);if(!l)return null;let c=Qb(l,t);if(!c)return null;let u=Kb(i,c,t,n);return!a||l.type!=="note"&&l.type!=="unresolved"?u:n.has(l.id)?l.id:o.fileCount>=r?Jb(t,u,n,s,o):(n.set(l.id,{...l,externalProxy:!0,externalParentId:u,externalAnchorPath:c}),o.fileCount++,s.push({id:`external-hierarchy:${u}->${l.id}`,type:"external-hierarchy",source:u,target:l.id,weight:1}),l.id)}function Kb(i,e,t,n){let s=`external-group:${t}:${e}`;if(!n.has(s)){let r=i.nodes.get(e);n.set(s,{id:s,path:e,title:`Outside: ${r?.title??Gl(e)}`,type:"external",parentId:null,depth:(i.nodes.get(t)?.depth??0)+1,noteCount:r?.noteCount??r?.descendantCount??0,linkCount:0,backlinkCount:0,descendantCount:r?.descendantCount??r?.noteCount??0,externalAnchorPath:e})}return s}function Jb(i,e,t,n,s){return s.overflowId||(s.overflowId=`external-overflow:${i}`,t.set(s.overflowId,{id:s.overflowId,path:"outside current root",title:"More outside files",type:"external",parentId:null,depth:1,noteCount:0,linkCount:0,backlinkCount:0,descendantCount:0,externalProxy:!0,externalParentId:e,externalAnchorPath:null}),n.push({id:`external-hierarchy:${e}->${s.overflowId}`,type:"external-hierarchy",source:e,target:s.overflowId,weight:1})),s.overflowId}function jb(i,e){let t=new Map;for(let n of i){let s=e.get(n);if(s){if(s.externalParentId&&e.has(s.externalParentId)){let r=e.get(s.externalParentId);r&&t.set(r.id,r)}t.set(n,s)}}return[...t.values()].sort(Wl)}function jp(i,e,t){if(e.externalDetailMode==="exact")return!0;if(e.externalDetailMode==="grouped")return!1;if(e.selectedNodeId&&(i.source===e.selectedNodeId||i.target===e.selectedNodeId||t===e.selectedNodeId))return!0;let n=e.selectedLink;return!!(n&&(n.source===t||n.target===t||n.source===i.source||n.target===i.target))}function Qb(i,e){let t=i.type==="unresolved"?jh(i.path):i.path,n=t.split("/").filter(Boolean);if(!e)return n[0]??t;let s=e.split("/").filter(Boolean),r=0;for(;re.type==="note")?.id??null}function tu(i){return i.trim().toLowerCase()}function Xl(i,e){return i.title.toLowerCase().includes(e)||i.path.toLowerCase().includes(e)}function Qp(i,e,t,n){let s=0;return i.id===e&&(s+=1e6),i.id===t&&(s+=9e5),n&&Xl(i,tu(n))&&(s+=1e5),i.type==="folder"&&(s+=5e3+Math.min(4e3,Math.log2((i.noteCount||i.descendantCount||1)+1)*720)),i.type==="note"&&(s+=1e3+Math.min(3e3,Math.log2((i.linkCount||0)+(i.backlinkCount||0)+1)*520)),i.type==="unresolved"&&(s+=350),s-i.depth*3}function em(i){return(i.weight||1)*10+(i.rawCount||0)*4+(i.externalCount||0)*25+(i.unresolvedCount||0)*8}var ql=class{constructor(e,t){this.app=e;this.settings=t;this.model=null}get ready(){return this.model!==null}get nodes(){return this.model?.nodes??new Map}get stats(){return this.model?.stats??{loadedEntries:0,scannedMarkdown:0,folders:0,notes:0,unresolved:0,hierarchyEdges:0,linkEdges:0,maxDepth:0}}get linkEdgesBySource(){return this.model?.linkEdgesBySource??new Map}get linkEdgesByTarget(){return this.model?.linkEdgesByTarget??new Map}rebuild(e=this.settings){this.settings=e,this.model=Zp(this.collectVaultEntries(),this.app.metadataCache.resolvedLinks,this.app.metadataCache.unresolvedLinks,e)}buildVisibleGraph(e){return this.model||this.rebuild(),nm(this.model,e,this.settings)}visualNodeId(e){return this.model?eu(this.model,e):e??null}getActiveNotePath(){let e=this.app.workspace.getActiveFile();return e&&this.nodes.has(e.path)?e.path:[...this.nodes.values()].find(t=>t.type==="note")?.id??null}collectVaultEntries(){let e=[],t=typeof this.app.vault.getRoot=="function"?this.app.vault.getRoot():null,n=t&&Array.isArray(t.children)?[...t.children]:[];for(;n.length>0;){let s=n.pop();if(s instanceof Yl.TFolder){let r=Oi(s.path);if(r&&e.push({path:r,basename:s.name,kind:"folder"}),Array.isArray(s.children))for(let o=s.children.length-1;o>=0;o--){let a=s.children[o];a&&n.push(a)}}else s instanceof Yl.TFile&&s.extension==="md"&&e.push({path:s.path,basename:s.basename,kind:"note",size:s.stat.size})}if(e.length===0)for(let s of this.app.vault.getMarkdownFiles())e.push({path:s.path,basename:s.basename,kind:"note",size:s.stat.size});return e.some(s=>s.path===Ze)?e.filter(s=>s.path!==Ze):e}};var $l=class extends kt.Component{constructor(t,n,s,r,o,a){super();this.app=t;this.contentEl=n;this.settings=s;this.saveSettings=r;this.onViewMode=o;this.onLanguage=a;this.graph=null;this.layout=null;this.renderer=null;this.canvasHost=null;this.panel=null;this.panelBody=null;this.statsEl=null;this.activePanelPage="inspect";this.selectedNodeId=null;this.selectedLink=null;this.hoverNodeId=null;this.hoverLink=null;this.pinnedPaths=[];this.pinGroups=[];this.selectedPinIds=new Set;this.nextPinId=1;this.nextPinGroupId=1;this.pinGroupName="";this.drag=null;this.needsFit=!0;this.index=new ql(t,s.radial),this.state=tm(s.radial),this.saveSoon=(0,kt.debounce)(r,500,!0),this.redrawSoon=(0,kt.debounce)(()=>this.rebuild("metadata"),600,!0)}get counts(){return{nodes:this.graph?.nodes.length??0,links:this.graph?.linkEdges.length??0}}async start(){this.contentEl.addClass("mwm-radial-mode"),this.canvasHost=this.contentEl.createDiv({cls:"mwm-radial-host"}),this.renderer=new zl(this.canvasHost),this.syncThemeClass(),this.buildPanel(),this.buildFloatingControls(),this.bindRendererEvents(),this.registerVaultEvents(),this.rebuild("start")}resize(){!this.renderer||!this.canvasHost||(this.renderer.resize(this.canvasHost.clientWidth,this.canvasHost.clientHeight),this.needsFit&&(this.renderer.fitToLayout(),this.needsFit=!1),this.renderer.render())}onCssChange(){this.syncThemeClass()}rebuild(t){let n=this.radial();this.index.rebuild(n),this.state.hoverHighlightMode=n.hoverHighlightMode,this.state.labelVisibility=n.labelVisibility,this.state.hiddenLegendItems=n.hiddenLegendItems.slice(),this.state.pinNeedsHoverLinks=this.pinnedPathsNeedHoverLinks(),this.state.selectedNodeId=this.selectedNodeId,this.state.selectedLink=this.selectedLink;let s=this.index.buildVisibleGraph({...this.state,showLinkOverlay:!0,hiddenLegendItems:[],pinNeedsHoverLinks:!0}),r=new Set(this.state.hiddenLegendItems);this.state.showLinkOverlay||r.add("link");let o=nM(s,r);this.graph=o,this.layout=kp(s,{ringSpacing:Lp,nodeSpacing:Dp,swirlStrength:n.swirlStrength}),this.renderer?.setTheme(this.resolvedCanvasScheme()),this.renderer?.setData(o,this.layout,n.labelVisibility),this.resize(),this.applyActiveState(),this.renderPanel(),["start","manual","root","focus","atlas"].includes(t)&&this.renderer?.playRevealFromRoot(o.rootId,this.t("loading.radial"))}radial(){return this.settings.radial}registerVaultEvents(){this.registerEvent(this.app.metadataCache.on("resolved",this.redrawSoon)),this.registerEvent(this.app.vault.on("rename",this.redrawSoon)),this.registerEvent(this.app.vault.on("delete",this.redrawSoon)),this.registerEvent(this.app.vault.on("create",this.redrawSoon)),this.registerEvent(this.app.vault.on("modify",t=>{t instanceof kt.TFile&&t.extension==="md"&&this.redrawSoon()}))}bindRendererEvents(){let t=this.renderer?.domElement;if(!t)return;let n=u=>{u.preventDefault();let d=this.renderer;if(!d)return;let h=t.getBoundingClientRect(),f=d.screenToWorld(u.clientX-h.left,u.clientY-h.top),g=d.getView(),x=Math.pow(1.45,-u.deltaY/120),m=Math.min(Math.max(g.zoom*x,.003),6),p=f.x-(u.clientX-h.left-h.width/2)/m,v=f.y+(u.clientY-h.top-h.height/2)/m;d.setView(p,v,m)},s=u=>{if(u.button!==0)return;t.focus();let d=this.renderer?.getView();d&&(this.drag={pointerId:u.pointerId,startX:u.clientX,startY:u.clientY,centerX:d.centerX,centerY:d.centerY,moved:!1},t.setPointerCapture?.(u.pointerId))},r=u=>{let d=t.getBoundingClientRect();if(this.drag){let h=this.renderer;if(!h)return;let f=u.clientX-this.drag.startX,g=u.clientY-this.drag.startY;this.drag.moved=this.drag.moved||Math.hypot(f,g)>3;let x=h.getView();h.setView(this.drag.centerX-f/x.zoom,this.drag.centerY+g/x.zoom,x.zoom);return}this.updateHover(u.clientX-d.left,u.clientY-d.top)},o=u=>{let d=this.drag?.moved??!1;if(this.drag=null,t.releasePointerCapture?.(u.pointerId),d)return;let h=t.getBoundingClientRect();this.activateAt(u.clientX-h.left,u.clientY-h.top,u)},a=()=>{this.drag||(this.hoverNodeId=null,this.hoverLink=null,this.applyActiveState())},l=u=>{let d=t.getBoundingClientRect(),h=this.renderer?.hitTest(u.clientX-d.left,u.clientY-d.top,!1,this.hoverTargets().nodes);if(!h?.nodeId||!this.graph)return;let f=this.graph.nodesById.get(h.nodeId);f&&(u.preventDefault(),f.type==="note"?this.openNode(f.id,u):f.type==="folder"&&this.useAsRoot(f.id))},c=u=>{let d=t.getBoundingClientRect(),h=this.renderer?.hitTest(u.clientX-d.left,u.clientY-d.top,!1,this.hoverTargets().nodes);if(!h?.nodeId||!this.graph)return;let f=this.graph.nodesById.get(h.nodeId);f&&(u.preventDefault(),this.showNodeMenu(u,f))};t.addEventListener("wheel",n,{passive:!1}),t.addEventListener("pointerdown",s),t.addEventListener("pointermove",r),t.addEventListener("pointerup",o),t.addEventListener("pointerleave",a),t.addEventListener("dblclick",l),t.addEventListener("contextmenu",c),this.register(()=>{t.removeEventListener("wheel",n),t.removeEventListener("pointerdown",s),t.removeEventListener("pointermove",r),t.removeEventListener("pointerup",o),t.removeEventListener("pointerleave",a),t.removeEventListener("dblclick",l),t.removeEventListener("contextmenu",c)})}updateHover(t,n){let s=this.hoverTargets(),r=this.renderer?.hitTest(t,n,s.links,s.nodes),o=r?.nodeId??null,a=o?null:r?.edge??null;o===this.hoverNodeId&&(a?.id??null)===(this.hoverLink?.id??null)||(this.hoverNodeId=o,this.hoverLink=a,this.canvasHost?.toggleClass("is-pointing",!!(o||a)),this.applyActiveState())}activateAt(t,n,s){let r=this.hoverTargets(),o=this.renderer?.hitTest(t,n,r.links,r.nodes);o?.nodeId?(s.preventDefault(),this.selectedNodeId=o.nodeId,this.selectedLink=null,this.activePanelPage="inspect"):o?.edge?(s.preventDefault(),this.selectedNodeId=null,this.selectedLink=o.edge,this.activePanelPage="inspect"):(this.selectedNodeId=null,this.selectedLink=null),this.state.selectedNodeId=this.selectedNodeId,this.state.selectedLink=this.selectedLink,this.applyActiveState(),this.renderPanel()}applyActiveState(){if(!this.graph||!this.renderer)return;let t=this.resolveActiveState();this.renderer.setActive(t,this.radial().labelVisibility)}resolveActiveState(){if(!this.graph)return Vl();let t=this.hoverNodeId??this.selectedNodeId,n=this.hoverLink??this.selectedLink,s=Vl();s.activeNodeId=t,s.activeLinkId=n?.id??null;let r=(a,l,c)=>{if(!(!a||!this.graph?.nodesById.has(a))){if(s.relatedNodes.add(a),s.labelNodes.add(a),c&&s.pinnedNodeIds.add(a),Ys(l))for(let u of[...this.graph.linkEdges,...this.graph.hoverLinkEdges])u.source!==a&&u.target!==a||(s.highlightedEdges.add(u.id),s.relatedNodes.add(u.source),s.relatedNodes.add(u.target),s.labelNodes.add(u.source),s.labelNodes.add(u.target));tM(this.graph,a,zi(l),s)}},o=(a,l)=>{a&&(s.highlightedEdges.add(a.id),s.relatedNodes.add(a.source),s.relatedNodes.add(a.target),s.labelNodes.add(a.source),s.labelNodes.add(a.target),l&&(s.pinnedNodeIds.add(a.source),s.pinnedNodeIds.add(a.target)))};r(t,this.state.hoverHighlightMode,!1),o(n,!1);for(let a of this.pinnedPaths)a.kind==="node"?r(a.nodeId,a.mode,!0):o(this.findGraphEdgeForPin(a),!0);return s.hasActive=!!(t||n||this.pinnedPaths.length),s.dimOthers=!!(t||n),s}hoverTargets(){let t=os(this.radial().hoverTargetMode);return{nodes:t!=="links",links:t!=="nodes"}}buildPanel(){this.panel=this.contentEl.createDiv({cls:"galaxy-panel mwm-radial-panel mwm-map-panel"}),this.syncThemeClass();let t=this.panel.createDiv({cls:"galaxy-panel-header"});this.statsEl=t.createDiv({cls:"galaxy-panel-stats",text:"Mini World Map"});let n=t.createEl("button",{cls:"galaxy-panel-collapse",text:"-"});this.panelBody=this.panel.createDiv({cls:"galaxy-panel-body"}),n.addEventListener("click",()=>{let s=this.panelBody?.hasClass("is-hidden")??!1;this.panelBody?.toggleClass("is-hidden",!s),n.setText(s?"-":"+")}),this.renderPanel()}renderPanel(){if(!this.panelBody)return;let t=this.graph;this.panelBody.empty(),this.statsEl&&this.statsEl.setText(this.t("stats.counts",{nodes:t?.nodes.length??0,links:t?.linkEdges.length??0}));let n=this.panelBody.createDiv({cls:"mwm-mode-switch"});n.createDiv({cls:"mwm-mode-switch-label",text:this.t("view.mode")});let s=n.createDiv({cls:"galaxy-mode-row mwm-mode-row"});this.modeButton(s,ui(this.settings.language,"radial2d"),"radial2d"),this.modeButton(s,ui(this.settings.language,"map3d"),"map3d");let r=this.panelBody.createDiv({cls:"mwm-panel-settings mwm-radial-settings"}),o=r.createDiv({cls:"mwm-panel-tabs"});for(let[l,c]of[["inspect",this.t("tab.inspect")],["pins",this.t("tab.pins")],["view",this.t("tab.view")],["controls",this.t("tab.controls")],["defaults",this.t("tab.defaults")]])o.createEl("button",{cls:this.activePanelPage===l?"is-active":"",text:c}).addEventListener("click",()=>{this.activePanelPage=l,this.renderPanel()});let a=r.createDiv({cls:"mwm-panel-page"});this.activePanelPage==="pins"?this.renderPinsPage(a):this.activePanelPage==="view"?this.renderViewPage(a):this.activePanelPage==="controls"?this.renderControlsPage(a):this.activePanelPage==="defaults"?this.renderDefaultsPage(a):this.renderInspectPage(a)}modeButton(t,n,s){let r=this.settings.viewMode===s;t.createEl("button",{cls:r?"is-active":"",text:n}).addEventListener("click",()=>this.onViewMode(s))}renderInspectPage(t){if(!this.graph)return;if(this.selectedLink){this.renderLinkInspect(t,this.selectedLink);return}let n=this.graph.nodesById.get(this.selectedNodeId??this.graph.focusId??this.graph.rootId);if(t.createDiv({cls:"mwm-side-title",text:n?.title??"Mini World Map"}),!n)return;t.createDiv({cls:"mwm-side-path",text:n.path||"/"});let s=t.createDiv({cls:"mwm-facts"});for(let[o,a]of[[this.t("inspect.type"),n.externalProxy?`${this.t("inspect.external")} ${n.type}`:n.type],[this.t("inspect.depth"),String(n.depth)],[this.t("inspect.notes"),String(n.noteCount||n.descendantCount||0)],[this.t("inspect.out"),String(n.linkCount||0)],[this.t("inspect.in"),String(n.backlinkCount||0)]])s.createSpan({text:o}),s.createSpan({text:a});let r=t.createDiv({cls:"galaxy-panel-row"});this.button(r,this.t("common.pin"),()=>this.pinNode(n)),n.type==="note"&&this.button(r,this.t("common.open"),()=>void this.openNode(n.id)),n.type==="note"&&this.button(r,this.t("common.focus"),()=>this.focusNote(n.id)),n.type==="folder"&&this.button(r,this.t("common.root"),()=>this.useAsRoot(n.id)),this.renderNeighborList(t,n)}renderLinkInspect(t,n){t.createDiv({cls:"mwm-side-title",text:this.t("inspect.linkOverlay")});let s=this.graph?.nodesById.get(n.source),r=this.graph?.nodesById.get(n.target),o=t.createDiv({cls:"mwm-facts"});for(let[l,c]of[[this.t("common.source"),s?.title??n.source],[this.t("common.target"),r?.title??n.target],[this.t("inspect.weight"),String(n.weight||0)],[this.t("inspect.raw"),String(n.rawCount||n.weight||0)],[this.t("inspect.unresolved"),String(n.unresolvedCount||0)],[this.t("inspect.external"),String(n.externalCount||0)]])o.createSpan({text:l}),o.createSpan({text:c});let a=t.createDiv({cls:"galaxy-panel-row"});this.button(a,this.t("common.pin"),()=>this.pinLink(n)),this.button(a,this.t("common.source"),()=>this.inspectNode(n.source)),this.button(a,this.t("common.target"),()=>this.inspectNode(n.target))}renderNeighborList(t,n){let s=(this.index.linkEdgesBySource.get(n.id)??[]).slice(0,20),r=(this.index.linkEdgesByTarget.get(n.id)??[]).slice(0,20);for(let[o,a,l]of[[this.t("inspect.outgoing",{count:s.length}),s,"target"],[this.t("inspect.backlinks",{count:r.length}),r,"source"]]){t.createDiv({cls:"mwm-side-heading",text:o}),a.length===0&&t.createDiv({cls:"mwm-side-muted",text:this.t("common.none")});for(let c of a){let u=this.index.nodes.get(c[l]);if(!u)continue;t.createEl("button",{cls:"mwm-link-row",text:`${u.title} (${c.weight})`}).addEventListener("click",()=>this.inspectNode(u.id))}}}renderPinsPage(t){let n=t.createDiv({cls:"galaxy-panel-row"});this.button(n,this.t("common.pinCurrent"),()=>this.pinCurrent()),this.button(n,this.t("common.clear"),()=>this.clearPins());let s=t.createDiv({cls:"mwm-pin-group-row"}),r=s.createEl("input",{attr:{value:this.pinGroupName,placeholder:this.t("pins.groupName")}});if(r.addEventListener("input",()=>this.pinGroupName=r.value),this.button(s,this.t("common.group"),()=>this.groupSelectedPins()),this.pinnedPaths.length===0){t.createDiv({cls:"mwm-side-muted",text:this.t("pins.empty")});return}for(let o of this.pinnedPaths){let a=t.createDiv({cls:"mwm-pin-row"}),l=a.createEl("input",{attr:{type:"checkbox"}});l.checked=this.selectedPinIds.has(o.id),l.addEventListener("change",()=>{l.checked?this.selectedPinIds.add(o.id):this.selectedPinIds.delete(o.id),this.renderPanel()}),a.createEl("button",{cls:"mwm-pin-main",text:o.title}).addEventListener("click",()=>this.locatePin(o)),this.button(a,this.t("common.inspect"),()=>this.inspectPin(o)),this.button(a,"X",()=>this.removePin(o.id))}}renderViewPage(t){this.textInput(t,this.t("common.search"),this.state.search,r=>{this.state.search=r.trim(),this.needsFit=!0,this.rebuild("search")}),this.select(t,this.t("view.theme"),this.radial().colorScheme,fu(this.settings.language),r=>{this.radial().colorScheme=eo(r),this.saveSoon(),this.syncThemeClass()});let n=t.createDiv({cls:"galaxy-panel-row"});this.button(n,this.t("view.atlas"),()=>this.resetToAtlas(),this.state.mode==="atlas"),this.button(n,this.t("view.focus"),()=>this.focusActiveNote(),this.state.mode==="focus"),this.button(n,this.t("view.vaultRoot"),()=>this.resetToRoot());let s=t.createDiv({cls:"galaxy-panel-row"});this.button(s,this.t("common.fit"),()=>this.renderer?.fitToLayout()),this.button(s,this.t("common.rebuild"),()=>this.rebuild("manual"))}renderControlsPage(t){let n=this.radial();this.numberInput(t,this.t("control.depth"),this.state.atlasDepth,1,80,1,o=>{this.state.atlasDepth=o,this.rebuild("depth")}),this.numberInput(t,this.t("control.nodes"),this.state.nodeLimit,200,2e4,100,o=>{this.state.nodeLimit=o,this.rebuild("nodes")}),this.numberInput(t,this.t("control.noteLinks"),this.state.linkLimit,0,3e4,50,o=>{this.state.linkLimit=o,this.rebuild("links")});let s=new Set(n.hiddenLegendItems),r=this.state.showLinkOverlay&&!s.has("link");this.toggle(t,this.t("control.showNoteLinks"),r,o=>{n.showLinkOverlay=o,this.state.showLinkOverlay=o,this.setLegendHidden("link",!o),this.saveSoon(),this.rebuild("link-overlay")}),this.select(t,this.t("control.hover"),this.state.hoverHighlightMode,to(this.settings.language,Hs),o=>{let a=zi(o);this.state.hoverHighlightMode=a,n.hoverHighlightMode=a,this.saveSoon(),this.rebuild("hover")}),this.select(t,this.t("control.hoverTargets"),n.hoverTargetMode,no(this.settings.language,Ws),o=>{n.hoverTargetMode=os(o),this.clearDisallowedHoverTargets(),this.saveSoon(),this.applyActiveState(),this.renderPanel()}),this.select(t,this.t("control.labels"),n.labelVisibility,io(this.settings.language,jr),o=>{n.labelVisibility=qs(o),this.state.labelVisibility=n.labelVisibility,this.saveSoon(),this.applyActiveState()}),this.numberInput(t,this.t("control.spin"),n.swirlStrength,0,100,1,o=>{n.swirlStrength=o,this.saveSoon(),this.rebuild("spin")}),this.toggle(t,this.t("control.outsideLinks"),this.state.showExternalLinks,o=>{this.state.showExternalLinks=o,n.showExternalLinks=o,this.saveSoon(),this.rebuild("external")}),this.select(t,this.t("control.outsideDetail"),this.state.externalDetailMode,pu(this.settings.language),o=>{this.state.externalDetailMode=rc(o),n.externalDetailMode=this.state.externalDetailMode,this.saveSoon(),this.rebuild("external-mode")}),this.numberInput(t,this.t("control.exactOutsideFiles"),this.state.externalLinkAnchorLimit,0,2e4,50,o=>{this.state.externalLinkAnchorLimit=o,n.externalLinkAnchorLimit=o,this.saveSoon(),this.rebuild("external-limit")}),this.renderLegend(t)}renderDefaultsPage(t){let n=this.radial();this.numberInput(t,this.t("control.defaultDepth"),n.atlasDepth,1,80,1,s=>{n.atlasDepth=s,this.state.atlasDepth=s,this.saveSoon()}),this.numberInput(t,this.t("control.defaultNodes"),n.renderNodeLimit,200,2e4,100,s=>{n.renderNodeLimit=s,this.state.nodeLimit=s,this.saveSoon()}),this.numberInput(t,this.t("control.defaultNoteLinks"),n.linkLimit,0,3e4,50,s=>{n.linkLimit=s,this.state.linkLimit=s,this.saveSoon()}),this.toggle(t,this.t("control.unresolvedLinks"),n.includeUnresolvedLinks,s=>{n.includeUnresolvedLinks=s,this.saveSoon(),this.rebuild("unresolved")}),this.textArea(t,this.t("control.ignoredFolders"),n.ignoreFolders.join(` +`),s=>{n.ignoreFolders=s.split(` +`).map(r=>r.trim()).filter(Boolean),this.saveSoon(),this.rebuild("ignored")})}renderLegend(t){t.createDiv({cls:"mwm-side-heading",text:this.t("control.legend")});let n=new Set(this.radial().hiddenLegendItems);for(let[s,r,o,a]of ic){let l=t.createEl("label",{cls:n.has(s)?"mwm-legend-item is-muted":"mwm-legend-item",attr:{title:this.t(o)}}),c=l.createEl("input",{attr:{type:"checkbox"}});c.checked=!n.has(s),l.createSpan({cls:`mwm-legend-mark ${a}`});let u=l.createSpan({cls:"mwm-legend-copy"});u.createSpan({cls:"mwm-legend-label",text:this.t(r)}),u.createSpan({cls:"mwm-legend-desc",text:this.t(o)}),c.addEventListener("change",()=>{let d=c.checked;d?n.delete(s):n.add(s),this.radial().hiddenLegendItems=[...n],this.state.hiddenLegendItems=[...n],s==="link"&&(this.radial().showLinkOverlay=d,this.state.showLinkOverlay=d),this.saveSoon(),this.rebuild("legend")})}}setLegendHidden(t,n){let s=new Set(this.radial().hiddenLegendItems);n?s.add(t):s.delete(t),this.radial().hiddenLegendItems=[...s],this.state.hiddenLegendItems=[...s]}buildFloatingControls(){let t=this.canvasHost;if(!t)return;let n=t.createDiv({cls:"mwm-floating-controls"}),s=n.createEl("button",{cls:"mwm-floating-button",attr:{type:"button",title:this.t("language"),"aria-label":this.t("language")}});(0,kt.setIcon)(s,"languages"),s.addEventListener("click",r=>{r.preventDefault(),r.stopPropagation(),this.openLanguageMenu(s)}),this.iconButton(n,"home",this.t("view.vaultRoot"),()=>this.resetToRoot()),this.iconButton(n,"pin",this.t("common.pinCurrent"),()=>this.pinCurrent()),this.iconButton(n,"maximize",this.t("common.fit"),()=>this.renderer?.fitToLayout()),this.iconButton(n,"refresh-cw",this.t("common.rebuild"),()=>this.rebuild("manual"))}openLanguageMenu(t){let n=new kt.Menu;for(let[r,o]of as(this.settings.language))n.addItem(a=>{a.setTitle(o),r===this.settings.language&&a.setIcon("check"),a.onClick(()=>{let l=rs(r);l!==this.settings.language&&(this.settings.language=l,this.onLanguage(l),this.renderPanel())})});let s=t.getBoundingClientRect();n.showAtPosition({x:s.right,y:s.bottom})}iconButton(t,n,s,r){let o=t.createEl("button",{cls:"mwm-floating-button",attr:{type:"button",title:s,"aria-label":s}});return(0,kt.setIcon)(o,n),o.addEventListener("click",a=>{a.preventDefault(),a.stopPropagation(),r()}),o}button(t,n,s,r=!1){let o=t.createEl("button",{cls:r?"is-active":"",text:n});return o.addEventListener("click",s),o}textInput(t,n,s,r){let o=t.createEl("label",{cls:"mwm-panel-field"});o.createSpan({text:n});let a=o.createEl("input",{attr:{value:s,type:"search"}});a.addEventListener("change",()=>r(a.value))}numberInput(t,n,s,r,o,a,l){let c=t.createEl("label",{cls:"mwm-panel-field"});c.createSpan({text:n});let u=c.createEl("input",{attr:{value:String(s),type:"number",min:String(r),max:String(o),step:String(a)}});u.addEventListener("change",()=>l(Ct(u.value,r,o,s)))}textArea(t,n,s,r){let o=t.createEl("label",{cls:"mwm-panel-field"});o.createSpan({text:n});let a=o.createEl("textarea");a.value=s,a.addEventListener("change",()=>r(a.value))}toggle(t,n,s,r,o){let a=t.createEl("label",{cls:"mwm-panel-toggle",attr:{title:o??n}}),l=a.createEl("input",{attr:{type:"checkbox"}});l.checked=s,a.createSpan({text:n}),l.addEventListener("change",()=>r(l.checked))}select(t,n,s,r,o){let a=t.createEl("label",{cls:"mwm-panel-field"});a.createSpan({text:n});let l=a.createEl("select");for(let[c,u]of r)l.createEl("option",{attr:{value:c},text:u});l.value=s,l.addEventListener("change",()=>o(l.value))}pinCurrent(){this.selectedLink?this.pinLink(this.selectedLink):this.selectedNodeId&&this.graph?.nodesById.has(this.selectedNodeId)?this.pinNode(this.graph.nodesById.get(this.selectedNodeId)):this.hoverLink?this.pinLink(this.hoverLink):this.hoverNodeId&&this.graph?.nodesById.has(this.hoverNodeId)?this.pinNode(this.graph.nodesById.get(this.hoverNodeId)):new kt.Notice(this.t("pins.selectBeforePin"))}pinNode(t){this.addPin({kind:"node",nodeId:t.id,mode:this.state.hoverHighlightMode,title:t.title,path:t.path||"/"})}pinLink(t){let n=this.graph?.nodesById.get(t.source),s=this.graph?.nodesById.get(t.target);this.addPin({kind:"link",edgeId:t.id,source:t.source,target:t.target,mode:"note-links",title:`${n?.title??t.source} -> ${s?.title??t.target}`,path:`${n?.path??t.source} -> ${s?.path??t.target}`})}addPin(t){let n=t.kind==="node"?`node:${t.nodeId}:${t.mode}`:`link:${t.edgeId??`${t.source}->${t.target}`}`,s=this.pinnedPaths.find(o=>o.key===n);if(s){this.selectedPinIds.add(s.id),new kt.Notice(this.t("pins.already")),this.renderPanel();return}let r={...t,id:`pin-${this.nextPinId++}`,key:n,groupId:null};this.pinnedPaths.push(r),this.selectedPinIds.add(r.id),this.state.pinNeedsHoverLinks=this.pinnedPathsNeedHoverLinks(),this.applyActiveState(),this.activePanelPage="pins",this.renderPanel()}removePin(t){this.pinnedPaths=this.pinnedPaths.filter(n=>n.id!==t),this.selectedPinIds.delete(t),this.state.pinNeedsHoverLinks=this.pinnedPathsNeedHoverLinks(),this.applyActiveState(),this.renderPanel()}clearPins(){this.pinnedPaths=[],this.pinGroups=[],this.selectedPinIds.clear(),this.state.pinNeedsHoverLinks=!1,this.applyActiveState(),this.renderPanel()}groupSelectedPins(){let t=this.pinnedPaths.filter(s=>this.selectedPinIds.has(s.id));if(t.length===0){new kt.Notice(this.t("pins.selectFirst"));return}let n=`pin-group-${this.nextPinGroupId++}`;this.pinGroups.push({id:n,name:this.pinGroupName.trim()||`Group ${this.pinGroups.length+1}`});for(let s of t)s.groupId=n;this.pinGroupName="",this.selectedPinIds.clear(),this.renderPanel()}locatePin(t){let n=t.kind==="node"?t.nodeId:t.source;if(!n)return;let s=this.renderer?.nodePoint(n),r=this.renderer?.getView();s&&r&&this.renderer?.setView(s.x,s.y,Math.max(r.zoom,.22))}inspectPin(t){this.activePanelPage="inspect",t.kind==="node"&&t.nodeId?this.inspectNode(t.nodeId):this.selectedLink=this.findGraphEdgeForPin(t),this.applyActiveState(),this.renderPanel()}findGraphEdgeForPin(t){return t.kind!=="link"||!this.graph?null:[...this.graph.linkEdges,...this.graph.hoverLinkEdges].find(n=>n.id===t.edgeId||n.source===t.source&&n.target===t.target)??null}pinnedPathsNeedHoverLinks(){return this.pinnedPaths.some(t=>t.kind==="link"||t.kind==="node"&&Ys(t.mode))}clearDisallowedHoverTargets(){let t=this.hoverTargets();t.nodes||(this.hoverNodeId=null),t.links||(this.hoverLink=null),this.canvasHost?.toggleClass("is-pointing",!!(t.nodes&&this.hoverNodeId||t.links&&this.hoverLink))}inspectNode(t){this.selectedNodeId=t,this.selectedLink=null,this.applyActiveState(),this.renderPanel()}resetToAtlas(){this.state.mode="atlas",this.state.rootPath=this.state.rootPath||Ze,this.state.focusPath=null,this.needsFit=!0,this.rebuild("atlas")}resetToRoot(){this.state.mode="atlas",this.state.rootPath=Ze,this.state.focusPath=null,this.needsFit=!0,this.rebuild("root")}useAsRoot(t){this.state.mode="atlas",this.state.rootPath=t,this.state.focusPath=null,this.needsFit=!0,this.rebuild("root")}focusActiveNote(){let t=this.index.getActiveNotePath();t&&this.focusNote(t)}focusNote(t){this.state.mode="focus",this.state.focusPath=t,this.state.rootPath=Ze,this.needsFit=!0,this.rebuild("focus")}showNodeMenu(t,n){let s=new kt.Menu;n.type==="note"&&(s.addItem(r=>r.setTitle(this.t("context.openNote")).setIcon("file-text").onClick(()=>void this.openNode(n.id,t))),s.addItem(r=>r.setTitle(this.t("context.focusNote")).setIcon("locate-fixed").onClick(()=>this.focusNote(n.id)))),n.type==="folder"&&(s.addItem(r=>r.setTitle(this.t("context.useAsRoot")).setIcon("folder-open").onClick(()=>this.useAsRoot(n.id))),n.representativeFile&&s.addItem(r=>r.setTitle(this.t("context.openRepresentative")).setIcon("file-text").onClick(()=>void this.openNode(n.representativeFile,t)))),s.addItem(r=>r.setTitle(this.t("context.pinPath")).setIcon("pin").onClick(()=>this.pinNode(n))),s.showAtMouseEvent(t)}async openNode(t,n){let s=this.app.vault.getAbstractFileByPath(t);if(!(s instanceof kt.TFile))return;let r=n&&(n.metaKey||n.ctrlKey)?"split":"tab";await this.app.workspace.openLinkText(s.path,"",r,{active:!0})}syncThemeClass(){let t=this.resolvedCanvasScheme(),n=this.resolvedPanelScheme();this.contentEl.toggleClass("is-day-scheme",t==="day"),this.contentEl.toggleClass("is-night-scheme",t==="night"),this.panel?.removeClass("gx-theme-dark"),this.panel?.removeClass("gx-theme-light"),this.panel?.addClass(n==="day"?"gx-theme-light":"gx-theme-dark"),(this.renderer?.setTheme(t)??!1)&&this.graph&&this.layout&&this.renderer?.setData(this.graph,this.layout,this.radial().labelVisibility)}resolvedCanvasScheme(){let t=eo(this.radial().colorScheme);return t==="day"||t==="night"?t:activeDocument.body.hasClass("theme-dark")?"night":"day"}resolvedPanelScheme(){return activeDocument.body.hasClass("theme-dark")?"night":"day"}t(t,n={}){return Ce(this.settings.language,t,n)}dispose(){this.unload(),this.renderer?.dispose(),this.renderer=null,this.contentEl.removeClass("mwm-radial-mode"),this.contentEl.empty()}};function tM(i,e,t,n){if(t==="none"||t==="note-links")return;let s=new Map(i.hierarchyEdges.map(a=>[a.target,a])),r=new Map;for(let a of i.hierarchyEdges){let l=r.get(a.source);l?l.push(a):r.set(a.source,[a])}let o=a=>{n.highlightedEdges.add(a.id),n.relatedNodes.add(a.source),n.relatedNodes.add(a.target),n.labelNodes.add(a.source),n.labelNodes.add(a.target)};if(t==="hierarchy-parents"||t==="hierarchy-parents-direct"||t==="hierarchy-all"){let a=s.get(e);for(;a;)o(a),a=s.get(a.source)}if(t==="hierarchy-direct-children"||t==="hierarchy-parents-direct"||t==="hierarchy-all")for(let a of r.get(e)??[])o(a);if(t==="hierarchy-descendants"||t==="hierarchy-all"){let a=[...r.get(e)??[]];for(;a.length>0;){let l=a.pop();l&&(o(l),a.push(...r.get(l.target)??[]))}}}function nM(i,e){if(e.size===0)return i;let t=d=>!(d.id===i.rootId&&e.has("root")||d.externalProxy&&e.has("outside-file")||d.type==="external"&&!d.externalProxy&&e.has("outside")||d.type==="unresolved"&&e.has("missing")||d.type==="folder"&&d.representativeFile&&e.has("folder-meta")||d.type==="folder"&&!d.representativeFile&&e.has("folder")||d.type==="note"&&e.has("file")),n=i.nodes.filter(t),s=new Set(n.map(d=>d.id)),r=d=>s.has(d.source)&&s.has(d.target),o=e.has("tree")?[]:i.hierarchyEdges.filter(r),a=d=>!r(d)||d.externalCount&&e.has("outside-link")||d.unresolvedCount&&e.has("dashed-link")?!1:!!d.externalCount||!e.has("link"),l=d=>!(!r(d)||d.externalCount&&e.has("outside-link")||d.unresolvedCount&&e.has("dashed-link")),c=i.linkEdges.filter(a),u=i.hoverLinkEdges.filter(l);return{...i,nodes:n,nodesById:new Map(n.map(d=>[d.id,d])),hierarchyEdges:o,linkEdges:c,hoverLinkEdges:u}}var Ui=class extends rm.ItemView{constructor(t,n){super(t);this.host=n;this.navigation=!0;this.controller=null}getViewType(){return Wn}getDisplayText(){return"Mini World Map"}getIcon(){return"network"}get counts(){return this.controller?.counts??{nodes:0,links:0}}async onOpen(){this.contentEl.empty(),this.contentEl.addClass("mini-world-map-view","galaxy-view-content"),this.registerEvent(this.app.workspace.on("css-change",()=>this.controller?.onCssChange?.())),this.tryInit()}onResize(){if(!this.controller){this.tryInit();return}this.controller.resize()}switchMode(t){if(this.host.settings.viewMode!==t){this.host.setViewMode(t);return}this.rebuild()}tryInit(){if(this.controller)return;let{clientWidth:t,clientHeight:n}=this.contentEl;if(!(t<10||n<10))if(this.host.settings.viewMode==="map3d"){let s=new zs(this.app,this.contentEl,this.host.settings.galaxy3d,()=>void this.host.saveSettings(),r=>this.switchMode(r),this.host.settings.language,r=>this.host.setLanguage(r));s.onContextLost=()=>this.rebuild(),this.controller=s,this.addChild(s.store),s.start()}else{let s=new $l(this.app,this.contentEl,this.host.settings,()=>void this.host.saveSettings(),r=>this.switchMode(r),r=>this.host.setLanguage(r));this.controller=s,this.addChild(s),s.start()}}rebuild(){this.controller?.dispose(),this.controller=null,this.contentEl.empty(),this.tryInit()}async onClose(){this.controller?.dispose(),this.controller=null,this.contentEl.empty()}};var Jl=class extends Et.Plugin{constructor(){super(...arguments);this.settings=sc(null)}async onload(){this.settings=sc(await this.loadData()),this.registerView(Wn,t=>new Ui(t,this)),this.addRibbonIcon("network","Open Mini World Map",()=>{this.activateView()}),this.addCommand({id:"open-map",name:"Open Mini World Map",callback:()=>void this.activateView()}),this.addCommand({id:"toggle-render-mode",name:"Toggle Mini World Map render mode",callback:()=>{this.setViewMode(this.settings.viewMode==="radial2d"?"map3d":"radial2d"),this.activateView()}}),this.addCommand({id:"rebuild-index",name:"Rebuild Mini World Map index",callback:()=>{let t=!1;for(let n of this.app.workspace.getLeavesOfType(Wn)){let s=n.view;s instanceof Ui&&"rebuild"in(s.controller??{})&&(s.controller.rebuild?.("manual"),t=!0)}new Et.Notice(Ce(this.settings.language,t?"notice.rebuilt":"notice.openToBuild"))}}),this.addCommand({id:"search-3d",name:"Search 3D map node and fly",callback:()=>{this.activateView().then(t=>{t?.controller instanceof zs?t.controller.openSearch():new Et.Notice(Ce(this.settings.language,"notice.switchTo3d"))})}}),this.addSettingTab(new nu(this.app,this))}async saveSettings(){await this.saveData(this.settings)}setViewMode(t){this.settings.viewMode=Qr(t),this.saveSettings();for(let n of this.app.workspace.getLeavesOfType(Wn)){let s=n.view;s instanceof Ui&&s.switchMode(this.settings.viewMode)}}setLanguage(t){this.settings.language=rs(t),this.saveSettings();for(let n of this.app.workspace.getLeavesOfType(Wn)){let s=n.view;s instanceof Ui&&s.switchMode(this.settings.viewMode)}}async activateView(){let{workspace:t}=this.app,n=t.getLeavesOfType(Wn)[0]??null;return n||(n=t.getLeaf(!0),await n.setViewState({type:Wn,active:!0})),n.isDeferred&&await n.loadIfDeferred(),await t.revealLeaf(n),n.view instanceof Ui?n.view:null}},nu=class extends Et.PluginSettingTab{constructor(t,n){super(t,n);this.plugin=n}display(){let{containerEl:t}=this,n=this.plugin.settings.radial,s=this.plugin.settings.language;t.empty(),t.createEl("h2",{text:Ce(s,"settings.title")}),new Et.Setting(t).setName(Ce(s,"language")).setDesc(Ce(s,"settings.languageDesc")).addDropdown(r=>{for(let[o,a]of as(s))r.addOption(o,a);r.setValue(this.plugin.settings.language).onChange(async o=>{this.plugin.settings.language=rs(o),await this.plugin.saveSettings(),this.display()})}),new Et.Setting(t).setName(Ce(s,"settings.defaultMode")).setDesc(Ce(s,"settings.defaultModeDesc")).addDropdown(r=>r.addOption("radial2d",ui(s,"radial2d")).addOption("map3d",ui(s,"map3d")).setValue(this.plugin.settings.viewMode).onChange(async o=>{this.plugin.settings.viewMode=Qr(o),await this.plugin.saveSettings()})),new Et.Setting(t).setName(Ce(s,"control.defaultDepth")).setDesc(Ce(s,"settings.depthDesc")).addSlider(r=>r.setLimits(1,20,1).setValue(n.atlasDepth).setDynamicTooltip().onChange(async o=>{n.atlasDepth=Ct(o,1,80,Ot.atlasDepth),await this.plugin.saveSettings()})),new Et.Setting(t).setName(Ce(s,"control.defaultNodes")).setDesc(Ce(s,"settings.nodeLimitDesc")).addText(r=>r.setPlaceholder(String(Ot.renderNodeLimit)).setValue(String(n.renderNodeLimit)).onChange(async o=>{n.renderNodeLimit=Ct(o,200,2e4,Ot.renderNodeLimit),await this.plugin.saveSettings()})),new Et.Setting(t).setName(Ce(s,"control.defaultNoteLinks")).setDesc(Ce(s,"settings.linkLimitDesc")).addText(r=>r.setPlaceholder(String(Ot.linkLimit)).setValue(String(n.linkLimit)).onChange(async o=>{n.linkLimit=Ct(o,0,3e4,Ot.linkLimit),await this.plugin.saveSettings()})),new Et.Setting(t).setName(Ce(s,"control.showNoteLinks")).setDesc(Ce(s,"settings.showLinksDesc")).addToggle(r=>r.setValue(n.showLinkOverlay&&!n.hiddenLegendItems.includes("link")).onChange(async o=>{n.showLinkOverlay=o;let a=new Set(n.hiddenLegendItems);o?a.delete("link"):a.add("link"),n.hiddenLegendItems=[...a],await this.plugin.saveSettings()})),new Et.Setting(t).setName(Ce(s,"control.hover")).setDesc(Ce(s,"settings.hoverDesc")).addDropdown(r=>{for(let[o,a]of to(s,Hs))r.addOption(o,a);r.setValue(n.hoverHighlightMode).onChange(async o=>{n.hoverHighlightMode=zi(o),await this.plugin.saveSettings()})}),new Et.Setting(t).setName(Ce(s,"control.hoverTargets")).setDesc(Ce(s,"settings.hoverTargetsDesc")).addDropdown(r=>{for(let[o,a]of no(s,Ws))r.addOption(o,a);r.setValue(n.hoverTargetMode).onChange(async o=>{n.hoverTargetMode=os(o),await this.plugin.saveSettings()})}),new Et.Setting(t).setName(Ce(s,"control.labels")).setDesc(Ce(s,"settings.labelsDesc")).addDropdown(r=>{for(let[o,a]of io(s,jr))r.addOption(o,a);r.setValue(n.labelVisibility).onChange(async o=>{n.labelVisibility=qs(o),await this.plugin.saveSettings()})}),new Et.Setting(t).setName(Ce(s,"control.spin")).setDesc(Ce(s,"settings.spinDesc")).addSlider(r=>r.setLimits(0,100,1).setValue(n.swirlStrength).setDynamicTooltip().onChange(async o=>{n.swirlStrength=Ct(o,0,100,Ot.swirlStrength),await this.plugin.saveSettings()})),new Et.Setting(t).setName(Ce(s,"control.unresolvedLinks")).setDesc(Ce(s,"settings.unresolvedDesc")).addToggle(r=>r.setValue(n.includeUnresolvedLinks).onChange(async o=>{n.includeUnresolvedLinks=o,await this.plugin.saveSettings()})),new Et.Setting(t).setName(Ce(s,"control.ignoredFolders")).setDesc(Ce(s,"settings.ignoredDesc")).addTextArea(r=>r.setValue(n.ignoreFolders.join(` +`)).onChange(async o=>{n.ignoreFolders=o.split(` +`).map(a=>a.trim()).filter(Boolean),await this.plugin.saveSettings()}))}}; +/*! Bundled license information: -module.exports = MiniWorldMapPlugin; +three/build/three.core.js: +three/build/three.module.js: + (** + * @license + * Copyright 2010-2026 Three.js Authors + * SPDX-License-Identifier: MIT + *) +*/ diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..9e2e6db --- /dev/null +++ b/package-lock.json @@ -0,0 +1,7032 @@ +{ + "name": "mini-world-map", + "version": "0.1.3", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "mini-world-map", + "version": "0.1.3", + "license": "MIT", + "dependencies": { + "3d-force-graph": "1.80.0", + "d3-force-3d": "3.0.6", + "three": "0.184.0", + "three-spritetext": "1.10.0" + }, + "devDependencies": { + "@eslint/js": "^9.39.4", + "@types/node": "^22.15.17", + "@types/three": "^0.184.0", + "esbuild": "0.25.5", + "eslint": "^9.39.4", + "globals": "^17.6.0", + "jiti": "^2.6.1", + "obsidian": "latest", + "typescript": "^5.8.3", + "typescript-eslint": "^8.59.1", + "vitest": "^3.0.0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz", + "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@codemirror/state": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.5.0.tgz", + "integrity": "sha512-MwBHVK60IiIHDcoMet78lxt6iw5gJOGSbNbOIVBHWVXIH4/Nq1+GQgLLGgI1KlnN86WDXsPudVaqYHKBIx7Eyw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@marijn/find-cluster-break": "^1.0.0" + } + }, + "node_modules/@codemirror/view": { + "version": "6.38.6", + "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.38.6.tgz", + "integrity": "sha512-qiS0z1bKs5WOvHIAC0Cybmv4AJSkAXgX5aD6Mqd2epSLlVJsQl8NG23jCVouIgkh4All/mrbdsf2UOLFnJw0tw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@codemirror/state": "^6.5.0", + "crelt": "^1.0.6", + "style-mod": "^4.1.0", + "w3c-keyname": "^2.2.4" + } + }, + "node_modules/@dimforge/rapier3d-compat": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@dimforge/rapier3d-compat/-/rapier3d-compat-0.12.0.tgz", + "integrity": "sha512-uekIGetywIgopfD97oDL5PfeezkFpNhwlzlaEYNOA0N6ghdsOvh/HYjSMek5Q2O1PYvRSDFcqFVJl4r4ZBwOow==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.5.tgz", + "integrity": "sha512-9o3TMmpmftaCMepOdA5k/yDw8SfInyzWWTjYTFCX3kPSDJMROQTb8jg+h9Cnwnmm1vOzvxN7gIfB5V2ewpjtGA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.5.tgz", + "integrity": "sha512-AdJKSPeEHgi7/ZhuIPtcQKr5RQdo6OO2IL87JkianiMYMPbCtot9fxPbrMiBADOWWm3T2si9stAiVsGbTQFkbA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.5.tgz", + "integrity": "sha512-VGzGhj4lJO+TVGV1v8ntCZWJktV7SGCs3Pn1GRWI1SBFtRALoomm8k5E9Pmwg3HOAal2VDc2F9+PM/rEY6oIDg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.5.tgz", + "integrity": "sha512-D2GyJT1kjvO//drbRT3Hib9XPwQeWd9vZoBJn+bu/lVsOZ13cqNdDeqIF/xQ5/VmWvMduP6AmXvylO/PIc2isw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.5.tgz", + "integrity": "sha512-GtaBgammVvdF7aPIgH2jxMDdivezgFu6iKpmT+48+F8Hhg5J/sfnDieg0aeG/jfSvkYQU2/pceFPDKlqZzwnfQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.5.tgz", + "integrity": "sha512-1iT4FVL0dJ76/q1wd7XDsXrSW+oLoquptvh4CLR4kITDtqi2e/xwXwdCVH8hVHU43wgJdsq7Gxuzcs6Iq/7bxQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.5.tgz", + "integrity": "sha512-nk4tGP3JThz4La38Uy/gzyXtpkPW8zSAmoUhK9xKKXdBCzKODMc2adkB2+8om9BDYugz+uGV7sLmpTYzvmz6Sw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.5.tgz", + "integrity": "sha512-PrikaNjiXdR2laW6OIjlbeuCPrPaAl0IwPIaRv+SMV8CiM8i2LqVUHFC1+8eORgWyY7yhQY+2U2fA55mBzReaw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.5.tgz", + "integrity": "sha512-cPzojwW2okgh7ZlRpcBEtsX7WBuqbLrNXqLU89GxWbNt6uIg78ET82qifUy3W6OVww6ZWobWub5oqZOVtwolfw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.5.tgz", + "integrity": "sha512-Z9kfb1v6ZlGbWj8EJk9T6czVEjjq2ntSYLY2cw6pAZl4oKtfgQuS4HOq41M/BcoLPzrUbNd+R4BXFyH//nHxVg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.5.tgz", + "integrity": "sha512-sQ7l00M8bSv36GLV95BVAdhJ2QsIbCuCjh/uYrWiMQSUuV+LpXwIqhgJDcvMTj+VsQmqAHL2yYaasENvJ7CDKA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.5.tgz", + "integrity": "sha512-0ur7ae16hDUC4OL5iEnDb0tZHDxYmuQyhKhsPBV8f99f6Z9KQM02g33f93rNH5A30agMS46u2HP6qTdEt6Q1kg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.5.tgz", + "integrity": "sha512-kB/66P1OsHO5zLz0i6X0RxlQ+3cu0mkxS3TKFvkb5lin6uwZ/ttOkP3Z8lfR9mJOBk14ZwZ9182SIIWFGNmqmg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.5.tgz", + "integrity": "sha512-UZCmJ7r9X2fe2D6jBmkLBMQetXPXIsZjQJCjgwpVDz+YMcS6oFR27alkgGv3Oqkv07bxdvw7fyB71/olceJhkQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.5.tgz", + "integrity": "sha512-kTxwu4mLyeOlsVIFPfQo+fQJAV9mh24xL+y+Bm6ej067sYANjyEw1dNHmvoqxJUCMnkBdKpvOn0Ahql6+4VyeA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.5.tgz", + "integrity": "sha512-K2dSKTKfmdh78uJ3NcWFiqyRrimfdinS5ErLSn3vluHNeHVnBAFWC8a4X5N+7FgVE1EjXS1QDZbpqZBjfrqMTQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.5.tgz", + "integrity": "sha512-uhj8N2obKTE6pSZ+aMUbqq+1nXxNjZIIjCjGLfsWvVpy7gKCOL6rsY1MhRh9zLtUtAI7vpgLMK6DxjO8Qm9lJw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.5.tgz", + "integrity": "sha512-pwHtMP9viAy1oHPvgxtOv+OkduK5ugofNTVDilIzBLpoWAM16r7b/mxBvfpuQDpRQFMfuVr5aLcn4yveGvBZvw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.5.tgz", + "integrity": "sha512-WOb5fKrvVTRMfWFNCroYWWklbnXH0Q5rZppjq0vQIdlsQKuw6mdSihwSo4RV/YdQ5UCKKvBy7/0ZZYLBZKIbwQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.5.tgz", + "integrity": "sha512-7A208+uQKgTxHd0G0uqZO8UjK2R0DDb4fDmERtARjSHWxqMTye4Erz4zZafx7Di9Cv+lNHYuncAkiGFySoD+Mw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.5.tgz", + "integrity": "sha512-G4hE405ErTWraiZ8UiSoesH8DaCsMm0Cay4fsFWOOUcz8b8rC6uCvnagr+gnioEjWn0wC+o1/TAHt+It+MpIMg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz", + "integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.5.tgz", + "integrity": "sha512-l+azKShMy7FxzY0Rj4RCt5VD/q8mG/e+mDivgspo+yL8zW7qEwctQ6YqKX34DTEleFAvCIUviCFX1SDZRSyMQA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.5.tgz", + "integrity": "sha512-O2S7SNZzdcFG7eFKgvwUEZ2VG9D/sn/eIiz8XRZ1Q/DO5a3s76Xv0mdBzVM5j5R639lXQmPmSo0iRpHqUUrsxw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.5.tgz", + "integrity": "sha512-onOJ02pqs9h1iMJ1PQphR+VZv8qBMQ77Klcsqv9CNW2w6yLqoURLcgERAIurY6QE63bbLuqgP9ATqajFLK5AMQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.5.tgz", + "integrity": "sha512-TXv6YnJ8ZMVdX+SXWVBo/0p8LTcrUYngpWjvm91TMjjBQii7Oz11Lw5lbDV5Y0TzuhSJHwiH4hEtC1I42mMS0g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.2", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.2.tgz", + "integrity": "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.5" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.5.tgz", + "integrity": "sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.14.0", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.1", + "minimatch": "^3.1.5", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/js": { + "version": "9.39.4", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.4.tgz", + "integrity": "sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/json": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/@eslint/json/-/json-0.14.0.tgz", + "integrity": "sha512-rvR/EZtvUG3p9uqrSmcDJPYSH7atmWr0RnFWN6m917MAPx82+zQgPUmDu0whPFG6XTyM0vB/hR6c1Q63OaYtCQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0", + "@eslint/plugin-kit": "^0.4.1", + "@humanwhocodes/momoa": "^3.3.10", + "natural-compare": "^1.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", + "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/types": "^0.15.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", + "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.2", + "@humanfs/types": "^0.15.0", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/types": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", + "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/momoa": { + "version": "3.3.10", + "resolved": "https://registry.npmjs.org/@humanwhocodes/momoa/-/momoa-3.3.10.tgz", + "integrity": "sha512-KWiFQpSAqEIyrTXko3hFNLeQvSK8zXlJQzhhxsyVn58WFRYXST99b3Nqnu+ttOtjds2Pl2grUHGpe2NzhPynuQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@marijn/find-cluster-break": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@marijn/find-cluster-break/-/find-cluster-break-1.0.2.tgz", + "integrity": "sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/@microsoft/eslint-plugin-sdl": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@microsoft/eslint-plugin-sdl/-/eslint-plugin-sdl-1.1.0.tgz", + "integrity": "sha512-dxdNHOemLnBhfY3eByrujX9KyLigcNtW8sU+axzWv5nLGcsSBeKW2YYyTpfPo1hV8YPOmIGnfA4fZHyKVtWqBQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-plugin-n": "17.10.3", + "eslint-plugin-react": "7.37.3", + "eslint-plugin-security": "1.4.0" + }, + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "eslint": "^9" + } + }, + "node_modules/@microsoft/eslint-plugin-sdl/node_modules/eslint-plugin-security": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-security/-/eslint-plugin-security-1.4.0.tgz", + "integrity": "sha512-xlS7P2PLMXeqfhyf3NpqbvbnW04kN8M9NtmhpR3XGyOvt/vNKS7XPXT5EDbwKW9vCjWH4PpfQvgD/+JgN0VJKA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "safe-regex": "^1.1.0" + } + }, + "node_modules/@microsoft/eslint-plugin-sdl/node_modules/safe-regex": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz", + "integrity": "sha512-aJXcif4xnaNUzvUuC5gcb46oTS7zvg4jpMTnuqtrEPlR3vFr4pxtdTwaF1Qs3Enjn9HK+ZlwQui+a7z0SywIzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ret": "~0.1.10" + } + }, + "node_modules/@pkgr/core": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.1.2.tgz", + "integrity": "sha512-fdDH1LSGfZdTH2sxdpVMw31BanV28K/Gry0cVFxaNP77neJSkd82mM8ErPNYs9e+0O7SdHBLTDzDgwUuy18RnQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/unts" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.61.1.tgz", + "integrity": "sha512-JnBB8MdXj45cajvTuO5FmPlvFVJRQgvrz1uSEl3NwqFnReAPGwb8EanbGi4z2nRaqLzjJSv5/JmycoTKlRZxHA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.61.1.tgz", + "integrity": "sha512-Jx2g7iSjw4AOT0HDPHM9RV3GNjRXwybWtSFZiZAYUTjUwjVrYIwq3kBf+LnhqJlzXFAqTAh2F7IGI+O568exPw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.61.1.tgz", + "integrity": "sha512-0F1L/Z3Eqv8mT2n3dCpeO8GcTvHvVqkP5/t6DMsn0KzhYVcg+s7Ncl5DS8qjKYEeio6Az0Gt6nyBORay5qIlCA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.61.1.tgz", + "integrity": "sha512-qLttcH871ujY4YcVfUSShhOw+CsoTatYz8gRbHO7Bb92QH059/P0y5do1KMs41fY0BpD2x4AJH/gID0zFiqVKQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.61.1.tgz", + "integrity": "sha512-fUI4RapGE0Oh3mb8mgfvC1O2nU1RpDZUKnDQm3xB1Ipg7C2wTs5Kstz7G2uWK99a8S2yTMq8/P4uycwNa0nJyw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.61.1.tgz", + "integrity": "sha512-H5YrdvJaDtI/U9/emrD4b++xkvp3y/JvOe4rizHbxvkyMfRS/CiRYdji+Pl8D0brEaNFWUh1drQxgAGIl6Xudw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.61.1.tgz", + "integrity": "sha512-Q8CBCCQtDFrYtXoeUXSrnFXKOnyUhx6bz+SkL6A0E7V8kAiCJ5pamq1WtbfpVGhR5TSpXY6ak3avmDc5fHTyJA==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.61.1.tgz", + "integrity": "sha512-nwnhk1581l0FBVellGcVCAT0Oi06onEA3WB53sf01VO3I0UPBkMH9sXONYME2K0ovXcNayJfNtHfm6mpJElatQ==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.61.1.tgz", + "integrity": "sha512-x5Xr49hwt3hdW75UOZm3395YwwzPyauktslv29KpWL/T+vVAzoT3azLcTWv0eMciBNrx+DYjH4paehHoLpPvpg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.61.1.tgz", + "integrity": "sha512-unMS3H73DpaoPyyEVPjGKleM/s0mkmsauTENpw4INQY8y4+IuLNjkueQ5QCtC0D3N38Y38yhAU8OoZ20S2Tm6w==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.61.1.tgz", + "integrity": "sha512-zNZzGRnAhwjFEYmvphJRV5XaQGjs62cCmeYYHUT//NbvEnHauw+I85nGG+SiVg5ld4GX8D1IbKIX+ozITQnhMQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.61.1.tgz", + "integrity": "sha512-LdpWGL8X209B2SIvWjqlc8VZgM6PKfontSerGepuldQmHYrAOtnMCXeJkxXGbC+PPZVOuu5czJo7fNV6aeW8rQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.61.1.tgz", + "integrity": "sha512-EC5kTtNaNGOmbMGqar8dvJy6y/hg99GAwjfBz++pxZhQATXGcRjd6c5en5wcbru0vkRmiMGsQKdMJOOf6sza4g==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.61.1.tgz", + "integrity": "sha512-8hiwp6D4acEcNK78I4rP0/XtS1sknWIAMJBPdR4l6zUtyTm5KiTDr5bXmWt4foY7nAN7AThDHgkLIEZOWKbzWw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.61.1.tgz", + "integrity": "sha512-10dh/h/BqA7DuMPWSxkR8uks18FRwnwOEqr5zOTEl+NOwP/OMzKX8OFR/Of9xxDA7D5qef1Nzar5WDD2kCCr1g==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.61.1.tgz", + "integrity": "sha512-YKJ5lg35DP17gcAOggnihe+APw9HLyj1Xn7gsmGumBJAUDa6NGXNixJzmkWLhcK9TOuuyQjdamzvJefkO7qHZQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.61.1.tgz", + "integrity": "sha512-Mlil5G2Jj6a7B3LWGctg+XPL9vdXYuzCtNXfxOQ0nPjc2m6ueUktocPGH9bnAM0bNRKb/bAWTujUU7IJQdQA+g==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.61.1.tgz", + "integrity": "sha512-bVWIOIk6pV01p4CdUbPP7CJ/434z+OooYjDuFcR+44N35YvKUC66G8MGnvcWx5mWKW3g61J+t74l3Kj15Kwn2Q==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.61.1.tgz", + "integrity": "sha512-qy5pBvZbqNFheBz61R1rzsezjm0J7O2oNGoWtGoY89SZYLUfxAJTBAqDChqAIdB4rCiIbi9nF7yZ83GnNiLwSw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.61.1.tgz", + "integrity": "sha512-E83TXjI4zm0+5f2qO+UOudaCYIhYwpJ5jq6YCZNIZ+6CbfhKrkAGezeiASBL9ElxAxFsRS9ZhESv8mfnj6TKeg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.61.1.tgz", + "integrity": "sha512-fbWnKqVkjrJN38vNe3ahkbk6iejS/3b0Nt7EEtPpE6RBacZcGXNKbzfHN3GUUlXOPghUg0j6XUGrtjX9z1sIvA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.61.1.tgz", + "integrity": "sha512-ArMl38iVAbk0New1ogihQNY6iphLi4ZaRsa037gUzv5yeKPY8TD3Dmy4x2RNC1VztU/uqm+G+/RwFrSka3Oy2g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.61.1.tgz", + "integrity": "sha512-0mYtjHS9ucAbcATycCNK9IGBk/cCe/ma7EmSLGZdsxnOA8cjRIyU04wDpVAD9NiOfLUR9KTxdiO53uOkherqjQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.61.1.tgz", + "integrity": "sha512-gK1iCEPfpoSG9wfBihXxvBMi8ZfcWffYkEsC/Eih+iFENTaewvNcrEQ69lIOWYO5pePHKLHHO7nq5AILGO/HQQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.61.1.tgz", + "integrity": "sha512-X+zaP2x+j4RXGfbp/seSoRHWnPxzApilDszisZxbYH5C/jTxFhCtDNdPGZb9lJyYPs24wGxruPF7Y+sIXt9Gzw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rtsao/scc": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", + "integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tweenjs/tween.js": { + "version": "23.1.3", + "resolved": "https://registry.npmjs.org/@tweenjs/tween.js/-/tween.js-23.1.3.tgz", + "integrity": "sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA==", + "license": "MIT" + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/codemirror": { + "version": "5.60.8", + "resolved": "https://registry.npmjs.org/@types/codemirror/-/codemirror-5.60.8.tgz", + "integrity": "sha512-VjFgDF/eB+Aklcy15TtOTLQeMjTo07k7KAjql8OK5Dirr7a6sJY4T1uVBDuTVG9VEmn1uUsohOpYnVfgC6/jyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/tern": "*" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/eslint": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", + "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "*", + "@types/json-schema": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json5": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", + "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.19.21", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.21.tgz", + "integrity": "sha512-VMeFBSCKQKmm2swI2kW51SFusDqekC6q9trBCvJ/JliDchFSuoYYKN7yVNjPthP1HKZcx3U1gI/wTcEBjEFKTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/stats.js": { + "version": "0.17.4", + "resolved": "https://registry.npmjs.org/@types/stats.js/-/stats.js-0.17.4.tgz", + "integrity": "sha512-jIBvWWShCvlBqBNIZt0KAshWpvSjhkwkEu4ZUcASoAvhmrgAUI2t1dXrjSL4xXVLB4FznPrIsX3nKXFl/Dt4vA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/tern": { + "version": "0.23.9", + "resolved": "https://registry.npmjs.org/@types/tern/-/tern-0.23.9.tgz", + "integrity": "sha512-ypzHFE/wBzh+BlH6rrBgS5I/Z7RD21pGhZ2rltb/+ZrVM1awdZwjx7hE5XfuYgHWk9uvV5HLZN3SloevCAp3Bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "*" + } + }, + "node_modules/@types/three": { + "version": "0.184.1", + "resolved": "https://registry.npmjs.org/@types/three/-/three-0.184.1.tgz", + "integrity": "sha512-6q4VdiqVsrTRqmk62/BnlcAvIrnDM0zf2ZDVKI5kZiniWrSaOHaQzmbp+BNzoggc/8tgW412pL//wZIxu2PPTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@dimforge/rapier3d-compat": "~0.12.0", + "@tweenjs/tween.js": "~23.1.3", + "@types/stats.js": "*", + "@types/webxr": ">=0.5.17", + "fflate": "~0.8.2", + "meshoptimizer": "~1.1.1" + } + }, + "node_modules/@types/webxr": { + "version": "0.5.24", + "resolved": "https://registry.npmjs.org/@types/webxr/-/webxr-0.5.24.tgz", + "integrity": "sha512-h8fgEd/DpoS9CBrjEQXR+dIDraopAEfu4wYVNY2tEPwk60stPWhvZMf4Foo5FakuQ7HFZoa8WceaWFervK2Ovg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.61.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.61.0.tgz", + "integrity": "sha512-bFNvl9ZczlVb+wR2Akszf3gHfKVj/8WanXaGJ3UstTA7brNKg0cNdk6X1Psu5V7MZ2oQtzZKOEzIUehaoxbDGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.61.0", + "@typescript-eslint/type-utils": "8.61.0", + "@typescript-eslint/utils": "8.61.0", + "@typescript-eslint/visitor-keys": "8.61.0", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.61.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.61.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.61.0.tgz", + "integrity": "sha512-5B7PfA2e1NQGCnDHd/0lW7W3gvp3d59Ryw54FYO8Uswxo9f6ikw3AZV+Xj/TvpImmpsiYyUqAfhC6kJID1jF6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.61.0", + "@typescript-eslint/types": "8.61.0", + "@typescript-eslint/typescript-estree": "8.61.0", + "@typescript-eslint/visitor-keys": "8.61.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.61.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.61.0.tgz", + "integrity": "sha512-DV42F7MLJO6Rax7SK1yg43tcnEfGUrurSpSxKuVX+a3RCTzBlH3fuxprrOJXKCJGAaw82xXocikJ0uQaqwXgGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.61.0", + "@typescript-eslint/types": "^8.61.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.61.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.61.0.tgz", + "integrity": "sha512-IWdXFHFSb6mlC3HPc7QsLDm5zYEbUla6trDEHf32D3/dnuUyXd87plScSNXSbm0/RxMvObpI17sv/EDTGrGZkA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.61.0", + "@typescript-eslint/visitor-keys": "8.61.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.61.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.61.0.tgz", + "integrity": "sha512-O5Amvdv9ztMpxpf+vmFULGG78IE6Qwdr3bCGvqwG4nwc9H2qXkOYJJnRbRHyMkQTjv1d03olqwwwzHLMqpFePQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.61.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.61.0.tgz", + "integrity": "sha512-TuBiQYIkd97yBfInHCTKVYMbX4kvEmpOEuixIuzCU9p8BGT1SfyyO0d0IfDMbPIHcjn/hWnusUX5e8v5Xg+X8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.61.0", + "@typescript-eslint/typescript-estree": "8.61.0", + "@typescript-eslint/utils": "8.61.0", + "debug": "^4.4.3", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.61.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.61.0.tgz", + "integrity": "sha512-9QTQpZ5Iin4CdIodfbDQFSeiSJKidgYJYug1P9CC2xWgUTvlmixViqDZNciMjwLBZyJnG4tGmPl97rVAFb1AJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.61.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.61.0.tgz", + "integrity": "sha512-42zatd5qSvvcV1JdDBCLxYRznvP4eIHpPoZXdkPFnAmanA4FuZ5dibSnCBggY8hQnqajPpoGjXFdZ7fIJKQnlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.61.0", + "@typescript-eslint/tsconfig-utils": "8.61.0", + "@typescript-eslint/types": "8.61.0", + "@typescript-eslint/visitor-keys": "8.61.0", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", + "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.61.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.61.0.tgz", + "integrity": "sha512-3bzFt7ImFMW/jVYwJamDoe/dMOdFLSC6pom6rRjdh4SZJEYupyMzem8e7vKZLclLfpHjlwSAXOUxtKxGXUiLqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.61.0", + "@typescript-eslint/types": "8.61.0", + "@typescript-eslint/typescript-estree": "8.61.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.61.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.61.0.tgz", + "integrity": "sha512-QVLZu3ZPQEE+HICQyAMZ2yLQhxf0meY/wx6Hx14YcTNj13JB3qHlX3lJ02L3fLGHgERRH71kvYDwiXIguT3AjQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.61.0", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@vitest/expect": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.6.tgz", + "integrity": "sha512-1+7q9BtaKzEmO+fmNT3kYvoNn5Y71XWAx2Q5HRim4tTVRQVRv4uJFAQ5FbK0OPUeNP/WmVCpxYxoJdvuHVjzBQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/spy": "3.2.6", + "@vitest/utils": "3.2.6", + "chai": "^5.2.0", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.6.tgz", + "integrity": "sha512-EZOrpDbkKotFAP7wPAQV1UIyoGOk4oX7ynWhBhLB7v+meMHbQhU16oPpIYGTTe4oFlhpryGpgpcZP/sin3hYuw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "3.2.6", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.17" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.6.tgz", + "integrity": "sha512-lb7XXXzmm2h2ASzFnRvQpDo6onT1NmMJA3tkGTWiBFtRJ9lxGY3d3mm/Apt36gej2bkkOVLL/yTOtufDaFa/jA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.6.tgz", + "integrity": "sha512-HYcoSj1w5tcgUnzoF0HcyaAQjpA1gj9ftUJ7iSJSuipc02jW9gKkigwZbjFldAfYHA1fa8UZVRftdMY5msWM9Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "3.2.6", + "pathe": "^2.0.3", + "strip-literal": "^3.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.6.tgz", + "integrity": "sha512-H+ZjNTWGpObenh0YnlBctAPnJSI20P81PL8BPzWpx54YXLLTm8hEsWawtcYLMrwvpK48hGxLLbCS+1KRXhsKhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.6", + "magic-string": "^0.30.17", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.6.tgz", + "integrity": "sha512-oq6BbH68WzcWmwtBrU9nqLeaXTR4XwJF7FSLkKEZo4i6eoXcrxjcwSuTvWBIRUTC6VC72nXYunzqgZA+IKdtxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^4.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.6.tgz", + "integrity": "sha512-lI23nIs4bnT3T8NIoh+vFaz5s2/DdP0Jgt2jxwgWljvwn82cLJtyi/If+fjFyoLMGIOz0U/fKvWE0d4jsNQEfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.6", + "loupe": "^3.1.4", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/3d-force-graph": { + "version": "1.80.0", + "resolved": "https://registry.npmjs.org/3d-force-graph/-/3d-force-graph-1.80.0.tgz", + "integrity": "sha512-tzI353gW1nXPpnC7VTa3JjMg+3cp77qOLUFO0vucPTfF+q5R6sQsNsIqVTbRIb7RSypn14nBa4yfkOe9ThxASw==", + "license": "MIT", + "dependencies": { + "accessor-fn": "1", + "kapsule": "^1.16", + "three": ">=0.179 <1", + "three-forcegraph": "1", + "three-render-objects": "^1.41" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/accessor-fn": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/accessor-fn/-/accessor-fn-1.5.3.tgz", + "integrity": "sha512-rkAofCwe/FvYFUlMB0v0gWmhqtfAtV1IUkdPbfhTUyYniu5LrC0A0UJkTH0Jv3S8SvwkmfuAlY+mQIJATdocMA==", + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/acorn": { + "version": "8.17.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz", + "integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/array-buffer-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", + "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "is-array-buffer": "^3.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array-includes": { + "version": "3.1.9", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.9.tgz", + "integrity": "sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.24.0", + "es-object-atoms": "^1.1.1", + "get-intrinsic": "^1.3.0", + "is-string": "^1.1.1", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.findlast": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz", + "integrity": "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.findlastindex": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.6.tgz", + "integrity": "sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "es-shim-unscopables": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flat": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz", + "integrity": "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flatmap": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz", + "integrity": "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.tosorted": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz", + "integrity": "sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.3", + "es-errors": "^1.3.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/arraybuffer.prototype.slice": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", + "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.1", + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "is-array-buffer": "^3.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/async-function": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", + "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz", + "integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/call-bind": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.9.tgz", + "integrity": "sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "get-intrinsic": "^1.3.0", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/chai": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/check-error": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", + "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/crelt": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz", + "integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "license": "ISC", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-binarytree": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/d3-binarytree/-/d3-binarytree-1.0.2.tgz", + "integrity": "sha512-cElUNH+sHu95L04m92pG73t2MEJXKu+GeKUN1TJkFsu93E5W8E9Sc3kHEGJKgenGvj19m6upSn2EunvMgMD2Yw==", + "license": "MIT" + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dispatch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz", + "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-force-3d": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/d3-force-3d/-/d3-force-3d-3.0.6.tgz", + "integrity": "sha512-4tsKHUPLOVkyfEffZo1v6sFHvGFwAIIjt/W8IThbp08DYAsXZck+2pSHEG5W1+gQgEvFLdZkYvmJAbRM2EzMnA==", + "license": "MIT", + "dependencies": { + "d3-binarytree": "1", + "d3-dispatch": "1 - 3", + "d3-octree": "1", + "d3-quadtree": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.2.tgz", + "integrity": "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-octree": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/d3-octree/-/d3-octree-1.1.0.tgz", + "integrity": "sha512-F8gPlqpP+HwRPMO/8uOu5wjH110+6q4cgJvgJT6vlpy3BEaDIKlTZrgHKZSp/i1InRpVfh4puY/kvL6MxK930A==", + "license": "MIT" + }, + "node_modules/d3-quadtree": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-3.0.1.tgz", + "integrity": "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "license": "ISC", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale-chromatic": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", + "integrity": "sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3", + "d3-interpolate": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-selection": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", + "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "license": "ISC", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "license": "ISC", + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/data-bind-mapper": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/data-bind-mapper/-/data-bind-mapper-1.0.3.tgz", + "integrity": "sha512-QmU3lyEnbENQPo0M1F9BMu4s6cqNNp8iJA+b/HP2sSb7pf3dxwF3+EP1eO69rwBfH9kFJ1apmzrtogAmVt2/Xw==", + "license": "MIT", + "dependencies": { + "accessor-fn": "1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/data-view-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", + "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", + "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/inspect-js" + } + }, + "node_modules/data-view-byte-offset": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", + "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/empathic": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/empathic/-/empathic-2.0.1.tgz", + "integrity": "sha512-YGRs8knHhKHVShLkFET/rWAU8kmHbOV5LwN938RHI0pljAJ1Gf6SzXsSmRaEzcXTtOOmVqJ5+WtQPL5uigY50Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/enhanced-resolve": { + "version": "5.24.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.24.0.tgz", + "integrity": "sha512-SkE2t82KlkkxQRVMVLAGKxLfORGQfrkx5dkj+vlgXRVNEdPc4eZcR+J/Fvj8C+yKSFH5L0q3NFlyufOVQnCcYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/es-abstract": { + "version": "1.24.2", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.2.tgz", + "integrity": "sha512-2FpH9Q5i2RRwyEP1AylXe6nYLR5OhaJTZwmlcP0dL/+JCbgg7yyEo/sEK6HeGZRf3dFpWwThaRHVApXSkW3xeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.2", + "arraybuffer.prototype.slice": "^1.0.4", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "data-view-buffer": "^1.0.2", + "data-view-byte-length": "^1.0.2", + "data-view-byte-offset": "^1.0.1", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "es-set-tostringtag": "^2.1.0", + "es-to-primitive": "^1.3.0", + "function.prototype.name": "^1.1.8", + "get-intrinsic": "^1.3.0", + "get-proto": "^1.0.1", + "get-symbol-description": "^1.1.0", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "internal-slot": "^1.1.0", + "is-array-buffer": "^3.0.5", + "is-callable": "^1.2.7", + "is-data-view": "^1.0.2", + "is-negative-zero": "^2.0.3", + "is-regex": "^1.2.1", + "is-set": "^2.0.3", + "is-shared-array-buffer": "^1.0.4", + "is-string": "^1.1.1", + "is-typed-array": "^1.1.15", + "is-weakref": "^1.1.1", + "math-intrinsics": "^1.1.0", + "object-inspect": "^1.13.4", + "object-keys": "^1.1.1", + "object.assign": "^4.1.7", + "own-keys": "^1.0.1", + "regexp.prototype.flags": "^1.5.4", + "safe-array-concat": "^1.1.3", + "safe-push-apply": "^1.0.0", + "safe-regex-test": "^1.1.0", + "set-proto": "^1.0.0", + "stop-iteration-iterator": "^1.1.0", + "string.prototype.trim": "^1.2.10", + "string.prototype.trimend": "^1.0.9", + "string.prototype.trimstart": "^1.0.8", + "typed-array-buffer": "^1.0.3", + "typed-array-byte-length": "^1.0.3", + "typed-array-byte-offset": "^1.0.4", + "typed-array-length": "^1.0.7", + "unbox-primitive": "^1.1.0", + "which-typed-array": "^1.1.19" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-iterator-helpers": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.3.3.tgz", + "integrity": "sha512-0PuBxFi+4uPanB97iDxCLWuHeYud2FALrw5HFZGtAF38UpJDbDC8frwp2cnDyae692CQ0dou60UwWfhgsa4U/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.24.2", + "es-errors": "^1.3.0", + "es-set-tostringtag": "^2.1.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.3.0", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "internal-slot": "^1.1.0", + "iterator.prototype": "^1.1.5", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-shim-unscopables": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz", + "integrity": "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-to-primitive": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz", + "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7", + "is-date-object": "^1.0.5", + "is-symbol": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/esbuild": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.5.tgz", + "integrity": "sha512-P8OtKZRv/5J5hhz0cUAdu/cLuPIKXpQl1R9pZtvmHWQvrAUVd0UNIPT4IB4W3rNOqVO0rlqHmCIbSwxh/c9yUQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.5", + "@esbuild/android-arm": "0.25.5", + "@esbuild/android-arm64": "0.25.5", + "@esbuild/android-x64": "0.25.5", + "@esbuild/darwin-arm64": "0.25.5", + "@esbuild/darwin-x64": "0.25.5", + "@esbuild/freebsd-arm64": "0.25.5", + "@esbuild/freebsd-x64": "0.25.5", + "@esbuild/linux-arm": "0.25.5", + "@esbuild/linux-arm64": "0.25.5", + "@esbuild/linux-ia32": "0.25.5", + "@esbuild/linux-loong64": "0.25.5", + "@esbuild/linux-mips64el": "0.25.5", + "@esbuild/linux-ppc64": "0.25.5", + "@esbuild/linux-riscv64": "0.25.5", + "@esbuild/linux-s390x": "0.25.5", + "@esbuild/linux-x64": "0.25.5", + "@esbuild/netbsd-arm64": "0.25.5", + "@esbuild/netbsd-x64": "0.25.5", + "@esbuild/openbsd-arm64": "0.25.5", + "@esbuild/openbsd-x64": "0.25.5", + "@esbuild/sunos-x64": "0.25.5", + "@esbuild/win32-arm64": "0.25.5", + "@esbuild/win32-ia32": "0.25.5", + "@esbuild/win32-x64": "0.25.5" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.39.4", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.4.tgz", + "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.2", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.5", + "@eslint/js": "9.39.4", + "@eslint/plugin-kit": "^0.4.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.5", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-compat-utils": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/eslint-compat-utils/-/eslint-compat-utils-0.5.1.tgz", + "integrity": "sha512-3z3vFexKIEnjHE3zCMRo6fn/e44U7T1khUjg+Hp0ZQMCigh28rALD0nPFBcGZuiLC5rLZa2ubQHDRln09JfU2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "eslint": ">=6.0.0" + } + }, + "node_modules/eslint-import-resolver-node": { + "version": "0.3.10", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.10.tgz", + "integrity": "sha512-tRrKqFyCaKict5hOd244sL6EQFNycnMQnBe+j8uqGNXYzsImGbGUU4ibtoaBmv5FLwJwcFJNeg1GeVjQfbMrDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^3.2.7", + "is-core-module": "^2.16.1", + "resolve": "^2.0.0-next.6" + } + }, + "node_modules/eslint-import-resolver-node/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-module-utils": { + "version": "2.13.0", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.13.0.tgz", + "integrity": "sha512-bLohSkT6469rRs8czj0tLTD8vaeIS/whvPRJVjDr7IuoTT1k5DYDERlNycjDj/HkOlvQdYurmfZ/g3fG5bgeLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^3.2.7" + }, + "engines": { + "node": ">=4" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/eslint-module-utils/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-depend": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-depend/-/eslint-plugin-depend-1.3.1.tgz", + "integrity": "sha512-1uo2rFAr9vzNrCYdp7IBZRB54LiyVxfaIso0R6/QV3t6Dax6DTbW/EV2Hktf0f4UtmGHK8UyzJWI382pwW04jw==", + "dev": true, + "license": "MIT", + "dependencies": { + "empathic": "^2.0.0", + "module-replacements": "^2.8.0", + "semver": "^7.6.3" + } + }, + "node_modules/eslint-plugin-es-x": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-es-x/-/eslint-plugin-es-x-7.8.0.tgz", + "integrity": "sha512-7Ds8+wAAoV3T+LAKeu39Y5BzXCrGKrcISfgKEqTS4BDN8SFEDQd0S43jiQ8vIa3wUKD07qitZdfzlenSi8/0qQ==", + "dev": true, + "funding": [ + "https://github.com/sponsors/ota-meshi", + "https://opencollective.com/eslint" + ], + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.1.2", + "@eslint-community/regexpp": "^4.11.0", + "eslint-compat-utils": "^0.5.1" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "eslint": ">=8" + } + }, + "node_modules/eslint-plugin-import": { + "version": "2.32.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.32.0.tgz", + "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rtsao/scc": "^1.1.0", + "array-includes": "^3.1.9", + "array.prototype.findlastindex": "^1.2.6", + "array.prototype.flat": "^1.3.3", + "array.prototype.flatmap": "^1.3.3", + "debug": "^3.2.7", + "doctrine": "^2.1.0", + "eslint-import-resolver-node": "^0.3.9", + "eslint-module-utils": "^2.12.1", + "hasown": "^2.0.2", + "is-core-module": "^2.16.1", + "is-glob": "^4.0.3", + "minimatch": "^3.1.2", + "object.fromentries": "^2.0.8", + "object.groupby": "^1.0.3", + "object.values": "^1.2.1", + "semver": "^6.3.1", + "string.prototype.trimend": "^1.0.9", + "tsconfig-paths": "^3.15.0" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9" + } + }, + "node_modules/eslint-plugin-import/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-import/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/eslint-plugin-json-schema-validator": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-json-schema-validator/-/eslint-plugin-json-schema-validator-5.1.0.tgz", + "integrity": "sha512-ZmVyxRIjm58oqe2kTuy90PpmZPrrKvOjRPXKzq8WCgRgAkidCgm5X8domL2KSfadZ3QFAmifMgGTcVNhZ5ez2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.3.0", + "ajv": "^8.0.0", + "debug": "^4.3.1", + "eslint-compat-utils": "^0.5.0", + "json-schema-migrate": "^2.0.0", + "jsonc-eslint-parser": "^2.0.0", + "minimatch": "^8.0.0", + "synckit": "^0.9.0", + "toml-eslint-parser": "^0.9.0", + "tunnel-agent": "^0.6.0", + "yaml-eslint-parser": "^1.0.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ota-meshi" + }, + "peerDependencies": { + "eslint": ">=6.0.0" + } + }, + "node_modules/eslint-plugin-json-schema-validator/node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/eslint-plugin-json-schema-validator/node_modules/brace-expansion": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.1.tgz", + "integrity": "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/eslint-plugin-json-schema-validator/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, + "node_modules/eslint-plugin-json-schema-validator/node_modules/minimatch": { + "version": "8.0.7", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-8.0.7.tgz", + "integrity": "sha512-V+1uQNdzybxa14e/p00HZnQNNcTjnRJjDxg2V8wtkjFctq4M7hXFws4oekyTP0Jebeq7QYtpFyOeBAjc88zvYg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/eslint-plugin-n": { + "version": "17.10.3", + "resolved": "https://registry.npmjs.org/eslint-plugin-n/-/eslint-plugin-n-17.10.3.tgz", + "integrity": "sha512-ySZBfKe49nQZWR1yFaA0v/GsH6Fgp8ah6XV0WDz6CN8WO0ek4McMzb7A2xnf4DCYV43frjCygvb9f/wx7UUxRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "enhanced-resolve": "^5.17.0", + "eslint-plugin-es-x": "^7.5.0", + "get-tsconfig": "^4.7.0", + "globals": "^15.8.0", + "ignore": "^5.2.4", + "minimatch": "^9.0.5", + "semver": "^7.5.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": ">=8.23.0" + } + }, + "node_modules/eslint-plugin-n/node_modules/brace-expansion": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.1.tgz", + "integrity": "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/eslint-plugin-n/node_modules/globals": { + "version": "15.15.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-15.15.0.tgz", + "integrity": "sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint-plugin-n/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/eslint-plugin-no-unsanitized": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/eslint-plugin-no-unsanitized/-/eslint-plugin-no-unsanitized-4.1.5.tgz", + "integrity": "sha512-MSB4hXPVFQrI8weqzs6gzl7reP2k/qSjtCoL2vUMSDejIIq9YL1ZKvq5/ORBXab/PvfBBrWO2jWviYpL+4Ghfg==", + "dev": true, + "license": "MPL-2.0", + "peerDependencies": { + "eslint": "^9 || ^10" + } + }, + "node_modules/eslint-plugin-obsidianmd": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-obsidianmd/-/eslint-plugin-obsidianmd-0.3.0.tgz", + "integrity": "sha512-QvGDI6B2nxJBrsZKGTg31da2A/fEJNlnwN+fRZkaoPIu1QL3fYXUdpP7ThyMdr/0iTYQxifb9lt2X9cpydQx1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint/config-helpers": "^0.4.2", + "@eslint/js": "^9.30.1", + "@eslint/json": "0.14.0", + "@microsoft/eslint-plugin-sdl": "^1.1.0", + "@types/eslint": "9.6.1", + "@types/node": "20.12.12", + "@typescript-eslint/types": "^8.33.1", + "@typescript-eslint/utils": "^8.33.1", + "eslint": ">=9.0.0", + "eslint-plugin-depend": "1.3.1", + "eslint-plugin-import": "^2.31.0", + "eslint-plugin-json-schema-validator": "5.1.0", + "eslint-plugin-no-unsanitized": "^4.1.5", + "eslint-plugin-security": "2.1.1", + "globals": "14.0.0", + "obsidian": "1.12.3", + "semver": "^7.7.4", + "typescript": "5.4.5", + "typescript-eslint": "^8.35.1" + }, + "bin": { + "eslint-plugin-obsidian": "dist/lib/index.js" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@eslint/js": "^9.30.1", + "@eslint/json": "0.14.0", + "eslint": ">=9.0.0", + "obsidian": "1.8.7", + "typescript-eslint": "^8.35.1" + } + }, + "node_modules/eslint-plugin-obsidianmd/node_modules/@types/node": { + "version": "20.12.12", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.12.tgz", + "integrity": "sha512-eWLDGF/FOSPtAvEqeRAQ4C8LSA7M1I7i0ky1I8U7kD1J5ITyW3AsRhQrKVoWf5pFKZ2kILsEGJhsI9r93PYnOw==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/eslint-plugin-obsidianmd/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint-plugin-obsidianmd/node_modules/obsidian": { + "version": "1.12.3", + "resolved": "https://registry.npmjs.org/obsidian/-/obsidian-1.12.3.tgz", + "integrity": "sha512-HxWqe763dOqzXjnNiHmAJTRERN8KILBSqxDSEqbeSr7W8R8Jxezzbca+nz1LiiqXnMpM8lV2jzAezw3CZ4xNUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/codemirror": "5.60.8", + "moment": "2.29.4" + }, + "peerDependencies": { + "@codemirror/state": "6.5.0", + "@codemirror/view": "6.38.6" + } + }, + "node_modules/eslint-plugin-obsidianmd/node_modules/typescript": { + "version": "5.4.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz", + "integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/eslint-plugin-obsidianmd/node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "dev": true, + "license": "MIT" + }, + "node_modules/eslint-plugin-react": { + "version": "7.37.3", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.3.tgz", + "integrity": "sha512-DomWuTQPFYZwF/7c9W2fkKkStqZmBd3uugfqBYLdkZ3Hii23WzZuOLUskGxB8qkSKqftxEeGL1TB2kMhrce0jA==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-includes": "^3.1.8", + "array.prototype.findlast": "^1.2.5", + "array.prototype.flatmap": "^1.3.3", + "array.prototype.tosorted": "^1.1.4", + "doctrine": "^2.1.0", + "es-iterator-helpers": "^1.2.1", + "estraverse": "^5.3.0", + "hasown": "^2.0.2", + "jsx-ast-utils": "^2.4.1 || ^3.0.0", + "minimatch": "^3.1.2", + "object.entries": "^1.1.8", + "object.fromentries": "^2.0.8", + "object.values": "^1.2.1", + "prop-types": "^15.8.1", + "resolve": "^2.0.0-next.5", + "semver": "^6.3.1", + "string.prototype.matchall": "^4.0.12", + "string.prototype.repeat": "^1.0.0" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7" + } + }, + "node_modules/eslint-plugin-react/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/eslint-plugin-security": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-security/-/eslint-plugin-security-2.1.1.tgz", + "integrity": "sha512-7cspIGj7WTfR3EhaILzAPcfCo5R9FbeWvbgsPYWivSurTBKW88VQxtP3c4aWMG9Hz/GfJlJVdXEJ3c8LqS+u2w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "safe-regex": "^2.1.1" + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz", + "integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fflate": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.3.tgz", + "integrity": "sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA==", + "dev": true, + "license": "MIT" + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", + "dev": true, + "license": "ISC" + }, + "node_modules/float-tooltip": { + "version": "1.7.5", + "resolved": "https://registry.npmjs.org/float-tooltip/-/float-tooltip-1.7.5.tgz", + "integrity": "sha512-/kXzuDnnBqyyWyhDMH7+PfP8J/oXiAavGzcRxASOMRHFuReDtofizLLJsf7nnDLAfEaMW4pVWaXrAjtnglpEkg==", + "license": "MIT", + "dependencies": { + "d3-selection": "2 - 3", + "kapsule": "^1.16", + "preact": "10" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/function.prototype.name": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.2.0.tgz", + "integrity": "sha512-jObKIik1P2QjPHP5nz5BaOtUlfgS0fWo8IUByNXkM+o+02sJOi94em77GwJKQSJ3gfPHdgzLNrHc1uokV4P/ew==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2", + "hasown": "^2.0.4", + "is-callable": "^1.2.7", + "is-document.all": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/generator-function": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz", + "integrity": "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-symbol-description": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", + "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-tsconfig": { + "version": "4.14.0", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.14.0.tgz", + "integrity": "sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "17.6.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-17.6.0.tgz", + "integrity": "sha512-sepffkT8stwnIYbsMBpoCHJuJM5l98FUF2AnE07hfvE0m/qp3R586hw4jF4uadbhvg1ooIdzuu7CsfD2jzCaNA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globalthis": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-properties": "^1.2.1", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/has-bigints": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", + "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz", + "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/internal-slot": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", + "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "hasown": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/is-array-buffer": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", + "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-async-function": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", + "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "async-function": "^1.0.0", + "call-bound": "^1.0.3", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-bigint": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", + "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-bigints": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-boolean-object": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", + "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-core-module": { + "version": "2.16.2", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.2.tgz", + "integrity": "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-data-view": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz", + "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-date-object": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", + "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-document.all": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-document.all/-/is-document.all-1.0.0.tgz", + "integrity": "sha512-+XSoyS05OdBbhFuELhgTCpFNHkpBOJqtsZfUFFpe5QTw+9Sjbh8zitxhQkYAo6wV7e1Vb8cAPvpCk9jGam/82g==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-finalizationregistry": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", + "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-generator-function": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz", + "integrity": "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.4", + "generator-function": "^2.0.0", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-map": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", + "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-negative-zero": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", + "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-number-object": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", + "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-regex": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-set": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", + "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-shared-array-buffer": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", + "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-string": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", + "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-symbol": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", + "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-symbols": "^1.1.0", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakmap": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", + "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz", + "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakset": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", + "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true, + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/iterator.prototype": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.5.tgz", + "integrity": "sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.6", + "get-proto": "^1.0.0", + "has-symbols": "^1.1.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/jiti": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", + "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.2.0.tgz", + "integrity": "sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-migrate": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/json-schema-migrate/-/json-schema-migrate-2.0.0.tgz", + "integrity": "sha512-r38SVTtojDRp4eD6WsCqiE0eNDt4v1WalBXb9cyZYw9ai5cGtBwzRNWjHzJl38w6TxFkXAIA7h+fyX3tnrAFhQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + } + }, + "node_modules/json-schema-migrate/node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/json-schema-migrate/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.0" + }, + "bin": { + "json5": "lib/cli.js" + } + }, + "node_modules/jsonc-eslint-parser": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/jsonc-eslint-parser/-/jsonc-eslint-parser-2.4.2.tgz", + "integrity": "sha512-1e4qoRgnn448pRuMvKGsFFymUCquZV0mpGgOyIKNgD3JVDTsVJyRBGH/Fm0tBb8WsWGgmB1mDe6/yJMQM37DUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.5.0", + "eslint-visitor-keys": "^3.0.0", + "espree": "^9.0.0", + "semver": "^7.3.5" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ota-meshi" + } + }, + "node_modules/jsonc-eslint-parser/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/jsonc-eslint-parser/node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/jsx-ast-utils": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", + "integrity": "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-includes": "^3.1.6", + "array.prototype.flat": "^1.3.1", + "object.assign": "^4.1.4", + "object.values": "^1.1.6" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/kapsule": { + "version": "1.16.3", + "resolved": "https://registry.npmjs.org/kapsule/-/kapsule-1.16.3.tgz", + "integrity": "sha512-4+5mNNf4vZDSwPhKprKwz3330iisPrb08JyMgbsdFrimBCKNHecua/WBwvVg3n7vwx0C1ARjfhwIpbrbd9n5wg==", + "license": "MIT", + "dependencies": { + "lodash-es": "4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash-es": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.18.1.tgz", + "integrity": "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==", + "license": "MIT" + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/loupe": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/meshoptimizer": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/meshoptimizer/-/meshoptimizer-1.1.1.tgz", + "integrity": "sha512-oRFNWJRDA/WTrVj7NWvqa5HqE1t9MYDj2VaWirQCzCCrAd2GHrqR/sQezCxiWATPNlKTcRaPRHPJwIRoPBAp5g==", + "dev": true, + "license": "MIT" + }, + "node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/module-replacements": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/module-replacements/-/module-replacements-2.11.0.tgz", + "integrity": "sha512-j5sNQm3VCpQQ7nTqGeOZtoJtV3uKERgCBm9QRhmGRiXiqkf7iRFOkfxdJRZWLkqYY8PNf4cDQF/WfXUYLENrRA==", + "dev": true, + "license": "MIT" + }, + "node_modules/moment": { + "version": "2.29.4", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz", + "integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/ngraph.events": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/ngraph.events/-/ngraph.events-1.4.0.tgz", + "integrity": "sha512-NeDGI4DSyjBNBRtA86222JoYietsmCXbs8CEB0dZ51Xeh4lhVl1y3wpWLumczvnha8sFQIW4E0vvVWwgmX2mGw==", + "license": "BSD-3-Clause" + }, + "node_modules/ngraph.forcelayout": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/ngraph.forcelayout/-/ngraph.forcelayout-3.3.1.tgz", + "integrity": "sha512-MKBuEh1wujyQHFTW57y5vd/uuEOK0XfXYxm3lC7kktjJLRdt/KEKEknyOlc6tjXflqBKEuYBBcu7Ax5VY+S6aw==", + "license": "BSD-3-Clause", + "dependencies": { + "ngraph.events": "^1.0.0", + "ngraph.merge": "^1.0.0", + "ngraph.random": "^1.0.0" + } + }, + "node_modules/ngraph.graph": { + "version": "20.1.2", + "resolved": "https://registry.npmjs.org/ngraph.graph/-/ngraph.graph-20.1.2.tgz", + "integrity": "sha512-W/G3GBR3Y5UxMLHTUCPP9v+pbtpzwuAEIqP5oZV+9IwgxAIEZwh+Foc60iPc1idlnK7Zxu0p3puxAyNmDvBd0Q==", + "license": "BSD-3-Clause", + "dependencies": { + "ngraph.events": "^1.4.0" + } + }, + "node_modules/ngraph.merge": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/ngraph.merge/-/ngraph.merge-1.0.0.tgz", + "integrity": "sha512-5J8YjGITUJeapsomtTALYsw7rFveYkM+lBj3QiYZ79EymQcuri65Nw3knQtFxQBU1r5iOaVRXrSwMENUPK62Vg==", + "license": "MIT" + }, + "node_modules/ngraph.random": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/ngraph.random/-/ngraph.random-1.2.0.tgz", + "integrity": "sha512-4EUeAGbB2HWX9njd6bP6tciN6ByJfoaAvmVL9QTaZSeXrW46eNGA9GajiXiPBbvFqxUWFkEbyo6x5qsACUuVfA==", + "license": "BSD-3-Clause" + }, + "node_modules/node-exports-info": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/node-exports-info/-/node-exports-info-1.6.0.tgz", + "integrity": "sha512-pyFS63ptit/P5WqUkt+UUfe+4oevH+bFeIiPPdfb0pFeYEu/1ELnJu5l+5EcTKYL5M7zaAa7S8ddywgXypqKCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "array.prototype.flatmap": "^1.3.3", + "es-errors": "^1.3.0", + "object.entries": "^1.1.9", + "semver": "^6.3.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/node-exports-info/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", + "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0", + "has-symbols": "^1.1.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.entries": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.9.tgz", + "integrity": "sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.fromentries": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz", + "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.groupby": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.3.tgz", + "integrity": "sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.values": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.1.tgz", + "integrity": "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/obsidian": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/obsidian/-/obsidian-1.13.1.tgz", + "integrity": "sha512-qtTEA2pmhJzhuhJqzbBFRYhpIOqvW+krDYjtFynv66KbxBbumHBlsJfWw3I4jtnK/6fZwbQhCrmmDdRwXmX56w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/codemirror": "5.60.8", + "moment": "2.29.4" + }, + "peerDependencies": { + "@codemirror/state": "6.5.0", + "@codemirror/view": "6.38.6" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/own-keys": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", + "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-intrinsic": "^1.2.6", + "object-keys": "^1.1.1", + "safe-push-apply": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/polished": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/polished/-/polished-4.3.1.tgz", + "integrity": "sha512-OBatVyC/N7SCW/FaDHrSd+vn0o5cS855TOmYi4OkdWUMSJCET/xip//ch8xGUvtr3i44X9LVyWwQlRMTN3pwSA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.17.8" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/possible-typed-array-names": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/postcss": { + "version": "8.5.15", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", + "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/preact": { + "version": "10.29.2", + "resolved": "https://registry.npmjs.org/preact/-/preact-10.29.2.tgz", + "integrity": "sha512-7tNmwg/7mzzAoB/8kSg6Hl37JraAZw3Z3A0JSY7VXlZwo82Xn0G7wKbNNs2qoF4ZEEsQGTwDAroNdqKs1ofJxQ==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/preact" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "dev": true, + "license": "MIT", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/reflect.getprototypeof": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", + "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.7", + "get-proto": "^1.0.1", + "which-builtin-type": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/regexp-tree": { + "version": "0.1.27", + "resolved": "https://registry.npmjs.org/regexp-tree/-/regexp-tree-0.1.27.tgz", + "integrity": "sha512-iETxpjK6YoRWJG5o6hXLwvjYAoW+FEZn9os0PD/b6AP6xQwsa/Y7lCVgIixBbUPMfhu+i2LtdeAqVTgGlQarfA==", + "dev": true, + "license": "MIT", + "bin": { + "regexp-tree": "bin/regexp-tree" + } + }, + "node_modules/regexp.prototype.flags": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", + "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve": { + "version": "2.0.0-next.7", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.7.tgz", + "integrity": "sha512-tqt+NBWwyaMgw3zDsnygx4CByWjQEJHOPMdslYhppaQSJUtL/D4JO9CcBBlhPoI8lz9oJIDXkwXfhF4aWqP8xQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "is-core-module": "^2.16.2", + "node-exports-info": "^1.6.0", + "object-keys": "^1.1.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/ret": { + "version": "0.1.15", + "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", + "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12" + } + }, + "node_modules/rollup": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.61.1.tgz", + "integrity": "sha512-I4KW6iuRpuu2uHBLraZ1wNZe0DP7lnRha+VJ9tNaYVaVgKhW0aI3h4RYnoRPeql0flHm/Co55b7snEDcOfOJrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.61.1", + "@rollup/rollup-android-arm64": "4.61.1", + "@rollup/rollup-darwin-arm64": "4.61.1", + "@rollup/rollup-darwin-x64": "4.61.1", + "@rollup/rollup-freebsd-arm64": "4.61.1", + "@rollup/rollup-freebsd-x64": "4.61.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.61.1", + "@rollup/rollup-linux-arm-musleabihf": "4.61.1", + "@rollup/rollup-linux-arm64-gnu": "4.61.1", + "@rollup/rollup-linux-arm64-musl": "4.61.1", + "@rollup/rollup-linux-loong64-gnu": "4.61.1", + "@rollup/rollup-linux-loong64-musl": "4.61.1", + "@rollup/rollup-linux-ppc64-gnu": "4.61.1", + "@rollup/rollup-linux-ppc64-musl": "4.61.1", + "@rollup/rollup-linux-riscv64-gnu": "4.61.1", + "@rollup/rollup-linux-riscv64-musl": "4.61.1", + "@rollup/rollup-linux-s390x-gnu": "4.61.1", + "@rollup/rollup-linux-x64-gnu": "4.61.1", + "@rollup/rollup-linux-x64-musl": "4.61.1", + "@rollup/rollup-openbsd-x64": "4.61.1", + "@rollup/rollup-openharmony-arm64": "4.61.1", + "@rollup/rollup-win32-arm64-msvc": "4.61.1", + "@rollup/rollup-win32-ia32-msvc": "4.61.1", + "@rollup/rollup-win32-x64-gnu": "4.61.1", + "@rollup/rollup-win32-x64-msvc": "4.61.1", + "fsevents": "~2.3.2" + } + }, + "node_modules/safe-array-concat": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.4.tgz", + "integrity": "sha512-wtZlHyOje6OZTGqAoaDKxFkgRtkF9CnHAVnCHKfuj200wAgL+bSJhdsCD2l0Qx/2ekEXjPWcyKkfGb5CPboslg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "get-intrinsic": "^1.3.0", + "has-symbols": "^1.1.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">=0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safe-push-apply": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", + "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-2.1.1.tgz", + "integrity": "sha512-rx+x8AMzKb5Q5lQ95Zoi6ZbJqwCLkqi3XuJXp5P3rT8OEc6sZCJG5AE5dU3lsgRr/F4Bs31jSlVN+j5KrsGu9A==", + "dev": true, + "license": "MIT", + "dependencies": { + "regexp-tree": "~0.1.1" + } + }, + "node_modules/safe-regex-test": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-regex": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/semver": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.4.tgz", + "integrity": "sha512-rUCObTnP32Q08R2uuIrt7r9PlEonuTmtuXYcW6s5kjdlj3xbnwe+21yXptAUYcMAABLkYYTtnmzb3w3EDZfueA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-function-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-proto": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz", + "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", + "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4", + "side-channel-list": "^1.0.1", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, + "node_modules/stop-iteration-iterator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", + "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "internal-slot": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/string.prototype.matchall": { + "version": "4.0.12", + "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz", + "integrity": "sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.6", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.6", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "internal-slot": "^1.1.0", + "regexp.prototype.flags": "^1.5.3", + "set-function-name": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.repeat": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/string.prototype.repeat/-/string.prototype.repeat-1.0.0.tgz", + "integrity": "sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5" + } + }, + "node_modules/string.prototype.trim": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.11.tgz", + "integrity": "sha512-PwvK7BU+CMTJGYQCTZb5RWXIML92lftJLhQz1tBzgKiqGxJaMlBAa48POXaNAC2s4y8jr3EFqrkF9+44neS46w==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "define-data-property": "^1.1.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.24.2", + "es-object-atoms": "^1.1.2", + "has-property-descriptors": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimend": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.10.tgz", + "integrity": "sha512-2+3aDAOmPTmuFwjDnmJG2ctEkQKVki7vOSqaxkv42Mowj1V6PnvuwFCRrR5lChUux1TBskPjfkeTOhqczDMxTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimstart": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", + "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strip-literal": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.1.0.tgz", + "integrity": "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^9.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/strip-literal/node_modules/js-tokens": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/style-mod": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.3.tgz", + "integrity": "sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/synckit": { + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.9.3.tgz", + "integrity": "sha512-JJoOEKTfL1urb1mDoEblhD9NhEbWmq9jHEMEnxoC4ujUaZ4itA8vKgwkFAyNClgxplLi9tsUKX+EduK0p/l7sg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@pkgr/core": "^0.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/unts" + } + }, + "node_modules/tapable": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz", + "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/three": { + "version": "0.184.0", + "resolved": "https://registry.npmjs.org/three/-/three-0.184.0.tgz", + "integrity": "sha512-wtTRjG92pM5eUg/KuUnHsqSAlPM296brTOcLgMRqEeylYTh/CdtvKUvCyyCQTzFuStieWxvZb8mVTMvdPyUpxg==", + "license": "MIT" + }, + "node_modules/three-forcegraph": { + "version": "1.43.4", + "resolved": "https://registry.npmjs.org/three-forcegraph/-/three-forcegraph-1.43.4.tgz", + "integrity": "sha512-FtmiZP/T16ZQaHza3JDaDn0YTXFtg9e7pGnTeU8nzu0NNkx7MpWbF/GvmpbQsWHx3rukHtkRv1fTorLPB3FDEA==", + "license": "MIT", + "dependencies": { + "accessor-fn": "1", + "d3-array": "1 - 3", + "d3-force-3d": "2 - 3", + "d3-scale": "1 - 4", + "d3-scale-chromatic": "1 - 3", + "data-bind-mapper": "1", + "kapsule": "^1.16", + "ngraph.forcelayout": "3", + "ngraph.graph": "20", + "tinycolor2": "1" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "three": ">=0.118.3" + } + }, + "node_modules/three-render-objects": { + "version": "1.42.0", + "resolved": "https://registry.npmjs.org/three-render-objects/-/three-render-objects-1.42.0.tgz", + "integrity": "sha512-KYfkPrYGEbIK8ChFocWqOF1aAN80FBUBWVYB8mB2oBpVuVN+52FvvngVYB5ieFANQu7Rt21rPYZ/xKaAgVWWRQ==", + "license": "MIT", + "dependencies": { + "@tweenjs/tween.js": "18 - 25", + "accessor-fn": "1", + "float-tooltip": "^1.7", + "kapsule": "^1.16", + "polished": "4" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "three": ">=0.179" + } + }, + "node_modules/three-spritetext": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/three-spritetext/-/three-spritetext-1.10.0.tgz", + "integrity": "sha512-t08iP1FCU1lQh8T5MmCpdijKgas8GDHJE0LqMGBuVu3xqMMpFnEZhTlih7FlxLPQizHIGoumUSpfOlY1GO/Tgg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "three": ">=0.86.0" + } + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinycolor2": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.6.0.tgz", + "integrity": "sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==", + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", + "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.4.tgz", + "integrity": "sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/toml-eslint-parser": { + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/toml-eslint-parser/-/toml-eslint-parser-0.9.3.tgz", + "integrity": "sha512-moYoCvkNUAPCxSW9jmHmRElhm4tVJpHL8ItC/+uYD0EpPSFXbck7yREz9tNdJVTSpHVod8+HoipcpbQ0oE6gsw==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.0.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ota-meshi" + } + }, + "node_modules/toml-eslint-parser/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/ts-api-utils": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/tsconfig-paths": { + "version": "3.15.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", + "integrity": "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json5": "^0.0.29", + "json5": "^1.0.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD" + }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/typed-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", + "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/typed-array-byte-length": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz", + "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-byte-offset": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz", + "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.15", + "reflect.getprototypeof": "^1.0.9" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-length": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.8.tgz", + "integrity": "sha512-phPGCwqr2+Qo0fwniCE8e4pKnGu/yFb5nD5Y8bf0EEeiI5GklnACYA9GFy/DrAeRrKHXvHn+1SUsOWgJp6RO+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "for-each": "^0.3.5", + "gopd": "^1.2.0", + "is-typed-array": "^1.1.15", + "possible-typed-array-names": "^1.1.0", + "reflect.getprototypeof": "^1.0.10" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.61.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.61.0.tgz", + "integrity": "sha512-8y31Rd0eGTrDKqhy6vT0HtzhN+YLjQizwX3aA3hPXP/ynSfnrBXcQY5IzsP9/DM7+klX4IUncZZjkchP0z+rUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.61.0", + "@typescript-eslint/parser": "8.61.0", + "@typescript-eslint/typescript-estree": "8.61.0", + "@typescript-eslint/utils": "8.61.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/unbox-primitive": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", + "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-bigints": "^1.0.2", + "has-symbols": "^1.1.0", + "which-boxed-primitive": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/vite": { + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.5.tgz", + "integrity": "sha512-KuOaNhcnGFN2zIPGA7wRmzF+lJA1sea7rHq17aiJ++9lzY1WWG6Jpwqwe1KNbRVPIqHmr8GLYx7jbrQcN/7/ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.27.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite-node": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz", + "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.4.1", + "es-module-lexer": "^1.7.0", + "pathe": "^2.0.3", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vite/node_modules/@esbuild/aix-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", + "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz", + "integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz", + "integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/android-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz", + "integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz", + "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz", + "integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz", + "integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz", + "integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz", + "integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz", + "integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz", + "integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-loong64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz", + "integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-mips64el": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz", + "integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz", + "integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-riscv64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz", + "integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-s390x": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz", + "integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz", + "integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz", + "integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/netbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz", + "integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz", + "integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/openbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz", + "integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/sunos-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz", + "integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz", + "integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz", + "integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz", + "integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/esbuild": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz", + "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.7", + "@esbuild/android-arm": "0.27.7", + "@esbuild/android-arm64": "0.27.7", + "@esbuild/android-x64": "0.27.7", + "@esbuild/darwin-arm64": "0.27.7", + "@esbuild/darwin-x64": "0.27.7", + "@esbuild/freebsd-arm64": "0.27.7", + "@esbuild/freebsd-x64": "0.27.7", + "@esbuild/linux-arm": "0.27.7", + "@esbuild/linux-arm64": "0.27.7", + "@esbuild/linux-ia32": "0.27.7", + "@esbuild/linux-loong64": "0.27.7", + "@esbuild/linux-mips64el": "0.27.7", + "@esbuild/linux-ppc64": "0.27.7", + "@esbuild/linux-riscv64": "0.27.7", + "@esbuild/linux-s390x": "0.27.7", + "@esbuild/linux-x64": "0.27.7", + "@esbuild/netbsd-arm64": "0.27.7", + "@esbuild/netbsd-x64": "0.27.7", + "@esbuild/openbsd-arm64": "0.27.7", + "@esbuild/openbsd-x64": "0.27.7", + "@esbuild/openharmony-arm64": "0.27.7", + "@esbuild/sunos-x64": "0.27.7", + "@esbuild/win32-arm64": "0.27.7", + "@esbuild/win32-ia32": "0.27.7", + "@esbuild/win32-x64": "0.27.7" + } + }, + "node_modules/vitest": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.6.tgz", + "integrity": "sha512-xejya+bT/j/+R/AGa1XOfRxLmNUlLtlwjRsFUILF+xHfzElmGcmFydy2gqqIrd62ptIEfwVMofd19uNWD9L7Nw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/expect": "3.2.6", + "@vitest/mocker": "3.2.6", + "@vitest/pretty-format": "^3.2.6", + "@vitest/runner": "3.2.6", + "@vitest/snapshot": "3.2.6", + "@vitest/spy": "3.2.6", + "@vitest/utils": "3.2.6", + "chai": "^5.2.0", + "debug": "^4.4.1", + "expect-type": "^1.2.1", + "magic-string": "^0.30.17", + "pathe": "^2.0.3", + "picomatch": "^4.0.2", + "std-env": "^3.9.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.14", + "tinypool": "^1.1.1", + "tinyrainbow": "^2.0.0", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", + "vite-node": "3.2.4", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/debug": "^4.1.12", + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "@vitest/browser": "3.2.6", + "@vitest/ui": "3.2.6", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/debug": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/w3c-keyname": { + "version": "2.2.8", + "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz", + "integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/which-boxed-primitive": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", + "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-bigint": "^1.1.0", + "is-boolean-object": "^1.2.1", + "is-number-object": "^1.1.1", + "is-string": "^1.1.1", + "is-symbol": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-builtin-type": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz", + "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "function.prototype.name": "^1.1.6", + "has-tostringtag": "^1.0.2", + "is-async-function": "^2.0.0", + "is-date-object": "^1.1.0", + "is-finalizationregistry": "^1.1.0", + "is-generator-function": "^1.0.10", + "is-regex": "^1.2.1", + "is-weakref": "^1.0.2", + "isarray": "^2.0.5", + "which-boxed-primitive": "^1.1.0", + "which-collection": "^1.0.2", + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-collection": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", + "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-map": "^2.0.3", + "is-set": "^2.0.3", + "is-weakmap": "^2.0.2", + "is-weakset": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.22", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.22.tgz", + "integrity": "sha512-fvO4ExWMFsqyhG3AiPAObMuY1lxaqgYcxbc49CNdWDDECOJNgQyvsOWVwbZc+qf3rzRtxojBK+CMEv0Ld5CYpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/yaml": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz", + "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==", + "dev": true, + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, + "node_modules/yaml-eslint-parser": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/yaml-eslint-parser/-/yaml-eslint-parser-1.3.2.tgz", + "integrity": "sha512-odxVsHAkZYYglR30aPYRY4nUGJnoJ2y1ww2HDvZALo0BDETv9kWbi16J52eHs+PWRNmF4ub6nZqfVOeesOvntg==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.0.0", + "yaml": "^2.0.0" + }, + "engines": { + "node": "^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ota-meshi" + } + }, + "node_modules/yaml-eslint-parser/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..f2a82e0 --- /dev/null +++ b/package.json @@ -0,0 +1,35 @@ +{ + "name": "mini-world-map", + "version": "0.1.3", + "description": "Hierarchy-first 2D radial rings and 3D map views for your Obsidian vault", + "main": "main.js", + "type": "module", + "scripts": { + "dev": "node esbuild.config.mjs", + "build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production", + "test": "vitest run", + "version": "node version-bump.mjs && git add manifest.json versions.json", + "lint": "eslint ." + }, + "keywords": [], + "license": "MIT", + "dependencies": { + "3d-force-graph": "1.80.0", + "d3-force-3d": "3.0.6", + "three": "0.184.0", + "three-spritetext": "1.10.0" + }, + "devDependencies": { + "@eslint/js": "^9.39.4", + "@types/node": "^22.15.17", + "@types/three": "^0.184.0", + "esbuild": "0.25.5", + "eslint": "^9.39.4", + "globals": "^17.6.0", + "jiti": "^2.6.1", + "obsidian": "latest", + "typescript": "^5.8.3", + "typescript-eslint": "^8.59.1", + "vitest": "^3.0.0" + } +} diff --git a/src/bench/bench.ts b/src/bench/bench.ts new file mode 100644 index 0000000..807475f --- /dev/null +++ b/src/bench/bench.ts @@ -0,0 +1,83 @@ +import type { App } from 'obsidian'; +import { normalizePath } from 'obsidian'; +import type { BenchResult, FrameStats } from '../types'; + +const BENCH_DIR = '_galaxy_bench'; + +/** 收集 durationMs 内的 rAF 帧间隔;onFrame 可用于驱动脚本镜头 */ +export function collectFrames( + durationMs: number, + onFrame?: (elapsedMs: number) => void, +): Promise { + return new Promise((resolve) => { + const deltas: number[] = []; + let last = performance.now(); + const start = last; + const tick = (now: number) => { + deltas.push(now - last); + last = now; + const elapsed = now - start; + onFrame?.(elapsed); + if (elapsed < durationMs) { + window.requestAnimationFrame(tick); + } else { + deltas.shift(); // 首帧间隔含启动开销,丢弃 + const sorted = [...deltas].sort((a, b) => a - b); + const total = deltas.reduce((s, d) => s + d, 0); + const p95 = sorted[Math.min(Math.floor(sorted.length * 0.95), sorted.length - 1)] ?? 0; + resolve({ + frames: deltas.length, + avgFps: deltas.length > 0 ? 1000 / (total / deltas.length) : 0, + p95FrameMs: p95, + worstFrameMs: sorted[sorted.length - 1] ?? 0, + durationMs: total, + }); + } + }; + window.requestAnimationFrame(tick); + }); +} + +/** 观察主线程 longtask;返回 stop() 取回结果 */ +export function observeLongTasks(): { stop: () => { count: number; longestMs: number; totalMs: number } } { + let count = 0; + let longestMs = 0; + let totalMs = 0; + let observer: PerformanceObserver | null = null; + try { + observer = new PerformanceObserver((list) => { + for (const entry of list.getEntries()) { + count++; + totalMs += entry.duration; + if (entry.duration > longestMs) longestMs = entry.duration; + } + }); + observer.observe({ entryTypes: ['longtask'] }); + } catch { + // longtask observer 不可用时降级为零值(结果中可见) + } + return { + stop: () => { + observer?.disconnect(); + return { count, longestMs, totalMs }; + }, + }; +} + +export function heapUsed(): number { + const perf = performance as unknown as { memory?: { usedJSHeapSize: number } }; + return perf.memory?.usedJSHeapSize ?? -1; +} + +export async function writeBenchResult(app: App, result: BenchResult): Promise { + const adapter = app.vault.adapter; + const dir = normalizePath(BENCH_DIR); + if (!(await adapter.exists(dir))) await adapter.mkdir(dir); + const file = normalizePath(`${BENCH_DIR}/${result.scenario}-${Date.now()}.json`); + await adapter.write(file, JSON.stringify(result, null, 2)); + return file; +} + +export function sleep(ms: number): Promise { + return new Promise((r) => window.setTimeout(r, ms)); +} diff --git a/src/constants.ts b/src/constants.ts new file mode 100644 index 0000000..3d61323 --- /dev/null +++ b/src/constants.ts @@ -0,0 +1,34 @@ +export const VIEW_TYPE_MINI_WORLD_MAP = 'mini-world-map-view'; +export const VIEW_TYPE_GALAXY = VIEW_TYPE_MINI_WORLD_MAP; +// 力学/辉光/外观的默认值与持久化见 src/settings.ts + +// 节点尺寸(世界单位):2.2×(1+0.5√degree),上限 6 倍——枢纽不能吞掉画面 +export const NODE_BASE_RADIUS = 2.2; +export const NODE_MAX_RADIUS = NODE_BASE_RADIUS * 6; + +// NASA 配方(bloom 初值会立刻被 settings 覆盖,见 GraphController.applySettings) +export const BACKGROUND_COLOR = 0x000003; +export const BLOOM_DEFAULTS = { strength: 0.6, radius: 0.4, threshold: 0.18 }; +export const LINK_OPACITY = 0.16; + +// 镜头编排(数字来自视觉规格,实现者无需品味) +export const CRUISE = { + angularSpeed: 0.022, // rad/s + elevationDeg: 8, + elevationPeriodS: 90, + radiusBreath: 0.04, + radiusPeriodS: 60, + resumeDelayMs: 10_000, + rampUpMs: 2_000, +}; +export const FLY_TO = { + distancePerRadius: 12, + minDistance: 40, + maxDistance: 140, + azimuthOffsetRad: (15 * Math.PI) / 180, + minMs: 800, + maxMs: 1800, + msPerWorldUnit: 0.45, +}; + +export const STARFIELD_ROTATION_RAD_PER_S = 0.0008; diff --git a/src/data/GraphStore.ts b/src/data/GraphStore.ts new file mode 100644 index 0000000..f6bf063 --- /dev/null +++ b/src/data/GraphStore.ts @@ -0,0 +1,109 @@ +import type { App } from 'obsidian'; +import { Component, debounce } from 'obsidian'; +import type { GraphData } from '../types'; +import { buildGraph } from './buildGraph'; +import { seedPosition, seedRadius } from './seed'; + +/** + * 唯一读 metadataCache 的模块。 + * 持有 GraphData + 坐标数组;重建时按 id 保留旧坐标(身份保持合并), + * 布局只需低温重热,星系不爆炸。 + */ +export class GraphStore extends Component { + data: GraphData = { nodes: [], links: [] }; + /** x,y,z × nodes.length,布局引擎原地写,渲染器只读 */ + positions = new Float32Array(0); + + private includeUnresolved = false; + private includeOrphans = true; + private nodeCap: number | null = null; + private linkCap: number | null = null; + private onChanged: (() => void) | null = null; + + constructor(private app: App) { + super(); + } + + /** dataChanged 在防抖重建完成后触发(调用方负责 reheat + 重建渲染缓冲) */ + init(includeUnresolved: boolean, includeOrphans: boolean, onChanged: () => void): void { + this.includeUnresolved = includeUnresolved; + this.includeOrphans = includeOrphans; + this.onChanged = onChanged; + const rebuildSoon = debounce(() => this.rebuild(true), 800, true); + this.registerEvent(this.app.metadataCache.on('resolved', rebuildSoon)); + this.registerEvent(this.app.vault.on('rename', rebuildSoon)); + this.registerEvent(this.app.vault.on('delete', rebuildSoon)); + } + + async ensureCacheReady(): Promise { + if (Object.keys(this.app.metadataCache.resolvedLinks).length > 0) return; + await new Promise((resolve) => { + this.registerEvent(this.app.metadataCache.on('resolved', () => resolve())); + }); + } + + setIncludeUnresolved(v: boolean): void { + if (v === this.includeUnresolved) return; + this.includeUnresolved = v; + this.rebuild(true); + } + + getIncludeUnresolved(): boolean { + return this.includeUnresolved; + } + + /** 质量档位的节点/链接帽;变化时重建(保坐标) */ + setCaps(nodeCap: number | null, linkCap: number | null): void { + if (nodeCap === this.nodeCap && linkCap === this.linkCap) return; + this.nodeCap = nodeCap; + this.linkCap = linkCap; + this.rebuild(true); + } + + setIncludeOrphans(v: boolean): void { + if (v === this.includeOrphans) return; + this.includeOrphans = v; + this.rebuild(true); + } + + /** preservePositions=false 用于基准(全新确定性种子 → 完整冷布局) */ + rebuild(preservePositions: boolean): void { + const files = this.app.vault.getMarkdownFiles().map((f) => ({ + path: f.path, + basename: f.basename, + size: f.stat.size, + })); + const next = buildGraph(files, this.app.metadataCache.resolvedLinks, this.app.metadataCache.unresolvedLinks, { + includeUnresolved: this.includeUnresolved, + includeOrphans: this.includeOrphans, + nodeCap: this.nodeCap, + linkCap: this.linkCap, + }); + + const oldIndexById = new Map(); + if (preservePositions) { + this.data.nodes.forEach((n, i) => oldIndexById.set(n.id, i)); + } + const oldPositions = this.positions; + + const radius = seedRadius(next.nodes.length); + const positions = new Float32Array(next.nodes.length * 3); + next.nodes.forEach((n, i) => { + const oi = oldIndexById.get(n.id); + if (oi !== undefined && oi * 3 + 2 < oldPositions.length) { + positions[i * 3] = oldPositions[oi * 3] ?? 0; + positions[i * 3 + 1] = oldPositions[oi * 3 + 1] ?? 0; + positions[i * 3 + 2] = oldPositions[oi * 3 + 2] ?? 0; + } else { + const [x, y, z] = seedPosition(n.id, radius); + positions[i * 3] = x; + positions[i * 3 + 1] = y; + positions[i * 3 + 2] = z; + } + }); + + this.data = next; + this.positions = positions; + this.onChanged?.(); + } +} diff --git a/src/data/buildGraph.ts b/src/data/buildGraph.ts new file mode 100644 index 0000000..0d84e13 --- /dev/null +++ b/src/data/buildGraph.ts @@ -0,0 +1,151 @@ +import type { GraphData, GraphLink, GraphNode } from '../types'; + + +/** 输入用纯记录,不依赖 obsidian —— 可单测(设计要求) */ +export interface FileRecord { + path: string; + basename: string; + size?: number; // 字节 +} + +export type LinkTable = Record>; + +export interface BuildOptions { + includeUnresolved: boolean; + includeOrphans: boolean; + /** 移动档:按度数取 top N 节点(保枢纽结构),null=不限 */ + nodeCap?: number | null; + /** 移动档:按 min(端点度数) 取 top N 边,null=不限 */ + linkCap?: number | null; +} + +function topFolder(path: string): string { + const idx = path.indexOf('/'); + return idx === -1 ? '' : path.slice(0, idx); +} + +/** + * vault 快照 → 图模型。 + * - 只收 files 集合内的目标(附件等非 md 链接目标被丢弃) + * - resolvedLinks 本身已按 (src,dst) 去重(值是出现次数),无需再去重 + * - degree = 出度 + 入度 + */ +export function buildGraph( + files: FileRecord[], + resolvedLinks: LinkTable, + unresolvedLinks: LinkTable, + opts: BuildOptions, +): GraphData { + const nodes: GraphNode[] = []; + const indexById = new Map(); + + for (const f of files) { + indexById.set(f.path, nodes.length); + nodes.push({ + id: f.path, + name: f.basename, + folderTop: topFolder(f.path), + degree: 0, + inDegree: 0, + outDegree: 0, + fileSize: f.size ?? 0, + unresolved: false, + }); + } + + const links: GraphLink[] = []; + const addLink = (si: number, ti: number) => { + links.push({ source: si, target: ti }); + const s = nodes[si]; + const t = nodes[ti]; + if (s) { + s.degree++; + s.outDegree++; + } + if (t) { + t.degree++; + t.inDegree++; + } + }; + + for (const src of Object.keys(resolvedLinks)) { + const si = indexById.get(src); + if (si === undefined) continue; + const targets = resolvedLinks[src] ?? {}; + for (const dst of Object.keys(targets)) { + const ti = indexById.get(dst); + if (ti === undefined) continue; + addLink(si, ti); + } + } + + if (opts.includeUnresolved) { + for (const src of Object.keys(unresolvedLinks)) { + const si = indexById.get(src); + if (si === undefined) continue; + const targets = unresolvedLinks[src] ?? {}; + for (const name of Object.keys(targets)) { + const ghostId = `unresolved:${name}`; + let gi = indexById.get(ghostId); + if (gi === undefined) { + gi = nodes.length; + indexById.set(ghostId, gi); + nodes.push({ + id: ghostId, + name, + folderTop: '__unresolved__', + degree: 0, + inDegree: 0, + outDegree: 0, + fileSize: 0, + unresolved: true, + }); + } + addLink(si, gi); + } + } + } + + let result: GraphData = { nodes, links }; + if (!opts.includeOrphans) { + // 过滤孤儿(degree 0):被过滤节点必然无边,只需重排索引 + result = filterNodes(result, (n) => n.degree > 0); + } + const nodeCap = opts.nodeCap ?? null; + if (nodeCap !== null && result.nodes.length > nodeCap) { + // 度数榜 top N(并列按原序)——保住枢纽结构,「仍像这座库」 + const ranked = [...result.nodes.entries()].sort((a, b) => b[1].degree - a[1].degree || a[0] - b[0]); + const keepIdx = new Set(ranked.slice(0, nodeCap).map(([i]) => i)); + result = filterNodes(result, (_n, i) => keepIdx.has(i)); + } + const linkCap = opts.linkCap ?? null; + if (linkCap !== null && result.links.length > linkCap) { + const deg = (i: number) => result.nodes[i]?.degree ?? 0; + result = { + nodes: result.nodes, + links: [...result.links.entries()] + .sort((a, b) => Math.min(deg(b[1].source), deg(b[1].target)) - Math.min(deg(a[1].source), deg(a[1].target)) || a[0] - b[0]) + .slice(0, linkCap) + .map(([, l]) => l), + }; + } + return result; +} + +function filterNodes(g: GraphData, keep: (n: GraphNode, i: number) => boolean): GraphData { + const remap = new Map(); + const kept: GraphNode[] = []; + g.nodes.forEach((n, i) => { + if (keep(n, i)) { + remap.set(i, kept.length); + kept.push(n); + } + }); + const links: GraphLink[] = []; + for (const l of g.links) { + const s2 = remap.get(l.source); + const t2 = remap.get(l.target); + if (s2 !== undefined && t2 !== undefined) links.push({ source: s2, target: t2 }); + } + return { nodes: kept, links }; +} diff --git a/src/data/seed.ts b/src/data/seed.ts new file mode 100644 index 0000000..4e96c75 --- /dev/null +++ b/src/data/seed.ts @@ -0,0 +1,29 @@ +// FNV-1a 确定性散列:种子布局(基准可复现)与调色板分配共用 +export function hash32(s: string): number { + let h = 0x811c9dc5; + for (let i = 0; i < s.length; i++) { + h ^= s.charCodeAt(i); + h = Math.imul(h, 0x01000193); + } + return h >>> 0; +} + +export function unit(s: string): number { + return hash32(s) / 0xffffffff; +} + +/** 按 id 确定性地撒进半径 radius 的球内,返回 [x,y,z] */ +export function seedPosition(id: string, radius: number): [number, number, number] { + const u = unit(id); + const v = unit(id + ':v'); + const w = unit(id + ':w'); + const r = radius * Math.cbrt(u); + const theta = 2 * Math.PI * v; + const phi = Math.acos(2 * w - 1); + return [r * Math.sin(phi) * Math.cos(theta), r * Math.sin(phi) * Math.sin(theta), r * Math.cos(phi)]; +} + +/** 与节点规模匹配的种子球半径 */ +export function seedRadius(nodeCount: number): number { + return 80 * Math.cbrt(Math.max(nodeCount, 1) / 1000); +} diff --git a/src/i18n.ts b/src/i18n.ts new file mode 100644 index 0000000..4053c28 --- /dev/null +++ b/src/i18n.ts @@ -0,0 +1,309 @@ +import type { + ColorScheme, + ExternalDetailMode, + HoverHighlightMode, + HoverTargetMode, + LabelVisibility, + Language, + ViewMode, +} from './settings'; + +export const LANGUAGE_OPTIONS: [Language, string][] = [ + ['en', 'English'], + ['zh', '中文'], +]; + +const STRINGS: Record> = { + 'language': { en: 'Language', zh: '语言' }, + 'language.en': { en: 'English', zh: 'English' }, + 'language.zh': { en: 'Chinese', zh: '中文' }, + 'mode.radial2d': { en: '2D radial', zh: '2D 环形图' }, + 'mode.map3d': { en: '3D map', zh: '3D 地图' }, + 'stats.counts': { en: '{nodes} nodes / {links} links', zh: '{nodes} 个节点 / {links} 条链接' }, + 'stats.3d': { + en: '{fps} fps · {calls} calls · {nodes}n/{links}l · {state}', + zh: '{fps} fps · {calls} calls · {nodes} 节点/{links} 链接 · {state}', + }, + 'state.settled': { en: 'settled', zh: '已沉降' }, + 'state.layout': { en: 'layout', zh: '布局中' }, + + 'tab.inspect': { en: 'Inspect', zh: '检查' }, + 'tab.pins': { en: 'Pins', zh: '固定' }, + 'tab.view': { en: 'View', zh: '视图' }, + 'tab.controls': { en: 'Controls', zh: '控制' }, + 'tab.defaults': { en: 'Defaults', zh: '默认' }, + 'tab.appearance': { en: 'Appearance', zh: '外观' }, + 'tab.physics': { en: 'Physics', zh: '力学' }, + 'tab.motion': { en: 'Motion', zh: '运动' }, + 'tab.advanced': { en: 'Advanced', zh: '高级' }, + + 'common.search': { en: 'Search', zh: '搜索' }, + 'common.recenter': { en: 'Center', zh: '回中心' }, + 'common.fit': { en: 'Fit', zh: '适配' }, + 'common.rebuild': { en: 'Rebuild', zh: '重建' }, + 'common.pin': { en: 'Pin', zh: '固定' }, + 'common.pinCurrent': { en: 'Pin current', zh: '固定当前' }, + 'common.clear': { en: 'Clear', zh: '清空' }, + 'common.group': { en: 'Group', zh: '分组' }, + 'common.inspect': { en: 'Inspect', zh: '检查' }, + 'common.open': { en: 'Open', zh: '打开' }, + 'common.focus': { en: 'Focus', zh: '聚焦' }, + 'common.root': { en: 'Root', zh: '根节点' }, + 'common.source': { en: 'Source', zh: '来源' }, + 'common.target': { en: 'Target', zh: '目标' }, + 'common.none': { en: 'None', zh: '无' }, + 'common.default': { en: 'Default', zh: '默认' }, + 'common.resetDefaults': { en: 'Reset defaults', zh: '重置默认' }, + + 'view.atlas': { en: 'Atlas', zh: '图谱' }, + 'view.focus': { en: 'Focus', zh: '聚焦' }, + 'view.vaultRoot': { en: 'Vault root', zh: '库根目录' }, + 'view.mode': { en: 'Map mode', zh: '地图模式' }, + 'view.theme': { en: 'Theme', zh: '主题' }, + 'theme.auto': { en: 'System', zh: '跟随系统' }, + 'theme.radialAuto': { en: 'System', zh: '跟随系统' }, + 'theme.day': { en: 'Light', zh: '浅色' }, + 'theme.night': { en: 'Dark', zh: '深色' }, + 'theme.deep': { en: 'Deep space', zh: '深空' }, + + 'inspect.type': { en: 'Type', zh: '类型' }, + 'inspect.depth': { en: 'Depth', zh: '深度' }, + 'inspect.notes': { en: 'Notes', zh: '笔记' }, + 'inspect.out': { en: 'Out', zh: '出链' }, + 'inspect.in': { en: 'In', zh: '入链' }, + 'inspect.linkOverlay': { en: 'Note link', zh: '笔记链接' }, + 'inspect.weight': { en: 'Weight', zh: '权重' }, + 'inspect.raw': { en: 'Raw', zh: '原始' }, + 'inspect.unresolved': { en: 'Unresolved', zh: '未解析' }, + 'inspect.external': { en: 'Outside', zh: '外部' }, + 'inspect.outgoing': { en: 'Outgoing ({count})', zh: '出链({count})' }, + 'inspect.backlinks': { en: 'Backlinks ({count})', zh: '反向链接({count})' }, + + 'pins.groupName': { en: 'Group name', zh: '分组名称' }, + 'pins.empty': { en: 'No pinned paths.', zh: '暂无固定路径。' }, + 'pins.already': { en: 'That path is already pinned.', zh: '这条路径已经固定。' }, + 'pins.selectFirst': { en: 'Select pinned paths to group.', zh: '先选择要分组的固定路径。' }, + 'pins.selectBeforePin': { en: 'Select or hover a path before pinning.', zh: '先选择或悬停一条路径再固定。' }, + + 'control.depth': { en: 'Depth', zh: '深度' }, + 'control.nodes': { en: 'Nodes', zh: '节点' }, + 'control.noteLinks': { en: 'Note links', zh: '笔记链接' }, + 'control.showNoteLinks': { en: 'Show note links', zh: '显示笔记链接' }, + 'control.hover': { en: 'Hover', zh: '悬停' }, + 'control.hoverTargets': { en: 'Hover targets', zh: '悬停对象' }, + 'control.labels': { en: 'Labels', zh: '标签' }, + 'control.spin': { en: 'Ring spin', zh: '环形旋转' }, + 'control.outsideLinks': { en: 'Outside links', zh: '外部链接' }, + 'control.outsideDetail': { en: 'Outside detail', zh: '外部细节' }, + 'control.exactOutsideFiles': { en: 'Exact outside notes', zh: '精确外部笔记' }, + 'control.legend': { en: 'Legend', zh: '图例' }, + 'control.defaultDepth': { en: 'Default depth', zh: '默认深度' }, + 'control.defaultNodes': { en: 'Default nodes', zh: '默认节点数' }, + 'control.defaultNoteLinks': { en: 'Default note links', zh: '默认笔记链接' }, + 'control.unresolvedLinks': { en: 'Unresolved links', zh: '未解析链接' }, + 'control.ignoredFolders': { en: 'Ignored folders', zh: '忽略文件夹' }, + + 'hover.none': { en: 'None', zh: '无' }, + 'hover.note-links': { en: 'Note links', zh: '笔记链接' }, + 'hover.hierarchy-parents': { en: 'Parents', zh: '父级' }, + 'hover.hierarchy-direct-children': { en: 'Direct children', zh: '直接子级' }, + 'hover.hierarchy-descendants': { en: 'All children', zh: '全部子级' }, + 'hover.hierarchy-parents-direct': { en: 'Parents + direct children', zh: '父级 + 直接子级' }, + 'hover.hierarchy-all': { en: 'Parents + all children', zh: '父级 + 全部子级' }, + 'hoverTarget.nodes': { en: 'Nodes only', zh: '仅节点' }, + 'hoverTarget.links': { en: 'Links only', zh: '仅链接' }, + 'hoverTarget.both': { en: 'Nodes + links', zh: '节点 + 链接' }, + 'labels.auto': { en: 'Auto', zh: '自动' }, + 'labels.hover': { en: 'Hover only', zh: '仅悬停' }, + 'outside.grouped': { en: 'Groups', zh: '分组' }, + 'outside.selected': { en: 'Selected', zh: '选中' }, + 'outside.exact': { en: 'Exact', zh: '精确' }, + + 'legend.root': { en: 'Root', zh: '根节点' }, + 'legend.root.desc': { en: 'Current atlas root', zh: '当前图谱根节点' }, + 'legend.folder': { en: 'Folders', zh: '文件夹' }, + 'legend.folder.desc': { en: 'Folders without a same-name child note', zh: '没有同名子笔记的文件夹' }, + 'legend.folderMeta': { en: 'Folder notes', zh: '文件夹笔记' }, + 'legend.folderMeta.desc': { en: 'Folder merged with its same-name child note', zh: '与同名子笔记合并的文件夹' }, + 'legend.note': { en: 'Notes', zh: '笔记' }, + 'legend.note.desc': { en: 'Markdown notes', zh: 'Markdown 笔记' }, + 'legend.outsideGroup': { en: 'Outside groups', zh: '外部分组' }, + 'legend.outsideGroup.desc': { en: 'Grouped branches outside the root', zh: '根节点外的分组分支' }, + 'legend.outsideNote': { en: 'Outside notes', zh: '外部笔记' }, + 'legend.outsideNote.desc': { en: 'Exact linked notes outside the root', zh: '根节点外的精确链接笔记' }, + 'legend.unresolvedNote': { en: 'Unresolved notes', zh: '未解析笔记' }, + 'legend.unresolvedNote.desc': { en: 'Unresolved internal link targets', zh: '未解析的内部链接目标' }, + 'legend.hierarchy': { en: 'Hierarchy', zh: '层级' }, + 'legend.hierarchy.desc': { en: 'Parent-child hierarchy edges', zh: '父子层级边' }, + 'legend.noteLinks': { en: 'Note links', zh: '笔记链接' }, + 'legend.noteLinks.desc': { en: 'Internal markdown links', zh: '内部 Markdown 链接' }, + 'legend.outsideLinks': { en: 'Outside links', zh: '外部链接' }, + 'legend.outsideLinks.desc': { en: 'Links crossing the current root', zh: '跨出当前根节点的链接' }, + 'legend.unresolvedLinks': { en: 'Unresolved links', zh: '未解析链接' }, + 'legend.unresolvedLinks.desc': { en: 'Links involving unresolved targets', zh: '包含未解析目标的链接' }, + + 'loading.radial': { en: 'Building map…', zh: '构建地图…' }, + 'loading.3d': { en: 'Building map…', zh: '构建地图…' }, + 'context.openNote': { en: 'Open note', zh: '打开笔记' }, + 'context.focusNote': { en: 'Focus note', zh: '聚焦笔记' }, + 'context.useAsRoot': { en: 'Use as atlas root', zh: '设为图谱根节点' }, + 'context.openRepresentative': { en: 'Open representative note', zh: '打开代表笔记' }, + 'context.pinPath': { en: 'Pin highlighted path', zh: '固定高亮路径' }, + + '3d.cruiseOn': { en: 'Cruise: on', zh: '巡航:开' }, + '3d.cruiseOff': { en: 'Cruise: off', zh: '巡航:关' }, + '3d.reveal': { en: 'Reveal', zh: '创世动画' }, + '3d.glow': { en: 'Glow', zh: '辉光' }, + '3d.glowStrength': { en: 'Strength', zh: '强度' }, + '3d.glowRadius': { en: 'Radius', zh: '扩散' }, + '3d.glowThreshold': { en: 'Threshold', zh: '阈值' }, + '3d.repel': { en: 'Repel', zh: '斥力' }, + '3d.linkDistance': { en: 'Link distance', zh: '链接距离' }, + '3d.linkStrength': { en: 'Link strength', zh: '链接强度' }, + '3d.centerPull': { en: 'Center pull', zh: '向心力' }, + '3d.flatten': { en: 'Flatten', zh: '扁平度' }, + '3d.nodeSize': { en: 'Node size', zh: '节点大小' }, + '3d.linkOpacity': { en: 'Link opacity', zh: '链接透明度' }, + '3d.twinkle': { en: 'Twinkle', zh: '星星眨眼' }, + '3d.twinkleOff': { en: 'Off', zh: '关' }, + '3d.size.degree': { en: 'Size: links', zh: '大小:链接数' }, + '3d.size.fileSize': { en: 'Size: file size', zh: '大小:文档量' }, + '3d.size.uniform': { en: 'Size: uniform', zh: '大小:一致' }, + '3d.colorTheme': { en: 'Color theme…', zh: '配色主题…' }, + '3d.importColors': { en: 'Import 2D colors', zh: '导入二维配色' }, + '3d.shuffleColors': { en: 'Shuffle colors', zh: '配色洗牌' }, + '3d.speed': { en: 'Speed', zh: '速度' }, + '3d.unresolvedShow': { en: 'Unresolved: show', zh: '未解析:显示' }, + '3d.unresolvedHide': { en: 'Unresolved: hide', zh: '未解析:隐藏' }, + '3d.orphansShow': { en: 'Orphans: show', zh: '孤儿:显示' }, + '3d.orphansHide': { en: 'Orphans: hide', zh: '孤儿:隐藏' }, + '3d.quality.auto': { en: 'Quality: auto', zh: '画质:自动' }, + '3d.quality.high': { en: 'Quality: high', zh: '画质:高' }, + '3d.quality.low': { en: 'Quality: low', zh: '画质:低' }, + '3d.quality.mobile': { en: 'Quality: mobile', zh: '画质:移动模拟' }, + '3d.help.drag': { en: 'Left drag = orbit · wheel = zoom', zh: '左键拖 = 环绕 · 滚轮 = 缩放' }, + '3d.help.pan': { + en: 'Right drag / Cmd or Shift + left drag = pan', + zh: '右键拖 / ⌘或⇧+左键拖 = 平移', + }, + '3d.help.mac': { en: 'macOS treats Ctrl+click as right-click', zh: 'macOS 的 Ctrl+点击会被系统当右键' }, + '3d.help.fly': { en: 'WASD = fly · Q/E = rise/fall · Shift = fast', zh: 'WASD = 平飞 · Q/E = 升降 · Shift = 加速' }, + '3d.help.pick': { en: 'Click node = select and fly · ESC = clear', zh: '点击节点 = 选中飞行 · ESC = 取消' }, + '3d.help.keys': { en: 'F = fly to selected · R = overview', zh: 'F = 飞向选中 · R = 回总览' }, + '3d.help.slider': { en: 'Double-click a slider to reset it', zh: '双击滑杆 = 回默认值' }, + '3d.revealWait': { en: 'The map is still settling. Try reveal after it settles.', zh: '星系还在成形中,沉降后再试。' }, + '3d.workerFallback': { + en: 'Mini World Map 3D: background layout worker is unavailable; using the main thread.', + zh: 'Mini World Map 3D:后台线程不可用,已回退主线程布局。', + }, + '3d.mobileCap': { + en: 'Mobile quality: showing the top {cap} linked nodes out of {total}.', + zh: '移动档:已显示链接最多的前 {cap} 个节点(共 {total})。', + }, + '3d.performanceMode': { + en: 'Mini World Map 3D switched to performance mode. Change quality in Advanced.', + zh: 'Mini World Map 3D:已自动切换到性能模式,可在高级页改回。', + }, + '3d.importMissing': { en: 'No 2D graph color groups found in graph.json.', zh: '未找到自带图谱的颜色分组(graph.json)。' }, + '3d.importDone': { en: 'Imported {count} 2D color groups.', zh: '已导入 {count} 组 2D 图谱配色。' }, + '3d.shuffleMissing': { en: 'Import 2D colors before shuffling.', zh: '先导入二维图谱配色,才能洗牌。' }, + '3d.contextLost': { en: 'Rendering context lost. Click to rebuild.', zh: '渲染上下文丢失,点击重建。' }, + '3d.searchPlaceholder': { en: 'Search notes, press Enter to fly…', zh: '搜索笔记,回车飞过去…' }, + '3d.searchUnresolved': { en: 'Unresolved', zh: '未解析' }, + '3d.searchLinks': { en: '{count} links', zh: '{count} 链接' }, + '3d.card.unresolved': { en: 'Unresolved link (note does not exist)', zh: '未解析链接(笔记尚不存在)' }, + '3d.card.root': { en: 'Vault root', zh: '根目录' }, + '3d.card.stats': { en: '↩ {in} backlinks · → {out} outgoing', zh: '↩ {in} 反链 · → {out} 出链' }, + '3d.card.modified': { en: ' · modified {date}', zh: ' · 改于 {date}' }, + '3d.card.empty': { en: '(empty note)', zh: '(空笔记)' }, + '3d.bench.wait': { en: '{scenario}: waiting for layout to settle…', zh: '{scenario}:等待布局沉降…' }, + '3d.bench.orbit': { en: '{scenario}: 20s orbit FPS run…', zh: '{scenario}:20s 环绕测帧率…' }, + '3d.bench.done': { en: '{scenario} done: avg {fps} fps · {calls} calls', zh: '{scenario} 完成:avg {fps} fps · {calls} calls' }, + '3d.bench.s2Start': { + en: 'S2: cold layout started. The interface should remain responsive.', + zh: 'S2:冷布局开始(预算化 tick,期间界面应保持可用)…', + }, + '3d.bench.s2Done': { + en: 'S2 done: settled in {seconds}s / {ticks} ticks, longest block {longest}ms', + zh: 'S2 完成:沉降 {seconds}s / {ticks} ticks,最长阻塞 {longest}ms', + }, + + 'style.galaxy': { en: 'Galaxy', zh: '银河' }, + 'style.nebula': { en: 'Nebula', zh: '星云' }, + 'style.minimal': { en: 'Minimal', zh: '极简' }, + 'style.fireworks': { en: 'Fireworks', zh: '烟火' }, + 'color.hubble': { en: 'Hubble deep field', zh: '哈勃深空' }, + 'color.tiktok': { en: 'Neon pop', zh: '抖音霓虹' }, + 'color.sunset': { en: 'Sunset film', zh: '落日胶片' }, + 'color.cyber': { en: 'Cyber city', zh: '赛博都市' }, + 'color.matrix': { en: 'Matrix', zh: '黑客帝国' }, + 'color.aurora': { en: 'Aurora', zh: '极光' }, + + 'settings.title': { en: 'Mini World Map', zh: 'Mini World Map' }, + 'settings.defaultMode': { en: 'Default render mode', zh: '默认渲染模式' }, + 'settings.defaultModeDesc': { + en: '2D radial rings is the hierarchy-first map. 3D map uses the Galaxy renderer.', + zh: '2D 环形图以层级为主;3D 地图使用 Galaxy 渲染器。', + }, + 'settings.languageDesc': { en: 'Language used by both 2D and 3D panels.', zh: '2D 与 3D 面板共同使用的语言。' }, + 'settings.depthDesc': { + en: 'How many hierarchy levels to render before deeper nodes are summarized by budgets.', + zh: '渲染多少层级后由预算汇总更深节点。', + }, + 'settings.nodeLimitDesc': { en: 'Maximum visible nodes in the 2D radial map.', zh: '2D 环形图最多显示的节点数。' }, + 'settings.linkLimitDesc': { en: 'Maximum aggregated note links to draw in 2D.', zh: '2D 中最多绘制的聚合笔记链接数。' }, + 'settings.showLinksDesc': { + en: 'Keeps note-link roads visible. Hover/link pinning still works when this is off.', + zh: '保持笔记链接可见;关闭后悬停与链接固定仍可用。', + }, + 'settings.hoverDesc': { en: 'Choose what 2D hover highlights.', zh: '选择 2D 悬停高亮的内容。' }, + 'settings.hoverTargetsDesc': { en: 'Choose whether nodes, note-link roads, or both react to hover and click.', zh: '选择节点、笔记链接道路或两者是否响应悬停和点击。' }, + 'settings.labelsDesc': { en: 'Auto shows important names by zoom; hover only keeps the map quieter.', zh: '自动按缩放显示重要名称;仅悬停会更安静。' }, + 'settings.spinDesc': { en: 'Optional radial ring motion in the 2D map.', zh: '2D 环形图中的可选环形运动。' }, + 'settings.unresolvedDesc': { en: 'Represent unresolved internal links as temporary nodes.', zh: '将未解析内部链接显示为临时节点。' }, + 'settings.ignoredDesc': { en: 'One folder path per line.', zh: '每行一个文件夹路径。' }, + 'notice.rebuilt': { en: 'Mini World Map rebuilt', zh: 'Mini World Map 已重建' }, + 'notice.openToBuild': { en: 'Open Mini World Map to build the index', zh: '打开 Mini World Map 后才能构建索引' }, + 'notice.switchTo3d': { en: 'Switch to 3D map to use fly-to search.', zh: '切换到 3D 地图后可使用飞行搜索。' }, +}; + +export function t(language: Language, key: string, vars: Record = {}): string { + const template = STRINGS[key]?.[language] ?? STRINGS[key]?.en ?? key; + return template.replace(/\{(\w+)\}/g, (_, name: string) => String(vars[name] ?? `{${name}}`)); +} + +export function viewModeLabel(language: Language, mode: ViewMode): string { + return t(language, `mode.${mode}`); +} + +export function colorSchemeOptions(language: Language): [ColorScheme, string][] { + return [ + ['auto', t(language, 'theme.radialAuto')], + ['day', t(language, 'theme.day')], + ['night', t(language, 'theme.night')], + ]; +} + +export function languageOptions(language: Language): [Language, string][] { + return LANGUAGE_OPTIONS.map(([value]) => [value, t(language, `language.${value}`)]); +} + +export function hoverModeOptions(language: Language, values: readonly [HoverHighlightMode, string][]): [HoverHighlightMode, string][] { + return values.map(([value]) => [value, t(language, `hover.${value}`)]); +} + +export function hoverTargetOptions(language: Language, values: readonly [HoverTargetMode, string][]): [HoverTargetMode, string][] { + return values.map(([value]) => [value, t(language, `hoverTarget.${value}`)]); +} + +export function labelVisibilityOptions(language: Language, values: readonly [LabelVisibility, string][]): [LabelVisibility, string][] { + return values.map(([value]) => [value, t(language, `labels.${value}`)]); +} + +export function outsideDetailOptions(language: Language): [ExternalDetailMode, string][] { + return [ + ['grouped', t(language, 'outside.grouped')], + ['selected', t(language, 'outside.selected')], + ['exact', t(language, 'outside.exact')], + ]; +} diff --git a/src/interactions/CameraDirector.ts b/src/interactions/CameraDirector.ts new file mode 100644 index 0000000..def8784 --- /dev/null +++ b/src/interactions/CameraDirector.ts @@ -0,0 +1,271 @@ +import { MOUSE, PerspectiveCamera, Spherical, Vector3 } from 'three'; +import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'; +import { CRUISE, FLY_TO } from '../constants'; + +function easeInOutCubic(t: number): number { + return t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2; +} + +interface Tween { + t0: number; + durMs: number; + fromPos: Vector3; + toPos: Vector3; + fromTarget: Vector3; + toTarget: Vector3; + onDone?: () => void; +} + +export interface CameraHooks { + /** F 键:飞向当前选中(无选中则忽略) */ + onFlyToSelected: () => void; + /** R 键:回总览 */ + onResetView: () => void; +} + +const FLY_KEYS = new Set(['w', 'a', 's', 'd', 'q', 'e']); + +/** + * 镜头导演:三层交互自由度(G1 反馈「像 FPS / Google Earth 一样移动」) + * - 轨道基底:左键拖环绕 · 右键拖 / Ctrl(⌘)+左键拖 平移 · 滚轮缩放 + * - FPS 飞行:WASD 前后左右 + Q/E 升降,速度随离目标距离自适应,Shift ×3 + * - 编排:点击/搜索飞行 tween、闲置巡航(双不可通约周期)、F 飞向选中、R 回总览 + * 按键只在画布聚焦时生效(tabindex),不抢 Obsidian 全局快捷键。 + */ +export class CameraDirector { + cruiseEnabled = true; + /** 巡航角速度倍率(面板「巡航速度」滑杆) */ + cruiseSpeed = 1; + + private controls: OrbitControls; + private tween: Tween | null = null; + private lastInputAt = 0; + private cruiseAnchor: Spherical | null = null; + private cruiseT = 0; + private cruiseDir = 1; + private pendingDensityDir: Vector3 | null = null; + private pressed = new Set(); + private shiftHeld = false; + private tmpOffset = new Vector3(); + private tmpSph = new Spherical(); + private tmpDir = new Vector3(); + private tmpRight = new Vector3(); + private disposeFns: (() => void)[] = []; + + constructor( + private camera: PerspectiveCamera, + private dom: HTMLElement, + private hooks: CameraHooks, + ) { + this.controls = new OrbitControls(camera, dom); + this.controls.enableDamping = true; + this.controls.dampingFactor = 0.08; + this.lastInputAt = performance.now(); + + dom.tabIndex = 0; // 画布可聚焦 → 键盘飞行不影响 Obsidian 其他快捷键 + this.bindPointer(); + this.bindKeys(); + } + + get target(): Vector3 { + return this.controls.target; + } + + private markInput(): void { + this.lastInputAt = performance.now(); + this.cruiseAnchor = null; + this.tween = null; // 任何输入打断飞行(停在当前位置,不跳变) + } + + private bindPointer(): void { + const onDown = (e: PointerEvent) => { + this.dom.focus(); + // Google Earth 式平移:⌘/Shift/Ctrl + 左键拖(macOS 的 Ctrl+点击被系统征用为右键, + // 所以 Mac 上主用 ⌘ 或 Shift;右键拖原生就是平移) + this.controls.mouseButtons.LEFT = + e.metaKey || e.shiftKey || e.ctrlKey ? MOUSE.PAN : MOUSE.ROTATE; + this.markInput(); + }; + const onInput = () => this.markInput(); + this.dom.addEventListener('pointerdown', onDown, { capture: true }); + this.dom.addEventListener('wheel', onInput, { passive: true }); + this.dom.addEventListener('touchstart', onInput, { passive: true }); + this.disposeFns.push(() => { + this.dom.removeEventListener('pointerdown', onDown, { capture: true }); + this.dom.removeEventListener('wheel', onInput); + this.dom.removeEventListener('touchstart', onInput); + }); + } + + private bindKeys(): void { + const onKeyDown = (e: KeyboardEvent) => { + const k = e.key.toLowerCase(); + this.shiftHeld = e.shiftKey; + if (FLY_KEYS.has(k)) { + this.pressed.add(k); + this.markInput(); + e.preventDefault(); + e.stopPropagation(); + } else if (k === 'f') { + this.hooks.onFlyToSelected(); + e.preventDefault(); + } else if (k === 'r') { + this.hooks.onResetView(); + e.preventDefault(); + } + }; + const onKeyUp = (e: KeyboardEvent) => { + this.shiftHeld = e.shiftKey; + this.pressed.delete(e.key.toLowerCase()); + }; + const onBlur = () => this.pressed.clear(); + this.dom.addEventListener('keydown', onKeyDown); + this.dom.addEventListener('keyup', onKeyUp); + this.dom.addEventListener('blur', onBlur); + this.disposeFns.push(() => { + this.dom.removeEventListener('keydown', onKeyDown); + this.dom.removeEventListener('keyup', onKeyUp); + this.dom.removeEventListener('blur', onBlur); + }); + } + + /** WASD/QE:相机与轨道目标同步平移,飞完仍可正常环绕 */ + private applyFly(deltaS: number): boolean { + if (this.pressed.size === 0) return false; + const dist = this.camera.position.distanceTo(this.controls.target); + const speed = Math.min(Math.max(dist * 0.8, 10), 600) * (this.shiftHeld ? 3 : 1); + const fwd = this.camera.getWorldDirection(this.tmpDir); + this.tmpRight.crossVectors(fwd, this.camera.up).normalize(); + const move = new Vector3(); + if (this.pressed.has('w')) move.add(fwd); + if (this.pressed.has('s')) move.sub(fwd); + if (this.pressed.has('d')) move.add(this.tmpRight); + if (this.pressed.has('a')) move.sub(this.tmpRight); + if (this.pressed.has('e')) move.y += 1; + if (this.pressed.has('q')) move.y -= 1; + if (move.lengthSq() < 1e-8) return false; + move.normalize().multiplyScalar(speed * deltaS); + this.camera.position.add(move); + this.controls.target.add(move); + this.lastInputAt = performance.now(); + return true; + } + + /** 初始机位:质心外 2.2×半径、仰角 +18° */ + setInitialFraming(graphRadius: number): void { + this.camera.position.copy(this.framingPosition(graphRadius)); + this.controls.target.set(0, 0, 0); + this.controls.update(); + } + + private framingPosition(graphRadius: number): Vector3 { + const d = graphRadius * 2.2; + const elev = (18 * Math.PI) / 180; + return new Vector3(d * Math.cos(elev), d * Math.sin(elev), d * 0.35); + } + + /** R/回中心:平滑回总览 */ + resetView(graphRadius: number, onDone?: () => void): void { + this.startTween(this.framingPosition(graphRadius), new Vector3(0, 0, 0), 1200, onDone); + } + + /** + * 飞达节点后立即开始环绕(不等闲置 10s),且旋转方向优先扫过邻居密集的一侧 + * (G2 反馈:5 条链接 4 条朝南 → 先划过南方)。 + */ + beginFocusOrbit(densityDir: Vector3 | null): void { + this.pendingDensityDir = densityDir; + this.cruiseAnchor = null; + this.lastInputAt = performance.now() - CRUISE.resumeDelayMs - 1; + } + + flyTo(nodePos: Vector3, nodeRadius: number, onDone?: () => void): void { + const dist = Math.min(Math.max(nodeRadius * FLY_TO.distancePerRadius, FLY_TO.minDistance), FLY_TO.maxDistance); + // 保持当前视角方向但偏转 15° 方位角——到达时不正对节点,邻域可见 + const dir = this.camera.position.clone().sub(nodePos); + if (dir.lengthSq() < 1e-6) dir.set(0, 0, 1); + this.tmpSph.setFromVector3(dir); + this.tmpSph.theta += FLY_TO.azimuthOffsetRad; + this.tmpSph.radius = dist; + const toPos = nodePos.clone().add(new Vector3().setFromSpherical(this.tmpSph)); + const travel = this.camera.position.distanceTo(toPos); + const durMs = Math.min(Math.max(FLY_TO.minMs + FLY_TO.msPerWorldUnit * travel, FLY_TO.minMs), FLY_TO.maxMs); + this.startTween(toPos, nodePos.clone(), durMs, onDone); + } + + private startTween(toPos: Vector3, toTarget: Vector3, durMs: number, onDone?: () => void): void { + this.tween = { + t0: performance.now(), + durMs, + fromPos: this.camera.position.clone(), + toPos, + fromTarget: this.controls.target.clone(), + toTarget, + }; + if (onDone) this.tween.onDone = onDone; + } + + /** 每帧驱动;返回当前是否处于巡航中(HUD 显示用) */ + update(now: number, deltaS: number): boolean { + if (this.tween) { + const tw = this.tween; + const t = Math.min((now - tw.t0) / tw.durMs, 1); + const k = easeInOutCubic(t); + this.camera.position.lerpVectors(tw.fromPos, tw.toPos, k); + this.controls.target.lerpVectors(tw.fromTarget, tw.toTarget, k); + this.controls.update(); + if (t >= 1) { + this.tween = null; + tw.onDone?.(); + this.lastInputAt = now; // 到达后等满闲置时长再起巡航 + } + return false; + } + + const flying = this.applyFly(deltaS); + + const idleMs = now - this.lastInputAt; + if (!flying && this.cruiseEnabled && idleMs > CRUISE.resumeDelayMs) { + // 0→满速渐起,无顿挫 + const ramp = Math.min((idleMs - CRUISE.resumeDelayMs) / CRUISE.rampUpMs, 1); + if (!this.cruiseAnchor) { + this.cruiseAnchor = new Spherical().setFromVector3( + this.tmpOffset.copy(this.camera.position).sub(this.controls.target), + ); + this.cruiseT = 0; + this.cruiseDir = 1; + // 邻居密集方向 → 选择能更早扫过该侧的旋转方向 + if (this.pendingDensityDir && this.pendingDensityDir.lengthSq() > 1e-6) { + const densityTheta = new Spherical().setFromVector3(this.pendingDensityDir).theta; + let delta = densityTheta - this.cruiseAnchor.theta; + while (delta > Math.PI) delta -= 2 * Math.PI; + while (delta < -Math.PI) delta += 2 * Math.PI; + this.cruiseDir = delta >= 0 ? 1 : -1; + } + this.pendingDensityDir = null; + } + this.cruiseT += deltaS * ramp; + const t = this.cruiseT; + const a = this.cruiseAnchor; + const elev = ((CRUISE.elevationDeg * Math.PI) / 180) * Math.sin((2 * Math.PI * t) / CRUISE.elevationPeriodS); + const breath = 1 + CRUISE.radiusBreath * Math.sin((2 * Math.PI * t) / CRUISE.radiusPeriodS); + this.tmpSph.radius = a.radius * breath; + this.tmpSph.theta = a.theta + this.cruiseDir * CRUISE.angularSpeed * this.cruiseSpeed * t; + this.tmpSph.phi = Math.min(Math.max(a.phi + elev, 0.05), Math.PI - 0.05); + this.camera.position.setFromSpherical(this.tmpSph).add(this.controls.target); + this.camera.lookAt(this.controls.target); + return true; + } + + this.controls.update(); + return false; + } + + dispose(): void { + this.tween = null; + this.pressed.clear(); + for (const fn of this.disposeFns) fn(); + this.disposeFns = []; + this.controls.dispose(); + } +} diff --git a/src/layout/LayoutEngine.ts b/src/layout/LayoutEngine.ts new file mode 100644 index 0000000..923e388 --- /dev/null +++ b/src/layout/LayoutEngine.ts @@ -0,0 +1,23 @@ +import type { GraphData, LayoutParams } from '../types'; + +/** + * 布局引擎接口——前人死墙的隔离层。 + * M1: 主线程 d3-force-3d(预算化:每帧一个 tick,布局过程即动画) + * M3: Web Worker 实现(同接口,positions 经 transferable 回传) + */ +export interface LayoutEngine { + /** x,y,z × n;引擎原地写入,渲染器直接读 */ + readonly positions: Float32Array; + /** 自 init 起累计 tick 数(基准用,替代运行时方法替换 hack) */ + readonly ticks: number; + /** initialAlpha: 1=完整冷布局;低值(如 0.06 暖启动 / 0.3 增量更新)= 温和整理 */ + init(data: GraphData, positions: Float32Array, params: LayoutParams, initialAlpha?: number): void; + /** 跑一个 tick;返回 false 表示已沉降 */ + step(): boolean; + isSettled(): boolean; + /** 数据增量更新后低温重热 */ + reheat(alpha?: number): void; + /** 实时调参(控制面板滑杆):更新力参数并重热,星系当场重排 */ + updateParams(params: LayoutParams): void; + dispose(): void; +} diff --git a/src/layout/MainThreadForceLayout.ts b/src/layout/MainThreadForceLayout.ts new file mode 100644 index 0000000..47fed0e --- /dev/null +++ b/src/layout/MainThreadForceLayout.ts @@ -0,0 +1,109 @@ +import { forceLink, forceManyBody, forceSimulation, forceX, forceY, forceZ } from 'd3-force-3d'; +import type { SimLink, SimNode, Simulation } from 'd3-force-3d'; +import type { GraphData, LayoutParams } from '../types'; +import type { LayoutEngine } from './LayoutEngine'; + +interface LNode extends SimNode { + x: number; + y: number; + z: number; +} + +export class MainThreadForceLayout implements LayoutEngine { + positions: Float32Array = new Float32Array(0); + + private sim: Simulation | null = null; + private simNodes: LNode[] = []; + private degrees: number[] = []; + private settled = true; + private _ticks = 0; + + get ticks(): number { + return this._ticks; + } + + init(data: GraphData, positions: Float32Array, params: LayoutParams, initialAlpha = 1): void { + this.dispose(); + this.positions = positions; + this.degrees = data.nodes.map((n) => Math.max(n.degree, 1)); + this.simNodes = data.nodes.map((_, i) => ({ + x: positions[i * 3] ?? 0, + y: positions[i * 3 + 1] ?? 0, + z: positions[i * 3 + 2] ?? 0, + })); + const links: SimLink[] = data.links.map((l) => ({ source: l.source, target: l.target })); + + // forceSimulation 创建即自启动内部 timer——立刻 stop,改由渲染循环逐帧驱动 + this.sim = forceSimulation(this.simNodes, 3) + .alphaDecay(1 - Math.pow(0.001, 1 / 300)) + .velocityDecay(params.velocityDecay) + .force('link', forceLink(links).distance(params.linkDistance).strength(this.linkStrengthFn(params.linkStrength))) + .force('charge', forceManyBody().strength(params.charge).distanceMax(800)) + .force('x', forceX(0).strength(params.centerPull)) + .force('y', forceY(0).strength(params.centerPull + params.flatten)) + .force('z', forceZ(0).strength(params.centerPull)) + .stop(); + this.sim.alpha(initialAlpha); + this._ticks = 0; + this.settled = false; + } + + /** d3 默认 strength = 1/min(端点连接数),倍率叠加其上(保持枢纽不被拉爆的特性) */ + private linkStrengthFn(mult: number): (link: SimLink) => number { + const degrees = this.degrees; + return (link) => { + const s = typeof link.source === 'number' ? link.source : (link.source.index ?? 0); + const t = typeof link.target === 'number' ? link.target : (link.target.index ?? 0); + const base = 1 / Math.min(degrees[s] ?? 1, degrees[t] ?? 1); + return Math.min(base * mult, 1); + }; + } + + updateParams(params: LayoutParams): void { + const sim = this.sim; + if (!sim) return; + (sim.force('charge') as import('d3-force-3d').ManyBodyForce | undefined)?.strength(params.charge); + const link = sim.force('link') as import('d3-force-3d').LinkForce | undefined; + link?.distance(params.linkDistance); + link?.strength(this.linkStrengthFn(params.linkStrength)); + (sim.force('x') as import('d3-force-3d').PositionForce | undefined)?.strength(params.centerPull); + (sim.force('y') as import('d3-force-3d').PositionForce | undefined)?.strength(params.centerPull + params.flatten); + (sim.force('z') as import('d3-force-3d').PositionForce | undefined)?.strength(params.centerPull); + this.reheat(0.5); + } + + step(): boolean { + const sim = this.sim; + if (!sim || this.settled) return false; + sim.tick(); + this._ticks++; + const pos = this.positions; + const nodes = this.simNodes; + for (let i = 0; i < nodes.length; i++) { + const n = nodes[i]; + if (!n) continue; + pos[i * 3] = n.x; + pos[i * 3 + 1] = n.y; + pos[i * 3 + 2] = n.z; + } + if (sim.alpha() < sim.alphaMin()) this.settled = true; + return !this.settled; + } + + isSettled(): boolean { + return this.settled; + } + + reheat(alpha = 0.3): void { + if (!this.sim) return; + this.sim.alpha(Math.max(this.sim.alpha(), alpha)); + this.settled = false; + } + + dispose(): void { + this.sim?.stop(); + this.sim = null; + this.simNodes = []; + this.settled = true; + } +} diff --git a/src/layout/WorkerForceLayout.ts b/src/layout/WorkerForceLayout.ts new file mode 100644 index 0000000..e22321c --- /dev/null +++ b/src/layout/WorkerForceLayout.ts @@ -0,0 +1,115 @@ +import workerSource from 'worker:./forceWorker.ts'; +import type { GraphData, LayoutParams } from '../types'; +import type { LayoutEngine } from './LayoutEngine'; + +interface TickMsg { + type: 'tick'; + buffer: ArrayBuffer; + alpha: number; + settled: boolean; + ticks: number; +} + +/** + * Worker 布局(M3 性能硬化):d3-force-3d 跑在 Blob URL Worker 里, + * 坐标经 transferable 双缓冲乒乓回传——主线程每帧只剩一次 38KB memcpy。 + * 创建失败(罕见环境)由调用方回退 MainThreadForceLayout。 + */ +export class WorkerForceLayout implements LayoutEngine { + positions: Float32Array = new Float32Array(0); + + private worker: Worker | null = null; + private url = ''; + private dirty = false; + private settled = true; + private _ticks = 0; + + get ticks(): number { + return this._ticks; + } + + init(data: GraphData, positions: Float32Array, params: LayoutParams, initialAlpha = 1): void { + this.disposeWorker(); + this.positions = positions; + this._ticks = 0; + this.dirty = false; + + this.url = URL.createObjectURL(new Blob([workerSource], { type: 'text/javascript' })); + this.worker = new Worker(this.url); + this.worker.onmessage = (e: MessageEvent) => { + const m = e.data as TickMsg; + if (m.type !== 'tick') return; + const incoming = new Float32Array(m.buffer); + this.positions.set(incoming.subarray(0, this.positions.length)); + this._ticks = m.ticks; + this.settled = m.settled; + this.dirty = true; + // 归还 buffer(transferable 乒乓) + this.worker?.postMessage({ type: 'buffer', buffer: m.buffer }, [m.buffer]); + }; + + const n = data.nodes.length; + const posCopy = new Float32Array(positions); // worker 持有自己的副本 + const linkIdx = new Uint32Array(data.links.length * 2); + data.links.forEach((l, i) => { + linkIdx[i * 2] = l.source; + linkIdx[i * 2 + 1] = l.target; + }); + const degrees = new Float32Array(n); + data.nodes.forEach((node, i) => (degrees[i] = Math.max(node.degree, 1))); + const bufA = new ArrayBuffer(n * 3 * 4); + const bufB = new ArrayBuffer(n * 3 * 4); + + this.settled = initialAlpha < 0.001; + this.worker.postMessage( + { + type: 'init', + count: n, + positions: posCopy.buffer, + links: linkIdx.buffer, + degrees: degrees.buffer, + params, + initialAlpha, + bufA, + bufB, + }, + [posCopy.buffer, linkIdx.buffer, degrees.buffer, bufA, bufB], + ); + } + + /** 返回「本帧坐标有更新」——调用方据此刷新渲染缓冲 */ + step(): boolean { + const had = this.dirty; + this.dirty = false; + return had; + } + + isSettled(): boolean { + return this.settled; + } + + reheat(alpha = 0.3): void { + this.settled = false; + this.worker?.postMessage({ type: 'reheat', alpha }); + } + + updateParams(params: LayoutParams): void { + this.settled = false; + this.worker?.postMessage({ type: 'params', params }); + } + + private disposeWorker(): void { + this.worker?.terminate(); + this.worker = null; + if (this.url) { + URL.revokeObjectURL(this.url); + this.url = ''; + } + } + + dispose(): void { + this.disposeWorker(); + this.settled = true; + this.dirty = false; + } +} diff --git a/src/layout/forceWorker.ts b/src/layout/forceWorker.ts new file mode 100644 index 0000000..cef1f48 --- /dev/null +++ b/src/layout/forceWorker.ts @@ -0,0 +1,166 @@ +import { forceLink, forceManyBody, forceSimulation, forceX, forceY, forceZ } from 'd3-force-3d'; +import type { SimLink, SimNode, Simulation } from 'd3-force-3d'; +import type { LayoutParams } from '../types'; + +/** + * 布局 Worker(M3):d3-force-3d 完全离主线程。 + * 协议:init(坐标/边/度数 transferable 进)→ 批量 tick(每批 ≤12ms)→ + * 坐标经双缓冲乒乓 transferable 回传(零拷贝),消费完归还。 + */ + +interface WNode extends SimNode { + x: number; + y: number; + z: number; +} + +interface InitMsg { + type: 'init'; + count: number; + positions: ArrayBuffer; + links: ArrayBuffer; // Uint32Array [s0,t0,s1,t1,...] + degrees: ArrayBuffer; // Float32Array + params: LayoutParams; + initialAlpha: number; + bufA: ArrayBuffer; + bufB: ArrayBuffer; +} + +type InMsg = + | InitMsg + | { type: 'params'; params: LayoutParams } + | { type: 'reheat'; alpha: number } + | { type: 'buffer'; buffer: ArrayBuffer }; + +interface WorkerCtx { + onmessage: ((e: MessageEvent) => void) | null; + postMessage(msg: unknown, transfer?: Transferable[]): void; +} + +const ctx = self as unknown as WorkerCtx; + +let sim: Simulation | null = null; +let nodes: WNode[] = []; +let degrees: Float32Array = new Float32Array(0); +let freeBuffers: ArrayBuffer[] = []; +let settled = true; +let tickCount = 0; +let needPost = false; +let scheduled = false; + +function linkStrengthFn(mult: number): (link: SimLink) => number { + return (link) => { + const s = typeof link.source === 'number' ? link.source : (link.source.index ?? 0); + const t = typeof link.target === 'number' ? link.target : (link.target.index ?? 0); + const base = 1 / Math.min(degrees[s] ?? 1, degrees[t] ?? 1); + return Math.min(base * mult, 1); + }; +} + +function applyParams(params: LayoutParams): void { + if (!sim) return; + (sim.force('charge') as import('d3-force-3d').ManyBodyForce | undefined)?.strength(params.charge); + const link = sim.force('link') as import('d3-force-3d').LinkForce | undefined; + link?.distance(params.linkDistance); + link?.strength(linkStrengthFn(params.linkStrength)); + (sim.force('x') as import('d3-force-3d').PositionForce | undefined)?.strength(params.centerPull); + (sim.force('y') as import('d3-force-3d').PositionForce | undefined)?.strength(params.centerPull + params.flatten); + (sim.force('z') as import('d3-force-3d').PositionForce | undefined)?.strength(params.centerPull); +} + +function schedule(): void { + if (scheduled) return; + scheduled = true; + // Worker 作用域用 self(没有 window);宏任务 yield 让批次间能处理回传消息 + self.setTimeout(run, 0); +} + +function run(): void { + scheduled = false; + const s = sim; + if (!s || settled) return; + const t0 = Date.now(); + // 批量 tick:每批最多 12ms,主线程按消费节奏取最新帧 + while (Date.now() - t0 < 12) { + s.tick(); + tickCount++; + if (s.alpha() < s.alphaMin()) { + settled = true; + break; + } + } + needPost = true; + post(); + if (!settled) schedule(); +} + +function post(): void { + if (!needPost || !sim) return; + const buf = freeBuffers.pop(); + if (!buf) return; // 等 buffer 归还时再补发 + const arr = new Float32Array(buf); + for (let i = 0; i < nodes.length; i++) { + const n = nodes[i]; + if (!n) continue; + arr[i * 3] = n.x; + arr[i * 3 + 1] = n.y; + arr[i * 3 + 2] = n.z; + } + needPost = false; + ctx.postMessage({ type: 'tick', buffer: buf, alpha: sim.alpha(), settled, ticks: tickCount }, [buf]); +} + +ctx.onmessage = (e: MessageEvent) => { + const msg = e.data as InMsg; + switch (msg.type) { + case 'init': { + sim?.stop(); + const pos = new Float32Array(msg.positions); + degrees = new Float32Array(msg.degrees); + const linkIdx = new Uint32Array(msg.links); + nodes = []; + for (let i = 0; i < msg.count; i++) { + nodes.push({ x: pos[i * 3] ?? 0, y: pos[i * 3 + 1] ?? 0, z: pos[i * 3 + 2] ?? 0 }); + } + const links: SimLink[] = []; + for (let li = 0; li < linkIdx.length; li += 2) { + links.push({ source: linkIdx[li] ?? 0, target: linkIdx[li + 1] ?? 0 }); + } + sim = forceSimulation(nodes, 3) + .alphaDecay(1 - Math.pow(0.001, 1 / 300)) + .velocityDecay(msg.params.velocityDecay) + .force('link', forceLink(links).distance(msg.params.linkDistance).strength(linkStrengthFn(msg.params.linkStrength))) + .force('charge', forceManyBody().strength(msg.params.charge).distanceMax(800)) + .force('x', forceX(0).strength(msg.params.centerPull)) + .force('y', forceY(0).strength(msg.params.centerPull + msg.params.flatten)) + .force('z', forceZ(0).strength(msg.params.centerPull)) + .stop(); + sim.alpha(msg.initialAlpha); + freeBuffers = [msg.bufA, msg.bufB]; + tickCount = 0; + settled = msg.initialAlpha < sim.alphaMin(); + needPost = false; + if (!settled) schedule(); + break; + } + case 'params': + applyParams(msg.params); + if (sim) { + sim.alpha(Math.max(sim.alpha(), 0.5)); + settled = false; + schedule(); + } + break; + case 'reheat': + if (sim) { + sim.alpha(Math.max(sim.alpha(), msg.alpha)); + settled = false; + schedule(); + } + break; + case 'buffer': + freeBuffers.push(msg.buffer); + post(); // 沉降最后一帧可能在等 buffer + break; + } +}; diff --git a/src/layout/radial/layoutRadial.ts b/src/layout/radial/layoutRadial.ts new file mode 100644 index 0000000..be90b8b --- /dev/null +++ b/src/layout/radial/layoutRadial.ts @@ -0,0 +1,1316 @@ +import type { VisibleWorldGraph, WorldEdge, WorldNode } from '../../world/types'; +import { ROOT_ID } from '../../world/types'; + +export const DEFAULT_RING_SPACING = 960; +export const MIN_RING_SPACING = 720; +export const MAX_RING_SPACING = 2800; +export const DEFAULT_NODE_SPACING = 126; +export const MIN_NODE_SPACING = 72; +export const MAX_NODE_SPACING = 360; + +const RING_JAGGED_BAND_FACTOR = 0.36; +const RING_JAGGED_MAX_FACTOR = 0.28; + +export interface RadialPoint { + x: number; + y: number; + homeX: number; + homeY: number; + radius: number; + homeRadius: number; + angle: number; + homeAngle: number; + depth: number; + nodeRadius: number; + centerX: number; + centerY: number; + external: boolean; + ringRadius?: number; + ringBandMin?: number; + ringBandMax?: number; +} + +export interface RadialRing { + depth: number; + radius: number; + count: number; +} + +export interface RadialRoute { + kind: 'line' | 'curve' | 'outer'; + centerX: number; + centerY: number; + radius: number; + sourceAngle: number; + targetAngle: number; + endAngle?: number; + curveStrength?: number; +} + +export interface RadialLayout { + positions: Map; + rings: RadialRing[]; + routes: Map; + bounds: { minX: number; minY: number; maxX: number; maxY: number }; + width: number; + height: number; + centerX: number; + centerY: number; + ringSpacing: number; + nodeSpacing: number; +} + +export interface RadialLayoutOptions { + ringSpacing: number; + nodeSpacing: number; + swirlStrength: number; +} + +interface Metric { + weight: number; + count: number; + maxDepth: number; +} + +interface SpacingProfile { + baseRingGap: number; + baseNodeGap: number; + ringGap: number; + nodeGap: number; + branchFanSpan: number; + routeGapFactor: number; + radiusExpansion: number; + ringCountsByDepth: Map; + maxDensityDepth: number; + incidentPressureByNode: Map; +} + +interface RingItem { + id: string; + node: WorldNode; + point: RadialPoint; + depth: number; + parentId: string | null; + parentAngle: number; + visualRadius: number; + arcDemand: number; + preferred: number; +} + +export function layoutRadialGraph(graph: VisibleWorldGraph, options: RadialLayoutOptions): RadialLayout { + const baseRingGap = clamp(options.ringSpacing, MIN_RING_SPACING, MAX_RING_SPACING); + const baseNodeGap = clamp(options.nodeSpacing, MIN_NODE_SPACING, MAX_NODE_SPACING); + const positions = new Map(); + const nodesById = graph.nodesById; + const maxDegree = maxLinkDegree(graph.nodes); + const normalNodes = graph.nodes.filter((node) => !node.externalProxy && node.type !== 'external'); + const normalIds = new Set(normalNodes.map((node) => node.id)); + const spacing = adaptiveLayoutSpacing(graph, normalIds, baseRingGap, baseNodeGap); + const childrenByParent = childrenByParentMap(graph, normalIds, nodesById); + const metrics = measureMetrics(childrenByParent, nodesById, spacing); + const rootId = normalIds.has(graph.rootId) ? graph.rootId : normalIds.has(ROOT_ID) ? ROOT_ID : (normalNodes[0]?.id ?? null); + + if (rootId !== null) { + const rootPoint = makePoint(0, -Math.PI / 2, 0, nodeRadius(nodesById.get(rootId), maxDegree), false); + positions.set(rootId, rootPoint); + placeRadialChildren( + rootId, + 0, + -Math.PI / 2, + -Math.PI / 2 + Math.PI * 2, + -Math.PI / 2, + childrenByParent, + metrics, + positions, + nodesById, + spacing, + rootId, + maxDegree, + ); + } + + const reachable = new Set(positions.keys()); + const orphanNodes = normalNodes.filter((node) => node.id !== rootId && !reachable.has(node.id)).sort(compareLayoutNode); + const rootMetric = rootId !== null ? metrics.get(rootId) : null; + const maxTreeDepth = Math.max(0, rootMetric?.maxDepth ?? 0); + let outerRadius = Math.max(maxRadius(positions), spacing.ringGap); + if (orphanNodes.length > 0) { + outerRadius = Math.max( + outerRadius, + placeOuterCircleNodes(orphanNodes, positions, graph, nodesById, outerRadius + spacing.ringGap * 0.72, spacing.nodeGap, -Math.PI / 2, maxTreeDepth + 1, false, maxDegree), + ); + } + + const externalGroups = graph.nodes.filter((node) => node.type === 'external' && !node.externalProxy).sort(compareLayoutNode); + if (externalGroups.length > 0) { + outerRadius = Math.max( + outerRadius, + placeOuterCircleNodes(externalGroups, positions, graph, nodesById, outerRadius + spacing.ringGap * 0.62, spacing.nodeGap * 1.15, -Math.PI / 3, maxTreeDepth + 1, true, maxDegree), + ); + } + + const externalFiles = graph.nodes.filter((node) => node.externalProxy).sort(compareLayoutNode); + if (externalFiles.length > 0) { + placeOuterCircleNodes(externalFiles, positions, graph, nodesById, outerRadius + Math.max(220, spacing.ringGap * 0.52), spacing.nodeGap, -Math.PI / 5, maxTreeDepth + 2, true, maxDegree); + } + + const ringTargets = assignDepthRingTargets(positions, graph, spacing, spacing.nodeGap, maxDegree); + resolveRadialCollisions(positions, graph, spacing, spacing.nodeGap, ringTargets, maxDegree); + enforceDepthRingBands(positions, graph, spacing, spacing.nodeGap, ringTargets, maxDegree); + if (spacing.radiusExpansion > 1.001) applyAdaptiveRadiusExpansion(positions, ringTargets, spacing); + alignChildrenToParentLanes(positions, childrenByParent, graph, spacing); + if (options.swirlStrength > 0.001) applyRadialSwirl(positions, graph, spacing, options.swirlStrength / 100); + anchorHomePositions(positions); + const rings = computeRings(positions, ringTargets); + const routeMaxDepth = maxTreeDepth + (externalGroups.length || externalFiles.length ? 2 : orphanNodes.length ? 1 : 0); + const routes = routeLinks(graph.linkEdges, positions, routeMaxDepth, spacing.ringGap); + const bounds = computeBounds(positions, routes); + return { + positions, + rings, + routes, + bounds, + width: bounds.maxX - bounds.minX, + height: bounds.maxY - bounds.minY, + centerX: 0, + centerY: 0, + ringSpacing: spacing.ringGap, + nodeSpacing: spacing.nodeGap, + }; +} + +function childrenByParentMap( + graph: VisibleWorldGraph, + normalIds: Set, + nodesById: Map, +): Map { + const childrenByParent = new Map(); + for (const edge of graph.hierarchyEdges) { + if (!normalIds.has(edge.source) || !normalIds.has(edge.target)) continue; + const list = childrenByParent.get(edge.source); + if (list) list.push(edge.target); + else childrenByParent.set(edge.source, [edge.target]); + } + for (const children of childrenByParent.values()) children.sort((a, b) => compareLayoutNode(nodesById.get(a), nodesById.get(b))); + return childrenByParent; +} + +function measureMetrics( + childrenByParent: Map, + nodesById: Map, + spacing: SpacingProfile, +): Map { + const metrics = new Map(); + const measure = (id: string, depth: number, visiting = new Set()): Metric => { + const cached = metrics.get(id); + if (cached) return cached; + if (visiting.has(id)) return { weight: 1, count: 1, maxDepth: depth }; + visiting.add(id); + const incidentPressure = spacing.incidentPressureByNode.get(id) ?? 0; + let weight = Math.min(9, incidentPressure * 0.24); + let count = 1; + let maxDepth = depth; + for (const childId of childrenByParent.get(id) ?? []) { + const child = measure(childId, depth + 1, visiting); + weight += child.weight; + count += child.count; + maxDepth = Math.max(maxDepth, child.maxDepth); + } + visiting.delete(id); + const metric = { weight: Math.max(1, weight || 1), count, maxDepth }; + metrics.set(id, metric); + return metric; + }; + for (const id of childrenByParent.keys()) measure(id, 0); + return metrics; +} + +function placeRadialChildren( + parentId: string, + depth: number, + sectorStart: number, + sectorEnd: number, + parentAngle: number, + childrenByParent: Map, + metrics: Map, + positions: Map, + nodesById: Map, + spacing: SpacingProfile, + rootId: string, + maxDegree: number, +): void { + const children = childrenByParent.get(parentId) ?? []; + if (children.length === 0) return; + + let start = sectorStart; + let end = sectorEnd; + let span = Math.max(0.001, end - start); + const parentPoint = positions.get(parentId) ?? makePoint(0, parentAngle, depth, 5, false); + let childRadius = parentPoint.radius + localRadialGap(parentId, depth, children, metrics, spacing, rootId); + const nodeGap = localArcGap(parentId, children, metrics, spacing, rootId); + + if (children.length === 1 && parentId === rootId) { + const localSpan = Math.max(Math.PI * 0.36, spacing.branchFanSpan * 0.82); + start = parentAngle - localSpan / 2; + end = parentAngle + localSpan / 2; + span = localSpan; + } else if (parentId !== rootId) { + const demandSpan = children.length > 1 ? ((children.length - 1) * nodeGap) / childRadius + 0.08 : Math.max(Math.PI * 0.24, spacing.branchFanSpan * 0.62); + const cappedFan = Math.min(span, spacing.branchFanSpan); + const localSpan = Math.min(span, Math.max(cappedFan, demandSpan)); + start = parentAngle - localSpan / 2; + end = parentAngle + localSpan / 2; + span = localSpan; + } + + if (children.length > 1) { + const requiredRadius = ((children.length - 1) * nodeGap) / Math.max(0.16, span * 0.64); + const maxExtra = spacing.ringGap * (parentId === rootId ? 1.8 : 2.8); + childRadius = Math.max(childRadius, Math.min(parentPoint.radius + maxExtra, requiredRadius)); + } + + const totalWeight = Math.max(1, children.reduce((sum, childId) => sum + (metrics.get(childId)?.weight ?? 1), 0)); + const localGap = children.length > 1 ? Math.min(nodeGap / childRadius, (span * 0.38) / (children.length - 1)) : 0; + const usableSpan = Math.max(0.001, span - localGap * Math.max(0, children.length - 1)); + let cursor = start; + + for (const childId of children) { + const childSpan = children.length === 1 ? usableSpan : usableSpan * ((metrics.get(childId)?.weight ?? 1) / totalWeight); + const childStart = cursor; + const childEnd = cursor + childSpan; + const angle = childStart + childSpan / 2; + const node = nodesById.get(childId); + positions.set(childId, makePoint(childRadius, angle, depth + 1, nodeRadius(node, maxDegree), false)); + placeRadialChildren(childId, depth + 1, childStart, childEnd, angle, childrenByParent, metrics, positions, nodesById, spacing, rootId, maxDegree); + cursor = childEnd + localGap; + } +} + +function localRadialGap( + parentId: string, + depth: number, + children: string[], + metrics: Map, + spacing: SpacingProfile, + rootId: string, +): number { + const childCount = children.length; + const parentMetric = metrics.get(parentId) ?? { count: 1, weight: 1, maxDepth: depth }; + const incidentPressure = spacing.incidentPressureByNode.get(parentId) ?? 0; + const childRoot = Math.sqrt(Math.max(1, childCount)); + const subtreeSignal = Math.log2(Math.max(1, parentMetric.count || parentMetric.weight || 1)); + const pressureSignal = Math.sqrt(Math.max(0, incidentPressure)); + let factor = 0.62 + depth * 0.105 + childRoot * 0.135 + subtreeSignal * 0.094 + pressureSignal * 0.086; + if (parentId === rootId) factor *= 0.96; + if (childCount <= 4) factor = Math.min(factor, parentId === rootId ? 0.94 : 1.12); + if (childCount >= 14) factor = Math.max(factor, 1.24 + Math.min(1.1, childRoot * 0.09)); + if (childCount >= 40) factor = Math.max(factor, 1.56 + Math.min(1.34, childRoot * 0.084)); + return clamp(spacing.baseRingGap * factor, parentId === rootId ? 260 : 300, 4400); +} + +function localArcGap( + parentId: string, + children: string[], + metrics: Map, + spacing: SpacingProfile, + rootId: string, +): number { + const childCount = children.length; + const parentMetric = metrics.get(parentId) ?? { count: 1, weight: 1, maxDepth: 1 }; + const incidentPressure = spacing.incidentPressureByNode.get(parentId) ?? 0; + const densitySignal = + Math.sqrt(Math.max(1, childCount)) * 0.07 + + Math.log2(Math.max(1, parentMetric.count || 1)) * 0.052 + + Math.sqrt(Math.max(0, incidentPressure)) * 0.05; + let factor = 1.58 + densitySignal * 1.95; + if (parentId === rootId && childCount <= 6) factor *= 1.08; + if (childCount <= 4) factor = clamp(factor, 1.62, 2.08); + else if (childCount <= 10) factor = Math.max(factor, 1.86); + if (childCount >= 24) factor = Math.max(factor, 2.12); + if (childCount >= 64) factor = Math.max(factor, 2.58); + return clamp(spacing.baseNodeGap * factor, 132, 920); +} + +function placeOuterCircleNodes( + nodes: WorldNode[], + positions: Map, + graph: VisibleWorldGraph, + nodesById: Map, + radius: number, + nodeGap: number, + fallbackStart: number, + depth: number, + external: boolean, + maxDegree: number, +): number { + if (nodes.length === 0) return radius; + const slot = (Math.PI * 2) / nodes.length; + const crowding = nodes.length * (nodeGap / Math.max(1, radius)); + const items = nodes + .map((node, index) => ({ + node, + preferred: preferredAngleForNode(node, graph, positions, fallbackStart + index * slot), + })) + .sort((a, b) => normalizeAngle(a.preferred) - normalizeAngle(b.preferred)); + const offset = items[0] ? items[0].preferred - slot * 0.5 : fallbackStart; + for (let index = 0; index < items.length; index++) { + const item = items[index]; + if (!item) continue; + const evenAngle = offset + index * slot; + const preferredDelta = shortestAngleDelta(evenAngle, item.preferred); + const angle = crowding < Math.PI * 1.35 ? evenAngle + preferredDelta * 0.55 : evenAngle; + positions.set(item.node.id, makePoint(radius, angle, depth || item.node.depth || 1, nodeRadius(nodesById.get(item.node.id), maxDegree), external)); + } + return radius; +} + +function assignDepthRingTargets( + positions: Map, + graph: VisibleWorldGraph, + spacing: SpacingProfile, + nodeGap: number, + maxDegree: number, +): Map { + const ringTargets = new Map([[0, 0]]); + const byDepth = new Map(); + for (const [id, point] of positions.entries()) { + const depth = Math.max(0, Math.round(point.depth || 0)); + const node = graph.nodesById.get(id); + const visualRadius = nodeRadius(node, maxDegree); + const diameter = visualRadius * 2 + Math.max(18, nodeGap * 0.36); + const entry = byDepth.get(depth) ?? { count: 0, diameterTotal: 0, maxDiameter: 0, external: 0, linkPressure: 0 }; + entry.count++; + entry.diameterTotal += diameter; + entry.maxDiameter = Math.max(entry.maxDiameter, diameter); + entry.linkPressure += spacing.incidentPressureByNode.get(id) ?? 0; + if (point.external || node?.externalProxy || node?.type === 'external') entry.external++; + byDepth.set(depth, entry); + } + + let previousRadius = 0; + const depths = [...byDepth.keys()].filter((depth) => depth > 0).sort((a, b) => a - b); + const maxDepth = Math.max(...depths, 1); + const totalNodes = [...byDepth.values()].reduce((sum, entry) => sum + entry.count, 0); + const globalCompression = clamp(1 - Math.log10(Math.max(1, totalNodes)) * 0.085, 0.58, 0.9); + for (const depth of depths) { + const entry = byDepth.get(depth); + if (!entry) continue; + const avgDiameter = entry.diameterTotal / Math.max(1, entry.count); + const rawDemand = entry.count > 1 ? (entry.count * avgDiameter) / (Math.PI * 2) : 0; + const compressedDemand = rawDemand > 0 ? Math.pow(rawDemand, 0.64) * Math.pow(spacing.ringGap, 0.36) : 0; + const depthRatio = depth / Math.max(1, maxDepth); + const outerExpansion = 1 + Math.pow(depthRatio, 1.35) * 0.34; + const pressureExpansion = 1 + Math.min(0.28, Math.sqrt(entry.linkPressure / Math.max(1, entry.count)) * 0.036); + const depthCompression = clamp(1 - depthRatio * 0.1, 0.84, 0.98); + const baseRadius = depth * spacing.ringGap * globalCompression * depthCompression * outerExpansion * pressureExpansion; + const minSeparatedRadius = previousRadius + spacing.ringGap * (entry.external ? 0.54 : 0.62); + const crowdingRadius = compressedDemand * globalCompression * outerExpansion * pressureExpansion + entry.maxDiameter * 1.45; + const maxStepRadius = previousRadius + spacing.ringGap * (entry.external ? 1.45 : 1.72) * outerExpansion; + const radius = Math.min(maxStepRadius, Math.max(baseRadius, minSeparatedRadius, crowdingRadius)); + ringTargets.set(depth, radius); + previousRadius = radius; + } + for (const point of positions.values()) { + const depth = Math.max(0, Math.round(point.depth || 0)); + const targetRadius = ringTargets.get(depth); + if (!Number.isFinite(targetRadius)) continue; + const radius = targetRadius as number; + const angle = Number.isFinite(point.angle) ? point.angle : Math.atan2(point.y, point.x); + point.ringRadius = radius; + if (depth === 0) { + point.x = 0; + point.y = 0; + point.radius = 0; + point.angle = -Math.PI / 2; + continue; + } + point.x = Math.cos(angle) * radius; + point.y = Math.sin(angle) * radius; + point.radius = radius; + point.angle = normalizeAngle(angle); + } + return ringTargets; +} + +function resolveRadialCollisions( + positions: Map, + graph: VisibleWorldGraph, + spacing: SpacingProfile, + nodeGap: number, + ringTargets: Map, + maxDegree: number, +): void { + if (positions.size < 2) return; + const basePad = clamp(nodeGap * 1.12, 54, 280); + const items: { + id: string; + node: WorldNode; + point: RadialPoint; + visualRadius: number; + collisionRadius: number; + gravity: number; + fixed: boolean; + anchorAngle: number; + ringRadius: number | null; + }[] = []; + let maxCollisionRadius = 1; + + for (const [id, point] of positions.entries()) { + const node = graph.nodesById.get(id); + if (!node) continue; + const visualRadius = nodeRadius(node, maxDegree) * 1.72; + const spacingPad = Math.min(320, basePad + labelCollisionPadding(node)); + const collisionRadius = visualRadius + spacingPad; + const depth = Math.max(0, Math.round(point.depth || 0)); + const ringRadius = Number.isFinite(point.ringRadius) ? point.ringRadius! : ringTargets.get(depth); + maxCollisionRadius = Math.max(maxCollisionRadius, collisionRadius); + items.push({ + id, + node, + point, + visualRadius, + collisionRadius, + gravity: clamp(Math.sqrt(Math.max(1, visualRadius)) / 3.1, 0.85, 2.55), + fixed: id === graph.rootId, + anchorAngle: Number.isFinite(point.angle) ? point.angle : Math.atan2(point.y, point.x), + ringRadius: Number.isFinite(ringRadius) ? ringRadius! : null, + }); + } + if (items.length < 2) return; + + const iterations = items.length > 3500 ? 9 : items.length > 1200 ? 11 : 16; + const cellSize = Math.max(112, maxCollisionRadius * 2.48); + const separateItems = (strength: number, softRepel: number) => { + let moved = false; + const grid = new Map(); + for (let index = 0; index < items.length; index++) { + const item = items[index]!; + const gx = Math.floor(item.point.x / cellSize); + const gy = Math.floor(item.point.y / cellSize); + const key = `${gx},${gy}`; + const bucket = grid.get(key); + if (bucket) bucket.push(index); + else grid.set(key, [index]); + } + for (let index = 0; index < items.length; index++) { + const item = items[index]!; + const gx = Math.floor(item.point.x / cellSize); + const gy = Math.floor(item.point.y / cellSize); + for (let x = gx - 1; x <= gx + 1; x++) { + for (let y = gy - 1; y <= gy + 1; y++) { + const bucket = grid.get(`${x},${y}`); + if (!bucket) continue; + for (const otherIndex of bucket) { + if (otherIndex <= index) continue; + const other = items[otherIndex]!; + if (item.fixed && other.fixed) continue; + let dx = item.point.x - other.point.x; + let dy = item.point.y - other.point.y; + let distance = Math.hypot(dx, dy); + if (distance < 0.001) { + const angle = deterministicPairAngle(item.id, other.id); + dx = Math.cos(angle); + dy = Math.sin(angle); + distance = 1; + } + const minDistance = item.collisionRadius + other.collisionRadius; + const gravity = Math.sqrt(item.gravity * other.gravity); + const softDistance = minDistance + (item.visualRadius + other.visualRadius) * 1.75 * gravity; + if (distance >= softDistance) continue; + const overlapPush = distance < minDistance ? (minDistance - distance) * strength : 0; + const softPush = + distance >= minDistance + ? (softDistance - distance) * softRepel + : (softDistance - minDistance) * softRepel * 0.35; + const push = (overlapPush + softPush) * gravity + 0.01; + const nx = dx / distance; + const ny = dy / distance; + const itemShare = item.fixed ? 0 : other.fixed ? 1 : 0.5; + const otherShare = other.fixed ? 0 : item.fixed ? 1 : 0.5; + item.point.x += nx * push * itemShare; + item.point.y += ny * push * itemShare; + other.point.x -= nx * push * otherShare; + other.point.y -= ny * push * otherShare; + moved = true; + } + } + } + } + return moved; + }; + const pullItemsToRings = (strength: number) => { + for (const item of items) { + if (item.fixed || !Number.isFinite(item.ringRadius)) continue; + const currentRadius = Math.max(0.001, Math.hypot(item.point.x, item.point.y)); + const currentAngle = Math.atan2(item.point.y, item.point.x); + const external = item.node.externalProxy || item.node.type === 'external'; + const anglePull = external ? 0.016 : 0.024; + const ringTolerance = Math.max(item.visualRadius * (external ? 1.9 : 1.45), spacing.ringGap * (external ? 0.15 : 0.105)); + const nextAngle = currentAngle + shortestAngleDelta(currentAngle, item.anchorAngle) * anglePull; + const pulledRadius = currentRadius + (item.ringRadius! - currentRadius) * strength; + const nextRadius = clamp(pulledRadius, Math.max(0, item.ringRadius! - ringTolerance), item.ringRadius! + ringTolerance); + item.point.x = Math.cos(nextAngle) * nextRadius; + item.point.y = Math.sin(nextAngle) * nextRadius; + } + }; + + for (let pass = 0; pass < iterations; pass++) { + separateItems(pass === 0 ? 0.9 : 0.72, pass < 3 ? 0.22 : 0.13); + pullItemsToRings(pass < 3 ? 0.42 : 0.28); + } + for (let pass = 0; pass < 5; pass++) { + pullItemsToRings(pass === 0 ? 0.18 : 0.1); + if (!separateItems(pass === 0 ? 1 : 0.82, 0.05)) break; + } + for (const item of items) { + const radius = Math.hypot(item.point.x, item.point.y); + if (radius < 0.001) continue; + item.point.radius = radius; + item.point.angle = Math.atan2(item.point.y, item.point.x); + } +} + +function enforceDepthRingBands( + positions: Map, + graph: VisibleWorldGraph, + spacing: SpacingProfile, + nodeGap: number, + ringTargets: Map, + maxDegree: number, +): void { + const byDepth = new Map(); + for (const [id, point] of positions.entries()) { + const node = graph.nodesById.get(id); + if (!node) continue; + const depth = Math.max(0, Math.round(point.depth || 0)); + if (depth === 0) { + point.x = 0; + point.y = 0; + point.radius = 0; + point.angle = -Math.PI / 2; + continue; + } + const visualRadius = nodeRadius(node, maxDegree); + const labelDemand = labelCollisionPadding(node) + labelArcPadding(node) * clamp(0.58 + Math.min(1, depth / 4) * 0.42, 0.58, 1); + const arcDemand = visualRadius * 2.75 + labelDemand * 2.7 + Math.max(22, nodeGap * 0.22); + const currentAngle = normalizeAngle(Number.isFinite(point.angle) ? point.angle : Math.atan2(point.y, point.x)); + const parentPoint = node.parentId ? positions.get(node.parentId) : null; + const parentAngle = parentPoint && Number.isFinite(parentPoint.angle) ? normalizeAngle(parentPoint.angle) : currentAngle; + const preferred = parentPoint ? blendAngles(currentAngle, parentAngle, 0.68) : currentAngle; + const list = byDepth.get(depth) ?? []; + list.push({ id, node, point, depth, parentId: node.parentId ?? null, parentAngle, visualRadius, arcDemand, preferred }); + byDepth.set(depth, list); + } + + let previousOuterRadius = 0; + const depths = [...byDepth.keys()].sort((a, b) => a - b); + const maxDepth = Math.max(...depths, 1); + for (const depth of depths) { + const items = byDepth.get(depth); + if (!items?.length) continue; + const totalArcDemand = items.reduce((sum, item) => sum + item.arcDemand, 0); + const maxVisualRadius = Math.max(...items.map((item) => item.visualRadius), 4); + const baseTarget = ringTargets.get(depth) ?? Math.max(spacing.ringGap * depth, previousOuterRadius + spacing.ringGap * 0.62); + const depthRatio = clamp((depth - 1) / Math.max(1, maxDepth - 1), 0, 1); + const outerDensity = Math.pow(depthRatio, 1.35); + const laneUtilization = clamp(0.72 - outerDensity * 0.16 - Math.min(0.1, items.length / 4200), 0.5, 0.72); + const baseCapacity = Math.max(1, Math.PI * 2 * baseTarget * laneUtilization); + const laneCount = clamp(Math.ceil(totalArcDemand / baseCapacity), 1, outerRingLaneLimit(items.length, depthRatio)); + const laneGap = Math.max( + maxVisualRadius * (2.45 + outerDensity * 0.7) + Math.max(12, nodeGap * (0.13 + outerDensity * 0.04)), + spacing.ringGap * (0.11 + outerDensity * 0.045), + ); + const depthJaggedFactor = ringJaggedDepthFactor(depth, baseTarget, spacing.ringGap, items.length, totalArcDemand); + const firstLaneRadius = Math.max(baseTarget - laneGap * (laneCount - 1) * 0.5, previousOuterRadius + laneGap * 0.86); + const lanes = Array.from({ length: laneCount }, () => [] as RingItem[]); + for (const group of orderRingGroupsByParent(items)) { + const groupItems = orderRingItemsByPreferredGap(group.items); + if (laneCount === 1) lanes[0]?.push(...groupItems); + else lanes[chooseRingLaneForGroup(lanes, groupItems, group.parentAngle)]?.push(...groupItems); + } + + const laneRadii: number[] = []; + const laneOuterRadii: number[] = []; + for (let laneIndex = 0; laneIndex < lanes.length; laneIndex++) { + const laneItems = orderRingItemsByParentThenPreferred(lanes[laneIndex] ?? []); + if (!laneItems.length) continue; + let laneRadius = firstLaneRadius + laneIndex * laneGap; + const laneArcDemand = laneItems.reduce((sum, item) => sum + item.arcDemand, 0); + const requiredRadius = laneArcDemand / (Math.PI * 2 * laneUtilization); + laneRadius = Math.max(laneRadius, requiredRadius, previousOuterRadius + laneGap * 0.72); + const laneJaggedFactor = depthJaggedFactor * ringJaggedDensityFactor(laneItems.length, countRingParents(laneItems), laneArcDemand, laneRadius); + const candidateJitter = Math.min( + spacing.ringGap * RING_JAGGED_BAND_FACTOR * laneJaggedFactor, + laneRadius * RING_JAGGED_MAX_FACTOR * Math.min(1.35, laneJaggedFactor), + ); + laneRadius = Math.max(laneRadius, previousOuterRadius + candidateJitter * 0.56 + laneGap * 0.68); + const jitterBand = Math.min( + candidateJitter, + Math.max(0, laneRadius - previousOuterRadius - maxVisualRadius * 2.2 - Math.max(12, nodeGap * 0.08)), + ); + const laneOccupancy = laneArcDemand / Math.max(1, Math.PI * 2 * laneRadius); + placeItemsOnRingLane(laneItems, laneRadius, { + jitterBand, + preservePreferred: laneItems.length < 90 || laneOccupancy < 0.38 || outerDensity < 0.28, + }); + laneRadii.push(laneRadius); + laneOuterRadii.push(laneRadius + jitterBand); + } + if (laneRadii.length) { + ringTargets.set(depth, medianNumber(laneRadii, baseTarget)); + previousOuterRadius = Math.max(...laneOuterRadii) + maxVisualRadius * 1.55 + Math.max(12, nodeGap * 0.08); + } + } +} + +function adaptiveLayoutSpacing( + graph: VisibleWorldGraph, + normalIds: Set, + baseRingGap: number, + baseNodeGap: number, +): SpacingProfile { + const incidentPressureByNode = new Map(); + let totalPressure = 0; + let externalPressure = 0; + for (const edge of graph.linkEdges) { + const weightScore = Math.min(7, Math.log2((edge.weight || 1) + 1)); + const rawScore = Math.min(6, Math.log2((edge.rawCount || edge.weight || 1) + 1)); + const edgePressure = 0.75 + weightScore * 0.62 + rawScore * 0.28 + (edge.unresolvedCount ? 0.35 : 0) + (edge.externalCount ? 0.85 : 0); + totalPressure += edgePressure; + if (edge.externalCount) externalPressure += edgePressure; + for (const id of [edge.source, edge.target]) incidentPressureByNode.set(id, (incidentPressureByNode.get(id) ?? 0) + edgePressure); + } + let maxIncident = 0; + for (const pressure of incidentPressureByNode.values()) maxIncident = Math.max(maxIncident, pressure); + const visibleNodeCount = Math.max(1, graph.nodes.length || normalIds.size || 1); + const density = graphNodeDensityProfile(graph, normalIds); + const averagePressure = totalPressure / visibleNodeCount; + const overlayDensity = graph.linkEdges.length / visibleNodeCount; + const hubPressure = maxIncident / Math.max(1, Math.sqrt(visibleNodeCount) * 1.65); + const combinedPressure = averagePressure + Math.sqrt(Math.max(0, hubPressure)) * 0.58 + Math.min(1.6, overlayDensity) * 0.3 + (externalPressure / visibleNodeCount) * 0.32; + const pressureRoot = Math.sqrt(Math.max(0, combinedPressure)); + const nodeFactor = clamp(1.2 + pressureRoot * 0.92 + Math.min(0.86, averagePressure * 0.105), 1.2, 5.8); + const ringFactor = clamp(1.08 + pressureRoot * 0.34 + Math.min(0.34, averagePressure * 0.044), 1.08, 2.15); + const fanFactor = clamp(0.62 + pressureRoot * 0.24 + Math.min(0.3, overlayDensity * 0.17), 0.62, 1.35); + const routeGapFactor = clamp(0.9 + pressureRoot * 0.38 + Math.min(0.42, overlayDensity * 0.2), 0.9, 2.55); + const countExpansion = Math.max(0, Math.log2(density.normalCount / 520)) * 0.035; + const ringExpansion = Math.max(0, Math.sqrt(density.maxRingCount / 96) - 1) * 0.16; + const averageRingExpansion = Math.max(0, Math.sqrt(density.averageRingCount / 64) - 1) * 0.1; + const pressureExpansion = Math.max(0, pressureRoot - 0.75) * 0.028; + return { + baseRingGap, + baseNodeGap, + ringGap: clamp(baseRingGap * ringFactor, MIN_RING_SPACING, 4200), + nodeGap: clamp(baseNodeGap * nodeFactor, 86, 860), + branchFanSpan: Math.PI * fanFactor, + routeGapFactor, + radiusExpansion: clamp(1 + countExpansion + ringExpansion + averageRingExpansion + pressureExpansion, 1, 1.56), + ringCountsByDepth: density.countsByDepth, + maxDensityDepth: density.maxDepth, + incidentPressureByNode, + }; +} + +function graphNodeDensityProfile(graph: VisibleWorldGraph, normalIds: Set): { + normalCount: number; + maxRingCount: number; + averageRingCount: number; + maxDepth: number; + countsByDepth: Map; +} { + const byDepth = new Map(); + for (const node of graph.nodes) { + if (!normalIds.has(node.id)) continue; + const depth = Math.max(0, Math.round(node.depth || 0)); + if (depth <= 0) continue; + byDepth.set(depth, (byDepth.get(depth) ?? 0) + 1); + } + let maxRingCount = 0; + let totalRingCount = 0; + for (const count of byDepth.values()) { + maxRingCount = Math.max(maxRingCount, count); + totalRingCount += count; + } + return { + normalCount: Math.max(1, normalIds.size || 0), + maxRingCount, + averageRingCount: byDepth.size ? totalRingCount / byDepth.size : Math.max(1, normalIds.size || 0), + maxDepth: Math.max(...byDepth.keys(), 1), + countsByDepth: byDepth, + }; +} + +function applyAdaptiveRadiusExpansion( + positions: Map, + ringTargets: Map, + spacing: SpacingProfile, +): void { + const expansion = clamp(spacing.radiusExpansion, 1, 1.65); + if (expansion <= 1.001) return; + const maxDepth = Math.max(1, spacing.maxDensityDepth, ...ringTargets.keys()); + const scaleForDepth = (depth: number) => { + if (depth <= 0) return 1; + const depthRatio = clamp((depth - 1) / Math.max(1, maxDepth - 1), 0, 1); + const outerWeight = Math.pow(depthRatio, 1.42); + const ringCount = spacing.ringCountsByDepth.get(Math.round(depth)) ?? 0; + const crowdedBoost = Math.max(0, Math.sqrt(ringCount / 72) - 1) * 0.13 * outerWeight; + const innerCompression = (1 - outerWeight) * Math.min(0.08, (expansion - 1) * 0.42); + const adaptiveGrowth = (expansion - 1) * (0.04 + outerWeight * 1.06); + return clamp(1 - innerCompression + adaptiveGrowth + crowdedBoost, 0.93, 1.68); + }; + for (const [depth, radius] of [...ringTargets.entries()]) if (depth > 0) ringTargets.set(depth, radius * scaleForDepth(depth)); + for (const point of positions.values()) { + if (point.radius <= 0.001) continue; + const depth = Math.max(0, Math.round(point.depth || 0)); + const depthScale = scaleForDepth(depth); + const angle = Number.isFinite(point.angle) ? point.angle : Math.atan2(point.y, point.x); + const radius = point.radius * depthScale; + point.radius = radius; + point.x = Math.cos(angle) * radius; + point.y = Math.sin(angle) * radius; + point.angle = angle; + if (Number.isFinite(point.ringRadius)) point.ringRadius = point.ringRadius! * depthScale; + if (Number.isFinite(point.ringBandMin)) point.ringBandMin = point.ringBandMin! * depthScale; + if (Number.isFinite(point.ringBandMax)) point.ringBandMax = point.ringBandMax! * depthScale; + } +} + +function alignChildrenToParentLanes( + positions: Map, + childrenByParent: Map, + graph: VisibleWorldGraph, + spacing: SpacingProfile, +): void { + for (const [parentId, childIds] of childrenByParent.entries()) { + const parent = positions.get(parentId); + if (!parent || parent.radius <= 0.001 || childIds.length === 0) continue; + const parentAngle = Number.isFinite(parent.angle) ? parent.angle : Math.atan2(parent.y, parent.x); + const children = childIds + .map((id) => { + const point = positions.get(id); + const node = graph.nodesById.get(id); + return point && node && !point.external ? { id, point, node, delta: shortestAngleDelta(parentAngle, point.angle) } : null; + }) + .filter((item): item is { id: string; point: RadialPoint; node: WorldNode; delta: number } => Boolean(item)) + .sort((a, b) => a.delta - b.delta); + if (children.length === 0) continue; + + const averageRadius = children.reduce((sum, child) => sum + Math.max(1, child.point.radius), 0) / children.length; + const requiredArc = children.reduce((sum, child) => sum + child.point.nodeRadius * 2.25 + labelArcPadding(child.node) * 0.76, 0); + const requiredSpan = Math.min(Math.PI * 1.1, requiredArc / Math.max(1, averageRadius)); + const currentSpan = children.length > 1 ? Math.max(...children.map((child) => child.delta)) - Math.min(...children.map((child) => child.delta)) : 0; + const compactSpan = clamp(Math.max(requiredSpan, currentSpan * 0.58), children.length === 1 ? 0 : 0.035, Math.PI * 0.86); + const maxShift = Math.min(Math.PI * 0.18, Math.max(0.03, spacing.branchFanSpan * 0.08)); + const strength = children.length <= 2 ? 0.54 : children.length <= 8 ? 0.46 : 0.34; + + for (let index = 0; index < children.length; index++) { + const child = children[index]!; + const slot = + children.length === 1 + ? 0 + : -compactSpan / 2 + (compactSpan * index) / Math.max(1, children.length - 1); + const targetAngle = normalizeAngle(parentAngle + slot); + const currentAngle = normalizeAngle(child.point.angle); + const shift = clamp(shortestAngleDelta(currentAngle, targetAngle) * strength, -maxShift, maxShift); + const angle = normalizeAngle(currentAngle + shift); + const radius = child.point.radius; + child.point.x = Math.cos(angle) * radius; + child.point.y = Math.sin(angle) * radius; + child.point.angle = angle; + } + } +} + +function routeLinks( + edges: WorldEdge[], + positions: Map, + maxDepth: number, + ringSpacing: number, +): Map { + const routes = new Map(); + for (const edge of edges) { + const source = positions.get(edge.source); + const target = positions.get(edge.target); + if (!source || !target) continue; + const depthDelta = Math.abs(source.depth - target.depth); + const sourceRadius = Number.isFinite(source.radius) ? source.radius : 0; + const targetRadius = Number.isFinite(target.radius) ? target.radius : 0; + const sourceAngle = Number.isFinite(source.angle) ? source.angle : Math.atan2(target.y - source.y, target.x - source.x); + const targetAngle = Number.isFinite(target.angle) ? target.angle : sourceAngle; + const angleDistance = Math.abs(shortestAngleDelta(sourceAngle, targetAngle)); + const sameOrNearRing = Math.abs(sourceRadius - targetRadius) < ringSpacing * 0.44; + const touchesOuterRing = Math.max(source.depth || 0, target.depth || 0) >= Math.max(1, maxDepth - 1); + const isExternal = Boolean(source.external || target.external || edge.externalCount); + const shouldCurve = + isExternal || + sameOrNearRing || + (depthDelta <= 1 && touchesOuterRing && angleDistance > 0.34) || + angleDistance > Math.PI * 0.42; + if (shouldCurve) { + routes.set(edge.id, { + kind: 'curve', + centerX: 0, + centerY: 0, + radius: Math.max(sourceRadius, targetRadius), + sourceAngle, + targetAngle, + curveStrength: isExternal ? 0.3 : sameOrNearRing ? 0.22 : angleDistance > Math.PI * 0.72 ? 0.2 : 0.15, + }); + } + } + return routes; +} + +function computeRings(positions: Map, ringTargets: Map): RadialRing[] { + if (ringTargets.size > 1) { + const ringsByKey = new Map(); + for (const point of positions.values()) { + const depth = Math.max(0, Math.round(point.depth || 0)); + if (depth <= 0) continue; + const radius = Number.isFinite(point.ringRadius) ? point.ringRadius! : ringTargets.get(depth); + if (!Number.isFinite(radius) || radius! <= 0) continue; + const key = `${depth}:${Math.round(radius!)}`; + const existing = ringsByKey.get(key); + if (existing) { + existing.count++; + existing.radiusTotal += radius!; + } else { + ringsByKey.set(key, { depth, radiusTotal: radius!, count: 1 }); + } + } + return [...ringsByKey.values()] + .map((ring) => ({ depth: ring.depth, radius: ring.radiusTotal / Math.max(1, ring.count), count: ring.count })) + .sort((a, b) => a.radius - b.radius); + } + const byDepth = new Map(); + for (const point of positions.values()) { + if (point.depth <= 0) continue; + const radius = point.ringRadius ?? point.radius; + const entry = byDepth.get(point.depth) ?? { radius: 0, count: 0 }; + entry.radius += radius; + entry.count++; + byDepth.set(point.depth, entry); + } + return [...byDepth.entries()] + .map(([depth, entry]) => ({ depth, radius: entry.radius / Math.max(1, entry.count), count: entry.count })) + .sort((a, b) => a.radius - b.radius); +} + +function computeBounds(positions: Map, routes: Map): RadialLayout['bounds'] { + let minX = -500; + let minY = -500; + let maxX = 500; + let maxY = 500; + for (const point of positions.values()) { + const pad = point.nodeRadius + 180; + minX = Math.min(minX, point.x - pad); + minY = Math.min(minY, point.y - pad); + maxX = Math.max(maxX, point.x + pad); + maxY = Math.max(maxY, point.y + pad); + } + for (const route of routes.values()) { + if (route.kind !== 'outer') continue; + minX = Math.min(minX, -route.radius - 160); + minY = Math.min(minY, -route.radius - 160); + maxX = Math.max(maxX, route.radius + 160); + maxY = Math.max(maxY, route.radius + 160); + } + return { minX, minY, maxX, maxY }; +} + +function makePoint(radius: number, angle: number, depth: number, nodeRadiusValue: number, external: boolean): RadialPoint { + const x = Math.cos(angle) * radius; + const y = Math.sin(angle) * radius; + return { + x, + y, + homeX: x, + homeY: y, + radius, + homeRadius: radius, + angle, + homeAngle: angle, + depth, + nodeRadius: nodeRadiusValue, + centerX: 0, + centerY: 0, + external, + }; +} + +function nodeRadius(node: WorldNode | undefined, maxDegree: number): number { + if (!node) return 3.6; + const degree = Math.max(0, (node.linkCount || 0) + (node.backlinkCount || 0)); + const degreeRatio = Math.log1p(degree) / Math.max(1, Math.log1p(maxDegree || 1)); + const clampedRatio = clamp(degreeRatio, 0, 1); + const degreeCurve = Math.pow(clampedRatio, 0.48); + const hubCurve = Math.pow(clampedRatio, 1.32); + const degreeBoost = degreeCurve * 19 + hubCurve * 20 + Math.log2(degree + 1) * 1.35 + Math.sqrt(degree) * 0.32; + if (node.externalProxy) return node.type === 'unresolved' ? 5.4 : Math.min(27, 5.8 + degreeBoost * 0.62); + if (node.type === 'folder') { + const noteSignal = Math.log2((node.noteCount || node.descendantCount || 1) + 1); + return Math.min(66, 7 + noteSignal * 1.05 + degreeBoost * 1.18); + } + if (node.type === 'external') { + const noteSignal = Math.log2((node.noteCount || 1) + 1); + return Math.min(38, 5.8 + noteSignal * 0.72 + degreeBoost * 0.9); + } + if (node.type === 'unresolved') return Math.min(16, 4.2 + degreeBoost * 0.5); + return Math.min(58, 3.6 + degreeBoost * 1.05); +} + +function maxLinkDegree(nodes: WorldNode[]): number { + let max = 1; + for (const node of nodes) max = Math.max(max, (node.linkCount || 0) + (node.backlinkCount || 0)); + return max; +} + +function compareLayoutNode(a: WorldNode | undefined, b: WorldNode | undefined): number { + if (!a || !b) return a ? -1 : b ? 1 : 0; + const typeRank = (node: WorldNode) => (node.type === 'folder' ? 0 : node.type === 'note' ? 1 : node.type === 'external' ? 2 : 3); + return typeRank(a) - typeRank(b) || a.title.localeCompare(b.title) || a.id.localeCompare(b.id); +} + +function maxRadius(positions: Map): number { + let max = 0; + for (const point of positions.values()) max = Math.max(max, point.radius); + return max; +} + +function preferredAngleForNode( + node: WorldNode, + graph: VisibleWorldGraph, + positions: Map, + fallbackAngle: number, +): number { + const angles: number[] = []; + for (const edge of graph.linkEdges) { + const otherId = edge.source === node.id ? edge.target : edge.target === node.id ? edge.source : null; + if (!otherId) continue; + const other = positions.get(otherId); + if (other && Number.isFinite(other.angle)) angles.push(other.angle); + } + return averageAngles(angles, fallbackAngle); +} + +function averageAngles(angles: number[], fallbackAngle: number): number { + if (angles.length === 0) return fallbackAngle; + const sum = angles.reduce( + (acc, angle) => { + acc.x += Math.cos(angle); + acc.y += Math.sin(angle); + return acc; + }, + { x: 0, y: 0 }, + ); + if (Math.abs(sum.x) < 0.0001 && Math.abs(sum.y) < 0.0001) return fallbackAngle; + return Math.atan2(sum.y, sum.x); +} + +function orderRingItemsByPreferredGap(items: RingItem[]): RingItem[] { + const sorted = items.slice().sort((a, b) => normalizeAngle(a.preferred) - normalizeAngle(b.preferred)); + if (sorted.length <= 2) return sorted; + let largestGap = -1; + let largestGapIndex = 0; + for (let index = 0; index < sorted.length; index++) { + const current = normalizeAngle(sorted[index]?.preferred ?? 0); + const next = normalizeAngle(sorted[(index + 1) % sorted.length]?.preferred ?? 0) + (index === sorted.length - 1 ? Math.PI * 2 : 0); + const gap = next - current; + if (gap > largestGap) { + largestGap = gap; + largestGapIndex = index; + } + } + const start = (largestGapIndex + 1) % sorted.length; + return sorted.slice(start).concat(sorted.slice(0, start)); +} + +function outerRingLaneLimit(itemCount: number, depthRatio: number): number { + const count = Math.max(0, itemCount || 0); + const outer = clamp(depthRatio, 0, 1); + const base = + count > 1600 + ? 14 + : count > 900 + ? 12 + : count > 420 + ? 10 + : count > 180 + ? 8 + : count > 80 + ? 6 + : 4; + const outerBonus = outer > 0.72 ? 3 : outer > 0.48 ? 2 : outer > 0.28 ? 1 : 0; + return clamp(base + outerBonus, 4, 16); +} + +function orderRingGroupsByParent(items: RingItem[]): { parentId: string; parentAngle: number; arcDemand: number; items: RingItem[] }[] { + const groupsByParent = new Map(); + for (const item of items) { + const key = item.parentId ?? item.id; + const group = groupsByParent.get(key) ?? { parentId: key, parentAngle: item.parentAngle, arcDemand: 0, items: [] }; + group.items.push(item); + group.arcDemand += item.arcDemand || 0; + group.parentAngle = averageAngles(group.items.map((child) => child.parentAngle), group.parentAngle); + groupsByParent.set(key, group); + } + return [...groupsByParent.values()].sort((a, b) => normalizeAngle(a.parentAngle) - normalizeAngle(b.parentAngle)); +} + +function chooseRingLaneForGroup(lanes: RingItem[][], groupItems: RingItem[], parentAngle: number): number { + let bestIndex = 0; + let bestScore = Infinity; + const groupArc = groupItems.reduce((sum, item) => sum + (item.arcDemand || 0), 0); + for (let index = 0; index < lanes.length; index++) { + const lane = lanes[index] ?? []; + const laneArc = lane.reduce((sum, item) => sum + (item.arcDemand || 0), 0); + const last = lane[lane.length - 1]; + const angleCost = last ? Math.abs(shortestAngleDelta(last.parentAngle || last.preferred, parentAngle)) : 0; + const score = laneArc + groupArc * 0.18 + angleCost * 180; + if (score < bestScore) { + bestScore = score; + bestIndex = index; + } + } + return bestIndex; +} + +function orderRingItemsByParentThenPreferred(items: RingItem[]): RingItem[] { + const ordered: RingItem[] = []; + for (const group of orderRingGroupsByParent(items)) ordered.push(...orderRingItemsByPreferredGap(group.items)); + return ordered; +} + +function placeItemsOnRingLane(items: RingItem[], radius: number, options: { jitterBand: number; preservePreferred: boolean }): void { + if (!items.length || !Number.isFinite(radius) || radius <= 0) return; + const fullCircle = Math.PI * 2; + const arcs = items.map((item) => Math.max(0.003, item.arcDemand / radius)); + const totalArc = arcs.reduce((sum, arc) => sum + arc, 0); + const jitterBand = clamp(options.jitterBand, 0, Math.max(0, radius * RING_JAGGED_MAX_FACTOR)); + const parentOffsets = parentRadialOffsetsForLane(items, jitterBand); + const minGap = Math.min(0.11, Math.max(0.012, (totalArc / Math.max(1, items.length)) * 0.18)); + const minDemand = totalArc + minGap * Math.max(0, items.length - 1); + const canPreserve = options.preservePreferred && items.length <= 640 && minDemand < fullCircle * 0.9; + if (canPreserve && placeItemsNearPreferredAngles(items, arcs, radius, jitterBand, parentOffsets, minGap)) return; + const extraGap = Math.max(0, (fullCircle - totalArc) / items.length); + let cursor = normalizeAngle(items[0]?.preferred ?? 0) - ((arcs[0] ?? 0) + extraGap) * 0.5; + for (let index = 0; index < items.length; index++) { + const width = (arcs[index] ?? 0) + extraGap; + const angle = cursor + width * 0.5; + const item = items[index]; + if (item) setRingLanePoint(item, radius, angle, jitterBand, parentOffsets); + cursor += width; + } +} + +function placeItemsNearPreferredAngles( + items: RingItem[], + arcs: number[], + radius: number, + jitterBand: number, + parentOffsets: Map, + minGap: number, +): boolean { + if (!items.length) return true; + const fullCircle = Math.PI * 2; + let entries = items + .map((item, index) => ({ item, arc: arcs[index] ?? 0.003, preferred: normalizeAngle(item.preferred) })) + .sort((a, b) => a.preferred - b.preferred); + if (entries.length > 1) { + let largestGap = -1; + let largestGapIndex = 0; + for (let index = 0; index < entries.length; index++) { + const current = entries[index]?.preferred ?? 0; + const next = (entries[(index + 1) % entries.length]?.preferred ?? 0) + (index === entries.length - 1 ? fullCircle : 0); + const gap = next - current; + if (gap > largestGap) { + largestGap = gap; + largestGapIndex = index; + } + } + const start = (largestGapIndex + 1) % entries.length; + entries = entries.slice(start).concat(entries.slice(0, start)); + } + const angles: number[] = []; + let wrapOffset = 0; + let previous = entries[0]?.preferred ?? 0; + angles[0] = previous; + for (let index = 1; index < entries.length; index++) { + let angle = (entries[index]?.preferred ?? 0) + wrapOffset; + while (angle <= previous) { + wrapOffset += fullCircle; + angle = (entries[index]?.preferred ?? 0) + wrapOffset; + } + angles[index] = angle; + previous = angle; + } + const preferredCenter = ((angles[0] ?? 0) + (angles[angles.length - 1] ?? 0)) * 0.5; + for (let pass = 0; pass < 3; pass++) { + for (let index = 1; index < entries.length; index++) { + const minDelta = ((entries[index - 1]?.arc ?? 0) + (entries[index]?.arc ?? 0)) * 0.5 + minGap; + if ((angles[index] ?? 0) - (angles[index - 1] ?? 0) < minDelta) angles[index] = (angles[index - 1] ?? 0) + minDelta; + } + } + const span = + (angles[angles.length - 1] ?? 0) + + (entries[entries.length - 1]?.arc ?? 0) * 0.5 - + ((angles[0] ?? 0) - (entries[0]?.arc ?? 0) * 0.5); + if (span > fullCircle - minGap) return false; + const currentCenter = ((angles[0] ?? 0) + (angles[angles.length - 1] ?? 0)) * 0.5; + const centerShift = preferredCenter - currentCenter; + for (let index = 0; index < entries.length; index++) { + setRingLanePoint(entries[index]!.item, radius, normalizeAngle((angles[index] ?? 0) + centerShift), jitterBand, parentOffsets); + } + return true; +} + +function setRingLanePoint(item: RingItem, radius: number, angle: number, jitterBand: number, parentOffsets: Map): void { + const actualRadius = jaggedRingRadius(item, radius, jitterBand, parentOffsets); + const point = item.point; + point.x = Math.cos(angle) * actualRadius; + point.y = Math.sin(angle) * actualRadius; + point.radius = actualRadius; + point.ringRadius = radius; + point.ringBandMin = radius - jitterBand; + point.ringBandMax = radius + jitterBand; + point.angle = angle; +} + +function parentRadialOffsetsForLane(items: RingItem[], jitterBand: number): Map { + const offsets = new Map(); + if (!items.length || jitterBand <= 0) return offsets; + const parentOrder: string[] = []; + const seen = new Set(); + for (const item of items) { + const key = ringParentKey(item); + if (seen.has(key)) continue; + seen.add(key); + parentOrder.push(key); + } + const lanePattern = [0, -0.96, 0.96, -0.54, 0.54, -0.78, 0.78, -0.28, 0.28]; + for (let index = 0; index < parentOrder.length; index++) { + const key = parentOrder[index] ?? ''; + const base = lanePattern[index % lanePattern.length] ?? 0; + const variation = deterministicUnitOffset(key, 'ring-parent-variation') * 0.12; + offsets.set(key, clamp(base + variation, -1, 1) * jitterBand); + } + return offsets; +} + +function ringJaggedDepthFactor(depth: number, radius: number, ringGap: number, itemCount: number, arcDemand: number): number { + const normalizedDepth = Math.max(0, depth || 0); + const normalizedRadius = Number.isFinite(radius) && ringGap > 0 ? radius / ringGap : normalizedDepth; + const occupancy = Number.isFinite(radius) && radius > 0 ? arcDemand / Math.max(1, Math.PI * 2 * radius) : 0; + const outerFactor = clamp(0.82 + normalizedDepth * 0.06 + Math.sqrt(Math.max(0, normalizedRadius)) * 0.13, 0.86, 1.62); + const densityFactor = itemCount <= 5 ? 0.48 : itemCount <= 10 ? 0.68 : occupancy < 0.12 ? 0.62 : occupancy < 0.24 ? 0.82 : occupancy > 0.52 ? 1.16 : 1; + return clamp(outerFactor * densityFactor, 0.42, 1.72); +} + +function ringJaggedDensityFactor(itemCount: number, parentCount: number, arcDemand: number, radius: number): number { + const count = Math.max(0, itemCount || 0); + const parents = Math.max(1, parentCount || 1); + const occupancy = Number.isFinite(radius) && radius > 0 ? arcDemand / Math.max(1, Math.PI * 2 * radius) : 0; + const childFactor = count <= 3 ? 0.42 : count <= 7 ? 0.66 : count <= 14 ? 0.86 : 1.05; + const parentFactor = parents <= 1 ? 0.52 : parents <= 2 ? 0.72 : parents <= 4 ? 0.92 : 1.08; + const occupancyFactor = occupancy < 0.1 ? 0.56 : occupancy < 0.2 ? 0.78 : occupancy > 0.55 ? 1.14 : 1; + return clamp(childFactor * parentFactor * occupancyFactor, 0.32, 1.22); +} + +function countRingParents(items: RingItem[]): number { + return new Set(items.map(ringParentKey)).size; +} + +function jaggedRingRadius(item: RingItem, radius: number, jitterBand: number, parentOffsets: Map): number { + if (jitterBand <= 0) return radius; + const parentKey = ringParentKey(item); + const parentOffset = parentOffsets.get(parentKey) ?? deterministicUnitOffset(parentKey, 'ring-parent') * jitterBand * 0.92; + const childOffset = deterministicUnitOffset(item.id, 'ring-node') * jitterBand * 0.08; + return radius + clamp(parentOffset + childOffset, -jitterBand, jitterBand); +} + +function ringParentKey(item: RingItem): string { + return item.parentId ?? item.id; +} + +function labelCollisionPadding(node: WorldNode): number { + const titleLength = Array.from(String(node.title || '')).length; + const typePad = node.type === 'folder' ? 13 : node.type === 'external' || node.externalProxy ? 9 : 5; + return clamp(Math.sqrt(Math.max(1, titleLength)) * 4.2 + typePad, 10, 60); +} + +function labelArcPadding(node: WorldNode): number { + const titleLength = Array.from(String(node.title || '')).length; + const folderPad = node.type === 'folder' ? 14 : 0; + const externalPad = node.type === 'external' || node.externalProxy ? 9 : 0; + return clamp(Math.sqrt(Math.max(1, titleLength)) * 7.5 + Math.min(110, titleLength * 1.25) + folderPad + externalPad, 24, 150); +} + +function applyRadialSwirl(positions: Map, graph: VisibleWorldGraph, spacing: SpacingProfile, amount: number): void { + const rootId = graph.rootId || ROOT_ID; + const direction = deterministicUnitOffset(rootId || 'vault', 'swirl-direction') >= 0 ? 1 : -1; + for (const [id, point] of positions.entries()) { + if (point.radius <= 0.001) continue; + const depth = Math.max(0, point.depth || 0); + const baseAngle = Number.isFinite(point.angle) ? point.angle : Math.atan2(point.y, point.x); + const radialPhase = Math.sqrt(Math.max(0, point.radius) / Math.max(1, spacing.ringGap)); + const armVariation = deterministicUnitOffset(id, 'swirl-arm') * 0.16; + const wave = Math.sin(baseAngle * 2.35 + depth * 0.78) * 0.11; + const turn = direction * amount * (depth * 0.32 + radialPhase * 0.22 + armVariation + wave); + const angle = normalizeAngle(baseAngle + turn); + point.x = Math.cos(angle) * point.radius; + point.y = Math.sin(angle) * point.radius; + point.angle = angle; + } +} + +function anchorHomePositions(positions: Map): void { + for (const point of positions.values()) { + point.homeX = point.x; + point.homeY = point.y; + point.homeRadius = point.radius; + point.homeAngle = point.angle; + } +} + +function blendAngles(from: number, to: number, amount: number): number { + return normalizeAngle(from + shortestAngleDelta(from, to) * clamp(amount, 0, 1)); +} + +function normalizeAngle(angle: number): number { + let next = Number.isFinite(angle) ? angle : 0; + next %= Math.PI * 2; + if (next < 0) next += Math.PI * 2; + return next; +} + +function shortestAngleDelta(from: number, to: number): number { + let delta = normalizeAngle(to) - normalizeAngle(from); + if (delta > Math.PI) delta -= Math.PI * 2; + if (delta < -Math.PI) delta += Math.PI * 2; + return delta; +} + +function deterministicPairAngle(a: string, b: string): number { + const text = `${a}|${b}`; + let hash = 2166136261; + for (let index = 0; index < text.length; index++) { + hash ^= text.charCodeAt(index); + hash = Math.imul(hash, 16777619); + } + return ((hash >>> 0) / 4294967296) * Math.PI * 2; +} + +function deterministicUnitOffset(value: string, salt: string): number { + return Math.sin(deterministicPairAngle(String(value || ''), String(salt || ''))); +} + +function medianNumber(values: number[], fallback: number): number { + const numbers = values.filter(Number.isFinite).sort((a, b) => a - b); + if (!numbers.length) return fallback; + const middle = Math.floor(numbers.length / 2); + return numbers.length % 2 ? (numbers[middle] ?? fallback) : ((numbers[middle - 1] ?? fallback) + (numbers[middle] ?? fallback)) / 2; +} + +function clamp(value: number, min: number, max: number): number { + return Math.min(Math.max(Number.isFinite(value) ? value : min, min), max); +} diff --git a/src/main.ts b/src/main.ts new file mode 100644 index 0000000..614a42e --- /dev/null +++ b/src/main.ts @@ -0,0 +1,281 @@ +import { Notice, Plugin, PluginSettingTab, Setting } from 'obsidian'; +import { VIEW_TYPE_MINI_WORLD_MAP } from './constants'; +import type { Language, MiniWorldMapSettings, ViewMode } from './settings'; +import { + DEFAULT_RADIAL_SETTINGS, + HOVER_HIGHLIGHT_MODE_OPTIONS, + HOVER_TARGET_MODE_OPTIONS, + LABEL_VISIBILITY_OPTIONS, + MAX_ATLAS_DEPTH, + MAX_LINK_LIMIT, + MAX_RENDER_NODE_LIMIT, + MAX_SWIRL_STRENGTH, + clampNumber, + mergeSettings, + normalizeHoverHighlightMode, + normalizeHoverTargetMode, + normalizeLabelVisibility, + normalizeLanguage, + normalizeViewMode, +} from './settings'; +import { hoverModeOptions, hoverTargetOptions, labelVisibilityOptions, languageOptions, t, viewModeLabel } from './i18n'; +import { MiniWorldMapView } from './view/GalaxyView'; +import { Map3DController } from './view/Map3DController'; + +export default class MiniWorldMapPlugin extends Plugin { + settings: MiniWorldMapSettings = mergeSettings(null); + + async onload(): Promise { + this.settings = mergeSettings(await this.loadData()); + this.registerView(VIEW_TYPE_MINI_WORLD_MAP, (leaf) => new MiniWorldMapView(leaf, this)); + + this.addRibbonIcon('network', 'Open Mini World Map', () => { + void this.activateView(); + }); + + this.addCommand({ + id: 'open-map', + name: 'Open Mini World Map', + callback: () => void this.activateView(), + }); + + this.addCommand({ + id: 'toggle-render-mode', + name: 'Toggle Mini World Map render mode', + callback: () => { + this.setViewMode(this.settings.viewMode === 'radial2d' ? 'map3d' : 'radial2d'); + void this.activateView(); + }, + }); + + this.addCommand({ + id: 'rebuild-index', + name: 'Rebuild Mini World Map index', + callback: () => { + let rebuilt = false; + for (const leaf of this.app.workspace.getLeavesOfType(VIEW_TYPE_MINI_WORLD_MAP)) { + const view = leaf.view; + if (view instanceof MiniWorldMapView && 'rebuild' in (view.controller ?? {})) { + (view.controller as { rebuild?: (reason: string) => void }).rebuild?.('manual'); + rebuilt = true; + } + } + new Notice(t(this.settings.language, rebuilt ? 'notice.rebuilt' : 'notice.openToBuild')); + }, + }); + + this.addCommand({ + id: 'search-3d', + name: 'Search 3D map node and fly', + callback: () => { + void this.activateView().then((view) => { + if (view?.controller instanceof Map3DController) view.controller.openSearch(); + else new Notice(t(this.settings.language, 'notice.switchTo3d')); + }); + }, + }); + + this.addSettingTab(new MiniWorldMapSettingTab(this.app, this)); + } + + async saveSettings(): Promise { + await this.saveData(this.settings); + } + + setViewMode(mode: ViewMode): void { + this.settings.viewMode = normalizeViewMode(mode); + void this.saveSettings(); + for (const leaf of this.app.workspace.getLeavesOfType(VIEW_TYPE_MINI_WORLD_MAP)) { + const view = leaf.view; + if (view instanceof MiniWorldMapView) view.switchMode(this.settings.viewMode); + } + } + + setLanguage(language: Language): void { + this.settings.language = normalizeLanguage(language); + void this.saveSettings(); + for (const leaf of this.app.workspace.getLeavesOfType(VIEW_TYPE_MINI_WORLD_MAP)) { + const view = leaf.view; + if (view instanceof MiniWorldMapView) view.switchMode(this.settings.viewMode); + } + } + + async activateView(): Promise { + const { workspace } = this.app; + let leaf = workspace.getLeavesOfType(VIEW_TYPE_MINI_WORLD_MAP)[0] ?? null; + if (!leaf) { + leaf = workspace.getLeaf(true); + await leaf.setViewState({ type: VIEW_TYPE_MINI_WORLD_MAP, active: true }); + } + if (leaf.isDeferred) await leaf.loadIfDeferred(); + await workspace.revealLeaf(leaf); + return leaf.view instanceof MiniWorldMapView ? leaf.view : null; + } +} + +class MiniWorldMapSettingTab extends PluginSettingTab { + constructor( + app: import('obsidian').App, + private plugin: MiniWorldMapPlugin, + ) { + super(app, plugin); + } + + display(): void { + const { containerEl } = this; + const radial = this.plugin.settings.radial; + const language = this.plugin.settings.language; + containerEl.empty(); + containerEl.createEl('h2', { text: t(language, 'settings.title') }); + + new Setting(containerEl) + .setName(t(language, 'language')) + .setDesc(t(language, 'settings.languageDesc')) + .addDropdown((dropdown) => { + for (const [value, label] of languageOptions(language)) dropdown.addOption(value, label); + dropdown.setValue(this.plugin.settings.language).onChange(async (value) => { + this.plugin.settings.language = normalizeLanguage(value); + await this.plugin.saveSettings(); + this.display(); + }); + }); + + new Setting(containerEl) + .setName(t(language, 'settings.defaultMode')) + .setDesc(t(language, 'settings.defaultModeDesc')) + .addDropdown((dropdown) => + dropdown + .addOption('radial2d', viewModeLabel(language, 'radial2d')) + .addOption('map3d', viewModeLabel(language, 'map3d')) + .setValue(this.plugin.settings.viewMode) + .onChange(async (value) => { + this.plugin.settings.viewMode = normalizeViewMode(value); + await this.plugin.saveSettings(); + }), + ); + + new Setting(containerEl) + .setName(t(language, 'control.defaultDepth')) + .setDesc(t(language, 'settings.depthDesc')) + .addSlider((slider) => + slider + .setLimits(1, 20, 1) + .setValue(radial.atlasDepth) + .setDynamicTooltip() + .onChange(async (value) => { + radial.atlasDepth = clampNumber(value, 1, MAX_ATLAS_DEPTH, DEFAULT_RADIAL_SETTINGS.atlasDepth); + await this.plugin.saveSettings(); + }), + ); + + new Setting(containerEl) + .setName(t(language, 'control.defaultNodes')) + .setDesc(t(language, 'settings.nodeLimitDesc')) + .addText((text) => + text + .setPlaceholder(String(DEFAULT_RADIAL_SETTINGS.renderNodeLimit)) + .setValue(String(radial.renderNodeLimit)) + .onChange(async (value) => { + radial.renderNodeLimit = clampNumber(value, 200, MAX_RENDER_NODE_LIMIT, DEFAULT_RADIAL_SETTINGS.renderNodeLimit); + await this.plugin.saveSettings(); + }), + ); + + new Setting(containerEl) + .setName(t(language, 'control.defaultNoteLinks')) + .setDesc(t(language, 'settings.linkLimitDesc')) + .addText((text) => + text + .setPlaceholder(String(DEFAULT_RADIAL_SETTINGS.linkLimit)) + .setValue(String(radial.linkLimit)) + .onChange(async (value) => { + radial.linkLimit = clampNumber(value, 0, MAX_LINK_LIMIT, DEFAULT_RADIAL_SETTINGS.linkLimit); + await this.plugin.saveSettings(); + }), + ); + + new Setting(containerEl) + .setName(t(language, 'control.showNoteLinks')) + .setDesc(t(language, 'settings.showLinksDesc')) + .addToggle((toggle) => + toggle.setValue(radial.showLinkOverlay && !radial.hiddenLegendItems.includes('link')).onChange(async (value) => { + radial.showLinkOverlay = value; + const hidden = new Set(radial.hiddenLegendItems); + if (value) hidden.delete('link'); + else hidden.add('link'); + radial.hiddenLegendItems = [...hidden]; + await this.plugin.saveSettings(); + }), + ); + + new Setting(containerEl) + .setName(t(language, 'control.hover')) + .setDesc(t(language, 'settings.hoverDesc')) + .addDropdown((dropdown) => { + for (const [value, label] of hoverModeOptions(language, HOVER_HIGHLIGHT_MODE_OPTIONS)) dropdown.addOption(value, label); + dropdown.setValue(radial.hoverHighlightMode).onChange(async (value) => { + radial.hoverHighlightMode = normalizeHoverHighlightMode(value); + await this.plugin.saveSettings(); + }); + }); + + new Setting(containerEl) + .setName(t(language, 'control.hoverTargets')) + .setDesc(t(language, 'settings.hoverTargetsDesc')) + .addDropdown((dropdown) => { + for (const [value, label] of hoverTargetOptions(language, HOVER_TARGET_MODE_OPTIONS)) dropdown.addOption(value, label); + dropdown.setValue(radial.hoverTargetMode).onChange(async (value) => { + radial.hoverTargetMode = normalizeHoverTargetMode(value); + await this.plugin.saveSettings(); + }); + }); + + new Setting(containerEl) + .setName(t(language, 'control.labels')) + .setDesc(t(language, 'settings.labelsDesc')) + .addDropdown((dropdown) => { + for (const [value, label] of labelVisibilityOptions(language, LABEL_VISIBILITY_OPTIONS)) dropdown.addOption(value, label); + dropdown.setValue(radial.labelVisibility).onChange(async (value) => { + radial.labelVisibility = normalizeLabelVisibility(value); + await this.plugin.saveSettings(); + }); + }); + + new Setting(containerEl) + .setName(t(language, 'control.spin')) + .setDesc(t(language, 'settings.spinDesc')) + .addSlider((slider) => + slider + .setLimits(0, MAX_SWIRL_STRENGTH, 1) + .setValue(radial.swirlStrength) + .setDynamicTooltip() + .onChange(async (value) => { + radial.swirlStrength = clampNumber(value, 0, MAX_SWIRL_STRENGTH, DEFAULT_RADIAL_SETTINGS.swirlStrength); + await this.plugin.saveSettings(); + }), + ); + + new Setting(containerEl) + .setName(t(language, 'control.unresolvedLinks')) + .setDesc(t(language, 'settings.unresolvedDesc')) + .addToggle((toggle) => + toggle.setValue(radial.includeUnresolvedLinks).onChange(async (value) => { + radial.includeUnresolvedLinks = value; + await this.plugin.saveSettings(); + }), + ); + + new Setting(containerEl) + .setName(t(language, 'control.ignoredFolders')) + .setDesc(t(language, 'settings.ignoredDesc')) + .addTextArea((text) => + text.setValue(radial.ignoreFolders.join('\n')).onChange(async (value) => { + radial.ignoreFolders = value + .split('\n') + .map((line) => line.trim()) + .filter(Boolean); + await this.plugin.saveSettings(); + }), + ); + } +} diff --git a/src/overlay/ControlPanel.ts b/src/overlay/ControlPanel.ts new file mode 100644 index 0000000..ee8482b --- /dev/null +++ b/src/overlay/ControlPanel.ts @@ -0,0 +1,347 @@ +import type { GalaxySettings, Language, ViewMode } from '../settings'; +import { DEFAULT_GALAXY_SETTINGS } from '../settings'; +import type { StylePreset } from '../render/stylePresets'; +import { STYLE_PRESETS } from '../render/stylePresets'; +import type { ColorTheme } from '../render/colorThemes'; +import { COLOR_THEMES } from '../render/colorThemes'; +import { t, viewModeLabel } from '../i18n'; +import { Slider } from './Slider'; + +type PanelPage = 'view' | 'appearance' | 'physics' | 'motion' | 'advanced'; + +export interface ControlPanelCallbacks { + onViewMode?: (mode: ViewMode) => void; + onBloom: () => void; + onPhysics: () => void; + onLook: () => void; + onCruise: (on: boolean) => void; + onCruiseSpeed: () => void; + onPreset: () => void; + onStylePreset: (p: StylePreset) => void; + onShowUnresolved: (on: boolean) => void; + onImportColors: () => void; + onShuffleColors: () => void; + onColorTheme: (t: ColorTheme) => void; + onRecenter: () => void; + onReveal: () => void; + onShowOrphans: (on: boolean) => void; + onSizeBy: () => void; + onQuality: () => void; + onSearch: () => void; + onReset: () => void; + runScenario: (s: 'S1' | 'S2' | 'S3') => void; +} + +export class ControlPanel { + readonly statsEl: HTMLElement; + private root: HTMLElement; + private body: HTMLElement; + private page: PanelPage = 'view'; + private sliders: Slider[] = []; + private cruiseBtn: HTMLButtonElement | null = null; + private presetBtn: HTMLButtonElement | null = null; + private unresolvedBtn: HTMLButtonElement | null = null; + private orphanBtn: HTMLButtonElement | null = null; + private sizeByBtn: HTMLButtonElement | null = null; + private qualityBtn: HTMLButtonElement | null = null; + private styleChips: HTMLButtonElement[] = []; + + constructor( + parent: HTMLElement, + private settings: GalaxySettings, + private language: Language, + private cb: ControlPanelCallbacks, + private viewMode: ViewMode = 'map3d', + ) { + this.root = parent.createDiv({ cls: 'galaxy-panel gx-theme-dark mwm-map-panel' }); + const header = this.root.createDiv({ cls: 'galaxy-panel-header' }); + this.statsEl = header.createDiv({ cls: 'galaxy-panel-stats', text: '…' }); + const collapseBtn = header.createEl('button', { cls: 'galaxy-panel-collapse', text: '-' }); + this.body = this.root.createDiv({ cls: 'galaxy-panel-body' }); + collapseBtn.addEventListener('click', () => { + const hidden = this.body.hasClass('is-hidden'); + this.body.toggleClass('is-hidden', !hidden); + collapseBtn.setText(hidden ? '-' : '+'); + }); + this.render(); + } + + private render(): void { + this.body.empty(); + this.sliders = []; + this.styleChips = []; + this.cruiseBtn = null; + this.presetBtn = null; + this.unresolvedBtn = null; + this.orphanBtn = null; + this.sizeByBtn = null; + this.qualityBtn = null; + + const modeSwitch = this.body.createDiv({ cls: 'mwm-mode-switch' }); + modeSwitch.createDiv({ cls: 'mwm-mode-switch-label', text: this.tt('view.mode') }); + const modeRow = modeSwitch.createDiv({ cls: 'galaxy-mode-row mwm-mode-row' }); + this.modeButton(modeRow, 'radial2d'); + this.modeButton(modeRow, 'map3d'); + + const settings = this.body.createDiv({ cls: 'mwm-panel-settings mwm-3d-settings' }); + const tabs = settings.createDiv({ cls: 'mwm-panel-tabs' }); + for (const [id, label] of [ + ['view', this.tt('tab.view')], + ['appearance', this.tt('tab.appearance')], + ['physics', this.tt('tab.physics')], + ['motion', this.tt('tab.motion')], + ['advanced', this.tt('tab.advanced')], + ] as const) { + const button = tabs.createEl('button', { cls: this.page === id ? 'is-active' : '', text: label }); + button.addEventListener('click', () => { + this.page = id; + this.render(); + }); + } + + const pageEl = settings.createDiv({ cls: 'mwm-panel-page' }); + if (this.page === 'appearance') this.renderAppearancePage(pageEl); + else if (this.page === 'physics') this.renderPhysicsPage(pageEl); + else if (this.page === 'motion') this.renderMotionPage(pageEl); + else if (this.page === 'advanced') this.renderAdvancedPage(pageEl); + else this.renderViewPage(pageEl); + } + + private renderViewPage(parent: HTMLElement): void { + const row = parent.createDiv({ cls: 'galaxy-panel-row' }); + const searchBtn = row.createEl('button', { text: this.tt('common.search') }); + searchBtn.addEventListener('click', this.cb.onSearch); + const recenterBtn = row.createEl('button', { text: this.tt('common.recenter') }); + recenterBtn.addEventListener('click', this.cb.onRecenter); + const revealBtn = row.createEl('button', { text: this.tt('3d.reveal') }); + revealBtn.addEventListener('click', this.cb.onReveal); + } + + private renderAppearancePage(parent: HTMLElement): void { + const s = this.settings; + const d = DEFAULT_GALAXY_SETTINGS; + const themeRow = parent.createDiv({ cls: 'galaxy-panel-row' }); + this.presetBtn = themeRow.createEl('button', { text: this.presetLabel() }); + this.presetBtn.addEventListener('click', () => { + s.preset = s.preset === 'deep-space' ? 'adaptive' : 'deep-space'; + this.presetBtn?.setText(this.presetLabel()); + this.cb.onPreset(); + }); + + const chipRow = parent.createDiv({ cls: 'gx-chips' }); + for (const preset of STYLE_PRESETS) { + const chip = chipRow.createEl('button', { cls: 'gx-chip', text: this.tt(`style.${preset.id}`) }); + chip.addEventListener('click', () => { + this.cb.onStylePreset(preset); + this.refreshAll(); + this.markActiveChip(preset.id); + }); + chip.dataset['presetId'] = preset.id; + this.styleChips.push(chip); + } + + this.sliders.push( + new Slider(parent, this.slider('3d.nodeSize', 0.3, 2.5, 0.05, d.look.nodeSize, () => s.look.nodeSize, (v) => (s.look.nodeSize = v), cbFmt('x'), this.cb.onLook)), + new Slider(parent, this.slider('3d.linkOpacity', 0, 0.6, 0.01, d.look.linkOpacity, () => s.look.linkOpacity, (v) => (s.look.linkOpacity = v), undefined, this.cb.onLook)), + new Slider(parent, this.slider('3d.twinkle', 0, 2, 0.1, d.look.twinkle, () => s.look.twinkle, (v) => (s.look.twinkle = v), (v) => (v < 0.05 ? this.tt('3d.twinkleOff') : v.toFixed(1)), this.cb.onLook)), + new Slider(parent, this.slider('3d.glowStrength', 0, 2.5, 0.05, d.bloom.strength, () => s.bloom.strength, (v) => (s.bloom.strength = v), undefined, this.cb.onBloom)), + new Slider(parent, this.slider('3d.glowRadius', 0, 1.2, 0.05, d.bloom.radius, () => s.bloom.radius, (v) => (s.bloom.radius = v), undefined, this.cb.onBloom)), + new Slider(parent, this.slider('3d.glowThreshold', 0, 1, 0.05, d.bloom.threshold, () => s.bloom.threshold, (v) => (s.bloom.threshold = v), undefined, this.cb.onBloom)), + ); + + const sizeRow = parent.createDiv({ cls: 'galaxy-panel-row' }); + this.sizeByBtn = sizeRow.createEl('button', { text: this.sizeByLabel() }); + this.sizeByBtn.addEventListener('click', () => { + const order: typeof s.look.sizeBy[] = ['degree', 'fileSize', 'uniform']; + s.look.sizeBy = order[(order.indexOf(s.look.sizeBy) + 1) % order.length] ?? 'degree'; + this.sizeByBtn?.setText(this.sizeByLabel()); + this.cb.onSizeBy(); + }); + + const themeSel = parent.createEl('select', { cls: 'gx-theme-select' }); + const customOpt = themeSel.createEl('option', { text: this.tt('3d.colorTheme'), value: '' }); + customOpt.disabled = true; + for (const colorTheme of COLOR_THEMES) { + themeSel.createEl('option', { text: this.tt(`color.${colorTheme.id}`), value: colorTheme.id }); + } + themeSel.value = COLOR_THEMES.some((theme) => theme.id === s.colorTheme) ? s.colorTheme : ''; + if (!themeSel.value) customOpt.selected = true; + themeSel.addEventListener('change', () => { + const colorTheme = COLOR_THEMES.find((theme) => theme.id === themeSel.value); + if (colorTheme) this.cb.onColorTheme(colorTheme); + }); + + const colorRow = parent.createDiv({ cls: 'galaxy-panel-row' }); + const importBtn = colorRow.createEl('button', { text: this.tt('3d.importColors') }); + importBtn.addEventListener('click', () => { + this.cb.onImportColors(); + customOpt.selected = true; + }); + const shuffleBtn = colorRow.createEl('button', { text: this.tt('3d.shuffleColors') }); + shuffleBtn.addEventListener('click', () => { + this.cb.onShuffleColors(); + customOpt.selected = true; + }); + } + + private renderPhysicsPage(parent: HTMLElement): void { + const s = this.settings; + const d = DEFAULT_GALAXY_SETTINGS; + this.sliders.push( + new Slider(parent, this.slider('3d.repel', 20, 400, 5, d.physics.repel, () => s.physics.repel, (v) => (s.physics.repel = v), (v) => String(Math.round(v)), this.cb.onPhysics)), + new Slider(parent, this.slider('3d.linkDistance', 20, 200, 5, d.physics.linkDistance, () => s.physics.linkDistance, (v) => (s.physics.linkDistance = v), (v) => String(Math.round(v)), this.cb.onPhysics)), + new Slider(parent, this.slider('3d.linkStrength', 0.1, 2, 0.1, d.physics.linkStrength, () => s.physics.linkStrength, (v) => (s.physics.linkStrength = v), cbFmt('x1'), this.cb.onPhysics)), + new Slider(parent, this.slider('3d.centerPull', 0, 0.2, 0.005, d.physics.centerPull, () => s.physics.centerPull, (v) => (s.physics.centerPull = v), (v) => v.toFixed(3), this.cb.onPhysics)), + new Slider(parent, this.slider('3d.flatten', 0, 0.8, 0.02, d.physics.flatten, () => s.physics.flatten, (v) => (s.physics.flatten = v), undefined, this.cb.onPhysics)), + ); + } + + private renderMotionPage(parent: HTMLElement): void { + const s = this.settings; + const d = DEFAULT_GALAXY_SETTINGS; + const row = parent.createDiv({ cls: 'galaxy-panel-row' }); + this.cruiseBtn = row.createEl('button', { text: this.cruiseLabel() }); + this.cruiseBtn.addEventListener('click', () => { + s.cruise = !s.cruise; + this.cruiseBtn?.setText(this.cruiseLabel()); + this.cb.onCruise(s.cruise); + }); + this.sliders.push( + new Slider(parent, this.slider('3d.speed', 0.2, 3, 0.1, d.cruiseSpeed, () => s.cruiseSpeed, (v) => (s.cruiseSpeed = v), cbFmt('x1'), this.cb.onCruiseSpeed)), + ); + const helpBody = parent.createDiv({ cls: 'galaxy-panel-help' }); + for (const key of ['3d.help.drag', '3d.help.pan', '3d.help.mac', '3d.help.fly', '3d.help.pick', '3d.help.keys', '3d.help.slider']) { + helpBody.createDiv({ text: this.tt(key) }); + } + } + + private renderAdvancedPage(parent: HTMLElement): void { + const s = this.settings; + const advRow = parent.createDiv({ cls: 'galaxy-panel-row' }); + this.unresolvedBtn = advRow.createEl('button', { text: this.unresolvedLabel() }); + this.unresolvedBtn.addEventListener('click', () => { + s.showUnresolved = !s.showUnresolved; + this.unresolvedBtn?.setText(this.unresolvedLabel()); + this.cb.onShowUnresolved(s.showUnresolved); + }); + this.orphanBtn = advRow.createEl('button', { text: this.orphanLabel() }); + this.orphanBtn.addEventListener('click', () => { + s.showOrphans = !s.showOrphans; + this.orphanBtn?.setText(this.orphanLabel()); + this.cb.onShowOrphans(s.showOrphans); + }); + const advRow2 = parent.createDiv({ cls: 'galaxy-panel-row' }); + this.qualityBtn = advRow2.createEl('button', { text: this.qualityLabel() }); + this.qualityBtn.addEventListener('click', () => { + const order: typeof s.qualityOverride[] = ['auto', 'high', 'low', 'mobile']; + s.qualityOverride = order[(order.indexOf(s.qualityOverride) + 1) % order.length] ?? 'auto'; + this.qualityBtn?.setText(this.qualityLabel()); + this.cb.onQuality(); + }); + const resetBtn = advRow2.createEl('button', { text: this.tt('common.resetDefaults') }); + resetBtn.addEventListener('click', () => { + this.cb.onReset(); + this.refreshAll(); + }); + if (__GALAXY_DEV__) { + const devRow = parent.createDiv({ cls: 'galaxy-panel-row' }); + for (const sc of ['S1', 'S2', 'S3'] as const) { + const b = devRow.createEl('button', { text: sc }); + b.addEventListener('click', () => this.cb.runScenario(sc)); + } + } + } + + private modeButton(parent: HTMLElement, mode: ViewMode): void { + const button = parent.createEl('button', { cls: this.viewMode === mode ? 'is-active' : '', text: viewModeLabel(this.language, mode) }); + button.addEventListener('click', () => this.cb.onViewMode?.(mode)); + } + + private slider( + key: string, + min: number, + max: number, + step: number, + defaultValue: number, + get: () => number, + set: (v: number) => void, + fmt: ((v: number) => string) | undefined, + onInput: () => void, + ): ConstructorParameters[1] { + return { + label: this.tt(key), + min, + max, + step, + defaultValue, + get, + set, + fmt, + onInput, + defaultLabel: this.tt('common.default'), + }; + } + + private presetLabel(): string { + return `${this.tt('view.theme')}: ${this.settings.preset === 'deep-space' ? this.tt('theme.deep') : this.tt('theme.auto')}`; + } + + private cruiseLabel(): string { + return this.settings.cruise ? this.tt('3d.cruiseOn') : this.tt('3d.cruiseOff'); + } + + private unresolvedLabel(): string { + return this.settings.showUnresolved ? this.tt('3d.unresolvedShow') : this.tt('3d.unresolvedHide'); + } + + private orphanLabel(): string { + return this.settings.showOrphans ? this.tt('3d.orphansShow') : this.tt('3d.orphansHide'); + } + + private sizeByLabel(): string { + return this.tt(`3d.size.${this.settings.look.sizeBy}`); + } + + private qualityLabel(): string { + return this.tt(`3d.quality.${this.settings.qualityOverride}`); + } + + private markActiveChip(id: string): void { + for (const chip of this.styleChips) chip.toggleClass('is-active', chip.dataset['presetId'] === id); + } + + refreshAll(): void { + for (const sl of this.sliders) sl.refresh(); + this.cruiseBtn?.setText(this.cruiseLabel()); + this.presetBtn?.setText(this.presetLabel()); + this.unresolvedBtn?.setText(this.unresolvedLabel()); + this.orphanBtn?.setText(this.orphanLabel()); + this.sizeByBtn?.setText(this.sizeByLabel()); + this.qualityBtn?.setText(this.qualityLabel()); + } + + setLanguage(language: Language): void { + this.language = language; + this.render(); + } + + setPanelTheme(cls: 'gx-theme-dark' | 'gx-theme-light'): void { + this.root.removeClass('gx-theme-dark'); + this.root.removeClass('gx-theme-light'); + this.root.addClass(cls); + } + + dispose(): void { + this.root.remove(); + this.sliders = []; + this.styleChips = []; + } + + private tt(key: string, vars: Record = {}): string { + return t(this.language, key, vars); + } +} + +function cbFmt(kind: 'x' | 'x1'): (value: number) => string { + return (value) => (kind === 'x1' ? `${value.toFixed(1)}x` : `${value.toFixed(2)}x`); +} diff --git a/src/overlay/OverlayManager.ts b/src/overlay/OverlayManager.ts new file mode 100644 index 0000000..a9b043e --- /dev/null +++ b/src/overlay/OverlayManager.ts @@ -0,0 +1,237 @@ +import type { App } from 'obsidian'; +import { TFile, getAllTags } from 'obsidian'; +import type { Language } from '../settings'; +import type { GraphData, GraphNode } from '../types'; +import type { AggregateRenderer } from '../render/AggregateRenderer'; +import { t } from '../i18n'; + +export interface OverlayCallbacks { + openNote: (id: string) => void; + focusNode: (index: number) => void; +} + +/** + * DOM 浮层(NASA 模式:标签和卡片不进画布)。 + * 硬预算:枢纽 14 + hover 1 + 邻居 ≤20 + 卡片 1 —— 每帧 ≤36 次投影,可忽略。 + */ +export class OverlayManager { + private root: HTMLElement; + private hubEls: { index: number; el: HTMLElement }[] = []; + private neighborEls: { index: number; el: HTMLElement }[] = []; + private hoverEl: HTMLElement; + private hoverIndex = -1; + private card: HTMLElement; + private cardIndex = -1; + private data: GraphData = { nodes: [], links: [] }; + private graphRadius = 200; + private snippetToken = 0; + private hubBudget = 14; + private neighborBudget = 20; + private mobileCard = false; + + constructor( + parent: HTMLElement, + private app: App, + private renderer: AggregateRenderer, + private cb: OverlayCallbacks, + private language: Language = 'en', + ) { + this.root = parent.createDiv({ cls: 'gx-overlay' }); + this.hoverEl = this.root.createDiv({ cls: 'gx-label gx-label-hover' }); + this.hoverEl.hide(); + this.card = this.root.createDiv({ cls: 'gx-card' }); + this.card.hide(); + } + + setLanguage(language: Language): void { + this.language = language; + if (this.cardIndex >= 0) { + const node = this.data.nodes[this.cardIndex]; + if (node) this.buildCard(node, this.cardIndex); + } + } + + /** 质量档位预算;卡片切底部抽屉模式(移动端) */ + setBudgets(hub: number, neighbor: number, mobileCard: boolean): void { + this.hubBudget = hub; + this.neighborBudget = neighbor; + this.mobileCard = mobileCard; + this.setData(this.data, this.graphRadius); + } + + setData(data: GraphData, graphRadius: number): void { + this.data = data; + this.graphRadius = graphRadius; + for (const h of this.hubEls) h.el.remove(); + this.hubEls = [...data.nodes.entries()] + .filter(([, n]) => !n.unresolved) + .sort((a, b) => b[1].degree - a[1].degree) + .slice(0, this.hubBudget) + .map(([index, n]) => ({ + index, + el: this.root.createDiv({ cls: 'gx-label gx-label-hub', text: n.name }), + })); + // 数据重建后旧索引失效,清掉依赖索引的状态 + this.setHover(-1); + this.setSelection(-1, new Set()); + } + + setHover(index: number): void { + this.hoverIndex = index; + if (index < 0) { + this.hoverEl.hide(); + return; + } + const node = this.data.nodes[index]; + if (!node) return; + this.hoverEl.setText(node.name); + this.hoverEl.show(); + } + + /** + * 自适应底部留白(M4.1):实测 .mobile-navbar 与画布的重叠像素。 + * 官方未暴露 navbar 高度变量,且平板/隐藏设置/安卓变体下可能不存在—— + * 运行时测量在所有形态下自适应:无 navbar 时为 0,不会多出空白。 + */ + private refreshBottomInset(): void { + let inset = 0; + const navbar = activeDocument.querySelector('.mobile-navbar'); + if (navbar) { + const nb = navbar.getBoundingClientRect(); + const ce = this.root.getBoundingClientRect(); + inset = Math.max(0, Math.round(ce.bottom - nb.top)); + } + this.root.setCssProps({ '--gx-bottom-inset': `${inset}px` }); + } + + /** 选中:邻居标签 + 卡片;index<0 清空 */ + setSelection(index: number, neighbors: Set): void { + for (const e of this.neighborEls) e.el.remove(); + this.neighborEls = []; + this.cardIndex = index; + if (index < 0) { + this.card.hide(); + return; + } + const byDegree = [...neighbors] + .filter((i) => i !== index) + .sort((a, b) => (this.data.nodes[b]?.degree ?? 0) - (this.data.nodes[a]?.degree ?? 0)) + .slice(0, this.neighborBudget); + this.neighborEls = byDegree.map((i) => ({ + index: i, + el: this.root.createDiv({ cls: 'gx-label gx-label-neighbor', text: this.data.nodes[i]?.name ?? '' }), + })); + const node = this.data.nodes[index]; + if (node) { + if (this.mobileCard) { + this.refreshBottomInset(); + // 移除桌面定位残留的内联 transform → CSS 底部抽屉定位靠选择器特异性接管(免 !important) + this.card.style.removeProperty('transform'); + } + this.buildCard(node, index); + } + } + + private buildCard(node: GraphNode, index: number): void { + this.card.empty(); + this.card.show(); + + this.card.createDiv({ cls: 'gx-card-title', text: node.name }); + const meta = this.card.createDiv({ cls: 'gx-card-meta' }); + const dot = meta.createSpan({ cls: 'gx-card-dot' }); + dot.style.background = this.renderer.nodeColorHex(index); + meta.createSpan({ + text: node.unresolved ? this.tt('3d.card.unresolved') : node.id.includes('/') ? node.id.slice(0, node.id.lastIndexOf('/')) : this.tt('3d.card.root'), + }); + + const file = node.unresolved ? null : this.app.vault.getAbstractFileByPath(node.id); + const tfile = file instanceof TFile ? file : null; + + if (tfile) { + const cache = this.app.metadataCache.getFileCache(tfile); + const tags = cache ? (getAllTags(cache) ?? []) : []; + if (tags.length > 0) { + const tagRow = this.card.createDiv({ cls: 'gx-card-tags' }); + for (const t of tags.slice(0, 5)) tagRow.createSpan({ cls: 'gx-card-tag', text: t }); + } + } + + const stats = this.card.createDiv({ cls: 'gx-card-stats' }); + stats.setText( + this.tt('3d.card.stats', { in: node.inDegree, out: node.outDegree }) + + (tfile ? this.tt('3d.card.modified', { date: new Date(tfile.stat.mtime).toLocaleDateString(this.language === 'zh' ? 'zh-CN' : 'en-US') }) : ''), + ); + + if (tfile) { + const snippetEl = this.card.createDiv({ cls: 'gx-card-snippet', text: '…' }); + const token = ++this.snippetToken; + void this.app.vault.cachedRead(tfile).then((text) => { + if (token !== this.snippetToken) return; // 已切换选中,丢弃过期结果 + snippetEl.setText(stripMarkdown(text).slice(0, 120) || this.tt('3d.card.empty')); + }); + } + + const actions = this.card.createDiv({ cls: 'gx-card-actions' }); + if (!node.unresolved) { + const openBtn = actions.createEl('button', { text: this.tt('context.openNote') }); + openBtn.addEventListener('click', () => this.cb.openNote(node.id)); + } + const focusBtn = actions.createEl('button', { text: this.tt('common.focus') }); + focusBtn.addEventListener('click', () => this.cb.focusNode(index)); + } + + /** 每帧:投影所有被追踪节点,translate3d 定位(GPU 合成,无重排) */ + update(w: number, h: number): void { + const far = this.graphRadius * 2.6; + const near = this.graphRadius * 1.2; + for (const { index, el } of this.hubEls) { + const p = this.renderer.projectNode(index, w, h); + if (p.behind || p.x < 0 || p.x > w || p.y < 0 || p.y > h) { + el.setCssProps({ opacity: '0' }); + continue; + } + const dist = this.renderer.cameraDistanceTo(index); + const a = Math.min(Math.max((far - dist) / (far - near), 0), 1); + el.style.opacity = a.toFixed(2); + el.style.transform = `translate3d(${p.x.toFixed(1)}px, ${(p.y - 14).toFixed(1)}px, 0)`; + } + for (const { index, el } of this.neighborEls) { + const p = this.renderer.projectNode(index, w, h); + el.style.opacity = p.behind ? '0' : '0.85'; + if (!p.behind) el.style.transform = `translate3d(${p.x.toFixed(1)}px, ${(p.y - 12).toFixed(1)}px, 0)`; + } + if (this.hoverIndex >= 0) { + const p = this.renderer.projectNode(this.hoverIndex, w, h); + if (!p.behind) this.hoverEl.style.transform = `translate3d(${p.x.toFixed(1)}px, ${(p.y - 18).toFixed(1)}px, 0)`; + } + if (this.cardIndex >= 0 && !this.mobileCard) { + const p = this.renderer.projectNode(this.cardIndex, w, h); + if (!p.behind) { + const flip = p.x + 296 > w; + const x = flip ? p.x - 296 : p.x + 16; + const y = Math.min(Math.max(p.y - 40, 12), Math.max(h - this.card.clientHeight - 12, 12)); + this.card.style.transform = `translate3d(${x.toFixed(1)}px, ${y.toFixed(1)}px, 0)`; + } + } + } + + dispose(): void { + this.root.remove(); + this.hubEls = []; + this.neighborEls = []; + } + + private tt(key: string, vars: Record = {}): string { + return t(this.language, key, vars); + } +} + +function stripMarkdown(text: string): string { + return text + .replace(/^---\n[\s\S]*?\n---\n?/, '') // frontmatter + .replace(/!?\[\[([^\]|]+)(\|[^\]]+)?\]\]/g, '$1') + .replace(/\[([^\]]*)\]\([^)]*\)/g, '$1') + .replace(/[#*`>~_]|---/g, '') + .replace(/\s+/g, ' ') + .trim(); +} diff --git a/src/overlay/Slider.ts b/src/overlay/Slider.ts new file mode 100644 index 0000000..6f94cab --- /dev/null +++ b/src/overlay/Slider.ts @@ -0,0 +1,110 @@ +export interface SliderSpec { + label: string; + min: number; + max: number; + step: number; + defaultValue: number; + get: () => number; + set: (v: number) => void; + fmt?: (v: number) => string; + onInput: () => void; + defaultLabel?: string; +} + +/** + * Lightroom 式参数滑杆(G1 反馈:默认值居中 + 限位可视化)。 + * - 默认值永远在轨道几何中心:左半轴 [min,default]、右半轴 [default,max] 分段线性 + * - 中心刻痕 = 默认位;填充条从刻痕画到滑块(一眼看出偏离方向与幅度) + * - 轨道两端常驻 min/max 限位值;双击回默认 + */ +export class Slider { + private thumbEl: HTMLElement; + private fillEl: HTMLElement; + private valueEl: HTMLElement; + private trackEl: HTMLElement; + + constructor( + parent: HTMLElement, + private spec: SliderSpec, + ) { + const root = parent.createDiv({ cls: 'gx-slider' }); + + const head = root.createDiv({ cls: 'gx-slider-head' }); + head.createSpan({ cls: 'gx-slider-label', text: spec.label }); + this.valueEl = head.createSpan({ cls: 'gx-slider-value' }); + + this.trackEl = root.createDiv({ cls: 'gx-slider-track' }); + this.trackEl.createDiv({ cls: 'gx-slider-rail' }); + this.fillEl = this.trackEl.createDiv({ cls: 'gx-slider-fill' }); + this.trackEl.createDiv({ + cls: 'gx-slider-notch', + attr: { title: `${spec.defaultLabel ?? 'Default'} ${this.fmt(spec.defaultValue)}` }, + }); + this.thumbEl = this.trackEl.createDiv({ cls: 'gx-slider-thumb' }); + + const bounds = root.createDiv({ cls: 'gx-slider-bounds' }); + bounds.createSpan({ text: this.fmt(spec.min) }); + bounds.createSpan({ text: this.fmt(spec.max) }); + + this.bindDrag(); + this.trackEl.addEventListener('dblclick', () => { + spec.set(spec.defaultValue); + this.refresh(); + spec.onInput(); + }); + this.refresh(); + } + + private fmt(v: number): string { + return this.spec.fmt ? this.spec.fmt(v) : v.toFixed(2); + } + + /** 位置(0..1) → 值:默认值锚定在 0.5 */ + private posToValue(p: number): number { + const { min, max, defaultValue: d, step } = this.spec; + const raw = p <= 0.5 ? min + (d - min) * (p / 0.5) : d + (max - d) * ((p - 0.5) / 0.5); + const stepped = Math.round(raw / step) * step; + return Math.min(Math.max(stepped, min), max); + } + + private valueToPos(v: number): number { + const { min, max, defaultValue: d } = this.spec; + if (v <= d) return d === min ? 0 : (0.5 * (v - min)) / (d - min); + return max === d ? 1 : 0.5 + (0.5 * (v - d)) / (max - d); + } + + private bindDrag(): void { + const onPointer = (e: PointerEvent) => { + const rect = this.trackEl.getBoundingClientRect(); + const p = Math.min(Math.max((e.clientX - rect.left) / rect.width, 0), 1); + this.spec.set(this.posToValue(p)); + this.refresh(); + this.spec.onInput(); + }; + this.trackEl.addEventListener('pointerdown', (e) => { + e.preventDefault(); + this.trackEl.setPointerCapture(e.pointerId); + onPointer(e); + const move = (ev: PointerEvent) => onPointer(ev); + const up = (ev: PointerEvent) => { + this.trackEl.releasePointerCapture(ev.pointerId); + this.trackEl.removeEventListener('pointermove', move); + this.trackEl.removeEventListener('pointerup', up); + }; + this.trackEl.addEventListener('pointermove', move); + this.trackEl.addEventListener('pointerup', up); + }); + } + + refresh(): void { + const v = this.spec.get(); + const p = this.valueToPos(v); + this.thumbEl.style.left = `${(p * 100).toFixed(2)}%`; + const from = Math.min(p, 0.5); + const width = Math.abs(p - 0.5); + this.fillEl.style.left = `${(from * 100).toFixed(2)}%`; + this.fillEl.style.width = `${(width * 100).toFixed(2)}%`; + this.valueEl.setText(this.fmt(v)); + this.valueEl.toggleClass('is-default', Math.abs(v - this.spec.defaultValue) < this.spec.step / 2); + } +} diff --git a/src/quality/tiers.ts b/src/quality/tiers.ts new file mode 100644 index 0000000..4ce0f42 --- /dev/null +++ b/src/quality/tiers.ts @@ -0,0 +1,54 @@ +/** + * 质量档位(M4):三档静态预设。 + * Platform.isMobile 是硬上限(移动永不自动升档);手动覆盖绝对优先; + * auto = high 起步 + FPS 看门狗单向降档(沉降后采样,会话内不回升)。 + */ +export type TierId = 'high' | 'low' | 'mobile'; + +export interface QualityTier { + id: TierId; + pixelRatioCap: number; + bloomAllowed: boolean; // mobile 关 bloom:shader 热核保住 80% 观感,省掉全部后期开销 + starScale: number; // 星空点数缩放 + nodeCap: number | null; // 按度数排序取 top N(实测 top1500 保 94% 链接质量) + linkCap: number | null; // 按 min(端点度数) 取 top N + hubLabels: number; + neighborLabels: number; + hoverThrottleMs: number | null; // null = 仅 tap(触屏无 hover) +} + +export const TIERS: Record = { + high: { + id: 'high', + pixelRatioCap: 2, + bloomAllowed: true, + starScale: 1, + nodeCap: null, + linkCap: null, + hubLabels: 14, + neighborLabels: 20, + hoverThrottleMs: 30, + }, + low: { + id: 'low', + pixelRatioCap: 1, + bloomAllowed: true, + starScale: 0.4, + nodeCap: null, + linkCap: null, + hubLabels: 8, + neighborLabels: 12, + hoverThrottleMs: 80, + }, + mobile: { + id: 'mobile', + pixelRatioCap: 1.5, + bloomAllowed: false, + starScale: 0.32, + nodeCap: 1500, + linkCap: 12_000, + hubLabels: 6, + neighborLabels: 8, + hoverThrottleMs: null, + }, +}; diff --git a/src/render/AggregateRenderer.ts b/src/render/AggregateRenderer.ts new file mode 100644 index 0000000..f50c15a --- /dev/null +++ b/src/render/AggregateRenderer.ts @@ -0,0 +1,672 @@ +import { + ACESFilmicToneMapping, + BufferAttribute, + BufferGeometry, + Color, + Group, + LineBasicMaterial, + LineSegments, + NoToneMapping, + PerspectiveCamera, + Points, + PointsMaterial, + Scene, + ShaderMaterial, + Vector2, + Vector3, + WebGLRenderer, +} from 'three'; +import { EffectComposer } from 'three/examples/jsm/postprocessing/EffectComposer.js'; +import { RenderPass } from 'three/examples/jsm/postprocessing/RenderPass.js'; +import { UnrealBloomPass } from 'three/examples/jsm/postprocessing/UnrealBloomPass.js'; +import { OutputPass } from 'three/examples/jsm/postprocessing/OutputPass.js'; +import type { GraphData } from '../types'; +import { BLOOM_DEFAULTS, NODE_BASE_RADIUS, NODE_MAX_RADIUS, STARFIELD_ROTATION_RAD_PER_S } from '../constants'; +import { NODE_FRAGMENT_SHADER, NODE_VERTEX_SHADER } from './shaders'; +import { linkColor, fallbackColorFn } from './palette'; +import type { NodeColorFn } from './palette'; +import { buildStarfield, disposeStarfield, Twinkler } from './starfield'; +import type { VisualTokens } from './presets'; +import type { QualityTier } from '../quality/tiers'; +import { DEEP_SPACE } from './presets'; + +const FOCUS_DIM = 0.12; +const FOCUS_FADE_S = 0.28; + +/** + * 聚合渲染器:全部节点 1×Points、全部链接 1×LineSegments、星空 3×Points、 + * 选中高亮链接 1×LineSegments。整个场景 <10 draw call。 + * 视觉方向(深空/晨昼)通过 VisualTokens 切换,无需重建 WebGL。 + */ +export class AggregateRenderer { + readonly camera: PerspectiveCamera; + readonly renderer: WebGLRenderer; + + private scene = new Scene(); + private composer: EffectComposer; + private bloomPass: UnrealBloomPass; + private outputPass: OutputPass; + private renderPass: RenderPass; + + private nodePoints: Points | null = null; + private nodeMaterial: ShaderMaterial | null = null; + private nodeGeometry: BufferGeometry | null = null; + private linkSegments: LineSegments | null = null; + private linkGeometry: BufferGeometry | null = null; + private linkMaterial: LineBasicMaterial | null = null; + private selSegments: LineSegments | null = null; + private selGeometry: BufferGeometry | null = null; + private selMaterial: LineBasicMaterial | null = null; + private selLinkIdx: number[] = []; + private starfield: Group; + private twinkler: Twinkler; + twinkleFreq = 0.5; + private motes: Points | null = null; + private reveal: { t0: number; durMs: number; maxR: number } | null = null; + private revealBuf: Float32Array = new Float32Array(0); + + private data: GraphData = { nodes: [], links: [] }; + private positions: Float32Array = new Float32Array(0); + private sizes: Float32Array = new Float32Array(0); + private dimCurrent: Float32Array = new Float32Array(0); + private dimTarget: Float32Array = new Float32Array(0); + private dimAnimating = false; + + private colorFn: NodeColorFn = fallbackColorFn; + private tokens: VisualTokens = DEEP_SPACE; + private tierBloomAllowed = true; + private lastW = 2; + private lastH = 2; + private baseLinkOpacity = 0.16; + private focusActive = false; + private graphRadiusEstimate: number; + + private projVec = new Vector3(); + private pixelScale = 1; + private nodeScale = 1; + + constructor(container: HTMLElement, graphRadiusEstimate: number) { + this.graphRadiusEstimate = graphRadiusEstimate; + this.renderer = new WebGLRenderer({ antialias: false, alpha: false }); + this.renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); + this.renderer.toneMapping = ACESFilmicToneMapping; + this.renderer.toneMappingExposure = 1.05; + this.renderer.info.autoReset = false; + container.appendChild(this.renderer.domElement); + + this.scene.background = new Color(this.tokens.background); + this.camera = new PerspectiveCamera(60, 1, 0.5, 50_000); + + const sf = buildStarfield(graphRadiusEstimate * 6.5); + this.starfield = sf.group; + this.twinkler = sf.twinkler; + this.scene.add(this.starfield); + + this.composer = new EffectComposer(this.renderer); + this.renderPass = new RenderPass(this.scene, this.camera); + this.bloomPass = new UnrealBloomPass( + new Vector2(2, 2), + BLOOM_DEFAULTS.strength, + BLOOM_DEFAULTS.radius, + BLOOM_DEFAULTS.threshold, + ); + this.outputPass = new OutputPass(); + this.composer.addPass(this.renderPass); + this.composer.addPass(this.bloomPass); + this.composer.addPass(this.outputPass); + } + + // ---------- 数据与颜色 ---------- + + setColorFn(fn: NodeColorFn): void { + this.colorFn = fn; + } + + setData(data: GraphData, positions: Float32Array): void { + this.data = data; + this.positions = positions; + this.disposeGraphObjects(); + + const n = data.nodes.length; + const m = data.links.length; + + // —— 节点 —— + const nodePos = new Float32Array(n * 3); + nodePos.set(positions.subarray(0, n * 3)); + const ghost = new Float32Array(n); + this.sizes = new Float32Array(n); + this.dimCurrent = new Float32Array(n).fill(1); + this.dimTarget = new Float32Array(n).fill(1); + for (let i = 0; i < n; i++) { + const node = data.nodes[i]; + if (!node) continue; + ghost[i] = node.unresolved ? 1 : 0; + this.sizes[i] = this.computeSize(node); + } + this.nodeGeometry = new BufferGeometry(); + this.nodeGeometry.setAttribute('position', new BufferAttribute(nodePos, 3)); + this.nodeGeometry.setAttribute('color', new BufferAttribute(new Float32Array(n * 3), 3)); + this.nodeGeometry.setAttribute('aSize', new BufferAttribute(this.sizes, 1)); + this.nodeGeometry.setAttribute('aGhost', new BufferAttribute(ghost, 1)); + this.nodeGeometry.setAttribute('aDim', new BufferAttribute(this.dimCurrent, 1)); + this.nodeMaterial = new ShaderMaterial({ + vertexShader: NODE_VERTEX_SHADER, + fragmentShader: NODE_FRAGMENT_SHADER, + vertexColors: true, + transparent: true, + depthWrite: false, + uniforms: { + uPixelScale: { value: this.pixelScale }, + uSizeMul: { value: this.nodeScale }, + uLightMode: { value: this.tokens.lightMode ? 1 : 0 }, + uMaxPoint: { value: 110 * this.renderer.getPixelRatio() }, + }, + }); + this.nodePoints = new Points(this.nodeGeometry, this.nodeMaterial); + this.nodePoints.renderOrder = 1; // 节点永远盖住链接网 + this.nodePoints.frustumCulled = false; + this.scene.add(this.nodePoints); + + // —— 链接 —— + this.linkGeometry = new BufferGeometry(); + this.linkGeometry.setAttribute('position', new BufferAttribute(new Float32Array(m * 2 * 3), 3)); + this.linkGeometry.setAttribute('color', new BufferAttribute(new Float32Array(m * 2 * 3), 3)); + this.linkMaterial = new LineBasicMaterial({ + vertexColors: true, + transparent: true, + opacity: this.effectiveLinkOpacity(), + depthWrite: false, + }); + this.linkSegments = new LineSegments(this.linkGeometry, this.linkMaterial); + this.linkSegments.renderOrder = 0; + this.linkSegments.frustumCulled = false; + this.scene.add(this.linkSegments); + + this.recolor(); + this.updatePositions(); + this.setSelectedLinks(this.selLinkIdx); // 数据重建后恢复高亮层 + } + + private sizeMode: 'degree' | 'fileSize' | 'uniform' = 'degree'; + + private computeSize(node: import('../types').GraphNode): number { + switch (this.sizeMode) { + case 'fileSize': + // 中位笔记 ~2KB;立方根压缩长尾,巨型文档不吞画面 + return Math.min(Math.max(NODE_BASE_RADIUS * (0.7 + 1.1 * Math.cbrt(node.fileSize / 4096)), 1.6), NODE_MAX_RADIUS); + case 'uniform': + return NODE_BASE_RADIUS * 1.3; + default: + return Math.min(NODE_BASE_RADIUS * (1 + 0.5 * Math.sqrt(node.degree)), NODE_MAX_RADIUS); + } + } + + setSizeMode(mode: 'degree' | 'fileSize' | 'uniform'): void { + this.sizeMode = mode; + if (!this.nodeGeometry) return; + for (let i = 0; i < this.data.nodes.length; i++) { + const node = this.data.nodes[i]; + if (node) this.sizes[i] = this.computeSize(node); + } + (this.nodeGeometry.getAttribute('aSize') as BufferAttribute).needsUpdate = true; + } + + /** + * 创世动画(G2.5 反馈):节点从中心按半径波次绽放到沉降坐标。 + * 仅在坐标已知(暖启动/已沉降)时调用;链接随节点坐标自然伸展 + 透明度渐入。 + */ + playReveal(durMs = 2600): void { + const n = this.data.nodes.length; + if (n === 0) return; + let maxR = 1; + for (let i = 0; i < n; i++) { + const r = Math.hypot(this.positions[i * 3] ?? 0, this.positions[i * 3 + 1] ?? 0, this.positions[i * 3 + 2] ?? 0); + if (r > maxR) maxR = r; + } + if (this.revealBuf.length < n * 3) this.revealBuf = new Float32Array(n * 3); + this.reveal = { t0: performance.now(), durMs, maxR }; + } + + private stepReveal(now: number): void { + if (!this.reveal || !this.nodeGeometry || !this.linkGeometry) return; + const { t0, durMs, maxR } = this.reveal; + const p = (now - t0) / durMs; + if (p >= 1) { + this.reveal = null; + this.updatePositions(); + if (this.linkMaterial) this.linkMaterial.opacity = this.effectiveLinkOpacity(); + return; + } + const n = this.data.nodes.length; + const buf = this.revealBuf; + const pos = this.positions; + for (let i = 0; i < n; i++) { + const x = pos[i * 3] ?? 0; + const y = pos[i * 3 + 1] ?? 0; + const z = pos[i * 3 + 2] ?? 0; + const delay = (Math.hypot(x, y, z) / maxR) * 0.55; // 内圈先亮,波次向外 + const local = Math.min(Math.max((p - delay) / 0.45, 0), 1); + const k = 1 - Math.pow(1 - local, 3); // easeOutCubic + buf[i * 3] = x * k; + buf[i * 3 + 1] = y * k; + buf[i * 3 + 2] = z * k; + } + const nodeAttr = this.nodeGeometry.getAttribute('position') as BufferAttribute; + (nodeAttr.array as Float32Array).set(buf.subarray(0, n * 3)); + nodeAttr.needsUpdate = true; + const linkAttr = this.linkGeometry.getAttribute('position') as BufferAttribute; + const arr = linkAttr.array as Float32Array; + const links = this.data.links; + for (let li = 0; li < links.length; li++) { + const l = links[li]; + if (!l) continue; + const sI = l.source * 3; + const tI = l.target * 3; + const o = li * 6; + arr[o] = buf[sI] ?? 0; + arr[o + 1] = buf[sI + 1] ?? 0; + arr[o + 2] = buf[sI + 2] ?? 0; + arr[o + 3] = buf[tI] ?? 0; + arr[o + 4] = buf[tI + 1] ?? 0; + arr[o + 5] = buf[tI + 2] ?? 0; + } + linkAttr.needsUpdate = true; + if (this.linkMaterial) this.linkMaterial.opacity = this.effectiveLinkOpacity() * Math.min(p * 1.6, 1); + } + + get revealing(): boolean { + return this.reveal !== null; + } + + /** 配色/视觉方向变化时重算颜色(不动坐标) */ + recolor(): void { + if (!this.nodeGeometry || !this.linkGeometry) return; + const n = this.data.nodes.length; + const nodeColAttr = this.nodeGeometry.getAttribute('color') as BufferAttribute; + const nodeCol = nodeColAttr.array as Float32Array; + const resolved: Color[] = new Array(n); + const hsl = { h: 0, s: 0, l: 0 }; + for (let i = 0; i < n; i++) { + const node = this.data.nodes[i]; + if (!node) continue; + let c = this.colorFn(node).clone(); + if (this.tokens.nodeLightness !== null) { + c.getHSL(hsl); + c = c.setHSL(hsl.h, hsl.s * 0.95, this.tokens.nodeLightness); + } + resolved[i] = c; + nodeCol[i * 3] = c.r; + nodeCol[i * 3 + 1] = c.g; + nodeCol[i * 3 + 2] = c.b; + } + nodeColAttr.needsUpdate = true; + + const linkColAttr = this.linkGeometry.getAttribute('color') as BufferAttribute; + const linkCol = linkColAttr.array as Float32Array; + const ink = this.tokens.linkInk ? new Color(this.tokens.linkInk) : null; + const fallback = new Color('#7a87a8'); + for (let li = 0; li < this.data.links.length; li++) { + const l = this.data.links[li]; + if (!l) continue; + const c = ink ?? linkColor(resolved[l.source] ?? fallback, resolved[l.target] ?? fallback); + for (const v of [0, 1]) { + linkCol[(li * 2 + v) * 3] = c.r; + linkCol[(li * 2 + v) * 3 + 1] = c.g; + linkCol[(li * 2 + v) * 3 + 2] = c.b; + } + } + linkColAttr.needsUpdate = true; + } + + /** 布局热时每帧调用:节点直拷,链接按索引 gather */ + updatePositions(): void { + if (!this.nodeGeometry || !this.linkGeometry) return; + const n = this.data.nodes.length; + const nodeAttr = this.nodeGeometry.getAttribute('position') as BufferAttribute; + (nodeAttr.array as Float32Array).set(this.positions.subarray(0, n * 3)); + nodeAttr.needsUpdate = true; + + const linkAttr = this.linkGeometry.getAttribute('position') as BufferAttribute; + const arr = linkAttr.array as Float32Array; + const pos = this.positions; + const links = this.data.links; + for (let li = 0; li < links.length; li++) { + const l = links[li]; + if (!l) continue; + const s = l.source * 3; + const t = l.target * 3; + const o = li * 6; + arr[o] = pos[s] ?? 0; + arr[o + 1] = pos[s + 1] ?? 0; + arr[o + 2] = pos[s + 2] ?? 0; + arr[o + 3] = pos[t] ?? 0; + arr[o + 4] = pos[t + 1] ?? 0; + arr[o + 5] = pos[t + 2] ?? 0; + } + linkAttr.needsUpdate = true; + this.updateSelPositions(); + } + + // ---------- 聚焦与选中高亮 ---------- + + /** 聚焦模式:非邻居淡出(280ms 缓动,CPU 插值 3k floats 可忽略) */ + setFocus(selected: number, neighbors: Set | null): void { + const n = this.data.nodes.length; + this.focusActive = neighbors !== null; + for (let i = 0; i < n; i++) { + this.dimTarget[i] = !neighbors || neighbors.has(i) || i === selected ? 1 : FOCUS_DIM; + } + this.dimAnimating = true; + if (this.linkMaterial) this.linkMaterial.opacity = this.effectiveLinkOpacity(); + } + + /** 选中节点自身的链接 → 独立高亮层(全饱和、盖在最上) */ + setSelectedLinks(linkIndices: number[]): void { + this.selLinkIdx = linkIndices; + if (this.selSegments) { + this.scene.remove(this.selSegments); + this.selGeometry?.dispose(); + this.selMaterial?.dispose(); + this.selSegments = null; + this.selGeometry = null; + this.selMaterial = null; + } + if (linkIndices.length === 0) return; + const m = linkIndices.length; + const pos = new Float32Array(m * 6); + const col = new Float32Array(m * 6); + const hsl = { h: 0, s: 0, l: 0 }; + for (let k = 0; k < m; k++) { + const l = this.data.links[linkIndices[k] ?? -1]; + if (!l) continue; + const sNode = this.data.nodes[l.source]; + const c = (sNode ? this.colorFn(sNode).clone() : new Color('#9aa4b2')); + c.getHSL(hsl); + c.setHSL(hsl.h, Math.min(hsl.s * 1.2, 1), this.tokens.lightMode ? 0.42 : 0.62); + for (const v of [0, 1]) { + col[k * 6 + v * 3] = c.r; + col[k * 6 + v * 3 + 1] = c.g; + col[k * 6 + v * 3 + 2] = c.b; + } + } + this.selGeometry = new BufferGeometry(); + this.selGeometry.setAttribute('position', new BufferAttribute(pos, 3)); + this.selGeometry.setAttribute('color', new BufferAttribute(col, 3)); + this.selMaterial = new LineBasicMaterial({ + vertexColors: true, + transparent: true, + opacity: 0.85, + depthWrite: false, + }); + this.selSegments = new LineSegments(this.selGeometry, this.selMaterial); + this.selSegments.renderOrder = 2; + this.selSegments.frustumCulled = false; + this.scene.add(this.selSegments); + this.updateSelPositions(); + } + + private updateSelPositions(): void { + if (!this.selGeometry || this.selLinkIdx.length === 0) return; + const attr = this.selGeometry.getAttribute('position') as BufferAttribute; + const arr = attr.array as Float32Array; + const pos = this.positions; + for (let k = 0; k < this.selLinkIdx.length; k++) { + const l = this.data.links[this.selLinkIdx[k] ?? -1]; + if (!l) continue; + const s = l.source * 3; + const t = l.target * 3; + arr[k * 6] = pos[s] ?? 0; + arr[k * 6 + 1] = pos[s + 1] ?? 0; + arr[k * 6 + 2] = pos[s + 2] ?? 0; + arr[k * 6 + 3] = pos[t] ?? 0; + arr[k * 6 + 4] = pos[t + 1] ?? 0; + arr[k * 6 + 5] = pos[t + 2] ?? 0; + } + attr.needsUpdate = true; + } + + private effectiveLinkOpacity(): number { + const base = this.baseLinkOpacity * this.tokens.linkOpacityScale; + return this.focusActive ? base * 0.25 : base; + } + + // ---------- 视觉方向 ---------- + + applyTokens(tokens: VisualTokens, bloomStrengthFromSettings: number): void { + this.tokens = tokens; + this.scene.background = new Color(tokens.background); + this.starfield.visible = tokens.starfield; + this.renderer.toneMapping = tokens.lightMode ? NoToneMapping : ACESFilmicToneMapping; + if (this.nodeMaterial) { + this.nodeMaterial.uniforms['uLightMode']!.value = tokens.lightMode ? 1 : 0; + } + this.bloomPass.enabled = tokens.bloomEnabled && this.tierBloomAllowed && bloomStrengthFromSettings > 0.001; + if (tokens.motes && !this.motes) this.buildMotes(); + if (this.motes) this.motes.visible = tokens.motes; + if (this.linkMaterial) this.linkMaterial.opacity = this.effectiveLinkOpacity(); + this.recolor(); + this.setSelectedLinks(this.selLinkIdx); + } + + get currentTokens(): VisualTokens { + return this.tokens; + } + + /** 晨昼模式的尘埃微粒:600 点、近大远小、缓慢漂移 */ + private buildMotes(): void { + const count = 600; + const pos = new Float32Array(count * 3); + const R = this.graphRadiusEstimate * 2.2; + for (let i = 0; i < count; i++) { + pos[i * 3] = (Math.random() * 2 - 1) * R; + pos[i * 3 + 1] = (Math.random() * 2 - 1) * R; + pos[i * 3 + 2] = (Math.random() * 2 - 1) * R; + } + const geo = new BufferGeometry(); + geo.setAttribute('position', new BufferAttribute(pos, 3)); + const mat = new PointsMaterial({ + color: new Color('#d8d4cb'), + size: 1.6, + sizeAttenuation: true, + transparent: true, + opacity: 0.5, + depthWrite: false, + }); + this.motes = new Points(geo, mat); + this.motes.renderOrder = -1; + this.scene.add(this.motes); + } + + // ---------- 渲染循环 ---------- + + render(deltaS: number): void { + this.starfield.rotation.y += STARFIELD_ROTATION_RAD_PER_S * deltaS; + if (this.starfield.visible) this.twinkler.update(deltaS, this.twinkleFreq); + if (this.motes?.visible) this.motes.rotation.y -= STARFIELD_ROTATION_RAD_PER_S * 2 * deltaS; + if (this.dimAnimating) this.stepDim(deltaS); + if (this.reveal) this.stepReveal(performance.now()); + this.renderer.info.reset(); + this.composer.render(); + } + + private stepDim(deltaS: number): void { + const k = Math.min(deltaS / FOCUS_FADE_S, 1); + let active = false; + for (let i = 0; i < this.dimCurrent.length; i++) { + const cur = this.dimCurrent[i] ?? 1; + const tgt = this.dimTarget[i] ?? 1; + const next = cur + (tgt - cur) * k; + this.dimCurrent[i] = Math.abs(next - tgt) < 0.005 ? tgt : next; + if (this.dimCurrent[i] !== tgt) active = true; + } + this.dimAnimating = active; + if (this.nodeGeometry) { + (this.nodeGeometry.getAttribute('aDim') as BufferAttribute).needsUpdate = true; + } + } + + get drawCalls(): number { + return this.renderer.info.render.calls; + } + + // ---------- 参数 ---------- + + setBloomParams(p: { strength: number; radius: number; threshold: number }): void { + this.bloomPass.strength = p.strength; + this.bloomPass.radius = p.radius; + this.bloomPass.threshold = p.threshold; + this.bloomPass.enabled = this.tokens.bloomEnabled && this.tierBloomAllowed && p.strength > 0.001; + } + + getBloomStrength(): number { + return this.bloomPass.enabled ? this.bloomPass.strength : 0; + } + + setBloomStrength(v: number): void { + this.bloomPass.strength = v; + this.bloomPass.enabled = this.tokens.bloomEnabled && this.tierBloomAllowed && v > 0.001; + } + + /** 质量档位(M4):pixelRatio / bloom 门控 / 星空密度,全部免重建即时生效 */ + applyTier(tier: QualityTier, bloomStrengthFromSettings: number): void { + this.tierBloomAllowed = tier.bloomAllowed; + this.renderer.setPixelRatio(Math.min(window.devicePixelRatio, tier.pixelRatioCap)); + this.bloomPass.enabled = this.tokens.bloomEnabled && this.tierBloomAllowed && bloomStrengthFromSettings > 0.001; + // 星空按档位密度重建(一次性,毫秒级) + const visible = this.starfield.visible; + const rotation = this.starfield.rotation.y; + disposeStarfield(this.starfield); + this.scene.remove(this.starfield); + const sf = buildStarfield(this.graphRadiusEstimate * 6.5, tier.starScale); + this.starfield = sf.group; + this.twinkler = sf.twinkler; + this.starfield.visible = visible; + this.starfield.rotation.y = rotation; + this.scene.add(this.starfield); + this.resize(this.lastW, this.lastH); // pixelRatio 变化 → 重算 uPixelScale/uMaxPoint 与缓冲尺寸 + const u = this.nodeMaterial?.uniforms['uMaxPoint']; + if (u) u.value = 110 * this.renderer.getPixelRatio(); + } + + setLinkOpacity(v: number): void { + this.baseLinkOpacity = v; + if (this.linkMaterial) this.linkMaterial.opacity = this.effectiveLinkOpacity(); + } + + setNodeScale(v: number): void { + this.nodeScale = v; + const u = this.nodeMaterial?.uniforms['uSizeMul']; + if (u) u.value = v; + } + + resize(w: number, h: number): void { + if (w < 2 || h < 2) return; + this.lastW = w; + this.lastH = h; + this.camera.aspect = w / h; + this.camera.updateProjectionMatrix(); + this.renderer.setSize(w, h); + this.composer.setSize(w, h); + this.bloomPass.resolution.set(w, h); + const physH = h * this.renderer.getPixelRatio(); + this.pixelScale = physH / (2 * Math.tan(((this.camera.fov / 2) * Math.PI) / 180)); + const u = this.nodeMaterial?.uniforms['uPixelScale']; + if (u) u.value = this.pixelScale; + } + + // ---------- 拾取与投影 ---------- + + /** 投影到屏幕逻辑像素;z>1 = 在镜头后 */ + projectNode(i: number, w: number, h: number): { x: number; y: number; behind: boolean } { + this.projVec.set(this.positions[i * 3] ?? 0, this.positions[i * 3 + 1] ?? 0, this.positions[i * 3 + 2] ?? 0); + this.projVec.project(this.camera); + return { + x: ((this.projVec.x + 1) / 2) * w, + y: ((1 - this.projVec.y) / 2) * h, + behind: this.projVec.z > 1, + }; + } + + /** 屏幕空间最近邻拾取(O(n) 仅在点击/节流 hover 时跑) */ + pickNearest(px: number, py: number, w: number, h: number, maxPx: number): number { + let best = -1; + let bestDist = maxPx; + for (let i = 0; i < this.data.nodes.length; i++) { + const p = this.projectNode(i, w, h); + if (p.behind) continue; + const d = Math.hypot(p.x - px, p.y - py); + if (d < bestDist) { + bestDist = d; + best = i; + } + } + return best; + } + + nodeRadius(i: number): number { + return this.sizes[i] ?? NODE_BASE_RADIUS; + } + + nodePosition(i: number, out: Vector3): Vector3 { + return out.set(this.positions[i * 3] ?? 0, this.positions[i * 3 + 1] ?? 0, this.positions[i * 3 + 2] ?? 0); + } + + nodeColorHex(i: number): string { + const node = this.data.nodes[i]; + return node ? `#${this.colorFn(node).getHexString()}` : '#9aa4b2'; + } + + cameraDistanceTo(i: number): number { + this.projVec.set(this.positions[i * 3] ?? 0, this.positions[i * 3 + 1] ?? 0, this.positions[i * 3 + 2] ?? 0); + return this.camera.position.distanceTo(this.projVec); + } + + // ---------- 销毁合同 ---------- + + private disposeGraphObjects(): void { + if (this.nodePoints) this.scene.remove(this.nodePoints); + if (this.linkSegments) this.scene.remove(this.linkSegments); + if (this.selSegments) { + this.scene.remove(this.selSegments); + this.selGeometry?.dispose(); + this.selMaterial?.dispose(); + this.selSegments = null; + this.selGeometry = null; + this.selMaterial = null; + } + this.nodeGeometry?.dispose(); + this.nodeMaterial?.dispose(); + this.linkGeometry?.dispose(); + this.linkMaterial?.dispose(); + this.nodePoints = null; + this.linkSegments = null; + this.nodeGeometry = null; + this.linkGeometry = null; + this.nodeMaterial = null; + this.linkMaterial = null; + } + + /** 销毁合同:composer 目标 → 场景资源 → renderer → 强制丢上下文 */ + dispose(): void { + this.disposeGraphObjects(); + disposeStarfield(this.starfield); + this.scene.remove(this.starfield); + if (this.motes) { + this.motes.geometry.dispose(); + (this.motes.material as PointsMaterial).dispose(); + this.scene.remove(this.motes); + this.motes = null; + } + this.bloomPass.dispose(); + this.outputPass.dispose(); + this.renderPass.dispose(); + this.composer.dispose(); + this.renderer.dispose(); + try { + this.renderer.forceContextLoss(); + } catch { + // 上下文可能已丢失 + } + this.renderer.domElement.remove(); + } +} diff --git a/src/render/RadialRenderer.ts b/src/render/RadialRenderer.ts new file mode 100644 index 0000000..b5d7035 --- /dev/null +++ b/src/render/RadialRenderer.ts @@ -0,0 +1,790 @@ +import { + BufferAttribute, + BufferGeometry, + Color, + LineBasicMaterial, + LineSegments, + Mesh, + MeshBasicMaterial, + OrthographicCamera, + Points, + Scene, + ShaderMaterial, + Vector3, + WebGLRenderer, +} from 'three'; +import type { LabelVisibility } from '../settings'; +import type { RadialLayout, RadialPoint, RadialRoute } from '../layout/radial/layoutRadial'; +import type { VisibleWorldGraph, WorldEdge, WorldNode } from '../world/types'; +import { NODE_FRAGMENT_SHADER, NODE_VERTEX_SHADER } from './shaders'; + +export interface RadialActiveState { + hasActive: boolean; + dimOthers: boolean; + activeNodeId: string | null; + activeLinkId: string | null; + relatedNodes: Set; + highlightedEdges: Set; + labelNodes: Set; + pinnedNodeIds: Set; +} + +interface EdgeVisual { + key: string; + edge: WorldEdge; + points: { x: number; y: number }[]; +} + +export type RadialResolvedScheme = 'day' | 'night'; + +interface RadialPalette { + bg: string; + ring: string; + tree: string; + link: string; + external: string; + externalLink: string; + unresolved: string; + focus: string; + folder: string; + folderMeta: string; + note: string; + root: string; + ringOpacity: number; + treeOpacity: number; + linkOpacity: number; + externalLinkOpacity: number; + highlightOpacity: number; + nodeScale: number; + maxLabels: number; +} + +const PALETTES: Record = { + day: { + bg: '#f7f8fb', + ring: '#94a3b8', + tree: '#5b677a', + link: '#525e70', + external: '#5b5fe0', + externalLink: '#cb7622', + unresolved: '#dc2626', + focus: '#5368ee', + folder: '#3f4a5a', + folderMeta: '#4f5ee8', + note: '#536073', + root: '#4f5ee8', + ringOpacity: 0.16, + treeOpacity: 0.17, + linkOpacity: 0.06, + externalLinkOpacity: 0.08, + highlightOpacity: 0.9, + nodeScale: 0.19, + maxLabels: 140, + }, + night: { + bg: '#0f1117', + ring: '#818cf8', + tree: '#818cf8', + link: '#94a3b8', + external: '#c4b5fd', + externalLink: '#fb923c', + unresolved: '#fb7185', + focus: '#aab6ff', + folder: '#d7dae2', + folderMeta: '#f59e0b', + note: '#a1a8b5', + root: '#7c9cff', + ringOpacity: 0.15, + treeOpacity: 0.18, + linkOpacity: 0.065, + externalLinkOpacity: 0.09, + highlightOpacity: 0.98, + nodeScale: 0.2, + maxLabels: 140, + }, +}; + +export class RadialRenderer { + readonly renderer: WebGLRenderer; + readonly camera: OrthographicCamera; + readonly domElement: HTMLCanvasElement; + + private scene = new Scene(); + private labelRoot: HTMLElement; + private ringSegments: Mesh | null = null; + private hierarchySegments: LineSegments | null = null; + private linkSegments: LineSegments | null = null; + private highlightSegments: Mesh | null = null; + private nodePoints: Points | null = null; + private nodeGeometry: BufferGeometry | null = null; + private nodeMaterial: ShaderMaterial | null = null; + private nodeIds: string[] = []; + private edgeVisuals = new Map(); + private graph: VisibleWorldGraph | null = null; + private layout: RadialLayout | null = null; + private active: RadialActiveState = emptyActiveState(); + private width = 1; + private height = 1; + private centerX = 0; + private centerY = 0; + private zoom = 1; + private scheme: RadialResolvedScheme = 'night'; + private revealFrame = 0; + private revealOverlay: HTMLElement | null = null; + + constructor(private container: HTMLElement) { + this.renderer = new WebGLRenderer({ antialias: true, alpha: false }); + this.renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2)); + this.renderer.setClearColor(this.palette().bg, 1); + this.domElement = this.renderer.domElement; + this.domElement.classList.add('mwm-radial-canvas'); + this.domElement.tabIndex = 0; + container.appendChild(this.domElement); + this.labelRoot = container.createDiv({ cls: 'mwm-radial-labels' }); + this.camera = new OrthographicCamera(-1, 1, 1, -1, 0.1, 5000); + this.camera.position.set(0, 0, 1000); + this.camera.lookAt(0, 0, 0); + } + + setTheme(scheme: RadialResolvedScheme): boolean { + if (this.scheme === scheme) return false; + this.scheme = scheme; + this.renderer.setClearColor(this.palette().bg, 1); + if (this.nodeMaterial) { + const lightMode = this.nodeMaterial.uniforms['uLightMode']; + const pixelScale = this.nodeMaterial.uniforms['uPixelScale']; + if (lightMode) lightMode.value = scheme === 'day' ? 1 : 0; + if (pixelScale) pixelScale.value = 1000 * Math.min(window.devicePixelRatio || 1, 2); + this.updateNodeScale(); + } + this.render(); + return true; + } + + setData(graph: VisibleWorldGraph, layout: RadialLayout, labelVisibility: LabelVisibility): void { + this.graph = graph; + this.layout = layout; + this.edgeVisuals.clear(); + this.disposeObjects(); + this.buildRings(); + this.buildEdges(graph.hierarchyEdges, layout, 'hierarchy'); + this.buildEdges(graph.linkEdges, layout, 'links'); + for (const edge of graph.hoverLinkEdges) { + if (this.edgeVisuals.has(edge.id)) continue; + const visual = this.edgeVisual(edge, layout); + if (visual) this.edgeVisuals.set(visual.key, visual); + } + this.buildNodes(graph, layout); + this.setActive(this.active, labelVisibility); + this.render(); + } + + resize(width: number, height: number): void { + this.width = Math.max(1, Math.floor(width)); + this.height = Math.max(1, Math.floor(height)); + this.renderer.setSize(this.width, this.height, false); + this.camera.left = -this.width / 2; + this.camera.right = this.width / 2; + this.camera.top = this.height / 2; + this.camera.bottom = -this.height / 2; + this.applyCamera(); + this.updateLabels(); + } + + setView(centerX: number, centerY: number, zoom: number): void { + this.centerX = Number.isFinite(centerX) ? centerX : 0; + this.centerY = Number.isFinite(centerY) ? centerY : 0; + this.zoom = Math.min(Math.max(Number.isFinite(zoom) ? zoom : 1, 0.003), 6); + this.updateNodeScale(); + this.applyCamera(); + const rebuiltHighlights = this.active.highlightedEdges.size > 0; + if (rebuiltHighlights) this.rebuildHighlights(); + this.updateLabels(); + if (rebuiltHighlights) this.render(); + } + + getView(): { centerX: number; centerY: number; zoom: number } { + return { centerX: this.centerX, centerY: this.centerY, zoom: this.zoom }; + } + + fitToLayout(): void { + if (!this.layout) return; + const padding = 160; + const w = Math.max(1, this.layout.bounds.maxX - this.layout.bounds.minX + padding * 2); + const h = Math.max(1, this.layout.bounds.maxY - this.layout.bounds.minY + padding * 2); + const zoom = Math.min(this.width / w, this.height / h, 1.2) * 0.92; + this.setView((this.layout.bounds.minX + this.layout.bounds.maxX) / 2, (this.layout.bounds.minY + this.layout.bounds.maxY) / 2, zoom); + } + + playRevealFromRoot(rootId: string, text: string, durMs = 1200): void { + if (!this.layout || this.width < 2 || this.height < 2) return; + cancelAnimationFrame(this.revealFrame); + this.revealOverlay?.remove(); + const root = this.layout.positions.get(rootId) ?? { x: 0, y: 0 }; + const screen = this.worldToScreen(root.x, root.y); + const maxRadius = Math.hypot(Math.max(screen.x, this.width - screen.x), Math.max(screen.y, this.height - screen.y)) + 120; + const start = performance.now(); + this.container.style.setProperty('--mwm-reveal-x', `${screen.x.toFixed(1)}px`); + this.container.style.setProperty('--mwm-reveal-y', `${screen.y.toFixed(1)}px`); + this.container.style.setProperty('--mwm-reveal-radius', '0px'); + this.container.addClass('is-radial-revealing'); + this.revealOverlay = this.container.createDiv({ cls: 'mwm-radial-loading gx-mask-text', text }); + const step = (now: number) => { + const p = Math.min(Math.max((now - start) / durMs, 0), 1); + const eased = 1 - Math.pow(1 - p, 3); + this.container.style.setProperty('--mwm-reveal-radius', `${(maxRadius * eased).toFixed(1)}px`); + if (p < 1) { + this.revealFrame = window.requestAnimationFrame(step); + return; + } + this.container.removeClass('is-radial-revealing'); + this.revealOverlay?.addClass('is-fading'); + window.setTimeout(() => { + this.revealOverlay?.remove(); + this.revealOverlay = null; + }, 220); + }; + this.revealFrame = window.requestAnimationFrame(step); + } + + setActive(active: RadialActiveState, labelVisibility: LabelVisibility): void { + this.active = active; + this.updateNodeDim(); + this.rebuildHighlights(); + this.updateLabels(labelVisibility); + this.render(); + } + + render(): void { + this.renderer.render(this.scene, this.camera); + } + + worldToScreen(x: number, y: number): { x: number; y: number } { + const projected = new Vector3(x, y, 0).project(this.camera); + return { + x: ((projected.x + 1) / 2) * this.width, + y: ((1 - projected.y) / 2) * this.height, + }; + } + + screenToWorld(x: number, y: number): { x: number; y: number } { + return { + x: this.centerX + (x - this.width / 2) / this.zoom, + y: this.centerY - (y - this.height / 2) / this.zoom, + }; + } + + hitTest( + screenX: number, + screenY: number, + includeLinks: boolean, + includeNodes = true, + ): { nodeId: string | null; edge: WorldEdge | null } { + if (!this.graph || !this.layout) return { nodeId: null, edge: null }; + const world = this.screenToWorld(screenX, screenY); + let bestNode: { id: string; distance: number } | null = null; + if (includeNodes) { + for (const node of this.graph.nodes) { + const point = this.layout.positions.get(node.id); + if (!point) continue; + const distance = Math.hypot(world.x - point.x, world.y - point.y); + const visualRadius = Math.max(4, point.nodeRadius * this.palette().nodeScale * nodeScreenScale(this.zoom) * 0.55); + const radius = Math.max(point.nodeRadius * 0.36, visualRadius / Math.max(0.003, this.zoom)) + Math.max(5, 6 / this.zoom); + if (distance <= radius && (!bestNode || distance < bestNode.distance)) bestNode = { id: node.id, distance }; + } + } + if (bestNode) return { nodeId: bestNode.id, edge: null }; + if (!includeLinks) return { nodeId: null, edge: null }; + let bestEdge: { edge: WorldEdge; distance: number } | null = null; + for (const visual of this.edgeVisuals.values()) { + if (visual.edge.type === 'hierarchy' || visual.edge.type === 'external-hierarchy') continue; + const distance = distanceToPolyline(world, visual.points); + if (distance <= Math.max(10, 8 / this.zoom) && (!bestEdge || distance < bestEdge.distance)) { + bestEdge = { edge: visual.edge, distance }; + } + } + return { nodeId: null, edge: bestEdge?.edge ?? null }; + } + + nodePoint(nodeId: string): RadialPoint | null { + return this.layout?.positions.get(nodeId) ?? null; + } + + dispose(): void { + cancelAnimationFrame(this.revealFrame); + this.disposeObjects(); + this.renderer.dispose(); + this.revealOverlay?.remove(); + this.domElement.remove(); + this.labelRoot.remove(); + } + + private palette(): RadialPalette { + return PALETTES[this.scheme]; + } + + private applyCamera(): void { + this.camera.position.set(this.centerX, this.centerY, 1000); + this.camera.zoom = this.zoom; + this.camera.updateProjectionMatrix(); + this.render(); + } + + private buildRings(): void { + this.ringSegments = null; + } + + private buildEdges(edges: WorldEdge[], layout: RadialLayout, kind: 'hierarchy' | 'links'): void { + const positions: number[] = []; + const colors: number[] = []; + const palette = this.palette(); + for (const edge of edges) { + const visual = this.edgeVisual(edge, layout); + if (!visual) continue; + this.edgeVisuals.set(visual.key, visual); + const color = edgeColor(edge, kind, palette); + for (let i = 0; i < visual.points.length - 1; i++) { + const a = visual.points[i]; + const b = visual.points[i + 1]; + if (!a || !b) continue; + positions.push(a.x, a.y, kind === 'hierarchy' ? -1 : -2); + positions.push(b.x, b.y, kind === 'hierarchy' ? -1 : -2); + pushColor(colors, color, kind === 'hierarchy' ? 0.46 : edge.externalCount ? 0.22 : 0.16); + pushColor(colors, color, kind === 'hierarchy' ? 0.46 : edge.externalCount ? 0.22 : 0.16); + } + } + const line = new LineSegments( + makeLineGeometry(positions, colors), + new LineBasicMaterial({ + vertexColors: true, + transparent: true, + opacity: kind === 'hierarchy' ? palette.treeOpacity : edges.some((edge) => edge.externalCount) ? palette.externalLinkOpacity : palette.linkOpacity, + depthWrite: false, + }), + ); + if (kind === 'hierarchy') this.hierarchySegments = line; + else this.linkSegments = line; + this.scene.add(line); + } + + private buildNodes(graph: VisibleWorldGraph, layout: RadialLayout): void { + const palette = this.palette(); + const positions = new Float32Array(graph.nodes.length * 3); + const colors = new Float32Array(graph.nodes.length * 3); + const sizes = new Float32Array(graph.nodes.length); + const ghost = new Float32Array(graph.nodes.length); + const dim = new Float32Array(graph.nodes.length).fill(1); + this.nodeIds = graph.nodes.map((node) => node.id); + graph.nodes.forEach((node, index) => { + const point = layout.positions.get(node.id); + const color = nodeColor(node, graph.rootId, palette); + positions[index * 3] = point?.x ?? 0; + positions[index * 3 + 1] = point?.y ?? 0; + positions[index * 3 + 2] = 1; + colors[index * 3] = color.r; + colors[index * 3 + 1] = color.g; + colors[index * 3 + 2] = color.b; + sizes[index] = Math.max(2.1, (point?.nodeRadius ?? 8) * palette.nodeScale); + ghost[index] = node.type === 'unresolved' || node.type === 'external' || node.externalProxy ? 1 : 0; + }); + this.nodeGeometry = new BufferGeometry(); + this.nodeGeometry.setAttribute('position', new BufferAttribute(positions, 3)); + this.nodeGeometry.setAttribute('color', new BufferAttribute(colors, 3)); + this.nodeGeometry.setAttribute('aSize', new BufferAttribute(sizes, 1)); + this.nodeGeometry.setAttribute('aGhost', new BufferAttribute(ghost, 1)); + this.nodeGeometry.setAttribute('aDim', new BufferAttribute(dim, 1)); + this.nodeMaterial = new ShaderMaterial({ + vertexShader: NODE_VERTEX_SHADER, + fragmentShader: NODE_FRAGMENT_SHADER, + vertexColors: true, + transparent: true, + depthWrite: false, + uniforms: { + uPixelScale: { value: 1000 * Math.min(window.devicePixelRatio || 1, 2) }, + uSizeMul: { value: nodeScreenScale(this.zoom) }, + uLightMode: { value: this.scheme === 'day' ? 1 : 0 }, + uMaxPoint: { value: 58 * Math.min(window.devicePixelRatio || 1, 2) }, + }, + }); + this.nodePoints = new Points(this.nodeGeometry, this.nodeMaterial); + this.nodePoints.frustumCulled = false; + this.scene.add(this.nodePoints); + } + + private updateNodeDim(): void { + if (!this.nodeGeometry) return; + const attr = this.nodeGeometry.getAttribute('aDim') as BufferAttribute; + const dim = attr.array as Float32Array; + for (let i = 0; i < this.nodeIds.length; i++) { + const id = this.nodeIds[i] ?? ''; + const focused = + id === this.active.activeNodeId || this.active.pinnedNodeIds.has(id) || id === this.graph?.focusId; + const related = this.active.relatedNodes.has(id); + dim[i] = this.active.dimOthers && !focused && !related ? 0.23 : focused ? 1.12 : related ? 0.95 : 0.82; + } + attr.needsUpdate = true; + } + + private updateNodeScale(): void { + if (!this.nodeMaterial) return; + const sizeMul = this.nodeMaterial.uniforms['uSizeMul']; + const maxPoint = this.nodeMaterial.uniforms['uMaxPoint']; + if (sizeMul) sizeMul.value = nodeScreenScale(this.zoom); + if (maxPoint) maxPoint.value = 58 * Math.min(window.devicePixelRatio || 1, 2); + } + + private rebuildHighlights(): void { + if (this.highlightSegments) { + this.scene.remove(this.highlightSegments); + this.highlightSegments.geometry.dispose(); + if (Array.isArray(this.highlightSegments.material)) this.highlightSegments.material.forEach((m) => m.dispose()); + else this.highlightSegments.material.dispose(); + this.highlightSegments = null; + } + const positions: number[] = []; + const colors: number[] = []; + const palette = this.palette(); + const color = new Color(palette.focus); + const zoom = Math.max(0.003, this.zoom || 1); + for (const key of this.active.highlightedEdges) { + const visual = this.edgeVisuals.get(key); + if (!visual) continue; + for (let i = 0; i < visual.points.length - 1; i++) { + const a = visual.points[i]; + const b = visual.points[i + 1]; + if (!a || !b) continue; + pushSegmentBand(positions, colors, color, a, b, highlightWidthPx(visual.edge) / zoom, 2); + } + } + this.highlightSegments = new Mesh( + makeMeshGeometry(positions, colors), + new MeshBasicMaterial({ + vertexColors: true, + transparent: true, + opacity: palette.highlightOpacity, + depthWrite: false, + depthTest: false, + }), + ); + this.scene.add(this.highlightSegments); + } + + private edgeVisual(edge: WorldEdge, layout: RadialLayout): EdgeVisual | null { + const source = layout.positions.get(edge.source); + const target = layout.positions.get(edge.target); + if (!source || !target) return null; + const route = layout.routes.get(edge.id); + return { + key: edge.id, + edge, + points: routePoints(source, target, route), + }; + } + + private updateLabels(labelVisibility: LabelVisibility = 'auto'): void { + if (!this.graph || !this.layout) return; + this.labelRoot.empty(); + const directIds = new Set([ + ...this.active.labelNodes, + ...this.active.pinnedNodeIds, + this.active.activeNodeId ?? '', + this.graph.rootId, + this.graph.focusId ?? '', + ]); + directIds.delete(''); + const ranked = this.graph.nodes + .map((node) => { + const point = this.layout?.positions.get(node.id) ?? null; + return point ? { node, point, score: labelScore(node, point, this.graph!) } : null; + }) + .filter((item): item is { node: WorldNode; point: RadialPoint; score: number } => Boolean(item)) + .sort((a, b) => b.score - a.score); + const denominator = Math.max(1, ranked.length - 1); + const autoBudget = + labelVisibility === 'auto' && this.zoom >= 0.08 + ? Math.round(Math.min(this.palette().maxLabels, Math.max(0, (this.zoom - 0.08) * 84 + Math.sqrt(this.zoom) * 18))) + : 0; + let autoShown = 0; + for (let rank = 0; rank < ranked.length; rank++) { + const item = ranked[rank]!; + const { node, point } = item; + const direct = directIds.has(node.id); + const strength = direct + ? 1 + : labelVisibility === 'auto' + ? zoomLabelStrength(node, point, this.zoom, rank, denominator, this.graph) + : 0; + if (!direct) { + if (strength <= 0.08 || autoShown >= autoBudget) continue; + autoShown++; + } + const screen = this.worldToScreen(point.x, point.y); + if (screen.x < -160 || screen.y < -80 || screen.x > this.width + 160 || screen.y > this.height + 120) continue; + const label = this.labelRoot.createDiv({ cls: node.id === this.graph.rootId ? 'mwm-radial-label is-root' : 'mwm-radial-label' }); + label.setText(node.title); + const scale = labelScreenScale(this.zoom) * (node.id === this.graph.rootId ? 1.18 : point.nodeRadius >= 24 ? 1.1 : point.nodeRadius >= 15 ? 1.04 : 1); + const fontSize = Math.max(node.id === this.graph.rootId ? 12 : 9.5, 12 * scale); + label.style.fontSize = `${fontSize.toFixed(2)}px`; + label.style.maxWidth = `${Math.round((node.id === this.graph.rootId ? 190 : node.type === 'folder' ? 156 : point.nodeRadius >= 18 ? 146 : 124) * scale)}px`; + label.style.opacity = String(direct ? 0.96 : Math.min(0.9, 0.22 + strength * 0.68)); + const visualNodeRadius = Math.max(5, point.nodeRadius * this.palette().nodeScale * nodeScreenScale(this.zoom) * 0.62); + const labelOffset = Math.max(9, visualNodeRadius + 6 * scale); + label.style.transform = `translate3d(${screen.x.toFixed(1)}px, ${(screen.y + labelOffset).toFixed(1)}px, 0)`; + if (this.active.dimOthers && !directIds.has(node.id) && !this.active.relatedNodes.has(node.id)) { + label.style.opacity = String(Math.min(Number(label.style.opacity) || 1, 0.34)); + label.addClass('is-dim'); + } + } + } + + private disposeObjects(): void { + for (const obj of [this.ringSegments, this.hierarchySegments, this.linkSegments, this.highlightSegments, this.nodePoints]) { + if (!obj) continue; + this.scene.remove(obj); + if ('geometry' in obj) obj.geometry.dispose(); + const material = Array.isArray(obj.material) ? obj.material : [obj.material]; + for (const item of material) item.dispose(); + } + this.ringSegments = null; + this.hierarchySegments = null; + this.linkSegments = null; + this.highlightSegments = null; + this.nodePoints = null; + this.nodeGeometry = null; + this.nodeMaterial = null; + this.labelRoot.empty(); + } +} + +export function emptyActiveState(): RadialActiveState { + return { + hasActive: false, + dimOthers: false, + activeNodeId: null, + activeLinkId: null, + relatedNodes: new Set(), + highlightedEdges: new Set(), + labelNodes: new Set(), + pinnedNodeIds: new Set(), + }; +} + +function makeLineGeometry(positions: number[], colors: number[]): BufferGeometry { + const geometry = new BufferGeometry(); + geometry.setAttribute('position', new BufferAttribute(new Float32Array(positions), 3)); + geometry.setAttribute('color', new BufferAttribute(new Float32Array(colors), 3)); + return geometry; +} + +function makeMeshGeometry(positions: number[], colors: number[]): BufferGeometry { + const geometry = makeLineGeometry(positions, colors); + geometry.computeBoundingSphere(); + return geometry; +} + +function pushSegmentBand( + positions: number[], + colors: number[], + color: Color, + a: { x: number; y: number }, + b: { x: number; y: number }, + width: number, + z: number, +): void { + const dx = b.x - a.x; + const dy = b.y - a.y; + const length = Math.hypot(dx, dy); + if (!Number.isFinite(length) || length <= 0.001) return; + const half = Math.max(0.1, width * 0.5); + const nx = (-dy / length) * half; + const ny = (dx / length) * half; + const vertices = [ + [a.x + nx, a.y + ny, z], + [a.x - nx, a.y - ny, z], + [b.x - nx, b.y - ny, z], + [a.x + nx, a.y + ny, z], + [b.x - nx, b.y - ny, z], + [b.x + nx, b.y + ny, z], + ]; + for (const vertex of vertices) { + positions.push(vertex[0]!, vertex[1]!, vertex[2]!); + pushColor(colors, color, 1); + } +} + +function pushColor(colors: number[], color: Color, alpha: number): void { + void alpha; + colors.push(color.r, color.g, color.b); +} + +function nodeColor(node: WorldNode, rootId: string, palette: RadialPalette): Color { + if (node.id === rootId) return new Color(palette.root); + if (node.type === 'unresolved') return new Color(palette.unresolved); + if (node.type === 'external' || node.externalProxy) return new Color(palette.external); + if (node.type === 'folder' && node.representativeFile) return new Color(palette.folderMeta); + if (node.type === 'folder') return new Color(palette.folder); + return new Color(palette.note); +} + +function edgeColor(edge: WorldEdge, kind: 'hierarchy' | 'links', palette: RadialPalette): Color { + if (edge.unresolvedCount) return new Color(palette.unresolved); + if (edge.externalCount) return new Color(kind === 'links' ? palette.externalLink : palette.external); + return new Color(kind === 'hierarchy' ? palette.tree : palette.link); +} + +function routePoints(source: RadialPoint, target: RadialPoint, route: RadialRoute | undefined): { x: number; y: number }[] { + if (!route) return [source, target]; + if (route.kind === 'outer') { + const points: { x: number; y: number }[] = [source]; + const start = route.sourceAngle; + let end = route.endAngle ?? route.targetAngle; + let delta = end - start; + if (Math.abs(delta) > Math.PI) delta += delta > 0 ? -Math.PI * 2 : Math.PI * 2; + end = start + delta; + const steps = Math.max(8, Math.ceil(Math.abs(delta) / 0.18)); + for (let i = 0; i <= steps; i++) { + const angle = start + (delta * i) / steps; + points.push({ + x: route.centerX + Math.cos(angle) * route.radius, + y: route.centerY + Math.sin(angle) * route.radius, + }); + } + points.push(target); + return points; + } + const midX = (source.x + target.x) / 2; + const midY = (source.y + target.y) / 2; + const pull = route.curveStrength ?? 0.16; + const ctrl = { x: midX - midX * pull, y: midY - midY * pull }; + const points: { x: number; y: number }[] = []; + for (let i = 0; i <= 12; i++) { + const t = i / 12; + const a = (1 - t) * (1 - t); + const b = 2 * (1 - t) * t; + const c = t * t; + points.push({ x: a * source.x + b * ctrl.x + c * target.x, y: a * source.y + b * ctrl.y + c * target.y }); + } + return points; +} + +function distanceToPolyline(point: { x: number; y: number }, points: { x: number; y: number }[]): number { + let best = Infinity; + for (let i = 0; i < points.length - 1; i++) { + const a = points[i]; + const b = points[i + 1]; + if (!a || !b) continue; + best = Math.min(best, distanceToSegment(point, a, b)); + } + return best; +} + +function distanceToSegment(point: { x: number; y: number }, a: { x: number; y: number }, b: { x: number; y: number }): number { + const dx = b.x - a.x; + const dy = b.y - a.y; + const len = dx * dx + dy * dy; + if (len <= 1e-6) return Math.hypot(point.x - a.x, point.y - a.y); + const t = Math.max(0, Math.min(1, ((point.x - a.x) * dx + (point.y - a.y) * dy) / len)); + return Math.hypot(point.x - (a.x + dx * t), point.y - (a.y + dy * t)); +} + +function highlightWidthPx(edge: WorldEdge): number { + if (edge.type === 'hierarchy' || edge.type === 'external-hierarchy') return 3.1; + if (edge.externalCount) return 3.6; + return 4.1; +} + +function labelScore(node: WorldNode, point: RadialPoint, graph: VisibleWorldGraph): number { + const degree = Math.max(0, (node.linkCount || 0) + (node.backlinkCount || 0)); + let score = Math.max(0, point.nodeRadius || 0) * 2.8 + Math.log1p(degree) * 13 + Math.sqrt(degree) * 1.4 - Math.min(34, (node.depth || 0) * 3.2); + if (node.id === graph.rootId) score += 10000; + if (node.id === graph.focusId) score += 9000; + if (node.type === 'folder') { + score += 32 + Math.log1p(node.noteCount || node.descendantCount || 0) * 8; + if (node.representativeFile) score += 8; + } else if (node.type === 'note') { + score += 10; + } else if (node.type === 'external' || node.externalProxy) { + score -= 10; + } else if (node.type === 'unresolved') { + score -= 22; + } + return score; +} + +function zoomLabelStrength( + node: WorldNode, + point: RadialPoint, + zoom: number, + rank: number, + denominator: number, + graph: VisibleWorldGraph, +): number { + const clampedZoom = clampNumber(zoom, 0.003, 6); + const root = node.id === graph.rootId; + if (root) return 0.68 + smoothstep(0.04, 0.2, clampedZoom) * 0.32; + const degree = Math.max(0, (node.linkCount || 0) + (node.backlinkCount || 0)); + const folder = node.type === 'folder'; + const external = node.type === 'external' || node.externalProxy; + const unresolved = node.type === 'unresolved'; + const sizeSignal = clampNumber((point.nodeRadius - 4) / 42, 0, 1); + const degreeSignal = clampNumber(Math.log1p(degree) / Math.log1p(80), 0, 1); + const salienceSignal = clampNumber(1 - rank / Math.max(1, denominator), 0, 1); + const nodeCount = Math.max(1, graph.nodes.length || 1); + const leading = rank < Math.max(10, Math.min(58, Math.ceil(nodeCount * 0.05))); + const secondary = rank < Math.max(28, Math.min(170, Math.ceil(nodeCount * 0.18))); + const tertiary = rank < Math.max(80, Math.min(520, Math.ceil(nodeCount * 0.38))); + let threshold = 1.24 - sizeSignal * 0.64 - degreeSignal * 0.22 - salienceSignal * 0.42; + + if (leading) threshold -= 0.28; + else if (secondary) threshold -= 0.2; + else if (tertiary) threshold -= 0.08; + if (folder) threshold -= node.representativeFile ? 0.18 : 0.12; + else if (external) threshold -= 0.04; + if (unresolved) threshold += 0.18; + + threshold = clampNumber(threshold, 0.16, 1.38); + const fade = smoothstep(threshold - 0.14, threshold + 0.1, clampedZoom); + const leadingFade = leading + ? smoothstep(0.12, 0.38, clampedZoom) + : secondary + ? smoothstep(0.22, 0.62, clampedZoom) * 0.9 + : tertiary + ? smoothstep(0.46, 0.96, clampedZoom) * 0.7 + : 0; + const largeFade = + point.nodeRadius >= 24 + ? smoothstep(0.22, 0.56, clampedZoom) * 0.96 + : point.nodeRadius >= 15 + ? smoothstep(0.42, 0.86, clampedZoom) * 0.78 + : 0; + const smallFade = !unresolved ? smoothstep(0.86, 1.28, clampedZoom) * 0.82 : 0; + const closeFade = !unresolved ? smoothstep(1.12, 1.46, clampedZoom) * 0.98 : 0; + return clampNumber(Math.max(fade, leadingFade, largeFade, smallFade, closeFade), 0, 1); +} + +function labelScreenScale(zoom: number): number { + const clampedZoom = clampNumber(zoom, 0.003, 6); + return 0.62 + smoothstep(0.06, 1.48, clampedZoom) * 0.88; +} + +function nodeScreenScale(zoom: number): number { + const clampedZoom = clampNumber(zoom, 0.003, 6); + return ( + 0.7 + + smoothstep(0.07, 0.36, clampedZoom) * 0.36 + + smoothstep(0.36, 1.32, clampedZoom) * 0.82 + + smoothstep(1.32, 3.2, clampedZoom) * 0.72 + + smoothstep(3.2, 6, clampedZoom) * 0.28 + ); +} + +function smoothstep(edge0: number, edge1: number, value: number): number { + if (edge0 === edge1) return value >= edge1 ? 1 : 0; + const t = clampNumber((value - edge0) / (edge1 - edge0), 0, 1); + return t * t * (3 - 2 * t); +} + +function clampNumber(value: number, min: number, max: number): number { + if (!Number.isFinite(value)) return min; + return Math.min(max, Math.max(min, value)); +} diff --git a/src/render/colorThemes.ts b/src/render/colorThemes.ts new file mode 100644 index 0000000..6c0d3d7 --- /dev/null +++ b/src/render/colorThemes.ts @@ -0,0 +1,49 @@ +/** + * 配色主题:成组的具体配色(G2.5 反馈——靠洗牌不如给经典组合)。 + * 选取标准:在近黑太空底上的呈现效果。非商用项目,大胆借鉴品牌色。 + * 应用方式:按序染给用户的颜色分组(组多于色则循环)。 + */ +export interface ColorTheme { + id: string; + name: string; + colors: string[]; +} + +export const COLOR_THEMES: ColorTheme[] = [ + { + id: 'hubble', + name: '哈勃深空', + // 哈勃望远镜假彩色调色板:电离氧青、氢α金、硫离子锈红——天文摄影正统 + colors: ['#46d4dc', '#ffc35c', '#d05a32', '#7fd0a0', '#e8d9a0', '#5a9bd8', '#d87fa8', '#9a7fe0', '#cfd8e8'], + }, + { + id: 'tiktok', + name: '抖音霓虹', + // TikTok 青/红双主色 + 衍生明度级——黑底霓虹 + colors: ['#25f4ee', '#fe2c55', '#ffffff', '#7ae8e2', '#ff7a9c', '#19b8b2', '#c2244a', '#a8f0ec', '#ffd0dc'], + }, + { + id: 'sunset', + name: '落日胶片', + // Instagram 渐变:橙→品红→紫→蓝 + colors: ['#f58529', '#dd2a7b', '#8134af', '#515bd4', '#feda77', '#e1306c', '#c13584', '#fd8d32', '#405de6'], + }, + { + id: 'cyber', + name: '赛博都市', + // Cyberpunk 2077:信号黄/电青/警告红 + colors: ['#fcee0a', '#00f0ff', '#ff003c', '#9d00ff', '#00ff9f', '#ff6ec7', '#3df5ff', '#ffe600', '#c800ff'], + }, + { + id: 'matrix', + name: '黑客帝国', + // Matrix 纯绿阶——单色也是品味 + colors: ['#00ff41', '#33ff66', '#00cc34', '#66ff8c', '#00b32d', '#80ffa0', '#1aff4d', '#00e639', '#4dff79'], + }, + { + id: 'aurora', + name: '极光', + // Spotify 绿 + 冰蓝 + 紫罗兰——高纬夜空 + colors: ['#1db954', '#00d4ff', '#7f5fff', '#38f0c0', '#4fa8ff', '#9f7fff', '#22e6a8', '#66c2ff', '#b08fff'], + }, +]; diff --git a/src/render/palette.ts b/src/render/palette.ts new file mode 100644 index 0000000..556539e --- /dev/null +++ b/src/render/palette.ts @@ -0,0 +1,57 @@ +import { Color } from 'three'; +import { hash32 } from '../data/seed'; +import type { GraphNode } from '../types'; +import type { ColorGroup } from '../settings/graphJsonImport'; + +export type NodeColorFn = (node: GraphNode) => Color; + +/** + * 用户的 2D 图谱配色 → 节点调色函数。 + * 语义对齐自带图谱:path: 前缀匹配、自上而下首个命中生效;无命中走 hash 回退调色板。 + */ +export function makeNodeColorFn(groups: ColorGroup[]): NodeColorFn { + const parsed = groups.map((g) => ({ + prefix: g.query.startsWith('path:') ? g.query.slice(5).trim() : null, + raw: g.query, + color: new Color(g.color), + })); + return (node) => { + if (node.unresolved) return UNRESOLVED; + for (const g of parsed) { + if (g.prefix !== null ? node.id.startsWith(g.prefix) : node.id.includes(g.raw)) return g.color; + } + return folderColor(node.folderTop, false); + }; +} + +export const fallbackColorFn: NodeColorFn = (node) => folderColor(node.folderTop, node.unresolved); + +// Obsidian 标准色族 hsl(h, 60%, 60%) 的色相轮(与 Rick 的 9 组配色同族); +// M2 接 graph.json 真实 colorGroups,本表是无配置时的回退 +const HUES = [0, 40, 80, 120, 160, 200, 240, 280, 320]; + +const NEUTRAL = new Color('#9aa4b2'); // 未分组 +const UNRESOLVED = new Color('#7a8499'); // 幽灵 + +const cache = new Map(); + +export function folderColor(folderTop: string, unresolved: boolean): Color { + if (unresolved) return UNRESOLVED; + if (folderTop === '') return NEUTRAL; + let c = cache.get(folderTop); + if (!c) { + const hue = HUES[hash32(folderTop) % HUES.length] ?? 0; + c = new Color().setHSL(hue / 360, 0.6, 0.6); + cache.set(folderTop, c); + } + return c; +} + +/** 链接色:端点色 50/50 混合 → 去饱和 60% + 压亮度(NASA 细灰线,辉光由 bloom 给) */ +export function linkColor(a: Color, b: Color): Color { + const c = a.clone().lerp(b, 0.5); + const hsl = { h: 0, s: 0, l: 0 }; + c.getHSL(hsl); + c.setHSL(hsl.h, hsl.s * 0.4, Math.min(hsl.l, 0.35)); + return c; +} diff --git a/src/render/presets.ts b/src/render/presets.ts new file mode 100644 index 0000000..0bc8c23 --- /dev/null +++ b/src/render/presets.ts @@ -0,0 +1,41 @@ +// 双视觉方向的全部 token 集中在此(按 Rick 协议:跑起来看着选,G2 门定默认) + +export interface VisualTokens { + id: 'deep-space' | 'daylight'; + background: number; + starfield: boolean; + motes: boolean; // 晨昼的尘埃微粒(替代星空) + bloomEnabled: boolean; // 亮底辉光=雾霾,晨昼强制关 + lightMode: boolean; // 节点 shader 变体:墨水圆盘 + rim + /** 晨昼把 9 色相重定向到纸面对比度(保色相、压亮度) */ + nodeLightness: number | null; + linkInk: string | null; // 晨昼链接 = 铅笔线(统一墨色,不用端点混色) + linkOpacityScale: number; + panelClass: string; // 面板风格 class +} + +export const DEEP_SPACE: VisualTokens = { + id: 'deep-space', + background: 0x000003, + starfield: true, + motes: false, + bloomEnabled: true, + lightMode: false, + nodeLightness: null, + linkInk: null, + linkOpacityScale: 1, + panelClass: 'gx-theme-dark', +}; + +export const DAYLIGHT: VisualTokens = { + id: 'daylight', + background: 0xf6f4ef, // 暖纸底 + starfield: false, + motes: true, + bloomEnabled: false, + lightMode: true, + nodeLightness: 0.44, + linkInk: '#2e2a24', + linkOpacityScale: 0.65, + panelClass: 'gx-theme-light', +}; diff --git a/src/render/shaders.ts b/src/render/shaders.ts new file mode 100644 index 0000000..42f263a --- /dev/null +++ b/src/render/shaders.ts @@ -0,0 +1,47 @@ +// 节点 = 单次 draw call 的 THREE.Points + 发光球 shader(NASA「luminous orb」配方) +// 深空模式:白热核心 + 软边缘;浅色「晨昼」模式:实心墨水圆盘 + 深色 rim(bloom 关) +// aDim: 聚焦模式下非邻居淡出(0.12..1) + +export const NODE_VERTEX_SHADER = /* glsl */ ` +attribute float aSize; +attribute float aGhost; +attribute float aDim; +varying vec3 vColor; +varying float vGhost; +varying float vDim; +uniform float uPixelScale; // drawingBufferHeight / (2·tan(fov/2)) +uniform float uSizeMul; // 控制面板「节点大小」倍率 +uniform float uMaxPoint; // 设备像素钳制:穿行星团时防满屏大精灵打爆填充率(M3) + +void main() { + vColor = color; + vGhost = aGhost; + vDim = aDim; + vec4 mv = modelViewMatrix * vec4(position, 1.0); + gl_PointSize = min(aSize * uSizeMul * uPixelScale / max(-mv.z, 1.0), uMaxPoint); + gl_Position = projectionMatrix * mv; +} +`; + +export const NODE_FRAGMENT_SHADER = /* glsl */ ` +varying vec3 vColor; +varying float vGhost; +varying float vDim; +uniform float uLightMode; // 0 = 深空(白热核心),1 = 晨昼(墨水圆盘 + rim) + +void main() { + vec2 uv = gl_PointCoord - 0.5; + float d = length(uv); + + float core = smoothstep(0.18, 0.0, d) * 0.55 * (1.0 - vGhost) * (1.0 - uLightMode); + vec3 col = mix(vColor, vec3(1.0), core); + + // 晨昼:外缘 1px 深色 rim,让节点「坐在纸上」 + float rim = smoothstep(0.40, 0.46, d) * smoothstep(0.50, 0.46, d); + col = mix(col, col * 0.72, rim * uLightMode); + + float alpha = smoothstep(0.5, 0.42, d) * mix(1.0, 0.45, vGhost) * vDim; + if (alpha < 0.01) discard; + gl_FragColor = vec4(col, alpha); +} +`; diff --git a/src/render/starfield.ts b/src/render/starfield.ts new file mode 100644 index 0000000..62bdacb --- /dev/null +++ b/src/render/starfield.ts @@ -0,0 +1,120 @@ +import { BufferAttribute, BufferGeometry, Color, Group, Points, PointsMaterial } from 'three'; + +// 星空:3 个尺寸级 = 3 个 draw call(视觉规格 §1.2);球壳分布近似无穷远 +const CLASSES = [ + { count: 2600, size: 1.2 }, + { count: 900, size: 2.0 }, + { count: 250, size: 3.0 }, +]; + +const COOL_A = new Color('#9da8c4'); +const COOL_B = new Color('#ffffff'); +const WARM = new Color('#ffe9c9'); +const BLUE = new Color('#bfd3ff'); + +function mulberry(seed: number): () => number { + let a = seed >>> 0; + return () => { + a = (a + 0x6d2b79f5) >>> 0; + let t = a; + t = Math.imul(t ^ (t >>> 15), t | 1); + t ^= t + Math.imul(t ^ (t >>> 7), t | 61); + return ((t ^ (t >>> 14)) >>> 0) / 4294967296; + }; +} + +export function disposeStarfield(group: Group): void { + for (const child of group.children) { + const p = child as Points; + p.geometry.dispose(); + p.material.dispose(); + } +} + +/** + * 亮星眨眼(G2.5 反馈)。品味逻辑: + * 只有最大尺寸级的「真星」会眨——背景星不闪;同一时刻最多一颗; + * 泊松随机间隔 + 1.6s 正弦包络——像真夜空偶尔的大气闪烁,不是圣诞彩灯。 + */ +export class Twinkler { + private baseColors: Float32Array; + private attr: BufferAttribute; + private active: { index: number; t: number } | null = null; + private nextIn = 3; + + constructor( + private geometry: BufferGeometry, + private starCount: number, + ) { + this.attr = geometry.getAttribute('color') as BufferAttribute; + this.baseColors = new Float32Array(this.attr.array as Float32Array); + } + + /** freq:期望每分钟眨眼次数 ÷ 10(滑杆 0–2,0=关) */ + update(deltaS: number, freq: number): void { + if (this.active) { + this.active.t += deltaS; + const t = this.active.t; + const DUR = 1.6; + const arr = this.attr.array as Float32Array; + const i = this.active.index * 3; + const k = t >= DUR ? 1 : 1 + 2.2 * Math.sin((Math.PI * t) / DUR); + arr[i] = (this.baseColors[i] ?? 1) * k; + arr[i + 1] = (this.baseColors[i + 1] ?? 1) * k; + arr[i + 2] = (this.baseColors[i + 2] ?? 1) * k; + this.attr.needsUpdate = true; + if (t >= DUR) this.active = null; + return; + } + if (freq <= 0.01) return; + this.nextIn -= deltaS; + if (this.nextIn <= 0) { + this.active = { index: Math.floor(Math.random() * this.starCount), t: 0 }; + // 泊松间隔:均值 6/freq 秒(freq=0.5 → 平均 12s 一次) + this.nextIn = Math.min(Math.max(-Math.log(Math.random() + 1e-9) * (6 / freq), 1.5), 90); + } + } +} + +export function buildStarfield(shellRadius: number, scale = 1): { group: Group; twinkler: Twinkler } { + const group = new Group(); + const rand = mulberry(0x517cc1); + let twinkler: Twinkler | null = null; + for (const base of CLASSES) { + const cls = { count: Math.max(Math.round(base.count * scale), 50), size: base.size }; + const pos = new Float32Array(cls.count * 3); + const col = new Float32Array(cls.count * 3); + for (let i = 0; i < cls.count; i++) { + const theta = 2 * Math.PI * rand(); + const phi = Math.acos(2 * rand() - 1); + const r = shellRadius * (0.95 + 0.1 * rand()); + pos[i * 3] = r * Math.sin(phi) * Math.cos(theta); + pos[i * 3 + 1] = r * Math.sin(phi) * Math.sin(theta); + pos[i * 3 + 2] = r * Math.cos(phi); + + const pick = rand(); + const c = pick < 0.85 ? COOL_A.clone().lerp(COOL_B, rand()) : pick < 0.95 ? WARM.clone() : BLUE.clone(); + // 大星级里 ~3% 提到 HDR 亮度,独享 bloom —— 仅有的几颗「真星」 + if (cls.size >= 3.0 && rand() < 0.03) c.multiplyScalar(1.8); + col[i * 3] = c.r; + col[i * 3 + 1] = c.g; + col[i * 3 + 2] = c.b; + } + const geo = new BufferGeometry(); + geo.setAttribute('position', new BufferAttribute(pos, 3)); + geo.setAttribute('color', new BufferAttribute(col, 3)); + const mat = new PointsMaterial({ + size: cls.size, + sizeAttenuation: false, + vertexColors: true, + transparent: true, + opacity: 0.55, + depthWrite: false, + }); + const points = new Points(geo, mat); + points.renderOrder = -1; // 星空垫底 + group.add(points); + if (cls.size >= 3.0) twinkler = new Twinkler(geo, cls.count); // 只有「真星」级会眨眼 + } + return { group, twinkler: twinkler ?? new Twinkler(new BufferGeometry().setAttribute('color', new BufferAttribute(new Float32Array(3), 3)), 1) }; +} diff --git a/src/render/stylePresets.ts b/src/render/stylePresets.ts new file mode 100644 index 0000000..3ac16f3 --- /dev/null +++ b/src/render/stylePresets.ts @@ -0,0 +1,44 @@ +import type { BloomSettings, LookSettings, PhysicsSettings } from '../settings'; + +/** + * 风格预设 = 辉光 + 力学 + 外观 的成套参数(G2 反馈:给经典组合,新用户第一印象优先)。 + * 「银河」即出厂默认:扁平星盘靠 flatten(Y 轴额外向心力)实现——自然引斥力做不出盘。 + */ +export interface StylePreset { + id: string; + name: string; + bloom: BloomSettings; + physics: PhysicsSettings; + look: LookSettings; +} + +export const STYLE_PRESETS: StylePreset[] = [ + { + id: 'galaxy', + name: '银河', + bloom: { strength: 0.35, radius: 0.35, threshold: 0.22 }, + physics: { repel: 200, linkDistance: 70, linkStrength: 1, centerPull: 0.04, flatten: 0.3 }, + look: { nodeSize: 1, linkOpacity: 0.14, twinkle: 0.5, sizeBy: 'degree' }, + }, + { + id: 'nebula', + name: '星云', + bloom: { strength: 0.6, radius: 0.4, threshold: 0.18 }, + physics: { repel: 180, linkDistance: 80, linkStrength: 1, centerPull: 0.04, flatten: 0 }, + look: { nodeSize: 1, linkOpacity: 0.16, twinkle: 0.5, sizeBy: 'degree' }, + }, + { + id: 'minimal', + name: '极简', + bloom: { strength: 0, radius: 0.3, threshold: 0.3 }, + physics: { repel: 220, linkDistance: 80, linkStrength: 1, centerPull: 0.04, flatten: 0 }, + look: { nodeSize: 0.85, linkOpacity: 0.08, twinkle: 0.2, sizeBy: 'degree' }, + }, + { + id: 'fireworks', + name: '烟火', + bloom: { strength: 1.2, radius: 0.6, threshold: 0.1 }, + physics: { repel: 160, linkDistance: 60, linkStrength: 1.2, centerPull: 0.05, flatten: 0 }, + look: { nodeSize: 1.15, linkOpacity: 0.25, twinkle: 1.2, sizeBy: 'degree' }, + }, +]; diff --git a/src/settings.ts b/src/settings.ts new file mode 100644 index 0000000..0369beb --- /dev/null +++ b/src/settings.ts @@ -0,0 +1,344 @@ +export type ViewMode = 'radial2d' | 'map3d'; +export type Language = 'en' | 'zh'; + +export interface BloomSettings { + strength: number; + radius: number; + threshold: number; +} + +export interface PhysicsSettings { + repel: number; // Positive value; the layout uses the negative charge. + linkDistance: number; + linkStrength: number; + centerPull: number; + flatten: number; +} + +export type SizeBy = 'degree' | 'fileSize' | 'uniform'; + +export interface LookSettings { + nodeSize: number; + linkOpacity: number; + twinkle: number; + sizeBy: SizeBy; +} + +export type VisualPreset = 'deep-space' | 'adaptive'; + +export interface GalaxySettings { + bloom: BloomSettings; + physics: PhysicsSettings; + look: LookSettings; + cruise: boolean; + cruiseSpeed: number; + showUnresolved: boolean; + showOrphans: boolean; + colorTheme: string; + qualityOverride: 'auto' | 'high' | 'low' | 'mobile'; + preset: VisualPreset; + colorGroups: import('./settings/graphJsonImport').ColorGroup[]; + positionCache: Record; +} + +export type ColorScheme = 'auto' | 'day' | 'night'; +export type LabelVisibility = 'auto' | 'hover'; +export type HoverTargetMode = 'nodes' | 'links' | 'both'; +export type HoverHighlightMode = + | 'none' + | 'note-links' + | 'hierarchy-parents' + | 'hierarchy-direct-children' + | 'hierarchy-descendants' + | 'hierarchy-parents-direct' + | 'hierarchy-all'; +export type ExternalDetailMode = 'grouped' | 'selected' | 'exact'; + +export interface RadialSettings { + atlasDepth: number; + focusSiblingLimit: number; + linkLimit: number; + renderNodeLimit: number; + externalLinkAnchorLimit: number; + adaptiveDetail: boolean; + includeUnresolvedLinks: boolean; + showLinkOverlay: boolean; + showExternalLinks: boolean; + externalDetailMode: ExternalDetailMode; + colorScheme: ColorScheme; + labelVisibility: LabelVisibility; + hoverHighlightMode: HoverHighlightMode; + hoverTargetMode: HoverTargetMode; + swirlStrength: number; + hiddenLegendItems: string[]; + ignoreFolders: string[]; +} + +export interface MiniWorldMapSettings { + language: Language; + viewMode: ViewMode; + radial: RadialSettings; + galaxy3d: GalaxySettings; +} + +export const MAX_ATLAS_DEPTH = 80; +export const MAX_RENDER_NODE_LIMIT = 20_000; +export const MAX_LINK_LIMIT = 30_000; +export const MAX_EXTERNAL_LINK_ANCHOR_LIMIT = 20_000; +export const MAX_SWIRL_STRENGTH = 100; + +export const HOVER_HIGHLIGHT_MODE_OPTIONS: [HoverHighlightMode, string][] = [ + ['none', 'None'], + ['note-links', 'Note links'], + ['hierarchy-parents', 'Hierarchy parents'], + ['hierarchy-direct-children', 'Hierarchy direct children'], + ['hierarchy-descendants', 'Hierarchy all children'], + ['hierarchy-parents-direct', 'Hierarchy parents + direct'], + ['hierarchy-all', 'Hierarchy parents + all children'], +]; + +export const LABEL_VISIBILITY_OPTIONS: [LabelVisibility, string][] = [ + ['auto', 'Auto'], + ['hover', 'Hover only'], +]; + +export const HOVER_TARGET_MODE_OPTIONS: [HoverTargetMode, string][] = [ + ['nodes', 'Nodes'], + ['links', 'Links'], + ['both', 'Nodes + links'], +]; + +export const LEGEND_ITEM_DEFINITIONS: [string, string, string, string][] = [ + ['root', 'legend.root', 'legend.root.desc', 'mwm-legend-root'], + ['folder', 'legend.folder', 'legend.folder.desc', 'mwm-legend-folder'], + ['folder-meta', 'legend.folderMeta', 'legend.folderMeta.desc', 'mwm-legend-meta'], + ['file', 'legend.note', 'legend.note.desc', 'mwm-legend-note'], + ['outside', 'legend.outsideGroup', 'legend.outsideGroup.desc', 'mwm-legend-external'], + ['outside-file', 'legend.outsideNote', 'legend.outsideNote.desc', 'mwm-legend-outside-file'], + ['missing', 'legend.unresolvedNote', 'legend.unresolvedNote.desc', 'mwm-legend-unresolved'], + ['tree', 'legend.hierarchy', 'legend.hierarchy.desc', 'mwm-legend-tree'], + ['link', 'legend.noteLinks', 'legend.noteLinks.desc', 'mwm-legend-link'], + ['outside-link', 'legend.outsideLinks', 'legend.outsideLinks.desc', 'mwm-legend-link-external'], + ['dashed-link', 'legend.unresolvedLinks', 'legend.unresolvedLinks.desc', 'mwm-legend-link-unresolved'], +]; + +export const DEFAULT_RADIAL_SETTINGS: RadialSettings = { + atlasDepth: 6, + focusSiblingLimit: 160, + linkLimit: 1200, + renderNodeLimit: 4200, + externalLinkAnchorLimit: 700, + adaptiveDetail: true, + includeUnresolvedLinks: true, + showLinkOverlay: true, + showExternalLinks: true, + externalDetailMode: 'grouped', + colorScheme: 'auto', + labelVisibility: 'auto', + hoverHighlightMode: 'hierarchy-all', + hoverTargetMode: 'nodes', + swirlStrength: 0, + hiddenLegendItems: [], + ignoreFolders: ['.git', '.obsidian'], +}; + +export const DEFAULT_GALAXY_SETTINGS: GalaxySettings = { + bloom: { strength: 0.35, radius: 0.35, threshold: 0.22 }, + physics: { repel: 200, linkDistance: 70, linkStrength: 1, centerPull: 0.04, flatten: 0.3 }, + look: { nodeSize: 1, linkOpacity: 0.14, twinkle: 0.5, sizeBy: 'degree' }, + cruise: true, + cruiseSpeed: 1, + showUnresolved: false, + showOrphans: true, + colorTheme: 'imported', + qualityOverride: 'auto', + preset: 'adaptive', + colorGroups: [], + positionCache: {}, +}; + +export const DEFAULT_SETTINGS: MiniWorldMapSettings = { + language: 'en', + viewMode: 'radial2d', + radial: DEFAULT_RADIAL_SETTINGS, + galaxy3d: DEFAULT_GALAXY_SETTINGS, +}; + +export function mergeSettings(saved: unknown): MiniWorldMapSettings { + const raw = isRecord(saved) ? saved : {}; + const hasNestedRadial = isRecord(raw['radial']); + const radialSource = hasNestedRadial ? raw['radial'] : raw; + const galaxySource = isRecord(raw['galaxy3d']) ? raw['galaxy3d'] : raw; + const radial = mergeRadialSettings(radialSource); + if (!hasNestedRadial && raw['showLinkOverlay'] === false) radial.showLinkOverlay = DEFAULT_RADIAL_SETTINGS.showLinkOverlay; + return { + language: normalizeLanguage(raw['language'] ?? raw['locale'] ?? raw['lang']), + viewMode: normalizeViewMode(raw['viewMode']), + radial, + galaxy3d: mergeGalaxySettings(galaxySource), + }; +} + +export function mergeRadialSettings(saved: unknown): RadialSettings { + const s = isRecord(saved) ? saved : {}; + const d = DEFAULT_RADIAL_SETTINGS; + const legendIds = new Set(LEGEND_ITEM_DEFINITIONS.map(([id]) => id)); + const hoverMode = s['hoverHighlightMode'] ?? (s['enableLinkHover'] === true ? 'note-links' : d.hoverHighlightMode); + return { + atlasDepth: clampNumber(s['atlasDepth'], 1, MAX_ATLAS_DEPTH, d.atlasDepth), + focusSiblingLimit: clampNumber(s['focusSiblingLimit'], 10, 1000, d.focusSiblingLimit), + linkLimit: clampNumber(s['linkLimit'], 0, MAX_LINK_LIMIT, d.linkLimit), + renderNodeLimit: clampNumber(s['renderNodeLimit'], 200, MAX_RENDER_NODE_LIMIT, d.renderNodeLimit), + externalLinkAnchorLimit: clampNumber( + s['externalLinkAnchorLimit'], + 0, + MAX_EXTERNAL_LINK_ANCHOR_LIMIT, + d.externalLinkAnchorLimit, + ), + adaptiveDetail: typeof s['adaptiveDetail'] === 'boolean' ? s['adaptiveDetail'] : d.adaptiveDetail, + includeUnresolvedLinks: + typeof s['includeUnresolvedLinks'] === 'boolean' ? s['includeUnresolvedLinks'] : d.includeUnresolvedLinks, + showLinkOverlay: typeof s['showLinkOverlay'] === 'boolean' ? s['showLinkOverlay'] : d.showLinkOverlay, + showExternalLinks: typeof s['showExternalLinks'] === 'boolean' ? s['showExternalLinks'] : d.showExternalLinks, + externalDetailMode: normalizeExternalDetailMode(s['externalDetailMode']), + colorScheme: normalizeColorScheme(s['colorScheme']), + labelVisibility: normalizeLabelVisibility(s['labelVisibility']), + hoverHighlightMode: normalizeHoverHighlightMode(hoverMode), + hoverTargetMode: normalizeHoverTargetMode(s['hoverTargetMode']), + swirlStrength: clampNumber(s['swirlStrength'], 0, MAX_SWIRL_STRENGTH, d.swirlStrength), + hiddenLegendItems: Array.isArray(s['hiddenLegendItems']) + ? s['hiddenLegendItems'].filter((id): id is string => typeof id === 'string' && legendIds.has(id)) + : d.hiddenLegendItems.slice(), + ignoreFolders: Array.isArray(s['ignoreFolders']) + ? s['ignoreFolders'].filter((v): v is string => typeof v === 'string' && v.trim().length > 0) + : d.ignoreFolders.slice(), + }; +} + +export function mergeGalaxySettings(saved: unknown): GalaxySettings { + const s = isRecord(saved) ? saved : {}; + const d = DEFAULT_GALAXY_SETTINGS; + const bloom = isRecord(s['bloom']) ? s['bloom'] : {}; + const physics = isRecord(s['physics']) ? s['physics'] : {}; + const look = isRecord(s['look']) ? s['look'] : {}; + return { + bloom: { + strength: finiteNumber(bloom['strength'], d.bloom.strength), + radius: finiteNumber(bloom['radius'], d.bloom.radius), + threshold: finiteNumber(bloom['threshold'], d.bloom.threshold), + }, + physics: { + repel: finiteNumber(physics['repel'], d.physics.repel), + linkDistance: finiteNumber(physics['linkDistance'], d.physics.linkDistance), + linkStrength: finiteNumber(physics['linkStrength'], d.physics.linkStrength), + centerPull: finiteNumber(physics['centerPull'], d.physics.centerPull), + flatten: finiteNumber(physics['flatten'], d.physics.flatten), + }, + look: { + nodeSize: finiteNumber(look['nodeSize'], d.look.nodeSize), + linkOpacity: finiteNumber(look['linkOpacity'], d.look.linkOpacity), + twinkle: finiteNumber(look['twinkle'], d.look.twinkle), + sizeBy: (['degree', 'fileSize', 'uniform'] as const).includes(look['sizeBy'] as SizeBy) + ? (look['sizeBy'] as SizeBy) + : d.look.sizeBy, + }, + cruise: typeof s['cruise'] === 'boolean' ? s['cruise'] : d.cruise, + cruiseSpeed: finiteNumber(s['cruiseSpeed'], d.cruiseSpeed), + showUnresolved: + typeof s['showUnresolved'] === 'boolean' + ? s['showUnresolved'] + : typeof s['includeUnresolvedLinks'] === 'boolean' + ? s['includeUnresolvedLinks'] + : d.showUnresolved, + showOrphans: typeof s['showOrphans'] === 'boolean' ? s['showOrphans'] : d.showOrphans, + colorTheme: typeof s['colorTheme'] === 'string' ? s['colorTheme'] : d.colorTheme, + qualityOverride: (['auto', 'high', 'low', 'mobile'] as const).includes(s['qualityOverride'] as 'auto') + ? (s['qualityOverride'] as GalaxySettings['qualityOverride']) + : d.qualityOverride, + preset: s['preset'] === 'deep-space' ? 'deep-space' : 'adaptive', + colorGroups: Array.isArray(s['colorGroups']) + ? s['colorGroups'].filter( + (g): g is import('./settings/graphJsonImport').ColorGroup => + isRecord(g) && typeof g['query'] === 'string' && typeof g['color'] === 'string', + ) + : [], + positionCache: + isRecord(s['positionCache']) && !Array.isArray(s['positionCache']) + ? (s['positionCache'] as Record) + : {}, + }; +} + +export interface SettingsHost { + settings: MiniWorldMapSettings; + saveSettings(): Promise; + setViewMode(mode: ViewMode): void; + setLanguage(language: Language): void; +} + +export function toLayoutParams(p: PhysicsSettings): import('./types').LayoutParams { + return { + charge: -p.repel, + linkDistance: p.linkDistance, + linkStrength: p.linkStrength, + centerPull: p.centerPull, + flatten: p.flatten, + velocityDecay: 0.6, + }; +} + +export function normalizeViewMode(value: unknown): ViewMode { + return value === 'map3d' || value === 'radial2d' ? value : DEFAULT_SETTINGS.viewMode; +} + +export function normalizeLanguage(value: unknown): Language { + return value === 'zh' ? 'zh' : DEFAULT_SETTINGS.language; +} + +export function normalizeColorScheme(value: unknown): ColorScheme { + return value === 'auto' || value === 'day' || value === 'night' ? value : DEFAULT_RADIAL_SETTINGS.colorScheme; +} + +export function normalizeLabelVisibility(value: unknown): LabelVisibility { + return value === 'hover' ? 'hover' : DEFAULT_RADIAL_SETTINGS.labelVisibility; +} + +export function normalizeHoverHighlightMode(value: unknown): HoverHighlightMode { + return HOVER_HIGHLIGHT_MODE_OPTIONS.some(([id]) => id === value) + ? (value as HoverHighlightMode) + : DEFAULT_RADIAL_SETTINGS.hoverHighlightMode; +} + +export function normalizeHoverTargetMode(value: unknown): HoverTargetMode { + return HOVER_TARGET_MODE_OPTIONS.some(([id]) => id === value) + ? (value as HoverTargetMode) + : DEFAULT_RADIAL_SETTINGS.hoverTargetMode; +} + +export function hoverHighlightsNoteLinks(mode: unknown): boolean { + return normalizeHoverHighlightMode(mode) === 'note-links'; +} + +export function hoverHighlightModeLabel(mode: unknown): string { + const normalized = normalizeHoverHighlightMode(mode); + return HOVER_HIGHLIGHT_MODE_OPTIONS.find(([id]) => id === normalized)?.[1].toLowerCase() ?? normalized; +} + +export function normalizeExternalDetailMode(value: unknown): ExternalDetailMode { + return value === 'selected' || value === 'exact' || value === 'grouped' + ? value + : DEFAULT_RADIAL_SETTINGS.externalDetailMode; +} + +export function clampNumber(value: unknown, min: number, max: number, fallback: number): number { + const n = finiteNumber(value, fallback); + return Math.min(Math.max(n, min), max); +} + +function finiteNumber(value: unknown, fallback: number): number { + return typeof value === 'number' && Number.isFinite(value) ? value : Number.parseFloat(String(value)) || fallback; +} + +function isRecord(value: unknown): value is Record { + return typeof value === 'object' && value !== null; +} diff --git a/src/settings/graphJsonImport.ts b/src/settings/graphJsonImport.ts new file mode 100644 index 0000000..ceda4b3 --- /dev/null +++ b/src/settings/graphJsonImport.ts @@ -0,0 +1,31 @@ +import type { App } from 'obsidian'; +import { normalizePath } from 'obsidian'; + +export interface ColorGroup { + query: string; // 已 trim(真实配置带尾随空格) + color: string; // #rrggbb +} + +/** + * 读自带图谱的 colorGroups(.obsidian/graph.json,未文档化格式——尽力解析,失败回 null)。 + * 颜色存的是十进制 int;query 形如 "path:01学习 "(注意尾随空格)。只读,永不回写。 + */ +export async function readGraphColorGroups(app: App): Promise { + try { + const path = normalizePath(app.vault.configDir + '/graph.json'); + if (!(await app.vault.adapter.exists(path))) return null; + const parsed = JSON.parse(await app.vault.adapter.read(path)) as { + colorGroups?: { query?: unknown; color?: { rgb?: unknown } }[]; + }; + const groups: ColorGroup[] = []; + for (const g of parsed.colorGroups ?? []) { + const query = typeof g.query === 'string' ? g.query.trim() : ''; + const rgb = g.color?.rgb; + if (!query || typeof rgb !== 'number') continue; + groups.push({ query, color: `#${(rgb & 0xffffff).toString(16).padStart(6, '0')}` }); + } + return groups; + } catch { + return null; + } +} diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 0000000..c92b30e --- /dev/null +++ b/src/types.ts @@ -0,0 +1,47 @@ +export interface GraphNode { + id: string; // vault path;未解析为 "unresolved:<名字>" + name: string; + folderTop: string; // 顶层文件夹;根目录 '';未解析 '__unresolved__' + degree: number; // 出 + 入 + inDegree: number; + outDegree: number; + fileSize: number; // 字节;未解析为 0(「质量」可选依据) + unresolved: boolean; +} + +/** 边用节点数组下标表示——聚合渲染按索引 gather 坐标 */ +export interface GraphLink { + source: number; + target: number; +} + +export interface GraphData { + nodes: GraphNode[]; + links: GraphLink[]; +} + +export interface LayoutParams { + charge: number; // 负值=斥力 + linkDistance: number; + linkStrength: number; // 倍率:1 = d3 默认(1/min(端点度数)) + centerPull: number; // forceX/Y/Z 强度,防孤儿飞逸 + flatten: number; // 0=自然球体;>0 在 Y 轴额外加压 → 银河盘(自然引斥力做不出盘,这是必要的额外力) + velocityDecay: number; +} + +export interface FrameStats { + frames: number; + avgFps: number; + p95FrameMs: number; + worstFrameMs: number; + durationMs: number; +} + +export interface BenchResult { + scenario: string; + timestamp: string; + nodes: number; + links: number; + bloom: boolean; + [key: string]: unknown; +} diff --git a/src/typings/d3-force-3d.d.ts b/src/typings/d3-force-3d.d.ts new file mode 100644 index 0000000..d1d40bc --- /dev/null +++ b/src/typings/d3-force-3d.d.ts @@ -0,0 +1,90 @@ +declare module 'd3-force-3d' { + export interface SimNode { + index?: number; + x?: number; + y?: number; + z?: number; + vx?: number; + vy?: number; + vz?: number; + fx?: number | null; + fy?: number | null; + fz?: number | null; + } + export interface SimLink { + source: number | N; + target: number | N; + index?: number; + } + + export interface Force { + (alpha: number): void; + initialize?(nodes: N[], random: () => number, nDim: number): void; + } + + export interface Simulation { + tick(iterations?: number): this; + restart(): this; + stop(): this; + nodes(): N[]; + nodes(nodes: N[]): this; + alpha(): number; + alpha(alpha: number): this; + alphaMin(): number; + alphaMin(min: number): this; + alphaDecay(): number; + alphaDecay(decay: number): this; + alphaTarget(): number; + alphaTarget(target: number): this; + velocityDecay(): number; + velocityDecay(decay: number): this; + force(name: string): Force | undefined; + force(name: string, force: Force | null): this; + on(typenames: string, listener: ((this: this) => void) | null): this; + } + + export interface LinkForce extends Force { + links(): SimLink[]; + links(links: SimLink[]): this; + distance(d: number | ((link: SimLink) => number)): this; + strength(s: number | ((link: SimLink) => number)): this; + } + + export interface ManyBodyForce extends Force { + strength(s: number | ((node: N) => number)): this; + theta(t: number): this; + distanceMax(d: number): this; + } + + export interface PositionForce extends Force { + strength(s: number | ((node: N) => number)): this; + x?(v: number): this; + y?(v: number): this; + z?(v: number): this; + } + + export interface CenterForce extends Force { + x(v: number): this; + y(v: number): this; + z(v: number): this; + strength(s: number): this; + } + + export function forceSimulation( + nodes?: N[], + numDimensions?: 1 | 2 | 3, + ): Simulation; + export function forceLink(links?: SimLink[]): LinkForce; + export function forceManyBody(): ManyBodyForce; + export function forceCenter(x?: number, y?: number, z?: number): CenterForce; + export function forceX(x?: number): PositionForce; + export function forceY(y?: number): PositionForce; + export function forceZ(z?: number): PositionForce; + export function forceCollide(radius?: number): Force; + export function forceRadial( + radius: number, + x?: number, + y?: number, + z?: number, + ): Force; +} diff --git a/src/typings/galaxy-dev.d.ts b/src/typings/galaxy-dev.d.ts new file mode 100644 index 0000000..295eb9a --- /dev/null +++ b/src/typings/galaxy-dev.d.ts @@ -0,0 +1,2 @@ +// esbuild define:dev 构建 true,商店/release 构建 false(基准等开发工具被摇树剔除) +declare const __GALAXY_DEV__: boolean; diff --git a/src/typings/inline-worker.d.ts b/src/typings/inline-worker.d.ts new file mode 100644 index 0000000..3c748e0 --- /dev/null +++ b/src/typings/inline-worker.d.ts @@ -0,0 +1,5 @@ +// esbuild inline-worker 插件:'worker:' 前缀的导入被打包成 IIFE 文本字符串 +declare module 'worker:*' { + const source: string; + export default source; +} diff --git a/src/view/GalaxyView.ts b/src/view/GalaxyView.ts new file mode 100644 index 0000000..44b5ccc --- /dev/null +++ b/src/view/GalaxyView.ts @@ -0,0 +1,106 @@ +import { ItemView, WorkspaceLeaf } from 'obsidian'; +import { VIEW_TYPE_MINI_WORLD_MAP } from '../constants'; +import type { SettingsHost, ViewMode } from '../settings'; +import { Map3DController } from './Map3DController'; +import { Radial2DController } from './Radial2DController'; + +type ActiveController = Radial2DController | Map3DController; + +export class MiniWorldMapView extends ItemView { + navigation = true; + controller: ActiveController | null = null; + + constructor( + leaf: WorkspaceLeaf, + private host: SettingsHost, + ) { + super(leaf); + } + + getViewType(): string { + return VIEW_TYPE_MINI_WORLD_MAP; + } + + getDisplayText(): string { + return 'Mini World Map'; + } + + getIcon(): string { + return 'network'; + } + + get counts(): { nodes: number; links: number } { + return this.controller?.counts ?? { nodes: 0, links: 0 }; + } + + async onOpen(): Promise { + this.contentEl.empty(); + this.contentEl.addClass('mini-world-map-view', 'galaxy-view-content'); + this.registerEvent(this.app.workspace.on('css-change', () => this.controller?.onCssChange?.())); + this.tryInit(); + } + + onResize(): void { + if (!this.controller) { + this.tryInit(); + return; + } + this.controller.resize(); + } + + switchMode(mode: ViewMode): void { + if (this.host.settings.viewMode !== mode) { + this.host.setViewMode(mode); + return; + } + this.rebuild(); + } + + private tryInit(): void { + if (this.controller) return; + const { clientWidth: width, clientHeight: height } = this.contentEl; + if (width < 10 || height < 10) return; + if (this.host.settings.viewMode === 'map3d') { + const controller = new Map3DController( + this.app, + this.contentEl, + this.host.settings.galaxy3d, + () => void this.host.saveSettings(), + (mode) => this.switchMode(mode), + this.host.settings.language, + (language) => this.host.setLanguage(language), + ); + controller.onContextLost = () => this.rebuild(); + this.controller = controller; + this.addChild(controller.store); + void controller.start(); + } else { + const controller = new Radial2DController( + this.app, + this.contentEl, + this.host.settings, + () => void this.host.saveSettings(), + (mode) => this.switchMode(mode), + (language) => this.host.setLanguage(language), + ); + this.controller = controller; + this.addChild(controller); + void controller.start(); + } + } + + private rebuild(): void { + this.controller?.dispose(); + this.controller = null; + this.contentEl.empty(); + this.tryInit(); + } + + async onClose(): Promise { + this.controller?.dispose(); + this.controller = null; + this.contentEl.empty(); + } +} + +export { MiniWorldMapView as GalaxyView }; diff --git a/src/view/GraphController.ts b/src/view/GraphController.ts new file mode 100644 index 0000000..1c3dc29 --- /dev/null +++ b/src/view/GraphController.ts @@ -0,0 +1,779 @@ +import type { App } from 'obsidian'; +import { Menu, Notice, Platform, debounce, setIcon } from 'obsidian'; +import { Spherical, Vector3 } from 'three'; +import type { BenchResult } from '../types'; +import type { GalaxySettings, Language, ViewMode } from '../settings'; +import { DEFAULT_GALAXY_SETTINGS, toLayoutParams } from '../settings'; +import { languageOptions, t } from '../i18n'; +import { readGraphColorGroups } from '../settings/graphJsonImport'; +import type { ColorTheme } from '../render/colorThemes'; +import { GraphStore } from '../data/GraphStore'; +import { seedRadius } from '../data/seed'; +import type { LayoutEngine } from '../layout/LayoutEngine'; +import { MainThreadForceLayout } from '../layout/MainThreadForceLayout'; +import { WorkerForceLayout } from '../layout/WorkerForceLayout'; +import { AggregateRenderer } from '../render/AggregateRenderer'; +import { makeNodeColorFn, fallbackColorFn } from '../render/palette'; +import { DAYLIGHT, DEEP_SPACE } from '../render/presets'; +import { CameraDirector } from '../interactions/CameraDirector'; +import { ControlPanel } from '../overlay/ControlPanel'; +import { OverlayManager } from '../overlay/OverlayManager'; +import { NodeSearchModal } from './SearchModal'; +import { collectFrames, observeLongTasks, writeBenchResult, sleep } from '../bench/bench'; +import type { QualityTier } from '../quality/tiers'; +import { TIERS } from '../quality/tiers'; + +const WARM_CACHE_MIN_COVERAGE = 0.8; +const ESTABLISHING_MS = 3200; + +/** + * 唯一的组装点:Store → Layout → Renderer → Director → Overlay → Panel。 + * 自有 rAF 循环:布局热时每帧 1 tick;沉降后零上传。 + */ +export class GraphController { + readonly store: GraphStore; + private layout: LayoutEngine = new WorkerForceLayout(); + private renderer: AggregateRenderer | null = null; + private director: CameraDirector | null = null; + private overlay: OverlayManager | null = null; + private panel: ControlPanel | null = null; + + private rafId = 0; + private lastNow = 0; + private paused = false; + private visible = true; + private benchMode = false; + private benchRunning = false; + private selected = -1; + private graphRadius = 200; + private wasSettled = false; + private shot: { t0: number; durMs: number; fromBloom: number } | null = null; + private maskEl: HTMLElement | null = null; + + private hudFrames: number[] = []; + private intersection: IntersectionObserver | null = null; + private disposeFns: (() => void)[] = []; + private saveSoon: () => void; + private tier: QualityTier = TIERS.high; + private watchdogTripped = false; + private lowFpsChecks = 0; + private lastWatchdogAt = 0; + /** WebGL 上下文丢失时由视图重建(GalaxyView 注入) */ + onContextLost: (() => void) | null = null; + + constructor( + private app: App, + private contentEl: HTMLElement, + private settings: GalaxySettings, + saveSettings: () => void, + private onViewMode: ((mode: ViewMode) => void) | null = null, + private language: Language = 'en', + private onLanguage: ((language: Language) => void) | null = null, + ) { + this.store = new GraphStore(app); + this.saveSoon = debounce(saveSettings, 800, true); + } + + get counts(): { nodes: number; links: number } { + return { nodes: this.store.data.nodes.length, links: this.store.data.links.length }; + } + + async start(): Promise { + await this.store.ensureCacheReady(); + this.store.init(this.settings.showUnresolved, this.settings.showOrphans, () => this.onDataChanged()); + this.store.rebuild(false); + + // 暖启动:用上次沉降坐标覆盖种子 → 重开即成形 + const coverage = this.applyPositionCache(); + const warm = coverage >= WARM_CACHE_MIN_COVERAGE; + + const container = this.contentEl.createDiv({ cls: 'galaxy-view-canvas' }); + this.graphRadius = seedRadius(this.store.data.nodes.length) * 1.6; + const renderer = new AggregateRenderer(container, this.graphRadius); + this.renderer = renderer; + renderer.setColorFn( + this.settings.colorGroups.length > 0 ? makeNodeColorFn(this.settings.colorGroups) : fallbackColorFn, + ); + renderer.setData(this.store.data, this.store.positions); + this.initLayout(warm ? 0.06 : 1); + + this.director = new CameraDirector(renderer.camera, renderer.renderer.domElement, { + onFlyToSelected: () => this.flyToSelected(), + onResetView: () => this.recenter(), + }); + + this.overlay = new OverlayManager(this.contentEl, this.app, renderer, { + openNote: (id) => void this.app.workspace.openLinkText(id, '', true), + focusNode: (i) => this.selectNode(i, true), + }, this.language); + this.overlay.setData(this.store.data, this.graphRadius); + + this.applySettings(); + this.applyTier(); + this.buildPanel(); + this.buildFloatingControls(); + this.applyPreset(); + this.bindPicking(renderer.renderer.domElement); + this.bindContextLost(renderer.renderer.domElement); + this.bindVisibility(); + this.resize(); + + // 首次导入 2D 配色(仅当从未导入过) + if (this.settings.colorGroups.length === 0) void this.importColors(false); + + // 开场:暖启动走「拉出式」开场镜头;冷启动直接看星系成形(本身就是剧场) + if (warm) this.playEstablishing(); + else this.director.setInitialFraming(this.graphRadius); + + this.lastNow = performance.now(); + const loop = (now: number) => { + const deltaS = Math.min((now - this.lastNow) / 1000, 0.1); + this.lastNow = now; + if (!this.paused) { + if (this.layout.step()) this.renderer?.updatePositions(); + this.checkSettled(); + if (!this.benchMode) this.director?.update(now, deltaS); + this.stepShot(now); + this.renderer?.render(deltaS); + const { clientWidth: w, clientHeight: h } = this.contentEl; + this.overlay?.update(w, h); + } + this.updateHud(now); + this.watchdog(now); + this.rafId = window.requestAnimationFrame(loop); + }; + this.rafId = window.requestAnimationFrame(loop); + } + + /** Electron GPU 重置 → 遮罩 + 一键重建(前人插件的隐性死因之一) */ + private bindContextLost(canvas: HTMLElement): void { + const onLost = (e: Event) => { + e.preventDefault(); + const mask = this.contentEl.createDiv({ cls: 'gx-mask' }); + const btn = mask.createEl('button', { cls: 'gx-mask-btn', text: this.tt('3d.contextLost') }); + btn.addEventListener('click', () => this.onContextLost?.()); + }; + canvas.addEventListener('webglcontextlost', onLost); + this.disposeFns.push(() => canvas.removeEventListener('webglcontextlost', onLost)); + } + + resize(): void { + const { clientWidth: w, clientHeight: h } = this.contentEl; + this.renderer?.resize(w, h); + } + + // ---------- 暖启动与开场镜头 ---------- + + private applyPositionCache(): number { + const cache = this.settings.positionCache; + const nodes = this.store.data.nodes; + if (nodes.length === 0) return 0; + let hits = 0; + nodes.forEach((n, i) => { + const p = cache[n.id]; + if (!p) return; + this.store.positions[i * 3] = p[0]; + this.store.positions[i * 3 + 1] = p[1]; + this.store.positions[i * 3 + 2] = p[2]; + hits++; + }); + return hits / nodes.length; + } + + private checkSettled(): void { + const settled = this.layout.isSettled(); + if (settled && !this.wasSettled) { + // 沉降时刻:写暖启动缓存(坐标取整 1 位小数,控制 data.json 体积) + const cache: Record = {}; + const pos = this.store.positions; + this.store.data.nodes.forEach((n, i) => { + cache[n.id] = [ + Math.round((pos[i * 3] ?? 0) * 10) / 10, + Math.round((pos[i * 3 + 1] ?? 0) * 10) / 10, + Math.round((pos[i * 3 + 2] ?? 0) * 10) / 10, + ]; + }); + this.settings.positionCache = cache; + this.saveSoon(); + } + this.wasSettled = settled; + } + + private playEstablishing(): void { + const renderer = this.renderer; + const director = this.director; + if (!renderer || !director) return; + this.maskEl = this.contentEl.createDiv({ cls: 'gx-mask' }); + this.maskEl.createDiv({ cls: 'gx-mask-text', text: this.tt('loading.3d') }); + // 等几帧让首批渲染就绪,再揭幕拉出 + window.setTimeout(() => { + if (!this.maskEl) return; + this.maskEl.addClass('is-fading'); + window.setTimeout(() => { + this.maskEl?.remove(); + this.maskEl = null; + }, 650); + const inner = this.graphRadius * 0.5; + const elev = (10 * Math.PI) / 180; + renderer.camera.position.set(inner * Math.cos(elev), inner * Math.sin(elev), inner * 0.2); + director.target.set(0, 0, 0); + director.resetView(this.graphRadius, () => director.beginFocusOrbit(null)); // 内部 → 总览 → 即时巡航 + renderer.playReveal(2600); // 创世动画:节点从中心波次绽放(G2.5 反馈) + this.shot = { t0: performance.now(), durMs: ESTABLISHING_MS, fromBloom: this.settings.bloom.strength * 1.8 }; + }, 450); + } + + /** 开场期间辉光从 1.8× 回落到设置值(NASA「明亮诞生」) */ + private stepShot(now: number): void { + if (!this.shot || !this.renderer) return; + const t = Math.min((now - this.shot.t0) / this.shot.durMs, 1); + const v = this.shot.fromBloom + (this.settings.bloom.strength - this.shot.fromBloom) * t; + this.renderer.setBloomStrength(v); + if (t >= 1) this.shot = null; + } + + // ---------- 数据 ---------- + + private onDataChanged(): void { + if (!this.renderer) return; + this.clearSelection(); + this.renderer.setData(this.store.data, this.store.positions); + this.overlay?.setData(this.store.data, this.graphRadius); + // 身份保持合并已保住旧坐标,低温重热让新节点滑入而不是全图爆炸 + this.initLayout(0.3); + this.wasSettled = false; + } + + /** Worker 优先,创建失败(罕见环境)回退主线程实现 */ + private initLayout(initialAlpha: number): void { + const params = toLayoutParams(this.settings.physics); + try { + this.layout.init(this.store.data, this.store.positions, params, initialAlpha); + } catch { + if (this.layout instanceof MainThreadForceLayout) throw new Error('layout init failed'); + this.layout.dispose(); + this.layout = new MainThreadForceLayout(); + this.layout.init(this.store.data, this.store.positions, params, initialAlpha); + new Notice(this.tt('3d.workerFallback')); + } + } + + // ---------- 质量档位(M4) ---------- + + /** Platform.isMobile 硬上限;手动覆盖绝对优先;auto=high+看门狗 */ + private pickTier(): QualityTier { + if (Platform.isMobile) return TIERS.mobile; + const o = this.settings.qualityOverride; + if (o === 'high' || o === 'low' || o === 'mobile') return TIERS[o]; + return this.watchdogTripped ? TIERS.low : TIERS.high; + } + + applyTier(): void { + const prev = this.tier.id; + this.tier = this.pickTier(); + this.renderer?.applyTier(this.tier, this.settings.bloom.strength); + this.overlay?.setBudgets(this.tier.hubLabels, this.tier.neighborLabels, this.tier.id === 'mobile'); + this.contentEl.toggleClass('gx-mobile', this.tier.id === 'mobile'); + const total = this.app.vault.getMarkdownFiles().length; + this.store.setCaps(this.tier.nodeCap, this.tier.linkCap); // 变化时触发重建 + if (this.tier.nodeCap !== null && total > this.tier.nodeCap && prev !== this.tier.id) { + new Notice(this.tt('3d.mobileCap', { cap: this.tier.nodeCap, total })); + } + } + + /** 沉降后 FPS 看门狗:连续 3 次 5s 采样 <30fps → 单向降到 low(会话内不回升) */ + private watchdog(now: number): void { + if (this.watchdogTripped || Platform.isMobile) return; + if (this.settings.qualityOverride !== 'auto' || !this.layout.isSettled() || this.benchRunning) return; + if (now - this.lastWatchdogAt < 5000) return; + this.lastWatchdogAt = now; + if (this.hudFrames.length > 0 && this.hudFrames.length < 30 && !this.paused) { + this.lowFpsChecks++; + if (this.lowFpsChecks >= 3) { + this.watchdogTripped = true; + this.applyTier(); + new Notice(this.tt('3d.performanceMode')); + } + } else { + this.lowFpsChecks = 0; + } + } + + // ---------- 设置与视觉方向 ---------- + + private applySettings(): void { + const s = this.settings; + this.renderer?.setBloomParams(s.bloom); + this.renderer?.setNodeScale(s.look.nodeSize); + this.renderer?.setLinkOpacity(s.look.linkOpacity); + this.renderer?.setSizeMode(s.look.sizeBy); + if (this.renderer) this.renderer.twinkleFreq = s.look.twinkle; + if (this.director) { + this.director.cruiseEnabled = s.cruise; + this.director.cruiseSpeed = s.cruiseSpeed; + } + } + + /** 风格预设 = 辉光+力学+外观 成套切换 */ + applyStylePreset(p: { bloom: typeof DEFAULT_GALAXY_SETTINGS.bloom; physics: typeof DEFAULT_GALAXY_SETTINGS.physics; look: typeof DEFAULT_GALAXY_SETTINGS.look }): void { + Object.assign(this.settings.bloom, p.bloom); + Object.assign(this.settings.physics, p.physics); + Object.assign(this.settings.look, p.look); + this.applySettings(); + this.layout.updateParams(toLayoutParams(this.settings.physics)); + this.wasSettled = false; + this.saveSoon(); + } + + /** 回中心:清选中 + 平滑回总览 + 到达即绕全局中心巡航 */ + recenter(): void { + this.clearSelection(); + this.director?.resetView(this.graphRadius, () => this.director?.beginFocusOrbit(null)); + } + + /** 应用配色主题:按序染给现有颜色组(无组则按节点数从顶层文件夹生成) */ + applyColorTheme(theme: ColorTheme): void { + let groups = this.settings.colorGroups; + if (groups.length === 0) { + const byFolder = new Map(); + for (const n of this.store.data.nodes) { + if (n.folderTop && !n.unresolved) byFolder.set(n.folderTop, (byFolder.get(n.folderTop) ?? 0) + 1); + } + groups = [...byFolder.entries()] + .sort((a, b) => b[1] - a[1]) + .slice(0, 9) + .map(([folder]) => ({ query: `path:${folder}`, color: '#9aa4b2' })); + this.settings.colorGroups = groups; + } + groups.forEach((g, i) => (g.color = theme.colors[i % theme.colors.length] ?? g.color)); + this.settings.colorTheme = theme.id; + this.renderer?.setColorFn(makeNodeColorFn(groups)); + this.renderer?.recolor(); + this.saveSoon(); + } + + /** 手动触发创世动画(坐标未沉降时给提示) */ + playRevealManually(): void { + if (!this.layout.isSettled()) { + new Notice(this.tt('3d.revealWait')); + return; + } + this.renderer?.playReveal(); + } + + /** 在已导入的颜色组之间洗牌(同组不变,颜色互换) */ + shuffleColors(): void { + const groups = this.settings.colorGroups; + if (groups.length < 2) { + new Notice(this.tt('3d.shuffleMissing')); + return; + } + const colors = groups.map((g) => g.color); + for (let i = colors.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)); + [colors[i], colors[j]] = [colors[j]!, colors[i]!]; + } + groups.forEach((g, i) => (g.color = colors[i] ?? g.color)); + this.settings.colorTheme = 'custom'; + this.renderer?.setColorFn(makeNodeColorFn(groups)); + this.renderer?.recolor(); + this.saveSoon(); + } + + /** preset + app 主题 → tokens(adaptive 深色与深空共用场景) */ + applyPreset(): void { + if (!this.renderer) return; + const isDark = activeDocument.body.hasClass('theme-dark'); + const tokens = this.settings.preset === 'deep-space' || isDark ? DEEP_SPACE : DAYLIGHT; + this.renderer.applyTokens(tokens, this.settings.bloom.strength); + this.panel?.setPanelTheme(tokens.id === 'daylight' ? 'gx-theme-light' : 'gx-theme-dark'); + this.contentEl.toggleClass('gx-daylight', tokens.id === 'daylight'); + } + + /** workspace css-change(由视图转发) */ + onCssChange(): void { + if (this.settings.preset === 'adaptive') this.applyPreset(); + } + + private async importColors(notify: boolean): Promise { + const groups = await readGraphColorGroups(this.app); + if (!groups || groups.length === 0) { + if (notify) new Notice(this.tt('3d.importMissing')); + return; + } + this.settings.colorGroups = groups; + this.renderer?.setColorFn(makeNodeColorFn(groups)); + this.renderer?.recolor(); + this.saveSoon(); + if (notify) new Notice(this.tt('3d.importDone', { count: groups.length })); + } + + // ---------- 选中 / 聚焦 / 搜索 ---------- + + openSearch(): void { + new NodeSearchModal(this.app, this.store.data.nodes, (i) => this.selectNode(i, true), this.language).open(); + } + + selectNode(index: number, fly: boolean): void { + const renderer = this.renderer; + const director = this.director; + if (!renderer || !director) return; + this.selected = index; + const neighbors = new Set(); + const linkIdx: number[] = []; + this.store.data.links.forEach((l, li) => { + if (l.source === index || l.target === index) { + neighbors.add(l.source); + neighbors.add(l.target); + linkIdx.push(li); + } + }); + renderer.setFocus(index, neighbors); + renderer.setSelectedLinks(linkIdx); + this.overlay?.setSelection(index, neighbors); + if (fly) { + const pos = renderer.nodePosition(index, new Vector3()); + // 邻居质心方向:到达后环绕优先扫过链接密集的一侧 + const density = new Vector3(); + let count = 0; + const tmp = new Vector3(); + for (const ni of neighbors) { + if (ni === index) continue; + density.add(renderer.nodePosition(ni, tmp)); + count++; + } + const densityDir = count > 0 ? density.divideScalar(count).sub(pos) : null; + director.flyTo(pos, renderer.nodeRadius(index), () => director.beginFocusOrbit(densityDir)); + } + } + + clearSelection(): void { + this.selected = -1; + this.renderer?.setFocus(-1, null); + this.renderer?.setSelectedLinks([]); + this.overlay?.setSelection(-1, new Set()); + } + + private flyToSelected(): void { + if (this.selected < 0 || !this.renderer || !this.director) return; + const pos = this.renderer.nodePosition(this.selected, new Vector3()); + this.director.flyTo(pos, this.renderer.nodeRadius(this.selected)); + } + + // ---------- 拾取 ---------- + + private bindPicking(dom: HTMLElement): void { + let downX = 0; + let downY = 0; + const onDown = (e: PointerEvent) => { + downX = e.clientX; + downY = e.clientY; + }; + const onUp = (e: PointerEvent) => { + if (e.button !== 0 || e.ctrlKey || e.metaKey) return; // 平移手势不选中 + if (Math.hypot(e.clientX - downX, e.clientY - downY) > 5) return; + const rect = dom.getBoundingClientRect(); + const i = this.renderer?.pickNearest(e.clientX - rect.left, e.clientY - rect.top, rect.width, rect.height, 14) ?? -1; + if (i >= 0) this.selectNode(i, true); + else this.clearSelection(); + }; + let hoverPending = false; + const onMove = (e: PointerEvent) => { + const throttle = this.tier.hoverThrottleMs; + if (throttle === null || hoverPending) return; // 移动档:仅 tap,无 hover + hoverPending = true; + window.setTimeout(() => { + hoverPending = false; + const renderer = this.renderer; + if (!renderer) return; + const rect = dom.getBoundingClientRect(); + const i = renderer.pickNearest(e.clientX - rect.left, e.clientY - rect.top, rect.width, rect.height, 10); + this.overlay?.setHover(i); + dom.style.cursor = i >= 0 ? 'pointer' : 'default'; + }, throttle); + }; + const onKey = (e: KeyboardEvent) => { + if (e.key === 'Escape') { + this.clearSelection(); + e.preventDefault(); + } + }; + dom.addEventListener('pointerdown', onDown); + dom.addEventListener('pointerup', onUp); + dom.addEventListener('pointermove', onMove); + dom.addEventListener('keydown', onKey); + this.disposeFns.push(() => { + dom.removeEventListener('pointerdown', onDown); + dom.removeEventListener('pointerup', onUp); + dom.removeEventListener('pointermove', onMove); + dom.removeEventListener('keydown', onKey); + }); + } + + // ---------- 可见性暂停 ---------- + + private bindVisibility(): void { + const onVis = () => { + this.paused = activeDocument.hidden || !this.visible; + }; + activeDocument.addEventListener('visibilitychange', onVis); + this.disposeFns.push(() => activeDocument.removeEventListener('visibilitychange', onVis)); + this.intersection = new IntersectionObserver((entries) => { + this.visible = entries[0]?.isIntersecting ?? true; + onVis(); + }); + this.intersection.observe(this.contentEl); + } + + // ---------- 控制面板 ---------- + + private buildPanel(): void { + this.panel = new ControlPanel(this.contentEl, this.settings, this.language, { + onViewMode: (mode) => this.onViewMode?.(mode), + onBloom: () => { + this.renderer?.setBloomParams(this.settings.bloom); + this.saveSoon(); + }, + onPhysics: () => { + this.layout.updateParams(toLayoutParams(this.settings.physics)); + this.wasSettled = false; + this.saveSoon(); + }, + onLook: () => { + this.renderer?.setNodeScale(this.settings.look.nodeSize); + this.renderer?.setLinkOpacity(this.settings.look.linkOpacity); + if (this.renderer) this.renderer.twinkleFreq = this.settings.look.twinkle; + this.saveSoon(); + }, + onSizeBy: () => { + this.renderer?.setSizeMode(this.settings.look.sizeBy); + this.saveSoon(); + }, + onCruise: (on) => { + if (this.director) this.director.cruiseEnabled = on; + this.saveSoon(); + }, + onPreset: () => { + this.applyPreset(); + this.saveSoon(); + }, + onShowUnresolved: (on) => { + this.store.setIncludeUnresolved(on); + this.saveSoon(); + }, + onImportColors: () => void this.importColors(true), + onShuffleColors: () => this.shuffleColors(), + onColorTheme: (t) => this.applyColorTheme(t), + onRecenter: () => this.recenter(), + onReveal: () => this.playRevealManually(), + onShowOrphans: (on) => { + this.store.setIncludeOrphans(on); + this.saveSoon(); + }, + onStylePreset: (p) => this.applyStylePreset(p), + onCruiseSpeed: () => { + if (this.director) this.director.cruiseSpeed = this.settings.cruiseSpeed; + this.saveSoon(); + }, + onQuality: () => { + this.watchdogTripped = false; + this.lowFpsChecks = 0; + this.applyTier(); + this.saveSoon(); + }, + onSearch: () => this.openSearch(), + onReset: () => { + Object.assign(this.settings.bloom, DEFAULT_GALAXY_SETTINGS.bloom); + Object.assign(this.settings.physics, DEFAULT_GALAXY_SETTINGS.physics); + Object.assign(this.settings.look, DEFAULT_GALAXY_SETTINGS.look); + this.settings.cruise = DEFAULT_GALAXY_SETTINGS.cruise; + this.settings.cruiseSpeed = DEFAULT_GALAXY_SETTINGS.cruiseSpeed; + this.applySettings(); + this.layout.updateParams(toLayoutParams(this.settings.physics)); + this.wasSettled = false; + this.saveSoon(); + }, + runScenario: (s) => void this.runScenario(s), + }, 'map3d'); + } + + private buildFloatingControls(): void { + const controls = this.contentEl.createDiv({ cls: 'mwm-floating-controls mwm-3d-floating-controls' }); + const button = controls.createEl('button', { + cls: 'mwm-floating-button', + attr: { type: 'button', title: this.tt('language'), 'aria-label': this.tt('language') }, + }); + setIcon(button, 'languages'); + button.addEventListener('click', (event) => { + event.preventDefault(); + event.stopPropagation(); + this.openLanguageMenu(button); + }); + this.disposeFns.push(() => controls.remove()); + } + + private openLanguageMenu(anchor: HTMLElement): void { + const menu = new Menu(); + for (const [value, label] of languageOptions(this.language)) { + menu.addItem((item) => { + item.setTitle(label); + if (value === this.language) item.setIcon('check'); + item.onClick(() => { + if (value === this.language) return; + this.language = value; + this.overlay?.setLanguage(value); + this.panel?.setLanguage(value); + this.onLanguage?.(value); + }); + }); + } + const rect = anchor.getBoundingClientRect(); + menu.showAtPosition({ x: rect.right, y: rect.bottom }); + } + + private updateHud(now: number): void { + this.hudFrames.push(now); + while (this.hudFrames.length > 0 && now - (this.hudFrames[0] ?? 0) > 1000) this.hudFrames.shift(); + const el = this.panel?.statsEl; + if (!el || now % 500 > 250) return; + const c = this.counts; + el.setText( + this.tt('stats.3d', { + fps: this.hudFrames.length, + calls: this.renderer?.drawCalls ?? 0, + nodes: c.nodes, + links: c.links, + state: this.layout.isSettled() ? this.tt('state.settled') : this.tt('state.layout'), + }), + ); + } + + private tt(key: string, vars: Record = {}): string { + return t(this.language, key, vars); + } + + // ---------- 基准(与 M0/M1 同场景语义) ---------- + + private waitSettle(timeoutMs = 120_000): Promise { + return new Promise((resolve) => { + const t0 = performance.now(); + const check = () => { + if (this.layout.isSettled() || performance.now() - t0 > timeoutMs) resolve(); + else window.setTimeout(check, 100); + }; + check(); + }); + } + + async runScenario(scenario: 'S1' | 'S2' | 'S3'): Promise { + if (this.benchRunning || !this.renderer || !this.director) return null; + this.benchRunning = true; + try { + if (scenario === 'S2') return await this.benchColdLayout(); + return await this.benchOrbit(scenario); + } finally { + this.benchRunning = false; + } + } + + private async benchOrbit(scenario: 'S1' | 'S3'): Promise { + const renderer = this.renderer; + const director = this.director; + if (!renderer || !director) throw new Error('not ready'); + + const wantUnresolved = scenario === 'S3'; + if (this.store.getIncludeUnresolved() !== wantUnresolved) { + this.store.setIncludeUnresolved(wantUnresolved); + } + new Notice(this.tt('3d.bench.wait', { scenario })); + await this.waitSettle(); + if (renderer.getBloomStrength() < 0.01) renderer.setBloomStrength(0.9); + await sleep(300); + + this.benchMode = true; + const target = director.target.clone(); + const sph = new Spherical().setFromVector3(renderer.camera.position.clone().sub(target)); + new Notice(this.tt('3d.bench.orbit', { scenario })); + const stats = await collectFrames(20_000, (elapsed) => { + const angle = sph.theta + (elapsed / 20_000) * Math.PI * 2; + renderer.camera.position.setFromSpherical(new Spherical(sph.radius, sph.phi, angle)).add(target); + renderer.camera.lookAt(target); + }); + this.benchMode = false; + + const result: BenchResult = { + scenario, + timestamp: new Date().toISOString(), + nodes: this.counts.nodes, + links: this.counts.links, + bloom: renderer.getBloomStrength() > 0, + drawCalls: renderer.drawCalls, + renderer: 'aggregate', + ...stats, + }; + await writeBenchResult(this.app, result); + new Notice(this.tt('3d.bench.done', { scenario, fps: stats.avgFps.toFixed(1), calls: renderer.drawCalls })); + return result; + } + + private async benchColdLayout(): Promise { + new Notice(this.tt('3d.bench.s2Start')); + if (this.store.getIncludeUnresolved()) this.store.setIncludeUnresolved(false); + await sleep(300); + const longTasks = observeLongTasks(); + const t0 = performance.now(); + this.settings.positionCache = {}; // 冷布局必须无暖启动 + this.store.rebuild(false); // 触发 onDataChanged(alpha 0.3)…… + this.initLayout(1); // ……随即以完整冷布局语义重新点火(M2 起的语义偏差在此修正) + this.wasSettled = false; + await this.waitSettle(); + const settleMs = performance.now() - t0; + const ticks = this.layout.ticks; + const lt = longTasks.stop(); + + const result: BenchResult = { + scenario: 'S2', + timestamp: new Date().toISOString(), + nodes: this.counts.nodes, + links: this.counts.links, + bloom: (this.renderer?.getBloomStrength() ?? 0) > 0, + renderer: 'aggregate', + settleMs, + ticks, + avgTickMs: ticks > 0 ? settleMs / ticks : -1, + longTaskCount: lt.count, + longestTaskMs: lt.longestMs, + longTaskTotalMs: lt.totalMs, + }; + await writeBenchResult(this.app, result); + new Notice( + this.tt('3d.bench.s2Done', { + seconds: (settleMs / 1000).toFixed(1), + ticks, + longest: lt.longestMs.toFixed(0), + }), + ); + return result; + } + + // ---------- 销毁合同 ---------- + + dispose(): void { + window.cancelAnimationFrame(this.rafId); + this.intersection?.disconnect(); + this.intersection = null; + for (const fn of this.disposeFns) fn(); + this.disposeFns = []; + this.maskEl?.remove(); + this.maskEl = null; + this.overlay?.dispose(); + this.overlay = null; + this.director?.dispose(); + this.director = null; + this.layout.dispose(); + this.renderer?.dispose(); + this.renderer = null; + this.panel?.dispose(); + this.panel = null; + } +} diff --git a/src/view/Map3DController.ts b/src/view/Map3DController.ts new file mode 100644 index 0000000..08cd88d --- /dev/null +++ b/src/view/Map3DController.ts @@ -0,0 +1,3 @@ +import { GraphController } from './GraphController'; + +export class Map3DController extends GraphController {} diff --git a/src/view/Radial2DController.ts b/src/view/Radial2DController.ts new file mode 100644 index 0000000..cc10140 --- /dev/null +++ b/src/view/Radial2DController.ts @@ -0,0 +1,1069 @@ +import { Component, Menu, Notice, TFile, debounce, setIcon, type App } from 'obsidian'; +import type { Language, MiniWorldMapSettings, RadialSettings, ViewMode } from '../settings'; +import { + HOVER_HIGHLIGHT_MODE_OPTIONS, + HOVER_TARGET_MODE_OPTIONS, + LABEL_VISIBILITY_OPTIONS, + LEGEND_ITEM_DEFINITIONS, + MAX_ATLAS_DEPTH, + MAX_EXTERNAL_LINK_ANCHOR_LIMIT, + MAX_LINK_LIMIT, + MAX_RENDER_NODE_LIMIT, + MAX_SWIRL_STRENGTH, + clampNumber, + hoverHighlightsNoteLinks, + normalizeColorScheme, + normalizeExternalDetailMode, + normalizeHoverHighlightMode, + normalizeHoverTargetMode, + normalizeLabelVisibility, + normalizeLanguage, +} from '../settings'; +import { + colorSchemeOptions, + hoverModeOptions, + hoverTargetOptions, + labelVisibilityOptions, + languageOptions, + outsideDetailOptions, + t, + viewModeLabel, +} from '../i18n'; +import { + DEFAULT_NODE_SPACING, + DEFAULT_RING_SPACING, + layoutRadialGraph, + type RadialLayout, +} from '../layout/radial/layoutRadial'; +import { RadialRenderer, emptyActiveState, type RadialActiveState, type RadialResolvedScheme } from '../render/RadialRenderer'; +import { ROOT_ID, type VisibleGraphState, type VisibleWorldGraph, type WorldEdge, type WorldNode } from '../world/types'; +import { WorldMapIndex } from '../world/WorldMapIndex'; +import { defaultVisibleGraphState } from '../world/visibleGraph'; + +interface PinPath { + id: string; + key: string; + kind: 'node' | 'link'; + nodeId?: string; + edgeId?: string; + source?: string; + target?: string; + mode: string; + title: string; + path: string; + groupId: string | null; +} + +interface PinGroup { + id: string; + name: string; +} + +export class Radial2DController extends Component { + private index: WorldMapIndex; + private state: VisibleGraphState; + private graph: VisibleWorldGraph | null = null; + private layout: RadialLayout | null = null; + private renderer: RadialRenderer | null = null; + private canvasHost: HTMLElement | null = null; + private panel: HTMLElement | null = null; + private panelBody: HTMLElement | null = null; + private statsEl: HTMLElement | null = null; + private activePanelPage: 'inspect' | 'pins' | 'view' | 'controls' | 'defaults' = 'inspect'; + private selectedNodeId: string | null = null; + private selectedLink: WorldEdge | null = null; + private hoverNodeId: string | null = null; + private hoverLink: WorldEdge | null = null; + private pinnedPaths: PinPath[] = []; + private pinGroups: PinGroup[] = []; + private selectedPinIds = new Set(); + private nextPinId = 1; + private nextPinGroupId = 1; + private pinGroupName = ''; + private drag: + | { pointerId: number; startX: number; startY: number; centerX: number; centerY: number; moved: boolean } + | null = null; + private needsFit = true; + private saveSoon: () => void; + private redrawSoon: () => void; + + constructor( + private app: App, + private contentEl: HTMLElement, + private settings: MiniWorldMapSettings, + private saveSettings: () => void, + private onViewMode: (mode: ViewMode) => void, + private onLanguage: (language: Language) => void, + ) { + super(); + this.index = new WorldMapIndex(app, settings.radial); + this.state = defaultVisibleGraphState(settings.radial); + this.saveSoon = debounce(saveSettings, 500, true); + this.redrawSoon = debounce(() => this.rebuild('metadata'), 600, true); + } + + get counts(): { nodes: number; links: number } { + return { nodes: this.graph?.nodes.length ?? 0, links: this.graph?.linkEdges.length ?? 0 }; + } + + async start(): Promise { + this.contentEl.addClass('mwm-radial-mode'); + this.canvasHost = this.contentEl.createDiv({ cls: 'mwm-radial-host' }); + this.renderer = new RadialRenderer(this.canvasHost); + this.syncThemeClass(); + this.buildPanel(); + this.buildFloatingControls(); + this.bindRendererEvents(); + this.registerVaultEvents(); + this.rebuild('start'); + } + + resize(): void { + if (!this.renderer || !this.canvasHost) return; + this.renderer.resize(this.canvasHost.clientWidth, this.canvasHost.clientHeight); + if (this.needsFit) { + this.renderer.fitToLayout(); + this.needsFit = false; + } + this.renderer.render(); + } + + onCssChange(): void { + this.syncThemeClass(); + } + + rebuild(reason: string): void { + const radial = this.radial(); + this.index.rebuild(radial); + this.state.hoverHighlightMode = radial.hoverHighlightMode; + this.state.labelVisibility = radial.labelVisibility; + this.state.hiddenLegendItems = radial.hiddenLegendItems.slice(); + this.state.pinNeedsHoverLinks = this.pinnedPathsNeedHoverLinks(); + this.state.selectedNodeId = this.selectedNodeId; + this.state.selectedLink = this.selectedLink; + const layoutGraph = this.index.buildVisibleGraph({ + ...this.state, + showLinkOverlay: true, + hiddenLegendItems: [], + pinNeedsHoverLinks: true, + }); + const renderHidden = new Set(this.state.hiddenLegendItems); + if (!this.state.showLinkOverlay) renderHidden.add('link'); + const graph = filterLegendGraph(layoutGraph, renderHidden); + this.graph = graph; + this.layout = layoutRadialGraph(layoutGraph, { + ringSpacing: DEFAULT_RING_SPACING, + nodeSpacing: DEFAULT_NODE_SPACING, + swirlStrength: radial.swirlStrength, + }); + this.renderer?.setTheme(this.resolvedCanvasScheme()); + this.renderer?.setData(graph, this.layout, radial.labelVisibility); + this.resize(); + this.applyActiveState(); + this.renderPanel(); + if (['start', 'manual', 'root', 'focus', 'atlas'].includes(reason)) { + this.renderer?.playRevealFromRoot(graph.rootId, this.t('loading.radial')); + } + } + + private radial(): RadialSettings { + return this.settings.radial; + } + + private registerVaultEvents(): void { + this.registerEvent(this.app.metadataCache.on('resolved', this.redrawSoon)); + this.registerEvent(this.app.vault.on('rename', this.redrawSoon)); + this.registerEvent(this.app.vault.on('delete', this.redrawSoon)); + this.registerEvent(this.app.vault.on('create', this.redrawSoon)); + this.registerEvent( + this.app.vault.on('modify', (file) => { + if (file instanceof TFile && file.extension === 'md') this.redrawSoon(); + }), + ); + } + + private bindRendererEvents(): void { + const canvas = this.renderer?.domElement; + if (!canvas) return; + const onWheel = (event: WheelEvent) => { + event.preventDefault(); + const renderer = this.renderer; + if (!renderer) return; + const rect = canvas.getBoundingClientRect(); + const before = renderer.screenToWorld(event.clientX - rect.left, event.clientY - rect.top); + const view = renderer.getView(); + const factor = Math.pow(1.45, -event.deltaY / 120); + const zoom = Math.min(Math.max(view.zoom * factor, 0.003), 6); + const afterCenterX = before.x - (event.clientX - rect.left - rect.width / 2) / zoom; + const afterCenterY = before.y + (event.clientY - rect.top - rect.height / 2) / zoom; + renderer.setView(afterCenterX, afterCenterY, zoom); + }; + const onPointerDown = (event: PointerEvent) => { + if (event.button !== 0) return; + canvas.focus(); + const view = this.renderer?.getView(); + if (!view) return; + this.drag = { pointerId: event.pointerId, startX: event.clientX, startY: event.clientY, centerX: view.centerX, centerY: view.centerY, moved: false }; + canvas.setPointerCapture?.(event.pointerId); + }; + const onPointerMove = (event: PointerEvent) => { + const rect = canvas.getBoundingClientRect(); + if (this.drag) { + const renderer = this.renderer; + if (!renderer) return; + const dx = event.clientX - this.drag.startX; + const dy = event.clientY - this.drag.startY; + this.drag.moved = this.drag.moved || Math.hypot(dx, dy) > 3; + const view = renderer.getView(); + renderer.setView(this.drag.centerX - dx / view.zoom, this.drag.centerY + dy / view.zoom, view.zoom); + return; + } + this.updateHover(event.clientX - rect.left, event.clientY - rect.top); + }; + const onPointerUp = (event: PointerEvent) => { + const wasDrag = this.drag?.moved ?? false; + this.drag = null; + canvas.releasePointerCapture?.(event.pointerId); + if (wasDrag) return; + const rect = canvas.getBoundingClientRect(); + this.activateAt(event.clientX - rect.left, event.clientY - rect.top, event); + }; + const onLeave = () => { + if (this.drag) return; + this.hoverNodeId = null; + this.hoverLink = null; + this.applyActiveState(); + }; + const onDblClick = (event: MouseEvent) => { + const rect = canvas.getBoundingClientRect(); + const hit = this.renderer?.hitTest(event.clientX - rect.left, event.clientY - rect.top, false, this.hoverTargets().nodes); + if (!hit?.nodeId || !this.graph) return; + const node = this.graph.nodesById.get(hit.nodeId); + if (!node) return; + event.preventDefault(); + if (node.type === 'note') void this.openNode(node.id, event); + else if (node.type === 'folder') this.useAsRoot(node.id); + }; + const onContextMenu = (event: MouseEvent) => { + const rect = canvas.getBoundingClientRect(); + const hit = this.renderer?.hitTest(event.clientX - rect.left, event.clientY - rect.top, false, this.hoverTargets().nodes); + if (!hit?.nodeId || !this.graph) return; + const node = this.graph.nodesById.get(hit.nodeId); + if (!node) return; + event.preventDefault(); + this.showNodeMenu(event, node); + }; + canvas.addEventListener('wheel', onWheel, { passive: false }); + canvas.addEventListener('pointerdown', onPointerDown); + canvas.addEventListener('pointermove', onPointerMove); + canvas.addEventListener('pointerup', onPointerUp); + canvas.addEventListener('pointerleave', onLeave); + canvas.addEventListener('dblclick', onDblClick); + canvas.addEventListener('contextmenu', onContextMenu); + this.register(() => { + canvas.removeEventListener('wheel', onWheel); + canvas.removeEventListener('pointerdown', onPointerDown); + canvas.removeEventListener('pointermove', onPointerMove); + canvas.removeEventListener('pointerup', onPointerUp); + canvas.removeEventListener('pointerleave', onLeave); + canvas.removeEventListener('dblclick', onDblClick); + canvas.removeEventListener('contextmenu', onContextMenu); + }); + } + + private updateHover(x: number, y: number): void { + const targets = this.hoverTargets(); + const hit = this.renderer?.hitTest(x, y, targets.links, targets.nodes); + const nextNode = hit?.nodeId ?? null; + const nextLink = nextNode ? null : (hit?.edge ?? null); + if (nextNode === this.hoverNodeId && (nextLink?.id ?? null) === (this.hoverLink?.id ?? null)) return; + this.hoverNodeId = nextNode; + this.hoverLink = nextLink; + this.canvasHost?.toggleClass('is-pointing', Boolean(nextNode || nextLink)); + this.applyActiveState(); + } + + private activateAt(x: number, y: number, event: MouseEvent): void { + const targets = this.hoverTargets(); + const hit = this.renderer?.hitTest(x, y, targets.links, targets.nodes); + if (hit?.nodeId) { + event.preventDefault(); + this.selectedNodeId = hit.nodeId; + this.selectedLink = null; + this.activePanelPage = 'inspect'; + } else if (hit?.edge) { + event.preventDefault(); + this.selectedNodeId = null; + this.selectedLink = hit.edge; + this.activePanelPage = 'inspect'; + } else { + this.selectedNodeId = null; + this.selectedLink = null; + } + this.state.selectedNodeId = this.selectedNodeId; + this.state.selectedLink = this.selectedLink; + this.applyActiveState(); + this.renderPanel(); + } + + private applyActiveState(): void { + if (!this.graph || !this.renderer) return; + const active = this.resolveActiveState(); + this.renderer.setActive(active, this.radial().labelVisibility); + } + + private resolveActiveState(): RadialActiveState { + if (!this.graph) return emptyActiveState(); + const activeNode = this.hoverNodeId ?? this.selectedNodeId; + const activeLink = this.hoverLink ?? this.selectedLink; + const state = emptyActiveState(); + state.activeNodeId = activeNode; + state.activeLinkId = activeLink?.id ?? null; + const addNode = (nodeId: string | null | undefined, mode: string, pinned: boolean) => { + if (!nodeId || !this.graph?.nodesById.has(nodeId)) return; + state.relatedNodes.add(nodeId); + state.labelNodes.add(nodeId); + if (pinned) state.pinnedNodeIds.add(nodeId); + if (hoverHighlightsNoteLinks(mode)) { + for (const edge of [...this.graph.linkEdges, ...this.graph.hoverLinkEdges]) { + if (edge.source !== nodeId && edge.target !== nodeId) continue; + state.highlightedEdges.add(edge.id); + state.relatedNodes.add(edge.source); + state.relatedNodes.add(edge.target); + state.labelNodes.add(edge.source); + state.labelNodes.add(edge.target); + } + } + addHierarchyHighlights(this.graph, nodeId, normalizeHoverHighlightMode(mode), state); + }; + const addLink = (edge: WorldEdge | null | undefined, pinned: boolean) => { + if (!edge) return; + state.highlightedEdges.add(edge.id); + state.relatedNodes.add(edge.source); + state.relatedNodes.add(edge.target); + state.labelNodes.add(edge.source); + state.labelNodes.add(edge.target); + if (pinned) { + state.pinnedNodeIds.add(edge.source); + state.pinnedNodeIds.add(edge.target); + } + }; + addNode(activeNode, this.state.hoverHighlightMode, false); + addLink(activeLink, false); + for (const pin of this.pinnedPaths) { + if (pin.kind === 'node') addNode(pin.nodeId, pin.mode, true); + else addLink(this.findGraphEdgeForPin(pin), true); + } + state.hasActive = Boolean(activeNode || activeLink || this.pinnedPaths.length); + state.dimOthers = Boolean(activeNode || activeLink); + return state; + } + + private hoverTargets(): { nodes: boolean; links: boolean } { + const mode = normalizeHoverTargetMode(this.radial().hoverTargetMode); + return { nodes: mode !== 'links', links: mode !== 'nodes' }; + } + + private buildPanel(): void { + this.panel = this.contentEl.createDiv({ cls: 'galaxy-panel mwm-radial-panel mwm-map-panel' }); + this.syncThemeClass(); + const header = this.panel.createDiv({ cls: 'galaxy-panel-header' }); + this.statsEl = header.createDiv({ cls: 'galaxy-panel-stats', text: 'Mini World Map' }); + const collapse = header.createEl('button', { cls: 'galaxy-panel-collapse', text: '-' }); + this.panelBody = this.panel.createDiv({ cls: 'galaxy-panel-body' }); + collapse.addEventListener('click', () => { + const hidden = this.panelBody?.hasClass('is-hidden') ?? false; + this.panelBody?.toggleClass('is-hidden', !hidden); + collapse.setText(hidden ? '-' : '+'); + }); + this.renderPanel(); + } + + private renderPanel(): void { + if (!this.panelBody) return; + const graph = this.graph; + this.panelBody.empty(); + if (this.statsEl) { + this.statsEl.setText(this.t('stats.counts', { nodes: graph?.nodes.length ?? 0, links: graph?.linkEdges.length ?? 0 })); + } + const modeSwitch = this.panelBody.createDiv({ cls: 'mwm-mode-switch' }); + modeSwitch.createDiv({ cls: 'mwm-mode-switch-label', text: this.t('view.mode') }); + const modeRow = modeSwitch.createDiv({ cls: 'galaxy-mode-row mwm-mode-row' }); + this.modeButton(modeRow, viewModeLabel(this.settings.language, 'radial2d'), 'radial2d'); + this.modeButton(modeRow, viewModeLabel(this.settings.language, 'map3d'), 'map3d'); + + const settings = this.panelBody.createDiv({ cls: 'mwm-panel-settings mwm-radial-settings' }); + const tabs = settings.createDiv({ cls: 'mwm-panel-tabs' }); + for (const [id, label] of [ + ['inspect', this.t('tab.inspect')], + ['pins', this.t('tab.pins')], + ['view', this.t('tab.view')], + ['controls', this.t('tab.controls')], + ['defaults', this.t('tab.defaults')], + ] as const) { + const button = tabs.createEl('button', { cls: this.activePanelPage === id ? 'is-active' : '', text: label }); + button.addEventListener('click', () => { + this.activePanelPage = id; + this.renderPanel(); + }); + } + const body = settings.createDiv({ cls: 'mwm-panel-page' }); + if (this.activePanelPage === 'pins') this.renderPinsPage(body); + else if (this.activePanelPage === 'view') this.renderViewPage(body); + else if (this.activePanelPage === 'controls') this.renderControlsPage(body); + else if (this.activePanelPage === 'defaults') this.renderDefaultsPage(body); + else this.renderInspectPage(body); + } + + private modeButton(parent: HTMLElement, label: string, mode: ViewMode): void { + const active = this.settings.viewMode === mode; + const button = parent.createEl('button', { cls: active ? 'is-active' : '', text: label }); + button.addEventListener('click', () => this.onViewMode(mode)); + } + + private renderInspectPage(parent: HTMLElement): void { + if (!this.graph) return; + if (this.selectedLink) { + this.renderLinkInspect(parent, this.selectedLink); + return; + } + const node = this.graph.nodesById.get(this.selectedNodeId ?? this.graph.focusId ?? this.graph.rootId); + parent.createDiv({ cls: 'mwm-side-title', text: node?.title ?? 'Mini World Map' }); + if (!node) return; + parent.createDiv({ cls: 'mwm-side-path', text: node.path || '/' }); + const facts = parent.createDiv({ cls: 'mwm-facts' }); + for (const [label, value] of [ + [this.t('inspect.type'), node.externalProxy ? `${this.t('inspect.external')} ${node.type}` : node.type], + [this.t('inspect.depth'), String(node.depth)], + [this.t('inspect.notes'), String(node.noteCount || node.descendantCount || 0)], + [this.t('inspect.out'), String(node.linkCount || 0)], + [this.t('inspect.in'), String(node.backlinkCount || 0)], + ]) { + facts.createSpan({ text: label }); + facts.createSpan({ text: value }); + } + const actions = parent.createDiv({ cls: 'galaxy-panel-row' }); + this.button(actions, this.t('common.pin'), () => this.pinNode(node)); + if (node.type === 'note') this.button(actions, this.t('common.open'), () => void this.openNode(node.id)); + if (node.type === 'note') this.button(actions, this.t('common.focus'), () => this.focusNote(node.id)); + if (node.type === 'folder') this.button(actions, this.t('common.root'), () => this.useAsRoot(node.id)); + this.renderNeighborList(parent, node); + } + + private renderLinkInspect(parent: HTMLElement, edge: WorldEdge): void { + parent.createDiv({ cls: 'mwm-side-title', text: this.t('inspect.linkOverlay') }); + const source = this.graph?.nodesById.get(edge.source); + const target = this.graph?.nodesById.get(edge.target); + const facts = parent.createDiv({ cls: 'mwm-facts' }); + for (const [label, value] of [ + [this.t('common.source'), source?.title ?? edge.source], + [this.t('common.target'), target?.title ?? edge.target], + [this.t('inspect.weight'), String(edge.weight || 0)], + [this.t('inspect.raw'), String(edge.rawCount || edge.weight || 0)], + [this.t('inspect.unresolved'), String(edge.unresolvedCount || 0)], + [this.t('inspect.external'), String(edge.externalCount || 0)], + ]) { + facts.createSpan({ text: label }); + facts.createSpan({ text: value }); + } + const actions = parent.createDiv({ cls: 'galaxy-panel-row' }); + this.button(actions, this.t('common.pin'), () => this.pinLink(edge)); + this.button(actions, this.t('common.source'), () => this.inspectNode(edge.source)); + this.button(actions, this.t('common.target'), () => this.inspectNode(edge.target)); + } + + private renderNeighborList(parent: HTMLElement, node: WorldNode): void { + const outgoing = (this.index.linkEdgesBySource.get(node.id) ?? []).slice(0, 20); + const incoming = (this.index.linkEdgesByTarget.get(node.id) ?? []).slice(0, 20); + for (const [title, edges, side] of [ + [this.t('inspect.outgoing', { count: outgoing.length }), outgoing, 'target'], + [this.t('inspect.backlinks', { count: incoming.length }), incoming, 'source'], + ] as const) { + parent.createDiv({ cls: 'mwm-side-heading', text: title }); + if (edges.length === 0) parent.createDiv({ cls: 'mwm-side-muted', text: this.t('common.none') }); + for (const edge of edges) { + const neighbor = this.index.nodes.get(edge[side]); + if (!neighbor) continue; + const button = parent.createEl('button', { cls: 'mwm-link-row', text: `${neighbor.title} (${edge.weight})` }); + button.addEventListener('click', () => this.inspectNode(neighbor.id)); + } + } + } + + private renderPinsPage(parent: HTMLElement): void { + const actions = parent.createDiv({ cls: 'galaxy-panel-row' }); + this.button(actions, this.t('common.pinCurrent'), () => this.pinCurrent()); + this.button(actions, this.t('common.clear'), () => this.clearPins()); + const groupRow = parent.createDiv({ cls: 'mwm-pin-group-row' }); + const input = groupRow.createEl('input', { attr: { value: this.pinGroupName, placeholder: this.t('pins.groupName') } }); + input.addEventListener('input', () => (this.pinGroupName = input.value)); + this.button(groupRow, this.t('common.group'), () => this.groupSelectedPins()); + if (this.pinnedPaths.length === 0) { + parent.createDiv({ cls: 'mwm-side-muted', text: this.t('pins.empty') }); + return; + } + for (const pin of this.pinnedPaths) { + const row = parent.createDiv({ cls: 'mwm-pin-row' }); + const check = row.createEl('input', { attr: { type: 'checkbox' } }); + check.checked = this.selectedPinIds.has(pin.id); + check.addEventListener('change', () => { + if (check.checked) this.selectedPinIds.add(pin.id); + else this.selectedPinIds.delete(pin.id); + this.renderPanel(); + }); + const main = row.createEl('button', { cls: 'mwm-pin-main', text: pin.title }); + main.addEventListener('click', () => this.locatePin(pin)); + this.button(row, this.t('common.inspect'), () => this.inspectPin(pin)); + this.button(row, 'X', () => this.removePin(pin.id)); + } + } + + private renderViewPage(parent: HTMLElement): void { + this.textInput(parent, this.t('common.search'), this.state.search, (value) => { + this.state.search = value.trim(); + this.needsFit = true; + this.rebuild('search'); + }); + this.select(parent, this.t('view.theme'), this.radial().colorScheme, colorSchemeOptions(this.settings.language), (value) => { + this.radial().colorScheme = normalizeColorScheme(value); + this.saveSoon(); + this.syncThemeClass(); + }); + const row = parent.createDiv({ cls: 'galaxy-panel-row' }); + this.button(row, this.t('view.atlas'), () => this.resetToAtlas(), this.state.mode === 'atlas'); + this.button(row, this.t('view.focus'), () => this.focusActiveNote(), this.state.mode === 'focus'); + this.button(row, this.t('view.vaultRoot'), () => this.resetToRoot()); + const row2 = parent.createDiv({ cls: 'galaxy-panel-row' }); + this.button(row2, this.t('common.fit'), () => this.renderer?.fitToLayout()); + this.button(row2, this.t('common.rebuild'), () => this.rebuild('manual')); + } + + private renderControlsPage(parent: HTMLElement): void { + const radial = this.radial(); + this.numberInput(parent, this.t('control.depth'), this.state.atlasDepth, 1, MAX_ATLAS_DEPTH, 1, (value) => { + this.state.atlasDepth = value; + this.rebuild('depth'); + }); + this.numberInput(parent, this.t('control.nodes'), this.state.nodeLimit, 200, MAX_RENDER_NODE_LIMIT, 100, (value) => { + this.state.nodeLimit = value; + this.rebuild('nodes'); + }); + this.numberInput(parent, this.t('control.noteLinks'), this.state.linkLimit, 0, MAX_LINK_LIMIT, 50, (value) => { + this.state.linkLimit = value; + this.rebuild('links'); + }); + const hidden = new Set(radial.hiddenLegendItems); + const noteLinksVisible = this.state.showLinkOverlay && !hidden.has('link'); + this.toggle(parent, this.t('control.showNoteLinks'), noteLinksVisible, (value) => { + radial.showLinkOverlay = value; + this.state.showLinkOverlay = value; + this.setLegendHidden('link', !value); + this.saveSoon(); + this.rebuild('link-overlay'); + }); + this.select(parent, this.t('control.hover'), this.state.hoverHighlightMode, hoverModeOptions(this.settings.language, HOVER_HIGHLIGHT_MODE_OPTIONS), (value) => { + const mode = normalizeHoverHighlightMode(value); + this.state.hoverHighlightMode = mode; + radial.hoverHighlightMode = mode; + this.saveSoon(); + this.rebuild('hover'); + }); + this.select(parent, this.t('control.hoverTargets'), radial.hoverTargetMode, hoverTargetOptions(this.settings.language, HOVER_TARGET_MODE_OPTIONS), (value) => { + radial.hoverTargetMode = normalizeHoverTargetMode(value); + this.clearDisallowedHoverTargets(); + this.saveSoon(); + this.applyActiveState(); + this.renderPanel(); + }); + this.select(parent, this.t('control.labels'), radial.labelVisibility, labelVisibilityOptions(this.settings.language, LABEL_VISIBILITY_OPTIONS), (value) => { + radial.labelVisibility = normalizeLabelVisibility(value); + this.state.labelVisibility = radial.labelVisibility; + this.saveSoon(); + this.applyActiveState(); + }); + this.numberInput(parent, this.t('control.spin'), radial.swirlStrength, 0, MAX_SWIRL_STRENGTH, 1, (value) => { + radial.swirlStrength = value; + this.saveSoon(); + this.rebuild('spin'); + }); + this.toggle(parent, this.t('control.outsideLinks'), this.state.showExternalLinks, (value) => { + this.state.showExternalLinks = value; + radial.showExternalLinks = value; + this.saveSoon(); + this.rebuild('external'); + }); + this.select( + parent, + this.t('control.outsideDetail'), + this.state.externalDetailMode, + outsideDetailOptions(this.settings.language), + (value) => { + this.state.externalDetailMode = normalizeExternalDetailMode(value); + radial.externalDetailMode = this.state.externalDetailMode; + this.saveSoon(); + this.rebuild('external-mode'); + }, + ); + this.numberInput(parent, this.t('control.exactOutsideFiles'), this.state.externalLinkAnchorLimit, 0, MAX_EXTERNAL_LINK_ANCHOR_LIMIT, 50, (value) => { + this.state.externalLinkAnchorLimit = value; + radial.externalLinkAnchorLimit = value; + this.saveSoon(); + this.rebuild('external-limit'); + }); + this.renderLegend(parent); + } + + private renderDefaultsPage(parent: HTMLElement): void { + const radial = this.radial(); + this.numberInput(parent, this.t('control.defaultDepth'), radial.atlasDepth, 1, MAX_ATLAS_DEPTH, 1, (value) => { + radial.atlasDepth = value; + this.state.atlasDepth = value; + this.saveSoon(); + }); + this.numberInput(parent, this.t('control.defaultNodes'), radial.renderNodeLimit, 200, MAX_RENDER_NODE_LIMIT, 100, (value) => { + radial.renderNodeLimit = value; + this.state.nodeLimit = value; + this.saveSoon(); + }); + this.numberInput(parent, this.t('control.defaultNoteLinks'), radial.linkLimit, 0, MAX_LINK_LIMIT, 50, (value) => { + radial.linkLimit = value; + this.state.linkLimit = value; + this.saveSoon(); + }); + this.toggle(parent, this.t('control.unresolvedLinks'), radial.includeUnresolvedLinks, (value) => { + radial.includeUnresolvedLinks = value; + this.saveSoon(); + this.rebuild('unresolved'); + }); + this.textArea(parent, this.t('control.ignoredFolders'), radial.ignoreFolders.join('\n'), (value) => { + radial.ignoreFolders = value + .split('\n') + .map((line) => line.trim()) + .filter(Boolean); + this.saveSoon(); + this.rebuild('ignored'); + }); + } + + private renderLegend(parent: HTMLElement): void { + parent.createDiv({ cls: 'mwm-side-heading', text: this.t('control.legend') }); + const hidden = new Set(this.radial().hiddenLegendItems); + for (const [id, labelKey, titleKey, markerClass] of LEGEND_ITEM_DEFINITIONS) { + const row = parent.createEl('label', { + cls: hidden.has(id) ? 'mwm-legend-item is-muted' : 'mwm-legend-item', + attr: { title: this.t(titleKey) }, + }); + const checkbox = row.createEl('input', { attr: { type: 'checkbox' } }); + checkbox.checked = !hidden.has(id); + row.createSpan({ cls: `mwm-legend-mark ${markerClass}` }); + const text = row.createSpan({ cls: 'mwm-legend-copy' }); + text.createSpan({ cls: 'mwm-legend-label', text: this.t(labelKey) }); + text.createSpan({ cls: 'mwm-legend-desc', text: this.t(titleKey) }); + checkbox.addEventListener('change', () => { + const value = checkbox.checked; + if (value) hidden.delete(id); + else hidden.add(id); + this.radial().hiddenLegendItems = [...hidden]; + this.state.hiddenLegendItems = [...hidden]; + if (id === 'link') { + this.radial().showLinkOverlay = value; + this.state.showLinkOverlay = value; + } + this.saveSoon(); + this.rebuild('legend'); + }); + } + } + + private setLegendHidden(id: string, isHidden: boolean): void { + const hidden = new Set(this.radial().hiddenLegendItems); + if (isHidden) hidden.add(id); + else hidden.delete(id); + this.radial().hiddenLegendItems = [...hidden]; + this.state.hiddenLegendItems = [...hidden]; + } + + private buildFloatingControls(): void { + const host = this.canvasHost; + if (!host) return; + const controls = host.createDiv({ cls: 'mwm-floating-controls' }); + const languageButton = controls.createEl('button', { + cls: 'mwm-floating-button', + attr: { type: 'button', title: this.t('language'), 'aria-label': this.t('language') }, + }); + setIcon(languageButton, 'languages'); + languageButton.addEventListener('click', (event) => { + event.preventDefault(); + event.stopPropagation(); + this.openLanguageMenu(languageButton); + }); + this.iconButton(controls, 'home', this.t('view.vaultRoot'), () => this.resetToRoot()); + this.iconButton(controls, 'pin', this.t('common.pinCurrent'), () => this.pinCurrent()); + this.iconButton(controls, 'maximize', this.t('common.fit'), () => this.renderer?.fitToLayout()); + this.iconButton(controls, 'refresh-cw', this.t('common.rebuild'), () => this.rebuild('manual')); + } + + private openLanguageMenu(anchor: HTMLElement): void { + const menu = new Menu(); + for (const [value, label] of languageOptions(this.settings.language)) { + menu.addItem((item) => { + item.setTitle(label); + if (value === this.settings.language) item.setIcon('check'); + item.onClick(() => { + const language = normalizeLanguage(value); + if (language === this.settings.language) return; + this.settings.language = language; + this.onLanguage(language); + this.renderPanel(); + }); + }); + } + const rect = anchor.getBoundingClientRect(); + menu.showAtPosition({ x: rect.right, y: rect.bottom }); + } + + private iconButton(parent: HTMLElement, icon: string, title: string, onClick: () => void): HTMLButtonElement { + const button = parent.createEl('button', { cls: 'mwm-floating-button', attr: { type: 'button', title, 'aria-label': title } }); + setIcon(button, icon); + button.addEventListener('click', (event) => { + event.preventDefault(); + event.stopPropagation(); + onClick(); + }); + return button; + } + + private button(parent: HTMLElement, label: string, onClick: () => void, active = false): HTMLButtonElement { + const button = parent.createEl('button', { cls: active ? 'is-active' : '', text: label }); + button.addEventListener('click', onClick); + return button; + } + + private textInput(parent: HTMLElement, label: string, value: string, onChange: (value: string) => void): void { + const field = parent.createEl('label', { cls: 'mwm-panel-field' }); + field.createSpan({ text: label }); + const input = field.createEl('input', { attr: { value, type: 'search' } }); + input.addEventListener('change', () => onChange(input.value)); + } + + private numberInput(parent: HTMLElement, label: string, value: number, min: number, max: number, step: number, onChange: (value: number) => void): void { + const field = parent.createEl('label', { cls: 'mwm-panel-field' }); + field.createSpan({ text: label }); + const input = field.createEl('input', { attr: { value: String(value), type: 'number', min: String(min), max: String(max), step: String(step) } }); + input.addEventListener('change', () => onChange(clampNumber(input.value, min, max, value))); + } + + private textArea(parent: HTMLElement, label: string, value: string, onChange: (value: string) => void): void { + const field = parent.createEl('label', { cls: 'mwm-panel-field' }); + field.createSpan({ text: label }); + const input = field.createEl('textarea'); + input.value = value; + input.addEventListener('change', () => onChange(input.value)); + } + + private toggle(parent: HTMLElement, label: string, value: boolean, onChange: (value: boolean) => void, title?: string): void { + const field = parent.createEl('label', { cls: 'mwm-panel-toggle', attr: { title: title ?? label } }); + const input = field.createEl('input', { attr: { type: 'checkbox' } }); + input.checked = value; + field.createSpan({ text: label }); + input.addEventListener('change', () => onChange(input.checked)); + } + + private select(parent: HTMLElement, label: string, value: T, options: [T, string][], onChange: (value: T) => void): void { + const field = parent.createEl('label', { cls: 'mwm-panel-field' }); + field.createSpan({ text: label }); + const select = field.createEl('select'); + for (const [id, text] of options) select.createEl('option', { attr: { value: id }, text }); + select.value = value; + select.addEventListener('change', () => onChange(select.value as T)); + } + + private pinCurrent(): void { + if (this.selectedLink) this.pinLink(this.selectedLink); + else if (this.selectedNodeId && this.graph?.nodesById.has(this.selectedNodeId)) this.pinNode(this.graph.nodesById.get(this.selectedNodeId)!); + else if (this.hoverLink) this.pinLink(this.hoverLink); + else if (this.hoverNodeId && this.graph?.nodesById.has(this.hoverNodeId)) this.pinNode(this.graph.nodesById.get(this.hoverNodeId)!); + else new Notice(this.t('pins.selectBeforePin')); + } + + private pinNode(node: WorldNode): void { + this.addPin({ + kind: 'node', + nodeId: node.id, + mode: this.state.hoverHighlightMode, + title: node.title, + path: node.path || '/', + }); + } + + private pinLink(edge: WorldEdge): void { + const source = this.graph?.nodesById.get(edge.source); + const target = this.graph?.nodesById.get(edge.target); + this.addPin({ + kind: 'link', + edgeId: edge.id, + source: edge.source, + target: edge.target, + mode: 'note-links', + title: `${source?.title ?? edge.source} -> ${target?.title ?? edge.target}`, + path: `${source?.path ?? edge.source} -> ${target?.path ?? edge.target}`, + }); + } + + private addPin(candidate: Omit): void { + const key = candidate.kind === 'node' ? `node:${candidate.nodeId}:${candidate.mode}` : `link:${candidate.edgeId ?? `${candidate.source}->${candidate.target}`}`; + const existing = this.pinnedPaths.find((pin) => pin.key === key); + if (existing) { + this.selectedPinIds.add(existing.id); + new Notice(this.t('pins.already')); + this.renderPanel(); + return; + } + const pin: PinPath = { ...candidate, id: `pin-${this.nextPinId++}`, key, groupId: null }; + this.pinnedPaths.push(pin); + this.selectedPinIds.add(pin.id); + this.state.pinNeedsHoverLinks = this.pinnedPathsNeedHoverLinks(); + this.applyActiveState(); + this.activePanelPage = 'pins'; + this.renderPanel(); + } + + private removePin(id: string): void { + this.pinnedPaths = this.pinnedPaths.filter((pin) => pin.id !== id); + this.selectedPinIds.delete(id); + this.state.pinNeedsHoverLinks = this.pinnedPathsNeedHoverLinks(); + this.applyActiveState(); + this.renderPanel(); + } + + private clearPins(): void { + this.pinnedPaths = []; + this.pinGroups = []; + this.selectedPinIds.clear(); + this.state.pinNeedsHoverLinks = false; + this.applyActiveState(); + this.renderPanel(); + } + + private groupSelectedPins(): void { + const pins = this.pinnedPaths.filter((pin) => this.selectedPinIds.has(pin.id)); + if (pins.length === 0) { + new Notice(this.t('pins.selectFirst')); + return; + } + const groupId = `pin-group-${this.nextPinGroupId++}`; + this.pinGroups.push({ id: groupId, name: this.pinGroupName.trim() || `Group ${this.pinGroups.length + 1}` }); + for (const pin of pins) pin.groupId = groupId; + this.pinGroupName = ''; + this.selectedPinIds.clear(); + this.renderPanel(); + } + + private locatePin(pin: PinPath): void { + const nodeId = pin.kind === 'node' ? pin.nodeId : pin.source; + if (!nodeId) return; + const point = this.renderer?.nodePoint(nodeId); + const view = this.renderer?.getView(); + if (point && view) this.renderer?.setView(point.x, point.y, Math.max(view.zoom, 0.22)); + } + + private inspectPin(pin: PinPath): void { + this.activePanelPage = 'inspect'; + if (pin.kind === 'node' && pin.nodeId) this.inspectNode(pin.nodeId); + else this.selectedLink = this.findGraphEdgeForPin(pin); + this.applyActiveState(); + this.renderPanel(); + } + + private findGraphEdgeForPin(pin: PinPath): WorldEdge | null { + if (pin.kind !== 'link' || !this.graph) return null; + return [...this.graph.linkEdges, ...this.graph.hoverLinkEdges].find((edge) => edge.id === pin.edgeId || (edge.source === pin.source && edge.target === pin.target)) ?? null; + } + + private pinnedPathsNeedHoverLinks(): boolean { + return this.pinnedPaths.some((pin) => pin.kind === 'link' || (pin.kind === 'node' && hoverHighlightsNoteLinks(pin.mode))); + } + + private clearDisallowedHoverTargets(): void { + const targets = this.hoverTargets(); + if (!targets.nodes) this.hoverNodeId = null; + if (!targets.links) this.hoverLink = null; + this.canvasHost?.toggleClass('is-pointing', Boolean((targets.nodes && this.hoverNodeId) || (targets.links && this.hoverLink))); + } + + private inspectNode(nodeId: string): void { + this.selectedNodeId = nodeId; + this.selectedLink = null; + this.applyActiveState(); + this.renderPanel(); + } + + private resetToAtlas(): void { + this.state.mode = 'atlas'; + this.state.rootPath = this.state.rootPath || ROOT_ID; + this.state.focusPath = null; + this.needsFit = true; + this.rebuild('atlas'); + } + + private resetToRoot(): void { + this.state.mode = 'atlas'; + this.state.rootPath = ROOT_ID; + this.state.focusPath = null; + this.needsFit = true; + this.rebuild('root'); + } + + private useAsRoot(nodeId: string): void { + this.state.mode = 'atlas'; + this.state.rootPath = nodeId; + this.state.focusPath = null; + this.needsFit = true; + this.rebuild('root'); + } + + private focusActiveNote(): void { + const active = this.index.getActiveNotePath(); + if (active) this.focusNote(active); + } + + private focusNote(nodeId: string): void { + this.state.mode = 'focus'; + this.state.focusPath = nodeId; + this.state.rootPath = ROOT_ID; + this.needsFit = true; + this.rebuild('focus'); + } + + private showNodeMenu(event: MouseEvent, node: WorldNode): void { + const menu = new Menu(); + if (node.type === 'note') { + menu.addItem((item) => item.setTitle(this.t('context.openNote')).setIcon('file-text').onClick(() => void this.openNode(node.id, event))); + menu.addItem((item) => item.setTitle(this.t('context.focusNote')).setIcon('locate-fixed').onClick(() => this.focusNote(node.id))); + } + if (node.type === 'folder') { + menu.addItem((item) => item.setTitle(this.t('context.useAsRoot')).setIcon('folder-open').onClick(() => this.useAsRoot(node.id))); + if (node.representativeFile) { + menu.addItem((item) => item.setTitle(this.t('context.openRepresentative')).setIcon('file-text').onClick(() => void this.openNode(node.representativeFile!, event))); + } + } + menu.addItem((item) => item.setTitle(this.t('context.pinPath')).setIcon('pin').onClick(() => this.pinNode(node))); + menu.showAtMouseEvent(event); + } + + private async openNode(path: string, event?: MouseEvent): Promise { + const file = this.app.vault.getAbstractFileByPath(path); + if (!(file instanceof TFile)) return; + const target = event && (event.metaKey || event.ctrlKey) ? 'split' : 'tab'; + await this.app.workspace.openLinkText(file.path, '', target, { active: true }); + } + + private syncThemeClass(): void { + const canvasScheme = this.resolvedCanvasScheme(); + const panelScheme = this.resolvedPanelScheme(); + this.contentEl.toggleClass('is-day-scheme', canvasScheme === 'day'); + this.contentEl.toggleClass('is-night-scheme', canvasScheme === 'night'); + this.panel?.removeClass('gx-theme-dark'); + this.panel?.removeClass('gx-theme-light'); + this.panel?.addClass(panelScheme === 'day' ? 'gx-theme-light' : 'gx-theme-dark'); + const changed = this.renderer?.setTheme(canvasScheme) ?? false; + if (changed && this.graph && this.layout) this.renderer?.setData(this.graph, this.layout, this.radial().labelVisibility); + } + + private resolvedCanvasScheme(): RadialResolvedScheme { + const scheme = normalizeColorScheme(this.radial().colorScheme); + if (scheme === 'day' || scheme === 'night') return scheme; + return activeDocument.body.hasClass('theme-dark') ? 'night' : 'day'; + } + + private resolvedPanelScheme(): RadialResolvedScheme { + return activeDocument.body.hasClass('theme-dark') ? 'night' : 'day'; + } + + private t(key: string, vars: Record = {}): string { + return t(this.settings.language, key, vars); + } + + dispose(): void { + this.unload(); + this.renderer?.dispose(); + this.renderer = null; + this.contentEl.removeClass('mwm-radial-mode'); + this.contentEl.empty(); + } +} + +function addHierarchyHighlights( + graph: VisibleWorldGraph, + nodeId: string, + mode: ReturnType, + state: RadialActiveState, +): void { + if (mode === 'none' || mode === 'note-links') return; + const parentByChild = new Map(graph.hierarchyEdges.map((edge) => [edge.target, edge])); + const childrenByParent = new Map(); + for (const edge of graph.hierarchyEdges) { + const list = childrenByParent.get(edge.source); + if (list) list.push(edge); + else childrenByParent.set(edge.source, [edge]); + } + const addEdge = (edge: WorldEdge) => { + state.highlightedEdges.add(edge.id); + state.relatedNodes.add(edge.source); + state.relatedNodes.add(edge.target); + state.labelNodes.add(edge.source); + state.labelNodes.add(edge.target); + }; + if (mode === 'hierarchy-parents' || mode === 'hierarchy-parents-direct' || mode === 'hierarchy-all') { + let edge = parentByChild.get(nodeId); + while (edge) { + addEdge(edge); + edge = parentByChild.get(edge.source); + } + } + if (mode === 'hierarchy-direct-children' || mode === 'hierarchy-parents-direct' || mode === 'hierarchy-all') { + for (const edge of childrenByParent.get(nodeId) ?? []) addEdge(edge); + } + if (mode === 'hierarchy-descendants' || mode === 'hierarchy-all') { + const stack = [...(childrenByParent.get(nodeId) ?? [])]; + while (stack.length > 0) { + const edge = stack.pop(); + if (!edge) continue; + addEdge(edge); + stack.push(...(childrenByParent.get(edge.target) ?? [])); + } + } +} + +function filterLegendGraph(graph: VisibleWorldGraph, hidden: Set): VisibleWorldGraph { + if (hidden.size === 0) return graph; + const keepNode = (node: WorldNode) => { + if (node.id === graph.rootId && hidden.has('root')) return false; + if (node.externalProxy && hidden.has('outside-file')) return false; + if (node.type === 'external' && !node.externalProxy && hidden.has('outside')) return false; + if (node.type === 'unresolved' && hidden.has('missing')) return false; + if (node.type === 'folder' && node.representativeFile && hidden.has('folder-meta')) return false; + if (node.type === 'folder' && !node.representativeFile && hidden.has('folder')) return false; + if (node.type === 'note' && hidden.has('file')) return false; + return true; + }; + const nodes = graph.nodes.filter(keepNode); + const ids = new Set(nodes.map((node) => node.id)); + const keepEdge = (edge: WorldEdge) => ids.has(edge.source) && ids.has(edge.target); + const hierarchyEdges = hidden.has('tree') ? [] : graph.hierarchyEdges.filter(keepEdge); + const keepLinkEdge = (edge: WorldEdge) => { + if (!keepEdge(edge)) return false; + if (edge.externalCount && hidden.has('outside-link')) return false; + if (edge.unresolvedCount && hidden.has('dashed-link')) return false; + return Boolean(edge.externalCount) || !hidden.has('link'); + }; + const keepHoverLinkEdge = (edge: WorldEdge) => { + if (!keepEdge(edge)) return false; + if (edge.externalCount && hidden.has('outside-link')) return false; + if (edge.unresolvedCount && hidden.has('dashed-link')) return false; + return true; + }; + const linkEdges = graph.linkEdges.filter(keepLinkEdge); + const hoverLinkEdges = graph.hoverLinkEdges.filter(keepHoverLinkEdge); + return { ...graph, nodes, nodesById: new Map(nodes.map((node) => [node.id, node])), hierarchyEdges, linkEdges, hoverLinkEdges }; +} diff --git a/src/view/SearchModal.ts b/src/view/SearchModal.ts new file mode 100644 index 0000000..fb0183f --- /dev/null +++ b/src/view/SearchModal.ts @@ -0,0 +1,61 @@ +import type { App } from 'obsidian'; +import { SuggestModal, prepareFuzzySearch } from 'obsidian'; +import type { Language } from '../settings'; +import type { GraphNode } from '../types'; +import { t } from '../i18n'; + +interface Hit { + index: number; + node: GraphNode; + score: number; +} + +/** 搜索星系节点 → 镜头飞行(Obsidian 原生 SuggestModal,键盘友好) */ +export class NodeSearchModal extends SuggestModal { + constructor( + app: App, + private nodes: GraphNode[], + private onPick: (index: number) => void, + private language: Language = 'en', + ) { + super(app); + this.setPlaceholder(this.tt('3d.searchPlaceholder')); + } + + getSuggestions(query: string): Hit[] { + const q = query.trim(); + if (!q) { + // 空查询:按度数给出枢纽 top 20——「星座导览」 + return [...this.nodes.entries()] + .filter(([, n]) => !n.unresolved) + .sort((a, b) => b[1].degree - a[1].degree) + .slice(0, 20) + .map(([index, node]) => ({ index, node, score: 0 })); + } + const fuzzy = prepareFuzzySearch(q); + const hits: Hit[] = []; + for (let i = 0; i < this.nodes.length; i++) { + const node = this.nodes[i]; + if (!node) continue; + const m = fuzzy(node.name) ?? fuzzy(node.id); + if (m) hits.push({ index: i, node, score: m.score }); + } + return hits.sort((a, b) => b.score - a.score || b.node.degree - a.node.degree).slice(0, 50); + } + + renderSuggestion(hit: Hit, el: HTMLElement): void { + el.createDiv({ text: hit.node.name }); + el.createDiv({ + cls: 'gx-search-path', + text: `${hit.node.unresolved ? this.tt('3d.searchUnresolved') : hit.node.id} · ${this.tt('3d.searchLinks', { count: hit.node.degree })}`, + }); + } + + onChooseSuggestion(hit: Hit): void { + this.onPick(hit.index); + } + + private tt(key: string, vars: Record = {}): string { + return t(this.language, key, vars); + } +} diff --git a/src/world/WorldMapIndex.ts b/src/world/WorldMapIndex.ts new file mode 100644 index 0000000..9d10bbe --- /dev/null +++ b/src/world/WorldMapIndex.ts @@ -0,0 +1,98 @@ +import type { App } from 'obsidian'; +import { TFile, TFolder } from 'obsidian'; +import type { RadialSettings } from '../settings'; +import { buildWorldMap, normalizeVaultPath } from './buildWorldMap'; +import type { LinkTable, VisibleGraphState, VisibleWorldGraph, WorldFileRecord, WorldModel, WorldNode } from './types'; +import { ROOT_ID } from './types'; +import { buildVisibleWorldGraph, visualNodeId } from './visibleGraph'; + +export class WorldMapIndex { + model: WorldModel | null = null; + + constructor( + private app: App, + private settings: RadialSettings, + ) {} + + get ready(): boolean { + return this.model !== null; + } + + get nodes(): Map { + return this.model?.nodes ?? new Map(); + } + + get stats() { + return this.model?.stats ?? { + loadedEntries: 0, + scannedMarkdown: 0, + folders: 0, + notes: 0, + unresolved: 0, + hierarchyEdges: 0, + linkEdges: 0, + maxDepth: 0, + }; + } + + get linkEdgesBySource() { + return this.model?.linkEdgesBySource ?? new Map(); + } + + get linkEdgesByTarget() { + return this.model?.linkEdgesByTarget ?? new Map(); + } + + rebuild(settings: RadialSettings = this.settings): void { + this.settings = settings; + this.model = buildWorldMap( + this.collectVaultEntries(), + this.app.metadataCache.resolvedLinks as LinkTable, + this.app.metadataCache.unresolvedLinks as LinkTable, + settings, + ); + } + + buildVisibleGraph(state: VisibleGraphState): VisibleWorldGraph { + if (!this.model) this.rebuild(); + return buildVisibleWorldGraph(this.model!, state, this.settings); + } + + visualNodeId(id: string | null | undefined): string | null { + return this.model ? visualNodeId(this.model, id) : (id ?? null); + } + + getActiveNotePath(): string | null { + const active = this.app.workspace.getActiveFile(); + if (active && this.nodes.has(active.path)) return active.path; + return [...this.nodes.values()].find((node) => node.type === 'note')?.id ?? null; + } + + private collectVaultEntries(): WorldFileRecord[] { + const entries: WorldFileRecord[] = []; + const root = typeof this.app.vault.getRoot === 'function' ? this.app.vault.getRoot() : null; + const stack = root && Array.isArray(root.children) ? [...root.children] : []; + while (stack.length > 0) { + const entry = stack.pop(); + if (entry instanceof TFolder) { + const path = normalizeVaultPath(entry.path); + if (path) entries.push({ path, basename: entry.name, kind: 'folder' }); + if (Array.isArray(entry.children)) { + for (let index = entry.children.length - 1; index >= 0; index--) { + const child = entry.children[index]; + if (child) stack.push(child); + } + } + } else if (entry instanceof TFile && entry.extension === 'md') { + entries.push({ path: entry.path, basename: entry.basename, kind: 'note', size: entry.stat.size }); + } + } + if (entries.length === 0) { + for (const file of this.app.vault.getMarkdownFiles()) { + entries.push({ path: file.path, basename: file.basename, kind: 'note', size: file.stat.size }); + } + } + if (!entries.some((entry) => entry.path === ROOT_ID)) return entries; + return entries.filter((entry) => entry.path !== ROOT_ID); + } +} diff --git a/src/world/buildWorldMap.ts b/src/world/buildWorldMap.ts new file mode 100644 index 0000000..354efb3 --- /dev/null +++ b/src/world/buildWorldMap.ts @@ -0,0 +1,255 @@ +import type { RadialSettings } from '../settings'; +import type { LinkTable, WorldEdge, WorldFileRecord, WorldModel, WorldNode, WorldStats } from './types'; +import { ROOT_ID, ROOT_TITLE } from './types'; + +export function buildWorldMap( + records: WorldFileRecord[], + resolvedLinks: LinkTable, + unresolvedLinks: LinkTable, + settings: Pick, +): WorldModel { + const nodes = new Map(); + const hierarchyEdges: WorldEdge[] = []; + const linkEdges: WorldEdge[] = []; + const childrenByParent = new Map(); + const linkEdgesBySource = new Map(); + const linkEdgesByTarget = new Map(); + const folderRepresentatives = new Map(); + let loadedEntries = 0; + let scannedMarkdown = 0; + + const ignored = (path: string) => shouldIgnorePath(path, settings.ignoreFolders); + const addNode = (node: WorldNode) => { + if (nodes.has(node.id) || ignored(node.id)) return; + nodes.set(node.id, node); + }; + + addNode({ + id: ROOT_ID, + path: ROOT_ID, + title: ROOT_TITLE, + type: 'folder', + parentId: null, + depth: 0, + noteCount: 0, + linkCount: 0, + backlinkCount: 0, + descendantCount: 0, + }); + + const ensureFolder = (folderPath: string): string => { + const normalized = normalizeVaultPath(folderPath); + if (!normalized) return ROOT_ID; + let current = ROOT_ID; + for (const part of normalized.split('/').filter(Boolean)) { + const next = current ? `${current}/${part}` : part; + if (ignored(next)) return current; + if (!nodes.has(next)) { + addNode({ + id: next, + path: next, + title: basename(next), + type: 'folder', + parentId: current, + depth: depthOfPath(next), + noteCount: 0, + linkCount: 0, + backlinkCount: 0, + descendantCount: 0, + }); + } + current = next; + } + return current; + }; + + for (const record of records) { + loadedEntries++; + const path = normalizeVaultPath(record.path); + if (!path || ignored(path)) continue; + if (record.kind === 'folder') { + ensureFolder(path); + continue; + } + scannedMarkdown++; + const parentId = ensureFolder(parentPath(path)); + addNode({ + id: path, + path, + title: record.basename || basename(path).replace(/\.md$/i, ''), + type: 'note', + parentId, + depth: depthOfPath(path), + noteCount: 1, + linkCount: 0, + backlinkCount: 0, + descendantCount: 0, + }); + } + + identifyFolderRepresentatives(nodes, folderRepresentatives); + + const addLink = (source: string, target: string, weight: number, type: 'link' | 'unresolved-link') => { + if (source === target || !nodes.has(source) || !nodes.has(target)) return; + const edge: WorldEdge = { + id: `${type}:${source}->${target}:${linkEdges.length}`, + type, + source, + target, + weight, + }; + linkEdges.push(edge); + const sourceNode = nodes.get(source); + const targetNode = nodes.get(target); + if (sourceNode) sourceNode.linkCount += weight; + if (targetNode) targetNode.backlinkCount += weight; + pushMapArray(linkEdgesBySource, source, edge); + pushMapArray(linkEdgesByTarget, target, edge); + }; + + for (const [source, targets] of Object.entries(resolvedLinks)) { + if (!nodes.has(source) || ignored(source)) continue; + for (const [target, count] of Object.entries(targets ?? {})) { + if (!nodes.has(target) || ignored(target)) continue; + addLink(source, target, Math.max(1, Number(count) || 1), 'link'); + } + } + + if (settings.includeUnresolvedLinks) { + for (const [source, targets] of Object.entries(unresolvedLinks)) { + if (!nodes.has(source) || ignored(source)) continue; + for (const [linkText, count] of Object.entries(targets ?? {})) { + const id = `unresolved:${source}:${linkText}`; + if (!nodes.has(id)) { + const sourceNode = nodes.get(source); + addNode({ + id, + path: linkText, + title: linkText, + type: 'unresolved', + parentId: sourceNode?.parentId ?? ROOT_ID, + depth: (sourceNode?.depth ?? 0) + 1, + noteCount: 0, + linkCount: 0, + backlinkCount: 0, + descendantCount: 0, + }); + } + addLink(source, id, Math.max(1, Number(count) || 1), 'unresolved-link'); + } + } + } + + buildHierarchy(nodes, hierarchyEdges, childrenByParent); + computeFolderCounts(nodes); + const stats: WorldStats = { + loadedEntries, + scannedMarkdown, + folders: [...nodes.values()].filter((node) => node.type === 'folder').length, + notes: [...nodes.values()].filter((node) => node.type === 'note').length, + unresolved: [...nodes.values()].filter((node) => node.type === 'unresolved').length, + hierarchyEdges: hierarchyEdges.length, + linkEdges: linkEdges.length, + maxDepth: Math.max(0, ...[...nodes.values()].map((node) => node.depth)), + }; + + return { nodes, hierarchyEdges, linkEdges, childrenByParent, linkEdgesBySource, linkEdgesByTarget, folderRepresentatives, stats }; +} + +export function compareWorldNodes(a: WorldNode | undefined, b: WorldNode | undefined): number { + if (!a || !b) return a ? -1 : b ? 1 : 0; + const typeRank = (node: WorldNode) => (node.type === 'folder' ? 0 : node.type === 'note' ? 1 : node.type === 'external' ? 2 : 3); + return a.depth - b.depth || typeRank(a) - typeRank(b) || a.title.localeCompare(b.title) || a.id.localeCompare(b.id); +} + +export function normalizeVaultPath(path: string): string { + return String(path || '') + .trim() + .replace(/^\/+|\/+$/g, ''); +} + +export function parentPath(path: string): string { + const normalized = normalizeVaultPath(path); + const index = normalized.lastIndexOf('/'); + return index === -1 ? ROOT_ID : normalized.slice(0, index); +} + +export function basename(path: string): string { + const normalized = normalizeVaultPath(path); + const index = normalized.lastIndexOf('/'); + return index === -1 ? normalized : normalized.slice(index + 1); +} + +export function depthOfPath(path: string): number { + const normalized = normalizeVaultPath(path); + return normalized ? normalized.split('/').filter(Boolean).length : 0; +} + +export function shouldIgnorePath(path: string, ignoreFolders: string[]): boolean { + const normalized = normalizeVaultPath(path); + if (!normalized) return false; + return ignoreFolders.some((prefix) => normalized === prefix || normalized.startsWith(`${prefix}/`)); +} + +function identifyFolderRepresentatives(nodes: Map, folderRepresentatives: Map): void { + const notesByParent = new Map(); + for (const node of nodes.values()) { + if (node.type === 'note' && node.parentId !== null) pushMapArray(notesByParent, node.parentId, node); + } + for (const folder of nodes.values()) { + if (folder.type !== 'folder' || folder.id === ROOT_ID) continue; + const folderTitle = comparableTitle(folder.title); + const representative = (notesByParent.get(folder.id) ?? []).find((note) => comparableTitle(note.title) === folderTitle); + if (!representative) continue; + folder.representativeFile = representative.id; + representative.isRepresentativeFile = true; + representative.representativeFor = folder.id; + folderRepresentatives.set(folder.id, representative.id); + } +} + +function buildHierarchy( + nodes: Map, + hierarchyEdges: WorldEdge[], + childrenByParent: Map, +): void { + for (const node of nodes.values()) { + if (node.parentId === null || !nodes.has(node.parentId)) continue; + hierarchyEdges.push({ + id: `hierarchy:${node.parentId}->${node.id}`, + type: 'hierarchy', + source: node.parentId, + target: node.id, + weight: 1, + }); + pushMapArray(childrenByParent, node.parentId, node.id); + } + for (const children of childrenByParent.values()) { + children.sort((a, b) => compareWorldNodes(nodes.get(a), nodes.get(b))); + } +} + +function computeFolderCounts(nodes: Map): void { + const sorted = [...nodes.values()].sort((a, b) => b.depth - a.depth); + for (const node of sorted) { + node.descendantCount = node.type === 'note' ? 1 : node.noteCount; + if (!node.parentId || !nodes.has(node.parentId)) continue; + const parent = nodes.get(node.parentId); + if (!parent) continue; + const subtreeNotes = node.type === 'note' ? 1 : node.noteCount; + parent.descendantCount += subtreeNotes; + parent.noteCount += subtreeNotes; + parent.linkCount += node.linkCount; + parent.backlinkCount += node.backlinkCount; + } +} + +function comparableTitle(value: string): string { + return value.replace(/\.md$/i, '').trim().toLowerCase(); +} + +function pushMapArray(map: Map, key: K, value: V): void { + const values = map.get(key); + if (values) values.push(value); + else map.set(key, [value]); +} diff --git a/src/world/types.ts b/src/world/types.ts new file mode 100644 index 0000000..88f6188 --- /dev/null +++ b/src/world/types.ts @@ -0,0 +1,103 @@ +import type { ExternalDetailMode, HoverHighlightMode, LabelVisibility } from '../settings'; + +export const ROOT_ID = ''; +export const ROOT_TITLE = 'Vault'; + +export type WorldNodeType = 'folder' | 'note' | 'unresolved' | 'external'; + +export interface WorldNode { + id: string; + path: string; + title: string; + type: WorldNodeType; + parentId: string | null; + depth: number; + noteCount: number; + linkCount: number; + backlinkCount: number; + descendantCount: number; + representativeFile?: string; + isRepresentativeFile?: boolean; + representativeFor?: string; + externalProxy?: boolean; + externalParentId?: string; + externalAnchorPath?: string | null; +} + +export interface WorldEdge { + id: string; + type: 'hierarchy' | 'external-hierarchy' | 'link' | 'unresolved-link' | 'visible-link'; + source: string; + target: string; + weight: number; + rawCount?: number; + unresolvedCount?: number; + externalCount?: number; +} + +export interface WorldStats { + loadedEntries: number; + scannedMarkdown: number; + folders: number; + notes: number; + unresolved: number; + hierarchyEdges: number; + linkEdges: number; + maxDepth: number; +} + +export interface WorldModel { + nodes: Map; + hierarchyEdges: WorldEdge[]; + linkEdges: WorldEdge[]; + childrenByParent: Map; + linkEdgesBySource: Map; + linkEdgesByTarget: Map; + folderRepresentatives: Map; + stats: WorldStats; +} + +export interface WorldFileRecord { + path: string; + basename: string; + kind: 'folder' | 'note'; + size?: number; +} + +export type LinkTable = Record>; + +export interface VisibleGraphState { + mode: 'atlas' | 'focus'; + rootPath: string; + focusPath: string | null; + search: string; + atlasDepth: number; + focusSiblingLimit: number; + nodeLimit: number; + linkLimit: number; + externalLinkAnchorLimit: number; + showLinkOverlay: boolean; + showExternalLinks: boolean; + externalDetailMode: ExternalDetailMode; + showCompleteRoot: boolean; + hoverHighlightMode: HoverHighlightMode; + pinNeedsHoverLinks: boolean; + selectedNodeId: string | null; + selectedLink: WorldEdge | null; + hiddenLegendItems: string[]; + labelVisibility: LabelVisibility; +} + +export interface VisibleWorldGraph { + nodes: WorldNode[]; + nodesById: Map; + hierarchyEdges: WorldEdge[]; + linkEdges: WorldEdge[]; + hoverLinkEdges: WorldEdge[]; + rootId: string; + focusId: string | null; + hiddenNodeCount: number; + externalNodeCount: number; + externalFileCount: number; + externalGroupCount: number; +} diff --git a/src/world/visibleGraph.ts b/src/world/visibleGraph.ts new file mode 100644 index 0000000..8a504eb --- /dev/null +++ b/src/world/visibleGraph.ts @@ -0,0 +1,509 @@ +import { + DEFAULT_RADIAL_SETTINGS, + MAX_EXTERNAL_LINK_ANCHOR_LIMIT, + MAX_LINK_LIMIT, + MAX_RENDER_NODE_LIMIT, + clampNumber, + hoverHighlightsNoteLinks, + type RadialSettings, +} from '../settings'; +import { basename, compareWorldNodes, parentPath } from './buildWorldMap'; +import type { VisibleGraphState, VisibleWorldGraph, WorldEdge, WorldModel, WorldNode } from './types'; +import { ROOT_ID } from './types'; + +export function defaultVisibleGraphState(settings: RadialSettings): VisibleGraphState { + const hiddenLegendItems = settings.hiddenLegendItems.slice(); + const hidden = new Set(hiddenLegendItems); + return { + mode: 'atlas', + rootPath: ROOT_ID, + focusPath: null, + search: '', + atlasDepth: settings.atlasDepth, + focusSiblingLimit: settings.focusSiblingLimit, + nodeLimit: settings.renderNodeLimit, + linkLimit: settings.linkLimit, + externalLinkAnchorLimit: settings.externalLinkAnchorLimit, + showLinkOverlay: settings.showLinkOverlay || !hidden.has('link'), + showExternalLinks: settings.showExternalLinks, + externalDetailMode: settings.externalDetailMode, + showCompleteRoot: false, + hoverHighlightMode: settings.hoverHighlightMode, + pinNeedsHoverLinks: false, + selectedNodeId: null, + selectedLink: null, + hiddenLegendItems, + labelVisibility: settings.labelVisibility, + }; +} + +export function buildVisibleWorldGraph( + model: WorldModel, + state: VisibleGraphState, + settings: RadialSettings = DEFAULT_RADIAL_SETTINGS, +): VisibleWorldGraph { + if (state.mode === 'focus') return buildFocusGraph(model, state, settings); + return buildAtlasGraph(model, state, settings); +} + +export function visualNodeId(model: WorldModel, id: string | null | undefined): string | null { + if (id === null || id === undefined) return null; + const node = model.nodes.get(id); + return node?.isRepresentativeFile && node.representativeFor && model.nodes.has(node.representativeFor) + ? node.representativeFor + : id; +} + +function buildAtlasGraph(model: WorldModel, state: VisibleGraphState, settings: RadialSettings): VisibleWorldGraph { + const rootId = model.nodes.has(state.rootPath) ? state.rootPath : ROOT_ID; + const rootDepth = model.nodes.get(rootId)?.depth ?? 0; + const maxDepth = clampNumber(state.atlasDepth, 1, 80, settings.atlasDepth); + const query = normalizedQuery(state.search); + const visible = new Set(); + + const stack = [rootId]; + while (stack.length > 0) { + const id = stack.pop(); + const node = id ? model.nodes.get(id) : model.nodes.get(ROOT_ID); + if (!node) continue; + const relDepth = Math.max(0, node.depth - rootDepth); + const withinDepth = relDepth <= maxDepth; + const matches = query.length > 0 && nodeMatches(node, query); + if (withinDepth || matches) { + visible.add(node.id); + if (matches) addAncestors(model, node.id, visible, rootId); + } + if (withinDepth || matches) { + for (const child of [...(model.childrenByParent.get(node.id) ?? [])].reverse()) stack.push(child); + } + } + + if (query) { + for (const node of model.nodes.values()) { + if (!nodeMatches(node, query)) continue; + visible.add(node.id); + addAncestors(model, node.id, visible, rootId); + } + } + + addDirectFilesForVisibleFolders(model, visible); + return materializeVisibleGraph(model, visible, rootId, state, settings, null); +} + +function buildFocusGraph(model: WorldModel, state: VisibleGraphState, settings: RadialSettings): VisibleWorldGraph { + const activePath = state.focusPath && model.nodes.has(state.focusPath) ? state.focusPath : firstNote(model); + const visible = new Set([ROOT_ID]); + const siblingLimit = clampNumber(state.focusSiblingLimit, 10, 1000, settings.focusSiblingLimit); + + if (activePath) { + visible.add(activePath); + addAncestors(model, activePath, visible, ROOT_ID); + const active = model.nodes.get(activePath); + if (active?.parentId) { + for (const sibling of (model.childrenByParent.get(active.parentId) ?? []).slice(0, siblingLimit)) visible.add(sibling); + } + const connected = [ + ...(model.linkEdgesBySource.get(activePath) ?? []), + ...(model.linkEdgesByTarget.get(activePath) ?? []), + ].sort((a, b) => b.weight - a.weight); + for (const edge of connected.slice(0, Math.max(30, siblingLimit))) { + visible.add(edge.source); + visible.add(edge.target); + addAncestors(model, edge.source, visible, ROOT_ID); + addAncestors(model, edge.target, visible, ROOT_ID); + } + } + + const query = normalizedQuery(state.search); + if (query) { + for (const node of model.nodes.values()) { + if (!nodeMatches(node, query)) continue; + visible.add(node.id); + addAncestors(model, node.id, visible, ROOT_ID); + } + } + addDirectFilesForVisibleFolders(model, visible); + return materializeVisibleGraph(model, visible, ROOT_ID, state, settings, activePath); +} + +function materializeVisibleGraph( + model: WorldModel, + visible: Set, + rootId: string, + state: VisibleGraphState, + settings: RadialSettings, + focusId: string | null, +): VisibleWorldGraph { + let nodes = [...visible].map((id) => model.nodes.get(id)).filter((node): node is WorldNode => Boolean(node)); + nodes.sort(compareWorldNodes); + const budget = applyNodeBudget(model, nodes, rootId, state, settings, focusId); + nodes = foldRepresentativeNodes(model, budget.nodes); + + const nodeSet = new Set(nodes.map((node) => node.id)); + const hierarchyEdges = model.hierarchyEdges.filter((edge) => nodeSet.has(edge.source) && nodeSet.has(edge.target)); + const bundle = aggregateVisibleLinkEdges(model, nodeSet, state, settings, rootId); + nodes = nodes.concat(bundle.externalNodes); + const nodesById = new Map(nodes.map((node) => [node.id, node])); + return { + nodes, + nodesById, + hierarchyEdges: hierarchyEdges.concat(bundle.externalHierarchyEdges), + linkEdges: bundle.linkEdges, + hoverLinkEdges: bundle.hoverLinkEdges, + rootId, + focusId: visualNodeId(model, focusId), + hiddenNodeCount: budget.hiddenNodeCount, + externalNodeCount: bundle.externalNodes.length, + externalFileCount: bundle.externalNodes.filter((node) => node.externalProxy).length, + externalGroupCount: bundle.externalNodes.filter((node) => node.type === 'external' && !node.externalProxy).length, + }; +} + +function aggregateVisibleLinkEdges( + model: WorldModel, + visible: Set, + state: VisibleGraphState, + settings: RadialSettings, + rootId: string, +): { + linkEdges: WorldEdge[]; + hoverLinkEdges: WorldEdge[]; + externalNodes: WorldNode[]; + externalHierarchyEdges: WorldEdge[]; +} { + const hidden = new Set(state.hiddenLegendItems); + const wantsInternal = state.showLinkOverlay && !hidden.has('link'); + const wantsUnresolved = wantsInternal && !hidden.has('dashed-link'); + const wantsExternalOverlay = !hidden.has('outside-link'); + const wantsExternalNodes = !hidden.has('outside') || !hidden.has('outside-file'); + const needsHoverLinks = hoverHighlightsNoteLinks(state.hoverHighlightMode) || state.pinNeedsHoverLinks; + const showExternal = state.showExternalLinks && rootId !== ROOT_ID && (wantsExternalNodes || wantsExternalOverlay); + if (!wantsInternal && !wantsUnresolved && !needsHoverLinks && !showExternal) { + return { linkEdges: [], hoverLinkEdges: [], externalNodes: [], externalHierarchyEdges: [] }; + } + + const aggregate = new Map(); + const externalNodes = new Map(); + const externalHierarchyEdges: WorldEdge[] = []; + const externalLimit = clampNumber( + state.externalLinkAnchorLimit, + 0, + MAX_EXTERNAL_LINK_ANCHOR_LIMIT, + settings.externalLinkAnchorLimit, + ); + const externalContext = { fileCount: 0, overflowId: null as string | null }; + + for (const raw of model.linkEdges) { + let source = nearestVisibleAncestor(model, raw.source, visible, rootId); + let target = nearestVisibleAncestor(model, raw.target, visible, rootId); + let externalCount = 0; + + if (showExternal) { + if (!source && target) { + source = externalEndpointFor(model, raw.source, rootId, externalNodes, externalHierarchyEdges, externalLimit, externalContext, shouldUseExactExternal(raw, state, target)); + if (source) externalCount = 1; + } + if (source && !target) { + target = externalEndpointFor(model, raw.target, rootId, externalNodes, externalHierarchyEdges, externalLimit, externalContext, shouldUseExactExternal(raw, state, source)); + if (target) externalCount = 1; + } + } + + if (!source || !target || source === target) continue; + const key = `${source}->${target}`; + const edge = aggregate.get(key) ?? { + id: `visible-link:${key}`, + type: 'visible-link', + source, + target, + weight: 0, + rawCount: 0, + unresolvedCount: 0, + externalCount: 0, + }; + edge.weight += raw.weight; + edge.rawCount = (edge.rawCount ?? 0) + 1; + if (raw.type === 'unresolved-link') edge.unresolvedCount = (edge.unresolvedCount ?? 0) + 1; + edge.externalCount = (edge.externalCount ?? 0) + externalCount; + aggregate.set(key, edge); + } + + const all = [...aggregate.values()].sort((a, b) => linkRenderScore(b) - linkRenderScore(a)); + const allowsVisibleLink = (edge: WorldEdge) => { + if (edge.externalCount) return wantsExternalOverlay && (!edge.unresolvedCount || wantsUnresolved); + if (edge.unresolvedCount) return wantsUnresolved; + return wantsInternal; + }; + const allowsHoverLink = (edge: WorldEdge) => { + if (edge.externalCount) return showExternal && wantsExternalOverlay && (!edge.unresolvedCount || !hidden.has('dashed-link')); + if (edge.unresolvedCount) return !hidden.has('dashed-link'); + return !hidden.has('link'); + }; + const limit = state.showCompleteRoot + ? MAX_LINK_LIMIT + : clampNumber(state.linkLimit, 0, MAX_LINK_LIMIT, settings.linkLimit); + const linkEdges = all.filter(allowsVisibleLink).slice(0, limit); + const hoverLinkEdges = state.showCompleteRoot ? linkEdges : all.filter(allowsHoverLink); + const usedExternalIds = new Set(); + for (const edge of showExternal ? all.filter((e) => e.externalCount) : needsHoverLinks ? hoverLinkEdges : []) { + if (externalNodes.has(edge.source)) usedExternalIds.add(edge.source); + if (externalNodes.has(edge.target)) usedExternalIds.add(edge.target); + } + + const collected = collectExternalNodes(usedExternalIds, externalNodes); + return { + linkEdges, + hoverLinkEdges, + externalNodes: collected, + externalHierarchyEdges: externalHierarchyEdges.filter((edge) => usedExternalIds.has(edge.target)), + }; +} + +function applyNodeBudget( + model: WorldModel, + nodes: WorldNode[], + rootId: string, + state: VisibleGraphState, + settings: RadialSettings, + focusId: string | null, +): { nodes: WorldNode[]; hiddenNodeCount: number } { + if (state.showCompleteRoot && nodes.length <= MAX_RENDER_NODE_LIMIT) return { nodes, hiddenNodeCount: 0 }; + const limit = clampNumber(state.nodeLimit, 200, MAX_RENDER_NODE_LIMIT, settings.renderNodeLimit); + if (nodes.length <= limit) return { nodes, hiddenNodeCount: 0 }; + + const candidates = nodes + .slice() + .sort((a, b) => nodeRenderScore(b, rootId, focusId, state.search) - nodeRenderScore(a, rootId, focusId, state.search)); + const nodeById = new Map(nodes.map((node) => [node.id, node])); + const keep = new Set([rootId, focusId].filter((id): id is string => id !== null && id !== undefined)); + addDirectFileChildren(model, rootId, keep, limit, state.showCompleteRoot ? 128 : 64); + for (const node of candidates) { + if (keep.size >= limit) break; + addWithAncestors(model, node.id, keep, nodeById, limit); + if (node.type === 'folder') addDirectFileChildren(model, node.id, keep, limit, state.showCompleteRoot ? 18 : 8); + } + const kept = nodes.filter((node) => keep.has(node.id)); + return { nodes: kept, hiddenNodeCount: Math.max(0, nodes.length - kept.length) }; +} + +function foldRepresentativeNodes(model: WorldModel, nodes: WorldNode[]): WorldNode[] { + const visibleIds = new Set(nodes.map((node) => node.id)); + return nodes.filter((node) => !node.isRepresentativeFile || !node.representativeFor || !visibleIds.has(node.representativeFor)); +} + +function addDirectFilesForVisibleFolders(model: WorldModel, visible: Set): void { + for (const id of [...visible]) { + const node = model.nodes.get(id); + if (!node || node.type !== 'folder') continue; + for (const childId of model.childrenByParent.get(id) ?? []) { + const child = model.nodes.get(childId); + if (child?.type === 'note' || child?.type === 'unresolved') visible.add(childId); + } + } +} + +function addAncestors(model: WorldModel, id: string, visible: Set, stopId: string): void { + let current = model.nodes.get(id); + while (current && current.parentId !== null) { + visible.add(current.id); + if (current.id === stopId) break; + current = model.nodes.get(current.parentId); + } + visible.add(stopId || ROOT_ID); +} + +function addWithAncestors( + model: WorldModel, + id: string, + keep: Set, + nodeById: Map, + limit: number, +): void { + const chain: string[] = []; + let current = nodeById.get(id); + while (current && !keep.has(current.id)) { + chain.push(current.id); + current = current.parentId === null ? undefined : nodeById.get(current.parentId); + } + for (let i = chain.length - 1; i >= 0 && keep.size < limit; i--) keep.add(chain[i] ?? ROOT_ID); +} + +function addDirectFileChildren(model: WorldModel, parentId: string, keep: Set, limit: number, perParentLimit: number): void { + let added = 0; + for (const childId of model.childrenByParent.get(parentId) ?? []) { + if (keep.size >= limit || added >= perParentLimit) return; + const child = model.nodes.get(childId); + if (child?.type !== 'note' && child?.type !== 'unresolved') continue; + keep.add(childId); + added++; + } +} + +function nearestVisibleAncestor(model: WorldModel, id: string, visible: Set, rootId: string): string | null { + let current = model.nodes.get(id); + while (current) { + if (visible.has(current.id)) return current.id; + if (current.id === rootId) return rootId; + current = current.parentId === null ? undefined : model.nodes.get(current.parentId); + } + return visible.has(ROOT_ID) ? ROOT_ID : null; +} + +function externalEndpointFor( + model: WorldModel, + nodeId: string, + rootId: string, + externalNodes: Map, + externalHierarchyEdges: WorldEdge[], + limit: number, + context: { fileCount: number; overflowId: string | null }, + exact: boolean, +): string | null { + const node = model.nodes.get(nodeId); + if (!node) return null; + const anchorPath = externalAnchorPath(node, rootId); + if (!anchorPath) return null; + const groupId = externalGroupFor(model, anchorPath, rootId, externalNodes); + if (!exact || (node.type !== 'note' && node.type !== 'unresolved')) return groupId; + if (externalNodes.has(node.id)) return node.id; + if (context.fileCount >= limit) return externalOverflowFor(rootId, groupId, externalNodes, externalHierarchyEdges, context); + + externalNodes.set(node.id, { ...node, externalProxy: true, externalParentId: groupId, externalAnchorPath: anchorPath }); + context.fileCount++; + externalHierarchyEdges.push({ + id: `external-hierarchy:${groupId}->${node.id}`, + type: 'external-hierarchy', + source: groupId, + target: node.id, + weight: 1, + }); + return node.id; +} + +function externalGroupFor(model: WorldModel, anchorPath: string, rootId: string, externalNodes: Map): string { + const id = `external-group:${rootId}:${anchorPath}`; + if (!externalNodes.has(id)) { + const anchor = model.nodes.get(anchorPath); + externalNodes.set(id, { + id, + path: anchorPath, + title: `Outside: ${anchor?.title ?? basename(anchorPath)}`, + type: 'external', + parentId: null, + depth: (model.nodes.get(rootId)?.depth ?? 0) + 1, + noteCount: anchor?.noteCount ?? anchor?.descendantCount ?? 0, + linkCount: 0, + backlinkCount: 0, + descendantCount: anchor?.descendantCount ?? anchor?.noteCount ?? 0, + externalAnchorPath: anchorPath, + }); + } + return id; +} + +function externalOverflowFor( + rootId: string, + groupId: string, + externalNodes: Map, + externalHierarchyEdges: WorldEdge[], + context: { overflowId: string | null }, +): string { + if (!context.overflowId) { + context.overflowId = `external-overflow:${rootId}`; + externalNodes.set(context.overflowId, { + id: context.overflowId, + path: 'outside current root', + title: 'More outside files', + type: 'external', + parentId: null, + depth: 1, + noteCount: 0, + linkCount: 0, + backlinkCount: 0, + descendantCount: 0, + externalProxy: true, + externalParentId: groupId, + externalAnchorPath: null, + }); + externalHierarchyEdges.push({ + id: `external-hierarchy:${groupId}->${context.overflowId}`, + type: 'external-hierarchy', + source: groupId, + target: context.overflowId, + weight: 1, + }); + } + return context.overflowId; +} + +function collectExternalNodes(ids: Set, externalNodes: Map): WorldNode[] { + const collected = new Map(); + for (const id of ids) { + const node = externalNodes.get(id); + if (!node) continue; + if (node.externalParentId && externalNodes.has(node.externalParentId)) { + const parent = externalNodes.get(node.externalParentId); + if (parent) collected.set(parent.id, parent); + } + collected.set(id, node); + } + return [...collected.values()].sort(compareWorldNodes); +} + +function shouldUseExactExternal(edge: WorldEdge, state: VisibleGraphState, visibleEndpoint: string): boolean { + if (state.externalDetailMode === 'exact') return true; + if (state.externalDetailMode === 'grouped') return false; + if (state.selectedNodeId && (edge.source === state.selectedNodeId || edge.target === state.selectedNodeId || visibleEndpoint === state.selectedNodeId)) { + return true; + } + const selected = state.selectedLink; + return Boolean( + selected && + (selected.source === visibleEndpoint || + selected.target === visibleEndpoint || + selected.source === edge.source || + selected.target === edge.target), + ); +} + +function externalAnchorPath(node: WorldNode, rootId: string): string { + const nodePath = node.type === 'unresolved' ? parentPath(node.path) : node.path; + const pathParts = nodePath.split('/').filter(Boolean); + if (!rootId) return pathParts[0] ?? nodePath; + const rootParts = rootId.split('/').filter(Boolean); + let common = 0; + while (common < rootParts.length && common < pathParts.length && rootParts[common] === pathParts[common]) common++; + return common < rootParts.length ? rootParts.slice(0, common + 1).join('/') : pathParts.slice(0, common + 1).join('/'); +} + +function firstNote(model: WorldModel): string | null { + return [...model.nodes.values()].find((node) => node.type === 'note')?.id ?? null; +} + +function normalizedQuery(value: string): string { + return value.trim().toLowerCase(); +} + +function nodeMatches(node: WorldNode, query: string): boolean { + return node.title.toLowerCase().includes(query) || node.path.toLowerCase().includes(query); +} + +function nodeRenderScore(node: WorldNode, rootId: string, focusId: string | null, query: string): number { + let score = 0; + if (node.id === rootId) score += 1_000_000; + if (node.id === focusId) score += 900_000; + if (query && nodeMatches(node, normalizedQuery(query))) score += 100_000; + if (node.type === 'folder') score += 5000 + Math.min(4000, Math.log2((node.noteCount || node.descendantCount || 1) + 1) * 720); + if (node.type === 'note') score += 1000 + Math.min(3000, Math.log2((node.linkCount || 0) + (node.backlinkCount || 0) + 1) * 520); + if (node.type === 'unresolved') score += 350; + return score - node.depth * 3; +} + +function linkRenderScore(edge: WorldEdge): number { + return ( + (edge.weight || 1) * 10 + + (edge.rawCount || 0) * 4 + + (edge.externalCount || 0) * 25 + + (edge.unresolvedCount || 0) * 8 + ); +} diff --git a/styles.css b/styles.css index ce14a4e..b8e340a 100644 --- a/styles.css +++ b/styles.css @@ -1,919 +1,1036 @@ +/* Mini World Map — shared 3D/radial view, control panels, and overlays */ + +.galaxy-view-content { + padding: 0; + overflow: hidden; + position: relative; +} + +.galaxy-view-canvas { + position: absolute; + inset: 0; +} + +.galaxy-view-canvas:focus { + outline: none; +} + +/* ---------- 控制面板 ---------- */ + +.galaxy-panel { + position: absolute; + top: 10px; + left: 10px; + z-index: 12; + width: min(360px, calc(100vw - 20px)); + border-radius: 8px; + font-size: 12px; + user-select: none; + max-height: calc(100% - 20px); + overflow: hidden; + display: flex; + flex-direction: column; + background: var(--background-secondary); + border: 1px solid var(--background-modifier-border); + color: var(--text-normal); + box-shadow: var(--shadow-s); +} + +.galaxy-panel.gx-theme-dark { + background: var(--background-secondary); + border-color: var(--background-modifier-border); + color: var(--text-normal); +} + +.galaxy-panel.gx-theme-light { + background: var(--background-secondary); + border-color: var(--background-modifier-border); + color: var(--text-normal); +} + +.galaxy-panel-header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; + padding: 8px 10px; +} + +.galaxy-panel-stats { + font-family: var(--font-monospace); + font-size: 11px; + opacity: 0.75; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.galaxy-panel-collapse { + flex-shrink: 0; + width: 22px; + height: 22px; + padding: 0; + line-height: 1; + background: transparent; + border: 1px solid currentColor; + color: inherit; + opacity: 0.6; + border-radius: 6px; + cursor: pointer; + box-shadow: none; +} + +.galaxy-panel-body { + padding: 0 10px 10px; + overflow-y: auto; + min-height: 0; + scrollbar-gutter: stable; +} + +.galaxy-panel-body.is-hidden { + display: none; +} + +.galaxy-panel-section { + margin-top: 4px; +} + +.galaxy-panel-section-title { + font-size: 11px; + font-weight: 600; + opacity: 0.55; + letter-spacing: 0.05em; + margin: 8px 0 2px; +} + +.galaxy-panel-row { + display: flex; + flex-wrap: wrap; + gap: 6px; + margin-top: 10px; +} + +.galaxy-panel-row button { + flex: 1 1 104px; + min-height: 28px; + font-size: 11px; + line-height: 1.2; + padding: 5px 8px; + background: var(--background-modifier-hover); + border: 1px solid var(--background-modifier-border); + color: inherit; + border-radius: 6px; + cursor: pointer; + box-shadow: none; + min-width: 0; + white-space: normal; + overflow-wrap: anywhere; +} + +.galaxy-panel-row button:hover { + background: var(--background-modifier-active-hover); +} + +.galaxy-panel-dev { + margin-top: 10px; + border-top: 1px solid rgba(127, 127, 127, 0.2); + padding-top: 6px; +} + +.galaxy-panel-dev summary { + font-size: 11px; + opacity: 0.6; + cursor: pointer; +} + +.galaxy-panel-help { + margin-top: 6px; + display: flex; + flex-direction: column; + gap: 3px; + font-size: 11px; + opacity: 0.8; +} + +/* ---------- Lightroom 式滑杆 ---------- */ + +.gx-slider { + padding: 4px 0 2px; +} + +.gx-slider-head { + display: flex; + justify-content: space-between; + align-items: baseline; +} + +.gx-slider-label { + opacity: 0.85; +} + +.gx-slider-value { + font-family: var(--font-monospace); + font-size: 11px; + opacity: 0.9; +} + +.gx-slider-value.is-default { + opacity: 0.45; +} + +.gx-slider-track { + position: relative; + height: 18px; + cursor: ew-resize; + touch-action: none; +} + +.gx-slider-rail { + position: absolute; + left: 0; + right: 0; + top: 8px; + height: 2px; + border-radius: 1px; + background: currentColor; + opacity: 0.18; +} + +.gx-slider-fill { + position: absolute; + top: 8px; + height: 2px; + border-radius: 1px; + background: var(--interactive-accent); +} + +.gx-theme-light .gx-slider-fill { + background: var(--interactive-accent); +} + +.gx-slider-notch { + position: absolute; + left: 50%; + top: 4px; + width: 1.5px; + height: 10px; + margin-left: -0.75px; + background: currentColor; + opacity: 0.4; +} + +.gx-slider-thumb { + position: absolute; + top: 4px; + width: 10px; + height: 10px; + margin-left: -5px; + border-radius: 50%; + background: var(--background-primary); + border: 1.5px solid var(--interactive-accent); + pointer-events: none; +} + +.gx-theme-light .gx-slider-thumb { + background: var(--background-primary); + border-color: var(--interactive-accent); +} + +.gx-slider-bounds { + display: flex; + justify-content: space-between; + font-family: var(--font-monospace); + font-size: 9px; + opacity: 0.38; + margin-top: -2px; +} + +/* ---------- 浮层:标签与卡片 ---------- */ + +.gx-overlay { + position: absolute; + inset: 0; + z-index: 8; + pointer-events: none; + overflow: hidden; +} + +.gx-label { + position: absolute; + top: 0; + left: 0; + z-index: 1; + transform: translate3d(-1000px, -1000px, 0); + translate: -50% -100%; + font-size: 11px; + white-space: nowrap; + color: #dbe2f2; + text-shadow: 0 0 6px rgba(0, 0, 6, 0.95); + transition: opacity 150ms ease; +} + +.gx-daylight .gx-label { + color: #1f2933; + text-shadow: 0 0 6px rgba(246, 244, 239, 0.95); +} + +.gx-label-hover, +.gx-label-neighbor { + font-size: 11px; +} + +.gx-label-hover { + font-size: 13px; + font-weight: 600; +} + +.gx-card { + position: absolute; + top: 0; + left: 0; + z-index: 6; /* 卡片永远盖住浮层标签(G2 bug 修复) */ + width: 280px; + transform: translate3d(-1000px, -1000px, 0); + pointer-events: auto; + border-radius: 10px; + padding: 12px 14px; + font-size: 12px; + background: rgba(10, 14, 24, 0.78); + backdrop-filter: blur(16px); + border: 1px solid rgba(255, 255, 255, 0.08); + color: #e8ecf6; +} + +.gx-daylight .gx-card { + background: rgba(252, 250, 245, 0.88); + border: 1px solid rgba(31, 41, 51, 0.14); + color: #1f2933; +} + +.gx-card-title { + font-size: 15px; + font-weight: 600; + margin-bottom: 4px; +} + +.gx-card-meta { + display: flex; + align-items: center; + gap: 6px; + font-size: 11px; + opacity: 0.7; +} + +.gx-card-dot { + width: 8px; + height: 8px; + border-radius: 50%; + flex-shrink: 0; +} + +.gx-card-tags { + display: flex; + flex-wrap: wrap; + gap: 4px; + margin-top: 8px; +} + +.gx-card-tag { + font-size: 10px; + padding: 1px 7px; + border-radius: 8px; + background: rgba(127, 155, 255, 0.18); + border: 1px solid rgba(127, 155, 255, 0.3); +} + +.gx-card-stats { + margin-top: 8px; + font-size: 11px; + opacity: 0.75; +} + +.gx-card-snippet { + margin-top: 6px; + font-size: 11px; + line-height: 1.5; + opacity: 0.65; + max-height: 50px; + overflow: hidden; +} + +.gx-card-actions { + display: flex; + gap: 6px; + margin-top: 10px; +} + +.gx-card-actions button { + flex: 1; + font-size: 11px; + padding: 3px 8px; + background: rgba(127, 127, 127, 0.12); + border: 1px solid rgba(127, 127, 127, 0.25); + color: inherit; + border-radius: 6px; + cursor: pointer; + box-shadow: none; +} + +.gx-search-path { + font-size: 11px; + color: var(--text-muted); +} + +/* ---------- 开场遮罩 ---------- */ + +.gx-mask { + position: absolute; + inset: 0; + z-index: 20; + background: #000003; + display: flex; + align-items: center; + justify-content: center; + transition: opacity 600ms ease; +} + +.gx-mask.is-fading { + opacity: 0; + pointer-events: none; +} + +.gx-mask-text { + color: #aeb8d0; + font-size: 13px; + letter-spacing: 0.3em; + animation: gx-pulse 1.6s ease-in-out infinite; +} + +@keyframes gx-pulse { + 0%, + 100% { + opacity: 0.35; + } + 50% { + opacity: 0.9; + } +} + +/* ---------- 风格预设 chips 与折叠分区(面板 v3) ---------- */ + +.gx-chips { + display: flex; + flex-wrap: wrap; + gap: 5px; + margin-top: 8px; +} + +.gx-chip { + flex: 1 1 72px; + font-size: 11px; + line-height: 1.2; + padding: 5px 7px; + border-radius: 6px; + background: var(--background-modifier-hover); + border: 1px solid var(--background-modifier-border); + color: inherit; + cursor: pointer; + box-shadow: none; + white-space: normal; + overflow-wrap: anywhere; +} + +.gx-chip:hover { + background: var(--background-modifier-active-hover); +} + +.gx-chip.is-active { + background: var(--interactive-accent); + border-color: var(--interactive-accent); + color: var(--text-on-accent); +} + +.gx-section { + margin-top: 8px; + border-top: 1px solid rgba(127, 127, 127, 0.16); + padding-top: 4px; +} + +.gx-section > summary { + font-size: 11px; + font-weight: 600; + opacity: 0.6; + letter-spacing: 0.05em; + cursor: pointer; + list-style: none; + display: flex; + align-items: center; + gap: 4px; +} + +.gx-section > summary::before { + content: '▸'; + font-size: 9px; + transition: transform 120ms ease; +} + +.gx-section[open] > summary::before { + transform: rotate(90deg); +} + +.gx-section-body { + padding-top: 2px; +} + +.gx-theme-select { + width: 100%; + margin-top: 8px; + font-size: 11px; + padding: 5px 6px; + border-radius: 6px; + background: var(--background-modifier-hover); + border: 1px solid var(--background-modifier-border); + color: inherit; + cursor: pointer; +} + +.gx-theme-select option { + color: var(--text-normal); + background: var(--background-primary); +} + +/* ---------- M4 移动端 ---------- */ + +/* .gx-mobile .gx-card 比 .gx-card 更具体 → 靠选择器特异性覆盖 left/top/transform + (JS 已在移动端清掉内联 transform,见 OverlayManager.setSelection) */ +.gx-mobile .gx-card { + left: 8px; + right: 8px; + top: auto; + /* 自适应:实测 navbar 重叠(--gx-bottom-inset)与 iPhone 横条安全区取大者 */ + bottom: calc(10px + max(var(--gx-bottom-inset, 0px), env(safe-area-inset-bottom, 0px))); + width: auto; + max-height: 40vh; + overflow-y: auto; + transform: none; +} + +.gx-mask-btn { + font-size: 13px; + padding: 8px 18px; + border-radius: 8px; + background: rgba(127, 155, 255, 0.2); + border: 1px solid rgba(127, 155, 255, 0.5); + color: #e8ecf6; + cursor: pointer; +} + +/* ---------- Mini World Map shared / radial mode ---------- */ + .mini-world-map-view { - --mwm-bg: var(--background-primary); - --mwm-graph-bg: var(--background-primary); - --mwm-panel: color-mix(in srgb, var(--background-secondary) 88%, var(--background-primary)); - --mwm-panel-alt: var(--background-modifier-hover); - --mwm-border: var(--background-modifier-border); - --mwm-text: var(--text-normal); - --mwm-muted: var(--text-muted); - --mwm-graph-line: var(--graph-line, color-mix(in srgb, var(--text-muted) 54%, transparent)); - --mwm-node: var(--graph-node, color-mix(in srgb, var(--text-muted) 76%, var(--background-primary))); - --mwm-node-focused: var(--graph-node-focused, var(--interactive-accent)); - --mwm-folder: color-mix(in srgb, var(--mwm-node) 74%, var(--text-normal) 26%); - --mwm-folder-meta: color-mix(in srgb, var(--mwm-folder) 86%, var(--mwm-node-focused) 14%); - --mwm-folder-ring: color-mix(in srgb, var(--mwm-graph-line) 58%, var(--text-accent) 42%); - --mwm-tree: color-mix(in srgb, var(--mwm-folder-ring) 78%, var(--text-normal) 22%); - --mwm-ring-guide: color-mix(in srgb, var(--mwm-folder-ring) 42%, transparent); - --mwm-note: var(--mwm-node); - --mwm-file-ring: color-mix(in srgb, var(--mwm-node) 44%, var(--mwm-border)); - --mwm-link: color-mix(in srgb, var(--mwm-graph-line) 58%, var(--text-accent) 42%); - --mwm-link-highlight: var(--mwm-node-focused); - --mwm-external: var(--mwm-node); - --mwm-external-link: color-mix(in srgb, var(--color-orange, var(--text-accent)) 62%, var(--text-muted) 38%); - --mwm-unresolved: var(--graph-node-unresolved, color-mix(in srgb, var(--text-error) 64%, var(--mwm-node))); - --mwm-node-stroke: color-mix(in srgb, var(--mwm-graph-bg) 84%, transparent); - --mwm-node-glow: color-mix(in srgb, var(--mwm-node-focused) 34%, transparent); - --mwm-label-text: var(--mwm-text); - --mwm-label-stroke: var(--mwm-graph-bg); - --mwm-shadow-soft: 0 12px 36px color-mix(in srgb, var(--background-modifier-box-shadow, rgba(0, 0, 0, 0.22)) 50%, transparent); - height: 100%; - display: flex; - flex-direction: column; - background: var(--mwm-bg); - color: var(--mwm-text); + padding: 0; + overflow: hidden; + position: relative; + background: var(--background-primary); + color: var(--text-normal); } -.theme-light .mini-world-map-view, -.theme-dark .mini-world-map-view.is-day-scheme, .mini-world-map-view.is-day-scheme { - --mwm-graph-bg: #f7f8fb; - --mwm-graph-line: rgba(91, 103, 122, 0.34); - --mwm-node: #596575; - --mwm-node-focused: #4f5ee8; - --mwm-folder: #3f4a5a; - --mwm-folder-meta: #4f5ee8; - --mwm-folder-ring: #647086; - --mwm-tree: rgba(91, 103, 122, 0.5); - --mwm-ring-guide: rgba(148, 163, 184, 0.15); - --mwm-note: #536073; - --mwm-file-ring: rgba(64, 76, 94, 0.36); - --mwm-link: rgba(82, 94, 112, 0.42); - --mwm-link-highlight: #5368ee; - --mwm-external: #5b5fe0; - --mwm-external-link: rgba(203, 118, 34, 0.58); - --mwm-unresolved: #dc2626; - --mwm-node-stroke: rgba(247, 248, 251, 0.9); - --mwm-node-glow: rgba(79, 94, 232, 0.24); - --mwm-label-text: #1f2937; - --mwm-label-stroke: rgba(247, 248, 251, 0.96); + --mwm-radial-bg: #f7f8fb; + --mwm-radial-text: #1f2937; + --mwm-radial-label: #1f2937; + --mwm-radial-label-root: #4f5ee8; + --mwm-radial-label-shadow: rgba(255, 255, 255, 0.92); + --mwm-legend-root-color: #4f5ee8; + --mwm-legend-folder-color: #3f4a5a; + --mwm-legend-meta-color: #4f5ee8; + --mwm-legend-note-color: #536073; + --mwm-legend-external-color: #5b5fe0; + --mwm-legend-unresolved-color: #dc2626; + --mwm-legend-tree-color: #5b677a; + --mwm-legend-link-color: #525e70; + --mwm-legend-external-link-color: #cb7622; + --mwm-floating-bg: rgba(255, 255, 255, 0.82); + --mwm-floating-border: rgba(31, 41, 51, 0.16); + --mwm-floating-color: #1f2937; } -.theme-dark .mini-world-map-view, -.theme-light .mini-world-map-view.is-night-scheme, -.mini-world-map-view.is-night-scheme { - --mwm-graph-bg: #0f1117; - --mwm-graph-line: rgba(156, 163, 175, 0.3); - --mwm-node: #9aa4b6; - --mwm-node-focused: #7c9cff; - --mwm-folder: #d7dae2; - --mwm-folder-meta: #f59e0b; - --mwm-folder-ring: #818cf8; - --mwm-tree: rgba(129, 140, 248, 0.36); - --mwm-ring-guide: rgba(129, 140, 248, 0.13); - --mwm-note: #a1a8b5; - --mwm-file-ring: rgba(124, 156, 255, 0.34); - --mwm-link: rgba(148, 163, 184, 0.38); - --mwm-link-highlight: #aab6ff; - --mwm-external: #c4b5fd; - --mwm-external-link: rgba(251, 146, 60, 0.66); - --mwm-unresolved: #fb7185; - --mwm-node-stroke: rgba(15, 17, 23, 0.9); - --mwm-node-glow: rgba(124, 156, 255, 0.3); - --mwm-label-text: #f8fafc; - --mwm-label-stroke: rgba(15, 17, 23, 0.96); +.mini-world-map-view.is-night-scheme, +.theme-dark .mini-world-map-view:not(.is-day-scheme) { + --mwm-radial-bg: #0f1117; + --mwm-radial-text: #f8fafc; + --mwm-radial-label: #f8fafc; + --mwm-radial-label-root: #7c9cff; + --mwm-radial-label-shadow: rgba(0, 0, 0, 0.92); + --mwm-legend-root-color: #7c9cff; + --mwm-legend-folder-color: #d7dae2; + --mwm-legend-meta-color: #f59e0b; + --mwm-legend-note-color: #a1a8b5; + --mwm-legend-external-color: #c4b5fd; + --mwm-legend-unresolved-color: #fb7185; + --mwm-legend-tree-color: #818cf8; + --mwm-legend-link-color: #94a3b8; + --mwm-legend-external-link-color: #fb923c; + --mwm-floating-bg: rgba(10, 14, 24, 0.64); + --mwm-floating-border: rgba(255, 255, 255, 0.12); + --mwm-floating-color: #e8ecf6; } -.mwm-toolbar { - display: flex; - align-items: center; - gap: 8px; - min-height: 52px; - padding: 8px 10px; - border-bottom: 1px solid var(--mwm-border); - background: color-mix(in srgb, var(--mwm-bg) 94%, var(--mwm-panel)); - flex-wrap: wrap; +.mwm-radial-host { + position: absolute; + inset: 0; + overflow: hidden; + background: var(--mwm-radial-bg, #0f1117); + cursor: grab; + touch-action: none; } -.mwm-icon-button { - width: 34px; - height: 34px; - display: inline-flex; - align-items: center; - justify-content: center; - border: 1px solid transparent; - border-radius: 6px; - background: transparent; - color: var(--mwm-muted); - cursor: pointer; - transition: background-color 120ms ease, border-color 120ms ease, color 120ms ease; +.mwm-radial-host.is-pointing { + cursor: pointer; } -.mwm-icon-button svg { - width: 18px; - height: 18px; +.mwm-radial-canvas { + display: block; + width: 100%; + height: 100%; } -.mwm-icon-button:hover, -.mwm-edge-list button:hover { - background: var(--mwm-panel-alt); - border-color: var(--mwm-border); - color: var(--mwm-text); +.mwm-radial-labels { + position: absolute; + inset: 0; + pointer-events: none; + z-index: 4; + overflow: hidden; } -.mwm-search { - min-width: 220px; - max-width: 420px; - flex: 1 1 260px; - height: 38px; +.mwm-radial-label { + position: absolute; + top: 0; + left: 0; + translate: -50% 0; + max-width: 160px; + padding: 0 3px; + color: var(--mwm-radial-label, #f8fafc); + font-size: 12px; + line-height: 1.18; + text-align: center; + text-shadow: 0 1px 4px var(--mwm-radial-label-shadow, rgba(0, 0, 0, 0.92)); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; } -.mwm-field { - display: inline-flex; - align-items: center; - gap: 6px; - color: var(--mwm-muted); - font-size: 12px; +.mwm-radial-label.is-root { + font-weight: 600; + color: var(--mwm-radial-label-root, #aab6ff); } -.mwm-field input { - width: 58px; - height: 30px; -} - -.mwm-select { - height: 30px; - min-width: 96px; -} - -.mwm-field-zoom input { - width: 120px; -} - -.mwm-value { - min-width: 42px; - color: var(--mwm-text); - font-variant-numeric: tabular-nums; -} - -.mwm-toggle { - display: inline-flex; - align-items: center; - gap: 6px; - height: 30px; - padding: 0 8px; - border: 1px solid var(--mwm-border); - border-radius: 6px; - color: var(--mwm-muted); - background: var(--mwm-panel); - font-size: 12px; -} - -.mwm-meta { - display: flex; - align-items: center; - gap: 6px; - min-height: 30px; - padding: 5px 10px; - border-bottom: 1px solid var(--mwm-border); - overflow-x: auto; - background: color-mix(in srgb, var(--mwm-bg) 96%, var(--mwm-panel)); -} - -.mwm-chip { - white-space: nowrap; - padding: 2px 7px; - border: 1px solid var(--mwm-border); - border-radius: 999px; - color: var(--mwm-muted); - background: color-mix(in srgb, var(--mwm-panel) 68%, transparent); - font-size: 12px; -} - -.mwm-chip-warn { - color: var(--text-warning); -} - -.mwm-body { - min-height: 0; - flex: 1; - display: flex; - --mwm-side-width: 360px; -} - -.mwm-graph-host { - min-width: 0; - min-height: 0; - flex: 1 1 auto; - overflow: hidden; - position: relative; - background: var(--mwm-graph-bg); - box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--mwm-border) 30%, transparent); - cursor: grab; - user-select: none; - touch-action: none; -} - -.mwm-graph-host.is-panning { - cursor: grabbing; -} - -.mwm-graph-host.is-pointing { - cursor: pointer; -} - -.mwm-splitter { - flex: 0 0 6px; - min-height: 0; - cursor: col-resize; - border-left: 1px solid var(--mwm-border); - border-right: 1px solid var(--mwm-border); - background: color-mix(in srgb, var(--background-secondary-alt) 76%, var(--mwm-bg)); -} - -.mwm-splitter:hover, -.mwm-body.is-resizing .mwm-splitter { - background: var(--text-accent); -} - -.mwm-side-panel { - flex: 0 0 var(--mwm-side-width); - width: var(--mwm-side-width); - min-width: 220px; - max-width: 720px; - min-height: 0; - overflow: hidden; - display: flex; - flex-direction: column; - background: var(--mwm-panel); - box-shadow: var(--mwm-shadow-soft); - container-type: inline-size; -} - -.mini-world-map-view.is-fullscreen .mwm-toolbar, -.mini-world-map-view.is-fullscreen .mwm-meta, -.mini-world-map-view.is-fullscreen .mwm-splitter, -.mini-world-map-view.is-fullscreen .mwm-side-panel { - display: none; -} - -.mini-world-map-view.is-fullscreen .mwm-body, -.mini-world-map-view.is-fullscreen .mwm-graph-host { - min-height: 0; - flex: 1 1 auto; -} - -.mwm-floating-button { - z-index: 5; - width: 36px; - height: 36px; - display: inline-flex; - align-items: center; - justify-content: center; - border: 1px solid var(--mwm-border); - border-radius: 6px; - background: color-mix(in srgb, var(--mwm-panel) 94%, transparent); - color: var(--mwm-text); - cursor: pointer; - box-shadow: var(--mwm-shadow-soft); +.mwm-radial-label.is-dim { + opacity: 0.38; } +.galaxy-mode-row, +.mwm-panel-tabs, .mwm-floating-controls { - position: absolute; - top: 12px; - left: 12px; - z-index: 6; - display: flex; - gap: 8px; + display: flex; + flex-wrap: wrap; + gap: 6px; } -.mwm-theme-button { - position: absolute; - top: 12px; - left: auto; - right: 12px; +.galaxy-mode-row { + margin: 0; } -.mwm-theme-button.is-auto { - color: var(--mwm-muted); +.mwm-mode-switch { + padding: 8px; + margin: 4px 0 10px; + border: 1px solid var(--background-modifier-border); + border-radius: 8px; + background: var(--background-primary); } -.mwm-theme-button.is-day, -.mwm-theme-button.is-night { - border-color: color-mix(in srgb, var(--text-accent) 54%, var(--mwm-border)); - color: var(--mwm-text); +.mwm-mode-switch-label { + margin-bottom: 6px; + color: var(--text-muted); + font-size: 10px; + font-weight: 600; + line-height: 1.2; + text-transform: uppercase; } -.mwm-empty { - padding: 24px; - color: var(--mwm-muted); +.mwm-mode-row { + flex-wrap: nowrap; + gap: 4px; + padding: 3px; + border-radius: 7px; + background: var(--background-secondary); } -.mwm-panel-header { - flex: 0 0 auto; - padding: 14px 12px 10px; +.mwm-mode-row button { + flex: 1 1 0; + min-width: 0; + min-height: 32px; + padding: 5px 7px; + border: 0; + border-radius: 5px; + background: transparent; + color: var(--text-muted); + box-shadow: none; + font-size: 11px; + font-weight: 600; + line-height: 1.2; + white-space: normal; + overflow-wrap: anywhere; + cursor: pointer; } -.mwm-panel-title-wrap { - min-width: 0; +.mwm-mode-row button:hover { + background: var(--background-modifier-hover); + color: var(--text-normal); } -.mwm-panel-title { - font-size: 16px; - font-weight: 700; - color: var(--mwm-text); +.mwm-mode-row button.is-active { + background: var(--interactive-accent); + color: var(--text-on-accent); } -.mwm-panel-subtitle { - margin-top: 2px; - color: var(--mwm-muted); - font-size: 12px; - font-variant-numeric: tabular-nums; +.mwm-panel-settings { + padding-top: 8px; + border-top: 1px solid var(--background-modifier-border); } -.mwm-page-tabs { - flex: 0 0 auto; - display: grid; - grid-template-columns: repeat(auto-fit, minmax(112px, 1fr)); - gap: 6px; - padding: 0 12px 12px; - border-bottom: 1px solid var(--mwm-border); +.mwm-panel-tabs { + gap: 4px; } -.mwm-page-tab { - min-width: 0; - min-height: 32px; - display: inline-flex; - align-items: center; - justify-content: flex-start; - gap: 7px; - padding: 0 9px; - border: 1px solid var(--mwm-border); - border-radius: 6px; - background: color-mix(in srgb, var(--mwm-bg) 70%, var(--mwm-panel)); - color: var(--mwm-muted); - cursor: pointer; - font-size: 12px; - transition: background-color 120ms ease, border-color 120ms ease, color 120ms ease; +.mwm-panel-tabs button { + flex: 1 1 86px; + min-height: 28px; + font-size: 11px; + line-height: 1.2; + padding: 4px 7px; + border: 1px solid var(--background-modifier-border); + border-radius: 6px; + background: transparent; + color: inherit; + cursor: pointer; + box-shadow: none; + min-width: 0; + white-space: normal; + overflow-wrap: anywhere; } -.mwm-page-tab svg { - flex: 0 0 auto; - width: 15px; - height: 15px; +.mwm-panel-tabs button.is-active, +.galaxy-panel-row button.is-active { + background: var(--interactive-accent); + border-color: var(--interactive-accent); + color: var(--text-on-accent); } -.mwm-page-tab span { - min-width: 0; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; +.mwm-map-panel, +.mwm-radial-panel { + width: min(360px, calc(100vw - 20px)); + color: var(--text-normal); } -.mwm-page-tab:hover, -.mwm-page-tab.is-active { - background: var(--mwm-panel-alt); - border-color: color-mix(in srgb, var(--text-accent) 58%, var(--mwm-border)); - color: var(--mwm-text); +.mwm-panel-page { + display: flex; + flex-direction: column; + gap: 8px; + padding-top: 10px; } -.mwm-page-tab.is-active { - box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--text-accent) 30%, transparent); +.mwm-panel-field, +.mwm-panel-toggle { + display: grid; + grid-template-columns: minmax(0, 1fr); + align-items: stretch; + gap: 4px; + font-size: 11px; } -.mwm-panel-content { - min-height: 0; - flex: 1 1 auto; - overflow: auto; - padding: 12px; +.mwm-panel-toggle { + grid-template-columns: auto minmax(0, 1fr); + justify-content: start; } -.mwm-panel-page-title { - color: var(--mwm-text); - font-size: 15px; - font-weight: 700; - line-height: 1.3; -} - -.mwm-panel-page-desc { - margin-top: 4px; - color: var(--mwm-muted); - font-size: 12px; - line-height: 1.35; -} - -.mwm-panel-section { - display: grid; - gap: 8px; - margin-top: 14px; -} - -.mwm-panel-section > .mwm-side-heading { - margin-top: 0; -} - -.mwm-panel-action-grid, -.mwm-panel-action-row { - display: grid; - gap: 8px; -} - -.mwm-panel-action-grid { - grid-template-columns: repeat(auto-fit, minmax(124px, 1fr)); -} - -.mwm-panel-action-grid-three { - grid-template-columns: repeat(auto-fit, minmax(82px, 1fr)); -} - -.mwm-panel-action-row { - grid-template-columns: repeat(auto-fit, minmax(42px, 1fr)); -} - -.mwm-panel-action { - min-width: 0; - min-height: 34px; - display: inline-flex; - align-items: center; - justify-content: center; - gap: 7px; - padding: 0 8px; - border: 1px solid var(--mwm-border); - border-radius: 6px; - background: color-mix(in srgb, var(--mwm-bg) 76%, var(--mwm-panel)); - color: var(--mwm-text); - cursor: pointer; - font-size: 12px; - transition: background-color 120ms ease, border-color 120ms ease, color 120ms ease; -} - -.mwm-panel-action svg { - flex: 0 0 auto; - width: 16px; - height: 16px; -} - -.mwm-panel-action span { - min-width: 0; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} - -@container (max-width: 290px) { - .mwm-page-tabs { - grid-template-columns: repeat(5, minmax(0, 1fr)); - } - - .mwm-page-tab { - justify-content: center; - padding: 0 6px; - } - - .mwm-page-tab span { - display: none; - } - - .mwm-panel-action-grid, - .mwm-panel-action-grid-three { - grid-template-columns: 1fr; - } -} - -@container (min-width: 560px) { - .mwm-panel-content-view, - .mwm-panel-content-controls, - .mwm-panel-content-defaults { - display: grid; - grid-template-columns: repeat(2, minmax(0, 1fr)); - align-content: start; - gap: 12px; - } - - .mwm-panel-content-view > .mwm-panel-page-title, - .mwm-panel-content-view > .mwm-panel-page-desc, - .mwm-panel-content-controls > .mwm-panel-page-title, - .mwm-panel-content-controls > .mwm-panel-page-desc, - .mwm-panel-content-defaults > .mwm-panel-page-title, - .mwm-panel-content-defaults > .mwm-panel-page-desc { - grid-column: 1 / -1; - } - - .mwm-panel-content-view > .mwm-panel-section, - .mwm-panel-content-controls > .mwm-panel-section, - .mwm-panel-content-defaults > .mwm-panel-section { - margin-top: 0; - } -} - -.mwm-panel-action:hover, -.mwm-panel-action.is-active { - background: var(--mwm-panel-alt); - border-color: color-mix(in srgb, var(--text-accent) 58%, var(--mwm-border)); -} - -.mwm-panel-action:disabled, -.mwm-icon-button:disabled { - opacity: 0.48; - cursor: not-allowed; -} - -.mwm-panel-field { - display: grid; - gap: 6px; - color: var(--mwm-muted); - font-size: 12px; -} - -.mwm-panel-label { - color: var(--mwm-muted); - font-size: 12px; - font-weight: 600; +.mwm-panel-field > span, +.mwm-panel-toggle > span { + min-width: 0; + overflow-wrap: anywhere; } .mwm-panel-field input, .mwm-panel-field select, .mwm-panel-field textarea { - width: 100%; - min-width: 0; -} - -.mwm-panel-field input, -.mwm-panel-field select { - min-height: 32px; + min-width: 0; + width: 100%; + max-width: 100%; + font-size: 11px; } .mwm-panel-field textarea { - min-height: 128px; - resize: vertical; - line-height: 1.35; -} - -.mwm-panel-field-header { - display: flex; - align-items: center; - justify-content: space-between; - gap: 8px; -} - -.mwm-panel-range-field input { - width: 100%; -} - -.mwm-panel-toggle { - min-height: 34px; - display: flex; - align-items: center; - gap: 8px; - padding: 0 10px; - border: 1px solid var(--mwm-border); - border-radius: 6px; - background: color-mix(in srgb, var(--mwm-bg) 76%, var(--mwm-panel)); - color: var(--mwm-text); - font-size: 12px; + min-height: 72px; + resize: vertical; } .mwm-panel-toggle input { - flex: 0 0 auto; -} - -.mwm-canvas { - display: block; - width: 100%; - height: 100%; - outline: none; -} - -.mwm-side-title { - font-weight: 700; - font-size: 16px; - line-height: 1.3; - margin-bottom: 6px; - color: var(--mwm-text); -} - -.mwm-side-path { - color: var(--mwm-muted); - font-size: 12px; - line-height: 1.35; - overflow-wrap: anywhere; - margin-bottom: 12px; -} - -.mwm-side-heading { - margin-top: 16px; - margin-bottom: 6px; - color: var(--mwm-muted); - font-size: 12px; - font-weight: 700; - text-transform: uppercase; - letter-spacing: 0; -} - -.mwm-side-muted { - color: var(--mwm-muted); - font-size: 12px; -} - -.mwm-facts { - display: grid; - grid-template-columns: 80px minmax(0, 1fr); - gap: 6px 10px; - padding: 10px; - border: 1px solid var(--mwm-border); - border-radius: 6px; - background: color-mix(in srgb, var(--mwm-bg) 76%, var(--mwm-panel)); - font-size: 12px; -} - -.mwm-facts span:nth-child(odd) { - color: var(--mwm-muted); -} - -.mwm-side-actions { - display: flex; - flex-wrap: wrap; - gap: 8px; - margin-top: 10px; -} - -.mwm-text-button { - min-height: 32px; - padding: 0 10px; - border: 1px solid var(--mwm-border); - border-radius: 6px; - background: color-mix(in srgb, var(--mwm-bg) 76%, var(--mwm-panel)); - color: var(--mwm-text); - cursor: pointer; - transition: background-color 120ms ease, border-color 120ms ease; -} - -.mwm-text-button:hover { - background: var(--mwm-panel-alt); -} - -.mwm-pin-group { - display: grid; - gap: 8px; - padding: 8px; - border: 1px solid var(--mwm-border); - border-radius: 6px; - background: color-mix(in srgb, var(--mwm-bg) 72%, var(--mwm-panel)); -} - -.mwm-pin-group + .mwm-pin-group { - margin-top: 8px; -} - -.mwm-pin-group-header { - display: flex; - align-items: center; - justify-content: space-between; - gap: 8px; -} - -.mwm-pin-group-title { - min-width: 0; - display: inline-flex; - align-items: center; - gap: 7px; - color: var(--mwm-text); - font-size: 12px; - font-weight: 700; -} - -.mwm-pin-count { - min-width: 20px; - padding: 1px 6px; - border: 1px solid var(--mwm-border); - border-radius: 999px; - color: var(--mwm-muted); - font-size: 11px; - font-weight: 500; - text-align: center; -} - -.mwm-pin-list { - display: grid; - gap: 6px; -} - -.mwm-pin-row { - display: grid; - grid-template-columns: 18px minmax(0, 1fr) auto; - align-items: center; - gap: 7px; -} - -.mwm-pin-check { - margin: 0; -} - -.mwm-pin-main { - min-width: 0; - min-height: 38px; - display: grid; - gap: 2px; - padding: 6px 8px; - border: 1px solid var(--mwm-border); - border-radius: 6px; - background: color-mix(in srgb, var(--mwm-bg) 78%, var(--mwm-panel)); - color: var(--mwm-text); - text-align: left; - cursor: pointer; -} - -.mwm-pin-main:hover { - background: var(--mwm-panel-alt); -} - -.mwm-pin-title, -.mwm-pin-meta { - min-width: 0; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} - -.mwm-pin-title { - font-size: 12px; - font-weight: 600; -} - -.mwm-pin-meta { - color: var(--mwm-muted); - font-size: 11px; -} - -.mwm-pin-actions { - display: inline-flex; - align-items: center; - gap: 4px; -} - -.mwm-pin-icon-button { - width: 28px; - height: 28px; -} - -.mwm-pin-icon-button svg { - width: 14px; - height: 14px; -} - -.mwm-edge-list { - list-style: none; - padding: 0; - margin: 0; - display: grid; - gap: 4px; -} - -.mwm-edge-list button { - width: 100%; - min-height: 28px; - text-align: left; - border: 1px solid var(--mwm-border); - border-radius: 6px; - background: color-mix(in srgb, var(--mwm-bg) 76%, var(--mwm-panel)); - color: var(--mwm-text); - font-size: 12px; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - cursor: pointer; - transition: background-color 120ms ease, border-color 120ms ease; -} - -.mwm-legend { - display: grid; - gap: 7px; - padding: 10px; - border: 1px solid var(--mwm-border); - border-radius: 6px; - background: color-mix(in srgb, var(--mwm-bg) 76%, var(--mwm-panel)); + flex: 0 0 auto; } .mwm-legend-item { - display: grid; - grid-template-columns: 24px 82px minmax(0, 1fr); - align-items: center; - gap: 7px; - font-size: 12px; + display: grid; + grid-template-columns: auto auto minmax(0, 1fr); + align-items: center; + gap: 8px; + min-width: 0; + padding: 6px 4px; + border-radius: 6px; + font-size: 11px; + cursor: pointer; +} + +.mwm-legend-item:hover { + background: var(--background-modifier-hover); +} + +.mwm-legend-item.is-muted { + opacity: 0.52; +} + +.mwm-legend-item input { + margin: 0; } .mwm-legend-mark { - display: inline-block; - position: relative; - box-sizing: border-box; - width: 22px; - height: 10px; - border-radius: 4px; + position: relative; + display: inline-flex; + align-items: center; + justify-content: center; + width: 18px; + height: 18px; + flex: 0 0 18px; } -.mwm-legend-root, -.mwm-legend-folder, -.mwm-legend-note, -.mwm-legend-meta, -.mwm-legend-outside-file, -.mwm-legend-external, -.mwm-legend-unresolved { - border: 1px solid color-mix(in srgb, var(--mwm-border) 70%, transparent); - box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--mwm-bg) 24%, transparent); +.mwm-legend-mark::before { + content: ""; + display: block; + width: 10px; + height: 10px; + border-radius: 999px; + background: var(--mwm-legend-color, var(--text-muted)); + box-shadow: 0 0 0 1px var(--background-modifier-border); } .mwm-legend-root { - background: var(--mwm-node-focused); - border-color: var(--mwm-node-focused); + --mwm-legend-color: var(--mwm-legend-root-color); } .mwm-legend-folder { - background: var(--mwm-folder); - border-color: var(--mwm-folder-ring); -} - -.mwm-legend-note { - background: var(--mwm-note); - border-color: color-mix(in srgb, var(--mwm-note) 42%, var(--mwm-border)); + --mwm-legend-color: var(--mwm-legend-folder-color); } .mwm-legend-meta { - background: var(--mwm-folder-meta); - border-color: var(--mwm-folder-ring); + --mwm-legend-color: var(--mwm-legend-meta-color); } -.mwm-legend-external { - background: var(--mwm-external); - border-color: var(--mwm-node-focused); +.mwm-legend-note { + --mwm-legend-color: var(--mwm-legend-note-color); } +.mwm-legend-external, .mwm-legend-outside-file { - background: var(--mwm-note); - border-color: var(--mwm-node-focused); + --mwm-legend-color: var(--mwm-legend-external-color); } .mwm-legend-unresolved { - background: var(--mwm-unresolved); - border-color: color-mix(in srgb, var(--mwm-unresolved) 74%, var(--mwm-border)); + --mwm-legend-color: var(--mwm-legend-unresolved-color); } -.mwm-legend-tree, -.mwm-legend-link, -.mwm-legend-link-external, -.mwm-legend-link-unresolved { - height: 10px; - border: 0; - background: transparent; +.mwm-legend-outside-file::before { + background: transparent; + box-shadow: 0 0 0 2px var(--mwm-legend-color); } .mwm-legend-tree::before, .mwm-legend-link::before, .mwm-legend-link-external::before, .mwm-legend-link-unresolved::before { - content: ""; - position: absolute; - left: 0; - right: 0; - top: 50%; - border-top: 2px solid var(--mwm-legend-line-color, var(--mwm-graph-line)); - border-radius: 999px; - transform: translateY(-50%); + width: 18px; + height: 0; + border-radius: 0; + background: transparent; + box-shadow: none; + border-top: 2px solid var(--mwm-legend-color, var(--text-muted)); } .mwm-legend-tree { - --mwm-legend-line-color: var(--mwm-tree); + --mwm-legend-color: var(--mwm-legend-tree-color); } .mwm-legend-link { - --mwm-legend-line-color: var(--mwm-link); + --mwm-legend-color: var(--mwm-legend-link-color); } .mwm-legend-link-external { - --mwm-legend-line-color: var(--mwm-external-link); + --mwm-legend-color: var(--mwm-legend-external-link-color); } .mwm-legend-link-unresolved { - --mwm-legend-line-color: var(--mwm-unresolved); -} - -.mwm-legend-tree::before { - border-top-width: 3px; -} - -.mwm-legend-link-external::before { - border-top-style: dashed; + --mwm-legend-color: var(--mwm-legend-unresolved-color); } .mwm-legend-link-unresolved::before { - border-top-style: dashed; + border-top-style: dashed; +} + +.mwm-legend-copy { + display: grid; + min-width: 0; + gap: 1px; +} + +.mwm-legend-label, +.mwm-legend-desc { + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; } .mwm-legend-label { - color: var(--mwm-text); - font-weight: 500; + color: var(--text-normal); } -.mwm-legend-text { - min-width: 0; - color: var(--mwm-muted); - overflow-wrap: anywhere; +.mwm-legend-desc { + color: var(--text-muted); + font-size: 10px; +} + +.mwm-side-title { + font-size: 14px; + font-weight: 600; +} + +.mwm-side-path, +.mwm-side-muted, +.mwm-pin-meta { + color: currentColor; + font-size: 11px; + opacity: 0.68; + overflow-wrap: anywhere; +} + +.mwm-side-heading { + margin-top: 4px; + font-size: 11px; + font-weight: 600; + opacity: 0.72; +} + +.mwm-facts { + display: grid; + grid-template-columns: minmax(72px, max-content) minmax(0, 1fr); + gap: 4px 8px; + font-size: 11px; +} + +.mwm-facts span { + min-width: 0; + overflow-wrap: anywhere; +} + +.mwm-facts span:nth-child(odd) { + opacity: 0.58; +} + +.mwm-link-row, +.mwm-pin-main { + width: 100%; + min-height: 28px; + padding: 5px 6px; + border: 1px solid var(--background-modifier-border); + border-radius: 6px; + background: var(--background-modifier-hover); + color: inherit; + font-size: 11px; + line-height: 1.25; + text-align: left; + cursor: pointer; + box-shadow: none; + white-space: normal; + overflow-wrap: anywhere; +} + +.mwm-pin-row, +.mwm-pin-group-row { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 6px; +} + +.mwm-pin-row > button:not(.mwm-pin-main), +.mwm-pin-group-row > button { + flex: 0 0 auto; + font-size: 10px; + padding: 3px 5px; +} + +.mwm-pin-main { + flex: 1; + flex-basis: 150px; + white-space: normal; + overflow: hidden; +} + +.mwm-floating-controls { + position: absolute; + top: 10px; + right: 10px; + z-index: 9; + justify-content: flex-end; + max-width: min(220px, calc(100% - 20px)); +} + +.mwm-floating-button { + width: 30px; + height: 30px; + display: inline-flex; + align-items: center; + justify-content: center; + padding: 0; + border: 1px solid var(--mwm-floating-border, rgba(255, 255, 255, 0.12)); + border-radius: 6px; + background: var(--mwm-floating-bg, rgba(10, 14, 24, 0.64)); + backdrop-filter: blur(14px); + color: var(--mwm-floating-color, #e8ecf6); + cursor: pointer; + box-shadow: none; +} + +.mwm-floating-button svg { + width: 16px; + height: 16px; +} + +.mwm-radial-host.is-radial-revealing .mwm-radial-canvas, +.mwm-radial-host.is-radial-revealing .mwm-radial-labels { + clip-path: circle(var(--mwm-reveal-radius, 0px) at var(--mwm-reveal-x, 50%) var(--mwm-reveal-y, 50%)); +} + +.mwm-radial-loading { + position: absolute; + left: 50%; + top: 50%; + z-index: 11; + translate: -50% -50%; + color: var(--mwm-radial-label, #aeb8d0); + text-shadow: 0 1px 6px var(--mwm-radial-label-shadow, rgba(0, 0, 0, 0.92)); + transition: opacity 220ms ease; + pointer-events: none; +} + +.mwm-radial-loading.is-fading { + opacity: 0; } diff --git a/tests/buildGraph.test.ts b/tests/buildGraph.test.ts new file mode 100644 index 0000000..2b31bb9 --- /dev/null +++ b/tests/buildGraph.test.ts @@ -0,0 +1,96 @@ +import { describe, expect, it } from 'vitest'; +import { buildGraph } from '../src/data/buildGraph'; + +const files = [ + { path: '01学习/笔记A.md', basename: '笔记A' }, + { path: '01学习/子目录/笔记B.md', basename: '笔记B' }, + { path: '02工作/笔记C.md', basename: '笔记C' }, + { path: '根笔记.md', basename: '根笔记' }, +]; + +describe('buildGraph', () => { + it('节点:路径为 id,顶层文件夹归组,根目录为空串', () => { + const g = buildGraph(files, {}, {}, { includeUnresolved: false, includeOrphans: true }); + expect(g.nodes).toHaveLength(4); + expect(g.nodes[0]).toMatchObject({ id: '01学习/笔记A.md', folderTop: '01学习', unresolved: false }); + expect(g.nodes[1]?.folderTop).toBe('01学习'); + expect(g.nodes[3]?.folderTop).toBe(''); + }); + + it('边:按索引,degree=出+入;非 md 集合内目标(附件/不存在)被丢弃', () => { + const resolved = { + '01学习/笔记A.md': { '01学习/子目录/笔记B.md': 3, '附件/图.png': 1 }, + '02工作/笔记C.md': { '01学习/笔记A.md': 1 }, + '幽灵来源.md': { '01学习/笔记A.md': 1 }, + }; + const g = buildGraph(files, resolved, {}, { includeUnresolved: false, includeOrphans: true }); + expect(g.links).toEqual([ + { source: 0, target: 1 }, + { source: 2, target: 0 }, + ]); + expect(g.nodes[0]?.degree).toBe(2); + expect(g.nodes[1]?.degree).toBe(1); + expect(g.nodes[2]?.degree).toBe(1); + expect(g.nodes[3]?.degree).toBe(0); + }); + + it('resolvedLinks 同对多次出现(值=次数)只产生一条边', () => { + const g = buildGraph(files, { '01学习/笔记A.md': { '02工作/笔记C.md': 99 } }, {}, { includeUnresolved: false, includeOrphans: true }); + expect(g.links).toHaveLength(1); + }); + + it('未解析:开关开启时生成幽灵节点并跨来源去重', () => { + const unresolved = { + '01学习/笔记A.md': { 概念词典: 2 }, + '02工作/笔记C.md': { 概念词典: 1, 另一个幽灵: 1 }, + }; + const off = buildGraph(files, {}, unresolved, { includeUnresolved: false, includeOrphans: true }); + expect(off.nodes).toHaveLength(4); + expect(off.links).toHaveLength(0); + + const on = buildGraph(files, {}, unresolved, { includeUnresolved: true, includeOrphans: true }); + const ghosts = on.nodes.filter((n) => n.unresolved); + expect(ghosts).toHaveLength(2); + expect(ghosts[0]).toMatchObject({ id: 'unresolved:概念词典', folderTop: '__unresolved__' }); + expect(on.links).toHaveLength(3); + expect(on.nodes.find((n) => n.id === 'unresolved:概念词典')?.degree).toBe(2); + }); + + it('空 vault 不炸', () => { + const g = buildGraph([], {}, {}, { includeUnresolved: true, includeOrphans: true }); + expect(g.nodes).toHaveLength(0); + expect(g.links).toHaveLength(0); + }); +}); + +describe('孤儿过滤', () => { + it('includeOrphans=false 时去掉零度节点并重排边索引', () => { + const resolved = { '01学习/笔记A.md': { '02工作/笔记C.md': 1 } }; + const g = buildGraph(files, resolved, {}, { includeUnresolved: false, includeOrphans: false }); + expect(g.nodes.map((n) => n.name)).toEqual(['笔记A', '笔记C']); + expect(g.links).toEqual([{ source: 0, target: 1 }]); + }); + + it('fileSize 从 FileRecord.size 透传', () => { + const sized = [{ path: 'a.md', basename: 'a', size: 12345 }]; + const g = buildGraph(sized, {}, {}, { includeUnresolved: false, includeOrphans: true }); + expect(g.nodes[0]?.fileSize).toBe(12345); + }); +}); + +describe('质量档位帽(M4)', () => { + it('nodeCap 按度数取 top N 并重排索引;linkCap 按 min(端点度数) 截断', () => { + const resolved = { + '01学习/笔记A.md': { '01学习/子目录/笔记B.md': 1, '02工作/笔记C.md': 1, '根笔记.md': 1 }, + '01学习/子目录/笔记B.md': { '02工作/笔记C.md': 1 }, + }; + // 度数:A=3, B=2, C=2, 根=1 + const capped = buildGraph(files, resolved, {}, { includeUnresolved: false, includeOrphans: true, nodeCap: 3 }); + expect(capped.nodes.map((n) => n.name)).toEqual(['笔记A', '笔记B', '笔记C']); + expect(capped.links).toHaveLength(3); // A-根 被丢弃 + const linkCapped = buildGraph(files, resolved, {}, { includeUnresolved: false, includeOrphans: true, linkCap: 2 }); + expect(linkCapped.links).toHaveLength(2); + // 保留的是 min 度数最高的边:A-B(min2)、A-C(min2),丢 B-C? min(B,C)=2 同分按原序——丢的是 A-根(min1) + expect(linkCapped.links.every((l) => l.source !== 3 && l.target !== 3)).toBe(true); + }); +}); diff --git a/tests/settings.test.ts b/tests/settings.test.ts new file mode 100644 index 0000000..e865ef8 --- /dev/null +++ b/tests/settings.test.ts @@ -0,0 +1,62 @@ +import { describe, expect, it } from 'vitest'; +import { mergeSettings } from '../src/settings'; + +describe('Mini World Map settings migration', () => { + it('defaults to 2D radial mode and preserves legacy radial keys', () => { + const settings = mergeSettings({ + atlasDepth: 9, + linkLimit: 42, + renderNodeLimit: 900, + includeUnresolvedLinks: false, + showLinkOverlay: false, + hoverHighlightMode: 'note-links', + hoverTargetMode: 'both', + hiddenLegendItems: ['link', 'bogus'], + ignoreFolders: ['.trash'], + }); + expect(settings.language).toBe('en'); + expect(settings.viewMode).toBe('radial2d'); + expect(settings.radial.atlasDepth).toBe(9); + expect(settings.radial.linkLimit).toBe(42); + expect(settings.radial.renderNodeLimit).toBe(900); + expect(settings.radial.includeUnresolvedLinks).toBe(false); + expect(settings.radial.showLinkOverlay).toBe(true); + expect(settings.radial.hoverHighlightMode).toBe('note-links'); + expect(settings.radial.hoverTargetMode).toBe('both'); + expect(settings.radial.hiddenLegendItems).toEqual(['link']); + expect(settings.radial.ignoreFolders).toEqual(['.trash']); + }); + + it('defaults 2D hover targets to nodes only', () => { + expect(mergeSettings({}).radial.hoverTargetMode).toBe('nodes'); + expect(mergeSettings({ radial: { hoverTargetMode: 'links' } }).radial.hoverTargetMode).toBe('links'); + expect(mergeSettings({ radial: { hoverTargetMode: 'anything' } }).radial.hoverTargetMode).toBe('nodes'); + }); + + it('preserves intentional nested 2D note-link visibility', () => { + expect(mergeSettings({ radial: { showLinkOverlay: false } }).radial.showLinkOverlay).toBe(false); + }); + + it('normalizes the shared language option', () => { + expect(mergeSettings({ language: 'zh' }).language).toBe('zh'); + expect(mergeSettings({ language: 'fr' }).language).toBe('en'); + }); + + it('keeps Galaxy settings nested for 3D map mode', () => { + const settings = mergeSettings({ + viewMode: 'map3d', + galaxy3d: { + cruise: false, + bloom: { strength: 1.2 }, + physics: { repel: 123 }, + look: { sizeBy: 'fileSize' }, + }, + }); + expect(settings.viewMode).toBe('map3d'); + expect(settings.galaxy3d.cruise).toBe(false); + expect(settings.galaxy3d.bloom.strength).toBe(1.2); + expect(settings.galaxy3d.physics.repel).toBe(123); + expect(settings.galaxy3d.look.sizeBy).toBe('fileSize'); + expect(settings.galaxy3d.preset).toBe('adaptive'); + }); +}); diff --git a/tests/worldMap.test.ts b/tests/worldMap.test.ts new file mode 100644 index 0000000..96ccd78 --- /dev/null +++ b/tests/worldMap.test.ts @@ -0,0 +1,163 @@ +import { describe, expect, it } from 'vitest'; +import { DEFAULT_RADIAL_SETTINGS } from '../src/settings'; +import { layoutRadialGraph } from '../src/layout/radial/layoutRadial'; +import { buildWorldMap } from '../src/world/buildWorldMap'; +import { ROOT_ID } from '../src/world/types'; +import { buildVisibleWorldGraph, defaultVisibleGraphState } from '../src/world/visibleGraph'; + +const records = [ + { path: 'Atlas', basename: 'Atlas', kind: 'folder' as const }, + { path: 'Atlas/Atlas.md', basename: 'Atlas', kind: 'note' as const }, + { path: 'Atlas/Topic A.md', basename: 'Topic A', kind: 'note' as const }, + { path: 'Atlas/Sub', basename: 'Sub', kind: 'folder' as const }, + { path: 'Atlas/Sub/Topic B.md', basename: 'Topic B', kind: 'note' as const }, + { path: 'Outside/Other.md', basename: 'Other', kind: 'note' as const }, + { path: '.obsidian/Hidden.md', basename: 'Hidden', kind: 'note' as const }, +]; + +function model(overrides: Partial = {}) { + const settings = { ...DEFAULT_RADIAL_SETTINGS, ...overrides }; + return buildWorldMap( + records, + { + 'Atlas/Topic A.md': { 'Atlas/Sub/Topic B.md': 2, 'Outside/Other.md': 1 }, + 'Outside/Other.md': { 'Atlas/Topic A.md': 1 }, + }, + { + 'Atlas/Topic A.md': { Missing: 1 }, + }, + settings, + ); +} + +describe('world-map model', () => { + it('indexes folders, notes, representatives, unresolved links, and ignored folders', () => { + const m = model(); + expect(m.nodes.has(ROOT_ID)).toBe(true); + expect(m.nodes.has('Atlas')).toBe(true); + expect(m.nodes.has('Atlas/Topic A.md')).toBe(true); + expect(m.nodes.has('.obsidian/Hidden.md')).toBe(false); + expect(m.nodes.get('Atlas')?.representativeFile).toBe('Atlas/Atlas.md'); + expect(m.nodes.get('Atlas/Atlas.md')?.isRepresentativeFile).toBe(true); + expect([...m.nodes.values()].filter((node) => node.type === 'unresolved')).toHaveLength(1); + expect(m.stats.notes).toBe(4); + }); + + it('can omit unresolved nodes', () => { + const m = model({ includeUnresolvedLinks: false }); + expect([...m.nodes.values()].filter((node) => node.type === 'unresolved')).toHaveLength(0); + }); +}); + +describe('visible world graph', () => { + it('builds an atlas graph with hierarchy edges and representative folding', () => { + const m = model(); + const state = defaultVisibleGraphState(DEFAULT_RADIAL_SETTINGS); + state.rootPath = 'Atlas'; + state.showLinkOverlay = true; + const graph = buildVisibleWorldGraph(m, state, DEFAULT_RADIAL_SETTINGS); + expect(graph.rootId).toBe('Atlas'); + expect(graph.nodesById.has('Atlas/Atlas.md')).toBe(false); + expect(graph.hierarchyEdges.length).toBeGreaterThan(0); + expect(graph.linkEdges.length).toBeGreaterThan(0); + }); + + it('creates grouped and exact outside-root proxies', () => { + const m = model(); + const state = defaultVisibleGraphState(DEFAULT_RADIAL_SETTINGS); + state.rootPath = 'Atlas'; + state.showLinkOverlay = true; + state.externalDetailMode = 'grouped'; + const grouped = buildVisibleWorldGraph(m, state, DEFAULT_RADIAL_SETTINGS); + expect(grouped.nodes.some((node) => node.type === 'external' && !node.externalProxy)).toBe(true); + expect(grouped.nodes.some((node) => node.externalProxy)).toBe(false); + + state.externalDetailMode = 'exact'; + const exact = buildVisibleWorldGraph(m, state, DEFAULT_RADIAL_SETTINGS); + expect(exact.nodes.some((node) => node.externalProxy)).toBe(true); + }); + + it('keeps hover links available when the visible link overlay is off', () => { + const m = model(); + const state = defaultVisibleGraphState(DEFAULT_RADIAL_SETTINGS); + state.rootPath = 'Atlas'; + state.showLinkOverlay = false; + state.showExternalLinks = false; + state.hoverHighlightMode = 'note-links'; + const graph = buildVisibleWorldGraph(m, state, DEFAULT_RADIAL_SETTINGS); + expect(graph.linkEdges).toHaveLength(0); + expect(graph.hoverLinkEdges.length).toBeGreaterThan(0); + }); + + it('builds a focus graph around the active note', () => { + const m = model(); + const state = defaultVisibleGraphState(DEFAULT_RADIAL_SETTINGS); + state.mode = 'focus'; + state.focusPath = 'Atlas/Topic A.md'; + state.showLinkOverlay = true; + const graph = buildVisibleWorldGraph(m, state, DEFAULT_RADIAL_SETTINGS); + expect(graph.focusId).toBe('Atlas/Topic A.md'); + expect(graph.nodesById.has('Atlas/Sub/Topic B.md')).toBe(true); + }); +}); + +describe('radial layout', () => { + it('keeps the root centered and ring depths ordered', () => { + const m = model(); + const state = defaultVisibleGraphState(DEFAULT_RADIAL_SETTINGS); + state.rootPath = 'Atlas'; + state.showLinkOverlay = true; + const graph = buildVisibleWorldGraph(m, state, DEFAULT_RADIAL_SETTINGS); + const layout = layoutRadialGraph(graph, { ringSpacing: 960, nodeSpacing: 126, swirlStrength: 0 }); + const root = layout.positions.get(graph.rootId); + expect(root?.x).toBeCloseTo(0); + expect(root?.y).toBeCloseTo(0); + expect([...layout.positions.values()].every((point) => Number.isFinite(point.x) && Number.isFinite(point.y))).toBe(true); + const radii = layout.rings.map((ring) => ring.radius); + expect(radii).toEqual([...radii].sort((a, b) => a - b)); + }); + + it('keeps folder descendants outside their parent folder rings', () => { + const m = model(); + const state = defaultVisibleGraphState(DEFAULT_RADIAL_SETTINGS); + state.rootPath = ROOT_ID; + state.showLinkOverlay = false; + const graph = buildVisibleWorldGraph(m, state, DEFAULT_RADIAL_SETTINGS); + const layout = layoutRadialGraph(graph, { ringSpacing: 960, nodeSpacing: 126, swirlStrength: 0 }); + const root = layout.positions.get(ROOT_ID); + const atlas = layout.positions.get('Atlas'); + const sub = layout.positions.get('Atlas/Sub'); + const topic = layout.positions.get('Atlas/Sub/Topic B.md'); + + expect(root?.radius).toBeCloseTo(0); + expect(atlas?.radius).toBeGreaterThan(root?.radius ?? -1); + expect(sub?.radius).toBeGreaterThan(atlas?.radius ?? -1); + expect(topic?.radius).toBeGreaterThan(sub?.radius ?? -1); + expect(atlas?.depth).toBe(1); + expect(sub?.depth).toBe(2); + expect(topic?.depth).toBe(3); + }); + + it('does not invent hierarchy rings for flat same-parent root notes', () => { + const crowdedRecords = Array.from({ length: 900 }, (_, index) => ({ + path: `Note ${index}.md`, + basename: `Note ${index}`, + kind: 'note' as const, + })); + const m = buildWorldMap(crowdedRecords, {}, {}, DEFAULT_RADIAL_SETTINGS); + const state = defaultVisibleGraphState(DEFAULT_RADIAL_SETTINGS); + state.showLinkOverlay = false; + state.nodeLimit = 1200; + const graph = buildVisibleWorldGraph(m, state, DEFAULT_RADIAL_SETTINGS); + const layout = layoutRadialGraph(graph, { ringSpacing: 960, nodeSpacing: 126, swirlStrength: 0 }); + const nodeRadii = [...layout.positions.entries()] + .filter(([id]) => id !== graph.rootId) + .map(([, point]) => Math.hypot(point.x, point.y)); + const depthOneRings = layout.rings.filter((ring) => ring.depth === 1); + const minRadius = Math.min(...nodeRadii); + const maxRadius = Math.max(...nodeRadii); + + expect(depthOneRings).toHaveLength(1); + expect(maxRadius - minRadius).toBeLessThan((depthOneRings[0]?.radius ?? 1) * 0.5); + }); +}); diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..c239825 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "inlineSourceMap": true, + "inlineSources": true, + "module": "ESNext", + "target": "ES2021", + "strict": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedIndexedAccess": true, + "moduleResolution": "node", + "isolatedModules": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "allowSyntheticDefaultImports": true, + "lib": ["ES2021", "DOM"] + }, + "include": ["src/**/*.ts", "tests/**/*.ts"] +} diff --git a/version-bump.mjs b/version-bump.mjs new file mode 100644 index 0000000..3fc1150 --- /dev/null +++ b/version-bump.mjs @@ -0,0 +1,17 @@ +import { readFileSync, writeFileSync } from 'fs'; + +const targetVersion = process.env.npm_package_version; + +// read minAppVersion from manifest.json and bump version to target version +const manifest = JSON.parse(readFileSync('manifest.json', 'utf8')); +const { minAppVersion } = manifest; +manifest.version = targetVersion; +writeFileSync('manifest.json', JSON.stringify(manifest, null, '\t')); + +// update versions.json with target version and minAppVersion from manifest.json +// but only if the target version is not already in versions.json +const versions = JSON.parse(readFileSync('versions.json', 'utf8')); +if (!(targetVersion in versions)) { + versions[targetVersion] = minAppVersion; + writeFileSync('versions.json', JSON.stringify(versions, null, '\t')); +} diff --git a/versions.json b/versions.json new file mode 100644 index 0000000..c65611d --- /dev/null +++ b/versions.json @@ -0,0 +1,4 @@ +{ + "0.1.0": "1.7.2", + "0.1.1": "1.7.2" +} \ No newline at end of file