World: explosive file sparks + 3 real views (Flat 2D / Live Gource / Brain)
- 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:
co-authored by
Claude Opus 4.8
parent
da20417fda
commit
3b012b12bf
@@ -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>
|
||||||
|
|||||||
@@ -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={{
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
Reference in New Issue
Block a user