← back to Dw War Room

docs/plans/2026-03-02-figma-threejs-photorealism-plan.md

1624 lines

# Figma + Three.js Photorealistic Showroom — Implementation Plan

> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.

**Goal:** Build a Figma MCP-driven pipeline that generates photorealistic endless-wings showroom scenes for both the War Room (port 4060, TypeScript/Vite) and Boardroom-3D (port 7681, vanilla JS), featuring architectural trade showroom aesthetics with exposed brick, track lighting, and PBR materials.

**Architecture:** Figma design → MCP `get_design_context` extraction → Claude transform → TypeScript/JS modules. The showroom uses an endless corridor with 50 vendor wings (lazy-loaded), exposed brick walls with sample book displays, polished concrete floors, and industrial track lighting. Rendering upgraded with SSAO, HDR environment maps, procedural normal maps, depth of field, and color grading.

**Tech Stack:** Three.js r170 (war room) / r128 CDN (boardroom-3d), Vite 6, TypeScript 5.7, Figma MCP tools, procedural canvas normal/roughness maps, EffectComposer post-processing pipeline.

---

## Phase 1: Figma Showroom Design + MCP Pipeline

### Task 1: Create Figma Showroom Floor Plan

**Files:**
- Create: `/root/Projects/dw-war-room/src/showroom/figma-layout.json` (extracted layout data)

**Step 1: Create the Figma design using generate_figma_design**

Use the Figma MCP `generate_figma_design` tool to create a new Figma file with the showroom floor plan. The design should include:
- Main corridor frame (40ft x 20ft, exposed brick fill)
- 6 vendor wing bay frames extending perpendicular (each 12ft x 10ft)
- Central display table rectangle
- Track lighting rail lines along ceiling
- Product display wall rectangles (4ft x 8ft each, wallpaper texture fills)
- Glass entry frame
- Color tokens: brick (#8b4513), concrete (#8a8580), aluminum (#c0c0c0), warm wood (#8b6914)

**Step 2: Extract design context via MCP**

```typescript
// Use get_design_context to extract the showroom layout
const designContext = await mcp.get_design_context({
  nodeId: '<root-frame-id>',
  fileKey: '<file-key>'
});
```

Save extracted JSON to `figma-layout.json`.

**Step 3: Extract design tokens via MCP**

```typescript
const tokens = await mcp.get_variable_defs({
  nodeId: '<root-frame-id>',
  fileKey: '<file-key>'
});
```

**Step 4: Commit**

```bash
git add src/showroom/figma-layout.json
git commit -m "feat(showroom): extract Figma showroom layout via MCP"
```

---

### Task 2: Build Figma-to-SceneGraph Transformer

**Files:**
- Create: `/root/Projects/dw-war-room/src/showroom/FigmaTransformer.ts`

**Step 1: Write the transformer module**

```typescript
// FigmaTransformer.ts — Converts Figma design context into Three.js scene graph config
import type { FigmaNode, SceneConfig, WallConfig, WingConfig, LightConfig } from './types';

export interface SceneConfig {
  corridor: {
    width: number;    // world units (1 unit = 1 foot)
    depth: number;
    height: number;
    wallMaterial: 'brick' | 'concrete' | 'charcoal';
    floorMaterial: 'polished-concrete' | 'wood' | 'marble';
  };
  wings: WingConfig[];
  walls: WallConfig[];
  lights: LightConfig[];
  furniture: FurnitureConfig[];
  entry: { position: [number, number, number]; signText: string };
}

export interface WingConfig {
  index: number;
  vendor: string;
  position: [number, number, number];
  rotation: number;
  panelCount: number;
  accentColor: string;   // hex from Figma fill
}

export interface WallConfig {
  position: [number, number, number];
  dimensions: [number, number];  // width, height
  material: string;
  hasBookshelf: boolean;
  hasSamplePanels: boolean;
  sampleCount: number;
}

export interface LightConfig {
  type: 'spot' | 'track' | 'ambient' | 'point';
  position: [number, number, number];
  target?: [number, number, number];
  color: string;
  intensity: number;
  angle?: number;    // SpotLight cone angle
  penumbra?: number;
}

export interface FurnitureConfig {
  type: 'display-table' | 'stool' | 'bookshelf' | 'sample-rack';
  position: [number, number, number];
  dimensions: [number, number, number];
}

/**
 * Transform Figma design JSON into Three.js scene configuration.
 * Maps Figma frames → positions, rectangles → geometry, fills → materials.
 */
export function transformFigmaToScene(figmaData: any): SceneConfig {
  const root = figmaData.document || figmaData;
  const config: SceneConfig = {
    corridor: { width: 40, depth: 20, height: 12, wallMaterial: 'brick', floorMaterial: 'polished-concrete' },
    wings: [],
    walls: [],
    lights: [],
    furniture: [],
    entry: { position: [0, 0, 10], signText: 'DESIGNER WALLCOVERINGS' },
  };

  // Traverse Figma node tree
  function traverse(node: any, depth = 0) {
    if (!node) return;

    // Map FRAME nodes to wings or rooms based on name
    if (node.type === 'FRAME' && node.name?.toLowerCase().includes('wing')) {
      const bbox = node.absoluteBoundingBox || {};
      config.wings.push({
        index: config.wings.length,
        vendor: node.name.replace(/wing[-_\s]*/i, '').trim(),
        position: [bbox.x / 10 || config.wings.length * 4, 0, bbox.y / 10 || 0],
        rotation: 0,
        panelCount: (node.children || []).filter((c: any) => c.type === 'RECTANGLE').length || 10,
        accentColor: extractFillColor(node) || '#c0c0c0',
      });
    }

    // Map RECTANGLE nodes with IMAGE fills to wall sample panels
    if (node.type === 'RECTANGLE' && node.fills?.some((f: any) => f.type === 'IMAGE')) {
      const bbox = node.absoluteBoundingBox || {};
      config.walls.push({
        position: [bbox.x / 10 || 0, bbox.height / 20 || 2, bbox.y / 10 || 0],
        dimensions: [bbox.width / 10 || 4, bbox.height / 10 || 8],
        material: 'brick',
        hasBookshelf: false,
        hasSamplePanels: true,
        sampleCount: 1,
      });
    }

    // Map circles/ellipses to spotlights
    if (node.type === 'ELLIPSE' && node.name?.toLowerCase().includes('light')) {
      const bbox = node.absoluteBoundingBox || {};
      config.lights.push({
        type: 'spot',
        position: [bbox.x / 10 || 0, 10, bbox.y / 10 || 0],
        target: [bbox.x / 10 || 0, 0, bbox.y / 10 || 0],
        color: extractFillColor(node) || '#ffffff',
        intensity: 1.5,
        angle: Math.PI / 6,
        penumbra: 0.5,
      });
    }

    if (node.children) node.children.forEach((c: any) => traverse(c, depth + 1));
  }

  traverse(root);

  // Add default track lighting if none extracted
  if (config.lights.length === 0) {
    for (let i = 0; i < 8; i++) {
      config.lights.push({
        type: 'spot',
        position: [i * 5 - 17.5, 11, 0],
        target: [i * 5 - 17.5, 0, 0],
        color: '#f5e6d0',
        intensity: 1.2,
        angle: Math.PI / 5,
        penumbra: 0.4,
      });
    }
  }

  return config;
}

function extractFillColor(node: any): string | null {
  const fill = (node.fills || []).find((f: any) => f.type === 'SOLID' && f.visible !== false);
  if (!fill?.color) return null;
  const r = Math.round(fill.color.r * 255);
  const g = Math.round(fill.color.g * 255);
  const b = Math.round(fill.color.b * 255);
  return `#${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${b.toString(16).padStart(2, '0')}`;
}
```

**Step 2: Commit**

```bash
git add src/showroom/FigmaTransformer.ts
git commit -m "feat(showroom): Figma-to-SceneGraph transformer"
```

---

## Phase 2: Photorealistic Material System

### Task 3: Procedural Normal Map Generator

**Files:**
- Create: `/root/Projects/dw-war-room/src/showroom/ProceduralMaps.ts`

**Step 1: Write procedural normal/roughness map generators**

```typescript
// ProceduralMaps.ts — Canvas-based procedural normal and roughness maps
// Zero external file dependencies — all generated at runtime
import * as THREE from 'three';

