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): ModifierDependencyGraph { const nodes: ModifierGraphNode[] = []; const known = new Set(); 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(); 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): 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] }); }