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:
@@ -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
|
planned/not-yet-installed) -- those are called out in their own box on the
|
||||||
diagram rather than silently dropped.
|
diagram rather than silently dropped.
|
||||||
|
|
||||||
Every connector is labeled with the port at each end (e.g. `9 ↔ 1`), and
|
Every connector is labeled with the port at each end (e.g. `9 ↔ 1`).
|
||||||
switches/patch panels look the part: switches get draw.io's built-in network
|
Switches and patch panels use draw.io's built-in **Rack** shape library
|
||||||
switch icon, patch panels get a small rack-unit look (header strip + port
|
(`mxgraph.rack.general.switches_1` / `...cat5e_rack_mount_patch_panel_24_ports`)
|
||||||
ticks) rather than a plain box.
|
-- 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
|
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
|
"VLAN <id> / name" block per VLAN on the left (each color also used for
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
Binary file not shown.
|
Before Width: | Height: | Size: 1.8 MiB After Width: | Height: | Size: 2.2 MiB |
+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
|
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
|
native glue works -- no hand-rolled formulas needed: dragging a shape in
|
||||||
the app keeps every attached line attached. Patch panel port ticks/accent
|
the app keeps every attached line attached.
|
||||||
strips are parented to the panel's body cell, so they move with it too.
|
|
||||||
|
|
||||||
Usage:
|
Usage:
|
||||||
python3 generate_drawio.py [topo_full.json] [Field-Switch-Topology.drawio]
|
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)
|
depth_dmo, yslot_dmo = layout_tree(7, dmo_comp)
|
||||||
|
|
||||||
# ---------------- geometry constants (px) ----------------
|
# ---------------- geometry constants (px) ----------------
|
||||||
SWITCH_ICON = 46 # icon box; label renders below it
|
# Realistic rack-equipment silhouettes (draw.io's built-in Rack shape
|
||||||
SWITCH_LABEL_H = 30
|
# library) instead of the old abstract pictogram / hand-drawn box -- both
|
||||||
PANEL_W, PANEL_H = 132, 42
|
# switches and patch panels are real 19" rack-mount gear, so both get the
|
||||||
COL_W = 200
|
# same width and a below-icon caption; only the icon height (and which
|
||||||
ROW_H = 86
|
# 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
|
PAD = 40
|
||||||
LABEL_STRIP = 34
|
LABEL_STRIP = 34
|
||||||
SECTION_GAP = 70
|
SECTION_GAP = 70
|
||||||
MARGIN = 40
|
MARGIN = 40
|
||||||
|
|
||||||
def footprint(dev):
|
def icon_h(dev):
|
||||||
if dev["role"] == "patch-panel":
|
return PANEL_H if dev["role"] == "patch-panel" else SWITCH_H
|
||||||
return PANEL_W, PANEL_H
|
|
||||||
return SWITCH_ICON, SWITCH_ICON + SWITCH_LABEL_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):
|
def place_section(depth_map, yslot_map, y_offset):
|
||||||
ys = list(yslot_map.values())
|
ys = list(yslot_map.values())
|
||||||
@@ -127,12 +130,12 @@ def place_section(depth_map, yslot_map, y_offset):
|
|||||||
max_x = max(depth_map.values())
|
max_x = max(depth_map.values())
|
||||||
for node, d in depth_map.items():
|
for node, d in depth_map.items():
|
||||||
y = yslot_map[node]
|
y = yslot_map[node]
|
||||||
w, h = footprint(devs[node])
|
h = icon_h(devs[node])
|
||||||
cx = MARGIN + PAD + d * COL_W + SWITCH_ICON / 2
|
cx = MARGIN + PAD + d * COL_W + NODE_W / 2
|
||||||
cy = y_offset + PAD + LABEL_STRIP + (y - min_y) * ROW_H + SWITCH_ICON / 2
|
top = y_offset + PAD + LABEL_STRIP + (y - min_y) * ROW_H
|
||||||
pos[node] = (cx - w / 2, cy - h / 2, w, h)
|
pos[node] = (cx - NODE_W / 2, top, NODE_W, h)
|
||||||
width = MARGIN * 2 + PAD * 2 + max_x * COL_W + SWITCH_ICON
|
width = MARGIN * 2 + PAD * 2 + max_x * COL_W + NODE_W
|
||||||
height = PAD * 2 + LABEL_STRIP + (max(ys) - min_y) * ROW_H + SWITCH_ICON + SWITCH_LABEL_H
|
height = PAD * 2 + LABEL_STRIP + (max(ys) - min_y) * ROW_H + SWITCH_H + CAPTION_H
|
||||||
return width, height
|
return width, height
|
||||||
|
|
||||||
TITLE_H = 60
|
TITLE_H = 60
|
||||||
@@ -167,13 +170,13 @@ iso_top = cursor_y
|
|||||||
iso_sorted = sorted(iso, key=lambda d: devs[d]["name"])
|
iso_sorted = sorted(iso, key=lambda d: devs[d]["name"])
|
||||||
for i, node in enumerate(iso_sorted):
|
for i, node in enumerate(iso_sorted):
|
||||||
r, c = divmod(i, ISO_COLS)
|
r, c = divmod(i, ISO_COLS)
|
||||||
w, h = footprint(devs[node])
|
h = icon_h(devs[node])
|
||||||
cx = MARGIN + PAD + c * COL_W + SWITCH_ICON / 2
|
cx = MARGIN + PAD + c * COL_W + NODE_W / 2
|
||||||
cy = iso_top + PAD + LABEL_STRIP + r * ROW_H + SWITCH_ICON / 2
|
top = iso_top + PAD + LABEL_STRIP + r * ROW_H
|
||||||
pos[node] = (cx - w / 2, cy - h / 2, w, h)
|
pos[node] = (cx - NODE_W / 2, top, NODE_W, h)
|
||||||
iso_rows = (len(iso_sorted) + ISO_COLS - 1) // ISO_COLS
|
iso_rows = (len(iso_sorted) + ISO_COLS - 1) // ISO_COLS
|
||||||
w_iso = MARGIN * 2 + PAD * 2 + (ISO_COLS - 1) * COL_W + SWITCH_ICON
|
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_ICON + SWITCH_LABEL_H
|
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_W = max(w_main, w_dmo, w_iso, VLAN_LEGEND_W + 30 + VLAN_TABLE_W + MARGIN * 2)
|
||||||
PAGE_H = cursor_y + h_iso + MARGIN
|
PAGE_H = cursor_y + h_iso + MARGIN
|
||||||
@@ -247,29 +250,23 @@ add_cell(
|
|||||||
)
|
)
|
||||||
|
|
||||||
# ---------------- node shapes ----------------
|
# ---------------- node shapes ----------------
|
||||||
# Switches: a real network-switch icon, hostname caption below it.
|
# Realistic 19" rack-equipment silhouettes from draw.io's built-in Rack
|
||||||
# Patch panels: understated -- pale body, a thin hairline where a rack unit's
|
# shape library (verified against the actual stencil source, not guessed):
|
||||||
# seam would be, and a row of faint port outlines. Reads as "passive
|
# a real network-switch front panel for switches, a real 24-port patch
|
||||||
# hardware, not the point of this diagram" rather than competing with the
|
# panel front panel for patch panels. Neither is FS.com/AFL/Primex/etc.
|
||||||
# switches for attention. Built from cells parented to the panel's own body
|
# branded art -- no such stencils exist anywhere, this vendor mix is far
|
||||||
# cell, so the whole unit moves together when dragged.
|
# too niche -- so "by model" is carried as text: every node's caption is
|
||||||
def panel_shape(did, x, y, w, h, name, st):
|
# its hostname plus its actual NetBox device_type.model, wrapped below the
|
||||||
body_id = f"n{did}"
|
# icon, instead of pretending the icon itself is model-specific.
|
||||||
vertex(body_id, name, x, y, w, h,
|
SWITCH_STENCIL = "mxgraph.rack.general.switches_1"
|
||||||
f'rounded=1;arcSize=6;whiteSpace=wrap;html=1;fillColor={st["fill"]};strokeColor={st["stroke"]};'
|
PANEL_STENCIL = "mxgraph.rack.general.cat5e_rack_mount_patch_panel_24_ports"
|
||||||
f'fontColor={st["font"]};fontSize=9;verticalAlign=bottom;spacingBottom=5;')
|
|
||||||
# hairline seam, relative to the body
|
def caption(name, model):
|
||||||
vertex(f"{body_id}-strip", "", 5, 6, w - 10, 1,
|
# single-quote the inner HTML attribute -- this whole string is itself
|
||||||
f'rounded=0;fillColor={st["stroke"]};strokeColor=none;', parent=body_id)
|
# going into an XML value="..." attribute (vertex() escapes it), and a
|
||||||
# faint port outlines along the top-middle, evenly spaced
|
# literal " from a double-quoted style="..." here would prematurely
|
||||||
n_ticks = 6
|
# close that outer XML attribute
|
||||||
tick_w, tick_h = 9, 6
|
return f"{escape(name)}<br><font style='font-size:9px'>{escape(model)}</font>"
|
||||||
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)
|
|
||||||
|
|
||||||
for did, dev in devs.items():
|
for did, dev in devs.items():
|
||||||
if did not in pos:
|
if did not in pos:
|
||||||
@@ -277,15 +274,14 @@ for did, dev in devs.items():
|
|||||||
x, y, w, h = pos[did]
|
x, y, w, h = pos[did]
|
||||||
st = style_for(dev)
|
st = style_for(dev)
|
||||||
dashed = "dashed=1;dashPattern=4 3;" if st["dashed"] else ""
|
dashed = "dashed=1;dashPattern=4 3;" if st["dashed"] else ""
|
||||||
if dev["role"] == "patch-panel":
|
stencil = PANEL_STENCIL if dev["role"] == "patch-panel" else SWITCH_STENCIL
|
||||||
panel_shape(did, x, y, w, h, dev["name"], st)
|
font = st["font"] if st["font"] != "#FFFFFF" else "#1A2233"
|
||||||
else:
|
style = (
|
||||||
style = (
|
f'shape={stencil};html=1;whiteSpace=wrap;verticalLabelPosition=bottom;verticalAlign=top;'
|
||||||
'shape=mxgraph.networks.switch;html=1;verticalLabelPosition=bottom;verticalAlign=top;'
|
f'labelBackgroundColor=none;fontSize=11;fontColor={font};'
|
||||||
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}'
|
||||||
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)
|
||||||
vertex(f"n{did}", dev["name"], x, y, SWITCH_ICON, SWITCH_ICON, style)
|
|
||||||
|
|
||||||
# ---------------- connectors, labeled with the port at each end ----------------
|
# ---------------- connectors, labeled with the port at each end ----------------
|
||||||
for idx, e in enumerate(edges):
|
for idx, e in enumerate(edges):
|
||||||
|
|||||||
Reference in New Issue
Block a user