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):