Add port labels, modernize patch panels, add a real PNG preview
- export_topology.py: edges now carry the exact port name on both ends (pulled directly from Cable/CableTermination, not inferred from the trace chain), so the diagram can label each connector. - generate_drawio.py: patch panels are now a small rack-unit shape (header strip + port ticks, parented to the panel body so they move together) instead of a plain box; connectors show 'porta <-> portb' and no longer carry a directional arrowhead (these are physical cables, not directed links); switches keep the network-switch icon. - render_png.js + render_host.html: headless Puppeteer driver for embed.diagrams.net's postMessage export protocol -- an actual render of the .drawio file, not an approximation. Field-Switch- Topology.png is the committed output, for anyone who wants a look without opening draw.io. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NM3nivbufxEWiKKmatEtEe
This commit is contained in:
+47
-26
@@ -5,7 +5,8 @@ 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.
|
||||
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.
|
||||
|
||||
Usage:
|
||||
python3 generate_drawio.py [topo_full.json] [Field-Switch-Topology.drawio]
|
||||
@@ -19,13 +20,13 @@ OUT_PATH = sys.argv[2] if len(sys.argv) > 2 else "Field-Switch-Topology.drawio"
|
||||
|
||||
data = json.load(open(IN_PATH))
|
||||
devs = {d["id"]: d for d in data["devices"]}
|
||||
edges = data["edges"]
|
||||
edges = data["edges"] # [{a, a_port, b, b_port}, ...]
|
||||
|
||||
# ---------------- components ----------------
|
||||
adj = {d: [] for d in devs}
|
||||
for a, b in edges:
|
||||
adj[a].append(b)
|
||||
adj[b].append(a)
|
||||
for e in edges:
|
||||
adj[e["a"]].append(e["b"])
|
||||
adj[e["b"]].append(e["a"])
|
||||
|
||||
parent = {d: d for d in devs}
|
||||
def find(x):
|
||||
@@ -37,8 +38,8 @@ def union(a, b):
|
||||
ra, rb = find(a), find(b)
|
||||
if ra != rb:
|
||||
parent[ra] = rb
|
||||
for a, b in edges:
|
||||
union(a, b)
|
||||
for e in edges:
|
||||
union(e["a"], e["b"])
|
||||
from collections import defaultdict
|
||||
comps = defaultdict(list)
|
||||
for d in devs:
|
||||
@@ -74,9 +75,9 @@ 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 = 130, 34
|
||||
COL_W = 190
|
||||
ROW_H = 82
|
||||
PANEL_W, PANEL_H = 132, 42
|
||||
COL_W = 200
|
||||
ROW_H = 86
|
||||
PAD = 40
|
||||
LABEL_STRIP = 34
|
||||
SECTION_GAP = 70
|
||||
@@ -137,7 +138,7 @@ STYLE = {
|
||||
"spare": dict(fill="#F1F1F2", stroke="#9AA0AA", font="#5B6478", dashed=1, bold=0),
|
||||
"decom": dict(fill="#FBE6E2", stroke="#C04B34", font="#8A3423", dashed=1, bold=0),
|
||||
"planned": dict(fill="#FDF1CF", stroke="#B8862C", font="#7A5C00", dashed=1, bold=0),
|
||||
"panel": dict(fill="#EEF0F2", stroke="#8C97A6", font="#495566", dashed=0, bold=0),
|
||||
"panel": dict(fill="#3A4457", stroke="#242C38", font="#FFFFFF", dashed=0, bold=0),
|
||||
}
|
||||
|
||||
def style_for(dev):
|
||||
@@ -166,9 +167,9 @@ def vertex(cell_id, value, x, y, w, h, style, parent="1"):
|
||||
f'</mxCell>'
|
||||
)
|
||||
|
||||
def edge(cell_id, source, target, style):
|
||||
def edge(cell_id, source, target, style, value=""):
|
||||
add_cell(
|
||||
f'<mxCell id="{cell_id}" style="{style}" edge="1" parent="1" '
|
||||
f'<mxCell id="{cell_id}" value="{escape(value)}" style="{style}" edge="1" parent="1" '
|
||||
f'source="{source}" target="{target}"><mxGeometry relative="1" as="geometry"/></mxCell>'
|
||||
)
|
||||
|
||||
@@ -198,10 +199,29 @@ add_cell(
|
||||
f'<mxGeometry x="{MARGIN}" y="10" width="600" height="30" as="geometry"/></mxCell>'
|
||||
)
|
||||
|
||||
# node shapes -- switches get a real network-switch icon (label rendered below it by
|
||||
# draw.io's own verticalLabelPosition, not baked into the geometry), patch panels get
|
||||
# a plain labeled box.
|
||||
node_style = {}
|
||||
# ---------------- node shapes ----------------
|
||||
# Switches: a real network-switch icon, hostname caption below it.
|
||||
# Patch panels: a small rack-style unit -- dark body, a lighter header strip,
|
||||
# and a row of port ticks along the bottom -- 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=10;whiteSpace=wrap;html=1;fillColor={st["fill"]};strokeColor={st["stroke"]};'
|
||||
f'fontColor={st["font"]};fontSize=9;verticalAlign=bottom;spacingBottom=5;')
|
||||
# header strip (lighter steel), relative to the body
|
||||
vertex(f"{body_id}-strip", "", 5, 4, w - 10, 5,
|
||||
'rounded=1;arcSize=40;fillColor=#8C97A6;strokeColor=none;', parent=body_id)
|
||||
# port ticks along the top-middle, evenly spaced
|
||||
n_ticks = 6
|
||||
tick_w, tick_h = 10, 7
|
||||
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, 15, tick_w, tick_h,
|
||||
'rounded=0;fillColor=#2A313D;strokeColor=#5B6577;strokeWidth=0.75;', parent=body_id)
|
||||
|
||||
for did, dev in devs.items():
|
||||
if did not in pos:
|
||||
continue
|
||||
@@ -209,11 +229,7 @@ for did, dev in devs.items():
|
||||
st = style_for(dev)
|
||||
dashed = "dashed=1;dashPattern=4 3;" if st["dashed"] else ""
|
||||
if dev["role"] == "patch-panel":
|
||||
style = (
|
||||
f'rounded=0;whiteSpace=wrap;html=1;fillColor={st["fill"]};strokeColor={st["stroke"]};'
|
||||
f'fontColor={st["font"]};fontSize=9;{dashed}'
|
||||
)
|
||||
vertex(f"n{did}", dev["name"], x, y, w, h, style)
|
||||
panel_shape(did, x, y, w, h, dev["name"], st)
|
||||
else:
|
||||
style = (
|
||||
'shape=mxgraph.networks.switch;html=1;verticalLabelPosition=bottom;verticalAlign=top;'
|
||||
@@ -222,18 +238,23 @@ for did, dev in devs.items():
|
||||
)
|
||||
vertex(f"n{did}", dev["name"], x, y, SWITCH_ICON, SWITCH_ICON, style)
|
||||
|
||||
# connectors -- glued natively via source/target, orthogonal routing
|
||||
for idx, (a, b) in enumerate(edges):
|
||||
# ---------------- connectors, labeled with the port at each end ----------------
|
||||
for idx, e in enumerate(edges):
|
||||
a, b = e["a"], e["b"]
|
||||
if a in pos and b in pos:
|
||||
label = f'{e["a_port"]} ↔ {e["b_port"]}'
|
||||
edge(f"e{idx}", f"n{a}", f"n{b}",
|
||||
"edgeStyle=orthogonalEdgeStyle;rounded=1;html=1;strokeColor=#9AA6B8;strokeWidth=1.2;"
|
||||
"jettySize=auto;orthogonalLoop=1;")
|
||||
"startArrow=none;endArrow=none;"
|
||||
"jettySize=auto;orthogonalLoop=1;fontSize=7.5;fontColor=#7C8AA6;"
|
||||
"labelBackgroundColor=#F7F8FBcc;align=center;verticalAlign=middle;",
|
||||
value=label)
|
||||
|
||||
# legend
|
||||
legend_items = [
|
||||
("#2F5FAD", "#173D6E", "Distribution switch"),
|
||||
("#F3F5FA", "#7C8AA6", "Field switch"),
|
||||
("#EEF0F2", "#8C97A6", "Patch panel / splice module"),
|
||||
("#3A4457", "#242C38", "Patch panel / splice module"),
|
||||
("#F1F1F2", "#9AA0AA", "Spare / decommissioned / planned"),
|
||||
]
|
||||
lx = MARGIN
|
||||
|
||||
Reference in New Issue
Block a user