← back to Dw War Room
src/showroom/ShowroomLayout.ts
344 lines
// ShowroomLayout.ts — Builds the photorealistic showroom corridor and wing bays
import * as THREE from 'three';
import type { SceneConfig, WingConfig, WallConfig, LightConfig } from './types';
import type { MaterialLibrary } from './ShowroomMaterials';
export class ShowroomLayout {
private scene: THREE.Scene;
private materials: MaterialLibrary;
private corridorGroup: THREE.Group;
private wingGroups: THREE.Group[] = [];
private spotLights: THREE.SpotLight[] = [];
constructor(scene: THREE.Scene, materials: MaterialLibrary) {
this.scene = scene;
this.materials = materials;
this.corridorGroup = new THREE.Group();
this.corridorGroup.name = 'showroom-corridor';
scene.add(this.corridorGroup);
}
/**
* Build the showroom from a SceneConfig (output of FigmaTransformer).
*/
buildFromConfig(config: SceneConfig): void {
this.buildCorridor(config.corridor);
this.buildWings(config.wings);
this.buildWallDisplays(config.walls);
this.buildLighting(config.lights);
this.buildFurniture(config.furniture);
this.buildEntry(config.entry);
}
private buildCorridor(corridor: SceneConfig['corridor']): void {
const { width, depth, height } = corridor;
const halfW = width / 2;
const halfD = depth / 2;
// Floor — polished concrete with normal map
const floorGeo = new THREE.PlaneGeometry(width, depth, 1, 1);
const floor = new THREE.Mesh(floorGeo, this.materials.concrete);
floor.rotation.x = -Math.PI / 2;
floor.receiveShadow = true;
this.corridorGroup.add(floor);
// Left wall — exposed brick
const leftWallGeo = new THREE.PlaneGeometry(depth, height);
const leftWall = new THREE.Mesh(leftWallGeo, this.materials.brick);
leftWall.position.set(-halfW, height / 2, 0);
leftWall.rotation.y = Math.PI / 2;
leftWall.receiveShadow = true;
this.corridorGroup.add(leftWall);
// Right wall — exposed brick
const rightWall = new THREE.Mesh(leftWallGeo, this.materials.brick);
rightWall.position.set(halfW, height / 2, 0);
rightWall.rotation.y = -Math.PI / 2;
rightWall.receiveShadow = true;
this.corridorGroup.add(rightWall);
// Back wall — dark charcoal
const backWallGeo = new THREE.PlaneGeometry(width, height);
const backWallMat = this.materials.steel.clone();
backWallMat.color.setHex(0x1a1a1a);
const backWall = new THREE.Mesh(backWallGeo, backWallMat);
backWall.position.set(0, height / 2, -halfD);
backWall.receiveShadow = true;
this.corridorGroup.add(backWall);
// Ceiling — steel with exposed duct geometry
const ceilGeo = new THREE.PlaneGeometry(width, depth);
const ceiling = new THREE.Mesh(ceilGeo, this.materials.steel);
ceiling.rotation.x = Math.PI / 2;
ceiling.position.y = height;
this.corridorGroup.add(ceiling);
// Exposed ducts — cylindrical pipes along ceiling
const ductMat = this.materials.steel.clone();
ductMat.metalness = 0.5;
for (let i = 0; i < 3; i++) {
const ductGeo = new THREE.CylinderGeometry(0.15, 0.15, depth, 8);
const duct = new THREE.Mesh(ductGeo, ductMat);
duct.rotation.x = Math.PI / 2;
duct.position.set(-halfW / 2 + i * (halfW / 2), height - 0.3, 0);
this.corridorGroup.add(duct);
}
// Baseboards — warm wood trim
const baseGeo = new THREE.BoxGeometry(depth, 0.15, 0.04);
for (const xPos of [-halfW + 0.02, halfW - 0.02]) {
const base = new THREE.Mesh(baseGeo, this.materials.wood);
base.position.set(xPos, 0.075, 0);
base.rotation.y = Math.PI / 2;
this.corridorGroup.add(base);
}
}
private buildWings(wings: WingConfig[]): void {
wings.forEach((wingConfig, i) => {
const group = new THREE.Group();
group.name = `wing-${i}-${wingConfig.vendor}`;
group.userData = { vendor: wingConfig.vendor, index: i, loaded: false };
// Wing entrance frame — brushed aluminum
const frameGeo = new THREE.BoxGeometry(0.08, 3.5, 0.08);
const leftFrame = new THREE.Mesh(frameGeo, this.materials.aluminum);
leftFrame.position.set(-1.5, 1.75, 0);
group.add(leftFrame);
const rightFrame = new THREE.Mesh(frameGeo, this.materials.aluminum);
rightFrame.position.set(1.5, 1.75, 0);
group.add(rightFrame);
// Header bar
const headerGeo = new THREE.BoxGeometry(3.08, 0.08, 0.08);
const header = new THREE.Mesh(headerGeo, this.materials.aluminum);
header.position.set(0, 3.5, 0);
group.add(header);
// Vendor label (canvas texture)
const labelCanvas = document.createElement('canvas');
labelCanvas.width = 512;
labelCanvas.height = 64;
const ctx = labelCanvas.getContext('2d');
if (ctx) {
ctx.fillStyle = '#1a1a1a';
ctx.fillRect(0, 0, 512, 64);
ctx.fillStyle = '#f0ebe4';
ctx.font = 'bold 28px Inter, -apple-system, sans-serif';
ctx.textAlign = 'center';
ctx.fillText(wingConfig.vendor.toUpperCase(), 256, 42);
}
const labelTex = new THREE.CanvasTexture(labelCanvas);
const labelGeo = new THREE.PlaneGeometry(2.5, 0.3);
const labelMat = new THREE.MeshBasicMaterial({ map: labelTex });
const label = new THREE.Mesh(labelGeo, labelMat);
label.position.set(0, 3.7, 0.01);
group.add(label);
// Position the wing in the corridor
group.position.set(
wingConfig.position[0],
wingConfig.position[1],
wingConfig.position[2]
);
group.rotation.y = wingConfig.rotation;
this.wingGroups.push(group);
this.corridorGroup.add(group);
});
}
private buildWallDisplays(walls: WallConfig[]): void {
walls.forEach(wall => {
if (wall.hasSamplePanels) {
// Large mounted sample panel
const panelGeo = new THREE.PlaneGeometry(wall.dimensions[0], wall.dimensions[1]);
const panel = new THREE.Mesh(panelGeo, this.materials.fabric);
panel.position.set(wall.position[0], wall.position[1], wall.position[2]);
this.corridorGroup.add(panel);
// Aluminum frame around panel
const frameThickness = 0.03;
const frameDepth = 0.04;
const frameMat = this.materials.aluminum;
// Top frame
const topGeo = new THREE.BoxGeometry(wall.dimensions[0] + frameThickness * 2, frameThickness, frameDepth);
const top = new THREE.Mesh(topGeo, frameMat);
top.position.set(wall.position[0], wall.position[1] + wall.dimensions[1] / 2, wall.position[2] + 0.02);
this.corridorGroup.add(top);
// Bottom frame
const bottom = top.clone();
bottom.position.y = wall.position[1] - wall.dimensions[1] / 2;
this.corridorGroup.add(bottom);
}
if (wall.hasBookshelf) {
// Glass-front bookshelf
const shelfGeo = new THREE.BoxGeometry(wall.dimensions[0], wall.dimensions[1], 0.3);
const shelf = new THREE.Mesh(shelfGeo, this.materials.wood);
shelf.position.set(wall.position[0], wall.position[1], wall.position[2]);
this.corridorGroup.add(shelf);
// Glass front
const glassGeo = new THREE.PlaneGeometry(wall.dimensions[0] - 0.05, wall.dimensions[1] - 0.05);
const glass = new THREE.Mesh(glassGeo, this.materials.glass);
glass.position.set(wall.position[0], wall.position[1], wall.position[2] + 0.16);
this.corridorGroup.add(glass);
}
});
}
private buildLighting(lights: LightConfig[]): void {
lights.forEach(lightConfig => {
if (lightConfig.type === 'spot') {
const spot = new THREE.SpotLight(
new THREE.Color(lightConfig.color),
lightConfig.intensity,
30,
lightConfig.angle || Math.PI / 6,
lightConfig.penumbra || 0.4,
1
);
spot.position.set(lightConfig.position[0], lightConfig.position[1], lightConfig.position[2]);
if (lightConfig.target) {
spot.target.position.set(lightConfig.target[0], lightConfig.target[1], lightConfig.target[2]);
this.scene.add(spot.target);
}
spot.castShadow = true;
spot.shadow.mapSize.set(1024, 1024);
this.spotLights.push(spot);
this.corridorGroup.add(spot);
// Track light housing (small chrome cylinder)
const housingGeo = new THREE.CylinderGeometry(0.08, 0.12, 0.15, 8);
const housing = new THREE.Mesh(housingGeo, this.materials.trackLight);
housing.position.set(lightConfig.position[0], lightConfig.position[1], lightConfig.position[2]);
this.corridorGroup.add(housing);
// Volumetric light cone (subtle)
const coneH = lightConfig.position[1] - (lightConfig.target?.[1] || 0);
const coneR = Math.tan(lightConfig.angle || Math.PI / 6) * coneH;
const coneGeo = new THREE.ConeGeometry(coneR, coneH, 16, 1, true);
const coneMat = new THREE.MeshBasicMaterial({
color: lightConfig.color,
transparent: true,
opacity: 0.015,
blending: THREE.AdditiveBlending,
side: THREE.DoubleSide,
depthWrite: false,
});
const cone = new THREE.Mesh(coneGeo, coneMat);
cone.position.set(
lightConfig.position[0],
lightConfig.position[1] - coneH / 2,
lightConfig.position[2]
);
this.corridorGroup.add(cone);
}
});
// Track rails (continuous aluminum rail along ceiling)
const railGeo = new THREE.BoxGeometry(40, 0.03, 0.05);
const rail = new THREE.Mesh(railGeo, this.materials.aluminum);
rail.position.set(0, 11.5, 0);
this.corridorGroup.add(rail);
}
private buildFurniture(furniture: SceneConfig['furniture']): void {
furniture.forEach(item => {
if (item.type === 'display-table') {
const tableGeo = new THREE.BoxGeometry(item.dimensions[0], item.dimensions[1], item.dimensions[2]);
const table = new THREE.Mesh(tableGeo, this.materials.wood);
table.position.set(item.position[0], item.position[1], item.position[2]);
table.castShadow = true;
this.corridorGroup.add(table);
}
});
}
private buildEntry(entry: SceneConfig['entry']): void {
// Entry signage
const signCanvas = document.createElement('canvas');
signCanvas.width = 1024;
signCanvas.height = 128;
const ctx = signCanvas.getContext('2d');
if (ctx) {
ctx.fillStyle = '#1a1a1a';
ctx.fillRect(0, 0, 1024, 128);
ctx.fillStyle = '#f0ebe4';
ctx.font = 'bold 36px Inter, -apple-system, sans-serif';
ctx.textAlign = 'center';
ctx.fillText(entry.signText, 512, 70);
ctx.font = '18px Inter, -apple-system, sans-serif';
ctx.fillStyle = '#888888';
ctx.fillText('ARCHITECTURAL TRADE SHOWROOM', 512, 105);
}
const signTex = new THREE.CanvasTexture(signCanvas);
const signGeo = new THREE.PlaneGeometry(6, 0.75);
const signMat = new THREE.MeshStandardMaterial({
map: signTex,
emissive: new THREE.Color(0xffffff),
emissiveMap: signTex,
emissiveIntensity: 0.3,
});
const sign = new THREE.Mesh(signGeo, signMat);
sign.position.set(entry.position[0], 10, entry.position[2]);
this.corridorGroup.add(sign);
}
/**
* Get wing groups for culling/visibility management.
*/
getWings(): THREE.Group[] {
return this.wingGroups;
}
/**
* Set visibility of wings by distance from camera for performance.
*/
updateVisibility(cameraPosition: THREE.Vector3, maxDistance = 30): void {
this.wingGroups.forEach(wing => {
const dist = wing.position.distanceTo(cameraPosition);
wing.visible = dist < maxDistance;
});
}
dispose(): void {
// Dispose spotlight targets from scene root
this.spotLights.forEach(spot => {
if (spot.target.parent) {
spot.target.parent.remove(spot.target);
}
spot.dispose();
});
this.corridorGroup.traverse(child => {
if (child instanceof THREE.Mesh) {
child.geometry.dispose();
// Dispose material (handles cloned materials and canvas textures)
const mat = child.material;
if (Array.isArray(mat)) {
mat.forEach(m => {
if (m.map) m.map.dispose();
if (m.emissiveMap) m.emissiveMap.dispose();
if (m.normalMap) m.normalMap.dispose();
m.dispose();
});
} else {
if (mat.map) mat.map.dispose();
if ((mat as any).emissiveMap) (mat as any).emissiveMap.dispose();
if ((mat as any).normalMap) (mat as any).normalMap.dispose();
mat.dispose();
}
}
});
this.scene.remove(this.corridorGroup);
this.wingGroups.length = 0;
this.spotLights.length = 0;
}
}