79 lines
3.2 KiB
TypeScript
79 lines
3.2 KiB
TypeScript
import type { MeshSummaryIR, ModifierIR, SceneSnapshotIR } from "./scene-ir";
|
|
|
|
export interface ModifierGraphNode {
|
|
uuid: string;
|
|
meshId: string;
|
|
modifier: ModifierIR;
|
|
dependencies: string[];
|
|
}
|
|
|
|
export interface ModifierDependencyGraph {
|
|
nodes: ModifierGraphNode[];
|
|
order: string[];
|
|
cycles: string[][];
|
|
missingDependencies: string[];
|
|
}
|
|
|
|
export interface ModifierEvaluationReport {
|
|
status: "EVALUATED" | "METADATA_ONLY" | "BLOCKED";
|
|
order: string[];
|
|
cycles: string[][];
|
|
blockedBy: string[];
|
|
}
|
|
|
|
export function buildModifierDependencyGraph(snapshot: Pick<SceneSnapshotIR, "meshes">): ModifierDependencyGraph {
|
|
const nodes: ModifierGraphNode[] = [];
|
|
const known = new Set<string>();
|
|
for (const mesh of snapshot.meshes) {
|
|
let previous: string | undefined;
|
|
for (const modifier of mesh.modifierStack ?? []) {
|
|
const dependencies = Array.from(new Set([...(modifier.dependsOn ?? []), ...(previous ? [previous] : [])]));
|
|
nodes.push({ uuid: modifier.uuid, meshId: mesh.id, modifier, dependencies });
|
|
known.add(modifier.uuid);
|
|
previous = modifier.uuid;
|
|
}
|
|
}
|
|
const nodeById = new Map(nodes.map((node) => [node.uuid, node]));
|
|
const state = new Map<string, 0 | 1 | 2>();
|
|
const order: string[] = [];
|
|
const cycles: string[][] = [];
|
|
const missingDependencies: string[] = [];
|
|
const path: string[] = [];
|
|
const visit = (uuid: string): void => {
|
|
const current = state.get(uuid) ?? 0;
|
|
if (current === 2) return;
|
|
if (current === 1) {
|
|
const start = path.indexOf(uuid);
|
|
cycles.push(start >= 0 ? [...path.slice(start), uuid] : [uuid]);
|
|
return;
|
|
}
|
|
state.set(uuid, 1);
|
|
path.push(uuid);
|
|
for (const dependency of nodeById.get(uuid)?.dependencies ?? []) {
|
|
if (known.has(dependency)) visit(dependency);
|
|
else missingDependencies.push(`${uuid}:${dependency}`);
|
|
}
|
|
path.pop();
|
|
state.set(uuid, 2);
|
|
order.push(uuid);
|
|
};
|
|
for (const node of nodes) visit(node.uuid);
|
|
return { nodes, order, cycles, missingDependencies: Array.from(new Set(missingDependencies)).sort() };
|
|
}
|
|
|
|
export function evaluateModifierDependencyGraph(snapshot: Pick<SceneSnapshotIR, "meshes">): ModifierEvaluationReport {
|
|
const graph = buildModifierDependencyGraph(snapshot);
|
|
const blockedBy = graph.nodes.filter((node) => node.modifier.evaluationStatus === "BLOCKED").map((node) => node.uuid);
|
|
if (graph.cycles.length > 0) return { status: "BLOCKED", order: graph.order, cycles: graph.cycles, blockedBy: [...blockedBy, "MODIFIER_DEPENDENCY_CYCLE"] };
|
|
if (graph.missingDependencies.length > 0) return { status: "BLOCKED", order: graph.order, cycles: [], blockedBy: [...blockedBy, ...graph.missingDependencies] };
|
|
if (blockedBy.length > 0) return { status: "BLOCKED", order: graph.order, cycles: [], blockedBy };
|
|
if (graph.nodes.some((node) => node.modifier.evaluationStatus === "METADATA_ONLY")) {
|
|
return { status: "METADATA_ONLY", order: graph.order, cycles: [], blockedBy: [] };
|
|
}
|
|
return { status: "EVALUATED", order: graph.order, cycles: [], blockedBy: [] };
|
|
}
|
|
|
|
export function evaluateMeshModifierStack(mesh: MeshSummaryIR): ModifierEvaluationReport {
|
|
return evaluateModifierDependencyGraph({ meshes: [mesh] });
|
|
}
|