/**
 * Generate a brick wall normal map using canvas.
 * Creates realistic mortar lines and brick surface variation.
 */
export function generateBrickNormalMap(width = 1024, height = 1024): THREE.CanvasTexture {
  const canvas = document.createElement('canvas');
  canvas.width = width;
  canvas.height = height;
  const ctx = canvas.getContext('2d')!;

  // Base neutral normal (pointing straight out: RGB 128,128,255)
  ctx.fillStyle = 'rgb(128, 128, 255)';
  ctx.fillRect(0, 0, width, height);

  const brickW = width / 8;
  const brickH = height / 16;
  const mortarW = 3;

  for (let row = 0; row < 16; row++) {
    const offset = (row % 2) * (brickW / 2); // stagger rows
    for (let col = -1; col < 9; col++) {
      const x = col * brickW + offset;
      const y = row * brickH;

      // Mortar lines (recessed = normal pointing inward)
      // Horizontal mortar
      ctx.fillStyle = 'rgb(128, 100, 255)'; // slight downward normal
      ctx.fillRect(0, y, width, mortarW);

      // Vertical mortar
      ctx.fillStyle = 'rgb(100, 128, 255)'; // slight leftward normal
      ctx.fillRect(x, y, mortarW, brickH);

      // Brick surface noise (subtle variation)
      for (let i = 0; i < 20; i++) {
        const nx = x + Math.random() * brickW;
        const ny = y + mortarW + Math.random() * (brickH - mortarW * 2);
        const r = 128 + (Math.random() - 0.5) * 12;
        const g = 128 + (Math.random() - 0.5) * 12;
        ctx.fillStyle = `rgb(${r}, ${g}, 255)`;
        ctx.fillRect(nx, ny, 3 + Math.random() * 6, 2 + Math.random() * 4);
      }
    }
  }

  const tex = new THREE.CanvasTexture(canvas);
  tex.wrapS = tex.wrapT = THREE.RepeatWrapping;
  tex.repeat.set(2, 2);
  return tex;
}

