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 = {}): 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; }