// ====== Iso 3D Engine for LegoDigital ======
// SOURCE OF TRUTH — edit in web-play/, ship via deploy-product.sh lego-digital.
// Math/physics/camera live in web-play/engine/*.js (window.LegoEngine).
// This file: SVG React shapes + IsoScene / TopDownScene.

const LE = window.LegoEngine;
if (!LE || !LE.ready) {
  throw new Error('LegoEngine scripts must load before engine.jsx (see index.html)');
}

const ISO = LE.ISO;
const TOPSTEP = LE.TOPSTEP;
const COLORS = LE.COLORS;
const COLOR_ORDER = LE.COLOR_ORDER;
const PIECE_TYPES = LE.PIECE_TYPES;

const darken = LE.darken;
const lighten = LE.lighten;
const brickH = LE.brickH;
const zKey = LE.zKey;
const footprintsOverlap = LE.footprintsOverlap;
const zRangesOverlap = LE.zRangesOverlap;
const collidesWith = LE.collidesWith;
const settleBricks = LE.settleBricks;
const canPlace = LE.canPlace;
const countSupports = LE.countSupports;
const supportGraph = LE.supportGraph;
const iso = LE.iso;
const unproject = LE.unproject;
const effSize = LE.effSize;
const applyView = LE.applyView;
const viewedBaseSize = LE.viewedBaseSize;
const invView = LE.invView;
const rotPoint = LE.rotPoint;
const colLabel = LE.colLabel;
const paintOrder = LE.paintOrder;
const pickGroundCell = LE.pickGroundCell;
const pickTopCell = LE.pickTopCell;
const pickTopBrick = LE.pickTopBrick;
const pickBrick = LE.pickBrick;
const pick = LE.pick;
const topZAt = LE.topZAt;
const colorHex = LE.colorHex;
const computeIsoViewBox = LE.computeIsoViewBox;
const computeTopViewBox = LE.computeTopViewBox;
const createCamera = LE.createCamera;
const applyCamera = LE.applyCamera;
const faceShades = LE.faceShades;
const createWorld = LE.createWorld;

const brickGeometry = LE.brickGeometry;

function studPositions(b, origin, coveredCells, opts = {}) {
  if (b.type === 'tile' || b.type === 'wheel' || b.type === 'door' || b.type === 'window' || b.type === 'minifig' || b.noStuds) return [];
  if (b.type === 'cylinder' || b.type === 'cone') return []; // open/round tops drawn separately
  const h = brickH(b);
  const zTop = b.z + (b._lift || 0) + h;
  // Anchor so the stud *top* ellipse lands on the projected cell center (AC-10).
  const sh = ISO.TW * 0.225;
  const studs = [];
  const onlyHigh = opts.slopeHigh;
  const step = (b.w * b.d > 48) ? 2 : 1; // LOD for huge bricks
  for (let i = 0; i < b.w; i += step) {
    for (let j = 0; j < b.d; j += step) {
      if (onlyHigh) {
        const dir = onlyHigh.dir || 'S';
        if (dir === 'S' && j !== 0) continue;
        if (dir === 'N' && j !== b.d - 1) continue;
        if (dir === 'W' && i !== 0) continue;
        if (dir === 'E' && i !== b.w - 1) continue;
      }
      if (coveredCells && coveredCells.has(`${b.x + i},${b.y + j},${zKey(b.z + h)}`)) continue;
      const c = iso(b.x + i + 0.5, b.y + j + 0.5, zTop);
      studs.push({ x: c.x + origin.x, y: c.y + origin.y + sh });
    }
  }
  return studs;
}

// Cells whose stud tops are covered: key "x,y,zTop" where another piece starts at zTop.
function buildCoveredCells(bricks) {
  const s = new Set();
  for (const b of bricks) {
    const ef = effSize(b);
    for (let i = 0; i < ef.w; i++) {
      for (let j = 0; j < ef.d; j++) {
        s.add(`${b.x + i},${b.y + j},${zKey(b.z)}`);
      }
    }
  }
  return s;
}

function svgPoint(evt, svg) {
  const pt = svg.createSVGPoint();
  pt.x = evt.clientX; pt.y = evt.clientY;
  return pt.matrixTransform(svg.getScreenCTM().inverse());
}

// ---------- Brick shape rendering (dispatch on type) ----------
function toStr(poly) { return poly.map(p => `${p.x.toFixed(1)},${p.y.toFixed(1)}`).join(' '); }

function BrickShape({ brick, origin, selected, ghost, dim, coveredCells }) {
  ghost = ghost || !!brick._ghost;
  const type = brick.type || 'brick';
  switch (type) {
    case 'wheel':    return <WheelShape brick={brick} origin={origin} selected={selected} ghost={ghost} dim={dim} />;
    case 'cone':     return <ConeShape brick={brick} origin={origin} selected={selected} ghost={ghost} dim={dim} />;
    case 'slope':    return <SlopeShape brick={brick} origin={origin} selected={selected} ghost={ghost} dim={dim} />;
    case 'cylinder': return <CylinderShape brick={brick} origin={origin} selected={selected} ghost={ghost} dim={dim} />;
    case 'window':   return <WindowShape brick={brick} origin={origin} selected={selected} ghost={ghost} dim={dim} />;
    case 'door':     return <DoorShape brick={brick} origin={origin} selected={selected} ghost={ghost} dim={dim} />;
    case 'minifig':  return <MinifigShape brick={brick} origin={origin} selected={selected} ghost={ghost} dim={dim} />;
    case 'tile':
    case 'brick':
    default:         return <CuboidShape brick={brick} origin={origin} selected={selected} ghost={ghost} dim={dim} showStuds={type === 'brick'} coveredCells={coveredCells} />;
  }
}

function StudDefs() {
  const sr = ISO.TW * 0.30;
  const sh = ISO.TW * 0.225;
  return (
    <defs>
      <symbol id="ld-stud" viewBox={`${-sr - 2} ${-sh - sr - 2} ${sr * 2 + 4} ${sh + sr * 2 + 4}`} overflow="visible">
        <ellipse cx="0" cy="1" rx={sr * 1.05} ry={sr * 0.55} fill="rgba(0,0,0,0.22)" />
        <path d={`M ${-sr} 0 L ${-sr} ${-sh} A ${sr} ${sr * 0.45} 0 0 0 ${sr} ${-sh} L ${sr} 0 A ${sr} ${sr * 0.45} 0 0 1 ${-sr} 0 Z`}
              fill="var(--stud-side, #777)" stroke="#1f1d1a" strokeWidth="0.65" strokeLinejoin="round" />
        <ellipse cx="0" cy={-sh} rx={sr} ry={sr} fill="var(--stud-top, #ccc)" stroke="#1f1d1a" strokeWidth="0.7" />
        <ellipse cx={-sr * 0.28} cy={-sh - sr * 0.22} rx={sr * 0.32} ry={sr * 0.22} fill="rgba(255,255,255,0.4)" />
      </symbol>
      <symbol id="ld-stud-hi" overflow="visible">
        <circle cx="0" cy="0" r={ISO.TW * 0.28} fill="none" stroke="#f5cd2f" strokeWidth="2.2" opacity="0.95" />
        <circle cx="0" cy="0" r={ISO.TW * 0.16} fill="rgba(245,205,47,0.35)" />
      </symbol>
    </defs>
  );
}

