feat(rnetwork): concentric spheres layout with Fibonacci distribution

Replace flat ring layout with 3D sphere distribution using Fibonacci
spiral for even node placement. Wireframe sphere guides replace flat
ring guides — visible from every camera angle.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Jeff Emmett 2026-03-15 17:05:30 -07:00
parent 246b51b2e0
commit 03de21ddd5
1 changed files with 18 additions and 15 deletions

View File

@ -1075,9 +1075,9 @@ class FolkGraphViewer extends HTMLElement {
n.fx = 0; n.fy = 0; n.fz = 0; n.fx = 0; n.fy = 0; n.fz = 0;
} }
this.placeRing(adminNodes, 30); this.placeSphere(adminNodes, 30);
this.placeRing(memberNodes, 80); this.placeSphere(memberNodes, 80);
this.placeRing(viewerNodes, 160); this.placeSphere(viewerNodes, 160);
this.graph.graphData(data); this.graph.graphData(data);
this.graph.d3ReheatSimulation(); this.graph.d3ReheatSimulation();
@ -1087,14 +1087,18 @@ class FolkGraphViewer extends HTMLElement {
this.addRingGuides(); this.addRingGuides();
} }
private placeRing(nodes: GraphNode[], radius: number) { private placeSphere(nodes: GraphNode[], radius: number) {
const count = nodes.length; const count = nodes.length;
if (count === 0) return; if (count === 0) return;
// Fibonacci sphere — even distribution on a sphere surface
const goldenAngle = Math.PI * (3 - Math.sqrt(5));
for (let i = 0; i < count; i++) { for (let i = 0; i < count; i++) {
const angle = (2 * Math.PI * i) / count; const y = 1 - (2 * i) / (count - 1 || 1); // -1 to 1
nodes[i].fx = Math.cos(angle) * radius; const r = Math.sqrt(1 - y * y);
nodes[i].fy = 0; const theta = goldenAngle * i;
nodes[i].fz = Math.sin(angle) * radius; nodes[i].fx = Math.cos(theta) * r * radius;
nodes[i].fy = y * radius;
nodes[i].fz = Math.sin(theta) * r * radius;
} }
} }
@ -1116,23 +1120,22 @@ class FolkGraphViewer extends HTMLElement {
const scene = this.graph.scene(); const scene = this.graph.scene();
if (!scene) return; if (!scene) return;
const ringRadii = [ const sphereRadii = [
{ r: 30, color: 0xa78bfa, label: "Admin" }, { r: 30, color: 0xa78bfa, label: "Admin" },
{ r: 80, color: 0x10b981, label: "Member" }, { r: 80, color: 0x10b981, label: "Member" },
{ r: 160, color: 0x3b82f6, label: "Viewer" }, { r: 160, color: 0x3b82f6, label: "Viewer" },
]; ];
for (const { r, color } of ringRadii) { for (const { r, color } of sphereRadii) {
const geometry = new THREE.RingGeometry(r - 0.5, r + 0.5, 128); // Wireframe sphere guide
const geometry = new THREE.SphereGeometry(r, 24, 16);
const material = new THREE.MeshBasicMaterial({ const material = new THREE.MeshBasicMaterial({
color, color,
transparent: true, transparent: true,
opacity: 0.15, opacity: 0.06,
side: THREE.DoubleSide, wireframe: true,
}); });
const mesh = new THREE.Mesh(geometry, material); const mesh = new THREE.Mesh(geometry, material);
mesh.rotation.x = -Math.PI / 2; // flat on XZ plane
mesh.position.y = 0;
scene.add(mesh); scene.add(mesh);
this.ringGuides.push(mesh); this.ringGuides.push(mesh);
} }