mock: バリアントの既定の印・バッジ・作成先を直す(T-30 単位3)
ポップオーバーの既定の印を「既定」の札にする。バッジには名前を書かず、 表示中のバリアント名はツールチップで示す(はみ出しをなくす。T-30の着手前の決定を反映)。 バリアントグループは選択の入れ物(クラスター・バリアント)の中に作る。 そのため domain に createVariantGroupInParent を足し、variant のIDを渡すと 例外になっていた parentNodes を直した。 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NznQtmVPoQAnietqwr1auL
This commit is contained in:
@@ -257,6 +257,26 @@ test("選択枠は画面上で見え、バリアントバッジで表示を切
|
|||||||
await expect(keys).toHaveCount(16);
|
await expect(keys).toHaveCount(16);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("クラスター内のキーから作ったバリアントグループはクラスターの中にでき、バッジは名前を書かない", async ({
|
||||||
|
page,
|
||||||
|
}) => {
|
||||||
|
const cluster = await makeCluster(page);
|
||||||
|
const key = cluster.locator('[data-node-type="key"]').first();
|
||||||
|
await key.click();
|
||||||
|
await page.getByRole("button", { name: "選択からバリアントグループ" }).click();
|
||||||
|
const group = cluster.locator('[data-node-type="variantGroup"]');
|
||||||
|
await expect(group).toHaveCount(1);
|
||||||
|
const badge = group.locator(".badge");
|
||||||
|
await expect(badge.locator("text")).toHaveText("⇄");
|
||||||
|
await expect(badge.locator("title")).toHaveText("Variant Group: Option A(既定)");
|
||||||
|
await badge.locator("rect").click();
|
||||||
|
const popover = page.locator(".popover");
|
||||||
|
await expect(popover.getByRole("button", { name: /Option A/ }).locator(".default-tag")).toHaveText("既定");
|
||||||
|
await expect(popover.getByRole("button", { name: /Option B/ }).locator(".default-tag")).toHaveCount(0);
|
||||||
|
await popover.getByRole("button", { name: /Option B/ }).click();
|
||||||
|
await expect(badge.locator("title")).toHaveText("Variant Group: Option B");
|
||||||
|
});
|
||||||
|
|
||||||
test("削除確認は参照を列挙し、実行後はIDXからも消える", async ({ page }) => {
|
test("削除確認は参照を列挙し、実行後はIDXからも消える", async ({ page }) => {
|
||||||
const key = page.locator('[data-node-type="key"]').first();
|
const key = page.locator('[data-node-type="key"]').first();
|
||||||
const id = await key.getAttribute("data-node-id");
|
const id = await key.getAttribute("data-node-id");
|
||||||
|
|||||||
@@ -114,9 +114,12 @@ export function insertNodes(f: PhyFile, parentId: string | null, nodes: Node[],
|
|||||||
|
|
||||||
function parentNodes(f: PhyFile, parentId: string | null): Node[] {
|
function parentNodes(f: PhyFile, parentId: string | null): Node[] {
|
||||||
if (!parentId) return f.root;
|
if (!parentId) return f.root;
|
||||||
const n = find(f, parentId);
|
// variantのIDは containers に子の入れ物として載るので、find ではノードとして引けない
|
||||||
if (n.type === "cluster") return n.children;
|
const n = containers(f)
|
||||||
if (n.type === "variantGroup") throw new Error("Choose a variant as the parent");
|
.get(parentId)
|
||||||
|
?.nodes.find((x) => x.id === parentId);
|
||||||
|
if (n?.type === "cluster") return n.children;
|
||||||
|
if (n?.type === "variantGroup") throw new Error("Choose a variant as the parent");
|
||||||
return findVariant(f, parentId).children;
|
return findVariant(f, parentId).children;
|
||||||
}
|
}
|
||||||
/** ローカル座標の差分でノードを移動する。 */
|
/** ローカル座標の差分でノードを移動する。 */
|
||||||
@@ -397,6 +400,17 @@ export function createVariantGroup(
|
|||||||
at: Transform,
|
at: Transform,
|
||||||
variants: [{ name: string; children?: Node[] }, { name: string; children?: Node[] }],
|
variants: [{ name: string; children?: Node[] }, { name: string; children?: Node[] }],
|
||||||
gen?: () => string,
|
gen?: () => string,
|
||||||
|
): PhyFile {
|
||||||
|
return createVariantGroupInParent(f, null, name, at, variants, gen);
|
||||||
|
}
|
||||||
|
/** 指定コンテナへVariantGroupを作成する。T-11。 */
|
||||||
|
export function createVariantGroupInParent(
|
||||||
|
f: PhyFile,
|
||||||
|
parentId: string | null,
|
||||||
|
name: string,
|
||||||
|
at: Transform,
|
||||||
|
variants: [{ name: string; children?: Node[] }, { name: string; children?: Node[] }],
|
||||||
|
gen?: () => string,
|
||||||
): PhyFile {
|
): PhyFile {
|
||||||
const o = mutable(f),
|
const o = mutable(f),
|
||||||
children = variants.map((x) => ({
|
children = variants.map((x) => ({
|
||||||
@@ -405,7 +419,7 @@ export function createVariantGroup(
|
|||||||
name: x.name,
|
name: x.name,
|
||||||
children: structuredClone(x.children ?? []),
|
children: structuredClone(x.children ?? []),
|
||||||
}));
|
}));
|
||||||
o.root.push({
|
parentNodes(o, parentId).push({
|
||||||
type: "variantGroup",
|
type: "variantGroup",
|
||||||
id: nodeId("variantGroup", gen),
|
id: nodeId("variantGroup", gen),
|
||||||
name,
|
name,
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ function bounds(n: Node) {
|
|||||||
}
|
}
|
||||||
function badgeBox(n: Node) {
|
function badgeBox(n: Node) {
|
||||||
const b = bounds(n);
|
const b = bounds(n);
|
||||||
const size = 26 / props.zoom;
|
const size = 20 / props.zoom;
|
||||||
return {
|
return {
|
||||||
x: b.x + b.width + 4 / props.zoom,
|
x: b.x + b.width + 4 / props.zoom,
|
||||||
y: b.y - size - 4 / props.zoom,
|
y: b.y - size - 4 / props.zoom,
|
||||||
@@ -36,6 +36,12 @@ function badgeBox(n: Node) {
|
|||||||
angle: -(props.angle + n.transform.rotation),
|
angle: -(props.angle + n.transform.rotation),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
/** バッジは名前を書かず、表示中のバリアント名はツールチップで示す。T-30。 */
|
||||||
|
function badgeTitle(n: Extract<Node, { type: "variantGroup" }>) {
|
||||||
|
const shown = props.variants[n.id] || n.default;
|
||||||
|
const name = n.children.find((v) => v.id === shown)?.name ?? "";
|
||||||
|
return `${n.name}: ${name}${shown === n.default ? "(既定)" : ""}`;
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<g
|
<g
|
||||||
@@ -135,15 +141,14 @@ function badgeBox(n: Node) {
|
|||||||
:y="badgeBox(n).y"
|
:y="badgeBox(n).y"
|
||||||
:width="badgeBox(n).size"
|
:width="badgeBox(n).size"
|
||||||
:height="badgeBox(n).size"
|
:height="badgeBox(n).size"
|
||||||
rx="3"
|
:rx="3 / zoom"
|
||||||
@click.stop="emit('badge', n, $event)"
|
@click.stop="emit('badge', n, $event)"
|
||||||
/><text
|
/><text
|
||||||
:x="badgeBox(n).x + 3 / zoom"
|
:x="badgeBox(n).x + badgeBox(n).size / 2"
|
||||||
:y="badgeBox(n).y + 16 / zoom"
|
:y="badgeBox(n).y + badgeBox(n).size / 2"
|
||||||
:style="{ fontSize: `${12 / zoom}px` }"
|
:style="{ fontSize: `${12 / zoom}px` }"
|
||||||
>{{ n.children.find((v) => v.id === (variants[n.id] || n.default))?.name
|
>⇄</text
|
||||||
}}{{ n.default === (variants[n.id] || n.default) ? " ✓" : "" }}</text
|
><title>{{ badgeTitle(n) }}</title></g
|
||||||
></g
|
|
||||||
></g
|
></g
|
||||||
>
|
>
|
||||||
</g>
|
</g>
|
||||||
@@ -213,6 +218,8 @@ function badgeBox(n: Node) {
|
|||||||
.badge text {
|
.badge text {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
fill: #302709;
|
fill: #302709;
|
||||||
|
text-anchor: middle;
|
||||||
|
dominant-baseline: central;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -431,7 +431,7 @@ function onFocus(_id: string) {
|
|||||||
>
|
>
|
||||||
<template v-for="g in variantGroups.filter((n) => n.id === popover)" :key="g.id"
|
<template v-for="g in variantGroups.filter((n) => n.id === popover)" :key="g.id"
|
||||||
><button v-for="v in g.children" :key="v.id" @click="chooseVariant(g, v.id)">
|
><button v-for="v in g.children" :key="v.id" @click="chooseVariant(g, v.id)">
|
||||||
{{ v.name }} {{ g.default === v.id ? "✓" : "" }}
|
{{ v.name }}<span v-if="g.default === v.id" class="default-tag">既定</span>
|
||||||
</button></template
|
</button></template
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
@@ -581,6 +581,14 @@ function onFocus(_id: string) {
|
|||||||
.warning {
|
.warning {
|
||||||
color: #9a4c00;
|
color: #9a4c00;
|
||||||
}
|
}
|
||||||
|
.default-tag {
|
||||||
|
margin-left: 6px;
|
||||||
|
padding: 0 4px;
|
||||||
|
border: 1px solid #8a691b;
|
||||||
|
border-radius: 3px;
|
||||||
|
font-size: 11px;
|
||||||
|
color: #6b520f;
|
||||||
|
}
|
||||||
.long-label {
|
.long-label {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
z-index: 3;
|
z-index: 3;
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import {
|
|||||||
addKeyToParent,
|
addKeyToParent,
|
||||||
clusterNodes,
|
clusterNodes,
|
||||||
cloneNodesWithNewIds,
|
cloneNodesWithNewIds,
|
||||||
createVariantGroup,
|
createVariantGroupInParent,
|
||||||
insertNodes,
|
insertNodes,
|
||||||
reparent,
|
reparent,
|
||||||
reparentToVariant,
|
reparentToVariant,
|
||||||
@@ -205,12 +205,16 @@ export function usePhyCommands(options: Options) {
|
|||||||
|
|
||||||
function variantGroup() {
|
function variantGroup() {
|
||||||
if (!options.selected.value.length) return;
|
if (!options.selected.value.length) return;
|
||||||
|
const parent = options.selectedContainer(options.selected.value[0]!);
|
||||||
let id = "";
|
let id = "";
|
||||||
options.dispatch("バリアントグループ作成", (project) => {
|
options.dispatch("バリアントグループ作成", (project) => {
|
||||||
let phy = createVariantGroup(project.phy, "Variant Group", { x: 0, y: 0, rotation: 0 }, [
|
let phy = createVariantGroupInParent(
|
||||||
{ name: "Option A" },
|
project.phy,
|
||||||
{ name: "Option B" },
|
parent,
|
||||||
]);
|
"Variant Group",
|
||||||
|
{ x: 0, y: 0, rotation: 0 },
|
||||||
|
[{ name: "Option A" }, { name: "Option B" }],
|
||||||
|
);
|
||||||
const group = options
|
const group = options
|
||||||
.allNodes(phy.root)
|
.allNodes(phy.root)
|
||||||
.find(
|
.find(
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import {
|
|||||||
addComponentToParent,
|
addComponentToParent,
|
||||||
addKeyToParent,
|
addKeyToParent,
|
||||||
cloneNodesWithNewIds,
|
cloneNodesWithNewIds,
|
||||||
|
createVariantGroupInParent,
|
||||||
duplicateNode,
|
duplicateNode,
|
||||||
insertNodes,
|
insertNodes,
|
||||||
} from "../src/domain/phyCommands";
|
} from "../src/domain/phyCommands";
|
||||||
@@ -102,4 +103,22 @@ describe("PHY parent insertion and duplication", () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
it("creates a variant group inside a cluster or a variant", () => {
|
||||||
|
const variants: Parameters<typeof createVariantGroupInParent>[4] = [{ name: "A" }, { name: "B" }];
|
||||||
|
let p = createVariantGroupInParent(file, "cl-parent", "G", { x: 0, y: 0, rotation: 0 }, variants, ids);
|
||||||
|
const parent = p.root[0] as import("../src/domain/phy").ClusterNode;
|
||||||
|
expect(p.root).toHaveLength(1);
|
||||||
|
expect(parent.children.map((n) => n.type)).toEqual(["variantGroup"]);
|
||||||
|
const group = parent.children[0] as import("../src/domain/phy").VariantGroupNode;
|
||||||
|
p = createVariantGroupInParent(
|
||||||
|
p,
|
||||||
|
group.children[1]!.id,
|
||||||
|
"Inner",
|
||||||
|
{ x: 0, y: 0, rotation: 0 },
|
||||||
|
variants,
|
||||||
|
ids,
|
||||||
|
);
|
||||||
|
const updated = (p.root[0] as import("../src/domain/phy").ClusterNode).children[0] as typeof group;
|
||||||
|
expect(updated.children[1]!.children.map((n) => n.type)).toEqual(["variantGroup"]);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ T-29 のコードを作り直さず、[phy-editor-feedback.md](phy-editor-feedba
|
|||||||
| --- | --- | --- | --- |
|
| --- | --- | --- | --- |
|
||||||
| 1 | 選択の描画層 | 選択枠を太くし、`NodeSvg` の再帰から外して最前面の層に描く。以後のオーバーレイ(5・8)もこの層に載せる | `NodeSvg.vue`、`PhyEditor.vue` |
|
| 1 | 選択の描画層 | 選択枠を太くし、`NodeSvg` の再帰から外して最前面の層に描く。以後のオーバーレイ(5・8)もこの層に載せる | `NodeSvg.vue`、`PhyEditor.vue` |
|
||||||
| 2 | パネルの幅 | 階層パネルを閉じたら、キャンバスが空いた幅を埋める | `PhyEditor.vue` の `grid-template-columns` |
|
| 2 | パネルの幅 | 階層パネルを閉じたら、キャンバスが空いた幅を埋める | `PhyEditor.vue` の `grid-template-columns` |
|
||||||
| 3 | バリアント | ポップオーバーの印を「既定」と分かる見た目にする。バッジの名前のはみ出しを直す。クラスター内のキーから作ったバリアントグループは、そのクラスターの中に作る | `NodeSvg.vue`、`usePhyCommands.ts` |
|
| 3 | バリアント | ポップオーバーの印を「既定」と分かる見た目にする。バッジには名前を書かず、表示中のバリアント名はツールチップで示す。クラスター内のキーから作ったバリアントグループは、そのクラスターの中に作る | `NodeSvg.vue`、`usePhyCommands.ts` |
|
||||||
| 4 | ドラッグ中の自動スクロール | キーを画面の端へ寄せると、キャンバスがスクロールする | `usePhyCanvasPointer.ts`、`usePhyCanvasView.ts` |
|
| 4 | ドラッグ中の自動スクロール | キーを画面の端へ寄せると、キャンバスがスクロールする | `usePhyCanvasPointer.ts`、`usePhyCanvasView.ts` |
|
||||||
| 5 | クラスターの原点 | 選択中のクラスターにだけ、原点に丸を出す | 1の層 |
|
| 5 | クラスターの原点 | 選択中のクラスターにだけ、原点に丸を出す | 1の層 |
|
||||||
| 6 | キー追加の位置 | 選択中のキーの行の右端に追加する。行の判定は `domain/` に純粋関数として実装してテストし、のちにIDX Editorでも使う | `domain/`、`usePhyCommands.ts` |
|
| 6 | キー追加の位置 | 選択中のキーの行の右端に追加する。行の判定は `domain/` に純粋関数として実装してテストし、のちにIDX Editorでも使う | `domain/`、`usePhyCommands.ts` |
|
||||||
@@ -94,7 +94,6 @@ T-29 のコードを作り直さず、[phy-editor-feedback.md](phy-editor-feedba
|
|||||||
|
|
||||||
着手前に決めること:
|
着手前に決めること:
|
||||||
|
|
||||||
- 3: バッジは名前を書かないのか、バッジの中に名前を収めるのか。
|
|
||||||
- 6: 行の判定は idx-editor-feedback.md の規則(そろっている軸は座標でまとめ、ずれている軸は隣とのつながりでたどる)をそのまま使ってよいか。
|
- 6: 行の判定は idx-editor-feedback.md の規則(そろっている軸は座標でまとめ、ずれている軸は隣とのつながりでたどる)をそのまま使ってよいか。
|
||||||
もう1つ、キーを選んでいない・複数選んでいるときにどこへ追加するか。
|
もう1つ、キーを選んでいない・複数選んでいるときにどこへ追加するか。
|
||||||
- 7b: 長押しの間隔の値。リスト方式でどう選ぶか(離した位置で決めるのか、リストを出したあとにクリックするのか)。
|
- 7b: 長押しの間隔の値。リスト方式でどう選ぶか(離した位置で決めるのか、リストを出したあとにクリックするのか)。
|
||||||
|
|||||||
Reference in New Issue
Block a user