Files
blueprints/src/components/CompositionMap.vue
Julien Calixte c66f13c1c5 fix: composition map viewBox clipped last row
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.
2026-07-02 18:38:28 +02:00

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>