← back to Dw Showroom

public/js/showroom.js

1316 lines

(function() {
'use strict';

// ============================================================
// CONFIG
// ============================================================
const CONFIG = {
  room: { width: 7.5, depth: 6.0, height: 2.75 },
  wing: {
    height: 1.83,
    totalCount: 50,
    wallMargin: 0.25,
    animSpeed: 0.06,
    fanCascade: {
      center: Math.PI / 2,  // 90° for clicked wing
      ring1: Math.PI / 3,   // 60° for ±1
      ring2: 0.7,           // ~40° for ±2
      ring3: 0.35           // ~20° for ±3
    }
  },
  camera: { startPos: { x: 0, y: 1.6, z: 1.0 }, fov: 55 }
};

// ============================================================
// STATE
// ============================================================
let scene, camera, renderer, controls, clock, raycaster, mouse;
let wingBoards = [], wingRacks = [], sampleTray = [], products = [], vendors = [];
let focusedWing = null, animatingWings = [], frameCount = 0, lastFpsTime = 0;
let fanWings = [], fanCenterIndex = -1; // Fan cascade state
let controlsLocked = false, lockedCamPos = null, lockedCamTarget = null, previousCamPos = null, previousCamTarget = null;
let cameraAnim = null; // { startPos, endPos, startTarget, endTarget, progress, duration, onComplete }

// SHARED materials — key perf optimization
const MAT = {};

// Pre-generated pattern texture pool (shared across wings)
let TEXTURE_POOL = [];

// Keyboard state for WASD navigation
const keysDown = {};
const WALK_SPEED = 2.5; // meters per second

// ============================================================
// INIT
// ============================================================
function init() {
  clock = new THREE.Clock();
  raycaster = new THREE.Raycaster();
  mouse = new THREE.Vector2(-99, -99);

  scene = new THREE.Scene();
  scene.background = new THREE.Color(0x0c0c14);
  scene.fog = new THREE.FogExp2(0x0c0c14, 0.02);

  camera = new THREE.PerspectiveCamera(CONFIG.camera.fov, window.innerWidth / window.innerHeight, 0.1, 40);
  camera.position.set(CONFIG.camera.startPos.x, CONFIG.camera.startPos.y, CONFIG.camera.startPos.z);

  const canvas = document.getElementById('showroom-canvas');
  renderer = new THREE.WebGLRenderer({ canvas, antialias: false });
  renderer.setSize(window.innerWidth, window.innerHeight);
  renderer.setPixelRatio(1.0);
  renderer.toneMapping = THREE.ACESFilmicToneMapping;
  renderer.toneMappingExposure = 1.2;

  controls = new THREE.OrbitControls(camera, canvas);
  controls.enableDamping = true;
  controls.dampingFactor = 0.08;
  controls.maxPolarAngle = Math.PI * 0.55;
  controls.minPolarAngle = Math.PI * 0.25;
  controls.minDistance = 1.0;
  controls.maxDistance = 8.0;
  controls.target.set(0, 1.2, -0.5);
  controls.panSpeed = 0.5;

  updateLoadStatus('Building materials...', 10);
  initMaterials();
  initTexturePool();

  updateLoadStatus('Building showroom...', 30);
  buildRoom();
  buildLighting();
  buildFurniture();

  updateLoadStatus('Loading products...', 60);
  loadProducts();

  canvas.addEventListener('click', onCanvasClick, false);
  canvas.addEventListener('mousemove', onCanvasMouseMove, false);
  window.addEventListener('resize', onResize, false);
  window.addEventListener('keydown', e => {
    keysDown[e.code] = true;
    if (e.code === 'KeyM') {
      minimapVisible = !minimapVisible;
      const mc = document.getElementById('minimap');
      if (mc) mc.classList.toggle('hidden', !minimapVisible);
    }
  });
  window.addEventListener('keyup', e => { keysDown[e.code] = false; });
  initHUD();
  animate();
}

// ============================================================
// MATERIALS — all shared
// ============================================================
function initMaterials() {
  MAT.floor = new THREE.MeshLambertMaterial({ color: 0x3a3a42 });
  MAT.ceiling = new THREE.MeshLambertMaterial({ color: 0xd8d8d0 });
  MAT.wall = new THREE.MeshLambertMaterial({ color: 0xc8c4bc });
  MAT.chrome = new THREE.MeshLambertMaterial({ color: 0xbbbbbb, emissive: 0x222222 });
  MAT.wood = new THREE.MeshLambertMaterial({ color: 0x5c4033 });
  MAT.chair = new THREE.MeshLambertMaterial({ color: 0x444450 });
  MAT.dark = new THREE.MeshLambertMaterial({ color: 0x111111 });
  MAT.frame = new THREE.MeshLambertMaterial({ color: 0x3a3a3a });
  MAT.lightPanel = new THREE.MeshBasicMaterial({ color: 0xfffff5 });
  MAT.baseboard = new THREE.MeshLambertMaterial({ color: 0x2a2a30 });

  // Shared book materials — 8 colors
  MAT.books = [0x1a5276, 0x7d3c98, 0x1e8449, 0xb9770e, 0x922b21, 0x2c3e50, 0xd4ac0d, 0xa04000]
    .map(c => new THREE.MeshLambertMaterial({ color: c }));

  // Brick texture
  const bc = document.createElement('canvas');
  bc.width = 128; bc.height = 128;
  const bx = bc.getContext('2d');
  bx.fillStyle = '#6b3a2a'; bx.fillRect(0, 0, 128, 128);
  for (let row = 0; row < 4; row++) {
    for (let col = 0; col < 2; col++) {
      bx.fillStyle = `rgb(${100+Math.random()*40|0},${50+Math.random()*25|0},${35+Math.random()*20|0})`;
      bx.fillRect(col * 64 + (row % 2 ? 32 : 0) + 2, row * 32 + 2, 60, 28);
    }
  }
  const bt = new THREE.CanvasTexture(bc);
  bt.wrapS = bt.wrapT = THREE.RepeatWrapping; bt.repeat.set(3, 2);
  MAT.brick = new THREE.MeshLambertMaterial({ map: bt });
}

// ============================================================
// TEXTURE POOL — 24 shared patterns (instead of 1 per wing)
// ============================================================
function initTexturePool() {
  const patterns = ['Damask', 'Grasscloth', 'Stripe', 'Geometric', 'Floral', 'Texture'];
  const colors = [
    { name: 'Navy', hex: '#1a2744' }, { name: 'Sage', hex: '#6b7f5e' },
    { name: 'Cream', hex: '#f0ead6' }, { name: 'Gold', hex: '#c9a96e' },
    { name: 'Silver', hex: '#b8b8c0' }, { name: 'Blush', hex: '#d4a0a0' },
    { name: 'Charcoal', hex: '#3a3a42' }, { name: 'Ivory', hex: '#f5f0e8' },
    { name: 'Slate', hex: '#5a6068' }, { name: 'Teal', hex: '#2a6b6b' },
    { name: 'Coral', hex: '#cd6858' }, { name: 'Burgundy', hex: '#6b2040' }
  ];

  // Generate 12 unique textures (6 patterns × 2 colors each) — fewer for better FPS
  for (let p = 0; p < patterns.length; p++) {
    for (let c = 0; c < 2; c++) {
      const color = colors[(p * 4 + c) % colors.length];
      const canvas = generatePatternCanvas(patterns[p], color.hex);
      const tex = new THREE.CanvasTexture(canvas);
      tex.wrapS = tex.wrapT = THREE.RepeatWrapping;
      const mat = new THREE.MeshLambertMaterial({ map: tex, side: THREE.FrontSide });
      TEXTURE_POOL.push({ material: mat, pattern: patterns[p], color: color.name });
    }
  }
}

function generatePatternCanvas(patternType, baseHex) {
  const canvas = document.createElement('canvas');
  canvas.width = 128; canvas.height = 256;
  const ctx = canvas.getContext('2d');
  const r = parseInt(baseHex.slice(1,3), 16);
  const g = parseInt(baseHex.slice(3,5), 16);
  const b = parseInt(baseHex.slice(5,7), 16);
  const lighter = `rgb(${Math.min(255,r+35)},${Math.min(255,g+35)},${Math.min(255,b+35)})`;
  const darker = `rgb(${Math.max(0,r-30)},${Math.max(0,g-30)},${Math.max(0,b-30)})`;

  ctx.fillStyle = baseHex; ctx.fillRect(0, 0, 128, 256);

  switch(patternType) {
    case 'Damask':
      ctx.globalAlpha = 0.3; ctx.fillStyle = lighter;
      for (let y = 0; y < 4; y++) for (let x = 0; x < 2; x++) {
        const cx = x * 64 + 32, cy = y * 64 + 32;
        ctx.beginPath(); ctx.moveTo(cx, cy-20); ctx.quadraticCurveTo(cx+20,cy,cx,cy+20);
        ctx.quadraticCurveTo(cx-20,cy,cx,cy-20); ctx.fill();
      }
      break;
    case 'Grasscloth':
      for (let i = 0; i < 80; i++) {
        ctx.strokeStyle = i%2 ? lighter : darker; ctx.globalAlpha = 0.15; ctx.lineWidth = 1;
        const y = Math.random()*256;
        ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(128, y+(Math.random()-0.5)*6); ctx.stroke();
      }
      break;
    case 'Stripe':
      ctx.globalAlpha = 0.25; ctx.fillStyle = lighter;
      for (let x = 0; x < 128; x += 32) ctx.fillRect(x, 0, 14, 256);
      break;
    case 'Geometric':
      ctx.globalAlpha = 0.2; ctx.strokeStyle = lighter; ctx.lineWidth = 1.5;
      for (let y = 0; y < 8; y++) for (let x = 0; x < 4; x++) {
        ctx.strokeRect(x*32+4, y*32+4, 24, 24);
        if ((x+y)%2===0) { ctx.fillStyle = lighter; ctx.fillRect(x*32+8, y*32+8, 16, 16); }
      }
      break;
    case 'Floral':
      ctx.globalAlpha = 0.25; ctx.fillStyle = lighter;
      for (let y = 0; y < 4; y++) for (let x = 0; x < 2; x++) {
        const fx = x*64+32+(y%2?32:0), fy = y*64+32;
        for (let p = 0; p < 5; p++) {
          const a = (p/5)*Math.PI*2;
          ctx.beginPath(); ctx.ellipse(fx+Math.cos(a)*8, fy+Math.sin(a)*8, 6, 4, a, 0, Math.PI*2); ctx.fill();
        }
        ctx.fillStyle = darker; ctx.beginPath(); ctx.arc(fx, fy, 3, 0, Math.PI*2); ctx.fill();
        ctx.fillStyle = lighter;
      }
      break;
    default: // Texture
      for (let i = 0; i < 150; i++) {
        ctx.fillStyle = i%2 ? lighter : darker; ctx.globalAlpha = 0.08;
        ctx.fillRect(Math.random()*128, Math.random()*256, 3+Math.random()*3, 3+Math.random()*3);
      }
  }
  ctx.globalAlpha = 1;
  return canvas;
}

// ============================================================
// ROOM
// ============================================================
function buildRoom() {
  const W = CONFIG.room.width, D = CONFIG.room.depth, H = CONFIG.room.height;

  // Floor — polished concrete with subtle grain
  const fc = document.createElement('canvas'); fc.width = 256; fc.height = 256;
  const fx = fc.getContext('2d');
  fx.fillStyle = '#3a3a42'; fx.fillRect(0, 0, 256, 256);
  // Grain texture
  for (let i = 0; i < 4000; i++) { fx.fillStyle = `rgba(${50+Math.random()*30|0},${50+Math.random()*30|0},${55+Math.random()*30|0},0.25)`; fx.fillRect(Math.random()*256, Math.random()*256, 2, 2); }
  // Subtle tile grid
  fx.strokeStyle = 'rgba(80,80,88,0.15)'; fx.lineWidth = 1;
  for (let i = 0; i <= 4; i++) { fx.beginPath(); fx.moveTo(i*64,0); fx.lineTo(i*64,256); fx.stroke(); fx.beginPath(); fx.moveTo(0,i*64); fx.lineTo(256,i*64); fx.stroke(); }
  const ft = new THREE.CanvasTexture(fc); ft.wrapS = ft.wrapT = THREE.RepeatWrapping; ft.repeat.set(4, 4);
  const floorMesh = new THREE.Mesh(new THREE.PlaneGeometry(W, D), new THREE.MeshPhongMaterial({ map: ft, shininess: 15, specular: new THREE.Color(0x222222) }));
  floorMesh.rotation.x = -Math.PI/2; scene.add(floorMesh);

  // Ceiling
  const cc = document.createElement('canvas'); cc.width = 64; cc.height = 64;
  const cx = cc.getContext('2d'); cx.fillStyle = '#d5d5cd'; cx.fillRect(0,0,64,64);
  cx.strokeStyle = '#b8b8b0'; cx.lineWidth = 1;
  for (let i = 0; i <= 2; i++) { cx.beginPath(); cx.moveTo(i*32,0); cx.lineTo(i*32,64); cx.stroke(); cx.beginPath(); cx.moveTo(0,i*32); cx.lineTo(64,i*32); cx.stroke(); }
  const ct = new THREE.CanvasTexture(cc); ct.wrapS = ct.wrapT = THREE.RepeatWrapping; ct.repeat.set(6,5);
  const ceil = new THREE.Mesh(new THREE.PlaneGeometry(W, D), new THREE.MeshLambertMaterial({ map: ct }));
  ceil.rotation.x = Math.PI/2; ceil.position.y = H; scene.add(ceil);

  // Walls
  addWall(0, H/2, -D/2, W, H, 0, MAT.wall);          // Back
  addWall(-W/2, H/2, 0, D, H, Math.PI/2, MAT.brick);  // Left (brick)
  addWall(W/2, H/2, 0, D, H, -Math.PI/2, MAT.wall);   // Right
  // Front (with doorway)
  addWall(-W/4-0.3, H/2, D/2, W/2-0.6, H, Math.PI, MAT.wall);
  addWall(W/4+0.3, H/2, D/2, W/2-0.6, H, Math.PI, MAT.wall);
  addWall(0, H-(H-2.1)/2, D/2, 1.2, H-2.1, Math.PI, MAT.wall);

  // Baseboards
  [[0, 0.04, -D/2+0.02, W, 0.08, 0.04], [-W/2+0.02, 0.04, 0, 0.04, 0.08, D], [W/2-0.02, 0.04, 0, 0.04, 0.08, D]].forEach(b => {
    const m = new THREE.Mesh(new THREE.BoxGeometry(b[3],b[4],b[5]), MAT.baseboard);
    m.position.set(b[0],b[1],b[2]); scene.add(m);
  });

  // Crown molding — thin profile along ceiling edges
  const crownMat = new THREE.MeshLambertMaterial({ color: 0xe8e4dc });
  const crownData = [
    [0, H-0.025, -D/2+0.03, W, 0.05, 0.06],   // Back wall
    [-W/2+0.03, H-0.025, 0, 0.06, 0.05, D],    // Left wall
    [W/2-0.03, H-0.025, 0, 0.06, 0.05, D],     // Right wall
    [0, H-0.025, D/2-0.03, W, 0.05, 0.06]      // Front wall
  ];
  crownData.forEach(([cx, cy, cz, cw, ch, cd]) => {
    const m = new THREE.Mesh(new THREE.BoxGeometry(cw, ch, cd), crownMat);
    m.position.set(cx, cy, cz); scene.add(m);
  });

  // DW entrance sign above doorway
  const signCanvas = document.createElement('canvas');
  signCanvas.width = 512; signCanvas.height = 128;
  const sx = signCanvas.getContext('2d');
  sx.fillStyle = '#0c0c14'; sx.fillRect(0, 0, 512, 128);
  sx.fillStyle = '#c9a96e'; sx.font = 'bold 54px sans-serif'; sx.textAlign = 'center'; sx.textBaseline = 'middle';
  sx.fillText('DESIGNER WALLCOVERINGS', 256, 50);
  sx.font = '22px sans-serif'; sx.fillStyle = '#888888'; sx.fillText('SHERMAN OAKS SHOWROOM', 256, 95);
  const signTex = new THREE.CanvasTexture(signCanvas);
  const signMesh = new THREE.Mesh(
    new THREE.PlaneGeometry(1.6, 0.4),
    new THREE.MeshBasicMaterial({ map: signTex, transparent: true })
  );
  signMesh.position.set(0, H - 0.35, D/2 - 0.02);
  signMesh.rotation.y = Math.PI;
  scene.add(signMesh);
}

function addWall(x, y, z, w, h, rotY, mat) {
  const m = new THREE.Mesh(new THREE.PlaneGeometry(w, h), mat);
  m.position.set(x, y, z); m.rotation.y = rotY; scene.add(m);
}

// ============================================================
// LIGHTING — minimal: ambient + hemi + 4 point
// ============================================================
function buildLighting() {
  // Ambient fills shadow areas — cheap
  scene.add(new THREE.AmbientLight(0xfff5e6, 0.7));
  // Hemisphere gives sky/ground color variation — cheap
  scene.add(new THREE.HemisphereLight(0xfff8f0, 0x303038, 0.5));

  // 2 DirectionalLights replace 6 PointLights — MUCH cheaper (no per-pixel distance calc)
  const d1 = new THREE.DirectionalLight(0xfff5e6, 0.6);
  d1.position.set(2, CONFIG.room.height - 0.2, 1); scene.add(d1);
  const d2 = new THREE.DirectionalLight(0xfff0dd, 0.4);
  d2.position.set(-2, CONFIG.room.height - 0.2, -1); scene.add(d2);

  // Light panel meshes (visual only, no light cost)
  const H = CONFIG.room.height;
  [[-1.5, -1.0], [1.5, -1.0], [-1.5, 1.5], [1.5, 1.5]].forEach(([x, z]) => {
    const p = new THREE.Mesh(new THREE.PlaneGeometry(0.5, 0.5), MAT.lightPanel);
    p.rotation.x = Math.PI/2; p.position.set(x, H-0.05, z); scene.add(p);
  });
}

// ============================================================
// FURNITURE — simplified
// ============================================================
function buildFurniture() {
  const W = CONFIG.room.width, D = CONFIG.room.depth;

  // Table
  const tt = new THREE.Mesh(new THREE.BoxGeometry(1.8, 0.04, 0.9), MAT.wood);
  tt.position.set(-1.0, 0.75, 1.5); scene.add(tt);
  [[-0.8,-0.35],[0.8,-0.35],[-0.8,0.35],[0.8,0.35]].forEach(([lx,lz]) => {
    const leg = new THREE.Mesh(new THREE.CylinderGeometry(0.02,0.02,0.74,6), MAT.chrome);
    leg.position.set(-1.0+lx, 0.37, 1.5+lz); scene.add(leg);
  });

  // 4 Chairs (simplified — just seat + back, shared material)
  [[-1.6,1.5,0],[-0.4,1.5,Math.PI],[-1.0,1.0,Math.PI/2],[-1.0,2.0,-Math.PI/2]].forEach(([cx,cz,rot]) => {
    const seat = new THREE.Mesh(new THREE.BoxGeometry(0.42, 0.04, 0.4), MAT.chair);
    seat.position.set(cx, 0.46, cz); seat.rotation.y = rot; scene.add(seat);
    const back = new THREE.Mesh(new THREE.BoxGeometry(0.42, 0.4, 0.03), MAT.chair);
    back.position.set(cx - Math.sin(rot)*0.19, 0.68, cz - Math.cos(rot)*0.19); back.rotation.y = rot; scene.add(back);
  });

  // TV on brick wall with dynamic slideshow
  const tv = new THREE.Mesh(new THREE.BoxGeometry(0.04, 0.55, 0.9), MAT.dark);
  tv.position.set(-W/2+0.06, 1.5, 0.5); scene.add(tv);
  // TV bezel
  const bezel = new THREE.Mesh(new THREE.BoxGeometry(0.01, 0.52, 0.87), new THREE.MeshLambertMaterial({ color: 0x1a1a1a }));
  bezel.position.set(-W/2+0.075, 1.5, 0.5); scene.add(bezel);
  // Screen canvas
  const tvCanvas = document.createElement('canvas'); tvCanvas.width = 480; tvCanvas.height = 270;
  const tvCtx = tvCanvas.getContext('2d');
  const tvTex = new THREE.CanvasTexture(tvCanvas);
  const scr = new THREE.Mesh(new THREE.PlaneGeometry(0.44, 0.78), new THREE.MeshBasicMaterial({ map: tvTex }));
  scr.rotation.y = Math.PI/2; scr.position.set(-W/2+0.085, 1.5, 0.5); scene.add(scr);
  // Store for slideshow updates
  window._tvScreen = { canvas: tvCanvas, ctx: tvCtx, texture: tvTex, slideIdx: 0 };
  drawTVSlide(0);

  // Bookshelves — near front wall, flanking doorway (don't block wing racks)
  buildBookShelf(-2.8, D/2-0.3, Math.PI, 1.2);
  buildBookShelf(2.8, D/2-0.3, Math.PI, 1.2);

  // Rug under meeting table
  const rugCanvas = document.createElement('canvas'); rugCanvas.width = 256; rugCanvas.height = 128;
  const rc = rugCanvas.getContext('2d');
  rc.fillStyle = '#4a3a2a'; rc.fillRect(0, 0, 256, 128);
  // Border pattern
  rc.strokeStyle = '#c9a96e'; rc.lineWidth = 6;
  rc.strokeRect(8, 8, 240, 112);
  rc.strokeStyle = '#8a7a5a'; rc.lineWidth = 2;
  rc.strokeRect(16, 16, 224, 96);
  // Center medallion
  rc.fillStyle = '#6a5a3a'; rc.beginPath(); rc.ellipse(128, 64, 40, 25, 0, 0, Math.PI*2); rc.fill();
  rc.strokeStyle = '#c9a96e'; rc.lineWidth = 1.5; rc.beginPath(); rc.ellipse(128, 64, 40, 25, 0, 0, Math.PI*2); rc.stroke();
  const rugTex = new THREE.CanvasTexture(rugCanvas);
  const rug = new THREE.Mesh(new THREE.PlaneGeometry(2.4, 1.4), new THREE.MeshLambertMaterial({ map: rugTex }));
  rug.rotation.x = -Math.PI/2; rug.position.set(-1.0, 0.005, 1.5); scene.add(rug);

  // Potted plants — 2 corners
  [[-W/2+0.4, D/2-0.4], [W/2-0.4, -D/2+0.4]].forEach(([px, pz]) => {
    // Pot
    const pot = new THREE.Mesh(new THREE.CylinderGeometry(0.1, 0.08, 0.2, 8), new THREE.MeshLambertMaterial({ color: 0x8b5e3c }));
    pot.position.set(px, 0.1, pz); scene.add(pot);
    // Soil
    const soil = new THREE.Mesh(new THREE.CylinderGeometry(0.09, 0.09, 0.02, 8), new THREE.MeshLambertMaterial({ color: 0x3a2a1a }));
    soil.position.set(px, 0.21, pz); scene.add(soil);
    // Foliage (sphere cluster)
    const foliageMat = new THREE.MeshLambertMaterial({ color: 0x2d5a1e });
    const f1 = new THREE.Mesh(new THREE.SphereGeometry(0.15, 8, 6), foliageMat);
    f1.position.set(px, 0.45, pz); scene.add(f1);
    const f2 = new THREE.Mesh(new THREE.SphereGeometry(0.1, 6, 5), foliageMat);
    f2.position.set(px+0.08, 0.55, pz-0.05); scene.add(f2);
  });

  // Framed art on right wall
  const artCanvas = document.createElement('canvas'); artCanvas.width = 256; artCanvas.height = 192;
  const ax = artCanvas.getContext('2d');
  // Generate an abstract wallcovering pattern
  ax.fillStyle = '#1a2744'; ax.fillRect(0, 0, 256, 192);
  ax.globalAlpha = 0.3; ax.fillStyle = '#c9a96e';
  for (let y = 0; y < 6; y++) for (let x = 0; x < 4; x++) {
    const cx = x*64+32+(y%2?32:0), cy = y*32+16;
    ax.beginPath(); ax.moveTo(cx, cy-12); ax.quadraticCurveTo(cx+14, cy, cx, cy+12);
    ax.quadraticCurveTo(cx-14, cy, cx, cy-12); ax.fill();
  }
  ax.globalAlpha = 1;
  // Frame border
  ax.strokeStyle = '#c9a96e'; ax.lineWidth = 4; ax.strokeRect(2, 2, 252, 188);
  const artTex = new THREE.CanvasTexture(artCanvas);
  const artFrame = new THREE.Mesh(new THREE.BoxGeometry(0.04, 0.55, 0.72), MAT.frame);
  artFrame.position.set(W/2-0.04, 1.5, -0.5); scene.add(artFrame);
  const artFace = new THREE.Mesh(new THREE.PlaneGeometry(0.5, 0.65), new THREE.MeshBasicMaterial({ map: artTex }));
  artFace.rotation.y = -Math.PI/2; artFace.position.set(W/2-0.06, 1.5, -0.5); scene.add(artFace);
}

function buildBookShelf(x, z, rot, unitW) {
  const g = new THREE.Group();
  g.position.set(x, 0, z); g.rotation.y = rot;

  // Frame verticals
  [[-unitW/2], [unitW/2]].forEach(([fx]) => {
    const v = new THREE.Mesh(new THREE.BoxGeometry(0.03, 1.5, 0.03), MAT.frame);
    v.position.set(fx, 0.75, 0); g.add(v);
  });

  // 4 shelves + books (merged blocks — fewer meshes for better FPS)
  for (let s = 0; s < 4; s++) {
    const sy = s * 0.35 + 0.05;
    const shelf = new THREE.Mesh(new THREE.BoxGeometry(unitW, 0.012, 0.32), MAT.frame);
    shelf.position.set(0, sy, 0); g.add(shelf);

    // Books — use 3 wide blocks per shelf instead of individual books
    const blockW = (unitW - 0.06) / 3;
    for (let b = 0; b < 3; b++) {
      const bh = 0.25 + Math.random() * 0.06;
      const book = new THREE.Mesh(new THREE.BoxGeometry(blockW - 0.02, bh, 0.27), MAT.books[Math.floor(Math.random() * 8)]);
      book.position.set(-unitW/2 + 0.03 + b * blockW + blockW/2, sy + 0.01 + bh/2, 0);
      g.add(book);
    }
  }
  scene.add(g);
}

// ============================================================
// PRODUCTS + WING BOARDS — optimized with shared texture pool
// ============================================================
// XHR wrapper — fetch() fails when page URL contains embedded credentials
function xhrGet(url) {
  return new Promise((resolve, reject) => {
    const xhr = new XMLHttpRequest();
    xhr.open('GET', url, true);
    xhr.setRequestHeader('Authorization', 'Basic ' + btoa('admin:DWSecure2024!'));
    xhr.onload = () => { try { resolve(JSON.parse(xhr.responseText)); } catch(e) { reject(e); } };
    xhr.onerror = () => reject(new Error('XHR failed'));
    xhr.timeout = 15000;
    xhr.ontimeout = () => reject(new Error('XHR timeout'));
    xhr.send();
  });
}

async function loadProducts() {
  try {
    const data = await xhrGet('/api/showroom/products');
    products = data.products || [];
  } catch (e) { console.warn('Products load error:', e.message); products = []; }

  try {
    const data = await xhrGet('/api/showroom/vendors');
    vendors = data.vendors || [];
  } catch (e) { console.warn('Vendors load error:', e.message); vendors = []; }

  updateLoadStatus('Building wing wall...', 80);
  buildWingWall();
  updateLoadStatus('Ready!', 100);
  setTimeout(() => {
    document.getElementById('loading-screen').classList.add('fade-out');
    // Show welcome tooltip, auto-fade after 6 seconds
    setTimeout(() => {
      const tip = document.getElementById('welcome-tip');
      if (tip) tip.classList.add('fade-out');
    }, 6000);
  }, 400);
  populateVendorSidebar();
  startTVSlideshow();
}

function buildWingWall() {
  const W = CONFIG.room.width, D = CONFIG.room.depth;
  const WC = CONFIG.wing;
  const margin = WC.wallMargin;
  const availW = W - 2 * margin;  // 7.0m usable
  const totalWings = WC.totalCount;
  const slotW = availW / totalWings;  // 0.14m per slot
  const wingW = slotW - 0.02;        // 0.12m wing face width
  const backZ = -D / 2 + 0.2;        // 0.2m off back wall

  // Group products by vendor
  const vendorOrder = ['Thibaut', 'Schumacher', 'Arte', 'Elitis'];
  const vendorGroups = {};
  products.forEach(p => {
    const v = p.vendor;
    if (!vendorGroups[v]) vendorGroups[v] = [];
    vendorGroups[v].push(p);
  });

  // Build flat product array in vendor order
  let allProds = [];
  const vendorSections = [];
  vendorOrder.forEach(v => {
    const prods = vendorGroups[v] || [];
    if (prods.length === 0) return;
    const startIdx = allProds.length;
    allProds = allProds.concat(prods);
    vendorSections.push({ vendor: v, startIdx, endIdx: allProds.length - 1 });
  });

  // Fill remaining slots with texture pool synthetics if < 50
  while (allProds.length < totalWings) {
    const i = allProds.length;
    const poolIdx = i % TEXTURE_POOL.length;
    allProds.push({
      pattern_name: TEXTURE_POOL[poolIdx].pattern,
      color_name: TEXTURE_POOL[poolIdx].color,
      sku: `DW-SYN-${1000 + i}`,
      vendor: 'Showroom Collection',
      width: 27,
      material: ['Vinyl','Non-woven','Paper','Grasscloth','Fabric'][i % 5],
      _poolIdx: poolIdx
    });
  }
  allProds = allProds.slice(0, totalWings);

  // Single wall group at back wall
  const wallGroup = new THREE.Group();
  wallGroup.position.set(0, 0, backZ);

  // Chrome rail spanning full width
  const rail = new THREE.Mesh(
    new THREE.BoxGeometry(availW + 0.04, 0.015, 0.015), MAT.chrome
  );
  rail.position.set(0, WC.height + 0.08, 0);
  wallGroup.add(rail);

  // Rail end caps
  [-1, 1].forEach(side => {
    const cap = new THREE.Mesh(new THREE.SphereGeometry(0.012, 8, 6), MAT.chrome);
    cap.position.set(side * (availW / 2 + 0.02), WC.height + 0.08, 0);
    wallGroup.add(cap);
  });

  // Build each wing
  for (let i = 0; i < totalWings; i++) {
    const product = allProds[i];
    const wingX = -availW / 2 + i * slotW + slotW / 2;

    const pivot = new THREE.Group();
    pivot.position.set(wingX, 0, 0);

    const poolIdx = product._poolIdx !== undefined ? product._poolIdx : (i % TEXTURE_POOL.length);
    const poolMat = TEXTURE_POOL[poolIdx].material;
    const face = new THREE.Mesh(new THREE.PlaneGeometry(wingW, WC.height), poolMat);
    face.position.set(wingW / 2, WC.height / 2 + 0.04, 0);
    face.userData = { isWingFace: true, parentPivot: pivot };
    pivot.add(face);

    if (product.image) {
      face.userData.pendingImage = product.image;
      face.userData.imageLoaded = false;
    }

    let widthInches = 27;
    if (typeof product.width === 'string') {
      const m = product.width.match(/([\d.]+)/);
      if (m) widthInches = parseFloat(m[1]);
    } else if (typeof product.width === 'number') {
      widthInches = product.width;
    }

    const normalizedProduct = {
      pattern_name: product.pattern_name || product.name || 'Pattern',
      color: product.color_name || product.color || '',
      sku: product.sku || product.mfr_sku || '',
      vendor: product.vendor || 'Unknown',
      width_inches: widthInches,
      material: product.material || 'Non-woven',
      name: product.pattern_name || product.name || 'Pattern',
      image: product.image || null,
      collection: product.collection || ''
    };

    pivot.userData = {
      product: normalizedProduct, index: i,
      isOpen: false, targetAngle: 0, currentAngle: 0,
      boardWidth: wingW, wallGroup, wingDirection: 1
    };
    wallGroup.add(pivot);
    wingBoards.push(pivot);
  }

  // Vendor section labels and dividers
  const vendorColors = { 'Thibaut':'#1a5276','Schumacher':'#922b21','Arte':'#d4ac0d','Elitis':'#a04000' };
  vendorSections.forEach(section => {
    const vColor = vendorColors[section.vendor] || '#555555';
    const startX = -availW / 2 + section.startIdx * slotW + slotW / 2;
    const endX = -availW / 2 + section.endIdx * slotW + slotW / 2;
    const centerX = (startX + endX) / 2;
    const sectionW = (section.endIdx - section.startIdx + 1) * slotW;

    // Vendor label canvas
    const lc = document.createElement('canvas'); lc.width = 512; lc.height = 48;
    const lx = lc.getContext('2d');
    lx.fillStyle = vColor; lx.fillRect(0, 0, 512, 48);
    const grad = lx.createLinearGradient(0, 0, 0, 48);
    grad.addColorStop(0, 'rgba(255,255,255,0.15)'); grad.addColorStop(1, 'rgba(0,0,0,0.2)');
    lx.fillStyle = grad; lx.fillRect(0, 0, 512, 48);
    lx.fillStyle = '#ffffff'; lx.font = 'bold 26px sans-serif'; lx.textAlign = 'center'; lx.textBaseline = 'middle';
    lx.fillText(section.vendor.toUpperCase(), 256, 26);
    const lt = new THREE.CanvasTexture(lc);
    const labelW = Math.max(sectionW * 0.8, 0.3);
    const label = new THREE.Mesh(new THREE.PlaneGeometry(labelW, 0.07), new THREE.MeshBasicMaterial({ map: lt }));
    label.position.set(centerX, WC.height + 0.15, 0.01);
    wallGroup.add(label);

    // Thin vertical divider at section start (except first)
    if (section.startIdx > 0) {
      const divX = -availW / 2 + section.startIdx * slotW - slotW * 0.1;
      const divider = new THREE.Mesh(
        new THREE.BoxGeometry(0.003, WC.height * 0.6, 0.003),
        new THREE.MeshBasicMaterial({ color: parseInt(vColor.replace('#',''), 16) })
      );
      divider.position.set(divX, WC.height * 0.3 + 0.04, 0);
      wallGroup.add(divider);
    }
  });

  scene.add(wallGroup);
  wingRacks.push({ group: wallGroup, vendor: 'All Vendors', products: allProds });
}

// Shared texture loader + image cache
const texLoader = new THREE.TextureLoader();
const imageTexCache = {};

// buildWingRack removed — replaced by buildWingWall() above

function loadWingTexture(faceMesh, imageUrl, onDone) {
  // Use image proxy to avoid CORS
  const proxyUrl = '/api/proxy/image?url=' + encodeURIComponent(imageUrl);

  // Check cache first
  if (imageTexCache[imageUrl]) {
    faceMesh.material = new THREE.MeshLambertMaterial({ map: imageTexCache[imageUrl] });
    if (onDone) onDone();
    return;
  }

  texLoader.load(proxyUrl, (texture) => {
    texture.wrapS = texture.wrapT = THREE.ClampToEdgeWrapping;
    texture.minFilter = THREE.LinearFilter;
    texture.magFilter = THREE.LinearFilter;
    imageTexCache[imageUrl] = texture;
    faceMesh.material = new THREE.MeshLambertMaterial({ map: texture });
    if (onDone) onDone();
  }, undefined, () => {
    if (onDone) onDone();
  });
}

// ============================================================
// WING INTERACTION — Two-step: click to move close, buttons to open
// ============================================================
function onCanvasClick(event) {
  // If controls are locked (focused on a wing), ignore canvas clicks
  if (controlsLocked) return;

  mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
  mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
  raycaster.setFromCamera(mouse, camera);

  const faces = [];
  wingBoards.forEach(p => p.children.forEach(c => { if (c.userData && c.userData.isWingFace) faces.push(c); }));

  const hits = raycaster.intersectObjects(faces, false);
  if (hits.length > 0) {
    focusOnWing(hits[0].object.userData.parentPivot);
  }
}

// STEP 1: Click on wing — fly camera close, then HARD LOCK position
function focusOnWing(pivot) {
  const product = pivot.userData.product;

  // Close any active fan cascade
  if (fanWings.length > 0) closeFanCascade(true);

  // If re-clicking same focused wing, unfocus
  if (focusedWing === pivot) { unfocusWing(); return; }
  // Close previous focus
  if (focusedWing) unfocusWing(true);

  // Save current camera to restore later
  previousCamPos = camera.position.clone();
  previousCamTarget = controls.target.clone();

  focusedWing = pivot;

  // Calculate world position of the wing
  const wingWorldPos = new THREE.Vector3();
  pivot.getWorldPosition(wingWorldPos);

  // All wings face forward (+Z) on back wall — camera goes 1.2m in front
  const camTarget = new THREE.Vector3(wingWorldPos.x, 1.2, wingWorldPos.z);
  const camPos = new THREE.Vector3(wingWorldPos.x, 1.6, wingWorldPos.z + 1.2);

  // Fly camera there, then hard-lock
  smoothCameraTo(camPos, camTarget, () => {
    lockControls(camPos, camTarget);
  });

  // Lazy-load this wing's image immediately on focus
  const face = pivot.children[0];
  if (face && face.userData.pendingImage && !face.userData.imageLoaded) {
    face.userData.imageLoaded = true;
    loadWingTexture(face, face.userData.pendingImage);
  }

  // Show detail panel with product info
  showWingDetail(product);
  updateOpenBtnState(false);
  document.getElementById('info-text').textContent = `Wing: ${product.pattern_name || product.name} by ${product.vendor} | Fan open to browse adjacent patterns`;
}

// STEP 2: Fan Cascade — open center wing + 3 adjacent on each side
function fanCascadeOpen(direction) {
  if (!focusedWing) return;
  const centerIdx = focusedWing.userData.index;
  const fc = CONFIG.wing.fanCascade;

  // If fan already open on this wing in this direction, close it
  if (fanCenterIndex === centerIdx && fanWings.length > 0 && focusedWing.userData.wingDirection === direction) {
    closeFanCascade();
    return;
  }

  // Close any existing fan first
  if (fanWings.length > 0) closeFanCascade(true);

  fanCenterIndex = centerIdx;
  fanWings = [];
  focusedWing.userData.wingDirection = direction;

  // Cascade rings: [offset, angle]
  const rings = [
    [0, fc.center],  // 90° center
    [1, fc.ring1],   // 60° ±1
    [2, fc.ring2],   // 40° ±2
    [3, fc.ring3]    // 20° ±3
  ];

  rings.forEach(([offset, angle]) => {
    if (offset === 0) {
      const pivot = wingBoards[centerIdx];
      pivot.userData.targetAngle = direction * angle;
      if (!animatingWings.includes(pivot)) animatingWings.push(pivot);
      fanWings.push(pivot);
    } else {
      // Left neighbor
      const leftIdx = centerIdx - offset;
      if (leftIdx >= 0) {
        const lp = wingBoards[leftIdx];
        lp.userData.targetAngle = direction * angle;
        if (!animatingWings.includes(lp)) animatingWings.push(lp);
        fanWings.push(lp);
      }
      // Right neighbor
      const rightIdx = centerIdx + offset;
      if (rightIdx < wingBoards.length) {
        const rp = wingBoards[rightIdx];
        rp.userData.targetAngle = direction * angle;
        if (!animatingWings.includes(rp)) animatingWings.push(rp);
        fanWings.push(rp);
      }
    }
  });

  updateOpenBtnState(direction);
}

function closeFanCascade(skipBtnUpdate) {
  fanWings.forEach(pivot => {
    pivot.userData.targetAngle = 0;
    if (!animatingWings.includes(pivot)) animatingWings.push(pivot);
  });
  fanWings = [];
  fanCenterIndex = -1;
  if (!skipBtnUpdate) updateOpenBtnState(0);
}

// Back button — close fan, unlock controls, fly camera back
function unfocusWing(skipPanel) {
  if (fanWings.length > 0) closeFanCascade();
  focusedWing = null;
  if (!skipPanel) document.getElementById('wing-detail').classList.add('hidden');

  // Unlock first so smoothCameraTo can move
  unlockControls();

  // Fly back to previous position
  if (previousCamPos && previousCamTarget) {
    smoothCameraTo(previousCamPos, previousCamTarget);
    previousCamPos = null;
    previousCamTarget = null;
  }
  document.getElementById('info-text').textContent = 'WASD to walk | Click a wing board to view pattern | M for minimap';
}

function showWingDetail(product) {
  const panel = document.getElementById('wing-detail');
  panel.classList.remove('hidden');
  document.getElementById('detail-pattern').textContent = product.pattern_name || product.name || 'Pattern';
  document.getElementById('detail-color').textContent = product.color || product.color_name || '';
  document.getElementById('detail-vendor').textContent = product.vendor || '';
  document.getElementById('detail-sku').textContent = product.sku || '';
  const w = product.width_inches || 27;
  document.getElementById('detail-specs').textContent = `${w}" wide | ${product.material||'Non-woven'}${product.collection ? ' | ' + product.collection : ''}`;
}

function updateOpenBtnState(dir) {
  const btnL = document.getElementById('btn-fan-left');
  const btnR = document.getElementById('btn-fan-right');
  if (btnL) {
    btnL.classList.toggle('active', dir === 1);
    btnL.textContent = dir === 1 ? '\u25AE\u25AE Close' : '\u25C4 Fan Left';
  }
  if (btnR) {
    btnR.classList.toggle('active', dir === -1);
    btnR.textContent = dir === -1 ? '\u25AE\u25AE Close' : 'Fan Right \u25BA';
  }
}

function lockControls(pos, target) {
  controlsLocked = true;
  controls.enabled = false;
  controls.enableRotate = false;
  controls.enablePan = false;
  controls.enableZoom = false;
  lockedCamPos = pos ? pos.clone() : camera.position.clone();
  lockedCamTarget = target ? target.clone() : controls.target.clone();
  document.getElementById('showroom-canvas').style.cursor = 'default';
}

function unlockControls() {
  controlsLocked = false;
  controls.enabled = true;
  controls.enableRotate = true;
  controls.enablePan = true;
  controls.enableZoom = true;
  lockedCamPos = null;
  lockedCamTarget = null;
}

let _lastMoveRaycast = 0;
function onCanvasMouseMove(event) {
  if (controlsLocked) return;
  // Throttle raycasting to every 100ms — saves huge CPU on mousemove
  const now = performance.now();
  if (now - _lastMoveRaycast < 100) return;
  _lastMoveRaycast = now;

  mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
  mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
  raycaster.setFromCamera(mouse, camera);

  const faces = [];
  wingBoards.forEach(p => p.children.forEach(c => { if (c.userData && c.userData.isWingFace) faces.push(c); }));

  document.getElementById('showroom-canvas').style.cursor = raycaster.intersectObjects(faces, false).length > 0 ? 'pointer' : 'default';
}

// ============================================================
// WASD WALKING — first-person navigation
// ============================================================
function updateWASD(dt) {
  if (controlsLocked || cameraAnim) return; // no walking when focused on wing or animating

  let moveX = 0, moveZ = 0;
  if (keysDown['KeyW'] || keysDown['ArrowUp']) moveZ = -1;
  if (keysDown['KeyS'] || keysDown['ArrowDown']) moveZ = 1;
  if (keysDown['KeyA'] || keysDown['ArrowLeft']) moveX = -1;
  if (keysDown['KeyD'] || keysDown['ArrowRight']) moveX = 1;
  if (moveX === 0 && moveZ === 0) return;

  // Get camera forward/right vectors projected onto XZ plane
  const forward = new THREE.Vector3();
  camera.getWorldDirection(forward);
  forward.y = 0; forward.normalize();
  const right = new THREE.Vector3().crossVectors(forward, new THREE.Vector3(0, 1, 0)).normalize();

  // Movement delta
  const speed = WALK_SPEED * dt;
  const delta = new THREE.Vector3();
  delta.addScaledVector(forward, -moveZ * speed);
  delta.addScaledVector(right, moveX * speed);

  // Move both camera and orbit target together (preserves view direction)
  camera.position.add(delta);
  controls.target.add(delta);
}

// ============================================================
// LAZY TEXTURE LOADING — only load images for nearby racks
// ============================================================
let _lazyLoadQueue = [];
let _lazyLoading = 0;
const MAX_CONCURRENT_LOADS = 3;

function lazyLoadNearbyTextures() {
  const camPos = camera.position;
  const loadRadius = 3.5; // Load textures within 3.5m of camera
  const _worldPos = new THREE.Vector3();

  wingBoards.forEach(pivot => {
    pivot.children.forEach(face => {
      if (!face.userData || !face.userData.pendingImage || face.userData.imageLoaded) return;
      // Get wing world position directly (all wings in single wallGroup)
      pivot.getWorldPosition(_worldPos);
      const dx = _worldPos.x - camPos.x;
      const dz = _worldPos.z - camPos.z;
      const dist = Math.sqrt(dx*dx + dz*dz);
      if (dist < loadRadius && _lazyLoading < MAX_CONCURRENT_LOADS) {
        face.userData.imageLoaded = true;
        _lazyLoading++;
        loadWingTexture(face, face.userData.pendingImage, () => { _lazyLoading--; });
      }
    });
  });
}

// ============================================================
// ANIMATION LOOP
// ============================================================
function animate() {
  requestAnimationFrame(animate);

  // Time delta — MUST call every frame to avoid huge spikes after pauses
  const dt = Math.min(clock.getDelta(), 0.1); // cap at 100ms to handle tab focus

  // Advance camera animation (time-based, runs in main loop)
  if (cameraAnim) {
    cameraAnim.progress += dt / cameraAnim.duration;
    if (cameraAnim.progress >= 1) {
      camera.position.copy(cameraAnim.endPos);
      controls.target.copy(cameraAnim.endTarget);
      const cb = cameraAnim.onComplete;
      cameraAnim = null;
      if (cb) cb();
    } else {
      const e = 1 - Math.pow(1 - cameraAnim.progress, 3); // cubic ease-out
      camera.position.lerpVectors(cameraAnim.startPos, cameraAnim.endPos, e);
      controls.target.lerpVectors(cameraAnim.startTarget, cameraAnim.endTarget, e);
    }
  }

  // HARD LOCK: if controls are locked, force camera position every frame
  if (controlsLocked && lockedCamPos && lockedCamTarget) {
    camera.position.copy(lockedCamPos);
    controls.target.copy(lockedCamTarget);
  } else if (!cameraAnim) {
    updateWASD(dt);
    controls.update();

    // Collision detection — prevent camera from walking through walls
    const margin = 0.3;
    const hw = CONFIG.room.width / 2 - margin;
    const hd = CONFIG.room.depth / 2 - margin;
    camera.position.x = Math.max(-hw, Math.min(hw, camera.position.x));
    camera.position.z = Math.max(-hd, Math.min(CONFIG.room.depth / 2 + 0.5, camera.position.z));
    camera.position.y = Math.max(0.5, Math.min(CONFIG.room.height - 0.2, camera.position.y));
    controls.target.x = Math.max(-hw, Math.min(hw, controls.target.x));
    controls.target.z = Math.max(-hd, Math.min(CONFIG.room.depth / 2 + 0.5, controls.target.z));
    controls.target.y = Math.max(0.3, Math.min(CONFIG.room.height - 0.3, controls.target.y));
  }

  // Animate wings
  for (let i = animatingWings.length - 1; i >= 0; i--) {
    const p = animatingWings[i], ud = p.userData;
    const diff = ud.targetAngle - ud.currentAngle;
    if (Math.abs(diff) < 0.005) {
      ud.currentAngle = ud.targetAngle;
      p.rotation.y = ud.currentAngle;
      animatingWings.splice(i, 1);
    } else {
      ud.currentAngle += diff * CONFIG.wing.animSpeed * 3;
      p.rotation.y = ud.currentAngle;
    }
  }

  // Lazy texture loading — check every 30 frames for nearby racks
  if (frameCount % 30 === 0) {
    lazyLoadNearbyTextures();
  }

  // Update minimap every 10 frames (cheap but no need every frame)
  if (frameCount % 10 === 0) updateMinimap();

  // FPS
  frameCount++;
  const now = performance.now();
  if (now - lastFpsTime > 1000) {
    const fps = Math.round(frameCount * 1000 / (now - lastFpsTime));
    const el = document.getElementById('fps-counter');
    el.textContent = fps + ' FPS';
    el.style.color = fps >= 50 ? '#4a4' : fps >= 25 ? '#aa4' : '#a44';
    frameCount = 0; lastFpsTime = now;
  }

  renderer.render(scene, camera);
}

// ============================================================
// HUD
// ============================================================
function initHUD() {
  document.querySelectorAll('.nav-btn').forEach(btn => btn.addEventListener('click', () => {
    document.querySelectorAll('.nav-btn').forEach(b => b.classList.remove('active'));
    btn.classList.add('active');
    navigateToSection(btn.dataset.section);
  }));

  document.getElementById('close-detail').addEventListener('click', () => unfocusWing());
  document.getElementById('btn-fan-left').addEventListener('click', () => fanCascadeOpen(1));
  document.getElementById('btn-fan-right').addEventListener('click', () => fanCascadeOpen(-1));
  document.getElementById('btn-back-wing').addEventListener('click', () => unfocusWing());

  document.getElementById('btn-music').addEventListener('click', () => document.getElementById('music-player').classList.toggle('hidden'));
  document.getElementById('close-music').addEventListener('click', () => document.getElementById('music-player').classList.add('hidden'));

  let audioCtx, musicPlaying = false;
  document.getElementById('music-play').addEventListener('click', () => {
    if (!audioCtx) audioCtx = new (window.AudioContext || window.webkitAudioContext)();
    musicPlaying = !musicPlaying;
    document.getElementById('music-play').innerHTML = musicPlaying ? '&#9646;&#9646;' : '&#9654;';
    if (musicPlaying) playAmbientMusic(audioCtx);
  });

  let lightMode = 0;
  const modes = ['Showroom', 'Gallery', 'Evening'];
  document.getElementById('btn-lighting').addEventListener('click', () => {
    lightMode = (lightMode + 1) % 3;
    applyLightingMode(modes[lightMode]);
    document.getElementById('info-text').textContent = `Lighting: ${modes[lightMode]}`;
  });

  document.getElementById('btn-fullscreen').addEventListener('click', () => {
    if (!document.fullscreenElement) document.documentElement.requestFullscreen();
    else document.exitFullscreen();
  });

  document.getElementById('btn-add-sample').addEventListener('click', () => {
    if (focusedWing) {
      const p = focusedWing.userData.product;
      if (!sampleTray.find(s => s.sku === p.sku)) { sampleTray.push(p); updateSampleTray(); }
    }
  });
}

function navigateToSection(section) {
  // If focused on a wing, release it first
  if (focusedWing) unfocusWing();
  if (controlsLocked) unlockControls();

  const D = CONFIG.room.depth, W = CONFIG.room.width;
  // Bookshelves are on front wall flanking the doorway (x=±2.8, z=D/2-0.3)
  const t = { overview: [[0,1.6,1.0],[0,1.2,-0.5]], wings: [[0,1.6,-D/2+3.0],[0,1.2,-D/2+0.2]], books: [[1.5,1.4,1.8],[2.8,0.9,D/2-0.3]], table: [[-1,1.8,2.0],[-1,0.75,1.0]] }[section] || [[0,1.6,1.0],[0,1.2,-0.5]];
  smoothCameraTo(new THREE.Vector3(...t[0]), new THREE.Vector3(...t[1]));
}

function populateVendorSidebar() {
  const list = document.getElementById('vendor-list');
  const vc = { 'Thibaut':'#1a5276','Schumacher':'#922b21','Arte':'#d4ac0d','Elitis':'#a04000','Showroom Collection':'#888888' };

  // Build vendor sections from wingBoards array
  const sections = [];
  let cur = null;
  wingBoards.forEach((pivot, i) => {
    const v = pivot.userData.product.vendor;
    if (v !== cur) {
      if (cur) sections[sections.length-1].end = i - 1;
      sections.push({ vendor: v, start: i, end: i });
      cur = v;
    }
  });
  if (sections.length > 0) sections[sections.length-1].end = wingBoards.length - 1;

  const D = CONFIG.room.depth;
  const backZ = -D / 2 + 0.2;

  sections.forEach(sec => {
    const count = sec.end - sec.start + 1;
    const div = document.createElement('div');
    div.className = 'vendor-item';
    const dot = document.createElement('span');
    dot.className = 'vendor-dot';
    dot.style.background = vc[sec.vendor] || '#888';
    div.appendChild(dot);
    div.appendChild(document.createTextNode(sec.vendor));
    const cnt = document.createElement('span');
    cnt.className = 'vendor-count';
    cnt.textContent = count;
    div.appendChild(cnt);

    div.addEventListener('click', () => {
      if (focusedWing) unfocusWing();
      document.querySelectorAll('.vendor-item').forEach(i => i.classList.remove('active'));
      div.classList.add('active');
      // Center camera on vendor section
      const midIdx = Math.floor((sec.start + sec.end) / 2);
      const midWing = wingBoards[midIdx];
      const wp = new THREE.Vector3();
      midWing.getWorldPosition(wp);
      smoothCameraTo(new THREE.Vector3(wp.x, 1.6, backZ + 2.0), new THREE.Vector3(wp.x, 1.2, backZ));
    });
    list.appendChild(div);
  });
}

function smoothCameraTo(pos, target, onComplete) {
  // Clock-based animation — runs inside main animate() loop, not a competing rAF chain
  cameraAnim = {
    startPos: camera.position.clone(),
    endPos: pos.clone(),
    startTarget: controls.target.clone(),
    endTarget: target.clone(),
    progress: 0,
    duration: 0.8, // seconds
    onComplete: onComplete || null
  };
}

function updateSampleTray() {
  const tray = document.getElementById('sample-tray'), items = document.getElementById('tray-items');
  document.getElementById('tray-count').textContent = sampleTray.length;
  if (sampleTray.length > 0) {
    tray.classList.add('visible'); items.innerHTML = '';
    const cc = {'Navy':'#1a2744','Sage':'#6b7f5e','Cream':'#f0ead6','Gold':'#c9a96e','Silver':'#b8b8c0','Blush':'#d4a0a0','Charcoal':'#3a3a42','Ivory':'#f5f0e8','Slate':'#5a6068','Teal':'#2a6b6b','Coral':'#cd6858','Burgundy':'#6b2040'};
    sampleTray.forEach(p => { const d = document.createElement('div'); d.className = 'tray-item'; d.style.background = cc[p.color]||'#d8d4cc'; d.title = `${p.pattern_name} - ${p.color}`; items.appendChild(d); });
  } else tray.classList.remove('visible');
}

function applyLightingMode(mode) {
  scene.children.filter(c => c.isLight).forEach(l => {
    if (l.isAmbientLight) l.intensity = mode==='Gallery' ? 0.45 : mode==='Evening' ? 0.2 : 0.7;
    else if (l.isHemisphereLight) l.intensity = mode==='Gallery' ? 0.35 : mode==='Evening' ? 0.15 : 0.5;
    else if (l.isDirectionalLight) l.intensity = mode==='Gallery' ? 0.4 : mode==='Evening' ? 0.15 : 0.6;
  });
  scene.fog.density = mode==='Evening' ? 0.06 : mode==='Gallery' ? 0.03 : 0.02;
}

function playAmbientMusic(ctx) {
  const o1 = ctx.createOscillator(), o2 = ctx.createOscillator(), g = ctx.createGain();
  o1.type = 'sine'; o1.frequency.value = 220; o2.type = 'sine'; o2.frequency.value = 329.63;
  g.gain.value = 0.04;
  const vol = document.getElementById('music-volume');
  vol.addEventListener('input', () => g.gain.value = (parseInt(vol.value)/100)*0.08);
  o1.connect(g); o2.connect(g); g.connect(ctx.destination); o1.start(); o2.start();
}

function drawTVSlide(idx) {
  const tv = window._tvScreen; if (!tv) return;
  const ctx = tv.ctx, w = 480, h = 270;
  // Color palette for slides
  const bgColors = ['#0f1923','#1a0f23','#0f2318','#23190f','#0f1a23','#1f0f23'];
  const accentColors = ['#c9a96e','#6eafc9','#c96e8a','#6ec99b','#c9b56e','#9b6ec9'];

  ctx.fillStyle = bgColors[idx % bgColors.length]; ctx.fillRect(0, 0, w, h);

  // Get product data or use fallback
  const allProds = products.length > 0 ? products : [];
  if (allProds.length > 0) {
    const p = allProds[idx % allProds.length];
    const accent = accentColors[idx % accentColors.length];
    // Decorative line
    ctx.fillStyle = accent; ctx.fillRect(30, 60, 3, 100);
    // Vendor
    ctx.fillStyle = accent; ctx.font = 'bold 14px sans-serif'; ctx.textAlign = 'left';
    ctx.fillText((p.vendor || '').toUpperCase(), 45, 78);
    // Pattern name
    ctx.fillStyle = '#ffffff'; ctx.font = 'bold 28px sans-serif';
    ctx.fillText(p.pattern_name || p.name || 'Pattern', 45, 115);
    // Color name
    ctx.fillStyle = '#888888'; ctx.font = '18px sans-serif';
    ctx.fillText(p.color_name || p.color || '', 45, 145);
    // SKU
    ctx.fillStyle = '#555555'; ctx.font = '12px monospace';
    ctx.fillText(p.sku || '', 45, 170);
    // DW branding
    ctx.fillStyle = '#c9a96e'; ctx.font = 'bold 16px sans-serif'; ctx.textAlign = 'right';
    ctx.fillText('DW', w - 30, h - 20);
    ctx.fillStyle = '#666'; ctx.font = '10px sans-serif';
    ctx.fillText('DESIGNER WALLCOVERINGS', w - 55, h - 20);
  } else {
    // Loading placeholder
    ctx.fillStyle = '#c9a96e'; ctx.font = 'bold 24px sans-serif'; ctx.textAlign = 'center';
    ctx.fillText('DESIGNER WALLCOVERINGS', w/2, h/2 - 10);
    ctx.fillStyle = '#666'; ctx.font = '14px sans-serif';
    ctx.fillText('Sherman Oaks Showroom', w/2, h/2 + 20);
  }
  tv.texture.needsUpdate = true;
}

// Start TV slideshow after products load
function startTVSlideshow() {
  if (!window._tvScreen) return;
  setInterval(() => {
    window._tvScreen.slideIdx++;
    drawTVSlide(window._tvScreen.slideIdx);
  }, 5000);
}

// ============================================================
// MINIMAP — 2D floor plan overlay
// ============================================================
let minimapVisible = true;
function updateMinimap() {
  const mc = document.getElementById('minimap');
  if (!mc || !minimapVisible) return;
  const ctx = mc.getContext('2d');
  const mw = 160, mh = 128;
  const W = CONFIG.room.width, D = CONFIG.room.depth;
  // Scale: room space → minimap pixels
  const sx = (mw - 20) / W, sy = (mh - 20) / D;
  const ox = mw/2, oy = mh/2; // center offset

  ctx.clearRect(0, 0, mw, mh);

  // Room outline
  ctx.strokeStyle = 'rgba(201,169,110,0.4)'; ctx.lineWidth = 1;
  ctx.strokeRect(ox - W/2*sx, oy - D/2*sy, W*sx, D*sy);

  // Door opening (front wall center)
  ctx.strokeStyle = 'rgba(10,10,15,1)'; ctx.lineWidth = 3;
  ctx.beginPath(); ctx.moveTo(ox - 0.6*sx, oy + D/2*sy); ctx.lineTo(ox + 0.6*sx, oy + D/2*sy); ctx.stroke();

  // Wing wall — single gold bar along back wall with vendor-colored sections
  const backY = oy + (-D/2 + 0.2) * sy;
  const vc = { 'Thibaut':'#1a5276','Schumacher':'#922b21','Arte':'#d4ac0d','Elitis':'#a04000' };
  // Draw vendor sections from wingBoards data
  let curVendor = null, secStart = 0;
  for (let i = 0; i <= wingBoards.length; i++) {
    const v = i < wingBoards.length ? wingBoards[i].userData.product.vendor : null;
    if (v !== curVendor) {
      if (curVendor) {
        const x0 = ox + (-CONFIG.room.width/2 + CONFIG.wing.wallMargin + secStart * (CONFIG.room.width - 2*CONFIG.wing.wallMargin)/CONFIG.wing.totalCount) * sx;
        const x1 = ox + (-CONFIG.room.width/2 + CONFIG.wing.wallMargin + i * (CONFIG.room.width - 2*CONFIG.wing.wallMargin)/CONFIG.wing.totalCount) * sx;
        ctx.fillStyle = vc[curVendor] || '#c9a96e';
        ctx.fillRect(x0, backY - 2, x1 - x0, 4);
      }
      curVendor = v;
      secStart = i;
    }
  }

  // Table
  ctx.fillStyle = 'rgba(92,64,51,0.6)';
  ctx.fillRect(ox + (-1.0)*sx - 9, oy + 1.5*sy - 5, 18, 10);

  // Camera position + direction
  const cx = ox + camera.position.x * sx, cy = oy + camera.position.z * sy;
  // Direction arrow
  const fwd = new THREE.Vector3(); camera.getWorldDirection(fwd);
  ctx.strokeStyle = '#c9a96e'; ctx.lineWidth = 2;
  ctx.beginPath(); ctx.moveTo(cx, cy); ctx.lineTo(cx + fwd.x*12, cy + fwd.z*12); ctx.stroke();
  // Dot
  ctx.fillStyle = '#c9a96e'; ctx.beginPath(); ctx.arc(cx, cy, 3, 0, Math.PI*2); ctx.fill();
}

function updateLoadStatus(text, pct) {
  const f = document.getElementById('loadFill'), s = document.getElementById('loadStatus');
  if (f) f.style.width = pct + '%'; if (s) s.textContent = text;
}

function onResize() {
  camera.aspect = window.innerWidth / window.innerHeight;
  camera.updateProjectionMatrix();
  renderer.setSize(window.innerWidth, window.innerHeight);
}

// Expose camera/controls for programmatic walkthrough scripts
window.addEventListener('DOMContentLoaded', function() {
  init();
  window._cam = camera;
  window._ctrl = controls;
  window._scene = scene;
  window._renderer = renderer;
  window._wingBoards = wingBoards;
  window._wingRacks = wingRacks;
});

// Diagnostics: expose scene stats
window._getSceneStats = function() {
  if (!scene || !renderer) return 'Scene not ready';
  let meshes = 0, groups = 0, lights = 0, total = 0;
  scene.traverse(obj => { total++; if (obj.isMesh) meshes++; if (obj.isGroup) groups++; if (obj.isLight) lights++; });
  const ri = renderer.info;
  return { total, meshes, groups, lights, drawCalls: ri.render.calls, triangles: ri.render.triangles, textures: ri.memory.textures, geometries: ri.memory.geometries, programs: ri.programs ? ri.programs.length : 'n/a' };
};
})();