function renderStuds(studs, hex) {
  const sr = ISO.TW * 0.30;
  const sh = ISO.TW * 0.225;
  const sideFill = darken(hex, 0.62);
  const topFill = lighten(hex, 0.08);
  // Colored studs stay inline (symbol can't recolor reliably); highlights use <use>.
  return studs.map((s, i) => (
    <g key={i}>
      <ellipse cx={s.x} cy={s.y + 1} rx={sr * 1.05} ry={sr * 0.55} fill="rgba(0,0,0,0.22)" />
      <path d={`M ${s.x - sr} ${s.y} L ${s.x - sr} ${s.y - sh}
                A ${sr} ${sr * 0.45} 0 0 0 ${s.x + sr} ${s.y - sh}
                L ${s.x + sr} ${s.y}
                A ${sr} ${sr * 0.45} 0 0 1 ${s.x - sr} ${s.y} Z`}
            fill={sideFill} stroke="#1f1d1a" strokeWidth="0.65" strokeLinejoin="round" />
      <ellipse cx={s.x} cy={s.y - sh} rx={sr} ry={sr} fill={topFill} stroke="#1f1d1a" strokeWidth="0.7" />
      <ellipse cx={s.x - sr * 0.28} cy={s.y - sh - sr * 0.22}
               rx={sr * 0.32} ry={sr * 0.22} fill="rgba(255,255,255,0.4)" />
    </g>
  ));
}

function renderClutchStuds(studs) {
  if (!studs || !studs.length) return null;
  return (
    <g className="clutch-highlights" style={{ pointerEvents: 'none' }}>
      {studs.map((s, i) => {
        const p = iso(s.x + 0.5, s.y + 0.5, s.z || 0);
        return <use key={i} href="#ld-stud-hi" xlinkHref="#ld-stud-hi" x={p.x} y={p.y} />;
      })}
    </g>
  );
}

function CuboidShape({ brick, origin, selected, ghost, dim, showStuds = true, coveredCells }) {
  const g = brickGeometry(brick, origin);
  const studs = showStuds ? studPositions(brick, origin, coveredCells) : [];
  const hex = colorHex(brick.color || 'red');
  const isTrans = brick.color === 'trans';
  const invalid = !!brick._invalid;
  const opacity = ghost ? 0.45 : (dim ? 0.3 : 1);
  const shades = faceShades(hex, ISO.yaw);
  const vis = g.visibleVertical || ['S', 'E'];
  const stroke = selected ? '#f5cd2f' : (invalid ? '#c91a09' : '#1b2a34');
  const isTile = (brick.type || 'brick') === 'tile';
  const fillFor = (face) => {
    if (isTrans) return (face === 'E' || face === 'W') ? 'rgba(120,190,200,0.55)' : 'rgba(174,233,239,0.55)';
    // Shade by axis (not painter index) so seams stay stable while orbiting.
    return (face === 'E' || face === 'W') ? shades.right : shades.front;
  };
  return (
    <g className={ghost ? 'brick-ghost' : undefined} opacity={opacity} style={{ pointerEvents: ghost ? 'none' : 'auto' }}>
      {!isTrans && <polygon points={toStr(g.silhouette)} fill={shades.right} stroke="none" />}
      {vis.map((face) => (
        <polygon key={face} points={toStr(g.faces[face])} fill={fillFor(face)} stroke="none" />
      ))}
      <polygon points={toStr(g.top)} fill={isTrans ? 'rgba(210,240,245,0.55)' : shades.top} stroke="none" />
      {isTile && !isTrans && (
        <polygon points={toStr(g.top.map((p) => {
          const c = { x: (g.c001.x + g.c101.x + g.c111.x + g.c011.x) / 4, y: (g.c001.y + g.c101.y + g.c111.y + g.c011.y) / 4 };
          return { x: p.x * 0.88 + c.x * 0.12, y: p.y * 0.88 + c.y * 0.12 };
        }))} fill="none" stroke={darken(hex, 0.75)} strokeWidth="0.8" opacity="0.7" />
      )}
      <polygon points={toStr(g.silhouette)} fill="none" stroke={stroke} strokeWidth={selected || invalid ? 2.4 : 1.15} strokeLinejoin="round" />
      {renderStuds(studs, hex)}
    </g>
  );
}

// Wheel: tire disc + hub (reads as a round wheel, not a black box)
function WheelShape({ brick, origin, selected, ghost, dim }) {
  const opacity = ghost ? 0.45 : (dim ? 0.3 : 1);
  const stroke = selected ? '#f5cd2f' : '#1b2a34';
  const O = origin;
  const cx = brick.x + brick.w / 2, cy = brick.y + brick.d / 2;
  const h = brickH(brick);
  const mid = iso(cx, cy, brick.z + h * 0.5);
  const top = iso(cx, cy, brick.z + h);
  const bot = iso(cx, cy, brick.z);
  const px = mid.x + O.x, py = mid.y + O.y;
  const r = Math.max(ISO.TW, ISO.UH * h) * 0.42;
  const thick = Math.max(3, Math.min(brick.w, brick.d) * ISO.TW * 0.22);
  return (
    <g opacity={opacity}>
      {/* tire thickness (side of disc) */}
      <ellipse cx={px + thick * 0.15} cy={py + 1} rx={r * 0.95} ry={r * 0.92} fill="#0d0d0d" stroke={stroke} strokeWidth="1" />
      <ellipse cx={px} cy={py} rx={r} ry={r * 0.96} fill="#1a1a1a" stroke={stroke} strokeWidth="1.2" />
      <ellipse cx={px} cy={py} rx={r * 0.72} ry={r * 0.7} fill="#2a2a2a" stroke="#1b2a34" strokeWidth="0.8" />
      <ellipse cx={px} cy={py} rx={r * 0.38} ry={r * 0.36} fill="#8d9499" stroke="#1b2a34" strokeWidth="0.9" />
      <ellipse cx={px} cy={py} rx={r * 0.14} ry={r * 0.13} fill="#1b2a34" />
      {/* axle pin hint */}
      <line x1={bot.x + O.x} y1={bot.y + O.y} x2={top.x + O.x} y2={top.y + O.y}
            stroke="#6b7278" strokeWidth="1.4" opacity="0.5" />
    </g>
  );
}

