mock: スイッチ・スタビの十字をトグルで出し、スタビ候補を選べるようにする(T-30 単位8)

スタビ候補はキーの長辺の長さで引き、キーごとに複数選べる。選択は ProjectFile.ui.phy.stabilizers に持つ。
プロパティ欄に switchOffset の入力欄を置く。異形キーは候補なし。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017yoUu9Xd8dhwyQdCfKfaXG
This commit is contained in:
2026-10-23 22:13:07 +09:00
co-authored by Claude Opus 5.5
parent 2c388e3bdb
commit eacf8a1581
8 changed files with 416 additions and 3 deletions
+73
View File
@@ -0,0 +1,73 @@
import type { Point, Size } from "./phy";
/** One candidate stabilizer layout: two mounts at ±offset along the key's long axis, in U. */
export type StabilizerCandidate = { id: string; label: string; offset: number };
const MM = 1 / 19.05;
/** Cherry MX spacings by key length. provisional/decisions.md「単位8のスタビ候補」. */
const TABLE: { lengths: number[]; candidates: StabilizerCandidate[] }[] = [
{
lengths: [2, 2.25, 2.5, 2.75],
candidates: [{ id: "11.938mm", label: "±11.938mm", offset: 11.938 * MM }],
},
{ lengths: [3], candidates: [{ id: "1u", label: "±1U", offset: 1 }] },
{ lengths: [4], candidates: [{ id: "1.5u", label: "±1.5U", offset: 1.5 }] },
{ lengths: [4.5], candidates: [{ id: "34.671mm", label: "±34.671mm", offset: 34.671 * MM }] },
{ lengths: [5.5], candidates: [{ id: "2.25u", label: "±2.25U", offset: 2.25 }] },
{
lengths: [6],
candidates: [
{ id: "2.5u", label: "±2.5U", offset: 2.5 },
{ id: "2u", label: "±2U", offset: 2 },
],
},
{
lengths: [6.25],
candidates: [
{ id: "50mm", label: "±50mm", offset: 50 * MM },
{ id: "40mm", label: "±40mm", offset: 40 * MM },
{ id: "42.5mm", label: "±42.5mm", offset: 42.5 * MM },
],
},
{ lengths: [6.5], candidates: [{ id: "2.75u", label: "±2.75U", offset: 2.75 }] },
{ lengths: [7], candidates: [{ id: "3u", label: "±3U", offset: 3 }] },
{ lengths: [8, 9, 9.75, 10], candidates: [{ id: "3.5u", label: "±3.5U", offset: 3.5 }] },
];
const EPS = 1e-9;
/** Non-rectangular keys (ISO Enter, BAE, stepped) have no candidates yet. */
function irregular(size: Size): boolean {
return [size.x2, size.y2, size.w2, size.h2].some((v) => v !== undefined);
}
export function stabilizerCandidates(size: Size): StabilizerCandidate[] {
if (irregular(size)) return [];
const length = Math.max(size.w, size.h);
return TABLE.find((row) => row.lengths.some((l) => Math.abs(l - length) < EPS))?.candidates ?? [];
}
/** A size with a single candidate shows it until chosen otherwise; several start unselected. */
export function defaultStabilizerSelection(size: Size): string[] {
const candidates = stabilizerCandidates(size);
return candidates.length === 1 ? [candidates[0]!.id] : [];
}
/** Mount points in key-local coordinates, around the (w, h) rectangle's center, not the switch. */
export function stabilizerPoints(size: Size, ids: string[]): Point[] {
const cx = size.w / 2,
cy = size.h / 2,
vertical = size.h > size.w;
return stabilizerCandidates(size)
.filter((c) => ids.includes(c.id))
.flatMap((c) =>
vertical
? [
{ x: cx, y: cy - c.offset },
{ x: cx, y: cy + c.offset },
]
: [
{ x: cx - c.offset, y: cy },
{ x: cx + c.offset, y: cy },
],
);
}
export function switchPoint(key: { size: Size; switchOffset?: Point }): Point {
return { x: key.size.w / 2 + (key.switchOffset?.x ?? 0), y: key.size.h / 2 + (key.switchOffset?.y ?? 0) };
}