← back to Dw War Room
src/scene/WarRoom.ts
199 lines
// ═══════════════════════════════════════════════
// DW War Room 3D — Main Scene Composition
// Assembles all scene components + raycasting
// ═══════════════════════════════════════════════
import * as THREE from 'three';
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
import { createLighting, pulseEscalationLight } from './Lighting';
import { createCommandTable, CommandTableHandle } from './CommandTable';
import { AgentNodeManager } from './AgentNodes';
import { DataWalls } from './DataWalls';
import { PostProcessingPipeline } from '../effects/PostProcessing';
import { ParticleSystem } from '../effects/Particles';
import { createEnvironment } from './Environment';
import type { AgentNode } from '../types';
export class WarRoom {
scene: THREE.Scene;
camera: THREE.PerspectiveCamera;
renderer: THREE.WebGLRenderer;
controls: OrbitControls;
agentNodes: AgentNodeManager;
dataWalls: DataWalls;
private clock: THREE.Clock;
private postProcessing: PostProcessingPipeline;
private particles: ParticleSystem;
private commandTable: CommandTableHandle;
private raycaster: THREE.Raycaster;
private mouse: THREE.Vector2;
// Callback for agent click
onAgentClick: ((agent: AgentNode, screenX: number, screenY: number) => void) | null = null;
constructor(container: HTMLElement) {
// Scene
this.scene = new THREE.Scene();
this.scene.background = new THREE.Color(0x050510);
this.scene.fog = new THREE.Fog(0x050510, 25, 80);
// Camera
this.camera = new THREE.PerspectiveCamera(60, container.clientWidth / container.clientHeight, 0.1, 100);
this.camera.position.set(0, 12, 18);
this.camera.lookAt(0, 2, 0);
// Renderer — physically-based rendering pipeline
this.renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
this.renderer.setSize(container.clientWidth, container.clientHeight);
this.renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
this.renderer.toneMapping = THREE.ACESFilmicToneMapping;
this.renderer.toneMappingExposure = 1.2;
this.renderer.outputColorSpace = THREE.SRGBColorSpace;
this.renderer.useLegacyLights = false;
this.renderer.shadowMap.enabled = true;
this.renderer.shadowMap.type = THREE.PCFSoftShadowMap;
container.appendChild(this.renderer.domElement);
// Controls
this.controls = new OrbitControls(this.camera, this.renderer.domElement);
this.controls.enableDamping = true;
this.controls.dampingFactor = 0.05;
this.controls.minDistance = 5;
this.controls.maxDistance = 40;
this.controls.maxPolarAngle = Math.PI / 2.1;
// Clock
this.clock = new THREE.Clock();
// Raycasting
this.raycaster = new THREE.Raycaster();
this.mouse = new THREE.Vector2();
// Build scene
createLighting(this.scene);
this.commandTable = createCommandTable(this.scene);
this.agentNodes = new AgentNodeManager(this.scene);
this.dataWalls = new DataWalls(this.scene);
createEnvironment(this.scene);
this.addEnvMap();
this.addStarfield();
// Particle system
this.particles = new ParticleSystem(this.scene);
// Post-processing
this.postProcessing = new PostProcessingPipeline(this.renderer, this.scene, this.camera);
// Resize handling
window.addEventListener('resize', () => this.onResize(container));
// Click handler for agent raycasting
this.renderer.domElement.addEventListener('click', (e) => this.handleClick(e));
}
private addEnvMap(): void {
// Generate a neutral PMREM environment from a simple gradient scene
// Gives all MeshStandardMaterial surfaces ambient IBL reflections
const pmrem = new THREE.PMREMGenerator(this.renderer);
pmrem.compileEquirectangularShader();
// Build a tiny gradient sky scene to bake into the env map
const envScene = new THREE.Scene();
// Cool dark-blue top gradient, warm dark base — corporate command center tone
const skyGeo = new THREE.SphereGeometry(50, 16, 8);
const skyMat = new THREE.MeshBasicMaterial({
color: 0x0a1520,
side: THREE.BackSide,
});
envScene.add(new THREE.Mesh(skyGeo, skyMat));
// Add a soft white highlight for IBL top reflection
const capGeo = new THREE.SphereGeometry(40, 8, 4, 0, Math.PI * 2, 0, Math.PI * 0.25);
const capMat = new THREE.MeshBasicMaterial({ color: 0x4488aa, side: THREE.BackSide });
const cap = new THREE.Mesh(capGeo, capMat);
cap.rotation.x = Math.PI;
envScene.add(cap);
const envRenderTarget = pmrem.fromScene(envScene as any, 0.04);
this.renderer.environment = envRenderTarget.texture;
pmrem.dispose();
skyMat.dispose();
capMat.dispose();
}
private addStarfield(): void {
const starGeo = new THREE.BufferGeometry();
const positions = new Float32Array(1500);
for (let i = 0; i < 1500; i += 3) {
positions[i] = (Math.random() - 0.5) * 80;
positions[i + 1] = Math.random() * 30 + 10;
positions[i + 2] = (Math.random() - 0.5) * 80;
}
starGeo.setAttribute('position', new THREE.BufferAttribute(positions, 3));
// Neutral cool-white stars — no neon purple
const starMat = new THREE.PointsMaterial({ color: 0xc8ddf0, size: 0.05, transparent: true, opacity: 0.5 });
const stars = new THREE.Points(starGeo, starMat);
this.scene.add(stars);
}
private handleClick(event: MouseEvent): void {
const rect = this.renderer.domElement.getBoundingClientRect();
this.mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1;
this.mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1;
this.raycaster.setFromCamera(this.mouse, this.camera);
const meshes = this.agentNodes.getMeshes();
const intersects = this.raycaster.intersectObjects(meshes);
if (intersects.length > 0) {
const agent = this.agentNodes.getNodeByMesh(intersects[0].object);
if (agent && this.onAgentClick) {
this.onAgentClick(agent, event.clientX, event.clientY);
}
}
}
setEscalationPulse(active: boolean): void {
const intensity = active ? 0.8 : 0.0;
pulseEscalationLight(this.scene, intensity);
this.dataWalls.pulsePanel('escalations', active ? 0.2 : 0.05);
}
animate(): void {
requestAnimationFrame(() => this.animate());
const elapsed = this.clock.getElapsedTime() * 1000;
this.controls.update();
this.commandTable.animate(elapsed);
this.agentNodes.animate(elapsed);
this.dataWalls.animate(elapsed);
this.particles.animate(elapsed);
this.postProcessing.render(this.scene, this.camera);
}
private onResize(container: HTMLElement): void {
const w = container.clientWidth;
const h = container.clientHeight;
this.camera.aspect = w / h;
this.camera.updateProjectionMatrix();
this.renderer.setSize(w, h);
this.postProcessing.resize(w, h);
}
// Preset camera views
viewOverview(): void {
this.camera.position.set(0, 12, 18);
this.camera.lookAt(0, 2, 0);
}
viewMeetingFocus(): void {
this.camera.position.set(0, 8, 10);
this.camera.lookAt(0, 3, 0);
}
viewAgentFocus(): void {
this.camera.position.set(5, 6, 8);
this.camera.lookAt(0, 3, 0);
}
}