// Cone: circular frustum (stacked ellipses tapering to a tip)
function ConeShape({ brick, origin, selected, ghost, dim }) {
  const opacity = ghost ? 0.45 : (dim ? 0.3 : 1);
  const stroke = selected ? '#f5cd2f' : '#1b2a34';
  const hex = colorHex(brick.color || 'brown');
  const O = origin;
  const h = brickH(brick);
  const cx = brick.x + brick.w / 2, cy = brick.y + brick.d / 2;
  const layers = Math.max(4, Math.round(h * 5));
  const baseR = Math.min(brick.w, brick.d) * ISO.TW * 0.48;
  const els = [];
  for (let i = 0; i <= layers; i++) {
    const t = i / layers;
    const z = brick.z + h * t;
    const r = baseR * (1 - t * 0.92);
    const p = iso(cx, cy, z);
    const fill = darken(hex, 0.55 + t * 0.4);
    els.push(
      <ellipse key={i} cx={p.x + O.x} cy={p.y + O.y}
               rx={Math.max(1.2, r)} ry={Math.max(0.8, r * 0.42)}
               fill={fill} stroke={stroke} strokeWidth={i === layers ? 1.1 : 0.7} />
    );
  }
  return <g opacity={opacity}>{els}</g>;
}

// Slope: top face slants from front (high) to back (low).
// slopeDir indicates which side is HIGH: 'N' = high side at y=gy+d, 'S' = high at y=gy,
// 'E' = high at x=gx+w, 'W' = high at x=gx.  Default 'S' (high on front face toward viewer).
function SlopeShape({ brick, origin, selected, ghost, dim }) {
  const g = brickGeometry(brick, origin);
  const opacity = ghost ? 0.45 : (dim ? 0.3 : 1);
  const hex = colorHex(brick.color || 'red');
  const dir = brick.slopeDir || 'S';
  const top = hex, front = darken(hex, 0.78), right = darken(hex, 0.55);
  // Always 2 corners stay high (forming the high edge), other 2 stay low.
  // Direction maps:
  let highTop1, highTop2, lowBot1, lowBot2;
  if (dir === 'S') { highTop1 = g.c001; highTop2 = g.c101; lowBot1 = g.c010; lowBot2 = g.c110; }
  else if (dir === 'N') { highTop1 = g.c011; highTop2 = g.c111; lowBot1 = g.c000; lowBot2 = g.c100; }
  else if (dir === 'W') { highTop1 = g.c001; highTop2 = g.c011; lowBot1 = g.c100; lowBot2 = g.c110; }
  else /* 'E' */ { highTop1 = g.c101; highTop2 = g.c111; lowBot1 = g.c000; lowBot2 = g.c010; }
  // Slanted top quad
  const slantTop = [highTop1, highTop2, lowBot2, lowBot1];
  // Visible vertical faces (depending on dir)
  // For 'S' slope: front face is full tall rect (c000→c100→c101→c001), right face is a triangle (c100→c110→c101).
  // For 'E' slope: right face full (c100→c110→c111→c101), front face triangle (c000→c100→c101).
  // For 'N' or 'W': the slope goes UP at the back/left, so front becomes a triangle and right becomes a triangle.
  const isTrans = brick.color === 'trans';
  // Determine visible verticals
  const facesToDraw = [];
  if (dir === 'S') {
    facesToDraw.push({ pts: g.front, fill: front });
    facesToDraw.push({ pts: [g.c100, g.c110, g.c101], fill: right });
  } else if (dir === 'E') {
    facesToDraw.push({ pts: g.right, fill: right });
    facesToDraw.push({ pts: [g.c000, g.c100, g.c101], fill: front });
  } else if (dir === 'N') {
    // Front face: triangle low-front-left, low-front-right, going up to top-back via slope (visible front face becomes wedge)
    facesToDraw.push({ pts: [g.c000, g.c100, g.c111, g.c011], fill: front, isQuad: true });
    facesToDraw.push({ pts: [g.c100, g.c110, g.c111], fill: right });
  } else {
    facesToDraw.push({ pts: [g.c100, g.c110, g.c111, g.c101], fill: right });
    facesToDraw.push({ pts: [g.c000, g.c100, g.c101, g.c011], fill: front });
  }
  const highStuds = !isTrans ? studPositions(brick, origin, null, { slopeHigh: { dir } }) : [];
  const hull = LE.slopeSilhouette ? LE.slopeSilhouette(g, dir) : slantTop;
  const invalid = !!brick._invalid;
  const hullStroke = selected ? '#f5cd2f' : (invalid ? '#c91a09' : '#1b2a34');
  const hullSw = selected || invalid ? 2.4 : 1.15;
  return (
    <g className={ghost ? 'brick-ghost' : undefined} opacity={opacity} style={{ pointerEvents: ghost ? 'none' : 'auto' }}>
      {!isTrans && <polygon points={toStr(hull)} fill={right} stroke="none" />}
      {facesToDraw.map((f, i) => (
        <polygon key={i} points={toStr(f.pts)} fill={isTrans ? 'rgba(174,233,239,0.55)' : f.fill} stroke="none" />
      ))}
      <polygon points={toStr(slantTop)} fill={isTrans ? 'rgba(210,240,245,0.55)' : top} stroke="none" />
      <polygon points={toStr(hull)} fill="none" stroke={hullStroke} strokeWidth={hullSw} strokeLinejoin="round" />
      {renderStuds(highStuds, hex)}
    </g>
  );
}

// Cylinder: upright round brick (stacked ellipses + top stud for 1×1)
function CylinderShape({ brick, origin, selected, ghost, dim }) {
  const opacity = ghost ? 0.45 : (dim ? 0.3 : 1);
  const stroke = selected ? '#f5cd2f' : '#1b2a34';
  const hex = colorHex(brick.color || 'red');
  const O = origin;
  const h = brickH(brick);
  const cx = brick.x + brick.w / 2, cy = brick.y + brick.d / 2;
  const rx = Math.min(brick.w, brick.d) * ISO.TW * 0.48;
  const ry = rx * 0.42;
  const layers = Math.max(3, Math.round(h * 4));
  const els = [];
  for (let i = 0; i <= layers; i++) {
    const t = i / layers;
    const z = brick.z + h * t;
    const p = iso(cx, cy, z);
    const fill = i === layers ? lighten(hex, 0.06) : darken(hex, 0.7 + t * 0.25);
    els.push(
      <ellipse key={i} cx={p.x + O.x} cy={p.y + O.y} rx={rx} ry={ry}
               fill={fill} stroke={stroke} strokeWidth={i === 0 || i === layers ? 1.1 : 0.65} />
    );
  }
  // 1×1 round bricks get a top stud
  if (brick.w === 1 && brick.d === 1) {
    const tip = iso(cx, cy, brick.z + h);
    els.push(...renderStuds([{ x: tip.x + O.x, y: tip.y + O.y }], hex));
  }
  return <g opacity={opacity}>{els}</g>;
}