/**
 * Generate a polished concrete normal map.
 * Subtle micro-texture with occasional aggregate patches.
 */
export function generateConcreteNormalMap(width = 1024, height = 1024): THREE.CanvasTexture {
  const canvas = document.createElement('canvas');
  canvas.width = width;
  canvas.height = height;
  const ctx = canvas.getContext('2d')!;

  // Base neutral normal
  ctx.fillStyle = 'rgb(128, 128, 255)';
  ctx.fillRect(0, 0, width, height);

  // Micro-texture noise — polished concrete has subtle surface variation
  for (let i = 0; i < 5000; i++) {
    const x = Math.random() * width;
    const y = Math.random() * height;
    const r = 128 + (Math.random() - 0.5) * 6;
    const g = 128 + (Math.random() - 0.5) * 6;
    ctx.fillStyle = `rgb(${r}, ${g}, 255)`;
    ctx.fillRect(x, y, 1 + Math.random() * 3, 1 + Math.random() * 3);
  }

  // Aggregate patches (larger bumps)
  for (let i = 0; i < 80; i++) {
    const x = Math.random() * width;
    const y = Math.random() * height;
    const radius = 4 + Math.random() * 12;
    const r = 128 + (Math.random() - 0.5) * 18;
    const g = 128 + (Math.random() - 0.5) * 18;
    ctx.beginPath();
    ctx.arc(x, y, radius, 0, Math.PI * 2);
    ctx.fillStyle = `rgb(${r}, ${g}, 255)`;
    ctx.fill();
  }

  const tex = new THREE.CanvasTexture(canvas);
  tex.wrapS = tex.wrapT = THREE.RepeatWrapping;
  tex.repeat.set(3, 3);
  return tex;
}

/**
 * Generate a brushed aluminum normal map.
 * Directional brushing lines for metallic surfaces.
 */
export function generateBrushedAluminumNormalMap(width = 512, height = 512): THREE.CanvasTexture {
  const canvas = document.createElement('canvas');
  canvas.width = width;
  canvas.height = height;
  const ctx = canvas.getContext('2d')!;

  ctx.fillStyle = 'rgb(128, 128, 255)';
  ctx.fillRect(0, 0, width, height);

  // Horizontal brush strokes
  for (let y = 0; y < height; y++) {
    const r = 128 + (Math.random() - 0.5) * 4;
    const g = 128 + Math.sin(y * 0.3) * 3 + (Math.random() - 0.5) * 2;
    ctx.fillStyle = `rgb(${r}, ${g}, 255)`;
    ctx.fillRect(0, y, width, 1);
  }

  const tex = new THREE.CanvasTexture(canvas);
  tex.wrapS = tex.wrapT = THREE.RepeatWrapping;
  return tex;
}

/**
 * Generate a wood grain normal map for shelving.
 */
export function generateWoodNormalMap(width = 512, height = 512): THREE.CanvasTexture {
  const canvas = document.createElement('canvas');
  canvas.width = width;
  canvas.height = height;
  const ctx = canvas.getContext('2d')!;

  ctx.fillStyle = 'rgb(128, 128, 255)';
  ctx.fillRect(0, 0, width, height);

  // Wood grain lines (vertical, slightly wavy)
  for (let x = 0; x < width; x += 4 + Math.random() * 8) {
    ctx.beginPath();
    ctx.moveTo(x, 0);
    for (let y = 0; y < height; y += 10) {
      ctx.lineTo(x + Math.sin(y * 0.02 + x * 0.1) * 3, y);
    }
    const r = 128 + (Math.random() - 0.5) * 10;
    ctx.strokeStyle = `rgb(${r}, 128, 255)`;
    ctx.lineWidth = 1 + Math.random() * 2;
    ctx.stroke();
  }

  const tex = new THREE.CanvasTexture(canvas);
  tex.wrapS = tex.wrapT = THREE.RepeatWrapping;
  return tex;
}

/**
 * Generate a fabric/paper subtle normal map for wallcovering product panels.
 * Very subtle — just enough to catch light at grazing angles.
 */
export function generateFabricNormalMap(width = 256, height = 256): THREE.CanvasTexture {
  const canvas = document.createElement('canvas');
  canvas.width = width;
  canvas.height = height;
  const ctx = canvas.getContext('2d')!;

  ctx.fillStyle = 'rgb(128, 128, 255)';
  ctx.fillRect(0, 0, width, height);

  // Woven texture: alternating horizontal and vertical micro-lines
  for (let y = 0; y < height; y += 2) {
    for (let x = 0; x < width; x += 2) {
      const horizontal = (y / 2) % 2 === 0;
      const r = 128 + (horizontal ? 2 : -2) + (Math.random() - 0.5) * 3;
      const g = 128 + (horizontal ? -2 : 2) + (Math.random() - 0.5) * 3;
      ctx.fillStyle = `rgb(${r}, ${g}, 255)`;
      ctx.fillRect(x, y, 2, 2);
    }
  }

  const tex = new THREE.CanvasTexture(canvas);
  tex.wrapS = tex.wrapT = THREE.RepeatWrapping;
  tex.repeat.set(4, 4);
  return tex;
}
```

**Step 2: Commit**

```bash
git add src/showroom/ProceduralMaps.ts
git commit -m "feat(showroom): procedural normal map generators (brick, concrete, aluminum, wood, fabric)"
```

---

### Task 4: PBR Material Factory

**Files:**
- Create: `/root/Projects/dw-war-room/src/showroom/ShowroomMaterials.ts`

**Step 1: Write the material factory**

```typescript
// ShowroomMaterials.ts — PBR material definitions for photorealistic showroom
import * as THREE from 'three';
import {
  generateBrickNormalMap,
  generateConcreteNormalMap,
  generateBrushedAluminumNormalMap,
  generateWoodNormalMap,
  generateFabricNormalMap,
} from './ProceduralMaps';

