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
130 lines
5.2 KiB
TypeScript
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;
|
|
}
|
|
}
|