// Window: open-frame brick (transparent middle)
function WindowShape({ brick, origin, selected, ghost, dim }) {
  const g = brickGeometry(brick, origin);
  const opacity = ghost ? 0.45 : (dim ? 0.3 : 1);
  const stroke = selected ? '#f5c518' : '#1f1d1a';
  const sw = selected ? 2.4 : 1;
  const hex = colorHex(brick.color || 'white');
  const top = hex, front = darken(hex, 0.85), right = darken(hex, 0.65);
  // Render a thin frame around the cuboid, with a translucent pane in the middle
  return (
    <g opacity={opacity}>
      <polygon points={toStr(g.front)} fill="rgba(168,216,232,0.45)" stroke={stroke} strokeWidth={sw} />
      <polygon points={toStr(g.right)} fill="rgba(120,170,190,0.45)" stroke={stroke} strokeWidth={sw} />
      <polygon points={toStr(g.top)}   fill={top} stroke={stroke} strokeWidth={sw} />
      {/* frame */}
      <polygon points={toStr(g.silhouette)} fill="none" stroke="#1f1d1a" strokeWidth={2.4} />
      {/* mullion cross */}
      <line x1={(g.c000.x + g.c100.x)/2} y1={(g.c000.y + g.c100.y)/2}
            x2={(g.c001.x + g.c101.x)/2} y2={(g.c001.y + g.c101.y)/2}
            stroke={front} strokeWidth={1.4} />
    </g>
  );
}

// Door: brown panel with a small handle dot
function DoorShape({ brick, origin, selected, ghost, dim }) {
  const g = brickGeometry(brick, origin);
  const opacity = ghost ? 0.45 : (dim ? 0.3 : 1);
  const stroke = selected ? '#f5c518' : '#1f1d1a';
  const sw = selected ? 2.4 : 1;
  const hex = colorHex(brick.color || 'brown');
  const top = hex, front = darken(hex, 0.7), right = darken(hex, 0.55);
  // Handle position (front face, middle-right area)
  const fx = (g.c000.x * 0.4 + g.c100.x * 0.6);
  const fy = (g.c000.y * 0.5 + g.c001.y * 0.5);
  return (
    <g opacity={opacity}>
      <polygon points={toStr(g.front)} fill={front} stroke={stroke} strokeWidth={sw} />
      <polygon points={toStr(g.right)} fill={right} stroke={stroke} strokeWidth={sw} />
      <polygon points={toStr(g.top)}   fill={top} stroke={stroke} strokeWidth={sw} />
      <circle cx={fx} cy={fy} r="2.5" fill="#f5c518" stroke="#1f1d1a" strokeWidth="1" />
      <polygon points={toStr(g.silhouette)} fill="none" stroke="#1f1d1a" strokeWidth={1.4} />
    </g>
  );
}

// Minifig: legs / hips / torso / head stacked in brick space (~real fig proportions)
function MinifigShape({ brick, origin, selected, ghost, dim }) {
  const opacity = ghost ? 0.45 : (dim ? 0.3 : 1);
  const stroke = selected ? '#f5cd2f' : '#1b2a34';
  const hex = colorHex(brick.color || 'yellow');
  const O = origin;
  const h = brickH(brick);
  const cx = brick.x + brick.w / 2, cy = brick.y + brick.d / 2;
  const layers = [
    { z0: 0,    z1: 0.35, color: '#0055bf', kind: 'legs' },
    { z0: 0.35, z1: 0.48, color: '#0055bf', kind: 'hips' },
    { z0: 0.48, z1: 0.78, color: '#c91a09', kind: 'torso' },
    { z0: 0.78, z1: 1.0,  color: hex,       kind: 'head' },
  ];
  return (
    <g opacity={opacity}>
      {layers.map((L, i) => {
        const z0 = brick.z + h * L.z0, z1 = brick.z + h * L.z1;
        const inset = L.kind === 'head' ? 0.22 : L.kind === 'torso' ? 0.08 : 0.12;
        const x0 = brick.x + inset, x1 = brick.x + brick.w - inset;
        const y0 = brick.y + inset, y1 = brick.y + brick.d - inset;
        const c000 = iso(x0, y0, z0), c100 = iso(x1, y0, z0), c010 = iso(x0, y1, z0), c110 = iso(x1, y1, z0);
        const c001 = iso(x0, y0, z1), c101 = iso(x1, y0, z1), c011 = iso(x0, y1, z1), c111 = iso(x1, y1, z1);
        const P = p => ({ x: p.x + O.x, y: p.y + O.y });
        const front = [P(c000), P(c100), P(c101), P(c001)];
        const right = [P(c100), P(c110), P(c111), P(c101)];
        const top = [P(c001), P(c101), P(c111), P(c011)];
        if (L.kind === 'head') {
          const mid = iso(cx, cy, (z0 + z1) / 2);
          const r = ISO.TW * 0.38;
          return (
            <g key={i}>
              <ellipse cx={mid.x + O.x} cy={mid.y + O.y} rx={r} ry={r * 0.85}
                       fill={L.color} stroke={stroke} strokeWidth="1.1" />
              <circle cx={mid.x + O.x - r * 0.28} cy={mid.y + O.y - r * 0.08} r="1.1" fill="#1b2a34" />
              <circle cx={mid.x + O.x + r * 0.28} cy={mid.y + O.y - r * 0.08} r="1.1" fill="#1b2a34" />
              <path d={`M ${mid.x + O.x - r * 0.22} ${mid.y + O.y + r * 0.22}
                        Q ${mid.x + O.x} ${mid.y + O.y + r * 0.38}
                        ${mid.x + O.x + r * 0.22} ${mid.y + O.y + r * 0.22}`}
                    stroke="#1b2a34" strokeWidth="0.9" fill="none" />
            </g>
          );
        }
        return (
          <g key={i}>
            <polygon points={toStr(front)} fill={darken(L.color, 0.82)} stroke={stroke} strokeWidth="1" />
            <polygon points={toStr(right)} fill={darken(L.color, 0.58)} stroke={stroke} strokeWidth="1" />
            <polygon points={toStr(top)} fill={lighten(L.color, 0.05)} stroke={stroke} strokeWidth="1" />
          </g>
        );
      })}
    </g>
  );
}

