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"]);