バッジは NodeSvg の再帰の中で各バリアントグループの <g> に描いていたため、 後から描かれる兄弟のキーに隠れていた。選択の描画層の後に PhyBadgeLayer を置き、 位置は純粋関数 variantBadges でワールド座標に直して正立で描く。 フォーカス中、フォーカス外のバッジは薄く描き、クリックを受けない。 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NznQtmVPoQAnietqwr1auL
155 lines
3.7 KiB
Vue
155 lines
3.7 KiB
Vue
<script setup lang="ts">
|
|
import type { Node } from "../../domain/phy";
|
|
defineOptions({ name: "NodeSvg" });
|
|
defineProps<{
|
|
nodes: Node[];
|
|
selected: string[];
|
|
variants: Record<string, string>;
|
|
cut: string[];
|
|
dim: boolean;
|
|
}>();
|
|
const emit = defineEmits<{
|
|
down: [e: PointerEvent, n: Node, el: SVGGElement];
|
|
focus: [id: string];
|
|
}>();
|
|
</script>
|
|
<template>
|
|
<g
|
|
v-for="n in nodes"
|
|
:key="n.id"
|
|
:data-node-type="n.type"
|
|
:data-node-id="n.id"
|
|
:transform="`translate(${n.transform.x} ${n.transform.y}) rotate(${n.transform.rotation})`"
|
|
:class="{ dim: dim, cut: cut.includes(n.id) }"
|
|
@dblclick.stop="emit('focus', n.id)"
|
|
>
|
|
<g
|
|
v-if="n.type === 'key'"
|
|
class="node"
|
|
@pointerdown.stop="emit('down', $event, n, $event.currentTarget as SVGGElement)"
|
|
><rect
|
|
:width="n.size.w"
|
|
:height="n.size.h"
|
|
rx=".06"
|
|
class="key-face"
|
|
:class="{ chosen: selected.includes(n.id), ghost: n.ghost, decal: n.decal }"
|
|
/><text class="key-label" x=".08" y=".24">{{ n.labels?.find(Boolean) }}</text></g
|
|
>
|
|
<g
|
|
v-else-if="n.type === 'component'"
|
|
class="node"
|
|
@pointerdown.stop="emit('down', $event, n, $event.currentTarget as SVGGElement)"
|
|
><rect
|
|
v-if="n.partGroup === '@led'"
|
|
x="-.14"
|
|
y="-.14"
|
|
width=".28"
|
|
height=".28"
|
|
transform="rotate(45)"
|
|
class="part led"
|
|
:class="{ chosen: selected.includes(n.id) }"
|
|
/><g v-else-if="n.partGroup === '@encoder'" class="part" :class="{ chosen: selected.includes(n.id) }"
|
|
><circle r=".16" /><path d="M -.1 0 H .1 M 0 -.1 V .1" /></g
|
|
><path
|
|
v-else-if="n.partGroup === '@screw'"
|
|
d="M -.14 -.14 L .14 .14 M .14 -.14 L -.14 .14"
|
|
class="part screw"
|
|
:class="{ chosen: selected.includes(n.id) }"
|
|
/><rect
|
|
v-else-if="n.partGroup === '@oled'"
|
|
x="-.2"
|
|
y="-.12"
|
|
width=".4"
|
|
height=".24"
|
|
rx=".025"
|
|
class="part oled"
|
|
:class="{ chosen: selected.includes(n.id) }"
|
|
/><circle v-else r=".14" class="part" :class="{ chosen: selected.includes(n.id) }" /><text
|
|
x=".2"
|
|
y=".04"
|
|
class="part-label"
|
|
>{{ n.partGroup || "part" }}</text
|
|
></g
|
|
>
|
|
<g
|
|
v-else-if="n.type === 'cluster'"
|
|
class="group"
|
|
@pointerdown.self="emit('down', $event, n, $event.currentTarget as SVGGElement)"
|
|
><NodeSvg
|
|
:nodes="n.children"
|
|
:selected="selected"
|
|
:variants="variants"
|
|
:cut="cut"
|
|
:dim="dim"
|
|
@down="(e, c, el) => emit('down', e, c, el)"
|
|
@focus="(id) => emit('focus', id)"
|
|
/><text x="0" y="-.12" class="group-label">{{ n.name }}</text></g
|
|
>
|
|
<g v-else class="group"
|
|
><NodeSvg
|
|
v-for="v in n.children.filter((v) => v.id === (variants[n.id] || n.default))"
|
|
:key="v.id"
|
|
:nodes="v.children"
|
|
:selected="selected"
|
|
:variants="variants"
|
|
:cut="cut"
|
|
:dim="dim"
|
|
@down="(e, c, el) => emit('down', e, c, el)"
|
|
@focus="(id) => emit('focus', id)"
|
|
/></g>
|
|
</g>
|
|
</template>
|
|
<style scoped>
|
|
.node {
|
|
cursor: grab;
|
|
}
|
|
.key-face {
|
|
fill: #f8cc78;
|
|
stroke: #5b4a2b;
|
|
stroke-width: 0.03;
|
|
}
|
|
.key-face.ghost {
|
|
opacity: 0.4;
|
|
}
|
|
.key-face.decal {
|
|
fill: #fff;
|
|
stroke-dasharray: 0.08 0.06;
|
|
}
|
|
.key-label,
|
|
.part-label,
|
|
.group-label {
|
|
font-size: 0.14px;
|
|
pointer-events: none;
|
|
}
|
|
.part {
|
|
fill: #b4d5ef;
|
|
stroke: #315d7a;
|
|
stroke-width: 0.03;
|
|
}
|
|
.part.chosen {
|
|
stroke: #1769d2;
|
|
stroke-width: 2px;
|
|
vector-effect: non-scaling-stroke;
|
|
}
|
|
.part.screw {
|
|
fill: none;
|
|
}
|
|
.part path {
|
|
fill: none;
|
|
stroke: #315d7a;
|
|
stroke-width: 0.03;
|
|
}
|
|
.group {
|
|
cursor: default;
|
|
}
|
|
.dim {
|
|
opacity: 0.25;
|
|
}
|
|
.cut {
|
|
opacity: 0.3;
|
|
}
|
|
.group-label {
|
|
fill: #222;
|
|
}
|
|
</style>
|