export interface MaterialLibrary {
  brick: THREE.MeshStandardMaterial;
  concrete: THREE.MeshStandardMaterial;
  aluminum: THREE.MeshStandardMaterial;
  wood: THREE.MeshStandardMaterial;
  fabric: THREE.MeshStandardMaterial;
  steel: THREE.MeshStandardMaterial;
  glass: THREE.MeshStandardMaterial;
  trackLight: THREE.MeshStandardMaterial;
}

/**
 * Create the full PBR material library for the showroom.
 * Each material has proper normal maps for photorealistic surface detail.
 */
export function createMaterialLibrary(): MaterialLibrary {
  return {
    brick: new THREE.MeshStandardMaterial({
      color: 0x8b4513,
      metalness: 0.0,
      roughness: 0.85,
      normalMap: generateBrickNormalMap(),
      normalScale: new THREE.Vector2(1.2, 1.2),
    }),

    concrete: new THREE.MeshStandardMaterial({
      color: 0x8a8580,
      metalness: 0.15,
      roughness: 0.55,
      normalMap: generateConcreteNormalMap(),
      normalScale: new THREE.Vector2(0.5, 0.5),
    }),

    aluminum: new THREE.MeshStandardMaterial({
      color: 0xc0c0c0,
      metalness: 0.7,
      roughness: 0.25,
      normalMap: generateBrushedAluminumNormalMap(),
      normalScale: new THREE.Vector2(0.3, 0.3),
      envMapIntensity: 1.5,
    }),

    wood: new THREE.MeshStandardMaterial({
      color: 0x8b6914,
      metalness: 0.0,
      roughness: 0.65,
      normalMap: generateWoodNormalMap(),
      normalScale: new THREE.Vector2(0.8, 0.8),
    }),

    fabric: new THREE.MeshStandardMaterial({
      color: 0xf0ebe4,
      metalness: 0.0,
      roughness: 0.4,
      normalMap: generateFabricNormalMap(),
      normalScale: new THREE.Vector2(0.2, 0.2),
      envMapIntensity: 0.3,
    }),

    steel: new THREE.MeshStandardMaterial({
      color: 0x2a2a2a,
      metalness: 0.6,
      roughness: 0.3,
      envMapIntensity: 1.0,
    }),

    glass: new THREE.MeshStandardMaterial({
      color: 0xffffff,
      metalness: 0.0,
      roughness: 0.05,
      transparent: true,
      opacity: 0.15,
      envMapIntensity: 2.0,
    }),

    trackLight: new THREE.MeshStandardMaterial({
      color: 0xffffff,
      metalness: 0.9,
      roughness: 0.1,
      emissive: new THREE.Color(0xf5e6d0),
      emissiveIntensity: 0.8,
    }),
  };
}

/**
 * Dispose all materials and their textures.
 */
export function disposeMaterialLibrary(lib: MaterialLibrary): void {
  Object.values(lib).forEach(mat => {
    if (mat.normalMap) mat.normalMap.dispose();
    mat.dispose();
  });
}
```

**Step 2: Commit**

```bash
git add src/showroom/ShowroomMaterials.ts
git commit -m "feat(showroom): PBR material factory with normal maps"
```

---

## Phase 3: HDR Environment + Post-Processing Upgrade

### Task 5: HDR Environment Map Loader

**Files:**
- Modify: `/root/Projects/dw-war-room/src/scene/WarRoom.ts:95-123`
- Create: `/root/Projects/dw-war-room/src/showroom/ShowroomEnvironment.ts`

**Step 1: Create showroom-specific environment module**

```typescript
// ShowroomEnvironment.ts — HDR environment map for photorealistic IBL
import * as THREE from 'three';

/**
 * Generate a high-quality procedural environment map for gallery/showroom IBL.
 * Creates a warm gallery interior environment rather than the cold command-center gradient.
 *
 * This procedural approach avoids needing an external HDRI file download.
 * It simulates a well-lit interior with warm ceiling lights and neutral walls.
 */
