import type { ProjectFile } from "./project"; import type { Node, Variant, VariantGroupNode } from "./phy"; export type CascadeReference = { layer: "idx" | "elc" | "map" | "ledChains"; location: string; id: string }; type DeleteTarget = { kind: "node"; node: Node } | { kind: "variant"; variant: Variant; group: VariantGroupNode }; function descendants(node: Node | Variant): string[] { const ids = [node.id]; if (node.type === "cluster") node.children.forEach((child) => ids.push(...descendants(child))); if (node.type === "variantGroup") for (const variant of node.children) ids.push(...descendants(variant)); if (node.type === "variant") node.children.forEach((child) => ids.push(...descendants(child))); return ids; } /** Lists all saved references to a PHY node, Variant, and its descendants. T-15. */ export function listDeleteReferences(p: ProjectFile, nodeId: string): CascadeReference[] { const target = findTarget(p.phy.root, nodeId); if (!target) throw new Error(`Unknown node: ${nodeId}`); const ids = new Set(descendants(target.kind === "node" ? target.node : target.variant)); const out: CascadeReference[] = []; p.idx.order.forEach((id) => { if (ids.has(id)) out.push({ layer: "idx", location: "/idx/order", id }); }); p.elc.keys.forEach((w, i) => { if (ids.has(w.key)) out.push({ layer: "elc", location: `/elc/keys/${i}`, id: w.key }); }); p.elc.ledChains?.forEach((chain) => chain.order.forEach((id) => { if (ids.has(id)) out.push({ layer: "ledChains", location: `/elc/ledChains/${chain.id}`, id }); }), ); p.map.layers.forEach((layer) => layer.keymap.forEach((assignment) => { if (ids.has(assignment.keyId)) out.push({ layer: "map", location: `/map/layers/${layer.name}`, id: assignment.keyId }); }), ); return out; } /** Removes a PHY node or Variant and its references in one immutable project result. T-15. */ export function deleteNodeCascade(p: ProjectFile, nodeId: string): ProjectFile { const o = structuredClone(p); const target = findTarget(o.phy.root, nodeId); if (!target) throw new Error(`Unknown node: ${nodeId}`); const deletedIds = new Set(descendants(target.kind === "node" ? target.node : target.variant)); removeTarget(o.phy.root, target); o.idx.order = o.idx.order.filter((id) => !deletedIds.has(id)); o.elc.keys = o.elc.keys.filter((w) => !deletedIds.has(w.key)); if (o.elc.ledChains) o.elc.ledChains = o.elc.ledChains.map((chain) => ({ ...chain, order: chain.order.filter((id) => !deletedIds.has(id)), })); o.map.layers = o.map.layers.map((layer) => ({ ...layer, keymap: layer.keymap.filter((assignment) => !deletedIds.has(assignment.keyId)), })); if (target.kind === "variant") { if (target.group.default === nodeId) target.group.default = target.group.children[0]!.id; if (o.output.variantSelection[target.group.id] === nodeId) o.output.variantSelection[target.group.id] = target.group.default; } pruneVariantSelection(o); return o; } function findTarget(nodes: Node[], id: string): DeleteTarget | undefined { for (const node of nodes) { if (node.id === id) return { kind: "node", node }; if (node.type === "cluster") { const found = findTarget(node.children, id); if (found) return found; } if (node.type === "variantGroup") for (const variant of node.children) { if (variant.id === id) return { kind: "variant", variant, group: node }; const found = findTarget(variant.children, id); if (found) return found; } } return undefined; } function removeTarget(nodes: Node[], target: DeleteTarget): void { const id = target.kind === "node" ? target.node.id : target.variant.id; if (target.kind === "variant") { const { group, variant } = target; if (group.children.length <= 2) throw new Error("Cannot delete a variant leaving fewer than two variants"); group.children.splice(group.children.indexOf(variant), 1); if (group.default === id) group.default = group.children[0]!.id; return; } const index = nodes.findIndex((node) => node.id === id); if (index >= 0) { nodes.splice(index, 1); return; } for (const node of nodes) { if (node.type === "cluster") removeTarget(node.children, target); if (node.type === "variantGroup") for (const variant of node.children) removeTarget(variant.children, target); } } function pruneVariantSelection(project: ProjectFile): void { const valid = new Map; defaultId: string }>(); const walk = (nodes: Node[]) => nodes.forEach((node) => { if (node.type === "cluster") walk(node.children); if (node.type === "variantGroup") { valid.set(node.id, { ids: new Set(node.children.map((variant) => variant.id)), defaultId: node.default, }); node.children.forEach((variant) => walk(variant.children)); } }); walk(project.phy.root); for (const [groupId, variantId] of Object.entries(project.output.variantSelection)) { const group = valid.get(groupId); if (!group) delete project.output.variantSelection[groupId]; else if (!group.ids.has(variantId)) project.output.variantSelection[groupId] = group.defaultId; } }