miro0o_miniWorldMap/src/data/GraphStore.ts

109 lines
3.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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<void> {
if (Object.keys(this.app.metadataCache.resolvedLinks).length > 0) return;
await new Promise<void>((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<string, number>();
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?.();
}
}