export function createShowroomEnvMap(renderer: THREE.WebGLRenderer): THREE.Texture {
  const pmrem = new THREE.PMREMGenerator(renderer);
  pmrem.compileEquirectangularShader();

  const envScene = new THREE.Scene();

  // Sky dome — warm neutral ceiling (gallery white)
  const skyGeo = new THREE.SphereGeometry(50, 32, 16);
  const skyMat = new THREE.MeshBasicMaterial({
    color: 0x1a1815,  // warm dark interior
    side: THREE.BackSide,
  });
  envScene.add(new THREE.Mesh(skyGeo, skyMat));

  // Ceiling highlight — warm white overhead panels (gallery lighting)
  const ceilGeo = new THREE.SphereGeometry(40, 16, 8, 0, Math.PI * 2, 0, Math.PI * 0.2);
  const ceilMat = new THREE.MeshBasicMaterial({
    color: 0xf5e6d0,  // warm white (3200K-ish)
    side: THREE.BackSide,
  });
  const ceil = new THREE.Mesh(ceilGeo, ceilMat);
  ceil.rotation.x = Math.PI;
  envScene.add(ceil);

  // Floor reflection — dark polished concrete
  const floorGeo = new THREE.SphereGeometry(40, 16, 8, 0, Math.PI * 2, Math.PI * 0.75, Math.PI * 0.25);
  const floorMat = new THREE.MeshBasicMaterial({
    color: 0x0d0c0a,
    side: THREE.BackSide,
  });
  envScene.add(new THREE.Mesh(floorGeo, floorMat));

  // Wall bands — warm brick tones from sides
  for (let i = 0; i < 4; i++) {
    const wallGeo = new THREE.PlaneGeometry(30, 15);
    const wallMat = new THREE.MeshBasicMaterial({
      color: 0x3a2820,  // dark warm brick
      side: THREE.DoubleSide,
    });
    const wall = new THREE.Mesh(wallGeo, wallMat);
    const angle = (i / 4) * Math.PI * 2;
    wall.position.set(Math.cos(angle) * 35, 5, Math.sin(angle) * 35);
    wall.lookAt(0, 5, 0);
    envScene.add(wall);
  }

  // Track light spots — bright warm points at ceiling level
  for (let i = 0; i < 6; i++) {
    const spotGeo = new THREE.SphereGeometry(2, 8, 8);
    const spotMat = new THREE.MeshBasicMaterial({ color: 0xffe8c0 });
    const spot = new THREE.Mesh(spotGeo, spotMat);
    const angle = (i / 6) * Math.PI * 2;
    spot.position.set(Math.cos(angle) * 15, 18, Math.sin(angle) * 15);
    envScene.add(spot);
  }

  const envRenderTarget = pmrem.fromScene(envScene as any, 0.02);
  const texture = envRenderTarget.texture;

  // Cleanup
  pmrem.dispose();
  skyMat.dispose();
  ceilMat.dispose();
  floorMat.dispose();

  return texture;
}
```

**Step 2: Commit**

```bash
git add src/showroom/ShowroomEnvironment.ts
git commit -m "feat(showroom): warm gallery HDR environment map for IBL"
```

---

### Task 6: SSAO + Depth of Field + Color Grading

**Files:**
- Create: `/root/Projects/dw-war-room/src/showroom/ShowroomPostProcess.ts`

**Step 1: Write the enhanced post-processing pipeline**

```typescript
// ShowroomPostProcess.ts — Photorealistic post-processing for the showroom
// Adds SSAO, DOF, and color grading on top of existing bloom/grain pipeline
import * as THREE from 'three';
import { EffectComposer } from 'three/examples/jsm/postprocessing/EffectComposer.js';
import { RenderPass } from 'three/examples/jsm/postprocessing/RenderPass.js';
import { UnrealBloomPass } from 'three/examples/jsm/postprocessing/UnrealBloomPass.js';
import { ShaderPass } from 'three/examples/jsm/postprocessing/ShaderPass.js';
import { SSAOPass } from 'three/examples/jsm/postprocessing/SSAOPass.js';
import { BokehPass } from 'three/examples/jsm/postprocessing/BokehPass.js';

// ── Color Grading Shader ─────────────────────────────────────────────────────
// Warm architectural tones: slight orange lift in shadows, cool highlight compression
const ColorGradingShader = {
  name: 'ColorGradingShader',
  uniforms: {
    tDiffuse: { value: null as THREE.Texture | null },
    uSaturation: { value: 1.1 },     // slight boost
    uContrast: { value: 1.08 },      // subtle contrast
    uBrightness: { value: 0.02 },    // tiny lift
    uWarmth: { value: 0.04 },        // warm shift
  },
  vertexShader: /* glsl */ `
    varying vec2 vUv;
    void main() {
      vUv = uv;
      gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
    }
  `,
  fragmentShader: /* glsl */ `
    uniform sampler2D tDiffuse;
    uniform float uSaturation;
    uniform float uContrast;
    uniform float uBrightness;
    uniform float uWarmth;
    varying vec2 vUv;

    void main() {
      vec4 color = texture2D(tDiffuse, vUv);

      // Saturation adjustment
      float luminance = dot(color.rgb, vec3(0.2126, 0.7152, 0.0722));
      color.rgb = mix(vec3(luminance), color.rgb, uSaturation);

      // Contrast (pivot at mid-gray)
      color.rgb = (color.rgb - 0.5) * uContrast + 0.5;

      // Brightness lift
      color.rgb += uBrightness;

      // Warm tint — add slight orange to shadows, cool highlights slightly
      float shadowMask = 1.0 - smoothstep(0.0, 0.5, luminance);
      color.r += uWarmth * shadowMask;
      color.g += uWarmth * 0.5 * shadowMask;

      // Vignette — darkens corners for photographic depth
      vec2 vUvCenter = vUv - 0.5;
      float vignette = 1.0 - dot(vUvCenter, vUvCenter) * 0.5;
      color.rgb *= vignette;

      gl_FragColor = color;
    }
  `,
};

