← back to Dw War Room
src/showroom/Showroom.ts
207 lines
// Showroom.ts — Main photorealistic showroom compositor
// Ties together: FigmaTransformer + Materials + Environment + Layout + PostProcess
import * as THREE from 'three';
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
import { createMaterialLibrary, disposeMaterialLibrary } from './ShowroomMaterials';
import { createShowroomEnvMap } from './ShowroomEnvironment';
import { ShowroomLayout } from './ShowroomLayout';
import { ShowroomPostProcess } from './ShowroomPostProcess';
import { transformFigmaToScene } from './FigmaTransformer';
import type { SceneConfig } from './types';
import type { MaterialLibrary } from './ShowroomMaterials';
export class Showroom {
scene: THREE.Scene;
camera: THREE.PerspectiveCamera;
renderer: THREE.WebGLRenderer;
controls: OrbitControls;
private clock: THREE.Clock;
private _elapsed = 0;
private _running = false;
private _resizeHandler: (() => void) | null = null;
private materials: MaterialLibrary;
private layout: ShowroomLayout;
private postProcess: ShowroomPostProcess;
private dustParticles: THREE.Points;
constructor(container: HTMLElement) {
// Scene
this.scene = new THREE.Scene();
this.scene.background = new THREE.Color(0x0d0c0a);
this.scene.fog = new THREE.Fog(0x0d0c0a, 20, 60);
// Camera — human eye level
this.camera = new THREE.PerspectiveCamera(
55, container.clientWidth / container.clientHeight, 0.1, 100
);
this.camera.position.set(0, 1.7, 8);
this.camera.lookAt(0, 1.5, 0);
// Renderer — photorealistic pipeline
this.renderer = new THREE.WebGLRenderer({ antialias: 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.4;
this.renderer.outputColorSpace = THREE.SRGBColorSpace;
this.renderer.shadowMap.enabled = true;
this.renderer.shadowMap.type = THREE.PCFSoftShadowMap;
container.appendChild(this.renderer.domElement);
// Controls — walking speed
this.controls = new OrbitControls(this.camera, this.renderer.domElement);
this.controls.enableDamping = true;
this.controls.dampingFactor = 0.08;
this.controls.maxPolarAngle = Math.PI / 2;
this.controls.minDistance = 1;
this.controls.maxDistance = 50;
// Clock
this.clock = new THREE.Clock();
// Materials
this.materials = createMaterialLibrary();
// Environment map (warm gallery IBL)
this.scene.environment = createShowroomEnvMap(this.renderer);
// Ambient + key lighting
this.addBaseLighting();
// Layout builder
this.layout = new ShowroomLayout(this.scene, this.materials);
// Atmospheric dust particles
this.dustParticles = this.createDustMotes();
this.scene.add(this.dustParticles);
// Post-processing
this.postProcess = new ShowroomPostProcess(this.renderer, this.scene, this.camera);
// Resize — store reference for cleanup
this._resizeHandler = () => this.onResize(container);
window.addEventListener('resize', this._resizeHandler);
}
/**
* Load showroom from Figma design data.
*/
loadFromFigma(figmaData: any): void {
const config = transformFigmaToScene(figmaData);
this.layout.buildFromConfig(config);
}
/**
* Load showroom from a pre-built SceneConfig.
*/
loadFromConfig(config: SceneConfig): void {
this.layout.buildFromConfig(config);
}
private addBaseLighting(): void {
// Ambient — warm neutral
const ambient = new THREE.AmbientLight(0x2a2520, 0.4);
this.scene.add(ambient);
// Hemisphere — sky/ground color contrast
const hemi = new THREE.HemisphereLight(0xf5e6d0, 0x1a1510, 0.3);
this.scene.add(hemi);
// Main key light — warm directional
const key = new THREE.DirectionalLight(0xf5e6d0, 0.8);
key.position.set(5, 15, 10);
key.castShadow = true;
key.shadow.mapSize.set(2048, 2048);
key.shadow.camera.near = 0.5;
key.shadow.camera.far = 50;
key.shadow.camera.left = -25;
key.shadow.camera.right = 25;
key.shadow.camera.top = 15;
key.shadow.camera.bottom = -15;
key.shadow.bias = -0.0003;
this.scene.add(key);
}
private createDustMotes(): THREE.Points {
const count = 200;
const positions = new Float32Array(count * 3);
for (let i = 0; i < count * 3; i += 3) {
positions[i] = (Math.random() - 0.5) * 40;
positions[i + 1] = Math.random() * 12;
positions[i + 2] = (Math.random() - 0.5) * 20;
}
const geo = new THREE.BufferGeometry();
geo.setAttribute('position', new THREE.BufferAttribute(positions, 3));
const mat = new THREE.PointsMaterial({
color: 0xf5e6d0,
size: 0.02,
transparent: true,
opacity: 0.3,
blending: THREE.AdditiveBlending,
depthWrite: false,
});
return new THREE.Points(geo, mat);
}
animate(): void {
if (!this._running) return;
requestAnimationFrame(() => this.animate());
// Use only getDelta() — getElapsedTime() internally calls getDelta() again,
// which double-advances the clock and yields a near-zero second delta
const dt = Math.min(this.clock.getDelta(), 0.1);
this._elapsed += dt;
const elapsed = this._elapsed;
this.controls.update();
// Animate dust motes
const dustPos = this.dustParticles.geometry.attributes.position;
for (let i = 0; i < dustPos.count; i++) {
const y = dustPos.getY(i) + Math.sin(elapsed * 0.3 + i) * 0.001;
dustPos.setY(i, y > 12 ? 0 : y);
}
dustPos.needsUpdate = true;
// Update wing visibility based on camera distance
this.layout.updateVisibility(this.camera.position);
// Update DOF focus to orbit target distance
const focusDist = this.camera.position.distanceTo(this.controls.target);
this.postProcess.setFocusDistance(focusDist);
// Render through post-processing pipeline
this.postProcess.render();
}
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.postProcess.resize(w, h);
}
start(): void {
this._running = true;
this.clock.start();
this.animate();
}
dispose(): void {
this._running = false;
if (this._resizeHandler) {
window.removeEventListener('resize', this._resizeHandler);
this._resizeHandler = null;
}
this.postProcess.dispose();
this.layout.dispose();
disposeMaterialLibrary(this.materials);
this.dustParticles.geometry.dispose();
(this.dustParticles.material as THREE.PointsMaterial).dispose();
this.controls.dispose();
this.renderer.dispose();
}
}