// ---------- Whole scene ----------
function IsoScene({
  bricks, baseSize, baseColor = COLORS.green, origin,
  view = 0, yawDeg = 0, panX = 0, panY = 0, zoom = 1,
  ghost, selectedId, clutchStuds,
  onPointerMove, onSceneClick, onSceneContext,
  setYawDeg, setPan,     // setYawDeg: orbit; setPan: middle/space drag
  viewBox, style,
  hideAboveZ, // optional clip layer
  showGrid = false,
  debug = false,
  freezeOrbitBounds = false,
}) {
  const svgRef = React.useRef(null);
  const orbitRef = React.useRef(null);      // { x0, y0, yaw0 } while drag-orbiting
  const suppressClickRef = React.useRef(false);
  const debugOn = debug || (typeof window !== 'undefined' && /[?&]debug=1(?:&|$)/.test(window.location.search || ''));
  // Iso uses total yawDeg only — never compound leftover top-view quarter.
  const cam = createCamera({
    mode: 'iso',
    yawDeg,
    panX: typeof panX === 'number' ? panX : 0,
    panY: typeof panY === 'number' ? panY : 0,
    zoom: typeof zoom === 'number' ? zoom : 1,
    debug: debugOn,
  });
  const restoreCam = applyCamera(cam, baseSize);
  const base = baseSize;

  // Bricks stay in their original world coords; rotation happens at projection
  // time inside iso(). paintOrder needs the active camera yaw — already applied.
  let bs = bricks.map(b => ({ ...b, _origId: b.id }));
  let clipZ = hideAboveZ;
  // Auto cull upper layers on huge builds (perf path).
  if (clipZ == null && bs.length > 400) {
    let maxTop = 0;
    for (const b of bs) maxTop = Math.max(maxTop, b.z + brickH(b));
    clipZ = Math.max(2, maxTop - 3);
  }
  if (typeof clipZ === 'number') bs = bs.filter(b => b.z < clipZ);
  const rotatedBricks = paintOrder(bs);
  const coveredCells = buildCoveredCells(rotatedBricks);
  const supports = debugOn ? supportGraph(bricks) : [];

  const rotatedGhost = ghost;

  const O = origin;
  const bp = [iso(0,0,0), iso(base.w,0,0), iso(base.w,base.d,0), iso(0,base.d,0)]
    .map(p => `${(p.x+O.x).toFixed(1)},${(p.y+O.y).toFixed(1)}`).join(' ');

  // Drag-to-orbit: Shift+left or middle-button only (right-click stays delete).
  // Touch: two-finger horizontal drag also orbits.
  const startOrbit = (clientX) => {
    if (!setYawDeg) return;
    orbitRef.current = { x0: clientX, yaw0: yawDeg };
    suppressClickRef.current = false;
  };
  const moveOrbit = (clientX) => {
    const o = orbitRef.current; if (!o) return;
    const dx = clientX - o.x0;
    if (Math.abs(dx) > 2) suppressClickRef.current = true;
    let next = Math.round(o.yaw0 + dx * 0.6);
    if (window.LegoEngine && typeof window.LegoEngine.normalizeYaw === 'function') {
      next = window.LegoEngine.normalizeYaw(next);
    }
    setYawDeg(next);
  };
  const endOrbit = () => {
    orbitRef.current = null;
    if (suppressClickRef.current) setTimeout(() => { suppressClickRef.current = false; }, 200);
  };
  const handleDown = e => {
    // Middle-drag or Space+drag → pan; Shift/middle with setYawDeg → orbit
    const wantsPan = setPan && (e.button === 1 || e.altKey);
    if (wantsPan) {
      e.preventDefault();
      let lastX = e.clientX, lastY = e.clientY;
      const onMove = ev => {
        const dx = ev.clientX - lastX, dy = ev.clientY - lastY;
        lastX = ev.clientX; lastY = ev.clientY;
        setPan(dx, dy);
      };
      const onUp = () => {
        window.removeEventListener('mousemove', onMove);
        window.removeEventListener('mouseup', onUp);
      };
      window.addEventListener('mousemove', onMove);
      window.addEventListener('mouseup', onUp);
      return;
    }
    if (!setYawDeg) return;
    const wantsOrbit = e.shiftKey || e.button === 1;
    if (!wantsOrbit) return;
    e.preventDefault();
    startOrbit(e.clientX);
    const onMove = ev => moveOrbit(ev.clientX);
    const onUp = () => {
      endOrbit();
      window.removeEventListener('mousemove', onMove);
      window.removeEventListener('mouseup', onUp);
    };
    window.addEventListener('mousemove', onMove);
    window.addEventListener('mouseup', onUp);
  };
  // Re-apply camera for picks: ISO is restored after render so thumbs don't leak yaw.
  const withCam = (fn) => {
    const r = applyCamera(cam, baseSize);
    try { return fn(); } finally { r(); }
  };
  const handleMove = e => {
    if (orbitRef.current) return;
    if (!onPointerMove) return;
    const p = svgPoint(e, svgRef.current);
    withCam(() => onPointerMove({ sx: p.x, sy: p.y, evt: e }));
  };
  const handleClick = e => {
    if (suppressClickRef.current) return;
    if (!onSceneClick) return;
    const p = svgPoint(e, svgRef.current);
    withCam(() => onSceneClick({ sx: p.x, sy: p.y, evt: e }));
  };
  const handleContext = e => {
    e.preventDefault();
    if (suppressClickRef.current || orbitRef.current) return;
    if (!onSceneContext) return;
    const p = svgPoint(e, svgRef.current);
    withCam(() => onSceneContext({ sx: p.x, sy: p.y, evt: e }));
  };
  const touchFingers = React.useRef(new Map());
  const tapRef = React.useRef(null);
  const handleTouchStart = e => {
    for (const t of e.changedTouches) touchFingers.current.set(t.identifier, { x: t.clientX, y: t.clientY });
    if (setYawDeg && touchFingers.current.size >= 2) {
      e.preventDefault();
      tapRef.current = null;
      const xs = [...touchFingers.current.values()].map(p => p.x);
      startOrbit((xs[0] + xs[1]) / 2);
    } else if (touchFingers.current.size === 1 && e.changedTouches.length === 1) {
      const t = e.changedTouches[0];
      tapRef.current = { id: t.identifier, x: t.clientX, y: t.clientY, t0: Date.now() };
      if (onPointerMove) {
        const p = svgPoint({ clientX: t.clientX, clientY: t.clientY }, svgRef.current);
        withCam(() => onPointerMove({ sx: p.x, sy: p.y, evt: e }));
      }
    }
  };
  const handleTouchMove = e => {
    for (const t of e.changedTouches) touchFingers.current.set(t.identifier, { x: t.clientX, y: t.clientY });
    if (orbitRef.current && touchFingers.current.size >= 2) {
      e.preventDefault();
      tapRef.current = null;
      const xs = [...touchFingers.current.values()].map(p => p.x);
      moveOrbit((xs[0] + xs[1]) / 2);
      return;
    }
    if (tapRef.current && e.touches.length === 1) {
      const t = e.touches[0];
      if (Math.hypot(t.clientX - tapRef.current.x, t.clientY - tapRef.current.y) > 8) {
        tapRef.current = null;
      }
    }
    if (orbitRef.current || !onPointerMove || e.touches.length !== 1) return;
    const fake = { clientX: e.touches[0].clientX, clientY: e.touches[0].clientY };
    const p = svgPoint(fake, svgRef.current);
    withCam(() => onPointerMove({ sx: p.x, sy: p.y, evt: e }));
  };
  const handleTouchEnd = e => {
    const tap = tapRef.current;
    for (const t of e.changedTouches) {
      if (tap && t.identifier === tap.id && !orbitRef.current && !suppressClickRef.current) {
        const dt = Date.now() - tap.t0;
        const dist = Math.hypot(t.clientX - tap.x, t.clientY - tap.y);
        if (dt <= 300 && dist <= 8 && onSceneClick) {
          e.preventDefault();
          const p = svgPoint({ clientX: t.clientX, clientY: t.clientY }, svgRef.current);
          withCam(() => onSceneClick({ sx: p.x, sy: p.y, evt: e }));
        }
      }
      touchFingers.current.delete(t.identifier);
    }
    tapRef.current = null;
    if (orbitRef.current && touchFingers.current.size < 2) endOrbit();
  };

  // Build grid-line endpoints (in viewed coords) for the interior cell divisions.
  // Lines at every integer 0..base.w and 0..base.d; we skip the outer 0 / base
  // because those coincide with the boundary polygon.
  const shift = p => `${(p.x + O.x).toFixed(1)},${(p.y + O.y).toFixed(1)}`;
  const gridLines = [];
  for (let x = 1; x < base.w; x++) {
    const a = iso(x, 0, 0), b = iso(x, base.d, 0);
    gridLines.push({ x1: a.x + O.x, y1: a.y + O.y, x2: b.x + O.x, y2: b.y + O.y, key: `vx-${x}` });
  }
  for (let y = 1; y < base.d; y++) {
    const a = iso(0, y, 0), b = iso(base.w, y, 0);
    gridLines.push({ x1: a.x + O.x, y1: a.y + O.y, x2: b.x + O.x, y2: b.y + O.y, key: `vy-${y}` });
  }

  // Tint set baseColor toward paper so grass/road still reads on the sketchy UI.
  const baseFill = (() => {
    const hex = (baseColor && String(baseColor).startsWith('#')) ? baseColor : '#f4ede0';
    return hex;
  })();

  const svg = (
    <svg ref={svgRef} viewBox={viewBox}
         style={{ ...style, cursor: setYawDeg ? (style?.cursor || 'grab') : style?.cursor, touchAction: setYawDeg ? 'none' : style?.touchAction }}
         onMouseMove={handleMove} onClick={handleClick}
         onContextMenu={handleContext} onMouseDown={handleDown}
         onTouchStart={handleTouchStart} onTouchMove={handleTouchMove} onTouchEnd={handleTouchEnd}
         onTouchCancel={handleTouchEnd}
         onWheel={e => {
           if (!setPan && !setYawDeg) return;
           e.preventDefault();
           // Parent can listen via custom — zoom via setYawDeg parent handles wheel separately
         }}>
      <StudDefs />
      {/* Baseplate top uses the set's baseColor (grass/road/sand), lightly opaque. */}
      <polygon points={bp} fill={baseFill} opacity="0.78" stroke="none" />
      {/* Molded baseplate studs (skip every other on huge plates for perf). */}
      {(() => {
        const step = (base.w * base.d > 220) ? 2 : 1;
        const sr = ISO.TW * 0.22;
        const nodes = [];
        for (let i = 0; i < base.w; i += step) {
          for (let j = 0; j < base.d; j += step) {
            const p = iso(i + 0.5, j + 0.5, 0);
            nodes.push(
              <ellipse key={`bstud-${i}-${j}`}
                cx={p.x + O.x} cy={p.y + O.y}
                rx={sr} ry={sr}
                fill="rgba(255,255,255,0.18)" stroke="rgba(27,42,52,0.22)" strokeWidth="0.5" />
            );
          }
        }
        return <g style={{ pointerEvents: 'none' }}>{nodes}</g>;
      })()}
      {showGrid && gridLines.map(l => (
        <line key={l.key} x1={l.x1} y1={l.y1} x2={l.x2} y2={l.y2}
              stroke="#7a6b58" strokeWidth="0.6" strokeDasharray="2 2" opacity="0.55" />
      ))}
      <polygon points={bp} fill="none" stroke="#1b2a34" strokeWidth="1.8" strokeLinejoin="round" />
      {(() => {
        const c0 = iso(0,0,0), cW = iso(base.w,0,0), cD = iso(0,base.d,0), cWD = iso(base.w,base.d,0);
        const h = 0.18;
        const c0d = iso(0,0,-h), cWd = iso(base.w,0,-h), cDd = iso(0,base.d,-h), cWDd = iso(base.w,base.d,-h);
        return (
          <g opacity="0.4">
            <polygon points={[c0,cW,cWd,c0d].map(shift).join(' ')} fill="#9a8e7a" stroke="#1b2a34" strokeWidth="0.8" />
            <polygon points={[cW,cWD,cWDd,cWd].map(shift).join(' ')} fill="#7a6e58" stroke="#1b2a34" strokeWidth="0.8" />
          </g>
        );
      })()}
      {showGrid && (() => {
        const labels = [];
        for (let i = 0; i < baseSize.w; i++) {
          const p = iso(i + 0.5, -0.7, 0);
          labels.push(
            <text key={`col-${i}`}
              x={p.x + O.x} y={p.y + O.y + 4}
              fontFamily="Patrick Hand, cursive" fontSize="14" fontWeight="700"
              textAnchor="middle" fill="#3a2f24"
              style={{ pointerEvents: 'none', paintOrder: 'stroke' }}
              stroke="rgba(244,237,224,0.92)" strokeWidth="3.5">
              {colLabel(i)}
            </text>
          );
        }
        for (let j = 0; j < baseSize.d; j++) {
          const p = iso(-0.7, j + 0.5, 0);
          labels.push(
            <text key={`row-${j}`}
              x={p.x + O.x} y={p.y + O.y + 4}
              fontFamily="Patrick Hand, cursive" fontSize="14" fontWeight="700"
              textAnchor="middle" fill="#3a2f24"
              style={{ pointerEvents: 'none', paintOrder: 'stroke' }}
              stroke="rgba(244,237,224,0.92)" strokeWidth="3.5">
              {j + 1}
            </text>
          );
        }
        return labels;
      })()}
      {/* Contact shadows on each brick's support plane (bottom face), same camera as bricks. */}
      {rotatedBricks.map(b => {
        const z = Math.max(0, b.z || 0);
        const corners = [
          iso(b.x, b.y, z),
          iso(b.x + b.w, b.y, z),
          iso(b.x + b.w, b.y + b.d, z),
          iso(b.x, b.y + b.d, z),
        ].map(p => `${(p.x + O.x).toFixed(1)},${(p.y + O.y).toFixed(1)}`).join(' ');
        const alpha = z < 1e-6 ? 0.16 : Math.max(0.06, 0.14 - z * 0.02);
        return <polygon key={`sh-${b._origId || b.id}`} points={corners} fill={`rgba(27,42,52,${alpha})`} stroke="none" />;
      })}
      {rotatedBricks.map(b => (
        <BrickShape key={b._origId || b.id} brick={b} origin={O}
          coveredCells={coveredCells}
          ghost={!!b._ghost}
          selected={selectedId === (b._origId || b.id)} />
      ))}
      {rotatedGhost && <BrickShape brick={rotatedGhost} origin={O} ghost />}
      {renderClutchStuds((clutchStuds || []).map(s => ({ ...s, x: s.x, y: s.y, z: s.z })))}
      {debugOn && (
        <text x={O.x + 8} y={O.y - 12} fontSize="11" fill="#c91a09" style={{ pointerEvents: 'none' }}>
          {`yaw=${yawDeg}° pan=${panX},${panY} zoom=${zoom}`}
        </text>
      )}
      {debugOn && rotatedBricks.map((b, i) => {
        const c = iso(b.x + b.w / 2, b.y + b.d / 2, b.z + brickH(b));
        return (
          <text key={`po-${b.id}`} x={c.x + O.x} y={c.y + O.y}
            fontSize="9" fill="#c91a09" textAnchor="middle"
            style={{ pointerEvents: 'none' }}>{i}</text>
        );
      })}
      {debugOn && supports.map((e, i) => {
        const from = bricks.find(b => b.id === e.fromId);
        const to = bricks.find(b => b.id === e.toId);
        if (!from || !to) return null;
        const a = iso(from.x + from.w / 2, from.y + from.d / 2, from.z);
        const bpt = iso(to.x + to.w / 2, to.y + to.d / 2, to.z + brickH(to));
        return (
          <line key={`sup-${i}`}
            x1={a.x + O.x} y1={a.y + O.y} x2={bpt.x + O.x} y2={bpt.y + O.y}
            stroke="#0055bf" strokeWidth="1.2" opacity="0.7"
            style={{ pointerEvents: 'none' }} />
        );
      })}
      {debugOn && rotatedBricks.map(b => {
        const corners = [
          iso(b.x, b.y, b.z),
          iso(b.x + b.w, b.y, b.z),
          iso(b.x + b.w, b.y + b.d, b.z),
          iso(b.x, b.y + b.d, b.z),
        ].map(p => `${(p.x + O.x).toFixed(1)},${(p.y + O.y).toFixed(1)}`).join(' ');
        return <polygon key={`aabb-${b.id}`} points={corners} fill="none" stroke="#fe8a18" strokeWidth="1" strokeDasharray="3 2" opacity="0.8" style={{ pointerEvents: 'none' }} />;
      })}
    </svg>
  );
  // Keep camera applied through child BrickShape renders (they call brickGeometry
  // during their own render, after this function returns). Restore after paint.
  React.useLayoutEffect(() => restoreCam);
  return svg;
}