// ── Film Grain (reused from war room) ────────────────────────────────────────
const FilmGrainShader = {
  name: 'FilmGrainShader',
  uniforms: {
    tDiffuse: { value: null as THREE.Texture | null },
    uTime: { value: 0.0 },
    uOpacity: { value: 0.006 },
  },
  vertexShader: /* glsl */ `
    varying vec2 vUv;
    void main() { vUv = uv; gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); }
  `,
  fragmentShader: /* glsl */ `
    uniform sampler2D tDiffuse;
    uniform float uTime;
    uniform float uOpacity;
    varying vec2 vUv;
    float hash(vec2 p) { p = fract(p * vec2(234.34, 435.345)); p += dot(p, p + 34.23); return fract(p.x * p.y); }
    void main() {
      vec4 color = texture2D(tDiffuse, vUv);
      float grain = hash(vUv + fract(uTime * 0.1));
      color.rgb += (grain - 0.5) * 2.0 * uOpacity;
      gl_FragColor = color;
    }
  `,
};

export class ShowroomPostProcess {
  private composer: EffectComposer;
  private ssaoPass: SSAOPass;
  private bloomPass: UnrealBloomPass;
  private bokehPass: BokehPass;
  private colorGradingPass: ShaderPass;
  private filmGrainPass: ShaderPass;
  private clock: THREE.Clock;

  constructor(
    renderer: THREE.WebGLRenderer,
    scene: THREE.Scene,
    camera: THREE.PerspectiveCamera
  ) {
    this.clock = new THREE.Clock();
    const size = renderer.getSize(new THREE.Vector2());

    this.composer = new EffectComposer(renderer);

    // Pass 1: Render
    this.composer.addPass(new RenderPass(scene, camera));

    // Pass 2: SSAO — ambient occlusion for depth
    this.ssaoPass = new SSAOPass(scene, camera, size.x, size.y);
    this.ssaoPass.kernelRadius = 0.5;
    this.ssaoPass.minDistance = 0.001;
    this.ssaoPass.maxDistance = 0.1;
    (this.ssaoPass as any).output = SSAOPass.OUTPUT.Default;
    this.composer.addPass(this.ssaoPass);

    // Pass 3: Bloom — subtle architectural glow
    this.bloomPass = new UnrealBloomPass(
      new THREE.Vector2(size.x, size.y),
      0.2,   // strength (slightly less than war room — showroom is brighter)
      0.4,   // radius
      0.7    // threshold
    );
    this.composer.addPass(this.bloomPass);

    // Pass 4: Depth of Field — cinematic focus
    this.bokehPass = new BokehPass(scene, camera, {
      focus: 15.0,     // focus distance (adjustable)
      aperture: 0.002, // subtle DOF
      maxblur: 0.005,  // max blur radius
    });
    this.composer.addPass(this.bokehPass);

    // Pass 5: Color Grading — warm architectural tones
    this.colorGradingPass = new ShaderPass(ColorGradingShader);
    this.composer.addPass(this.colorGradingPass);

    // Pass 6: Film Grain — organic texture
    this.filmGrainPass = new ShaderPass(FilmGrainShader);
    this.composer.addPass(this.filmGrainPass);
  }

  render(): void {
    this.filmGrainPass.uniforms['uTime'].value = this.clock.getElapsedTime();
    this.composer.render();
  }

  setFocusDistance(distance: number): void {
    (this.bokehPass as any).uniforms['focus'].value = distance;
  }

  resize(width: number, height: number): void {
    this.composer.setSize(width, height);
    this.bloomPass.resolution.set(width, height);
    this.ssaoPass.setSize(width, height);
  }

  getComposer(): EffectComposer {
    return this.composer;
  }
}
```

**Step 2: Commit**

```bash
git add src/showroom/ShowroomPostProcess.ts
git commit -m "feat(showroom): SSAO + DOF + color grading post-processing pipeline"
```

---

## Phase 4: Showroom Scene Builder

### Task 7: Showroom Layout Builder (Corridor + Wings)

**Files:**
- Create: `/root/Projects/dw-war-room/src/showroom/ShowroomLayout.ts`

**Step 1: Write the main showroom scene builder**

This module creates the architectural corridor with endless wings, using the material library from Task 4 and scene config from Task 2.

```typescript
// ShowroomLayout.ts — Builds the photorealistic showroom corridor and wing bays
import * as THREE from 'three';
import type { SceneConfig, WingConfig, WallConfig, LightConfig } from './FigmaTransformer';
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')!;
      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);
        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);
        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,  // distance
          lightConfig.angle || Math.PI / 6,
          lightConfig.penumbra || 0.4,
          1    // decay
        );
        spot.position.set(...lightConfig.position);
        if (lightConfig.target) {
          spot.target.position.set(...lightConfig.target);
          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);
        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);
        const table = new THREE.Mesh(tableGeo, this.materials.wood);
        table.position.set(...item.position);
        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')!;
    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 {
    this.corridorGroup.traverse(child => {
      if (child instanceof THREE.Mesh) {
        child.geometry.dispose();
      }
    });
    this.scene.remove(this.corridorGroup);
  }
}
```

**Step 2: Commit**

```bash
git add src/showroom/ShowroomLayout.ts
git commit -m "feat(showroom): corridor + wings scene builder with PBR materials"
```

---

### Task 8: Showroom Scene Compositor

**Files:**
- Create: `/root/Projects/dw-war-room/src/showroom/Showroom.ts`

**Step 1: Write the main Showroom class that ties everything together**

```typescript
// 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, SceneConfig } from './FigmaTransformer';
import type { MaterialLibrary } from './ShowroomMaterials';

