|
@@ -335,6 +335,10 @@
|
|
|
#topo .flow.c-200g { stroke: var(--accent); stroke-dasharray: 4 10; animation: flow 1.1s linear infinite; }
|
|
#topo .flow.c-200g { stroke: var(--accent); stroke-dasharray: 4 10; animation: flow 1.1s linear infinite; }
|
|
|
#topo .flow.c-dac { stroke: #a371f7; stroke-dasharray: 4 10; animation: flow 1.1s linear infinite; }
|
|
#topo .flow.c-dac { stroke: #a371f7; stroke-dasharray: 4 10; animation: flow 1.1s linear infinite; }
|
|
|
#topo .flow.c-10g { stroke: color-mix(in srgb, var(--fg) 50%, transparent); stroke-width: 1.5; stroke-dasharray: 3 9; animation: flow 1.6s linear infinite; }
|
|
#topo .flow.c-10g { stroke: color-mix(in srgb, var(--fg) 50%, transparent); stroke-width: 1.5; stroke-dasharray: 3 9; animation: flow 1.6s linear infinite; }
|
|
|
|
|
+ /* bus-mode crossings: the 10g run hops over the 200g lane */
|
|
|
|
|
+ #topo .bridge { fill: none; stroke: color-mix(in srgb, var(--fg) 70%, transparent); stroke-width: 1.5; }
|
|
|
|
|
+ #topo .cablemode { cursor: pointer; fill: var(--muted); font-size: 11px; }
|
|
|
|
|
+ #topo .cablemode:hover { fill: var(--fg); text-decoration: underline; }
|
|
|
@keyframes flow { to { stroke-dashoffset: -14; } }
|
|
@keyframes flow { to { stroke-dashoffset: -14; } }
|
|
|
#topo .port { fill: var(--led-free); }
|
|
#topo .port { fill: var(--led-free); }
|
|
|
#topo .port.on { fill: var(--led-on); }
|
|
#topo .port.on { fill: var(--led-on); }
|
|
@@ -1047,9 +1051,11 @@ function tgCablePath(l, t) {
|
|
|
const ex = b.dcx - 40, ey = b.y + b.h;
|
|
const ex = b.dcx - 40, ey = b.y + b.h;
|
|
|
return `M ${ex} ${ey} C ${ex} ${ey + 76} ${px} ${ey + 66} ${px} 290`;
|
|
return `M ${ex} ${ey} C ${ex} ${ey + 76} ${px} ${ey + 66} ${px} 290`;
|
|
|
}
|
|
}
|
|
|
- // row1: device top → fabric switch bottom
|
|
|
|
|
|
|
+ // row1: device top → fabric switch bottom — an S-curve inside the 40px
|
|
|
|
|
+ // corridor (switch bottom 346 .. device top 386) so the last approach
|
|
|
|
|
+ // to the port is vertical, not a slide in from the side
|
|
|
const ex = b.dcx - 40, ey = b.y;
|
|
const ex = b.dcx - 40, ey = b.y;
|
|
|
- return `M ${ex} ${ey} C ${ex} ${ey - 76} ${px} ${ey - 66} ${px} 346`;
|
|
|
|
|
|
|
+ return `M ${ex} ${ey} C ${ex} 363 ${px} 363 ${px} 346`;
|
|
|
}
|
|
}
|
|
|
// dac: arc between adjacent device tops
|
|
// dac: arc between adjacent device tops
|
|
|
const ai = tgIdx(t, l.a);
|
|
const ai = tgIdx(t, l.a);
|
|
@@ -1208,15 +1214,69 @@ function topoFrames(snap) {
|
|
|
return { frames, chips };
|
|
return { frames, chips };
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-function cablesSvg(t) {
|
|
|
|
|
|
|
+function cablesSvg(t, bus) {
|
|
|
let out = "";
|
|
let out = "";
|
|
|
for (const l of t.links) {
|
|
for (const l of t.links) {
|
|
|
- const p = tgCablePath(l, t);
|
|
|
|
|
|
|
+ const p = bus ? tgCablePathBus(l, t) : tgCablePath(l, t);
|
|
|
if (!p) continue;
|
|
if (!p) continue;
|
|
|
const key = `${l.kind}|${l.a}|${l.b}`;
|
|
const key = `${l.kind}|${l.a}|${l.b}`;
|
|
|
out += `<path class="cable c-${l.kind}" d="${p}"/>`;
|
|
out += `<path class="cable c-${l.kind}" d="${p}"/>`;
|
|
|
out += `<path class="cable c-${l.kind} flow" data-flow="${esc(key)}" d="${p}"/>`;
|
|
out += `<path class="cable c-${l.kind} flow" data-flow="${esc(key)}" d="${p}"/>`;
|
|
|
}
|
|
}
|
|
|
|
|
+ if (bus) out += bridgesSvg(t);
|
|
|
|
|
+ return out;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// Bus mode: row-1 (bottom) cables route as right-angle runs along two
|
|
|
|
|
+// shared lanes — 200g at y=366 (in the 40px corridor under the fabric
|
|
|
|
|
+// switch), 10g at y=278 (the 24px gap between the two switches). A 10g
|
|
|
|
|
+// cable whose vertical would cut the fabric switch detours around its
|
|
|
|
|
+// nearer side. Where a 10g run crosses the 200g lane it hops over it on
|
|
|
|
|
+// a bridge arc, so crossings read as intentional.
|
|
|
|
|
+function tgCablePathBus(l, t) {
|
|
|
|
|
+ const bi = tgIdx(t, l.b);
|
|
|
|
|
+ if (bi < 0 || bi < 4) return tgCablePath(l, t); // row 0 and DAC unchanged
|
|
|
|
|
+ const b = tgPos(bi);
|
|
|
|
|
+ if (l.kind === "200g") {
|
|
|
|
|
+ const ex = b.dcx - 40;
|
|
|
|
|
+ const fi = t.links.filter((x) => x.kind === "200g").findIndex((x) => x.b === l.b);
|
|
|
|
|
+ const px = 396 + (fi % 4) * 80;
|
|
|
|
|
+ return `M ${ex} 386 L ${ex} 366 L ${px} 366 L ${px} 346`;
|
|
|
|
|
+ }
|
|
|
|
|
+ // 10g: up the device side, across the inter-switch gap, down to the port
|
|
|
|
|
+ const px = 396 + bi * 38;
|
|
|
|
|
+ const x = b.dcx;
|
|
|
|
|
+ if (x < 370 || x > 690) return `M ${x} 386 L ${x} 278 L ${px} 278 L ${px} 266`;
|
|
|
|
|
+ const side = x < 530 ? 350 : 710; // nearest clearance past the fabric box
|
|
|
|
|
+ return `M ${x} 386 L ${x} 366 L ${side} 366 L ${side} 278 L ${px} 278 L ${px} 266`;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// Small semicircle hops where a 10g row-1 vertical crosses the 200g lane.
|
|
|
|
|
+function bridgesSvg(t) {
|
|
|
|
|
+ const lane = t.links
|
|
|
|
|
+ .filter((l) => l.kind === "200g")
|
|
|
|
|
+ .map((l) => {
|
|
|
|
|
+ const bi = tgIdx(t, l.b);
|
|
|
|
|
+ if (bi < 4) return null;
|
|
|
|
|
+ const b = tgPos(bi);
|
|
|
|
|
+ const ex = b.dcx - 40;
|
|
|
|
|
+ const fi = t.links.filter((x) => x.kind === "200g").findIndex((x) => x.b === l.b);
|
|
|
|
|
+ const px = 396 + (fi % 4) * 80;
|
|
|
|
|
+ return { y: 366, x1: Math.min(ex, px), x2: Math.max(ex, px) };
|
|
|
|
|
+ })
|
|
|
|
|
+ .filter(Boolean);
|
|
|
|
|
+ let out = "";
|
|
|
|
|
+ for (const l of t.links.filter((l) => l.kind === "10g")) {
|
|
|
|
|
+ const bi = tgIdx(t, l.b);
|
|
|
|
|
+ if (bi < 4) continue;
|
|
|
|
|
+ const x = tgPos(bi).dcx;
|
|
|
|
|
+ // detour devices turn ON the lane (a T-junction) — only a vertical that
|
|
|
|
|
+ // spans the lane (outer devices, x outside the fabric box) gets a hop
|
|
|
|
|
+ if (x >= 370 && x <= 690) continue;
|
|
|
|
|
+ for (const h of lane)
|
|
|
|
|
+ if (x > h.x1 + 2 && x < h.x2 - 2)
|
|
|
|
|
+ out += `<path class="bridge b-10g" d="M ${x - 5} ${h.y} A 5 5 0 0 1 ${x + 5} ${h.y}"/>`;
|
|
|
|
|
+ }
|
|
|
return out;
|
|
return out;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -1233,13 +1293,14 @@ function switchesSvg(t) {
|
|
|
|
|
|
|
|
const devsSvg = (t) => t.nodes.map((n, i) => devSvg(n, i)).join("");
|
|
const devsSvg = (t) => t.nodes.map((n, i) => devSvg(n, i)).join("");
|
|
|
|
|
|
|
|
-function legendSvg() {
|
|
|
|
|
|
|
+function legendSvg(bus) {
|
|
|
return `<g class="legend" transform="translate(20,526)">
|
|
return `<g class="legend" transform="translate(20,526)">
|
|
|
<line x1="0" y1="0" x2="26" y2="0" class="cable c-200g"/><text x="32" y="4">200G fabric</text>
|
|
<line x1="0" y1="0" x2="26" y2="0" class="cable c-200g"/><text x="32" y="4">200G fabric</text>
|
|
|
<line x1="120" y1="0" x2="146" y2="0" class="cable c-dac"/><text x="152" y="4">DAC direct</text>
|
|
<line x1="120" y1="0" x2="146" y2="0" class="cable c-dac"/><text x="152" y="4">DAC direct</text>
|
|
|
<line x1="240" y1="0" x2="266" y2="0" class="cable c-10g"/><text x="272" y="4">10G mgmt</text>
|
|
<line x1="240" y1="0" x2="266" y2="0" class="cable c-10g"/><text x="272" y="4">10G mgmt</text>
|
|
|
<circle cx="352" cy="0" r="4" class="pled on"/><text x="362" y="4">plug on</text>
|
|
<circle cx="352" cy="0" r="4" class="pled on"/><text x="362" y="4">plug on</text>
|
|
|
<circle cx="428" cy="0" r="4" class="pled off"/><text x="438" y="4">plug off / HA?</text>
|
|
<circle cx="428" cy="0" r="4" class="pled off"/><text x="438" y="4">plug off / HA?</text>
|
|
|
|
|
+ <text x="560" y="4" class="cablemode" data-topobus="1">cables: ${bus ? "bus + bridges" : "direct"} ▸</text>
|
|
|
</g>`;
|
|
</g>`;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -1330,12 +1391,13 @@ function renderTopo(snap) {
|
|
|
const host = document.getElementById("topo");
|
|
const host = document.getElementById("topo");
|
|
|
const t = snap.topo;
|
|
const t = snap.topo;
|
|
|
if (!host || !t) return;
|
|
if (!host || !t) return;
|
|
|
- const sig = topoSig(t);
|
|
|
|
|
|
|
+ if (state.topoBus === undefined) state.topoBus = localStorage.getItem("sm121-topobus") === "1";
|
|
|
|
|
+ const sig = topoSig(t) + (state.topoBus ? ":bus" : ":direct");
|
|
|
if (state.topoSig !== sig) {
|
|
if (state.topoSig !== sig) {
|
|
|
state.topoSig = sig;
|
|
state.topoSig = sig;
|
|
|
const tf = topoFrames(snap);
|
|
const tf = topoFrames(snap);
|
|
|
host.innerHTML = `<svg viewBox="0 0 ${TG_W} ${TG_H}" preserveAspectRatio="xMidYMid meet">
|
|
host.innerHTML = `<svg viewBox="0 0 ${TG_W} ${TG_H}" preserveAspectRatio="xMidYMid meet">
|
|
|
- ${tf.frames}${cablesSvg(t)}${switchesSvg(t)}${devsSvg(t)}${legendSvg()}${tf.chips}
|
|
|
|
|
|
|
+ ${tf.frames}${cablesSvg(t, state.topoBus)}${switchesSvg(t)}${devsSvg(t)}${legendSvg(state.topoBus)}${tf.chips}
|
|
|
</svg>`;
|
|
</svg>`;
|
|
|
for (const g of host.querySelectorAll("[data-cluster]")) {
|
|
for (const g of host.querySelectorAll("[data-cluster]")) {
|
|
|
g.onclick = () => {
|
|
g.onclick = () => {
|
|
@@ -1345,6 +1407,15 @@ function renderTopo(snap) {
|
|
|
onState(state.snap, true);
|
|
onState(state.snap, true);
|
|
|
};
|
|
};
|
|
|
}
|
|
}
|
|
|
|
|
+ // cable-mode toggle (legend): direct bezier runs ↔ bus + bridge arcs
|
|
|
|
|
+ const cm = host.querySelector("[data-topobus]");
|
|
|
|
|
+ if (cm)
|
|
|
|
|
+ cm.onclick = () => {
|
|
|
|
|
+ state.topoBus = !state.topoBus;
|
|
|
|
|
+ localStorage.setItem("sm121-topobus", state.topoBus ? "1" : "0");
|
|
|
|
|
+ state.topoSig = ""; // force a re-render with the other mode
|
|
|
|
|
+ renderTopo(state.snap || snap);
|
|
|
|
|
+ };
|
|
|
}
|
|
}
|
|
|
topoTick(snap);
|
|
topoTick(snap);
|
|
|
}
|
|
}
|