// ============================================================
// Top-down view: orthographic projection, no z-skew, flat squares.
// ============================================================

function TopBrick({ brick, origin, ghost, selected }) {
  const x = brick.x * TOPSTEP + origin.x;
  const y = brick.y * TOPSTEP + origin.y;
  const w = brick.w * TOPSTEP;
  const h = brick.d * TOPSTEP;
  const hex = colorHex(brick.color);
  const isTrans = brick.color === 'trans';
  const opacity = ghost ? 0.5 : 1;
  const stroke = selected ? '#f5c518' : '#1f1d1a';
  const sw = selected ? 2.4 : 1.2;
  const type = brick.type || 'brick';

  if (type === 'wheel') {
    const cx = x + w / 2, cy = y + h / 2;
    const r = Math.min(w, h) * 0.45;
    return (
      <g opacity={opacity} style={{ pointerEvents: ghost ? 'none' : 'auto' }}>
        <circle cx={cx} cy={cy} r={r} fill="#1a1a1a" stroke={stroke} strokeWidth={sw} />
        <circle cx={cx} cy={cy} r={r * 0.42} fill="#7a7a7a" stroke="#1f1d1a" strokeWidth="0.7" />
        <circle cx={cx} cy={cy} r={r * 0.14} fill="#1f1f1f" />
      </g>
    );
  }
  if (type === 'cone') {
    const cx = x + w / 2;
    return (
      <g opacity={opacity}>
        <rect x={x} y={y} width={w} height={h} fill={darken(hex, 0.8)} stroke={stroke} strokeWidth={sw} />
        <polygon points={`${x + 2},${y + h - 2} ${cx},${y + 2} ${x + w - 2},${y + h - 2}`}
                 fill={hex} stroke={stroke} strokeWidth={sw - 0.2} />
      </g>
    );
  }
  if (type === 'cylinder') {
    const cx = x + w / 2, cy = y + h / 2;
    return (
      <g opacity={opacity}>
        <ellipse cx={cx} cy={cy} rx={w / 2 - 1} ry={h / 2 - 1} fill={hex} stroke={stroke} strokeWidth={sw} />
        <ellipse cx={cx} cy={cy} rx={(w / 2 - 1) * 0.7} ry={(h / 2 - 1) * 0.7} fill="none" stroke="rgba(0,0,0,0.25)" strokeWidth="0.6" />
      </g>
    );
  }
  if (type === 'slope') {
    const arrow = brick.slopeDir === 'N' ? '↑' : brick.slopeDir === 'S' ? '↓' :
                  brick.slopeDir === 'E' ? '→' : '←';
    return (
      <g opacity={opacity}>
        <rect x={x} y={y} width={w} height={h} fill={hex} stroke={stroke} strokeWidth={sw} />
        <text x={x + w / 2} y={y + h / 2 + 5} textAnchor="middle" fontFamily="Patrick Hand"
              fontSize={Math.min(w, h) * 0.5} fontWeight="700" fill="rgba(0,0,0,0.55)">{arrow}</text>
      </g>
    );
  }
  if (type === 'window') {
    return (
      <g opacity={opacity}>
        <rect x={x} y={y} width={w} height={h} fill="rgba(168,216,232,0.55)" stroke={stroke} strokeWidth={sw * 1.4} />
        <line x1={x + w / 2} y1={y} x2={x + w / 2} y2={y + h} stroke={stroke} strokeWidth={sw * 0.7} />
        <line x1={x} y1={y + h / 2} x2={x + w} y2={y + h / 2} stroke={stroke} strokeWidth={sw * 0.7} />
      </g>
    );
  }
  if (type === 'door') {
    return (
      <g opacity={opacity}>
        <rect x={x} y={y} width={w} height={h} fill={hex} stroke={stroke} strokeWidth={sw} />
        <circle cx={x + w * 0.78} cy={y + h * 0.5} r={Math.min(w, h) * 0.07} fill="#f5c518" stroke="#1f1d1a" strokeWidth="0.8" />
      </g>
    );
  }
  if (type === 'minifig') {
    const cx = x + w / 2, cy = y + h / 2;
    return (
      <g opacity={opacity}>
        <rect x={x} y={y} width={w} height={h} fill="rgba(244,237,224,0.4)" stroke="#bcb29a" strokeWidth="0.6" strokeDasharray="2 2" />
        <circle cx={cx} cy={cy} r={Math.min(w, h) * 0.32} fill={hex} stroke="#1f1d1a" strokeWidth="1" />
        <circle cx={cx - w * 0.07} cy={cy - h * 0.02} r="1" fill="#1f1d1a" />
        <circle cx={cx + w * 0.07} cy={cy - h * 0.02} r="1" fill="#1f1d1a" />
      </g>
    );
  }
  // tile or default brick
  const studs = [];
  if (type === 'brick') {
    for (let i = 0; i < brick.w; i++) {
      for (let j = 0; j < brick.d; j++) {
        studs.push({ cx: x + (i + 0.5) * TOPSTEP, cy: y + (j + 0.5) * TOPSTEP });
      }
    }
  }
  return (
    <g opacity={opacity}>
      <rect x={x} y={y} width={w} height={h}
            fill={isTrans ? 'rgba(168,216,232,0.55)' : hex} stroke={stroke} strokeWidth={sw} />
      {studs.map((s, i) =>
        <circle key={i} cx={s.cx} cy={s.cy} r={TOPSTEP * 0.22}
                fill="rgba(255,255,255,0.55)" stroke="rgba(0,0,0,0.45)" strokeWidth="0.7" />
      )}
    </g>
  );
}

