From 812e2edfc265abf93035c57dee73ce6b55d25033 Mon Sep 17 00:00:00 2001 From: EwdErna Date: Thu, 22 Oct 2026 09:42:32 +0900 Subject: [PATCH] =?UTF-8?q?mock:=20=E9=81=B8=E6=8A=9E=E4=B8=AD=E3=81=AE?= =?UTF-8?q?=E3=82=AF=E3=83=A9=E3=82=B9=E3=82=BF=E3=83=BC=E3=81=AB=E3=81=A0?= =?UTF-8?q?=E3=81=91=E5=8E=9F=E7=82=B9=E3=81=AE=E4=B8=B8=E3=82=92=E5=87=BA?= =?UTF-8?q?=E3=81=99(T-30=20=E5=8D=98=E4=BD=8D5)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 単位1の選択の描画層に、選択中のクラスターとバリアントグループの原点の丸を描く。 長押しの候補になっているだけのクラスターには出さない。丸の大きさは画面上で固定する。 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01NznQtmVPoQAnietqwr1auL --- mock/e2e/phy.spec.ts | 17 +++++++++++++++++ mock/src/ui/phy/PhyEditor.vue | 1 + mock/src/ui/phy/PhySelectionLayer.vue | 18 ++++++++++++++++++ mock/src/ui/phy/logic.ts | 4 ++++ mock/tests/phyLogic.test.ts | 3 +++ 5 files changed, 43 insertions(+) diff --git a/mock/e2e/phy.spec.ts b/mock/e2e/phy.spec.ts index a543506..e935bac 100644 --- a/mock/e2e/phy.spec.ts +++ b/mock/e2e/phy.spec.ts @@ -115,6 +115,23 @@ test("ドラッグ中に右端で自動スクロールし、Undo 1回で戻る", await expect(key).toHaveAttribute("transform", before!); }); +test("選択中のクラスターにだけ原点の丸を出す", async ({ page }) => { + const keys = page.locator('[data-node-type="key"]'); + const corner = (await keys.nth(0).locator(".key-face").boundingBox())!; + const cluster = await makeCluster(page); + const clusterId = await cluster.getAttribute("data-node-id"); + await keys.nth(5).click(); + await expect(page.locator(".origin")).toHaveCount(0); + await page.locator(`[data-tree-id="${clusterId}"]`).first().click(); + const origin = page.locator(`[data-origin-id="${clusterId}"]`); + await expect(origin).toHaveCount(1); + const box = (await origin.boundingBox())!; + expect(Math.abs(box.x + box.width / 2 - corner.x)).toBeLessThan(2); + expect(Math.abs(box.y + box.height / 2 - corner.y)).toBeLessThan(2); + await keys.nth(5).click(); + await expect(page.locator(".origin")).toHaveCount(0); +}); + test("クラスター化で画面位置を保つ", async ({ page }) => { const keys = page.locator('[data-node-type="key"]'); const centers = async () => diff --git a/mock/src/ui/phy/PhyEditor.vue b/mock/src/ui/phy/PhyEditor.vue index d94e75e..7d1e7de 100644 --- a/mock/src/ui/phy/PhyEditor.vue +++ b/mock/src/ui/phy/PhyEditor.vue @@ -399,6 +399,7 @@ function onFocus(_id: string) { :nodes="focus ? [node(focus)!] : project.phy.root" :selected="selected" :variants="variants" + :zoom="zoom" :candidate="longLabel ? (holdCandidates[holdIndex] ?? null) : null" :parent="focus ? focusParentMatrix(focus) : undefined" /> diff --git a/mock/src/ui/phy/PhySelectionLayer.vue b/mock/src/ui/phy/PhySelectionLayer.vue index 6186cfc..2dc5e49 100644 --- a/mock/src/ui/phy/PhySelectionLayer.vue +++ b/mock/src/ui/phy/PhySelectionLayer.vue @@ -8,6 +8,7 @@ const props = withDefaults( nodes: Node[]; selected: string[]; variants: Record; + zoom: number; candidate?: string | null; parent?: Affine; }>(), @@ -16,6 +17,8 @@ const props = withDefaults( const frames = computed(() => selectionFrames(props.nodes, props.selected, props.variants, props.candidate, props.parent), ); +// 原点の丸は選択中のクラスター(VariantGroupを含む)にだけ出す。T-30。 +const origins = computed(() => frames.value.filter((frame) => frame.selected && frame.type !== "key")); const affineText = (m: Affine) => `matrix(${m.a} ${m.b} ${m.c} ${m.d} ${m.e} ${m.f})`; diff --git a/mock/src/ui/phy/logic.ts b/mock/src/ui/phy/logic.ts index 1872223..df5e9a6 100644 --- a/mock/src/ui/phy/logic.ts +++ b/mock/src/ui/phy/logic.ts @@ -235,6 +235,7 @@ export type SelectionFrame = { type: Node["type"]; transform: Affine; box: Box; + selected: boolean; candidate: boolean; }; /** 可視ノードの選択枠をワールド行列とローカル矩形で返す。 */ @@ -255,6 +256,7 @@ export function selectionFrames( type: node.type, transform, box: { x: 0, y: 0, width: node.size.w, height: node.size.h }, + selected: selected.includes(node.id), candidate: candidate === node.id, }); } else if (node.type === "cluster") { @@ -264,6 +266,7 @@ export function selectionFrames( type: node.type, transform, box: localBounds(node.children, variants), + selected: selected.includes(node.id), candidate: candidate === node.id, }); visit(node.children, transform); @@ -275,6 +278,7 @@ export function selectionFrames( type: node.type, transform, box: localBounds(variant?.children ?? [], variants), + selected: selected.includes(node.id), candidate: candidate === node.id, }); if (variant) visit(variant.children, transform); diff --git a/mock/tests/phyLogic.test.ts b/mock/tests/phyLogic.test.ts index a59f1eb..b4c24bb 100644 --- a/mock/tests/phyLogic.test.ts +++ b/mock/tests/phyLogic.test.ts @@ -65,6 +65,9 @@ describe("PHY pure interaction rules", () => { expect(frame.transform.f).toBeCloseTo(27); expect(frame.transform.a).toBeCloseTo(Math.cos(Math.PI / 6)); expect(frame.transform.b).toBeCloseTo(Math.sin(Math.PI / 6)); + expect(frame.selected).toBe(true); + const candidateOnly = selectionFrames(roots, [], {}, "target")[0]!; + expect([candidateOnly.selected, candidateOnly.candidate]).toEqual([false, true]); }); it("removes ancestors and descendants from mixed selections", () => { expect(normalizeSelection(phy, ["c", "k"])).toEqual(["k"]);