diff --git a/README.md b/README.md
index 12a36fd..8e220b9 100644
--- a/README.md
+++ b/README.md
@@ -24,10 +24,16 @@ both fabrics for a documented reason (spare/inventory, decommissioning, or
planned/not-yet-installed) -- those are called out in their own box on the
diagram rather than silently dropped.
-Every connector is labeled with the port at each end (e.g. `9 ↔ 1`), and
-switches/patch panels look the part: switches get draw.io's built-in network
-switch icon, patch panels get a small rack-unit look (header strip + port
-ticks) rather than a plain box.
+Every connector is labeled with the port at each end (e.g. `9 ↔ 1`).
+Switches and patch panels use draw.io's built-in **Rack** shape library
+(`mxgraph.rack.general.switches_1` / `...cat5e_rack_mount_patch_panel_24_ports`)
+-- real 19"-rack front-panel silhouettes, not the old abstract pictogram /
+hand-drawn box. There's no stencil library for FS.com, AFL, Primex,
+Clearview, or FIS specifically (those are niche fiber/telecom brands, not
+in any icon set draw.io ships), so brand accuracy is carried as text
+instead of guessed at in the art: every node's caption is its hostname
+*and* its real NetBox `device_type.model` (e.g. `IES3110-8TFP-R`, `xPAK
+6-Port SC/APC`), not just the hostname.
A **VLAN legend + table** sits at the top of the diagram: a colored
"VLAN <id> / name" block per VLAN on the left (each color also used for
diff --git a/topology/Field-Switch-Topology.drawio b/topology/Field-Switch-Topology.drawio
index b923259..7918138 100644
--- a/topology/Field-Switch-Topology.drawio
+++ b/topology/Field-Switch-Topology.drawio
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/topology/Field-Switch-Topology.png b/topology/Field-Switch-Topology.png
index 74f3323..46743dc 100644
Binary files a/topology/Field-Switch-Topology.png and b/topology/Field-Switch-Topology.png differ
diff --git a/topology/generate_drawio.py b/topology/generate_drawio.py
index f783521..cf8a6f9 100644
--- a/topology/generate_drawio.py
+++ b/topology/generate_drawio.py
@@ -5,8 +5,7 @@ Render topo_full.json (produced by export_topology.py) as a draw.io
Edges reference shapes by id (source=/target=), which is how draw.io's
native glue works -- no hand-rolled formulas needed: dragging a shape in
-the app keeps every attached line attached. Patch panel port ticks/accent
-strips are parented to the panel's body cell, so they move with it too.
+the app keeps every attached line attached.
Usage:
python3 generate_drawio.py [topo_full.json] [Field-Switch-Topology.drawio]
@@ -104,22 +103,26 @@ depth_main, yslot_main = layout_tree(1, main_comp)
depth_dmo, yslot_dmo = layout_tree(7, dmo_comp)
# ---------------- geometry constants (px) ----------------
-SWITCH_ICON = 46 # icon box; label renders below it
-SWITCH_LABEL_H = 30
-PANEL_W, PANEL_H = 132, 42
-COL_W = 200
-ROW_H = 86
+# Realistic rack-equipment silhouettes (draw.io's built-in Rack shape
+# library) instead of the old abstract pictogram / hand-drawn box -- both
+# switches and patch panels are real 19" rack-mount gear, so both get the
+# same width and a below-icon caption; only the icon height (and which
+# stencil) differs, matching each one's actual flat 1U proportions.
+NODE_W = 108
+SWITCH_H = 20 # ~108:20, close to the stencil's native ~5.3:1
+PANEL_H = 12 # ~108:12, close to the stencil's native ~10.1:1
+CAPTION_H = 46 # hostname + model, wrapped, below the icon
+COL_W = 320 # NODE_W + clearance for the connector's own port/VLAN label text
+ROW_H = 78
PAD = 40
LABEL_STRIP = 34
SECTION_GAP = 70
MARGIN = 40
-def footprint(dev):
- if dev["role"] == "patch-panel":
- return PANEL_W, PANEL_H
- return SWITCH_ICON, SWITCH_ICON + SWITCH_LABEL_H
+def icon_h(dev):
+ return PANEL_H if dev["role"] == "patch-panel" else SWITCH_H
-pos = {} # device_id -> (x, y) TOP-LEFT of the node's own bounding box (not center)
+pos = {} # device_id -> (x, y, w, h) of the node's own ICON (caption sits below it)
def place_section(depth_map, yslot_map, y_offset):
ys = list(yslot_map.values())
@@ -127,12 +130,12 @@ def place_section(depth_map, yslot_map, y_offset):
max_x = max(depth_map.values())
for node, d in depth_map.items():
y = yslot_map[node]
- w, h = footprint(devs[node])
- cx = MARGIN + PAD + d * COL_W + SWITCH_ICON / 2
- cy = y_offset + PAD + LABEL_STRIP + (y - min_y) * ROW_H + SWITCH_ICON / 2
- pos[node] = (cx - w / 2, cy - h / 2, w, h)
- width = MARGIN * 2 + PAD * 2 + max_x * COL_W + SWITCH_ICON
- height = PAD * 2 + LABEL_STRIP + (max(ys) - min_y) * ROW_H + SWITCH_ICON + SWITCH_LABEL_H
+ h = icon_h(devs[node])
+ cx = MARGIN + PAD + d * COL_W + NODE_W / 2
+ top = y_offset + PAD + LABEL_STRIP + (y - min_y) * ROW_H
+ pos[node] = (cx - NODE_W / 2, top, NODE_W, h)
+ width = MARGIN * 2 + PAD * 2 + max_x * COL_W + NODE_W
+ height = PAD * 2 + LABEL_STRIP + (max(ys) - min_y) * ROW_H + SWITCH_H + CAPTION_H
return width, height
TITLE_H = 60
@@ -167,13 +170,13 @@ iso_top = cursor_y
iso_sorted = sorted(iso, key=lambda d: devs[d]["name"])
for i, node in enumerate(iso_sorted):
r, c = divmod(i, ISO_COLS)
- w, h = footprint(devs[node])
- cx = MARGIN + PAD + c * COL_W + SWITCH_ICON / 2
- cy = iso_top + PAD + LABEL_STRIP + r * ROW_H + SWITCH_ICON / 2
- pos[node] = (cx - w / 2, cy - h / 2, w, h)
+ h = icon_h(devs[node])
+ cx = MARGIN + PAD + c * COL_W + NODE_W / 2
+ top = iso_top + PAD + LABEL_STRIP + r * ROW_H
+ pos[node] = (cx - NODE_W / 2, top, NODE_W, h)
iso_rows = (len(iso_sorted) + ISO_COLS - 1) // ISO_COLS
-w_iso = MARGIN * 2 + PAD * 2 + (ISO_COLS - 1) * COL_W + SWITCH_ICON
-h_iso = PAD * 2 + LABEL_STRIP + (iso_rows - 1) * ROW_H + SWITCH_ICON + SWITCH_LABEL_H
+w_iso = MARGIN * 2 + PAD * 2 + (ISO_COLS - 1) * COL_W + NODE_W
+h_iso = PAD * 2 + LABEL_STRIP + (iso_rows - 1) * ROW_H + SWITCH_H + CAPTION_H
PAGE_W = max(w_main, w_dmo, w_iso, VLAN_LEGEND_W + 30 + VLAN_TABLE_W + MARGIN * 2)
PAGE_H = cursor_y + h_iso + MARGIN
@@ -247,29 +250,23 @@ add_cell(
)
# ---------------- node shapes ----------------
-# Switches: a real network-switch icon, hostname caption below it.
-# Patch panels: understated -- pale body, a thin hairline where a rack unit's
-# seam would be, and a row of faint port outlines. Reads as "passive
-# hardware, not the point of this diagram" rather than competing with the
-# switches for attention. Built from cells parented to the panel's own body
-# cell, so the whole unit moves together when dragged.
-def panel_shape(did, x, y, w, h, name, st):
- body_id = f"n{did}"
- vertex(body_id, name, x, y, w, h,
- f'rounded=1;arcSize=6;whiteSpace=wrap;html=1;fillColor={st["fill"]};strokeColor={st["stroke"]};'
- f'fontColor={st["font"]};fontSize=9;verticalAlign=bottom;spacingBottom=5;')
- # hairline seam, relative to the body
- vertex(f"{body_id}-strip", "", 5, 6, w - 10, 1,
- f'rounded=0;fillColor={st["stroke"]};strokeColor=none;', parent=body_id)
- # faint port outlines along the top-middle, evenly spaced
- n_ticks = 6
- tick_w, tick_h = 9, 6
- usable = w - 16
- gap = usable / n_ticks
- for k in range(n_ticks):
- tx = 8 + gap * k + (gap - tick_w) / 2
- vertex(f"{body_id}-t{k}", "", tx, 14, tick_w, tick_h,
- f'rounded=0;fillColor=none;strokeColor={st["stroke"]};strokeWidth=0.75;', parent=body_id)
+# Realistic 19" rack-equipment silhouettes from draw.io's built-in Rack
+# shape library (verified against the actual stencil source, not guessed):
+# a real network-switch front panel for switches, a real 24-port patch
+# panel front panel for patch panels. Neither is FS.com/AFL/Primex/etc.
+# branded art -- no such stencils exist anywhere, this vendor mix is far
+# too niche -- so "by model" is carried as text: every node's caption is
+# its hostname plus its actual NetBox device_type.model, wrapped below the
+# icon, instead of pretending the icon itself is model-specific.
+SWITCH_STENCIL = "mxgraph.rack.general.switches_1"
+PANEL_STENCIL = "mxgraph.rack.general.cat5e_rack_mount_patch_panel_24_ports"
+
+def caption(name, model):
+ # single-quote the inner HTML attribute -- this whole string is itself
+ # going into an XML value="..." attribute (vertex() escapes it), and a
+ # literal " from a double-quoted style="..." here would prematurely
+ # close that outer XML attribute
+ return f"{escape(name)}
{escape(model)}"
for did, dev in devs.items():
if did not in pos:
@@ -277,15 +274,14 @@ for did, dev in devs.items():
x, y, w, h = pos[did]
st = style_for(dev)
dashed = "dashed=1;dashPattern=4 3;" if st["dashed"] else ""
- if dev["role"] == "patch-panel":
- panel_shape(did, x, y, w, h, dev["name"], st)
- else:
- style = (
- 'shape=mxgraph.networks.switch;html=1;verticalLabelPosition=bottom;verticalAlign=top;'
- f'labelBackgroundColor=none;fontSize=10;fontColor={st["font"] if st["font"] != "#FFFFFF" else "#1A2233"};'
- f'fillColor={st["fill"]};strokeColor={st["stroke"]};fontStyle={st["bold"]};{dashed}'
- )
- vertex(f"n{did}", dev["name"], x, y, SWITCH_ICON, SWITCH_ICON, style)
+ stencil = PANEL_STENCIL if dev["role"] == "patch-panel" else SWITCH_STENCIL
+ font = st["font"] if st["font"] != "#FFFFFF" else "#1A2233"
+ style = (
+ f'shape={stencil};html=1;whiteSpace=wrap;verticalLabelPosition=bottom;verticalAlign=top;'
+ f'labelBackgroundColor=none;fontSize=11;fontColor={font};'
+ f'fillColor={st["fill"]};strokeColor={st["stroke"]};fontStyle={st["bold"]};{dashed}'
+ )
+ vertex(f"n{did}", caption(dev["name"], dev.get("device_type", "")), x, y, w, h, style)
# ---------------- connectors, labeled with the port at each end ----------------
for idx, e in enumerate(edges):