From 64208eb34415d2ebb595523a8c9d152871b9f047 Mon Sep 17 00:00:00 2001 From: root Date: Sat, 18 Jul 2026 22:12:01 +0200 Subject: [PATCH] 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 --- ui/app.js | 229 +++++++++++++++++++++++++++----------------------- ui/index.html | 6 +- ui/styles.css | 3 - 3 files changed, 128 insertions(+), 110 deletions(-) diff --git a/ui/app.js b/ui/app.js index 7a69138..f41c17c 100644 --- a/ui/app.js +++ b/ui/app.js @@ -11,6 +11,7 @@ laneGuides: [], labelAnchors: [], bloomPetals: [], + constellationCharts: [], subnet: "all", model: null, groupHint: null, @@ -252,6 +253,7 @@ state.laneGuides = []; state.labelAnchors = []; state.bloomPetals = []; + state.constellationCharts = []; setTargets(nodes); } @@ -283,6 +285,7 @@ state.laneGuides = []; state.labelAnchors = []; state.bloomPetals = []; + state.constellationCharts = []; setTargets(nodes); } @@ -336,78 +339,100 @@ state.laneGuides = []; state.labelAnchors = anchors; state.bloomPetals = []; + state.constellationCharts = []; setTargets(nodes); } - function layoutBloom(devices, cx, cy, w, h) { + function layoutConstellation(devices, cx, cy, w, h) { const groups = groupBySubnet(devices); const nodes = []; const anchors = []; - const petals = []; - const R = Math.min(w, h) * 0.38; + const charts = []; + const orbitR = Math.min(w, h) * 0.3; const omeHub = { x: cx, y: cy }; const nGroups = Math.max(groups.length, 1); + const golden = Math.PI * (3 - Math.sqrt(5)); groups.forEach((g, si) => { 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 tipX = cx + Math.cos(a) * R; - const tipY = cy + Math.sin(a) * R; - 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); + const localR = 22 + Math.min(72, Math.sqrt(Math.max(g.members.length, 1)) * 15); + const pts = []; - petals.push({ - angle: a, - tipX, - tipY, - midX, - midY, - width: petalWidth, + g.members.forEach((n, i) => { + const r = localR * Math.sqrt((i + 0.55) / Math.max(g.members.length, 1)); + const ang = i * golden + si * 0.35; + const jx = Math.sin((n.id || i) * 0.17) * 2.8; + const jy = Math.cos((n.id || i) * 0.13) * 2.8; + const x = hx + Math.cos(ang) * r + jx; + 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, cidr: g.cidr, label: info.label, + edges, count: g.members.length, }); - anchors.push( makeLabelAnchor({ - x: tipX, - y: tipY, + x: hx, + y: hy, cidr: g.cidr, - localR: 10, - dirX: Math.cos(a), - dirY: Math.sin(a), - pipeExtra: 28, + localR, + dirX: hx - omeHub.x, + dirY: hy - omeHub.y, + 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.laneGuides = []; state.labelAnchors = anchors; - state.bloomPetals = petals; + state.bloomPetals = []; + state.constellationCharts = charts; setTargets(nodes); } @@ -452,6 +477,8 @@ state.clusterHubs = []; state.laneGuides = guides; state.labelAnchors = anchors; + state.bloomPetals = []; + state.constellationCharts = []; // hub left side state.hub = { x: 48, y: cy, r: 26 }; setTargets(nodes); @@ -495,6 +522,8 @@ state.clusterHubs = []; state.laneGuides = []; state.labelAnchors = anchors; + state.bloomPetals = []; + state.constellationCharts = []; setTargets(nodes); } @@ -514,8 +543,8 @@ case "clusters": layoutClusters(devices, cx, cy, w, h); break; - case "bloom": - layoutBloom(devices, cx, cy, w, h); + case "constellation": + layoutConstellation(devices, cx, cy, w, h); break; case "lanes": layoutLanes(devices, cx, cy, w, h); @@ -705,60 +734,51 @@ ctx.globalAlpha = 1; drawPulseLink(hub.x, hub.y, ch.x, ch.y, true, t, (ch.label || "").length * 17, 0.55); } - } else if (mode === "bloom") { - // Soft petal outlines + spine pulse - for (const p of state.bloomPetals || []) { - 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); + } else if (mode === "constellation") { + // Celestial guide rings + for (let i = 1; i <= 4; i++) { ctx.beginPath(); - ctx.moveTo(hub.x, hub.y); - ctx.quadraticCurveTo( - c1x + nx * p.width, - c1y + ny * p.width, - 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.arc(hub.x, hub.y, 55 * i * 0.55, 0, Math.PI * 2); + ctx.strokeStyle = canvasTheme().decor(i); + ctx.lineWidth = 1; + ctx.setLineDash(i % 2 ? [2, 6] : []); ctx.stroke(); - - // 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(); + ctx.setLineDash([]); + } + // Radial tick marks + for (let i = 0; i < 12; i++) { + const ang = (i / 12) * Math.PI * 2; + ctx.beginPath(); + 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.lineWidth = 1; + 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); } - // Inner halo - ctx.beginPath(); - ctx.arc(hub.x, hub.y, 42, 0, Math.PI * 2); - ctx.strokeStyle = canvasTheme().decor(2); - ctx.lineWidth = 1; - ctx.stroke(); } else if (mode === "lanes") { for (const g of state.laneGuides || []) { ctx.beginPath(); @@ -902,15 +922,14 @@ } return; } - if (mode === "bloom") { - // Node → petal mid (spine), plus faint hub link + if (mode === "constellation") { + // Soft hub link to each star; constellation edges already in decor for (const n of state.nodes) { - const petal = (state.bloomPetals || [])[n.bloom]; - if (petal) { - drawPulseLink(petal.midX, petal.midY, n.x, n.y, n.connected, t, n.id, 0.75); - if (n.connected) drawPulseLink(hub.x, hub.y, petal.midX, petal.midY, true, t, n.id + 9000, 0.35); + const ch = (state.constellationCharts || [])[n.constellation]; + if (ch) { + drawPulseLink(ch.x, ch.y, n.x, n.y, n.connected, t, n.id, 0.55); } 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; @@ -1677,6 +1696,8 @@ } 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 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 bh = Math.max(80, maxY - minY); diff --git a/ui/index.html b/ui/index.html index 05d6d55..c8dc70d 100644 --- a/ui/index.html +++ b/ui/index.html @@ -7,7 +7,7 @@ - + - + diff --git a/ui/styles.css b/ui/styles.css index 0b0dfed..19e3274 100644 --- a/ui/styles.css +++ b/ui/styles.css @@ -752,9 +752,6 @@ html[data-theme="light"] .inspector-body .kv-row .v { color: #122033; } gap: 0.08rem; min-height: 64px; } -.viz-card:nth-child(5) { - grid-column: 1 / -1; -} .viz-card:hover { border-color: var(--dell-bright); color: var(--text);