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
This commit is contained in:
2026-09-25 22:42:30 +09:00
co-authored by Claude Opus 5.5
parent 937fc27dbd
commit fccbc28bee
59 changed files with 15509 additions and 0 deletions
+71
View File
@@ -0,0 +1,71 @@
import type { FlatKey, Node, PhyFile, Transform } from "./phy";
/** Translate·Rotate のアフィン行列を返す。 */
export function matrix(t: Transform): Affine {
const a = (t.rotation * Math.PI) / 180,
c = Math.cos(a),
s = Math.sin(a);
return { a: c, b: s, c: -s, d: c, e: t.x, f: t.y };
}
export type Affine = { a: number; b: number; c: number; d: number; e: number; f: number };
/** 左から右の順でなく、通常の行列積 left·right を返す。 */
export function compose(left: Affine, right: Affine): Affine {
return {
a: left.a * right.a + left.c * right.b,
b: left.b * right.a + left.d * right.b,
c: left.a * right.c + left.c * right.d,
d: left.b * right.c + left.d * right.d,
e: left.a * right.e + left.c * right.f + left.e,
f: left.b * right.e + left.d * right.f + left.f,
};
}
/** 剛体変換の逆行列を返す。 */
export function inverse(m: Affine): Affine {
return { a: m.a, b: m.c, c: m.b, d: m.d, e: -(m.a * m.e + m.b * m.f), f: -(m.c * m.e + m.d * m.f) };
}
/** 点に行列を適用する。 */
export function apply(m: Affine, p: { x: number; y: number }): { x: number; y: number } {
return { x: m.a * p.x + m.c * p.y + m.e, y: m.b * p.x + m.d * p.y + m.f };
}
/** PHY ノードのワールド行列を計算する。 */
export function worldTransform(file: PhyFile, id: string): Affine {
const walk = (nodes: Node[], parent: Affine): Affine | undefined => {
for (const n of nodes) {
const own = compose(parent, matrix(n.transform));
if (n.id === id) return own;
if (n.type === "cluster") {
const r = walk(n.children, own);
if (r) return r;
}
if (n.type === "variantGroup")
for (const v of n.children) {
const r = walk(v.children, own);
if (r) return r;
}
}
return undefined;
};
const found = walk(file.root, { a: 1, b: 0, c: 0, d: 1, e: 0, f: 0 });
if (!found) throw new Error(`Unknown node: ${id}`);
return found;
}
/** 選択したVariantに従いキーを平坦化する。省略時は各Groupのdefaultを使う。 */
export function flattenKeys(file: PhyFile, selection: Record<string, string> = {}): FlatKey[] {
const out: FlatKey[] = [];
const visit = (nodes: Node[], parent: Affine, parentRotation: number, path: string[]) => {
for (const n of nodes) {
const wm = compose(parent, matrix(n.transform)),
p = [...path, n.id],
absoluteRotation = parentRotation + n.transform.rotation;
if (n.type === "key") out.push({ key: n, x: wm.e, y: wm.f, rotation: absoluteRotation, path: p });
else if (n.type === "cluster") visit(n.children, wm, absoluteRotation, p);
else if (n.type === "variantGroup") {
const v = n.children.find((x) => x.id === (selection[n.id] ?? n.default));
if (!v) throw new Error(`Unknown variant for ${n.id}`);
visit(v.children, wm, absoluteRotation, [...p, v.id]);
}
}
};
visit(file.root, { a: 1, b: 0, c: 0, d: 1, e: 0, f: 0 }, 0, []);
return out;
}