Realistic rack-equipment icons for switches and patch panels
Swapped the abstract mxgraph.networks.switch pictogram and the hand-drawn patch-panel box for draw.io's built-in Rack shape library -- real 19" front-panel silhouettes (verified against the actual stencil source on GitHub, not guessed): mxgraph.rack.general.switches_1 for switches, ...cat5e_rack_mount_patch_panel_24_ports for patch panels. No stencil library covers FS.com/AFL/Primex/Clearview/FIS (all niche fiber/telecom brands draw.io doesn't ship icons for), so per user's choice brand/model accuracy is carried as text instead: every node's caption is now 'hostname' + the real NetBox device_type.model on a second line, e.g. 'ls-bsls-sw01 / IES3110-8TFP-R'. Patch panels also lost their hand-built 7-cell composition (body + strip + 6 port-tick cells) now that a real stencil exists -- one cell each, cell count 530 -> 271. Fixed a spacing regression this surfaced: the new realistic icons are much shorter/flatter than the old ones, and COL_W (210) left only ~100px between adjacent columns -- not enough room for a ~150-180px connector label, so port/VLAN tags were overlapping neighboring icons. COL_W -> 320. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NM3nivbufxEWiKKmatEtEe
This commit is contained in:
+52
-56
@@ -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)}<br><font style='font-size:9px'>{escape(model)}</font>"
|
||||
|
||||
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):
|
||||
|
||||
Reference in New Issue
Block a user