mock: 選択中のクラスターにだけ原点の丸を出す(T-30 単位5)
単位1の選択の描画層に、選択中のクラスターとバリアントグループの原点の丸を描く。 長押しの候補になっているだけのクラスターには出さない。丸の大きさは画面上で固定する。 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NznQtmVPoQAnietqwr1auL
This commit is contained in:
@@ -115,6 +115,23 @@ test("ドラッグ中に右端で自動スクロールし、Undo 1回で戻る",
|
|||||||
await expect(key).toHaveAttribute("transform", before!);
|
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 }) => {
|
test("クラスター化で画面位置を保つ", async ({ page }) => {
|
||||||
const keys = page.locator('[data-node-type="key"]');
|
const keys = page.locator('[data-node-type="key"]');
|
||||||
const centers = async () =>
|
const centers = async () =>
|
||||||
|
|||||||
@@ -399,6 +399,7 @@ function onFocus(_id: string) {
|
|||||||
:nodes="focus ? [node(focus)!] : project.phy.root"
|
:nodes="focus ? [node(focus)!] : project.phy.root"
|
||||||
:selected="selected"
|
:selected="selected"
|
||||||
:variants="variants"
|
:variants="variants"
|
||||||
|
:zoom="zoom"
|
||||||
:candidate="longLabel ? (holdCandidates[holdIndex] ?? null) : null"
|
:candidate="longLabel ? (holdCandidates[holdIndex] ?? null) : null"
|
||||||
:parent="focus ? focusParentMatrix(focus) : undefined"
|
:parent="focus ? focusParentMatrix(focus) : undefined"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ const props = withDefaults(
|
|||||||
nodes: Node[];
|
nodes: Node[];
|
||||||
selected: string[];
|
selected: string[];
|
||||||
variants: Record<string, string>;
|
variants: Record<string, string>;
|
||||||
|
zoom: number;
|
||||||
candidate?: string | null;
|
candidate?: string | null;
|
||||||
parent?: Affine;
|
parent?: Affine;
|
||||||
}>(),
|
}>(),
|
||||||
@@ -16,6 +17,8 @@ const props = withDefaults(
|
|||||||
const frames = computed(() =>
|
const frames = computed(() =>
|
||||||
selectionFrames(props.nodes, props.selected, props.variants, props.candidate, props.parent),
|
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})`;
|
const affineText = (m: Affine) => `matrix(${m.a} ${m.b} ${m.c} ${m.d} ${m.e} ${m.f})`;
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
@@ -31,6 +34,15 @@ const affineText = (m: Affine) => `matrix(${m.a} ${m.b} ${m.c} ${m.d} ${m.e} ${m
|
|||||||
:width="frame.box.width"
|
:width="frame.box.width"
|
||||||
:height="frame.box.height"
|
:height="frame.box.height"
|
||||||
/>
|
/>
|
||||||
|
<circle
|
||||||
|
v-for="frame in origins"
|
||||||
|
:key="`origin-${frame.id}`"
|
||||||
|
:data-origin-id="frame.id"
|
||||||
|
class="origin"
|
||||||
|
:cx="frame.transform.e"
|
||||||
|
:cy="frame.transform.f"
|
||||||
|
:r="5 / zoom"
|
||||||
|
/>
|
||||||
</g>
|
</g>
|
||||||
</template>
|
</template>
|
||||||
<style scoped>
|
<style scoped>
|
||||||
@@ -48,4 +60,10 @@ const affineText = (m: Affine) => `matrix(${m.a} ${m.b} ${m.c} ${m.d} ${m.e} ${m
|
|||||||
stroke: #e87912;
|
stroke: #e87912;
|
||||||
stroke-width: 3.5px;
|
stroke-width: 3.5px;
|
||||||
}
|
}
|
||||||
|
.origin {
|
||||||
|
fill: #fff;
|
||||||
|
stroke: #1769d2;
|
||||||
|
stroke-width: 2px;
|
||||||
|
vector-effect: non-scaling-stroke;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -235,6 +235,7 @@ export type SelectionFrame = {
|
|||||||
type: Node["type"];
|
type: Node["type"];
|
||||||
transform: Affine;
|
transform: Affine;
|
||||||
box: Box;
|
box: Box;
|
||||||
|
selected: boolean;
|
||||||
candidate: boolean;
|
candidate: boolean;
|
||||||
};
|
};
|
||||||
/** 可視ノードの選択枠をワールド行列とローカル矩形で返す。 */
|
/** 可視ノードの選択枠をワールド行列とローカル矩形で返す。 */
|
||||||
@@ -255,6 +256,7 @@ export function selectionFrames(
|
|||||||
type: node.type,
|
type: node.type,
|
||||||
transform,
|
transform,
|
||||||
box: { x: 0, y: 0, width: node.size.w, height: node.size.h },
|
box: { x: 0, y: 0, width: node.size.w, height: node.size.h },
|
||||||
|
selected: selected.includes(node.id),
|
||||||
candidate: candidate === node.id,
|
candidate: candidate === node.id,
|
||||||
});
|
});
|
||||||
} else if (node.type === "cluster") {
|
} else if (node.type === "cluster") {
|
||||||
@@ -264,6 +266,7 @@ export function selectionFrames(
|
|||||||
type: node.type,
|
type: node.type,
|
||||||
transform,
|
transform,
|
||||||
box: localBounds(node.children, variants),
|
box: localBounds(node.children, variants),
|
||||||
|
selected: selected.includes(node.id),
|
||||||
candidate: candidate === node.id,
|
candidate: candidate === node.id,
|
||||||
});
|
});
|
||||||
visit(node.children, transform);
|
visit(node.children, transform);
|
||||||
@@ -275,6 +278,7 @@ export function selectionFrames(
|
|||||||
type: node.type,
|
type: node.type,
|
||||||
transform,
|
transform,
|
||||||
box: localBounds(variant?.children ?? [], variants),
|
box: localBounds(variant?.children ?? [], variants),
|
||||||
|
selected: selected.includes(node.id),
|
||||||
candidate: candidate === node.id,
|
candidate: candidate === node.id,
|
||||||
});
|
});
|
||||||
if (variant) visit(variant.children, transform);
|
if (variant) visit(variant.children, transform);
|
||||||
|
|||||||
@@ -65,6 +65,9 @@ describe("PHY pure interaction rules", () => {
|
|||||||
expect(frame.transform.f).toBeCloseTo(27);
|
expect(frame.transform.f).toBeCloseTo(27);
|
||||||
expect(frame.transform.a).toBeCloseTo(Math.cos(Math.PI / 6));
|
expect(frame.transform.a).toBeCloseTo(Math.cos(Math.PI / 6));
|
||||||
expect(frame.transform.b).toBeCloseTo(Math.sin(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", () => {
|
it("removes ancestors and descendants from mixed selections", () => {
|
||||||
expect(normalizeSelection(phy, ["c", "k"])).toEqual(["k"]);
|
expect(normalizeSelection(phy, ["c", "k"])).toEqual(["k"]);
|
||||||
|
|||||||
Reference in New Issue
Block a user