From eacf8a1581bbc68e719540abfe39b8460ef6c9b9 Mon Sep 17 00:00:00 2001 From: EwdErna Date: Fri, 23 Oct 2026 22:13:07 +0900 Subject: [PATCH] =?UTF-8?q?mock:=20=E3=82=B9=E3=82=A4=E3=83=83=E3=83=81?= =?UTF-8?q?=E3=83=BB=E3=82=B9=E3=82=BF=E3=83=93=E3=81=AE=E5=8D=81=E5=AD=97?= =?UTF-8?q?=E3=82=92=E3=83=88=E3=82=B0=E3=83=AB=E3=81=A7=E5=87=BA=E3=81=97?= =?UTF-8?q?=E3=80=81=E3=82=B9=E3=82=BF=E3=83=93=E5=80=99=E8=A3=9C=E3=82=92?= =?UTF-8?q?=E9=81=B8=E3=81=B9=E3=82=8B=E3=82=88=E3=81=86=E3=81=AB=E3=81=99?= =?UTF-8?q?=E3=82=8B(T-30=20=E5=8D=98=E4=BD=8D8)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit スタビ候補はキーの長辺の長さで引き、キーごとに複数選べる。選択は ProjectFile.ui.phy.stabilizers に持つ。 プロパティ欄に switchOffset の入力欄を置く。異形キーは候補なし。 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_017yoUu9Xd8dhwyQdCfKfaXG --- mock/e2e/phy.spec.ts | 34 +++++++++++ mock/src/domain/stabilizers.ts | 73 ++++++++++++++++++++++++ mock/src/ui/phy/PhyEditor.vue | 36 +++++++++++- mock/src/ui/phy/PhyMountLayer.vue | 44 ++++++++++++++ mock/src/ui/phy/PhyProperties.vue | 57 +++++++++++++++++++ mock/src/ui/phy/PhyToolbar.vue | 15 +++++ mock/src/ui/phy/logic.ts | 65 ++++++++++++++++++++- mock/tests/stabilizers.test.ts | 95 +++++++++++++++++++++++++++++++ 8 files changed, 416 insertions(+), 3 deletions(-) create mode 100644 mock/src/domain/stabilizers.ts create mode 100644 mock/src/ui/phy/PhyMountLayer.vue create mode 100644 mock/tests/stabilizers.test.ts diff --git a/mock/e2e/phy.spec.ts b/mock/e2e/phy.spec.ts index 51803a8..6a51259 100644 --- a/mock/e2e/phy.spec.ts +++ b/mock/e2e/phy.spec.ts @@ -706,3 +706,37 @@ test("キャンバスのviewBoxはツールバーを除いたSVGの大きさに expect(Math.abs(w! - box.width)).toBeLessThan(1); expect(Math.abs(h! - box.height)).toBeLessThan(1); }); + +test("スイッチ・スタビの十字はトグルで出し、スタビ候補を複数選べる", async ({ page }) => { + const key = page.locator('[data-node-type="key"]').first(); + const id = await key.getAttribute("data-node-id"); + await expect(page.locator(".mount-layer")).toHaveCount(0); + await page.getByRole("button", { name: "スイッチ・スタビライザーの位置" }).click(); + const mark = page.locator(`[data-mount-id="${id}"]`); + await expect(mark.locator(".switch")).toHaveCount(1); + await expect(mark.locator(".stab")).toHaveCount(0); + + await key.click(); + await expect(page.locator(".stabilizers")).toContainText("候補なし"); + await page.getByLabel("Width").fill("6.25"); + await page.getByLabel("Width").blur(); + for (const label of ["±50mm", "±40mm", "±42.5mm"]) await expect(page.getByLabel(label)).not.toBeChecked(); + await page.getByLabel("±50mm").check(); + await page.getByLabel("±40mm").check(); + await expect(mark.locator(".stab")).toHaveCount(4); + + const switchX = async () => (await mark.locator(".switch").boundingBox())!.x; + const before = await switchX(); + await page.getByLabel("switchOffset X").fill("0.625"); + await page.getByLabel("switchOffset X").blur(); + await expect.poll(switchX).toBeGreaterThan(before + 10); + await expect(mark.locator(".stab")).toHaveCount(4); + + await page.getByRole("button", { name: "Undo" }).click(); + await page.getByRole("button", { name: "Undo" }).click(); + await expect(mark.locator(".stab")).toHaveCount(2); + await expect(page.getByLabel("±40mm")).not.toBeChecked(); + + await page.getByRole("button", { name: "スイッチ・スタビライザーの位置" }).click(); + await expect(page.locator(".mount-layer")).toHaveCount(0); +}); diff --git a/mock/src/domain/stabilizers.ts b/mock/src/domain/stabilizers.ts new file mode 100644 index 0000000..7daefc1 --- /dev/null +++ b/mock/src/domain/stabilizers.ts @@ -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) }; +} diff --git a/mock/src/ui/phy/PhyEditor.vue b/mock/src/ui/phy/PhyEditor.vue index ce778ba..d34e6e7 100644 --- a/mock/src/ui/phy/PhyEditor.vue +++ b/mock/src/ui/phy/PhyEditor.vue @@ -1,5 +1,5 @@ + + diff --git a/mock/src/ui/phy/PhyProperties.vue b/mock/src/ui/phy/PhyProperties.vue index fadf553..4827c24 100644 --- a/mock/src/ui/phy/PhyProperties.vue +++ b/mock/src/ui/phy/PhyProperties.vue @@ -1,11 +1,13 @@