import { describe, expect, it } from "vitest"; import { stabilizerCandidates, stabilizerPoints, switchPoint } from "../src/domain/stabilizers"; import { mountMarks, selectedStabilizers, stabilizerSelections, withStabilizers } from "../src/ui/phy/logic"; import type { KeyNode } from "../src/domain/phy"; import type { ProjectFile } from "../src/domain/project"; const key = (id: string, w: number, h = 1, extra: Partial = {}): KeyNode => ({ type: "key", id, profile: "p", transform: { x: 0, y: 0, rotation: 0 }, size: { w, h }, ...extra, }); const ids = (w: number, h = 1) => stabilizerCandidates({ w, h }).map((c) => c.id); describe("stabilizer candidates", () => { it("looks up candidates by the long side", () => { expect(ids(1)).toEqual([]); expect(ids(1.75)).toEqual([]); expect(ids(2.25)).toEqual(["11.938mm"]); expect(ids(1, 2)).toEqual(["11.938mm"]); expect(ids(6)).toEqual(["2.5u", "2u"]); expect(ids(6.25)).toEqual(["50mm", "40mm", "42.5mm"]); expect(ids(10)).toEqual(["3.5u"]); expect(ids(6.75)).toEqual([]); }); it("gives no candidates to non-rectangular keys", () => { expect(stabilizerCandidates({ w: 1.25, h: 2, w2: 1.5, h2: 1, x2: -0.25 })).toEqual([]); expect(stabilizerCandidates({ w: 2, h: 1, w2: 2.25 })).toEqual([]); }); it("places mounts around the rectangle center along the long axis", () => { expect(stabilizerPoints({ w: 7, h: 1 }, ["3u"])).toEqual([ { x: 0.5, y: 0.5 }, { x: 6.5, y: 0.5 }, ]); const [top, bottom] = stabilizerPoints({ w: 1, h: 2 }, ["11.938mm"]); expect(top!.x).toBe(0.5); expect(bottom!.y - top!.y).toBeCloseTo(23.876 / 19.05, 12); expect(stabilizerPoints({ w: 6, h: 1 }, ["2.5u", "2u"])).toHaveLength(4); }); it("keeps stabilizers on the key center when the switch is offset", () => { const k = key("k", 6, 1, { switchOffset: { x: 0.5, y: 0 } }); expect(switchPoint(k)).toEqual({ x: 3.5, y: 0.5 }); expect(stabilizerPoints(k.size, ["2.5u"]).map((p) => p.x)).toEqual([0.5, 5.5]); }); }); describe("stabilizer selections in ProjectFile.ui", () => { const project = { ui: { other: 1 } } as unknown as ProjectFile; it("selects nothing by default and drops ids the size no longer offers", () => { expect(selectedStabilizers({}, key("a", 2))).toEqual([]); expect(selectedStabilizers({ a: ["11.938mm"] }, key("a", 2))).toEqual(["11.938mm"]); expect(selectedStabilizers({ a: ["2.5u", "3u"] }, key("a", 6))).toEqual(["2.5u"]); }); it("writes per key without touching other ui state", () => { const next = withStabilizers(withStabilizers(project, "a", ["50mm"]), "b", []); expect(stabilizerSelections(next)).toEqual({ a: ["50mm"], b: [] }); expect((next.ui as { other: number }).other).toBe(1); expect(stabilizerSelections(project)).toEqual({}); }); it("lists marks for visible keys only, skipping decals", () => { const marks = mountMarks( [ key("a", 2), key("d", 2, 1, { decal: true }), { type: "variantGroup", id: "g", name: "g", default: "v1", transform: { x: 0, y: 1, rotation: 0 }, children: [ { type: "variant", id: "v1", name: "1", children: [key("b", 1)] }, { type: "variant", id: "v2", name: "2", children: [key("c", 1)] }, ], }, ], { g: "v2" }, { a: ["11.938mm"] }, ); expect(marks.map((m) => m.id)).toEqual(["a", "c"]); expect(marks[1]!.transform.f).toBe(1); expect(marks[0]!.stabs).toHaveLength(2); }); });