World: explosive file sparks + 3 real views (Flat 2D / Live Gource / Brain)
ci / gates (push) Has been cancelled
ci / rust (push) Has been cancelled
ci / sandbox-k8s (push) Has been cancelled
ci / frontend (push) Has been cancelled
ci / e2e (push) Has been cancelled

- Explosive sparks on file/project I/O: /api/world/live tags file-ish tools
  (read/write/drive/vault/obsidian/edit/save) with touch weight 1; the engine
  carries it as a node "burst" and the renderer fires a big fast particle blast.
- The formation tabs are now genuinely different views:
  - Flat — the previous 2D React-Flow forest (WorldFlow), overlaid.
  - Live — the WebGL Gource clone (glow sprites + sparks + trails + beams).
  - Hierarchy — a NEW neural-brain view: agents are neurons in a dormant
    two-hemisphere mesh; active agents fire signal particles along pathway edges
    to their nearest neighbours (chaining), so the brain lights up as agents work.
- Render groups (gource/brain) + 3D particle system so signals travel in depth;
  shared particle update; OrbitControls (rotate + pinch-zoom) across all WebGL views.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
This commit is contained in:
Omar Sobh
2026-06-23 22:49:23 -07:00
co-authored by Claude Opus 4.8
parent da20417fda
commit 3b012b12bf
4 changed files with 234 additions and 37 deletions
+8 -2
View File
@@ -86,10 +86,16 @@ fn normalize_run_event(agent_id: &str, event_type: &str, payload: &Value) -> Vec
let tool = payload.get("tool").and_then(|v| v.as_str()).unwrap_or("tool"); let tool = payload.get("tool").and_then(|v| v.as_str()).unwrap_or("tool");
let node_id = format!("tool:{tool}"); let node_id = format!("tool:{tool}");
let target = payload.get("input").map(summarize_input).unwrap_or_default(); let target = payload.get("input").map(summarize_input).unwrap_or_default();
// File/project I/O gets an explosive burst (high touch weight).
let lower = tool.to_lowercase();
let file_op = ["file", "read", "write", "drive", "vault", "obsidian", "edit", "fs", "save"]
.iter()
.any(|k| lower.contains(k));
let weight = if file_op { 1.0 } else { 0.4 };
out.push(("agent.tool.call", json!({ "agentId": agent_id, "tool": tool, "target": target }))); out.push(("agent.tool.call", json!({ "agentId": agent_id, "tool": tool, "target": target })));
out.push(("node.activity", json!({ "nodeId": node_id, "label": tool, "kind": "service", "heat": 0.9 }))); out.push(("node.activity", json!({ "nodeId": node_id, "label": tool, "kind": "service", "heat": if file_op { 1.0 } else { 0.9 } })));
// the agent converges on the tool it's using (the Gource beam) // the agent converges on the tool it's using (the Gource beam)
out.push(("world.touch", json!({ "agentId": agent_id, "nodeId": node_id, "kind": "service" }))); out.push(("world.touch", json!({ "agentId": agent_id, "nodeId": node_id, "kind": "service", "weight": weight })));
} }
"approval_required" => { "approval_required" => {
let action = payload.get("action_type").and_then(|v| v.as_str()).unwrap_or("action"); let action = payload.get("action_type").and_then(|v| v.as_str()).unwrap_or("action");
@@ -572,7 +572,7 @@ export function Dashboard({ user, orgs, claws }: { user?: { display_name?: strin
<> <>
{/* Graph stage — condensed by the right slide-out's width. */} {/* Graph stage — condensed by the right slide-out's width. */}
<div style={{ position: "absolute", top: 0, bottom: 0, left: 0, right: "var(--world-width, 0px)", background: "radial-gradient(120% 90% at 55% 38%, #0e0e13 0%, #08080a 70%)", transition: "right var(--duration-normal) var(--ease-app)" }}> <div style={{ position: "absolute", top: 0, bottom: 0, left: 0, right: "var(--world-width, 0px)", background: "radial-gradient(120% 90% at 55% 38%, #0e0e13 0%, #08080a 70%)", transition: "right var(--duration-normal) var(--ease-app)" }}>
<WorldCanvas roots={worldRoots} selectedId={worldSel} onSelect={onWorldSelect} onOpenRuns={() => setRunsOpen(true)} /> <WorldCanvas roots={worldRoots} expanded={expanded} onToggleExpand={toggleExpand} selectedId={worldSel} onSelect={onWorldSelect} onOpenRuns={() => setRunsOpen(true)} />
{/* Open the slide-out (top-right) when it's closed. */} {/* Open the slide-out (top-right) when it's closed. */}
{!worldPanelOpen ? ( {!worldPanelOpen ? (
<button type="button" aria-label="Open panel" title="Panel" onClick={() => setWorldPanelOpen(true)} style={{ position: "absolute", top: 14, right: 16, zIndex: 50, width: 38, height: 38, borderRadius: "50%", border: "1px solid rgba(255,111,97,.4)", background: "rgba(255,111,97,.08)", color: "#ff6f61", cursor: "pointer", display: "flex", alignItems: "center", justifyContent: "center" }}><PanelRight aria-hidden size={19} /></button> <button type="button" aria-label="Open panel" title="Panel" onClick={() => setWorldPanelOpen(true)} style={{ position: "absolute", top: 14, right: 16, zIndex: 50, width: 38, height: 38, borderRadius: "50%", border: "1px solid rgba(255,111,97,.4)", background: "rgba(255,111,97,.08)", color: "#ff6f61", cursor: "pointer", display: "flex", alignItems: "center", justifyContent: "center" }}><PanelRight aria-hidden size={19} /></button>
+220 -34
View File
@@ -18,6 +18,8 @@ import { UnrealBloomPass } from "three/examples/jsm/postprocessing/UnrealBloomPa
import type { TaxonomyEvents } from "@/lib/live/taxonomy"; import type { TaxonomyEvents } from "@/lib/live/taxonomy";
import { useClawmatesLive } from "@/lib/live/useClawmatesLive"; import { useClawmatesLive } from "@/lib/live/useClawmatesLive";
import { WorldFlow, type WorldItem } from "../dashboard/flow/WorldFlow";
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> };
@@ -67,6 +69,27 @@ 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;
@@ -84,7 +107,7 @@ const FORMATIONS: { id: Formation; label: string }[] = [
const mono = "'Geist Mono', ui-monospace, monospace"; const mono = "'Geist Mono', ui-monospace, monospace";
export function WorldCanvas({ roots, selectedId, onSelect }: WorldCanvasProps) { export function WorldCanvas({ roots, selectedId, onSelect, expanded, onToggleExpand, onOpenRuns }: WorldCanvasProps) {
const mountRef = useRef<HTMLDivElement>(null); const mountRef = useRef<HTMLDivElement>(null);
const labelLayerRef = useRef<HTMLDivElement>(null); const labelLayerRef = useRef<HTMLDivElement>(null);
const live = useClawmatesLive(); const live = useClawmatesLive();
@@ -197,16 +220,24 @@ export function WorldCanvas({ roots, selectedId, onSelect }: WorldCanvasProps) {
const glow = makeGlowTexture(); const glow = makeGlowTexture();
const circle = new THREE.CircleGeometry(1, 28); const circle = new THREE.CircleGeometry(1, 28);
// Two render groups so the formation tabs switch whole views cleanly.
const gourceGroup = new THREE.Group();
const brainGroup = new THREE.Group();
brainGroup.visible = false;
scene.add(gourceGroup, brainGroup);
const nodeMeshes = new Map<string, THREE.Mesh>(); const nodeMeshes = new Map<string, THREE.Mesh>();
const nodeGlows = new Map<string, THREE.Sprite>(); const nodeGlows = new Map<string, THREE.Sprite>();
const pawnMeshes = new Map<string, THREE.Mesh>(); const pawnMeshes = new Map<string, THREE.Mesh>();
const pawnGlows = new Map<string, THREE.Sprite>(); const pawnGlows = new Map<string, THREE.Sprite>();
const neuronGlows = new Map<string, THREE.Sprite>();
const labels = new Map<string, HTMLSpanElement>(); const labels = new Map<string, HTMLSpanElement>();
const newGlow = () => { const newGlow = (group: THREE.Object3D) => {
const s = new THREE.Sprite( const s = new THREE.Sprite(
new THREE.SpriteMaterial({ map: glow, blending: THREE.AdditiveBlending, transparent: true, depthWrite: false, depthTest: false }), new THREE.SpriteMaterial({ map: glow, blending: THREE.AdditiveBlending, transparent: true, depthWrite: false, depthTest: false }),
); );
scene.add(s); group.add(s);
return s; return s;
}; };
@@ -215,14 +246,26 @@ export function WorldCanvas({ roots, selectedId, onSelect }: WorldCanvasProps) {
edgeGeom, edgeGeom,
new THREE.LineBasicMaterial({ color: 0xffffff, transparent: true, opacity: 0.1, depthWrite: false }), new THREE.LineBasicMaterial({ color: 0xffffff, transparent: true, opacity: 0.1, depthWrite: false }),
); );
scene.add(edges); gourceGroup.add(edges);
const beamGeom = new THREE.BufferGeometry(); const beamGeom = new THREE.BufferGeometry();
const beams = new THREE.LineSegments( const beams = new THREE.LineSegments(
beamGeom, beamGeom,
new THREE.LineBasicMaterial({ vertexColors: true, transparent: true, blending: THREE.AdditiveBlending, depthWrite: false }), new THREE.LineBasicMaterial({ vertexColors: true, transparent: true, blending: THREE.AdditiveBlending, depthWrite: false }),
); );
scene.add(beams); gourceGroup.add(beams);
// brain (hierarchy) — pathway edges between agent-neurons
const brainEdgeGeom = new THREE.BufferGeometry();
const brainEdges = new THREE.LineSegments(
brainEdgeGeom,
new THREE.LineBasicMaterial({ color: 0x6fb6ff, transparent: true, opacity: 0.14, blending: THREE.AdditiveBlending, depthWrite: false }),
);
brainGroup.add(brainEdges);
const brainPositions = new Map<string, THREE.Vector3>();
const neuronAct = new Map<string, number>();
const brainAdj = new Map<string, string[]>();
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;
@@ -242,16 +285,25 @@ export function WorldCanvas({ roots, selectedId, onSelect }: WorldCanvasProps) {
); );
scene.add(particles); scene.add(particles);
const tmp = new THREE.Color(); const tmp = new THREE.Color();
const spawn = (x: number, y: number, hex: string, vx: number, vy: number, decay: number) => { const spawn = (
x: number,
y: number,
z: number,
hex: string,
vx: number,
vy: number,
vz: number,
decay: number,
) => {
const i = pCur % MAXP; const i = pCur % MAXP;
pCur += 1; pCur += 1;
tmp.set(hex); tmp.set(hex);
pPos[i * 3] = x; pPos[i * 3] = x;
pPos[i * 3 + 1] = y; pPos[i * 3 + 1] = y;
pPos[i * 3 + 2] = 0; pPos[i * 3 + 2] = z;
pVel[i * 3] = vx; pVel[i * 3] = vx;
pVel[i * 3 + 1] = vy; pVel[i * 3 + 1] = vy;
pVel[i * 3 + 2] = 0; pVel[i * 3 + 2] = vz;
pBase[i * 3] = tmp.r; pBase[i * 3] = tmp.r;
pBase[i * 3 + 1] = tmp.g; pBase[i * 3 + 1] = tmp.g;
pBase[i * 3 + 2] = tmp.b; pBase[i * 3 + 2] = tmp.b;
@@ -300,6 +352,99 @@ export function WorldCanvas({ roots, selectedId, onSelect }: WorldCanvasProps) {
renderer.domElement.addEventListener("pointerdown", onDown); renderer.domElement.addEventListener("pointerdown", onDown);
renderer.domElement.addEventListener("pointerup", onUp); renderer.domElement.addEventListener("pointerup", onUp);
const updateParticles = (dt: number) => {
for (let i = 0; i < MAXP; i++) {
if (pLife[i] > 0) {
pLife[i] = Math.max(0, pLife[i] - dt * pDecay[i]);
pPos[i * 3] += pVel[i * 3] * dt;
pPos[i * 3 + 1] += pVel[i * 3 + 1] * dt;
pPos[i * 3 + 2] += pVel[i * 3 + 2] * dt;
const l = pLife[i];
pCol[i * 3] = pBase[i * 3] * l;
pCol[i * 3 + 1] = pBase[i * 3 + 1] * l;
pCol[i * 3 + 2] = pBase[i * 3 + 2] * l;
} else {
pCol[i * 3] = pCol[i * 3 + 1] = pCol[i * 3 + 2] = 0;
}
}
particleGeom.attributes.position.needsUpdate = true;
particleGeom.attributes.color.needsUpdate = true;
};
// Brain pathway graph: connect each neuron to its 3 nearest (rebuilt on count change).
const buildBrainGraph = (ids: string[]) => {
brainAdj.clear();
const pos = ids.map((id) => brainPositions.get(id)!);
const segs: number[] = [];
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);
}
brainAdj.set(ids[i], nbrs);
}
brainEdgeGeom.setAttribute("position", new THREE.Float32BufferAttribute(segs, 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 ids: string[] = [];
const seen = new Set<string>();
for (const p of engine.pawns.values()) {
ids.push(p.id);
seen.add(p.id);
if (!brainPositions.has(p.id)) brainPositions.set(p.id, neuronPos(p.id));
const pos = brainPositions.get(p.id)!;
let g = neuronGlows.get(p.id);
if (!g) {
g = newGlow(brainGroup);
neuronGlows.set(p.id, g);
}
let act = neuronAct.get(p.id) ?? 0.06;
if (p.status === "working") act = Math.min(1, act + dt * 0.9);
act = Math.max(0.06, act - dt * 0.22);
neuronAct.set(p.id, act);
g.position.copy(pos);
const gs = 16 + act * 36;
g.scale.set(gs, gs, 1);
const gm = g.material as THREE.SpriteMaterial;
gm.color.set(p.color);
gm.opacity = 0.18 + act * 0.82;
const nbrs = brainAdj.get(p.id);
if (nbrs && nbrs.length && Math.random() < act * dt * 7) {
const tid = nbrs[(Math.random() * nbrs.length) | 0];
const tp = brainPositions.get(tid);
if (tp) {
const dx = tp.x - pos.x;
const dy = tp.y - pos.y;
const dz = tp.z - pos.z;
const len = Math.hypot(dx, dy, dz) || 1;
const speed = 150;
spawn(pos.x, pos.y, pos.z, p.color, (dx / len) * speed, (dy / len) * speed, (dz / len) * speed, speed / len + 0.25);
neuronAct.set(tid, Math.min(1, (neuronAct.get(tid) ?? 0.06) + 0.12));
}
}
}
for (const [id, g] of neuronGlows) {
if (!seen.has(id)) {
brainGroup.remove(g);
(g.material as THREE.Material).dispose();
neuronGlows.delete(id);
}
}
if (ids.length !== brainCount) {
buildBrainGraph(ids);
brainCount = ids.length;
}
};
const col = new THREE.Color(); const col = new THREE.Color();
const white = new THREE.Color(0xffffff); const white = new THREE.Color(0xffffff);
let raf = 0; let raf = 0;
@@ -334,8 +479,32 @@ export function WorldCanvas({ roots, selectedId, onSelect }: WorldCanvasProps) {
engine.formation = formationRef.current; engine.formation = formationRef.current;
engine.step(dt); engine.step(dt);
const F = engine.formation; const F = engine.formation;
const showPawns = F !== "hierarchy";
const effects = F === "live"; gourceGroup.visible = F === "live";
brainGroup.visible = F === "hierarchy";
if (F === "flat") {
// the 2D React-Flow view (WorldFlow) is overlaid on top — skip GPU work
controls.update();
return;
}
if (F === "hierarchy") {
for (const [, s] of labels) s.remove();
labels.clear();
renderBrain(dt);
updateParticles(dt);
if (!userInteracted) {
controls.target.lerp(new THREE.Vector3(0, 0, 0), 0.05);
camera.position.lerp(new THREE.Vector3(0, 0, 520), 0.04);
}
controls.update();
bloom.strength = 0.75;
composer.render();
return;
}
const showPawns = true;
const effects = true;
let minX = Infinity; let minX = Infinity;
let minY = Infinity; let minY = Infinity;
@@ -351,7 +520,7 @@ export function WorldCanvas({ roots, selectedId, onSelect }: WorldCanvasProps) {
m = new THREE.Mesh(circle, new THREE.MeshBasicMaterial({ transparent: true, depthWrite: false })); m = new THREE.Mesh(circle, new THREE.MeshBasicMaterial({ transparent: true, depthWrite: false }));
m.userData.id = n.id; m.userData.id = n.id;
nodeMeshes.set(n.id, m); nodeMeshes.set(n.id, m);
scene.add(m); gourceGroup.add(m);
} }
const r = n.r * (1 + n.heat * 0.4); const r = n.r * (1 + n.heat * 0.4);
m.position.set(n.x, n.y, 0); m.position.set(n.x, n.y, 0);
@@ -362,7 +531,7 @@ export function WorldCanvas({ roots, selectedId, onSelect }: WorldCanvasProps) {
mat.opacity = n.alpha * (n.tier === "root" ? 1 : 0.6 + n.heat * 0.4); mat.opacity = n.alpha * (n.tier === "root" ? 1 : 0.6 + n.heat * 0.4);
let g = nodeGlows.get(n.id); let g = nodeGlows.get(n.id);
if (!g) { if (!g) {
g = newGlow(); g = newGlow(gourceGroup);
nodeGlows.set(n.id, g); nodeGlows.set(n.id, g);
} }
const gs = r * (3.2 + n.heat * 3.5); const gs = r * (3.2 + n.heat * 3.5);
@@ -374,7 +543,17 @@ export function WorldCanvas({ roots, selectedId, onSelect }: WorldCanvasProps) {
if (effects && n.heat > 0.35 && Math.random() < n.heat * dt * 8) { if (effects && n.heat > 0.35 && Math.random() < n.heat * dt * 8) {
const a = Math.random() * Math.PI * 2; const a = Math.random() * Math.PI * 2;
const sp = 18 + Math.random() * 30; const sp = 18 + Math.random() * 30;
spawn(n.x, n.y, n.color, Math.cos(a) * sp, Math.sin(a) * sp, 1.7 + Math.random()); spawn(n.x, n.y, 0, n.color, Math.cos(a) * sp, Math.sin(a) * sp, 0, 1.7 + Math.random());
}
// explosive burst on a file/project op (touch weight → node.burst)
if (n.burst > 0) {
const count = Math.floor(8 + n.burst * 30);
for (let k = 0; k < count; k++) {
const a = Math.random() * Math.PI * 2;
const sp = 55 + Math.random() * 180 * n.burst;
spawn(n.x, n.y, 0, n.color, Math.cos(a) * sp, Math.sin(a) * sp, 0, 1.1 + Math.random());
}
n.burst = 0;
} }
if (n.tier !== "root") { if (n.tier !== "root") {
minX = Math.min(minX, n.x - r); minX = Math.min(minX, n.x - r);
@@ -408,7 +587,7 @@ export function WorldCanvas({ roots, selectedId, onSelect }: WorldCanvasProps) {
edgeGeom.setAttribute("position", new THREE.Float32BufferAttribute(epos, 3)); edgeGeom.setAttribute("position", new THREE.Float32BufferAttribute(epos, 3));
edgeGeom.attributes.position.needsUpdate = true; edgeGeom.attributes.position.needsUpdate = true;
edgeGeom.setDrawRange(0, epos.length / 3); edgeGeom.setDrawRange(0, epos.length / 3);
(edges.material as THREE.LineBasicMaterial).opacity = F === "flat" ? 0.04 : 0.1; (edges.material as THREE.LineBasicMaterial).opacity = 0.1;
// pawns (solid + glow + trail particles) // pawns (solid + glow + trail particles)
const livePawnIds = new Set<string>(); const livePawnIds = new Set<string>();
@@ -419,7 +598,7 @@ export function WorldCanvas({ roots, selectedId, onSelect }: WorldCanvasProps) {
m = new THREE.Mesh(circle, new THREE.MeshBasicMaterial({ transparent: true, depthWrite: false })); m = new THREE.Mesh(circle, new THREE.MeshBasicMaterial({ transparent: true, depthWrite: false }));
m.userData.id = p.id; m.userData.id = p.id;
pawnMeshes.set(p.id, m); pawnMeshes.set(p.id, m);
scene.add(m); gourceGroup.add(m);
} }
const pa = p.alpha * (showPawns ? 1 : 0.5); const pa = p.alpha * (showPawns ? 1 : 0.5);
m.position.set(p.x, p.y, 2); m.position.set(p.x, p.y, 2);
@@ -429,7 +608,7 @@ export function WorldCanvas({ roots, selectedId, onSelect }: WorldCanvasProps) {
mat.opacity = pa; mat.opacity = pa;
let g = pawnGlows.get(p.id); let g = pawnGlows.get(p.id);
if (!g) { if (!g) {
g = newGlow(); g = newGlow(gourceGroup);
pawnGlows.set(p.id, g); pawnGlows.set(p.id, g);
} }
g.position.set(p.x, p.y, 1); g.position.set(p.x, p.y, 1);
@@ -437,7 +616,7 @@ export function WorldCanvas({ roots, selectedId, onSelect }: WorldCanvasProps) {
const gm = g.material as THREE.SpriteMaterial; const gm = g.material as THREE.SpriteMaterial;
gm.color.set(p.color); gm.color.set(p.color);
gm.opacity = 0.55 * pa; gm.opacity = 0.55 * pa;
if (showPawns && frameN % 2 === 0) spawn(p.x, p.y, p.color, 0, 0, 3.0); if (showPawns && frameN % 2 === 0) spawn(p.x, p.y, 0, p.color, 0, 0, 0, 3.0);
minX = Math.min(minX, p.x - 8); minX = Math.min(minX, p.x - 8);
minY = Math.min(minY, p.y - 8); minY = Math.min(minY, p.y - 8);
maxX = Math.max(maxX, p.x + 8); maxX = Math.max(maxX, p.x + 8);
@@ -470,7 +649,7 @@ export function WorldCanvas({ roots, selectedId, onSelect }: WorldCanvasProps) {
for (let k = 0; k < 7; k++) { for (let k = 0; k < 7; k++) {
const a = Math.random() * Math.PI * 2; const a = Math.random() * Math.PI * 2;
const sp = 25 + Math.random() * 45; const sp = 25 + Math.random() * 45;
spawn(b.x2, b.y2, b.color, Math.cos(a) * sp, Math.sin(a) * sp, 1.6 + Math.random()); spawn(b.x2, b.y2, 0, b.color, Math.cos(a) * sp, Math.sin(a) * sp, 0, 1.6 + Math.random());
} }
} }
} }
@@ -479,22 +658,7 @@ export function WorldCanvas({ roots, selectedId, onSelect }: WorldCanvasProps) {
beamGeom.attributes.position.needsUpdate = true; beamGeom.attributes.position.needsUpdate = true;
beamGeom.setDrawRange(0, bpos.length / 3); beamGeom.setDrawRange(0, bpos.length / 3);
// particle integrate + fade (additive: RGB→0 = invisible) updateParticles(dt);
for (let i = 0; i < MAXP; i++) {
if (pLife[i] > 0) {
pLife[i] = Math.max(0, pLife[i] - dt * pDecay[i]);
pPos[i * 3] += pVel[i * 3] * dt;
pPos[i * 3 + 1] += pVel[i * 3 + 1] * dt;
const l = pLife[i];
pCol[i * 3] = pBase[i * 3] * l;
pCol[i * 3 + 1] = pBase[i * 3 + 1] * l;
pCol[i * 3 + 2] = pBase[i * 3 + 2] * l;
} else {
pCol[i * 3] = pCol[i * 3 + 1] = pCol[i * 3 + 2] = 0;
}
}
particleGeom.attributes.position.needsUpdate = true;
particleGeom.attributes.color.needsUpdate = true;
// camera: auto-frame until the user grabs it (then OrbitControls owns it) // camera: auto-frame until the user grabs it (then OrbitControls owns it)
if (!userInteracted && minX < maxX) { if (!userInteracted && minX < maxX) {
@@ -561,9 +725,11 @@ export function WorldCanvas({ roots, selectedId, onSelect }: WorldCanvasProps) {
nodeGlows.forEach((s) => (s.material as THREE.Material).dispose()); nodeGlows.forEach((s) => (s.material as THREE.Material).dispose());
pawnMeshes.forEach((m) => (m.material as THREE.Material).dispose()); pawnMeshes.forEach((m) => (m.material as THREE.Material).dispose());
pawnGlows.forEach((s) => (s.material as THREE.Material).dispose()); pawnGlows.forEach((s) => (s.material as THREE.Material).dispose());
neuronGlows.forEach((s) => (s.material as THREE.Material).dispose());
circle.dispose(); circle.dispose();
edgeGeom.dispose(); edgeGeom.dispose();
beamGeom.dispose(); beamGeom.dispose();
brainEdgeGeom.dispose();
particleGeom.dispose(); particleGeom.dispose();
glow.dispose(); glow.dispose();
composer.dispose(); composer.dispose();
@@ -602,6 +768,26 @@ export function WorldCanvas({ roots, selectedId, onSelect }: WorldCanvasProps) {
<div style={{ position: "absolute", inset: 0 }}> <div style={{ position: "absolute", inset: 0 }}>
<div ref={mountRef} style={{ position: "absolute", inset: 0 }} /> <div ref={mountRef} style={{ position: "absolute", inset: 0 }} />
<div ref={labelLayerRef} style={{ position: "absolute", inset: 0, overflow: "hidden", pointerEvents: "none" }} /> <div ref={labelLayerRef} style={{ position: "absolute", inset: 0, overflow: "hidden", pointerEvents: "none" }} />
{/* Flat = the 2D React-Flow view (overlaid; covers the paused canvas) */}
{formation === "flat" ? (
<div
style={{
position: "absolute",
inset: 0,
zIndex: 10,
background: "radial-gradient(120% 90% at 55% 38%, #0e0e13 0%, #08080a 70%)",
}}
>
<WorldFlow
roots={roots as WorldItem[]}
expanded={expanded ?? new Set<string>()}
selectedId={selectedId}
onToggleExpand={onToggleExpand ?? (() => {})}
onSelect={onSelect}
onOpenRuns={onOpenRuns}
/>
</div>
) : null}
{/* Formation switch */} {/* Formation switch */}
<div <div
style={{ style={{
+5
View File
@@ -24,6 +24,7 @@ export interface GNode {
r: number; r: number;
color: string; color: string;
heat: number; // 0..1, decays — drives glow/pulse heat: number; // 0..1, decays — drives glow/pulse
burst: number; // one-shot spark-burst magnitude (file ops = explosive)
alpha: number; // fade in/out (world nodes) alpha: number; // fade in/out (world nodes)
lastSeen: number; lastSeen: number;
fixed?: boolean; fixed?: boolean;
@@ -97,6 +98,7 @@ export class WorldEngine {
r: 16, r: 16,
color: LEVEL_COLOR.root, color: LEVEL_COLOR.root,
heat: 0, heat: 0,
burst: 0,
alpha: 1, alpha: 1,
lastSeen: 0, lastSeen: 0,
fixed: true, fixed: true,
@@ -136,6 +138,7 @@ export class WorldEngine {
r: tier === "org" ? 15 : tier === "company" ? 12 : tier === "team" ? 9 : 7, r: tier === "org" ? 15 : tier === "company" ? 12 : tier === "team" ? 9 : 7,
color: LEVEL_COLOR[tier] ?? "#9a9aa2", color: LEVEL_COLOR[tier] ?? "#9a9aa2",
heat: 0, heat: 0,
burst: 0,
alpha: 1, alpha: 1,
lastSeen: this.now, lastSeen: this.now,
}; };
@@ -199,7 +202,9 @@ export class WorldEngine {
p.targetId = e.nodeId; p.targetId = e.nodeId;
p.idle = 0; p.idle = 0;
p.retime = 1.2 + Math.random() * 1.6; p.retime = 1.2 + Math.random() * 1.6;
const w = e.weight ?? 0.5;
node.heat = Math.min(1, node.heat + 0.5); node.heat = Math.min(1, node.heat + 0.5);
node.burst = Math.max(node.burst, w); // file ops (weight 1) → explosive burst
} }
onNodeActivity(e: TaxonomyEvents["node.activity"]) { onNodeActivity(e: TaxonomyEvents["node.activity"]) {
const node = this.ensureNode(e.nodeId, e.kind === "event" ? "event" : "service", e.label ?? e.nodeId, ROOT, 1); const node = this.ensureNode(e.nodeId, e.kind === "event" ? "event" : "service", e.label ?? e.nodeId, ROOT, 1);