function TopDownScene({
  bricks, baseSize, origin, view = 0, ghost, selectedId,
  onPointerMove, onSceneClick, onSceneContext,
  viewBox, style, showGrid = false, hideAboveZ,
}) {
  const svgRef = React.useRef(null);
  const base = viewedBaseSize(baseSize, view);

  const rotatedBricks = React.useMemo(() => {
    let bs = bricks.map(b => ({ ...applyView(b, view, baseSize), _origId: b.id }));
    if (typeof hideAboveZ === 'number') bs = bs.filter(b => b.z < hideAboveZ);
    return [...bs].sort((a, b) => a.z - b.z);
  }, [bricks, view, baseSize, hideAboveZ]);
  const rotatedGhost = ghost ? applyView(ghost, view, baseSize) : null;

  const W = base.w * TOPSTEP, H = base.d * TOPSTEP;
  const O = origin;

  const handleMove = e => {
    if (!onPointerMove) return;
    const p = svgPoint(e, svgRef.current);
    onPointerMove({ sx: p.x, sy: p.y, evt: e });
  };
  const handleClick = e => {
    if (!onSceneClick) return;
    const p = svgPoint(e, svgRef.current);
    onSceneClick({ sx: p.x, sy: p.y, evt: e });
  };
  const handleContext = e => {
    e.preventDefault();
    if (!onSceneContext) return;
    const p = svgPoint(e, svgRef.current);
    onSceneContext({ sx: p.x, sy: p.y, evt: e });
  };

  return (
    <svg ref={svgRef} viewBox={viewBox} style={style}
         onMouseMove={handleMove} onClick={handleClick} onContextMenu={handleContext}>
      <rect x={O.x} y={O.y} width={W} height={H} fill="rgba(244,237,224,0.65)" stroke="none" />
      {showGrid && Array.from({ length: base.w - 1 }).map((_, i) =>
        <line key={`vx-${i}`} x1={O.x + (i + 1) * TOPSTEP} y1={O.y} x2={O.x + (i + 1) * TOPSTEP} y2={O.y + H}
              stroke="#7a6b58" strokeWidth="0.6" strokeDasharray="2 2" opacity="0.55" />
      )}
      {showGrid && Array.from({ length: base.d - 1 }).map((_, j) =>
        <line key={`hy-${j}`} x1={O.x} y1={O.y + (j + 1) * TOPSTEP} x2={O.x + W} y2={O.y + (j + 1) * TOPSTEP}
              stroke="#7a6b58" strokeWidth="0.6" strokeDasharray="2 2" opacity="0.55" />
      )}
      <rect x={O.x} y={O.y} width={W} height={H} fill="none" stroke="#1f1d1a" strokeWidth="1.8" />
      {/* World-relative labels (same convention as iso): "A1" always = world (0,0).
          For top-down at a rotated view, we project each world cell's label through
          rotPoint so it lands at the *visually* correct edge of the rotated baseplate. */}
      {showGrid && Array.from({ length: baseSize.w }).map((_, i) => {
        const r = rotPoint(i + 0.5, -0.7, view, baseSize);
        return <text key={`cl-${i}`} x={O.x + r.x * TOPSTEP} y={O.y + r.y * TOPSTEP + 4}
              fontFamily="Patrick Hand" fontSize="14" fontWeight="700" textAnchor="middle"
              fill="#3a2f24" style={{ pointerEvents: 'none', paintOrder: 'stroke' }}
              stroke="rgba(244,237,224,0.92)" strokeWidth="3">{colLabel(i)}</text>;
      })}
      {showGrid && Array.from({ length: baseSize.d }).map((_, j) => {
        const r = rotPoint(-0.7, j + 0.5, view, baseSize);
        return <text key={`rl-${j}`} x={O.x + r.x * TOPSTEP} y={O.y + r.y * TOPSTEP + 4}
              fontFamily="Patrick Hand" fontSize="14" fontWeight="700" textAnchor="middle"
              fill="#3a2f24" style={{ pointerEvents: 'none', paintOrder: 'stroke' }}
              stroke="rgba(244,237,224,0.92)" strokeWidth="3">{j + 1}</text>;
      })}
      {rotatedBricks.map(b => (
        <TopBrick key={b._origId || b.id} brick={b} origin={O}
                  ghost={!!b._ghost}
                  selected={selectedId === (b._origId || b.id)} />
      ))}
      {rotatedGhost && <TopBrick brick={rotatedGhost} origin={O} ghost />}
    </svg>
  );
}

Object.assign(window, {
  ISO, TOPSTEP, COLORS, COLOR_ORDER, PIECE_TYPES, darken, lighten, iso, unproject,
  brickH, settleBricks, collidesWith, footprintsOverlap, zRangesOverlap,
  canPlace, countSupports, supportGraph, createWorld, createCamera, applyCamera, faceShades,
  effSize, brickGeometry, studPositions, paintOrder,
  pickGroundCell, topZAt, pickBrick, pick, svgPoint, colorHex,
  applyView, viewedBaseSize, invView, rotPoint, colLabel,
  pickTopCell, pickTopBrick, computeTopViewBox, computeIsoViewBox,
  viewQuarterFromYaw: LE.viewQuarterFromYaw,
  BrickShape, IsoScene, TopDownScene,
  LegoEngine: LE,
});
