diff --git a/ui/app.js b/ui/app.js
index f41c17c..553483c 100644
--- a/ui/app.js
+++ b/ui/app.js
@@ -11,7 +11,8 @@
laneGuides: [],
labelAnchors: [],
bloomPetals: [],
- constellationCharts: [],
+ radarSectors: [],
+ radarMaxR: 0,
subnet: "all",
model: null,
groupHint: null,
@@ -253,7 +254,8 @@
state.laneGuides = [];
state.labelAnchors = [];
state.bloomPetals = [];
- state.constellationCharts = [];
+ state.radarSectors = [];
+ state.radarMaxR = 0;
setTargets(nodes);
}
@@ -285,7 +287,8 @@
state.laneGuides = [];
state.labelAnchors = [];
state.bloomPetals = [];
- state.constellationCharts = [];
+ state.radarSectors = [];
+ state.radarMaxR = 0;
setTargets(nodes);
}
@@ -339,100 +342,83 @@
state.laneGuides = [];
state.labelAnchors = anchors;
state.bloomPetals = [];
- state.constellationCharts = [];
+ state.radarSectors = [];
+ state.radarMaxR = 0;
setTargets(nodes);
}
- function layoutConstellation(devices, cx, cy, w, h) {
+ function layoutRadar(devices, cx, cy, w, h) {
const groups = groupBySubnet(devices);
const nodes = [];
const anchors = [];
- const charts = [];
- const orbitR = Math.min(w, h) * 0.3;
- const omeHub = { x: cx, y: cy };
+ const sectors = [];
+ const maxR = Math.min(w, h) * 0.42;
const nGroups = Math.max(groups.length, 1);
- const golden = Math.PI * (3 - Math.sqrt(5));
+ const gap = 0.045;
+ const usable = Math.PI * 2 - gap * nGroups;
+ const slice = usable / nGroups;
+ let cursor = -Math.PI / 2;
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 a0 = cursor + gap / 2;
+ const a1 = a0 + slice;
+ const mid = (a0 + a1) / 2;
+ cursor = a1 + gap / 2;
const info = subnetLabelInfo(g.cidr);
- const localR = 22 + Math.min(72, Math.sqrt(Math.max(g.members.length, 1)) * 15);
- const pts = [];
- 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,
+ sectors.push({
+ a0,
+ a1,
+ mid,
color: info.color,
cidr: g.cidr,
label: info.label,
- edges,
count: g.members.length,
});
+
+ const labelR = maxR + 6;
anchors.push(
makeLabelAnchor({
- x: hx,
- y: hy,
+ x: cx + Math.cos(mid) * labelR,
+ y: cy + Math.sin(mid) * labelR,
cidr: g.cidr,
- localR,
- dirX: hx - omeHub.x,
- dirY: hy - omeHub.y,
- pipeExtra: 34,
+ localR: 4,
+ dirX: Math.cos(mid),
+ dirY: Math.sin(mid),
+ pipeExtra: 26,
})
);
+
+ const sorted = [...g.members].sort(
+ (a, b) => Number(b.connected) - Number(a.connected) || (b.watts || 0) - (a.watts || 0)
+ );
+ const ringCount = Math.max(3, Math.min(7, Math.ceil(Math.sqrt(Math.max(sorted.length, 1)))));
+ sorted.forEach((n, i) => {
+ const ring = 1 + (i % ringCount);
+ const slot = Math.floor(i / ringCount);
+ const slotsOnRing = Math.max(1, Math.ceil(sorted.length / ringCount));
+ const t = slotsOnRing <= 1 ? 0.5 : (slot + 0.5) / slotsOnRing;
+ // keep a small inset from sector edges
+ const ang = a0 + 0.08 * (a1 - a0) + t * 0.84 * (a1 - a0);
+ const r = maxR * (0.22 + (ring / (ringCount + 0.35)) * 0.72);
+ nodes.push({
+ ...n,
+ tx: cx + Math.cos(ang) * r,
+ ty: cy + Math.sin(ang) * r,
+ r: nodeRadius(n),
+ depth: 0.45 + ring * 0.12,
+ radar: si,
+ radarRing: ring,
+ });
+ });
});
state.clusterHubs = [];
state.laneGuides = [];
state.labelAnchors = anchors;
state.bloomPetals = [];
- state.constellationCharts = charts;
+ state.radarSectors = sectors;
+ state.radarMaxR = maxR;
setTargets(nodes);
}
@@ -478,7 +464,8 @@
state.laneGuides = guides;
state.labelAnchors = anchors;
state.bloomPetals = [];
- state.constellationCharts = [];
+ state.radarSectors = [];
+ state.radarMaxR = 0;
// hub left side
state.hub = { x: 48, y: cy, r: 26 };
setTargets(nodes);
@@ -523,7 +510,8 @@
state.laneGuides = [];
state.labelAnchors = anchors;
state.bloomPetals = [];
- state.constellationCharts = [];
+ state.radarSectors = [];
+ state.radarMaxR = 0;
setTargets(nodes);
}
@@ -543,8 +531,8 @@
case "clusters":
layoutClusters(devices, cx, cy, w, h);
break;
- case "constellation":
- layoutConstellation(devices, cx, cy, w, h);
+ case "radar":
+ layoutRadar(devices, cx, cy, w, h);
break;
case "lanes":
layoutLanes(devices, cx, cy, w, h);
@@ -734,52 +722,62 @@
ctx.globalAlpha = 1;
drawPulseLink(hub.x, hub.y, ch.x, ch.y, true, t, (ch.label || "").length * 17, 0.55);
}
- } else if (mode === "constellation") {
- // Celestial guide rings
+ } else if (mode === "radar") {
+ const maxR = state.radarMaxR || Math.min(w, h) * 0.42;
+ // Range rings
for (let i = 1; i <= 4; i++) {
+ const rr = maxR * (i / 4);
ctx.beginPath();
- ctx.arc(hub.x, hub.y, 55 * i * 0.55, 0, Math.PI * 2);
+ ctx.arc(hub.x, hub.y, rr, 0, Math.PI * 2);
ctx.strokeStyle = canvasTheme().decor(i);
ctx.lineWidth = 1;
- ctx.setLineDash(i % 2 ? [2, 6] : []);
+ ctx.setLineDash(i === 4 ? [] : [3, 5]);
ctx.stroke();
ctx.setLineDash([]);
}
- // Radial tick marks
- for (let i = 0; i < 12; i++) {
- const ang = (i / 12) * Math.PI * 2;
+ // Sector wedges + outer arcs
+ for (const s of state.radarSectors || []) {
+ const rgb = hexToRgb(s.color || "#00a8e8");
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.moveTo(hub.x, hub.y);
+ ctx.arc(hub.x, hub.y, maxR, s.a0, s.a1);
+ ctx.closePath();
+ ctx.fillStyle = `rgba(${rgb.r},${rgb.g},${rgb.b},0.045)`;
+ ctx.fill();
+ // divider rays
+ for (const ang of [s.a0, s.a1]) {
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.moveTo(hub.x, hub.y);
+ ctx.lineTo(hub.x + Math.cos(ang) * maxR, hub.y + Math.sin(ang) * maxR);
+ ctx.strokeStyle = `rgba(${rgb.r},${rgb.g},${rgb.b},0.22)`;
ctx.lineWidth = 1;
ctx.stroke();
}
- // Dim center marker
+ // outer rim arc
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);
+ ctx.arc(hub.x, hub.y, maxR, s.a0, s.a1);
+ ctx.strokeStyle = `rgba(${rgb.r},${rgb.g},${rgb.b},0.55)`;
+ ctx.lineWidth = 2;
+ ctx.stroke();
}
- } else if (mode === "lanes") {
+ // Rotating sweep beam
+ const sweep = (t * 0.018) % (Math.PI * 2);
+ const grad = ctx.createRadialGradient(hub.x, hub.y, 8, hub.x, hub.y, maxR);
+ grad.addColorStop(0, "rgba(61,255,224,0.18)");
+ grad.addColorStop(1, "rgba(61,255,224,0)");
+ ctx.beginPath();
+ ctx.moveTo(hub.x, hub.y);
+ ctx.arc(hub.x, hub.y, maxR, sweep - 0.22, sweep);
+ ctx.closePath();
+ ctx.fillStyle = grad;
+ ctx.fill();
+ ctx.beginPath();
+ ctx.moveTo(hub.x, hub.y);
+ ctx.lineTo(hub.x + Math.cos(sweep) * maxR, hub.y + Math.sin(sweep) * maxR);
+ ctx.strokeStyle = "rgba(61,255,224,0.55)";
+ ctx.lineWidth = 1.5;
+ ctx.stroke();
+ } else if (mode === "lanes") { } else if (mode === "lanes") {
for (const g of state.laneGuides || []) {
ctx.beginPath();
ctx.moveTo(70, g.y);
@@ -922,15 +920,10 @@
}
return;
}
- if (mode === "constellation") {
- // Soft hub link to each star; constellation edges already in decor
+ if (mode === "radar") {
+ // Radial blips to OME — classic radar spokes
for (const n of state.nodes) {
- 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.7);
- }
+ drawPulseLink(hub.x, hub.y, n.x, n.y, n.connected, t, n.id, n.connected ? 0.85 : 0.35);
}
return;
}
@@ -1696,8 +1689,10 @@
}
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);
+ if (state.layoutMode === "radar" && state.radarMaxR) {
+ expand(state.hub?.x || 0, state.hub?.y || 0, state.radarMaxR + 56);
+ }
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 c8dc70d..23df042 100644
--- a/ui/index.html
+++ b/ui/index.html
@@ -7,7 +7,7 @@
-
+
-
+