238 lines
6.7 KiB
JavaScript
238 lines
6.7 KiB
JavaScript
import * as THREE from 'three';
|
|
import { AGENT_DEFS, colorToCss } from './agent-defs.js';
|
|
|
|
const agents = new Map();
|
|
let scene;
|
|
let connectionLines = [];
|
|
|
|
class Agent {
|
|
constructor(def) {
|
|
this.id = def.id;
|
|
this.label = def.label;
|
|
this.color = def.color;
|
|
this.role = def.role;
|
|
this.position = new THREE.Vector3(def.x, 0, def.z);
|
|
this.state = 'idle';
|
|
this.pulsePhase = Math.random() * Math.PI * 2;
|
|
|
|
this.group = new THREE.Group();
|
|
this.group.position.copy(this.position);
|
|
|
|
this._buildMeshes();
|
|
this._buildLabel();
|
|
}
|
|
|
|
_buildMeshes() {
|
|
const mat = new THREE.MeshStandardMaterial({
|
|
color: this.color,
|
|
emissive: this.color,
|
|
emissiveIntensity: 0.4,
|
|
roughness: 0.3,
|
|
metalness: 0.8,
|
|
});
|
|
|
|
const geo = new THREE.IcosahedronGeometry(0.7, 1);
|
|
this.core = new THREE.Mesh(geo, mat);
|
|
this.group.add(this.core);
|
|
|
|
const ringGeo = new THREE.TorusGeometry(1.1, 0.04, 8, 32);
|
|
const ringMat = new THREE.MeshBasicMaterial({ color: this.color, transparent: true, opacity: 0.5 });
|
|
this.ring = new THREE.Mesh(ringGeo, ringMat);
|
|
this.ring.rotation.x = Math.PI / 2;
|
|
this.group.add(this.ring);
|
|
|
|
const glowGeo = new THREE.SphereGeometry(1.3, 16, 16);
|
|
const glowMat = new THREE.MeshBasicMaterial({
|
|
color: this.color,
|
|
transparent: true,
|
|
opacity: 0.05,
|
|
side: THREE.BackSide,
|
|
});
|
|
this.glow = new THREE.Mesh(glowGeo, glowMat);
|
|
this.group.add(this.glow);
|
|
|
|
const light = new THREE.PointLight(this.color, 1.5, 10);
|
|
this.group.add(light);
|
|
this.light = light;
|
|
}
|
|
|
|
_buildLabel() {
|
|
const canvas = document.createElement('canvas');
|
|
canvas.width = 256; canvas.height = 64;
|
|
const ctx = canvas.getContext('2d');
|
|
ctx.fillStyle = 'rgba(0,0,0,0)';
|
|
ctx.fillRect(0, 0, 256, 64);
|
|
ctx.font = 'bold 22px Courier New';
|
|
ctx.fillStyle = colorToCss(this.color);
|
|
ctx.textAlign = 'center';
|
|
ctx.fillText(this.label, 128, 28);
|
|
ctx.font = '14px Courier New';
|
|
ctx.fillStyle = '#007722';
|
|
ctx.fillText(this.role.toUpperCase(), 128, 50);
|
|
|
|
const tex = new THREE.CanvasTexture(canvas);
|
|
const spriteMat = new THREE.SpriteMaterial({ map: tex, transparent: true });
|
|
this.sprite = new THREE.Sprite(spriteMat);
|
|
this.sprite.scale.set(2.4, 0.6, 1);
|
|
this.sprite.position.y = 2;
|
|
this.group.add(this.sprite);
|
|
}
|
|
|
|
update(time) {
|
|
const pulse = Math.sin(time * 0.002 + this.pulsePhase);
|
|
const pulse2 = Math.sin(time * 0.005 + this.pulsePhase * 1.3);
|
|
|
|
let intensity, lightIntensity, ringSpeed, glowOpacity, scaleAmp;
|
|
|
|
switch (this.state) {
|
|
case 'working':
|
|
intensity = 1.0 + pulse * 0.3;
|
|
lightIntensity = 3.5 + pulse2 * 0.8;
|
|
ringSpeed = 0.07;
|
|
glowOpacity = 0.18 + pulse * 0.08;
|
|
scaleAmp = 0.14;
|
|
break;
|
|
case 'thinking':
|
|
intensity = 0.7 + pulse2 * 0.5;
|
|
lightIntensity = 2.2 + pulse * 0.5;
|
|
ringSpeed = 0.045;
|
|
glowOpacity = 0.12 + pulse2 * 0.06;
|
|
scaleAmp = 0.10;
|
|
break;
|
|
case 'active':
|
|
intensity = 0.6 + pulse * 0.4;
|
|
lightIntensity = 2.0 + pulse;
|
|
ringSpeed = 0.03;
|
|
glowOpacity = 0.08 + pulse * 0.04;
|
|
scaleAmp = 0.08;
|
|
break;
|
|
default:
|
|
intensity = 0.2 + pulse * 0.1;
|
|
lightIntensity = 0.8 + pulse * 0.3;
|
|
ringSpeed = 0.008;
|
|
glowOpacity = 0.03 + pulse * 0.02;
|
|
scaleAmp = 0.03;
|
|
}
|
|
|
|
this.core.material.emissiveIntensity = intensity;
|
|
this.light.intensity = lightIntensity;
|
|
this.core.scale.setScalar(1 + pulse * scaleAmp);
|
|
this.ring.rotation.y += ringSpeed;
|
|
this.ring.material.opacity = 0.3 + pulse * 0.2;
|
|
this.glow.material.opacity = glowOpacity;
|
|
|
|
this.group.position.y = this.position.y + Math.sin(time * 0.001 + this.pulsePhase) * 0.15;
|
|
}
|
|
|
|
setState(state) {
|
|
this.state = state;
|
|
}
|
|
|
|
dispose() {
|
|
this.core.geometry.dispose();
|
|
this.core.material.dispose();
|
|
this.ring.geometry.dispose();
|
|
this.ring.material.dispose();
|
|
this.glow.geometry.dispose();
|
|
this.glow.material.dispose();
|
|
if (this.sprite.material.map) this.sprite.material.map.dispose();
|
|
this.sprite.material.dispose();
|
|
}
|
|
}
|
|
|
|
export function initAgents(sceneRef) {
|
|
scene = sceneRef;
|
|
|
|
AGENT_DEFS.forEach(def => {
|
|
const agent = new Agent(def);
|
|
agents.set(def.id, agent);
|
|
scene.add(agent.group);
|
|
});
|
|
|
|
buildConnectionLines();
|
|
}
|
|
|
|
function buildConnectionLines() {
|
|
connectionLines.forEach(l => scene.remove(l));
|
|
connectionLines = [];
|
|
|
|
const agentList = [...agents.values()];
|
|
const lineMat = new THREE.LineBasicMaterial({
|
|
color: 0x003300,
|
|
transparent: true,
|
|
opacity: 0.4,
|
|
});
|
|
|
|
for (let i = 0; i < agentList.length; i++) {
|
|
for (let j = i + 1; j < agentList.length; j++) {
|
|
const a = agentList[i];
|
|
const b = agentList[j];
|
|
if (a.position.distanceTo(b.position) <= 8) {
|
|
const points = [a.position.clone(), b.position.clone()];
|
|
const geo = new THREE.BufferGeometry().setFromPoints(points);
|
|
const line = new THREE.Line(geo, lineMat.clone());
|
|
connectionLines.push(line);
|
|
scene.add(line);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
export function updateAgents(time) {
|
|
agents.forEach(agent => agent.update(time));
|
|
}
|
|
|
|
export function getAgentCount() {
|
|
return agents.size;
|
|
}
|
|
|
|
export function setAgentState(agentId, state) {
|
|
const agent = agents.get(agentId);
|
|
if (agent) agent.setState(state);
|
|
}
|
|
|
|
export function getAgentDefs() {
|
|
return [...agents.values()].map(a => ({
|
|
id: a.id, label: a.label, role: a.role, color: a.color, state: a.state,
|
|
}));
|
|
}
|
|
|
|
/**
|
|
* Return a snapshot of each agent's current runtime state.
|
|
* Call before teardown so the state can be reapplied after reinit.
|
|
* @returns {Object.<string, string>} — e.g. { alpha: 'active', beta: 'idle' }
|
|
*/
|
|
export function getAgentStates() {
|
|
const snapshot = {};
|
|
agents.forEach((agent, id) => { snapshot[id] = agent.state; });
|
|
return snapshot;
|
|
}
|
|
|
|
/**
|
|
* Apply a previously captured state snapshot to freshly-created agents.
|
|
* Call immediately after initAgents() during context-restore reinit.
|
|
* @param {Object.<string, string>} snapshot
|
|
*/
|
|
export function applyAgentStates(snapshot) {
|
|
if (!snapshot) return;
|
|
for (const [id, state] of Object.entries(snapshot)) {
|
|
const agent = agents.get(id);
|
|
if (agent) agent.setState(state);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Dispose all agent GPU resources (geometries, materials, textures).
|
|
* Called before context-loss teardown.
|
|
*/
|
|
export function disposeAgents() {
|
|
agents.forEach(agent => agent.dispose());
|
|
agents.clear();
|
|
connectionLines.forEach(l => {
|
|
l.geometry.dispose();
|
|
l.material.dispose();
|
|
});
|
|
connectionLines = [];
|
|
scene = null;
|
|
}
|