mock: 選択枠を太くし、最前面の描画層に移す(T-30 単位1)

選択枠と長押し候補の枠を NodeSvg の再帰から外し、ノードの描画より後に描く
PhySelectionLayer に移した。枠はワールド変換とローカル矩形から純粋関数
selectionFrames で求め、画面上3.5pxで描く。部品の選択表示は形状上に残す。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TDkA8qMQ3Uf1eY3Ar19vjy
This commit is contained in:
2026-10-21 08:42:14 +09:00
co-authored by Claude Opus 5.5
parent d110320cb8
commit e92708dc12
6 changed files with 202 additions and 53 deletions
+55
View File
@@ -229,6 +229,61 @@ export function localBounds(nodes: Node[], variants: Record<string, string> = {}
visit(nodes, { a: 1, b: 0, c: 0, d: 1, e: 0, f: 0 });
return obbAabb(points);
}
export type SelectionFrame = {
id: string;
type: Node["type"];
transform: Affine;
box: Box;
candidate: boolean;
};
/** 可視ノードの選択枠をワールド行列とローカル矩形で返す。 */
export function selectionFrames(
roots: Node[],
selected: string[],
variants: Record<string, string>,
candidate: string | null,
parent: Affine = { a: 1, b: 0, c: 0, d: 1, e: 0, f: 0 },
): SelectionFrame[] {
const frames: SelectionFrame[] = [];
const visit = (nodes: Node[], ancestor: Affine) => {
for (const node of nodes) {
const transform = compose(ancestor, matrix(node.transform));
if (node.type === "key" && (selected.includes(node.id) || candidate === node.id)) {
frames.push({
id: node.id,
type: node.type,
transform,
box: { x: 0, y: 0, width: node.size.w, height: node.size.h },
candidate: candidate === node.id,
});
} else if (node.type === "cluster") {
if (selected.includes(node.id) || candidate === node.id)
frames.push({
id: node.id,
type: node.type,
transform,
box: localBounds(node.children, variants),
candidate: candidate === node.id,
});
visit(node.children, transform);
} else if (node.type === "variantGroup") {
const variant = node.children.find((item) => item.id === (variants[node.id] ?? node.default));
if (selected.includes(node.id) || candidate === node.id)
frames.push({
id: node.id,
type: node.type,
transform,
box: localBounds(variant?.children ?? [], variants),
candidate: candidate === node.id,
});
if (variant) visit(variant.children, transform);
}
}
};
visit(roots, parent);
return frames;
}
/** ワールド位置を貼り先の親ローカルへ戻し、親軸で1刻みずらす。T-11/T-27。 */
export function pasteLocalTransform(sourceWorld: Affine, parentWorld: Affine, step: number): Transform {
const local = compose(inverse(parentWorld), sourceWorld);