import type { KleCompatibility, KeyNode, PhyFile } from "../domain/phy"; import { createNodeId } from "../domain/ids"; type Obj = Record; type Atom = string | Obj; // Output slot -> newline-field index. These fixed tables reproduce KLE's a=0..7 layout. const labelMap: readonly (readonly (number | null)[])[] = [ [0, 8, 2, 6, 9, 7, 1, 10, 3, 4, 11, 5], [null, 0, null, null, 6, null, null, 1, null, 4, 11, 5], [null, null, null, 0, 8, 2, null, null, null, 4, 11, 5], [null, null, null, null, 0, null, null, null, null, 4, 11, 5], [0, 8, 2, 6, 9, 7, 1, 10, 3, null, 4, null], [null, 0, null, null, 6, null, null, 1, null, null, 4, null], [null, null, null, 0, 8, 2, null, null, null, null, 4, null], [null, null, null, null, 0, null, null, null, null, null, 4, null], ]; const fileDefaults: Obj = { backcolor: "#eeeeee", name: "", author: "", notes: "", background: null, radii: "", switchMount: "", switchBrand: "", switchType: "", }; const keyDefaults: Obj = { c: "#cccccc", t: "#000000", p: "", a: 4, f: 3, sm: "", sb: "", st: "" }; function isObj(value: unknown): value is Obj { return !!value && typeof value === "object" && !Array.isArray(value); } function num(value: unknown, fallback: number): number { return typeof value === "number" && Number.isFinite(value) ? value : fallback; } function labelsFromKle(serialized: string, alignment: number): string[] { const fields = serialized.split("\n"); if (fields.length > 12) throw new Error("A KLE label cannot exceed 12 slots"); const map = labelMap[alignment] ?? labelMap[4]!; const labels = Array(12).fill(""); map.forEach((field, slot) => { if (field !== null && field < fields.length) labels[slot] = fields[field]!; }); while (labels.at(-1) === "") labels.pop(); return labels; } function oneShotArray(state: Obj, key: string): void { delete state[key]; } /** KLEの厳密JSON(またはVIA layouts.keymap)を新規PHYへ取り込む。T-7/T-18/T-20。 */ export function importKle( input: unknown, options: { idGenerator?: () => string; name?: string } = {}, ): { phy: PhyFile; keyOrder: string[] } { let data: unknown = input; if (isObj(data)) { const layouts = data.layouts; if (isObj(layouts) && Array.isArray(layouts.keymap)) data = layouts.keymap; else throw new Error("Expected KLE array or VIA layouts.keymap"); } if (!Array.isArray(data)) throw new Error("KLE input must be a JSON array"); const rows = [...data] as unknown[]; let rawMeta: Obj = {}; if (rows.length && isObj(rows[0])) rawMeta = rows.shift() as Obj; const gen = options.idGenerator ?? (() => globalThis.crypto.randomUUID()); const state: Obj = { ...keyDefaults }; let cursorX = 0; let cursorY = 0; let rotation = 0; let rotationX = 0; let rotationY = 0; const root: KeyNode[] = []; const keyOrder: string[] = []; for (const row of rows) { if (!Array.isArray(row)) throw new Error("KLE rows must be arrays"); cursorX = rotationX; for (const atom of row as Atom[]) { if (typeof atom === "string") { const w = num(state.w, 1); const h = num(state.h, 1); cursorX += num(state.x, 0); cursorY += num(state.y, 0); const radians = (rotation * Math.PI) / 180; const dx = cursorX - rotationX; const dy = cursorY - rotationY; const x = rotationX + Math.cos(radians) * dx - Math.sin(radians) * dy; const y = rotationY + Math.sin(radians) * dx + Math.cos(radians) * dy; const a = num(state.a, 4); const id = createNodeId("key", gen); const labels = labelsFromKle(atom, a); const compatibility: KleCompatibility = { c: structuredClone(state.c), t: structuredClone(state.t), p: structuredClone(state.p), a, f: num(state.f, 3), ...(state.f2 === undefined ? {} : { f2: structuredClone(state.f2) }), ...(state.fa === undefined ? {} : { fa: structuredClone(state.fa) }), l: state.l === true, n: state.n === true, sm: structuredClone(state.sm), sb: structuredClone(state.sb), st: structuredClone(state.st), }; const w2 = num(state.w2, 0) || w; const h2 = num(state.h2, 0) || h; const key: KeyNode = { type: "key", id, profile: "default", transform: { x, y, rotation }, size: { w, h, ...(num(state.x2, 0) === 0 ? {} : { x2: num(state.x2, 0) }), ...(num(state.y2, 0) === 0 ? {} : { y2: num(state.y2, 0) }), ...(w2 === w ? {} : { w2 }), ...(h2 === h ? {} : { h2 }), }, labels, kleCompatibility: compatibility, }; if (state.d === true) key.decal = true; if (state.g === true) key.ghost = true; root.push(key); if (!key.decal) keyOrder.push(id); cursorX += w; for (const field of ["w", "h", "x", "y", "x2", "y2", "w2", "h2", "d", "l", "n"]) oneShotArray(state, field); } else if (isObj(atom)) { for (const [key, value] of Object.entries(atom)) { if (key === "r") rotation = num(value, rotation); else if (key === "rx") { rotationX = num(value, rotationX); cursorX = rotationX; cursorY = rotationY; } else if (key === "ry") { rotationY = num(value, rotationY); cursorX = rotationX; cursorY = rotationY; } else if (key === "f") { state.f = value; delete state.f2; delete state.fa; } else if (key === "f2") { state.f2 = value; delete state.fa; } else if (key === "fa") { state.fa = structuredClone(value); delete state.f2; } else state[key] = structuredClone(value); } } else throw new Error("KLE row entries must be strings or objects"); } cursorY += 1; } const metadata = { ...fileDefaults, ...structuredClone(rawMeta) }; const phy: PhyFile = { version: "0.1.0", metadata: { ...(options.name ? { name: options.name } : {}) }, profiles: [{ id: "default", name: "Default", unitSize: { x: 19.05, y: 19.05 } }], root, kleCompatibility: metadata, }; return { phy, keyOrder }; } /** JSON文字列を厳密にパースして KLE を取り込む。 */ export function parseKleJson( json: string, options: { idGenerator?: () => string; name?: string } = {}, ): ReturnType { return importKle(JSON.parse(json) as unknown, options); } /** 正準12欄のラベルをKLEのa別固定欄順に戻す。 */ export function labelsToKle(labels: string[] | undefined, alignment: number): string { const map = labelMap[alignment] ?? labelMap[4]!; const fields = Array(12).fill(""); map.forEach((sourceSlot, slot) => { if (sourceSlot !== null) fields[sourceSlot] = labels?.[slot] ?? ""; }); while (fields.at(-1) === "") fields.pop(); return fields.join("\n"); }