fix(mazenet): auto-layout nets + deckies in a deterministic grid
Dropping more than one LAN near the same spot stacked the NetBox rectangles on top of each other, and multiple deckies in a LAN landed on identical per-LAN coordinates. Since canvas position persistence is deferred (localStorage pass), the stored x/y are not load-bearing — compute layout from the topology graph instead. adaptTopology now lays LANs out in a 3-col grid with the DMZ first and stacks deckies 2-wide inside their home LAN. New LAN palette drops append to the same grid, ignoring the raw drop point.
This commit is contained in:
@@ -67,9 +67,14 @@ const MazeNET: React.FC = () => {
|
||||
flashErr(null, 'topology already has a DMZ');
|
||||
return;
|
||||
}
|
||||
const w = 320, h = 240;
|
||||
const x = Math.round(world.x - w / 2);
|
||||
const y = Math.round(world.y - h / 2);
|
||||
// Append to the 3-col grid matching adaptTopology so new drops
|
||||
// never land on top of existing LANs. The raw drop point is
|
||||
// ignored — cleaner than trying to resolve collisions after.
|
||||
const w = 300, h = 240;
|
||||
const GAP = 40, COLS = 3;
|
||||
const i = nets.filter((n) => n.kind !== 'internet').length;
|
||||
const x = GAP + (i % COLS) * (w + GAP);
|
||||
const y = GAP + Math.floor(i / COLS) * (h + GAP);
|
||||
const name = isDmz ? `dmz-${hex4()}` : `subnet-${hex4()}`;
|
||||
try {
|
||||
const subnet = await api.getNextSubnet().catch(() => undefined);
|
||||
|
||||
@@ -62,35 +62,63 @@ export interface HydratedTopology {
|
||||
* placement. Decky-to-decky traffic edges are derived from
|
||||
* shared-LAN co-membership for visualization only. */
|
||||
export function adaptTopology(detail: TopologyDetail): HydratedTopology {
|
||||
const nets: Net[] = detail.lans.map((lan, i) => ({
|
||||
// Auto-layout: DMZ pinned top-left, subnets flow in a grid to the right.
|
||||
// We ignore lan.x/lan.y from the backend because canvas position
|
||||
// persistence is deferred (handled via localStorage in a later pass).
|
||||
// Computing layout from the graph keeps the canvas readable no matter
|
||||
// how sloppy the original drop points were.
|
||||
const NET_W = 300;
|
||||
const NET_H = 240;
|
||||
const GAP_X = 40;
|
||||
const GAP_Y = 40;
|
||||
const COLS = 3;
|
||||
const dmzs = detail.lans.filter((l) => l.is_dmz);
|
||||
const subnets = detail.lans.filter((l) => !l.is_dmz);
|
||||
const ordered = [...dmzs, ...subnets];
|
||||
const nets: Net[] = ordered.map((lan, i) => ({
|
||||
id: lan.id,
|
||||
label: lan.name.toUpperCase(),
|
||||
cidr: lan.subnet,
|
||||
kind: lan.is_dmz ? 'dmz' : 'subnet',
|
||||
x: lan.x ?? 40 + (i % 3) * 320,
|
||||
y: lan.y ?? 40 + Math.floor(i / 3) * 280,
|
||||
w: 300,
|
||||
h: 240,
|
||||
x: GAP_X + (i % COLS) * (NET_W + GAP_X),
|
||||
y: GAP_Y + Math.floor(i / COLS) * (NET_H + GAP_Y),
|
||||
w: NET_W,
|
||||
h: NET_H,
|
||||
}));
|
||||
|
||||
// Home LAN = first edge; a multi-homed gateway is drawn inside its
|
||||
// home LAN, membership in others is expressed via the edge list.
|
||||
const firstLanFor = new Map<string, string>();
|
||||
for (const e of detail.edges) {
|
||||
if (!firstLanFor.has(e.decky_uuid)) firstLanFor.set(e.decky_uuid, e.lan_id);
|
||||
}
|
||||
|
||||
const nodes: MazeNode[] = detail.deckies.map((d, i): DeckyNode => ({
|
||||
kind: 'decky',
|
||||
id: d.uuid,
|
||||
netId: firstLanFor.get(d.uuid) ?? (nets[0]?.id ?? ''),
|
||||
name: d.name,
|
||||
archetype: (d.decky_config as { archetype?: string } | null)?.archetype ?? 'linux-server',
|
||||
services: d.services,
|
||||
status: d.state === 'running' ? 'active' : d.state === 'failed' ? 'hot' : 'idle',
|
||||
x: d.x ?? 20 + (i % 2) * 160,
|
||||
y: d.y ?? 60 + Math.floor(i / 2) * 90,
|
||||
ip: d.ip ?? undefined,
|
||||
decky_config: d.decky_config ?? undefined,
|
||||
}));
|
||||
// Layout deckies in a 2-column grid inside their home LAN so two
|
||||
// members never overlap regardless of backend x/y. Same reasoning as
|
||||
// the LAN grid above.
|
||||
const NODE_COL_W = 140;
|
||||
const NODE_ROW_H = 82;
|
||||
const NODE_X0 = 12;
|
||||
const NODE_Y0 = 40;
|
||||
const perNetIndex = new Map<string, number>();
|
||||
const nodes: MazeNode[] = detail.deckies.map((d): DeckyNode => {
|
||||
const homeNetId = firstLanFor.get(d.uuid) ?? (nets[0]?.id ?? '');
|
||||
const idx = perNetIndex.get(homeNetId) ?? 0;
|
||||
perNetIndex.set(homeNetId, idx + 1);
|
||||
return {
|
||||
kind: 'decky',
|
||||
id: d.uuid,
|
||||
netId: homeNetId,
|
||||
name: d.name,
|
||||
archetype: (d.decky_config as { archetype?: string } | null)?.archetype ?? 'linux-server',
|
||||
services: d.services,
|
||||
status: d.state === 'running' ? 'active' : d.state === 'failed' ? 'hot' : 'idle',
|
||||
x: NODE_X0 + (idx % 2) * NODE_COL_W,
|
||||
y: NODE_Y0 + Math.floor(idx / 2) * NODE_ROW_H,
|
||||
ip: d.ip ?? undefined,
|
||||
decky_config: d.decky_config ?? undefined,
|
||||
};
|
||||
});
|
||||
|
||||
const byLan = new Map<string, string[]>();
|
||||
for (const e of detail.edges) {
|
||||
|
||||
Reference in New Issue
Block a user