World/Brain: an actual brain — procedural node+edge scaffold
Replaces the random per-id ellipsoid scatter with a fixed, deterministic brain scaffold (frontend/src/components/world/brain.ts): two folded cerebral hemispheres split by a longitudinal fissure (multi-octave noise = gyri/sulci), a cerebellum, and a brain-stem — ~430 nodes wired into a k-nearest-neighbour edge network. WorldCanvas Hierarchy renderer: the scaffold is one Points cloud + one LineSegments (the dormant brain), with an activation field over the nodes. Agents seat into a deterministic fixed region (agentRegion hash); their seat brightens on work and fires activity-coloured signals along the real edges, which chain through the mesh and light its pathways. Idle → a dim, gently-breathing brain. Same brain every reload; orbit/pinch to see the 3D anatomy. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
86524cb309
commit
db7569e5d3
@@ -20,6 +20,7 @@ import { useClawmatesLive, useLiveState } from "@/lib/live/useClawmatesLive";
|
|||||||
|
|
||||||
import { WorldFlow, type WorldItem } from "../dashboard/flow/WorldFlow";
|
import { WorldFlow, type WorldItem } from "../dashboard/flow/WorldFlow";
|
||||||
|
|
||||||
|
import { BRAIN, agentRegion } from "./brain";
|
||||||
import { WorldEngine, type Formation, type WorldSeed } from "./engine";
|
import { WorldEngine, type Formation, type WorldSeed } from "./engine";
|
||||||
|
|
||||||
type ReplayEvent = { t: number; type: string; data: Record<string, unknown> };
|
type ReplayEvent = { t: number; type: string; data: Record<string, unknown> };
|
||||||
@@ -69,27 +70,6 @@ function makeGlowTexture(): THREE.Texture {
|
|||||||
return tex;
|
return tex;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** A deterministic 3D position for an agent-neuron in the brain layout — a
|
|
||||||
* two-hemisphere ellipsoid keyed off the agent id. */
|
|
||||||
function neuronPos(id: string): THREE.Vector3 {
|
|
||||||
let h = 2166136261;
|
|
||||||
for (let i = 0; i < id.length; i++) {
|
|
||||||
h ^= id.charCodeAt(i);
|
|
||||||
h = Math.imul(h, 16777619);
|
|
||||||
}
|
|
||||||
const r1 = ((h >>> 0) % 1000) / 1000;
|
|
||||||
const r2 = ((h >>> 7) % 1000) / 1000;
|
|
||||||
const r3 = ((h >>> 13) % 1000) / 1000;
|
|
||||||
const theta = r1 * Math.PI * 2;
|
|
||||||
const phi = Math.acos(2 * r2 - 1);
|
|
||||||
const rad = 95 + r3 * 80;
|
|
||||||
let x = rad * Math.sin(phi) * Math.cos(theta) * 1.35;
|
|
||||||
const y = rad * Math.sin(phi) * Math.sin(theta) * 0.92;
|
|
||||||
const z = rad * Math.cos(phi) * 0.78;
|
|
||||||
x += (x >= 0 ? 1 : -1) * 48; // split into two hemispheres
|
|
||||||
return new THREE.Vector3(x, y, z);
|
|
||||||
}
|
|
||||||
|
|
||||||
interface WorldCanvasProps {
|
interface WorldCanvasProps {
|
||||||
roots: WorldSeed[];
|
roots: WorldSeed[];
|
||||||
selectedId: string | null;
|
selectedId: string | null;
|
||||||
@@ -285,19 +265,43 @@ export function WorldCanvas({ roots, selectedId, onSelect, expanded, onToggleExp
|
|||||||
);
|
);
|
||||||
gourceGroup.add(beams);
|
gourceGroup.add(beams);
|
||||||
|
|
||||||
// brain (hierarchy) — pathway edges between agent-neurons
|
// brain (hierarchy) — a FIXED procedural scaffold (nodes + edges shaped like a
|
||||||
|
// brain). Agents seat into fixed regions; activation diffuses through the edges.
|
||||||
|
const N = BRAIN.count;
|
||||||
|
const scaffoldGeom = new THREE.BufferGeometry();
|
||||||
|
scaffoldGeom.setAttribute("position", new THREE.Float32BufferAttribute(BRAIN.positions.slice(), 3));
|
||||||
|
const scaffoldColors = new Float32Array(N * 3);
|
||||||
|
scaffoldGeom.setAttribute("color", new THREE.BufferAttribute(scaffoldColors, 3));
|
||||||
|
const scaffoldPoints = new THREE.Points(
|
||||||
|
scaffoldGeom,
|
||||||
|
new THREE.PointsMaterial({ size: 5, map: glow, vertexColors: true, blending: THREE.AdditiveBlending, transparent: true, depthWrite: false, sizeAttenuation: true }),
|
||||||
|
);
|
||||||
|
brainGroup.add(scaffoldPoints);
|
||||||
|
|
||||||
|
const E = BRAIN.edges.length / 2; // edge count
|
||||||
|
const edgePos = new Float32Array(E * 2 * 3);
|
||||||
|
for (let e = 0; e < E; e++) {
|
||||||
|
const a = BRAIN.edges[e * 2];
|
||||||
|
const b = BRAIN.edges[e * 2 + 1];
|
||||||
|
edgePos[e * 6] = BRAIN.positions[a * 3];
|
||||||
|
edgePos[e * 6 + 1] = BRAIN.positions[a * 3 + 1];
|
||||||
|
edgePos[e * 6 + 2] = BRAIN.positions[a * 3 + 2];
|
||||||
|
edgePos[e * 6 + 3] = BRAIN.positions[b * 3];
|
||||||
|
edgePos[e * 6 + 4] = BRAIN.positions[b * 3 + 1];
|
||||||
|
edgePos[e * 6 + 5] = BRAIN.positions[b * 3 + 2];
|
||||||
|
}
|
||||||
const brainEdgeGeom = new THREE.BufferGeometry();
|
const brainEdgeGeom = new THREE.BufferGeometry();
|
||||||
|
brainEdgeGeom.setAttribute("position", new THREE.BufferAttribute(edgePos, 3));
|
||||||
|
const edgeColors = new Float32Array(E * 2 * 3);
|
||||||
|
brainEdgeGeom.setAttribute("color", new THREE.BufferAttribute(edgeColors, 3));
|
||||||
const brainEdges = new THREE.LineSegments(
|
const brainEdges = new THREE.LineSegments(
|
||||||
brainEdgeGeom,
|
brainEdgeGeom,
|
||||||
new THREE.LineBasicMaterial({ vertexColors: true, transparent: true, opacity: 0.95, blending: THREE.AdditiveBlending, depthWrite: false }),
|
new THREE.LineBasicMaterial({ vertexColors: true, transparent: true, opacity: 0.9, blending: THREE.AdditiveBlending, depthWrite: false }),
|
||||||
);
|
);
|
||||||
brainGroup.add(brainEdges);
|
brainGroup.add(brainEdges);
|
||||||
const brainPositions = new Map<string, THREE.Vector3>();
|
|
||||||
const neuronAct = new Map<string, number>();
|
const activation = new Float32Array(N).fill(0.04); // dormant baseline
|
||||||
const neuronExp = new Map<string, number>(); // cumulative experience → neuron size
|
const neuronExp = new Map<string, number>(); // per-agent cumulative experience
|
||||||
const brainAdj = new Map<string, string[]>();
|
|
||||||
let brainEdgeIds: string[] = []; // flat [a0,b0,a1,b1,…] for per-edge lighting
|
|
||||||
let brainCount = -1;
|
|
||||||
|
|
||||||
// particle system (sparks + trails): additive points, ring buffer, RGB→0 fade
|
// particle system (sparks + trails): additive points, ring buffer, RGB→0 fade
|
||||||
const MAXP = 1400;
|
const MAXP = 1400;
|
||||||
@@ -403,108 +407,54 @@ export function WorldCanvas({ roots, selectedId, onSelect, expanded, onToggleExp
|
|||||||
particleGeom.attributes.color.needsUpdate = true;
|
particleGeom.attributes.color.needsUpdate = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Brain pathway graph: connect each neuron to its 3 nearest (rebuilt on count change).
|
// Brain (hierarchy): the fixed scaffold is the dormant mesh; agents seat into
|
||||||
const buildBrainGraph = (ids: string[]) => {
|
// fixed regions whose activity diffuses through the real edges + fires signals.
|
||||||
brainAdj.clear();
|
const seatPos = new THREE.Vector3();
|
||||||
const pos = ids.map((id) => brainPositions.get(id)!);
|
|
||||||
const segs: number[] = [];
|
|
||||||
const pairs: string[] = [];
|
|
||||||
for (let i = 0; i < ids.length; i++) {
|
|
||||||
const ds = ids.map((_, j) => ({ j, d: i === j ? Infinity : pos[i].distanceToSquared(pos[j]) }));
|
|
||||||
ds.sort((a, b) => a.d - b.d);
|
|
||||||
const k = Math.min(3, ids.length - 1);
|
|
||||||
const nbrs: string[] = [];
|
|
||||||
for (let n = 0; n < k; n++) {
|
|
||||||
const j = ds[n].j;
|
|
||||||
nbrs.push(ids[j]);
|
|
||||||
if (i < j) {
|
|
||||||
segs.push(pos[i].x, pos[i].y, pos[i].z, pos[j].x, pos[j].y, pos[j].z);
|
|
||||||
pairs.push(ids[i], ids[j]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
brainAdj.set(ids[i], nbrs);
|
|
||||||
}
|
|
||||||
brainEdgeIds = pairs;
|
|
||||||
brainEdgeGeom.setAttribute("position", new THREE.Float32BufferAttribute(segs, 3));
|
|
||||||
brainEdgeGeom.setAttribute("color", new THREE.Float32BufferAttribute(new Float32Array(segs.length), 3));
|
|
||||||
brainEdgeGeom.attributes.position.needsUpdate = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Neural-brain view: agents are neurons in a dormant mesh; active agents fire
|
|
||||||
// signals along pathways to neighbors (chaining), so the brain comes alive.
|
|
||||||
const renderBrain = (dt: number) => {
|
const renderBrain = (dt: number) => {
|
||||||
// Neurons = agents (bright, drive firing) + structure nodes (org/company/
|
// decay activation toward the dormant baseline + a resting shimmer so the
|
||||||
// team) as a dim support mesh, so even a near-idle brain has body.
|
// brain keeps breathing when idle
|
||||||
type Neuron = { id: string; color: string; fire: string; agent: boolean; boost: number };
|
for (let i = 0; i < N; i++) activation[i] = Math.max(0.04, activation[i] - dt * 0.6);
|
||||||
const neurons: Neuron[] = [];
|
for (let k = 0; k < 3; k++) {
|
||||||
|
const i = (Math.random() * N) | 0;
|
||||||
|
activation[i] = Math.min(0.6, activation[i] + 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
const seen = new Set<string>();
|
||||||
for (const p of engine.pawns.values()) {
|
for (const p of engine.pawns.values()) {
|
||||||
const recent = p.idle < 2.5 ? 1 : 0; // a recent touch lights the neuron
|
seen.add(p.id);
|
||||||
neurons.push({ id: p.id, color: p.color, fire: p.fireColor ?? p.color, agent: true, boost: (p.status === "working" ? 0.9 : 0) + recent });
|
const seat = agentRegion(p.id, N);
|
||||||
}
|
const recent = p.idle < 2.5 ? 1 : 0;
|
||||||
for (const n of engine.nodes.values()) {
|
const boost = (p.status === "working" ? 0.9 : 0) + recent;
|
||||||
if (n.tier === "org" || n.tier === "company" || n.tier === "team")
|
activation[seat] = Math.min(1, activation[seat] + boost * dt * 1.6);
|
||||||
neurons.push({ id: n.id, color: n.color, fire: n.color, agent: false, boost: n.heat });
|
const act = activation[seat];
|
||||||
}
|
let exp = neuronExp.get(p.id) ?? 0;
|
||||||
const ids = neurons.map((n) => n.id);
|
exp = Math.min(1, exp + act * dt * 0.15);
|
||||||
const seen = new Set(ids);
|
neuronExp.set(p.id, exp);
|
||||||
for (const ne of neurons) {
|
let g = neuronGlows.get(p.id);
|
||||||
if (!brainPositions.has(ne.id)) brainPositions.set(ne.id, neuronPos(ne.id));
|
|
||||||
const pos = brainPositions.get(ne.id)!;
|
|
||||||
let g = neuronGlows.get(ne.id);
|
|
||||||
if (!g) {
|
if (!g) {
|
||||||
g = newGlow(brainGroup);
|
g = newGlow(brainGroup);
|
||||||
neuronGlows.set(ne.id, g);
|
neuronGlows.set(p.id, g);
|
||||||
}
|
}
|
||||||
let act = neuronAct.get(ne.id) ?? (ne.agent ? 0.06 : 0.03);
|
seatPos.set(BRAIN.positions[seat * 3], BRAIN.positions[seat * 3 + 1], BRAIN.positions[seat * 3 + 2]);
|
||||||
act = Math.min(1, act + ne.boost * dt * 1.3);
|
g.position.copy(seatPos);
|
||||||
if (Math.random() < dt * 0.5) act = Math.min(1, act + 0.08); // resting shimmer → the brain breathes
|
const gs = 18 + exp * 14 + act * 30;
|
||||||
act = Math.max(ne.agent ? 0.06 : 0.025, act - dt * 0.22);
|
|
||||||
neuronAct.set(ne.id, act);
|
|
||||||
let exp = neuronExp.get(ne.id) ?? 0;
|
|
||||||
exp = Math.min(1, exp + act * dt * 0.15); // experience grows with activity
|
|
||||||
neuronExp.set(ne.id, exp);
|
|
||||||
g.position.copy(pos);
|
|
||||||
const base = (ne.agent ? 16 : 8) + exp * (ne.agent ? 16 : 7);
|
|
||||||
const gs = base + act * (ne.agent ? 36 : 16);
|
|
||||||
g.scale.set(gs, gs, 1);
|
g.scale.set(gs, gs, 1);
|
||||||
const gm = g.material as THREE.SpriteMaterial;
|
const gm = g.material as THREE.SpriteMaterial;
|
||||||
gm.color.set(ne.color);
|
gm.color.set(p.color);
|
||||||
gm.opacity = (ne.agent ? 0.16 : 0.06) + act * (ne.agent ? 0.84 : 0.5);
|
gm.opacity = 0.3 + act * 0.7;
|
||||||
// fire a signal along a pathway (chaining through the mesh)
|
// fire a signal along a real edge to a neighbour (chaining through the mesh)
|
||||||
const nbrs = brainAdj.get(ne.id);
|
const nbrs = BRAIN.adjacency[seat];
|
||||||
if (nbrs && nbrs.length && Math.random() < act * dt * (ne.agent ? 7 : 3.5)) {
|
if (nbrs.length && Math.random() < act * dt * 8) {
|
||||||
const tid = nbrs[(Math.random() * nbrs.length) | 0];
|
const tj = nbrs[(Math.random() * nbrs.length) | 0];
|
||||||
const tp = brainPositions.get(tid);
|
const dx = BRAIN.positions[tj * 3] - seatPos.x;
|
||||||
if (tp) {
|
const dy = BRAIN.positions[tj * 3 + 1] - seatPos.y;
|
||||||
const dx = tp.x - pos.x;
|
const dz = BRAIN.positions[tj * 3 + 2] - seatPos.z;
|
||||||
const dy = tp.y - pos.y;
|
|
||||||
const dz = tp.z - pos.z;
|
|
||||||
const len = Math.hypot(dx, dy, dz) || 1;
|
const len = Math.hypot(dx, dy, dz) || 1;
|
||||||
const speed = 150;
|
const speed = 130;
|
||||||
spawn(pos.x, pos.y, pos.z, ne.fire, (dx / len) * speed, (dy / len) * speed, (dz / len) * speed, speed / len + 0.25);
|
spawn(seatPos.x, seatPos.y, seatPos.z, p.fireColor ?? p.color, (dx / len) * speed, (dy / len) * speed, (dz / len) * speed, speed / len + 0.3);
|
||||||
neuronAct.set(tid, Math.min(1, (neuronAct.get(tid) ?? 0.05) + 0.14));
|
activation[tj] = Math.min(1, activation[tj] + 0.2);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
// light up pathways by endpoint activity (lit pathways stand out; gradient
|
|
||||||
// from each end so the brighter neuron dominates its edges)
|
|
||||||
const ecol = brainEdgeGeom.getAttribute("color") as THREE.BufferAttribute | undefined;
|
|
||||||
if (ecol) {
|
|
||||||
const arr = ecol.array as Float32Array;
|
|
||||||
for (let e = 0; e + 1 < brainEdgeIds.length; e += 2) {
|
|
||||||
const va = 0.05 + (neuronAct.get(brainEdgeIds[e]) ?? 0) * 0.95;
|
|
||||||
const vb = 0.05 + (neuronAct.get(brainEdgeIds[e + 1]) ?? 0) * 0.95;
|
|
||||||
const ia = e * 3;
|
|
||||||
const ib = (e + 1) * 3;
|
|
||||||
arr[ia] = 0.43 * va;
|
|
||||||
arr[ia + 1] = 0.71 * va;
|
|
||||||
arr[ia + 2] = va;
|
|
||||||
arr[ib] = 0.43 * vb;
|
|
||||||
arr[ib + 1] = 0.71 * vb;
|
|
||||||
arr[ib + 2] = vb;
|
|
||||||
}
|
|
||||||
ecol.needsUpdate = true;
|
|
||||||
}
|
|
||||||
for (const [id, g] of neuronGlows) {
|
for (const [id, g] of neuronGlows) {
|
||||||
if (!seen.has(id)) {
|
if (!seen.has(id)) {
|
||||||
brainGroup.remove(g);
|
brainGroup.remove(g);
|
||||||
@@ -512,10 +462,27 @@ export function WorldCanvas({ roots, selectedId, onSelect, expanded, onToggleExp
|
|||||||
neuronGlows.delete(id);
|
neuronGlows.delete(id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (ids.length !== brainCount) {
|
|
||||||
buildBrainGraph(ids);
|
// scaffold point + edge colours from the activation field (lit pathways)
|
||||||
brainCount = ids.length;
|
for (let i = 0; i < N; i++) {
|
||||||
|
const v = 0.05 + activation[i] * 0.95;
|
||||||
|
scaffoldColors[i * 3] = 0.42 * v;
|
||||||
|
scaffoldColors[i * 3 + 1] = 0.66 * v;
|
||||||
|
scaffoldColors[i * 3 + 2] = v;
|
||||||
}
|
}
|
||||||
|
scaffoldGeom.attributes.color.needsUpdate = true;
|
||||||
|
for (let e = 0; e < E; e++) {
|
||||||
|
const va = 0.04 + activation[BRAIN.edges[e * 2]] * 0.96;
|
||||||
|
const vb = 0.04 + activation[BRAIN.edges[e * 2 + 1]] * 0.96;
|
||||||
|
const i0 = e * 6;
|
||||||
|
edgeColors[i0] = 0.42 * va;
|
||||||
|
edgeColors[i0 + 1] = 0.66 * va;
|
||||||
|
edgeColors[i0 + 2] = va;
|
||||||
|
edgeColors[i0 + 3] = 0.42 * vb;
|
||||||
|
edgeColors[i0 + 4] = 0.66 * vb;
|
||||||
|
edgeColors[i0 + 5] = vb;
|
||||||
|
}
|
||||||
|
brainEdgeGeom.attributes.color.needsUpdate = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
const col = new THREE.Color();
|
const col = new THREE.Color();
|
||||||
@@ -567,8 +534,8 @@ export function WorldCanvas({ roots, selectedId, onSelect, expanded, onToggleExp
|
|||||||
renderBrain(dt);
|
renderBrain(dt);
|
||||||
updateParticles(dt);
|
updateParticles(dt);
|
||||||
if (!userInteracted) {
|
if (!userInteracted) {
|
||||||
controls.target.lerp(new THREE.Vector3(0, 0, 0), 0.05);
|
controls.target.lerp(new THREE.Vector3(0, -18, 0), 0.05);
|
||||||
camera.position.lerp(new THREE.Vector3(0, 0, 520), 0.04);
|
camera.position.lerp(new THREE.Vector3(0, -18, 460), 0.04);
|
||||||
}
|
}
|
||||||
controls.update();
|
controls.update();
|
||||||
bloom.strength = 0.75;
|
bloom.strength = 0.75;
|
||||||
@@ -803,6 +770,9 @@ export function WorldCanvas({ roots, selectedId, onSelect, expanded, onToggleExp
|
|||||||
edgeGeom.dispose();
|
edgeGeom.dispose();
|
||||||
beamGeom.dispose();
|
beamGeom.dispose();
|
||||||
brainEdgeGeom.dispose();
|
brainEdgeGeom.dispose();
|
||||||
|
(brainEdges.material as THREE.Material).dispose();
|
||||||
|
scaffoldGeom.dispose();
|
||||||
|
(scaffoldPoints.material as THREE.Material).dispose();
|
||||||
particleGeom.dispose();
|
particleGeom.dispose();
|
||||||
glow.dispose();
|
glow.dispose();
|
||||||
composer.dispose();
|
composer.dispose();
|
||||||
|
|||||||
@@ -0,0 +1,136 @@
|
|||||||
|
// A procedural, deterministic brain scaffold for the Hierarchy view: a fixed
|
||||||
|
// node + edge network shaped like an actual brain — two folded cerebral
|
||||||
|
// hemispheres (split by a longitudinal fissure), a cerebellum, and a brain-stem.
|
||||||
|
// Agents seat into fixed regions of it (agentRegion); their activity diffuses
|
||||||
|
// through the real edges. Seeded so the brain is identical on every reload.
|
||||||
|
|
||||||
|
const R = 120; // overall brain radius scale
|
||||||
|
|
||||||
|
function mulberry32(seed: number): () => number {
|
||||||
|
let s = seed >>> 0;
|
||||||
|
return () => {
|
||||||
|
s = (s + 0x6d2b79f5) | 0;
|
||||||
|
let t = Math.imul(s ^ (s >>> 15), 1 | s);
|
||||||
|
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
|
||||||
|
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Multi-octave sinusoidal "folds" over (theta, phi) → gyri/sulci wrinkles.
|
||||||
|
function folds(theta: number, phi: number): number {
|
||||||
|
return (
|
||||||
|
0.1 * Math.sin(6 * theta + 1.3) * Math.cos(5 * phi) +
|
||||||
|
0.07 * Math.sin(9 * theta + 2.1) * Math.sin(7 * phi + 0.5) +
|
||||||
|
0.05 * Math.cos(13 * theta) * Math.sin(11 * phi + 1.1)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BrainScaffold {
|
||||||
|
positions: Float32Array; // count*3
|
||||||
|
adjacency: number[][]; // per-node neighbour indices (for signal propagation)
|
||||||
|
edges: Uint16Array; // deduped index pairs (for line rendering)
|
||||||
|
count: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildBrainScaffold(): BrainScaffold {
|
||||||
|
const rng = mulberry32(0x9e3779b9);
|
||||||
|
const pts: number[] = [];
|
||||||
|
const push = (x: number, y: number, z: number) => pts.push(x, y, z);
|
||||||
|
|
||||||
|
// Two cerebral hemispheres — an ellipsoid (longer front↔back, a bit wider than
|
||||||
|
// tall), folded to one side of a longitudinal fissure and wrinkled by folds().
|
||||||
|
const cere = { x: 0.86, y: 0.78, z: 1.12 };
|
||||||
|
const gap = 9; // fissure half-gap
|
||||||
|
const HEMI = 175;
|
||||||
|
for (const side of [-1, 1]) {
|
||||||
|
for (let i = 0; i < HEMI; i++) {
|
||||||
|
const u = rng() * 2 - 1; // cos(phi)
|
||||||
|
const t = rng() * Math.PI * 2; // theta
|
||||||
|
const s = Math.sqrt(1 - u * u);
|
||||||
|
const nx = Math.abs(s * Math.cos(t)) * side; // fold to one hemisphere
|
||||||
|
const ny = u;
|
||||||
|
const nz = s * Math.sin(t);
|
||||||
|
const f = 1 + folds(t, Math.acos(u));
|
||||||
|
let x = nx * cere.x * R * f;
|
||||||
|
let y = (ny * cere.y - 0.06) * R * f;
|
||||||
|
const z = nz * cere.z * R * f;
|
||||||
|
if (y < 0) y *= 0.82; // flatter underside
|
||||||
|
x += side * gap;
|
||||||
|
push(x, y, z);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cerebellum — a denser, more wrinkled ellipsoid at the back-bottom.
|
||||||
|
const CB = 64;
|
||||||
|
for (let i = 0; i < CB; i++) {
|
||||||
|
const u = rng() * 2 - 1;
|
||||||
|
const t = rng() * Math.PI * 2;
|
||||||
|
const s = Math.sqrt(1 - u * u);
|
||||||
|
const f = 1 + 1.8 * folds(t * 1.7, Math.acos(u));
|
||||||
|
push((s * Math.cos(t)) * 0.5 * R * f, (u * 0.34 - 0.92) * R, (s * Math.sin(t) * 0.42 - 0.64) * R);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Brain-stem — a short tapering column descending from the bottom-centre.
|
||||||
|
const ST = 16;
|
||||||
|
for (let i = 0; i < ST; i++) {
|
||||||
|
const tt = i / ST;
|
||||||
|
const r = (1 - tt) * 10 + 3;
|
||||||
|
const a = rng() * Math.PI * 2;
|
||||||
|
push(Math.cos(a) * r, (-0.74 - tt * 0.52) * R, (-0.5 + Math.sin(a) * 0.04) * R);
|
||||||
|
}
|
||||||
|
|
||||||
|
const positions = new Float32Array(pts);
|
||||||
|
const count = positions.length / 3;
|
||||||
|
|
||||||
|
// Edges: each node to its K nearest neighbours within MAXD (so edges hug the
|
||||||
|
// surface — no cross-brain chords). Build a node-edge network + adjacency.
|
||||||
|
const K = 4;
|
||||||
|
const MAXD = 50;
|
||||||
|
const adjacency: number[][] = Array.from({ length: count }, () => []);
|
||||||
|
for (let i = 0; i < count; i++) {
|
||||||
|
const ix = positions[i * 3];
|
||||||
|
const iy = positions[i * 3 + 1];
|
||||||
|
const iz = positions[i * 3 + 2];
|
||||||
|
const near: { j: number; d: number }[] = [];
|
||||||
|
for (let j = 0; j < count; j++) {
|
||||||
|
if (j === i) continue;
|
||||||
|
const dx = ix - positions[j * 3];
|
||||||
|
const dy = iy - positions[j * 3 + 1];
|
||||||
|
const dz = iz - positions[j * 3 + 2];
|
||||||
|
near.push({ j, d: dx * dx + dy * dy + dz * dz });
|
||||||
|
}
|
||||||
|
near.sort((a, b) => a.d - b.d);
|
||||||
|
for (let n = 0; n < near.length && adjacency[i].length < K; n++) {
|
||||||
|
if (Math.sqrt(near[n].d) > MAXD) break;
|
||||||
|
adjacency[i].push(near[n].j);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const seen = new Set<string>();
|
||||||
|
const edges: number[] = [];
|
||||||
|
for (let i = 0; i < count; i++) {
|
||||||
|
for (const j of adjacency[i]) {
|
||||||
|
const a = Math.min(i, j);
|
||||||
|
const b = Math.max(i, j);
|
||||||
|
const key = `${a}_${b}`;
|
||||||
|
if (!seen.has(key)) {
|
||||||
|
seen.add(key);
|
||||||
|
edges.push(a, b);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { positions, adjacency, edges: new Uint16Array(edges), count };
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Deterministic fixed "seat" node for an agent (FNV-1a hash). */
|
||||||
|
export function agentRegion(id: string, count: number): number {
|
||||||
|
let h = 2166136261;
|
||||||
|
for (let i = 0; i < id.length; i++) {
|
||||||
|
h ^= id.charCodeAt(i);
|
||||||
|
h = Math.imul(h, 16777619);
|
||||||
|
}
|
||||||
|
return (h >>> 0) % count;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The single shared brain (deterministic — same every reload). */
|
||||||
|
export const BRAIN = buildBrainScaffold();
|
||||||
Reference in New Issue
Block a user