Height was derived from the capabilities column only; the functions column (52px step) is taller and its last box + column labels fell outside the viewBox. Size to the taller column plus label room.
132 lines
3.5 KiB
Vue
132 lines
3.5 KiB
Vue
<script setup lang="ts">
|
|
import { computed } from "vue"
|
|
import type { Composition } from "@/data/blueprint"
|
|
|
|
const props = defineProps<{ composition: Composition }>()
|
|
|
|
const PAL = {
|
|
boxFill: "#103763",
|
|
boxStroke: "#3d5f85",
|
|
ink: "#dbe9f7",
|
|
inkFaint: "#4f7099",
|
|
cyan: "#8fd0ff",
|
|
amber: "#ffb84d",
|
|
}
|
|
|
|
const W = 640
|
|
const lx = 24
|
|
const lw = 170
|
|
const rx = 446
|
|
const rw = 170
|
|
const bh = 34
|
|
const top = 16
|
|
const lstep = 40
|
|
const rstep = 52
|
|
|
|
const capY = (i: number) => top + i * lstep + bh / 2
|
|
const funcY = (i: number) => top + i * rstep + bh / 2
|
|
|
|
// viewBox height must fit whichever column is taller — the functions column
|
|
// (rstep = 52) usually outruns the capabilities column (lstep = 40) — plus room
|
|
// for the bottom labels. Sizing off caps.length alone clipped the last row.
|
|
const H = computed(() => {
|
|
const leftBottom = top + (props.composition.caps.length - 1) * lstep + bh
|
|
const rightBottom = top + (props.composition.funcs.length - 1) * rstep + bh
|
|
return Math.max(leftBottom, rightBottom) + 26
|
|
})
|
|
|
|
const capIndex = computed(
|
|
() =>
|
|
Object.fromEntries(props.composition.caps.map((c, i) => [c.id, i])) as Record<string, number>,
|
|
)
|
|
const funcIndex = computed(
|
|
() => Object.fromEntries(props.composition.funcs.map((f, i) => [f, i])) as Record<string, number>,
|
|
)
|
|
|
|
const leftBoxes = computed(() =>
|
|
props.composition.caps.map((c, i) => ({ ...c, x: lx, y: capY(i) - bh / 2, w: lw, h: bh })),
|
|
)
|
|
const rightBoxes = computed(() =>
|
|
props.composition.funcs.map((f, i) => ({ label: f, x: rx, y: funcY(i) - bh / 2, w: rw, h: bh })),
|
|
)
|
|
|
|
const edges = computed(() =>
|
|
props.composition.links.map(([a, b]) => {
|
|
const y1 = capY(capIndex.value[a])
|
|
const y2 = funcY(funcIndex.value[b])
|
|
const x1 = lx + lw
|
|
const x2 = rx
|
|
const mx = (x1 + x2) / 2
|
|
const color = a === "core" ? PAL.inkFaint : PAL.cyan
|
|
return {
|
|
key: `${a}-${b}`,
|
|
d: `M${x1} ${y1} C ${mx} ${y1}, ${mx} ${y2}, ${x2} ${y2}`,
|
|
color,
|
|
x2,
|
|
y2,
|
|
}
|
|
}),
|
|
)
|
|
</script>
|
|
|
|
<template>
|
|
<svg :viewBox="`0 0 ${W} ${H}`" role="img" aria-label="Composition map" class="schematic">
|
|
<path
|
|
v-for="e in edges"
|
|
:key="e.key"
|
|
:d="e.d"
|
|
fill="none"
|
|
stroke-width="1.4"
|
|
:stroke="e.color"
|
|
opacity="0.55"
|
|
/>
|
|
<circle v-for="e in edges" :key="`c-${e.key}`" :cx="e.x2" :cy="e.y2" r="2.4" :fill="e.color" />
|
|
|
|
<g v-for="b in leftBoxes" :key="b.id">
|
|
<rect
|
|
:x="b.x"
|
|
:y="b.y"
|
|
:width="b.w"
|
|
:height="b.h"
|
|
rx="2"
|
|
:fill="PAL.boxFill"
|
|
:stroke="b.core ? PAL.amber : PAL.boxStroke"
|
|
stroke-width="1.5"
|
|
/>
|
|
<text :x="b.x + 12" :y="b.y + 15" :fill="PAL.ink" font-size="12">{{ b.label }}</text>
|
|
<text :x="b.x + 12" :y="b.y + 27" :fill="PAL.inkFaint" font-size="9.5">{{ b.sub }}</text>
|
|
</g>
|
|
|
|
<g v-for="b in rightBoxes" :key="b.label">
|
|
<rect
|
|
:x="b.x"
|
|
:y="b.y"
|
|
:width="b.w"
|
|
:height="b.h"
|
|
rx="2"
|
|
:fill="PAL.boxFill"
|
|
:stroke="PAL.amber"
|
|
stroke-width="1.5"
|
|
/>
|
|
<text :x="b.x + 12" :y="b.y + 21" :fill="PAL.amber" font-size="12">{{ b.label }}</text>
|
|
</g>
|
|
|
|
<text :x="lx" :y="H - 4" :fill="PAL.inkFaint" font-size="10" letter-spacing="1.5">
|
|
CAPABILITIES (compose onto host)
|
|
</text>
|
|
<text :x="rx" :y="H - 4" :fill="PAL.inkFaint" font-size="10" letter-spacing="1.5">
|
|
FUNCTIONS
|
|
</text>
|
|
</svg>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.schematic {
|
|
display: block;
|
|
max-width: 100%;
|
|
height: auto;
|
|
margin: 0 auto;
|
|
font-family: var(--font-mono);
|
|
}
|
|
</style>
|