export class Showroom {
  scene: THREE.Scene;
  camera: THREE.PerspectiveCamera;
  renderer: THREE.WebGLRenderer;
  controls: OrbitControls;

  private clock: THREE.Clock;
  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);  // standing at entry
    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.renderer.environment = createShowroomEnvMap(this.renderer) as THREE.Texture;

    // 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
    window.addEventListener('resize', () => this.onResize(container));
  }

  /**
   * 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;     // x
      positions[i + 1] = Math.random() * 12;          // y
      positions[i + 2] = (Math.random() - 0.5) * 20;  // z
    }
    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 {
    requestAnimationFrame(() => this.animate());
    const dt = Math.min(this.clock.getDelta(), 0.1);
    const elapsed = this.clock.getElapsedTime();

    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);
  }

  dispose(): void {
    this.layout.dispose();
    disposeMaterialLibrary(this.materials);
    this.dustParticles.geometry.dispose();
    (this.dustParticles.material as THREE.PointsMaterial).dispose();
    this.renderer.dispose();
  }
}
```

**Step 2: Commit**

```bash
git add src/showroom/Showroom.ts
git commit -m "feat(showroom): main Showroom compositor with dust particles and DOF"
```

---

### Task 9: Showroom Entry Point + HTML Page

**Files:**
- Create: `/root/Projects/dw-war-room/src/showroom/index.ts`
- Create: `/root/Projects/dw-war-room/showroom.html`

**Step 1: Write entry point**

```typescript
// src/showroom/index.ts — Showroom standalone entry point
import { Showroom } from './Showroom';
import type { SceneConfig } from './FigmaTransformer';

// Default showroom config (used when no Figma data available)
const DEFAULT_CONFIG: SceneConfig = {
  corridor: { width: 40, depth: 20, height: 12, wallMaterial: 'brick', floorMaterial: 'polished-concrete' },
  wings: Array.from({ length: 6 }, (_, i) => ({
    index: i,
    vendor: ['Thibaut', 'Schumacher', 'Arte', 'Scalamandre', 'Elitis', 'Phillip Jeffries'][i],
    position: [-12 + i * 5, 0, -8] as [number, number, number],
    rotation: 0,
    panelCount: 12,
    accentColor: ['#1a4b8c', '#c41e3a', '#d4a574', '#2d4a3e', '#e67e22', '#8b4513'][i],
  })),
  walls: [
    { position: [-18, 4, 0], dimensions: [4, 8], material: 'brick', hasBookshelf: true, hasSamplePanels: false, sampleCount: 0 },
    { position: [18, 4, 0], dimensions: [4, 8], material: 'brick', hasBookshelf: false, hasSamplePanels: true, sampleCount: 3 },
  ],
  lights: Array.from({ length: 8 }, (_, i) => ({
    type: 'spot' as const,
    position: [-14 + i * 4, 11, 0] as [number, number, number],
    target: [-14 + i * 4, 0, 0] as [number, number, number],
    color: '#f5e6d0',
    intensity: 1.2,
    angle: Math.PI / 5,
    penumbra: 0.4,
  })),
  furniture: [
    { type: 'display-table' as const, position: [0, 0.45, 0] as [number, number, number], dimensions: [3, 0.9, 1.5] as [number, number, number] },
  ],
  entry: { position: [0, 0, 10], signText: 'DESIGNER WALLCOVERINGS' },
};

// Boot
const container = document.getElementById('showroom-container');
if (container) {
  const showroom = new Showroom(container);
  showroom.loadFromConfig(DEFAULT_CONFIG);
  showroom.animate();

  // Expose for MCP/debug
  (window as any).__showroom = showroom;
}
```

**Step 2: Write HTML page**

```html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>DW Showroom — Photorealistic 3D</title>
  <style>
    * { margin: 0; padding: 0; box-sizing: border-box; }
    body { overflow: hidden; background: #0d0c0a; font-family: Inter, -apple-system, sans-serif; }
    #showroom-container { width: 100vw; height: 100vh; }
  </style>
</head>
<body>
  <div id="showroom-container"></div>
  <script type="module" src="/src/showroom/index.ts"></script>
</body>
</html>
```

**Step 3: Add Vite route for showroom page**

Modify `vite.config.ts` to add the showroom entry:

```typescript
import { defineConfig } from 'vite';
import { resolve } from 'path';

export default defineConfig({
  server: { port: 4060, host: '0.0.0.0' },
  preview: { port: 4060, host: '0.0.0.0' },
  build: {
    rollupOptions: {
      input: {
        main: resolve(__dirname, 'index.html'),
        showroom: resolve(__dirname, 'showroom.html'),
      },
    },
  },
});
```

**Step 4: Commit**

```bash
git add src/showroom/index.ts showroom.html vite.config.ts
git commit -m "feat(showroom): standalone showroom page with default 6-vendor config"
```

---

## Phase 5: Boardroom-3D Integration (Port 7681)

### Task 10: Backport Photorealistic Materials to wr-wings.js

**Files:**
- Modify: `/root/DW-Agents/boardroom-3d/public/js/modules/wr-wings.js`
- Create: `/root/DW-Agents/boardroom-3d/public/js/modules/wr-pbr-materials.js`

**Step 1: Create PBR materials module for the vanilla JS system**

Write a new module `wr-pbr-materials.js` that provides the same procedural normal maps and material factory, adapted for the older Three.js CDN version (r128). This module follows the existing `window.WR` namespace pattern.

Key adaptations:
- Use `THREE.MeshStandardMaterial` (available in r128+)
- Canvas normal maps generated identically to the TypeScript version
- Exposed via `window.DWS.materials` namespace

**Step 2: Integrate into wr-wings.js wing panel materials**

Replace the existing `MeshStandardMaterial({ roughness: 0.35, metalness: 0.0 })` wing panel material with the new PBR material that includes normal maps.

**Step 3: Build and test**

```bash
cd /root/DW-Agents/boardroom-3d
node build-warroom.js
node validate-warroom.js
```

**Step 4: Commit**

```bash
git add public/js/modules/wr-pbr-materials.js public/js/modules/wr-wings.js
git commit -m "feat(boardroom-3d): PBR materials with procedural normal maps for showroom wings"
```

---

### Task 11: Backport Post-Processing to wr-effects.js

**Files:**
- Modify: `/root/DW-Agents/boardroom-3d/public/js/modules/wr-effects.js`

**Step 1: Add SSAO and color grading to the existing effects pipeline**

The boardroom-3d system currently has bloom disabled by default (performance). Add optional SSAO and color grading that can be toggled:

```javascript
// In wr-effects.js — add after existing toggleBloom function
window.toggleSSAO = function() { /* ... */ };
window.toggleColorGrading = function() { /* ... */ };
window.setQualityPreset = function(level) {
  // 'performance' = no post-processing
  // 'balanced' = bloom only
  // 'quality' = bloom + color grading
  // 'ultra' = bloom + SSAO + color grading + grain
};
```

**Step 2: Build and validate**

```bash
node build-warroom.js && node validate-warroom.js
```

**Step 3: Commit**

```bash
git commit -am "feat(boardroom-3d): quality presets with SSAO and color grading"
```

---

## Phase 6: Testing + Polish

### Task 12: Visual Verification

**Step 1: Start dev server and verify war room showroom**

```bash
cd /root/Projects/dw-war-room && npm run dev
```

Navigate to `http://45.61.58.125:4060/showroom.html` and verify:
- Brick walls have visible mortar normal map detail
- Polished concrete floor reflects environment
- Track lights cast visible spot cones
- SSAO darkens wall-floor junctions
- DOF blurs background when looking at near wing
- Color grading gives warm architectural tone
- 30+ FPS maintained

