diff --git a/src/diagram/drawio.ts b/src/diagram/drawio.ts index 947b0c1..663d40d 100644 --- a/src/diagram/drawio.ts +++ b/src/diagram/drawio.ts @@ -29,7 +29,10 @@ import type { Diagram, DiagramMachine } from "./model.ts"; const LANE_MIN_HEIGHT = 170; -const LANE_GAP = 80; +const LANE_EMPTY_HEIGHT = 62; +// Wide enough that a gateway placed on a boundary sits BETWEEN the lanes rather than on +// top of one โ€” an earlier value let a router's box cover the lane's own name and ranges. +const LANE_GAP = 110; const LANE_X = 40; const LANE_WIDTH = 980; const NODE_WIDTH = 150; @@ -163,11 +166,19 @@ export function toDrawio(diagram: Diagram): string { // wrap onto a second row drew that row outside the box it was supposed to be inside. const occupants = new Map(); for (const machine of diagram.machines) { - const first = machine.attachments[0]?.segment; - if (first) occupants.set(first, (occupants.get(first) ?? 0) + 1); + // Only a machine that sits INSIDE a lane occupies it. A gateway is counted against the + // segment it faces but drawn in the gap, so counting it here left a segment holding no + // machines at full height with nothing in it. + if (machine.attachments.length !== 1) continue; + const only = machine.attachments[0]?.segment; + if (only) occupants.set(only, (occupants.get(only) ?? 0) + 1); } const laneHeight = (name: string): number => { - const rows = Math.max(1, Math.ceil((occupants.get(name) ?? 0) / SLOTS_PER_ROW)); + const rows = Math.ceil((occupants.get(name) ?? 0) / SLOTS_PER_ROW); + // A segment whose only occupants are the gateways in the gaps beside it holds nothing + // itself, so it collapses to its own name and ranges. Left at full height it padded a + // layered scenario with empty boxes and pushed the interesting rows apart. + if (rows === 0) return LANE_EMPTY_HEIGHT; return Math.max(LANE_MIN_HEIGHT, 45 + rows * (NODE_HEIGHT + 45)); }; @@ -233,13 +244,22 @@ export function toDrawio(diagram: Diagram): string { const row = Math.floor(slot / SLOTS_PER_ROW); x = LANE_X + 40 + column * SLOT_WIDTH; if (spans) { - // A gateway straddles, so it sits in the gap below the highest lane it joins โ€” - // which is what makes at-a-glance reading of "this is the way in" work. - const top = machine.attachments - .map((a) => a.segment) - .filter((name) => laneY.has(name)) - .reduce((best, name) => ((laneY.get(name) ?? 0) < (laneY.get(best) ?? 0) ? name : best)); - y = (laneY.get(top) ?? 80) + (laneH.get(top) ?? LANE_MIN_HEIGHT) + LANE_GAP / 2 - NODE_HEIGHT / 2; + // A gateway straddles a boundary, and WHICH boundary matters. Its first attachment + // is the segment it faces outward on; the rest are the ones it serves. Placing it + // below the outward lane put a gateway serving `home` and `devices` above an + // unrelated `cafe`, with its connection crossing a network it has nothing to do + // with. It belongs immediately above the topmost lane it actually serves. + const drawn = machine.attachments.map((a) => a.segment).filter((name) => laneY.has(name)); + const served = drawn.slice(1); + const highest = (names: string[]) => + names.reduce((best, name) => ((laneY.get(name) ?? 0) < (laneY.get(best) ?? 0) ? name : best)); + if (served.length > 0) { + y = (laneY.get(highest(served)) ?? 80) - LANE_GAP / 2 - NODE_HEIGHT / 2; + } else { + // Transit faces every lane and serves none, so it sits below the topmost. + const top = highest(drawn); + y = (laneY.get(top) ?? 80) + (laneH.get(top) ?? LANE_MIN_HEIGHT) + LANE_GAP / 2 - NODE_HEIGHT / 2; + } } else { y = (laneY.get(first.segment) ?? 80) + 45 + row * (NODE_HEIGHT + 45); } @@ -266,14 +286,15 @@ export function toDrawio(diagram: Diagram): string { .join(" ยท "), }); - // Badges sit along the top edge, right to left, so the first fact stated is nearest the - // resource's own corner and the row grows away from the label underneath it. + // Badges sit along the top edge and read left to right in the order the facts are + // stated, which is the order the declaration states them in. + const badgeRun = badges.length * BADGE + Math.max(0, badges.length - 1) * 3; badges.forEach((badge, index) => { cells.push({ id: `${id}-b${index}`, value: badge.code, style: `${STYLE.badge};fillColor=${badge.fill};strokeColor=${badge.stroke}`, - x: x + NODE_WIDTH - BADGE - index * (BADGE + 3), + x: x + NODE_WIDTH - badgeRun + index * (BADGE + 3), y: y - BADGE / 2, w: BADGE, h: BADGE, diff --git a/src/diagram/from-live.ts b/src/diagram/from-live.ts index 19315b7..2529ca0 100644 --- a/src/diagram/from-live.ts +++ b/src/diagram/from-live.ts @@ -87,7 +87,13 @@ export async function diagramFromLive(instanceId: string): Promise { const addresses = (mac ? heldByMac.get(mac) : undefined) ?? heldByName.get(device) ?? []; attachments.push({ segment, addresses }); } - attachments.sort((a, b) => a.segment.localeCompare(b.segment)); + // Outside first, then the segments behind it โ€” the same order the declared picture uses, + // and what lets the layout place a gateway above the lanes it SERVES rather than below + // the one it faces. Sorting alphabetically threw that away. + const outside = config["user.mesh-lab.outside"]; + attachments.sort((a, b) => + a.segment === outside ? -1 : b.segment === outside ? 1 : a.segment.localeCompare(b.segment), + ); const notes: string[] = []; notes.push(item.type === "container" ? "container" : "virtual machine");