forked from Rockachopa/the-matrix
3D visualization for AI agent swarms built with Three.js. Matrix green/noir cyberpunk aesthetic. - 4 agents: Timmy (orchestrator), Forge (builder), Seer (planner), Echo (comms) - Central core pillar, animated green grid, digital rain - Agent info panels, chat, task list, memory views - WebSocket protocol for real-time state updates - iPad-ready: touch controls, add-to-homescreen - Post-processing: bloom, scanlines, vignette - No build step — pure ES modules via esm.sh CDN Created with Perplexity Computer
148 lines
3.7 KiB
JavaScript
148 lines
3.7 KiB
JavaScript
// ===== Interaction: Raycasting, selection, camera control =====
|
|
import * as THREE from 'three';
|
|
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
|
|
|
|
export class InteractionManager {
|
|
constructor(camera, renderer, agents, ui) {
|
|
this.camera = camera;
|
|
this.renderer = renderer;
|
|
this.agents = agents;
|
|
this.ui = ui;
|
|
|
|
this.raycaster = new THREE.Raycaster();
|
|
this.pointer = new THREE.Vector2();
|
|
this.selectedAgent = null;
|
|
this.interactables = [];
|
|
this.coreObjects = [];
|
|
|
|
this._setupControls();
|
|
this._bindEvents();
|
|
}
|
|
|
|
_setupControls() {
|
|
this.controls = new OrbitControls(this.camera, this.renderer.domElement);
|
|
this.controls.enableDamping = true;
|
|
this.controls.dampingFactor = 0.08;
|
|
this.controls.rotateSpeed = 0.5;
|
|
this.controls.panSpeed = 0.5;
|
|
this.controls.zoomSpeed = 0.8;
|
|
|
|
// Limits
|
|
this.controls.minDistance = 10;
|
|
this.controls.maxDistance = 80;
|
|
this.controls.minPolarAngle = 0.2;
|
|
this.controls.maxPolarAngle = Math.PI / 2.1;
|
|
|
|
// Target center
|
|
this.controls.target.set(0, 3, 0);
|
|
|
|
// Touch config
|
|
this.controls.touches = {
|
|
ONE: THREE.TOUCH.ROTATE,
|
|
TWO: THREE.TOUCH.DOLLY_PAN,
|
|
};
|
|
}
|
|
|
|
setInteractables(interactables) {
|
|
this.interactables = interactables;
|
|
}
|
|
|
|
setCoreObjects(coreObjects) {
|
|
this.coreObjects = coreObjects;
|
|
}
|
|
|
|
_bindEvents() {
|
|
const canvas = this.renderer.domElement;
|
|
|
|
// Click / tap
|
|
let pointerDownTime = 0;
|
|
let pointerDownPos = { x: 0, y: 0 };
|
|
|
|
canvas.addEventListener('pointerdown', (e) => {
|
|
pointerDownTime = Date.now();
|
|
pointerDownPos = { x: e.clientX, y: e.clientY };
|
|
});
|
|
|
|
canvas.addEventListener('pointerup', (e) => {
|
|
const elapsed = Date.now() - pointerDownTime;
|
|
const dx = e.clientX - pointerDownPos.x;
|
|
const dy = e.clientY - pointerDownPos.y;
|
|
const dist = Math.sqrt(dx * dx + dy * dy);
|
|
|
|
// Short tap with minimal movement = click
|
|
if (elapsed < 400 && dist < 15) {
|
|
this._handleTap(e);
|
|
}
|
|
});
|
|
}
|
|
|
|
_handleTap(event) {
|
|
const rect = this.renderer.domElement.getBoundingClientRect();
|
|
this.pointer.x = ((event.clientX - rect.left) / rect.width) * 2 - 1;
|
|
this.pointer.y = -((event.clientY - rect.top) / rect.height) * 2 + 1;
|
|
|
|
this.raycaster.setFromCamera(this.pointer, this.camera);
|
|
|
|
// Check core first
|
|
const coreHits = this.raycaster.intersectObjects(this.coreObjects, true);
|
|
if (coreHits.length > 0) {
|
|
this._deselectAgent();
|
|
this.ui.selectCore();
|
|
return;
|
|
}
|
|
|
|
// Check agents
|
|
const hits = this.raycaster.intersectObjects(this.interactables, true);
|
|
if (hits.length > 0) {
|
|
// Walk up to find agentId
|
|
let agentId = null;
|
|
let obj = hits[0].object;
|
|
while (obj) {
|
|
if (obj.userData && obj.userData.agentId) {
|
|
agentId = obj.userData.agentId;
|
|
break;
|
|
}
|
|
obj = obj.parent;
|
|
}
|
|
|
|
if (agentId) {
|
|
this._selectAgent(agentId);
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Tap empty space — close panels
|
|
if (this.ui.isPanelOpen()) {
|
|
this._deselectAgent();
|
|
this.ui.closePanel();
|
|
this.ui.closeSystemPanel();
|
|
}
|
|
}
|
|
|
|
_selectAgent(agentId) {
|
|
if (this.selectedAgent === agentId) return;
|
|
|
|
// Deselect previous
|
|
this._deselectAgent();
|
|
|
|
this.selectedAgent = agentId;
|
|
this.agents.highlightAgent(agentId, true);
|
|
this.ui.selectAgent(agentId);
|
|
}
|
|
|
|
_deselectAgent() {
|
|
if (this.selectedAgent) {
|
|
this.agents.highlightAgent(this.selectedAgent, false);
|
|
this.selectedAgent = null;
|
|
}
|
|
}
|
|
|
|
update(delta) {
|
|
this.controls.update();
|
|
}
|
|
|
|
dispose() {
|
|
this.controls.dispose();
|
|
}
|
|
}
|