Replace Bloom with Constellation and fix viz picker grid.

Keep all layout cards in a strict 2x2 grid; constellation uses star-chart packing with neighbor edges and leader pipes.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
root
2026-07-18 22:12:01 +02:00
parent ae2a7163ef
commit 64208eb344
3 changed files with 128 additions and 110 deletions
+121 -100
View File
@@ -11,6 +11,7 @@
laneGuides: [], laneGuides: [],
labelAnchors: [], labelAnchors: [],
bloomPetals: [], bloomPetals: [],
constellationCharts: [],
subnet: "all", subnet: "all",
model: null, model: null,
groupHint: null, groupHint: null,
@@ -252,6 +253,7 @@
state.laneGuides = []; state.laneGuides = [];
state.labelAnchors = []; state.labelAnchors = [];
state.bloomPetals = []; state.bloomPetals = [];
state.constellationCharts = [];
setTargets(nodes); setTargets(nodes);
} }
@@ -283,6 +285,7 @@
state.laneGuides = []; state.laneGuides = [];
state.labelAnchors = []; state.labelAnchors = [];
state.bloomPetals = []; state.bloomPetals = [];
state.constellationCharts = [];
setTargets(nodes); setTargets(nodes);
} }
@@ -336,78 +339,100 @@
state.laneGuides = []; state.laneGuides = [];
state.labelAnchors = anchors; state.labelAnchors = anchors;
state.bloomPetals = []; state.bloomPetals = [];
state.constellationCharts = [];
setTargets(nodes); setTargets(nodes);
} }
function layoutBloom(devices, cx, cy, w, h) { function layoutConstellation(devices, cx, cy, w, h) {
const groups = groupBySubnet(devices); const groups = groupBySubnet(devices);
const nodes = []; const nodes = [];
const anchors = []; const anchors = [];
const petals = []; const charts = [];
const R = Math.min(w, h) * 0.38; const orbitR = Math.min(w, h) * 0.3;
const omeHub = { x: cx, y: cy }; const omeHub = { x: cx, y: cy };
const nGroups = Math.max(groups.length, 1); const nGroups = Math.max(groups.length, 1);
const golden = Math.PI * (3 - Math.sqrt(5));
groups.forEach((g, si) => { groups.forEach((g, si) => {
const a = (si / nGroups) * Math.PI * 2 - Math.PI / 2; const a = (si / nGroups) * Math.PI * 2 - Math.PI / 2;
const hx = cx + Math.cos(a) * orbitR;
const hy = cy + Math.sin(a) * orbitR;
const info = subnetLabelInfo(g.cidr); const info = subnetLabelInfo(g.cidr);
const tipX = cx + Math.cos(a) * R; const localR = 22 + Math.min(72, Math.sqrt(Math.max(g.members.length, 1)) * 15);
const tipY = cy + Math.sin(a) * R; const pts = [];
const midX = cx + Math.cos(a) * R * 0.52;
const midY = cy + Math.sin(a) * R * 0.52;
const petalWidth = 22 + Math.min(48, g.members.length * 2.8);
petals.push({ g.members.forEach((n, i) => {
angle: a, const r = localR * Math.sqrt((i + 0.55) / Math.max(g.members.length, 1));
tipX, const ang = i * golden + si * 0.35;
tipY, const jx = Math.sin((n.id || i) * 0.17) * 2.8;
midX, const jy = Math.cos((n.id || i) * 0.13) * 2.8;
midY, const x = hx + Math.cos(ang) * r + jx;
width: petalWidth, const y = hy + Math.sin(ang) * r + jy;
pts.push({ x, y, id: n.id });
nodes.push({
...n,
tx: x,
ty: y,
r: nodeRadius(n),
depth: 1,
constellation: si,
});
});
// Constellation lines: each star to its 2 nearest neighbors
const edgeSet = new Set();
const edges = [];
for (let i = 0; i < pts.length; i++) {
const ranked = pts
.map((p, j) => ({
j,
d: i === j ? Infinity : (p.x - pts[i].x) ** 2 + (p.y - pts[i].y) ** 2,
}))
.sort((aa, bb) => aa.d - bb.d);
for (const nb of ranked.slice(0, Math.min(2, ranked.length))) {
if (!Number.isFinite(nb.d)) continue;
const lo = Math.min(i, nb.j);
const hi = Math.max(i, nb.j);
const key = lo + ":" + hi;
if (edgeSet.has(key)) continue;
edgeSet.add(key);
edges.push({
x1: pts[lo].x,
y1: pts[lo].y,
x2: pts[hi].x,
y2: pts[hi].y,
});
}
}
charts.push({
x: hx,
y: hy,
localR,
color: info.color, color: info.color,
cidr: g.cidr, cidr: g.cidr,
label: info.label, label: info.label,
edges,
count: g.members.length, count: g.members.length,
}); });
anchors.push( anchors.push(
makeLabelAnchor({ makeLabelAnchor({
x: tipX, x: hx,
y: tipY, y: hy,
cidr: g.cidr, cidr: g.cidr,
localR: 10, localR,
dirX: Math.cos(a), dirX: hx - omeHub.x,
dirY: Math.sin(a), dirY: hy - omeHub.y,
pipeExtra: 28, pipeExtra: 34,
}) })
); );
g.members.forEach((n, i) => {
const t = g.members.length <= 1 ? 0.55 : 0.18 + (i / Math.max(g.members.length - 1, 1)) * 0.72;
const alongX = cx + Math.cos(a) * R * t;
const alongY = cy + Math.sin(a) * R * t;
// Petal bulge: widest mid-petal
const bulge = Math.sin(Math.min(1, Math.max(0, (t - 0.12) / 0.76)) * Math.PI);
const lane = (i % 2 === 0 ? 1 : -1) * (0.4 + ((i * 7) % 5) * 0.11);
const px = -Math.sin(a);
const py = Math.cos(a);
const spread = bulge * petalWidth * lane;
nodes.push({
...n,
tx: alongX + px * spread,
ty: alongY + py * spread,
r: nodeRadius(n),
depth: 0.65 + t * 0.55,
bloom: si,
bloomT: t,
});
});
}); });
state.clusterHubs = []; state.clusterHubs = [];
state.laneGuides = []; state.laneGuides = [];
state.labelAnchors = anchors; state.labelAnchors = anchors;
state.bloomPetals = petals; state.bloomPetals = [];
state.constellationCharts = charts;
setTargets(nodes); setTargets(nodes);
} }
@@ -452,6 +477,8 @@
state.clusterHubs = []; state.clusterHubs = [];
state.laneGuides = guides; state.laneGuides = guides;
state.labelAnchors = anchors; state.labelAnchors = anchors;
state.bloomPetals = [];
state.constellationCharts = [];
// hub left side // hub left side
state.hub = { x: 48, y: cy, r: 26 }; state.hub = { x: 48, y: cy, r: 26 };
setTargets(nodes); setTargets(nodes);
@@ -495,6 +522,8 @@
state.clusterHubs = []; state.clusterHubs = [];
state.laneGuides = []; state.laneGuides = [];
state.labelAnchors = anchors; state.labelAnchors = anchors;
state.bloomPetals = [];
state.constellationCharts = [];
setTargets(nodes); setTargets(nodes);
} }
@@ -514,8 +543,8 @@
case "clusters": case "clusters":
layoutClusters(devices, cx, cy, w, h); layoutClusters(devices, cx, cy, w, h);
break; break;
case "bloom": case "constellation":
layoutBloom(devices, cx, cy, w, h); layoutConstellation(devices, cx, cy, w, h);
break; break;
case "lanes": case "lanes":
layoutLanes(devices, cx, cy, w, h); layoutLanes(devices, cx, cy, w, h);
@@ -705,60 +734,51 @@
ctx.globalAlpha = 1; ctx.globalAlpha = 1;
drawPulseLink(hub.x, hub.y, ch.x, ch.y, true, t, (ch.label || "").length * 17, 0.55); drawPulseLink(hub.x, hub.y, ch.x, ch.y, true, t, (ch.label || "").length * 17, 0.55);
} }
} else if (mode === "bloom") { } else if (mode === "constellation") {
// Soft petal outlines + spine pulse // Celestial guide rings
for (const p of state.bloomPetals || []) { for (let i = 1; i <= 4; i++) {
const rgb = hexToRgb(p.color || "#00a8e8");
const a = p.angle || 0;
const nx = -Math.sin(a);
const ny = Math.cos(a);
// Petal path: hub → left bulge → tip → right bulge → hub
const c1x = hub.x + Math.cos(a) * (Math.hypot(p.midX - hub.x, p.midY - hub.y) || 80);
const c1y = hub.y + Math.sin(a) * (Math.hypot(p.midX - hub.x, p.midY - hub.y) || 80);
ctx.beginPath(); ctx.beginPath();
ctx.moveTo(hub.x, hub.y); ctx.arc(hub.x, hub.y, 55 * i * 0.55, 0, Math.PI * 2);
ctx.quadraticCurveTo( ctx.strokeStyle = canvasTheme().decor(i);
c1x + nx * p.width, ctx.lineWidth = 1;
c1y + ny * p.width, ctx.setLineDash(i % 2 ? [2, 6] : []);
p.tipX,
p.tipY
);
ctx.quadraticCurveTo(
c1x - nx * p.width,
c1y - ny * p.width,
hub.x,
hub.y
);
ctx.closePath();
ctx.fillStyle = `rgba(${rgb.r},${rgb.g},${rgb.b},0.06)`;
ctx.fill();
ctx.strokeStyle = `rgba(${rgb.r},${rgb.g},${rgb.b},0.28)`;
ctx.lineWidth = 1.2;
ctx.stroke(); ctx.stroke();
ctx.setLineDash([]);
// Spine
ctx.beginPath();
ctx.moveTo(hub.x, hub.y);
ctx.lineTo(p.tipX, p.tipY);
ctx.strokeStyle = `rgba(${rgb.r},${rgb.g},${rgb.b},0.35)`;
ctx.lineWidth = 1.4;
ctx.stroke();
// Traveling bloom spark along spine
const u = (t * 0.012 + (p.angle || 0)) % 1;
const sx = hub.x + (p.tipX - hub.x) * u;
const sy = hub.y + (p.tipY - hub.y) * u;
ctx.beginPath();
ctx.arc(sx, sy, 2.6, 0, Math.PI * 2);
ctx.fillStyle = `rgba(${rgb.r},${rgb.g},${rgb.b},0.85)`;
ctx.fill();
} }
// Inner halo // Radial tick marks
for (let i = 0; i < 12; i++) {
const ang = (i / 12) * Math.PI * 2;
ctx.beginPath(); ctx.beginPath();
ctx.arc(hub.x, hub.y, 42, 0, Math.PI * 2); ctx.moveTo(hub.x + Math.cos(ang) * 36, hub.y + Math.sin(ang) * 36);
ctx.lineTo(hub.x + Math.cos(ang) * 48, hub.y + Math.sin(ang) * 48);
ctx.strokeStyle = canvasTheme().decor(2); ctx.strokeStyle = canvasTheme().decor(2);
ctx.lineWidth = 1; ctx.lineWidth = 1;
ctx.stroke(); ctx.stroke();
}
for (const ch of state.constellationCharts || []) {
const rgb = hexToRgb(ch.color || "#00a8e8");
// Soft halo around constellation
ctx.beginPath();
ctx.arc(ch.x, ch.y, (ch.localR || 40) + 14, 0, Math.PI * 2);
ctx.strokeStyle = `rgba(${rgb.r},${rgb.g},${rgb.b},0.2)`;
ctx.lineWidth = 1;
ctx.stroke();
// Star-chart edges
for (const e of ch.edges || []) {
ctx.beginPath();
ctx.moveTo(e.x1, e.y1);
ctx.lineTo(e.x2, e.y2);
ctx.strokeStyle = `rgba(${rgb.r},${rgb.g},${rgb.b},0.38)`;
ctx.lineWidth = 1;
ctx.stroke();
}
// Dim center marker
ctx.beginPath();
ctx.arc(ch.x, ch.y, 3, 0, Math.PI * 2);
ctx.fillStyle = `rgba(${rgb.r},${rgb.g},${rgb.b},0.55)`;
ctx.fill();
drawPulseLink(hub.x, hub.y, ch.x, ch.y, true, t, (ch.label || "").length * 19, 0.4);
}
} else if (mode === "lanes") { } else if (mode === "lanes") {
for (const g of state.laneGuides || []) { for (const g of state.laneGuides || []) {
ctx.beginPath(); ctx.beginPath();
@@ -902,15 +922,14 @@
} }
return; return;
} }
if (mode === "bloom") { if (mode === "constellation") {
// Node → petal mid (spine), plus faint hub link // Soft hub link to each star; constellation edges already in decor
for (const n of state.nodes) { for (const n of state.nodes) {
const petal = (state.bloomPetals || [])[n.bloom]; const ch = (state.constellationCharts || [])[n.constellation];
if (petal) { if (ch) {
drawPulseLink(petal.midX, petal.midY, n.x, n.y, n.connected, t, n.id, 0.75); drawPulseLink(ch.x, ch.y, n.x, n.y, n.connected, t, n.id, 0.55);
if (n.connected) drawPulseLink(hub.x, hub.y, petal.midX, petal.midY, true, t, n.id + 9000, 0.35);
} else { } else {
drawPulseLink(hub.x, hub.y, n.x, n.y, n.connected, t, n.id, 0.8); drawPulseLink(hub.x, hub.y, n.x, n.y, n.connected, t, n.id, 0.7);
} }
} }
return; return;
@@ -1677,6 +1696,8 @@
} }
if (state.hub) expand(state.hub.x, state.hub.y, (state.hub.r || 28) + 16); if (state.hub) expand(state.hub.x, state.hub.y, (state.hub.r || 28) + 16);
for (const ch of state.clusterHubs || []) expand(ch.x, ch.y, 56); for (const ch of state.clusterHubs || []) expand(ch.x, ch.y, 56);
for (const c of state.constellationCharts || []) expand(c.x, c.y, (c.localR || 40) + 24);
for (const a of state.labelAnchors || []) expand(a.x, a.y, 48);
const bw = Math.max(80, maxX - minX); const bw = Math.max(80, maxX - minX);
const bh = Math.max(80, maxY - minY); const bh = Math.max(80, maxY - minY);
+3 -3
View File
@@ -7,7 +7,7 @@
<link rel="preconnect" href="https://fonts.googleapis.com" /> <link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin /> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500;600&family=IBM+Plex+Sans:wght@400;500;600;700&display=swap" rel="stylesheet" /> <link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500;600&family=IBM+Plex+Sans:wght@400;500;600;700&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="/styles.css?v=vlanall1" /> <link rel="stylesheet" href="/styles.css?v=constellation1" />
<link rel="icon" href="/dell.png" type="image/png" /> <link rel="icon" href="/dell.png" type="image/png" />
<link rel="stylesheet" href="/vendor/xterm/xterm.css?v=home1" /> <link rel="stylesheet" href="/vendor/xterm/xterm.css?v=home1" />
<script> <script>
@@ -58,7 +58,7 @@
<button type="button" class="viz-card active" data-layout="orbit"><span class="viz-ico"></span><span class="viz-t">Orbit</span><span class="viz-s">Hub &amp; rings</span></button> <button type="button" class="viz-card active" data-layout="orbit"><span class="viz-ico"></span><span class="viz-t">Orbit</span><span class="viz-s">Hub &amp; rings</span></button>
<button type="button" class="viz-card" data-layout="galaxy"><span class="viz-ico"></span><span class="viz-t">Galaxy</span><span class="viz-s">Spiral arms</span></button> <button type="button" class="viz-card" data-layout="galaxy"><span class="viz-ico"></span><span class="viz-t">Galaxy</span><span class="viz-s">Spiral arms</span></button>
<button type="button" class="viz-card" data-layout="clusters"><span class="viz-ico"></span><span class="viz-t">Clusters</span><span class="viz-s">Subnet islands</span></button> <button type="button" class="viz-card" data-layout="clusters"><span class="viz-ico"></span><span class="viz-t">Clusters</span><span class="viz-s">Subnet islands</span></button>
<button type="button" class="viz-card" data-layout="bloom"><span class="viz-ico"></span><span class="viz-t">Bloom</span><span class="viz-s">VLAN petals</span></button> <button type="button" class="viz-card" data-layout="constellation"><span class="viz-ico"></span><span class="viz-t">Constellation</span><span class="viz-s">Star chart</span></button>
<button type="button" class="viz-card" data-layout="lanes"><span class="viz-ico"></span><span class="viz-t">Lanes</span><span class="viz-s">Fiber highways</span></button> <button type="button" class="viz-card" data-layout="lanes"><span class="viz-ico"></span><span class="viz-t">Lanes</span><span class="viz-s">Fiber highways</span></button>
<button type="button" class="viz-card" data-layout="helix"><span class="viz-ico"></span><span class="viz-t">Helix</span><span class="viz-s">3D depth coil</span></button> <button type="button" class="viz-card" data-layout="helix"><span class="viz-ico"></span><span class="viz-t">Helix</span><span class="viz-s">3D depth coil</span></button>
</div> </div>
@@ -634,7 +634,7 @@
<script src="/vendor/xterm/xterm.min.js?v=home1"></script> <script src="/vendor/xterm/xterm.min.js?v=home1"></script>
<script src="/vendor/xterm/xterm-addon-fit.min.js?v=home1"></script> <script src="/vendor/xterm/xterm-addon-fit.min.js?v=home1"></script>
<script src="/app.js?v=bloom1"></script> <script src="/app.js?v=constellation1"></script>
<script src="/ops.js?v=opsfde2"></script> <script src="/ops.js?v=opsfde2"></script>
<script src="/ssh.js?v=fabric4"></script> <script src="/ssh.js?v=fabric4"></script>
<script src="/rdp.js?v=fabric4"></script> <script src="/rdp.js?v=fabric4"></script>
-3
View File
@@ -752,9 +752,6 @@ html[data-theme="light"] .inspector-body .kv-row .v { color: #122033; }
gap: 0.08rem; gap: 0.08rem;
min-height: 64px; min-height: 64px;
} }
.viz-card:nth-child(5) {
grid-column: 1 / -1;
}
.viz-card:hover { .viz-card:hover {
border-color: var(--dell-bright); border-color: var(--dell-bright);
color: var(--text); color: var(--text);