**Step 2: Verify boardroom-3d showroom**

```bash
cd /root/DW-Agents/boardroom-3d && pm2 restart boardroom-3d
```

Navigate to showroom wings at `http://45.61.58.125:7681/` and verify PBR materials apply.

**Step 3: Performance audit**

Check draw calls, FPS, texture memory via browser dev tools:
- Target: <300 draw calls, 30+ FPS, <256MB textures

---

### Task 13: Final Commit + Documentation

**Step 1: Update CLAUDE.md with showroom architecture**

Add showroom module documentation to `/root/Projects/dw-war-room/CLAUDE.md`.

**Step 2: Final commit**

```bash
cd /root/Projects/dw-war-room
git add -A
git commit -m "feat(showroom): complete Figma + Three.js photorealistic showroom pipeline"
```

**Step 3: Push to GitHub**

```bash
git push origin main
```

---

## Summary

| Phase | Tasks | Key Deliverables |
|-------|-------|-----------------|
| 1 | Tasks 1-2 | Figma design + MCP extraction pipeline |
| 2 | Tasks 3-4 | Procedural normal maps + PBR material factory |
| 3 | Tasks 5-6 | HDR environment + SSAO/DOF/color grading |
| 4 | Tasks 7-9 | Showroom layout builder + entry point |
| 5 | Tasks 10-11 | Boardroom-3D backport |
| 6 | Tasks 12-13 | Testing + documentation |

**Total new files:** ~8 TypeScript modules + 1 HTML page + 1 JS module
**Estimated draw calls:** ~150 base corridor, ~250 with 3 wings visible
**Target FPS:** 30+ at 1080p on desktop GPU