Files
Beyond-KLE/mock/src/domain/deleteCascade.ts
T
ewdernaandClaude Opus 5.5 fccbc28bee mock: 仮決定に沿ってモックを実装する(PHY Editor・IDX・ELC・MAP・Generator)
provisional/decisions.md の仮決定(T-1〜T-28)に従い、mock/ に Vue 3 + Vite + TypeScript のモックを置いた。
T-3 の1〜3(回転したクラスター内のドラッグと保存の往復、IDX・MAPとの連携、ELCと出力の対応表)まで。疑似デバイスとの往復(T-24)は未着手。

- KLE JSONの入出力は、KLE本家の serial.js の deserialize の結果を期待値とする fixture(tests/fixtures/kle-oracle.json)で照合した。本家のコードは取り込んでいない(T-7)
- 基本キーコードの表は QMK の data/constants/keycodes/keycodes_0.0.1_basic.hjson(1e340cd)から抽出した
- 実装は Codex(Luna)、レビューは Codex(Sol)。検証: Vitest 53件、Playwright 20件、vue-tsc、vite build
- TypeScript は vue-tsc が 7 系に未対応のため 6 系に固定した
- 操作性の改善(ELCの提供者の表示、一括配線とGeneratorのピン設定の引き継ぎ、キー内の文字のはみ出し)は別途行う

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AATqjRyp6rrnwTNdYw8XBT
2026-09-25 22:42:30 +09:00

130 lines
5.2 KiB
TypeScript

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<string, { ids: Set<string>; 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;
}
}