/* ========================================================================
   LUMINA — Advanced Output Mapping workspace
   Multi-projector blending · per-corner perspective warp ·
   irregular masks (polygon editor) · vector shapes · reference images ·
   per-display orientation/resolution · save/load presets.
   ======================================================================== */
const { useState: useStateMap, useEffect: useEffectMap, useRef: useRefMap } = React;

const SLICE_COLORS = ["#ff7a1a", "#4be6ff", "#7cff4f", "#ff5ad6", "#b785ff", "#ffae3a", "#ff3b3b", "#9bcfff"];
const STORE_PRESETS = "lumina_mapping_presets_v2";

/* === Templates === */
const MAPPING_TEMPLATES = {
  single: { label: "Single Screen", make: () => [
    makeSlice("Main", [0,0,1,1], [0,0,1,1], 1) ] },
  dual: { label: "Side by Side", make: () => [
    makeSlice("Left",  [0,0,0.5,1], [0,0,0.5,1], 1),
    makeSlice("Right", [0.5,0,0.5,1], [0.5,0,0.5,1], 2) ] },
  triple: { label: "Triple Wide", make: () => [
    makeSlice("L", [0,0,1/3,1], [0,0,1/3,1], 1),
    makeSlice("C", [1/3,0,1/3,1], [1/3,0,1/3,1], 2),
    makeSlice("R", [2/3,0,1/3,1], [2/3,0,1/3,1], 3) ] },
  grid: { label: "2 × 2 Grid", make: () => [
    makeSlice("TL", [0,0,0.5,0.5], [0,0,0.5,0.5], 1),
    makeSlice("TR", [0.5,0,0.5,0.5], [0.5,0,0.5,0.5], 1),
    makeSlice("BL", [0,0.5,0.5,0.5], [0,0.5,0.5,0.5], 1),
    makeSlice("BR", [0.5,0.5,0.5,0.5], [0.5,0.5,0.5,0.5], 1) ] },
  ledWall: { label: "LED Wall 4×2", make: () => {
    const arr = [];
    for (let r = 0; r < 2; r++) for (let c = 0; c < 4; c++) {
      arr.push(makeSlice(`P${r*4+c+1}`, [c/4, r/2, 1/4, 1/2], [c/4, r/2, 1/4, 1/2], 1));
    }
    return arr;
  } },
  proscenium: { label: "Stage + Wings", make: () => [
    makeSlice("Main",  [0.2,0,0.6,0.7], [0.2,0,0.6,0.7], 1),
    makeSlice("Left",  [0,0,0.2,1], [0,0,0.2,1], 2),
    makeSlice("Right", [0.8,0,0.2,1], [0.8,0,0.2,1], 3),
    makeSlice("Floor", [0.2,0.7,0.6,0.3], [0.2,0.7,0.6,0.3], 4) ] },
  edgeBlend2: { label: "2 Projector · Blend", make: () => [
    Object.assign(makeSlice("Proj L", [0,0,0.55,1], [0,0,0.55,1], 1), { blend: { right: 0.1 } }),
    Object.assign(makeSlice("Proj R", [0.45,0,0.55,1], [0.45,0,0.55,1], 2), { blend: { left: 0.1 } }),
  ] },
};

let _sid = 0;
function makeSlice(name, src, dst, display = 1) {
  return {
    id: ++_sid, name,
    src: [...src],                       // [x, y, w, h] in 0..1 (input crop)
    dst: [...dst],                       // [x, y, w, h] in 0..1 (output position)
    display,
    enabled: true,
    color: SLICE_COLORS[(_sid - 1) % SLICE_COLORS.length],
    rotation: 0,
    flipH: false, flipV: false,
    // 4-corner perspective warp — points relative to dst rect (in 0..1)
    warp: [[0,0],[1,0],[1,1],[0,1]],
    warpEnabled: false,
    // polygon mask (0..1 coords inside dst) — null = full rect
    mask: null,
    maskEnabled: false,
    feather: 0,                          // soft mask edge in px (1080p reference)
    // edge-blend gradient widths (0..1 of slice)
    blend: { left: 0, right: 0, top: 0, bottom: 0 },
    // color correction
    brightness: 1, contrast: 1, saturation: 1,
    // per-display physical info
    displayPx: [1920, 1080],
    displayOrient: "landscape",
  };
}

function MappingWorkspace({ audio, params, vizId }) {
  const [slices, setSlices] = useStateMap(() => {
    const ST = window.LuminaMapping;
    if (ST && ST.slices && ST.slices.length) {
      _sid = Math.max(_sid, ...ST.slices.map(s => s.id || 0));
      return ST.slices;
    }
    return MAPPING_TEMPLATES.single.make();
  });
  const [selectedId, setSelectedId] = useStateMap(slices[0]?.id);
  const [outRes, setOutRes] = useStateMap(() => window.LuminaMapping?.outRes || "1920x1080");
  const [blendGamma, setBlendGamma] = useStateMap(() => window.LuminaMapping?.blendGamma || 2.2);
  const [testPattern, setTestPattern] = useStateMap(() => !!window.LuminaMapping?.testPattern);
  const [, setProjTick] = useStateMap(0);   // re-render when projector windows open/close
  const [editorMode, setEditorMode] = useStateMap("dst"); // dst | warp | mesh | mask | reference
  const [pickCorners, setPickCorners] = useStateMap(null); // perspective pick: array of clicked pts
  const [autoThr, setAutoThr] = useStateMap(0.5);          // silhouette threshold
  const [autoInv, setAutoInv] = useStateMap(false);        // trace dark instead of bright
  const [refImage, setRefImage] = useStateMap(null);
  const [refOpacity, setRefOpacity] = useStateMap(0.4);
  const [showGrid, setShowGrid] = useStateMap(true);
  const [showLabels, setShowLabels] = useStateMap(true);
  const [fsOut, setFsOut] = useStateMap(false);
  const [presets, setPresets] = useStateMap(() => {
    try { return JSON.parse(localStorage.getItem(STORE_PRESETS) || "[]"); } catch { return []; }
  });
  const [presetName, setPresetName] = useStateMap("");

  useEffectMap(() => localStorage.setItem(STORE_PRESETS, JSON.stringify(presets)), [presets]);

  /* === Sync UI state → global mapping store (projector windows + project I/O read it) === */
  useEffectMap(() => {
    const ST = window.LuminaMapping;
    if (ST) ST.set({ slices, outRes, blendGamma, testPattern }, "ui");
  }, [slices, outRes, blendGamma, testPattern]);

  /* === External changes (project load) → UI state === */
  useEffectMap(() => {
    const ST = window.LuminaMapping;
    if (!ST) return;
    return ST.onChange((src) => {
      if (src === "ui") return;
      if (src === "proj") { setProjTick(t => t + 1); return; }
      _sid = Math.max(_sid, ...ST.slices.map(s => s.id || 0), 0);
      setSlices(ST.slices);
      setOutRes(ST.outRes);
      setBlendGamma(ST.blendGamma);
    });
  }, []);

  const [outW, outH] = outRes.split("x").map(Number);
  const sel = slices.find(s => s.id === selectedId);

  const canvasRef = useRefMap(null);
  const dragRef = useRefMap(null);  // { kind, id, idx, startX, startY, base }
  const mapSrcRef = useRefMap(null); // live composite source texture

  /* === Render preview === */
  useEffectMap(() => {
    const cnv = canvasRef.current;
    if (!cnv) return;
    const ctx = cnv.getContext("2d");
    let raf;

    const drawPolyHandle = (x, y, color, big = false) => {
      ctx.fillStyle = color;
      ctx.strokeStyle = "#000";
      ctx.lineWidth = 1.5;
      const r = big ? 7 : 5;
      ctx.beginPath();
      ctx.arc(x, y, r, 0, Math.PI * 2);
      ctx.fill(); ctx.stroke();
    };

    const tick = (t) => {
      const r = cnv.parentElement.getBoundingClientRect();
      const dpr = Math.min(2, window.devicePixelRatio || 1);
      const aspect = outW / outH;
      let W = r.width - 40, H = r.height - 40;
      if (W / H > aspect) W = H * aspect; else H = W / aspect;
      W = Math.floor(W); H = Math.floor(H);
      cnv.width = W * dpr; cnv.height = H * dpr;
      cnv.style.width = W + "px"; cnv.style.height = H + "px";
      ctx.setTransform(dpr, 0, 0, dpr, 0, 0);

      // bg
      ctx.fillStyle = "#0a0a0c";
      ctx.fillRect(0, 0, W, H);

      // reference image
      if (refImage && refImage.el) {
        ctx.save();
        ctx.globalAlpha = refOpacity;
        const sr = refImage.w / refImage.h, tr = W / H;
        let dw, dh;
        if (sr > tr) { dw = W; dh = W / sr; }
        else { dh = H; dw = H * sr; }
        ctx.drawImage(refImage.el, (W - dw) / 2, (H - dh) / 2, dw, dh);
        ctx.restore();
      }

      // outer guide
      ctx.strokeStyle = "rgba(255,255,255,0.1)";
      ctx.strokeRect(0, 0, W, H);

      if (showGrid) {
        ctx.strokeStyle = "rgba(255,255,255,0.05)";
        for (let i = 1; i < 10; i++) {
          ctx.beginPath();
          ctx.moveTo((i / 10) * W, 0); ctx.lineTo((i / 10) * W, H);
          ctx.moveTo(0, (i / 10) * H); ctx.lineTo(W, (i / 10) * H);
          ctx.stroke();
        }
        ctx.strokeStyle = "rgba(255,174,58,0.12)";
        ctx.beginPath(); ctx.moveTo(W/2, 0); ctx.lineTo(W/2, H);
        ctx.moveTo(0, H/2); ctx.lineTo(W, H/2); ctx.stroke();
      }

      // Each slice
      const time = t / 1000;
      const A = window.AudioEngine || {};
      // Render the LIVE composite into a source texture once per frame
      let mapSrc = mapSrcRef.current;
      if (!mapSrc) { mapSrc = mapSrcRef.current = document.createElement("canvas"); }
      if (mapSrc.width !== W || mapSrc.height !== H) { mapSrc.width = W; mapSrc.height = H; }
      const msx = mapSrc.getContext("2d");
      msx.setTransform(1, 0, 0, 1, 0, 0);
      if (window.LuminaComposite) { try { window.LuminaComposite(msx, W, H, time); } catch (e) { msx.fillStyle = "#111"; msx.fillRect(0, 0, W, H); } }
      else { msx.fillStyle = "#111"; msx.fillRect(0, 0, W, H); }
      slices.forEach(s => {
        if (!s.enabled) {
          const [dx, dy, dw, dh] = s.dst.map((v, i) => v * (i % 2 === 0 ? W : H));
          ctx.save();
          ctx.fillStyle = "rgba(40,40,46,0.5)";
          ctx.fillRect(dx, dy, dw, dh);
          ctx.strokeStyle = "rgba(255,255,255,0.1)";
          ctx.strokeRect(dx, dy, dw, dh);
          ctx.fillStyle = "rgba(255,255,255,0.3)";
          ctx.font = "bold 11px sans-serif";
          ctx.textAlign = "center"; ctx.textBaseline = "middle";
          if (showLabels) ctx.fillText(s.name + " (off)", dx + dw / 2, dy + dh / 2);
          ctx.restore();
          return;
        }

        const [dx, dy, dw, dh] = s.dst.map((v, i) => v * (i % 2 === 0 ? W : H));
        // Compute warped quad
        const corners = s.warpEnabled ? s.warp : [[0,0],[1,0],[1,1],[0,1]];
        const pts = corners.map(([u, v]) => [dx + u * dw, dy + v * dh]);

        // Content + mask/feather + warp/rotation + gamma edge blend —
        // same shared renderer used by the projector windows (true WYSIWYG).
        if (window.LuminaMapping) {
          window.LuminaMapping.renderSlice(ctx, s, W, H, mapSrc, testPattern);
        }

        // Outlines
        ctx.save();
        const isSel = s.id === selectedId;
        const bw = isSel ? 3 : 2;
        ctx.strokeStyle = s.color;
        ctx.shadowColor = s.color;
        ctx.shadowBlur = isSel ? 14 : 6;
        ctx.lineWidth = bw;
        if (s.warpEnabled) {
          ctx.beginPath();
          pts.forEach(([x, y], i) => i === 0 ? ctx.moveTo(x, y) : ctx.lineTo(x, y));
          ctx.closePath(); ctx.stroke();
        } else {
          ctx.strokeRect(dx + bw/2, dy + bw/2, dw - bw, dh - bw);
        }
        if (s.maskEnabled && s.mask) {
          ctx.strokeStyle = "#fff";
          ctx.setLineDash([4, 4]);
          ctx.beginPath();
          s.mask.forEach(([u, v], i) => {
            const x = dx + u * dw, y = dy + v * dh;
            if (i === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y);
          });
          ctx.closePath(); ctx.stroke();
          ctx.setLineDash([]);
        }
        ctx.shadowBlur = 0;
        ctx.restore();

        // Label
        if (showLabels) {
          ctx.fillStyle = "#fff";
          ctx.font = `bold ${Math.max(11, Math.min(dw, dh) / 12)}px sans-serif`;
          ctx.textAlign = "center"; ctx.textBaseline = "middle";
          ctx.fillText(s.name, dx + dw / 2, dy + dh / 2 - 6);
          ctx.fillStyle = "rgba(255,255,255,0.7)";
          ctx.font = "bold 9px monospace";
          ctx.fillText(`DISP ${s.display} · ${(s.displayPx[0])}×${s.displayPx[1]}`, dx + dw / 2, dy + dh / 2 + 12);
        }

        // Editing handles
        if (isSel) {
          if (editorMode === "dst") {
            // rect handles at corners
            [[dx, dy], [dx+dw, dy], [dx+dw, dy+dh], [dx, dy+dh]].forEach(([x, y]) =>
              drawPolyHandle(x, y, "var(--accent)"));
          }
          if (editorMode === "warp" && s.warpEnabled) {
            pts.forEach(([x, y]) => drawPolyHandle(x, y, "#4be6ff", true));
          }
          if (editorMode === "mask" && s.mask) {
            s.mask.forEach(([u, v], i) => {
              drawPolyHandle(dx + u * dw, dy + v * dh, i === 0 ? "#ff5ad6" : "#7cff4f", true);
            });
          }
          if (editorMode === "mesh" && s.meshEnabled && s.mesh) {
            const { cols, rows, pts: mp } = s.mesh;
            ctx.strokeStyle = "rgba(124,255,79,0.55)";
            ctx.lineWidth = 1;
            for (let r2 = 0; r2 <= rows; r2++) {
              ctx.beginPath();
              for (let c2 = 0; c2 <= cols; c2++) {
                const [u, v] = mp[r2 * (cols + 1) + c2];
                const X = dx + u * dw, Y = dy + v * dh;
                c2 === 0 ? ctx.moveTo(X, Y) : ctx.lineTo(X, Y);
              }
              ctx.stroke();
            }
            for (let c2 = 0; c2 <= cols; c2++) {
              ctx.beginPath();
              for (let r2 = 0; r2 <= rows; r2++) {
                const [u, v] = mp[r2 * (cols + 1) + c2];
                const X = dx + u * dw, Y = dy + v * dh;
                r2 === 0 ? ctx.moveTo(X, Y) : ctx.lineTo(X, Y);
              }
              ctx.stroke();
            }
            mp.forEach(([u, v]) => drawPolyHandle(dx + u * dw, dy + v * dh, "#7cff4f"));
          }
        }
      });

      // perspective pick markers (Reference mode)
      if (pickCorners) {
        pickCorners.forEach(([u, v], i) => {
          drawPolyHandle(u * W, v * H, "#ff5ad6", true);
          ctx.fillStyle = "#fff";
          ctx.font = "bold 11px monospace";
          ctx.textAlign = "center";
          ctx.fillText(["TL", "TR", "BR", "BL"][i] || "", u * W, v * H - 12);
        });
        ctx.fillStyle = "rgba(255,90,214,0.9)";
        ctx.font = "bold 11px monospace";
        ctx.textAlign = "center";
        ctx.fillText(`Click corner ${["TL — top left", "TR — top right", "BR — bottom right", "BL — bottom left"][pickCorners.length] || ""} on the photo`, W / 2, 22);
      }

      raf = requestAnimationFrame(tick);
    };
    raf = requestAnimationFrame(tick);
    return () => cancelAnimationFrame(raf);
  }, [slices, selectedId, outW, outH, refImage, refOpacity, showGrid, showLabels, editorMode, testPattern, blendGamma, pickCorners]);

  /* === Mouse interactions === */
  const onCanvasMouseDown = (e) => {
    const rect = canvasRef.current.getBoundingClientRect();
    const W = rect.width, H = rect.height;
    const x = e.clientX - rect.left, y = e.clientY - rect.top;
    const ux = x / W, uy = y / H;

    // perspective pick: collect 4 photo corners → slice warp
    if (pickCorners && sel) {
      const np = [...pickCorners, [ux, uy]];
      if (np.length >= 4) {
        const [dx, dy, dw, dh] = sel.dst;
        const rel = np.map(([u, v]) => [(u - dx) / dw, (v - dy) / dh]);
        updSel({ warp: rel, warpEnabled: true, meshEnabled: false });
        setPickCorners(null);
        setEditorMode("warp");
      } else setPickCorners(np);
      return;
    }

    if (sel) {
      const [dx, dy, dw, dh] = sel.dst;
      if (editorMode === "mesh" && sel.meshEnabled && sel.mesh) {
        const mp = sel.mesh.pts;
        for (let i = 0; i < mp.length; i++) {
          const px = (dx + mp[i][0] * dw) * W, py = (dy + mp[i][1] * dh) * H;
          if (Math.hypot(px - x, py - y) < 14) {
            dragRef.current = { kind: "meshpt", id: sel.id, idx: i };
            return;
          }
        }
      }
      if (editorMode === "warp" && sel.warpEnabled) {
        // find closest warp handle
        for (let i = 0; i < sel.warp.length; i++) {
          const [u, v] = sel.warp[i];
          const px = (dx + u * dw) * W, py = (dy + v * dh) * H;
          if (Math.hypot(px - x, py - y) < 14) {
            dragRef.current = { kind: "warp", id: sel.id, idx: i };
            return;
          }
        }
      }
      if (editorMode === "mask" && sel.mask) {
        for (let i = 0; i < sel.mask.length; i++) {
          const [u, v] = sel.mask[i];
          const px = (dx + u * dw) * W, py = (dy + v * dh) * H;
          if (Math.hypot(px - x, py - y) < 14) {
            dragRef.current = { kind: "mask", id: sel.id, idx: i };
            return;
          }
        }
        if (e.altKey) {
          // add point to mask polygon
          const u = (ux - dx) / dw, v = (uy - dy) / dh;
          if (u >= 0 && u <= 1 && v >= 0 && v <= 1) {
            updSel({ mask: [...sel.mask, [u, v]] });
            return;
          }
        }
      }
      if (editorMode === "dst") {
        // check rect corners for resize, body for move
        const corners = [[dx, dy], [dx+dw, dy], [dx+dw, dy+dh], [dx, dy+dh]];
        for (let i = 0; i < 4; i++) {
          const px = corners[i][0] * W, py = corners[i][1] * H;
          if (Math.hypot(px - x, py - y) < 14) {
            dragRef.current = { kind: "dstResize", id: sel.id, idx: i, base: [...sel.dst] };
            return;
          }
        }
      }
    }

    // Click selects a slice
    let foundId = null;
    for (let i = slices.length - 1; i >= 0; i--) {
      const s = slices[i];
      const [dx, dy, dw, dh] = s.dst;
      if (ux >= dx && ux <= dx + dw && uy >= dy && uy <= dy + dh) {
        foundId = s.id; break;
      }
    }
    if (foundId !== null) {
      setSelectedId(foundId);
      const s = slices.find(ss => ss.id === foundId);
      if (editorMode === "dst") {
        dragRef.current = { kind: "dstMove", id: foundId,
          startU: ux, startV: uy, base: [...s.dst] };
      }
    }
  };

  const onCanvasMouseMove = (e) => {
    if (!dragRef.current) return;
    const rect = canvasRef.current.getBoundingClientRect();
    const ux = (e.clientX - rect.left) / rect.width;
    const uy = (e.clientY - rect.top) / rect.height;
    const d = dragRef.current;
    const s = slices.find(ss => ss.id === d.id);
    if (!s) return;

    if (d.kind === "dstMove") {
      const nx = Math.max(0, Math.min(1 - d.base[2], d.base[0] + (ux - d.startU)));
      const ny = Math.max(0, Math.min(1 - d.base[3], d.base[1] + (uy - d.startV)));
      upd(d.id, { dst: [nx, ny, d.base[2], d.base[3]] });
    } else if (d.kind === "dstResize") {
      const [bx, by, bw, bh] = d.base;
      let nx = bx, ny = by, nw = bw, nh = bh;
      // corners: 0=TL, 1=TR, 2=BR, 3=BL
      if (d.idx === 0) { nw = bx + bw - ux; nh = by + bh - uy; nx = ux; ny = uy; }
      if (d.idx === 1) { nw = ux - bx; nh = by + bh - uy; ny = uy; }
      if (d.idx === 2) { nw = ux - bx; nh = uy - by; }
      if (d.idx === 3) { nw = bx + bw - ux; nh = uy - by; nx = ux; }
      nw = Math.max(0.02, nw); nh = Math.max(0.02, nh);
      upd(d.id, { dst: [Math.max(0, nx), Math.max(0, ny), nw, nh] });
    } else if (d.kind === "warp") {
      const [dx, dy, dw, dh] = s.dst;
      const u = (ux - dx) / dw, v = (uy - dy) / dh;
      const nw = [...s.warp];
      nw[d.idx] = [u, v];
      upd(d.id, { warp: nw });
    } else if (d.kind === "mask") {
      const [dx, dy, dw, dh] = s.dst;
      const u = Math.max(0, Math.min(1, (ux - dx) / dw));
      const v = Math.max(0, Math.min(1, (uy - dy) / dh));
      const nm = [...s.mask];
      nm[d.idx] = [u, v];
      upd(d.id, { mask: nm });
    } else if (d.kind === "meshpt") {
      const [dx, dy, dw, dh] = s.dst;
      const u = Math.max(-0.5, Math.min(1.5, (ux - dx) / dw));
      const v = Math.max(-0.5, Math.min(1.5, (uy - dy) / dh));
      const mesh = { ...s.mesh, pts: s.mesh.pts.map((p, i) => i === d.idx ? [u, v] : p) };
      upd(d.id, { mesh });
    }
  };
  const onCanvasMouseUp = () => { dragRef.current = null; };

  const upd = (id, patch) => setSlices(arr => arr.map(s => s.id === id ? { ...s, ...patch } : s));
  const updSel = (patch) => sel && upd(sel.id, patch);

  /* precision nudge: arrows move the selected slice (Shift = ×10) */
  useEffectMap(() => {
    const onKey = (e) => {
      const tag = e.target && e.target.tagName;
      if (tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT") return;
      if (!sel) return;
      const step = e.shiftKey ? 0.01 : 0.001;
      let [x, y, w2, h2] = sel.dst;
      if (e.key === "ArrowLeft") x -= step;
      else if (e.key === "ArrowRight") x += step;
      else if (e.key === "ArrowUp") y -= step;
      else if (e.key === "ArrowDown") y += step;
      else return;
      e.preventDefault();
      updSel({ dst: [Math.max(-0.5, Math.min(1.5, x)), Math.max(-0.5, Math.min(1.5, y)), w2, h2] });
    };
    document.addEventListener("keydown", onKey);
    return () => document.removeEventListener("keydown", onKey);
  }, [sel]);

  /* === Actions === */
  const addSlice = () => {
    const s = makeSlice("Slice " + (slices.length + 1), [0.1, 0.1, 0.3, 0.3], [0.1, 0.1, 0.3, 0.3], 1);
    setSlices(arr => [...arr, s]);
    setSelectedId(s.id);
  };
  const dupSlice = () => {
    if (!sel) return;
    const s = { ...sel, id: ++_sid, name: sel.name + " copy",
      dst: sel.dst.map((v, i) => i < 2 ? Math.min(0.95, v + 0.05) : v) };
    setSlices(arr => [...arr, s]);
    setSelectedId(s.id);
  };
  const removeSlice = (id) => {
    setSlices(arr => arr.filter(s => s.id !== id));
    if (selectedId === id) setSelectedId(null);
  };
  const applyTemplate = (key) => {
    const t = MAPPING_TEMPLATES[key];
    if (!t) return;
    if (slices.length && !confirm("Replace current slices with template?")) return;
    setSlices(t.make());
    setSelectedId(null);
  };
  const enableMask = () => {
    if (!sel) return;
    if (!sel.mask) {
      updSel({ mask: [[0.1, 0.1], [0.9, 0.1], [0.9, 0.9], [0.1, 0.9]], maskEnabled: true });
    } else {
      updSel({ maskEnabled: !sel.maskEnabled });
    }
    setEditorMode("mask");
  };
  const addMaskPoint = () => {
    if (!sel) return;
    const pts = sel.mask || [[0.1,0.1],[0.9,0.1],[0.9,0.9],[0.1,0.9]];
    // insert at midpoint of longest segment
    let maxLen = 0, maxIdx = 0;
    for (let i = 0; i < pts.length; i++) {
      const [u1, v1] = pts[i], [u2, v2] = pts[(i + 1) % pts.length];
      const d = Math.hypot(u2 - u1, v2 - v1);
      if (d > maxLen) { maxLen = d; maxIdx = i; }
    }
    const np = [...pts];
    const [u1, v1] = pts[maxIdx], [u2, v2] = pts[(maxIdx + 1) % pts.length];
    np.splice(maxIdx + 1, 0, [(u1 + u2) / 2, (v1 + v2) / 2]);
    updSel({ mask: np });
  };
  const removeMaskPoint = () => {
    if (!sel?.mask || sel.mask.length <= 3) return;
    updSel({ mask: sel.mask.slice(0, -1) });
  };
  const presetShape = (kind) => {
    if (!sel) return;
    let mask;
    if (kind === "tri") mask = [[0.5, 0.05], [0.95, 0.95], [0.05, 0.95]];
    else if (kind === "diamond") mask = [[0.5, 0.05], [0.95, 0.5], [0.5, 0.95], [0.05, 0.5]];
    else if (kind === "hex") {
      mask = [];
      for (let i = 0; i < 6; i++) {
        const a = (i / 6) * Math.PI * 2 - Math.PI / 2;
        mask.push([0.5 + Math.cos(a) * 0.45, 0.5 + Math.sin(a) * 0.45]);
      }
    }
    else if (kind === "circle") {
      mask = [];
      for (let i = 0; i < 24; i++) {
        const a = (i / 24) * Math.PI * 2;
        mask.push([0.5 + Math.cos(a) * 0.48, 0.5 + Math.sin(a) * 0.48]);
      }
    }
    else if (kind === "star") {
      mask = [];
      for (let i = 0; i < 10; i++) {
        const a = (i / 10) * Math.PI * 2 - Math.PI / 2;
        const r = i % 2 === 0 ? 0.48 : 0.22;
        mask.push([0.5 + Math.cos(a) * r, 0.5 + Math.sin(a) * r]);
      }
    }
    else if (kind === "arch") {
      // round-headed arch — windows/portals on historic façades
      mask = [[0.08, 0.95], [0.08, 0.45]];
      for (let i = 0; i <= 12; i++) {
        const a = Math.PI - (i / 12) * Math.PI;
        mask.push([0.5 + Math.cos(a) * 0.42, 0.45 - Math.sin(a) * 0.4]);
      }
      mask.push([0.92, 0.95]);
    }
    updSel({ mask, maskEnabled: true });
    setEditorMode("mask");
  };
  const importReference = (e) => {
    const f = e.target.files[0];
    if (!f) return;
    const url = URL.createObjectURL(f);
    const im = new Image();
    im.onload = () => {
      setRefImage({ src: url, el: im, w: im.naturalWidth, h: im.naturalHeight, name: f.name });
    };
    im.src = url;
  };
  const savePreset = () => {
    if (!presetName.trim()) { alert("Give your preset a name"); return; }
    const data = {
      id: Date.now(), name: presetName, outRes,
      slices: slices.map(({ color, ...rest }) => rest),
      created: new Date().toISOString(),
    };
    setPresets(arr => [...arr, data]);
    setPresetName("");
  };
  const loadPreset = (p) => {
    if (!confirm(`Load preset "${p.name}"? Current slices will be replaced.`)) return;
    setOutRes(p.outRes);
    setSlices(p.slices.map((s, i) => ({
      ...s, id: ++_sid,
      color: SLICE_COLORS[i % SLICE_COLORS.length],
    })));
    setSelectedId(null);
  };
  const deletePreset = (id) => setPresets(arr => arr.filter(p => p.id !== id));
  const exportPreset = (p) => {
    const blob = new Blob([JSON.stringify(p, null, 2)], { type: "application/json" });
    const a = document.createElement("a");
    a.href = URL.createObjectURL(blob);
    a.download = `lumina-mapping-${p.name.replace(/[^a-z0-9]/gi, "_")}.json`;
    a.click();
  };
  const importPreset = (e) => {
    const f = e.target.files[0];
    if (!f) return;
    const r = new FileReader();
    r.onload = () => {
      try {
        const data = JSON.parse(r.result);
        if (Array.isArray(data.slices)) setPresets(arr => [...arr, { ...data, id: Date.now() }]);
      } catch (err) { alert("Invalid preset: " + err.message); }
    };
    r.readAsText(f);
    e.target.value = "";
  };

  return (
    <div className="panel stage-bezel" style={{ padding: 0, display: "flex", flexDirection: "column", overflow: "hidden" }}>
      <Screws />
      <div className="stage-toolbar" style={{ flexWrap: "wrap" }}>
        <div className="stage-tabs"><div className="stage-tab active">Output Mapping</div></div>
        <div style={{ flex: 1 }} />
        <span className="cap">Mode:</span>
        <div className="chips" style={{ padding: 3 }}>
          {[
            { v: "dst", l: "Move/Resize", c: "var(--accent)" },
            { v: "warp", l: "Perspective", c: "var(--led-cyan)" },
            { v: "mesh", l: "Mesh Warp", c: "var(--led-green)" },
            { v: "mask", l: "Mask Shape", c: "var(--led-magenta)" },
            { v: "reference", l: "Reference", c: "var(--led-violet)" },
          ].map(o => (
            <button key={o.v}
              className={`chip ${editorMode === o.v ? "active" : ""}`}
              onClick={() => {
                setEditorMode(o.v);
                // one-click activation: picking a warp mode arms it on the slice
                if (o.v === "warp" && sel && !sel.warpEnabled) updSel({ warpEnabled: true, meshEnabled: false });
                if (o.v === "mesh" && sel && !sel.meshEnabled) {
                  updSel({ mesh: sel.mesh || window.LuminaMapping.makeMesh(3, 3), meshEnabled: true, warpEnabled: false });
                }
                if (o.v === "mask" && sel && !sel.mask) {
                  updSel({ mask: [[0.1, 0.1], [0.9, 0.1], [0.9, 0.9], [0.1, 0.9]], maskEnabled: true });
                }
              }}
              style={{ padding: "5px 10px", fontSize: 9 }}>{o.l}</button>
          ))}
        </div>
        <button className={`nbtn compact ${showGrid ? "glow pressed" : ""}`} onClick={() => setShowGrid(g => !g)}>Grid</button>
        <button className={`nbtn compact ${showLabels ? "glow pressed" : ""}`} onClick={() => setShowLabels(l => !l)}>Labels</button>
        <button className={`nbtn compact ${testPattern ? "glow pressed" : ""}`}
          onClick={() => setTestPattern(t => !t)}
          title="Replace content with numbered alignment grids on every output — line up each surface, then switch back"
          style={testPattern ? { color: "var(--led-cyan)" } : null}>▦ Test</button>
        <button className="nbtn compact glow pressed" onClick={addSlice}>+ Slice</button>
        <button className="nbtn compact" style={{ color: "var(--led-red)", fontWeight: 700 }}
          onClick={() => setFsOut(true)} title="Send mapped output fullscreen to the projector display">⛶ Output</button>
      </div>

      {fsOut && <MappedOutput slices={slices} outW={outW} outH={outH} onClose={() => setFsOut(false)} />}

      <div className="lum-wsgrid" style={{ display: "grid", gridTemplateColumns: "240px 1fr 280px", flex: 1, minHeight: 0 }}>
        {/* LEFT: templates + slice list + presets */}
        <div style={{
          padding: 10, background: "linear-gradient(180deg, #14141a, #0a0a0c)",
          borderRight: "1px solid rgba(255,255,255,0.04)",
          display: "flex", flexDirection: "column", gap: 10, overflowY: "auto", minHeight: 0,
        }}>
          <div className="cap">Output Resolution</div>
          <select value={outRes} onChange={e => setOutRes(e.target.value)}
            style={{
              padding: "5px 8px", borderRadius: 6, border: "none", outline: "none",
              background: "linear-gradient(180deg,#0a0a0c,#16161a)",
              color: "var(--led-cyan)", textShadow: "0 0 3px var(--led-cyan-glow)",
              fontFamily: "var(--font-mono)", fontSize: 10, boxShadow: "var(--nshadow-in-sm)",
            }}>
            {["1280x720","1920x1080","2560x1440","3840x2160","1920x540","3840x540",
              "1440x1080","2048x1024","2560x800","5120x1440"].map(r => <option key={r}>{r}</option>)}
          </select>

          <div className="cap" style={{ marginTop: 4 }}>Templates</div>
          <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 4 }}>
            {Object.entries(MAPPING_TEMPLATES).map(([k, t]) => (
              <button key={k} className="nbtn compact"
                onClick={() => applyTemplate(k)}
                style={{ padding: "8px 4px", fontSize: 9, flexDirection: "column", gap: 4 }}>
                <TemplatePreview slices={t.make()} />
                {t.label}
              </button>
            ))}
          </div>

          <div className="cap" style={{ marginTop: 6 }}>Slices ({slices.length})</div>
          <div style={{ display: "flex", flexDirection: "column", gap: 4, maxHeight: 200, overflowY: "auto" }}>
            {slices.map(s => (
              <div key={s.id}
                onClick={() => setSelectedId(s.id)}
                style={{
                  display: "grid", gridTemplateColumns: "8px 1fr auto auto", gap: 6, alignItems: "center",
                  padding: "5px 7px", cursor: "pointer", borderRadius: 5, flexShrink: 0,
                  background: s.id === selectedId
                    ? `linear-gradient(90deg, ${s.color}40, transparent)`
                    : "linear-gradient(180deg, var(--panel-hi), var(--panel))",
                  border: s.id === selectedId ? `1px solid ${s.color}` : "1px solid rgba(255,255,255,0.04)",
                  boxShadow: s.id === selectedId ? `0 0 8px ${s.color}66` : "none",
                }}>
                <span style={{
                  width: 8, height: 8, borderRadius: 2, background: s.color,
                  boxShadow: `0 0 6px ${s.color}`,
                }} />
                <span style={{
                  fontFamily: "var(--font-mono)", fontSize: 10,
                  color: s.id === selectedId ? "var(--ink)" : "var(--ink-dim)",
                  whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis",
                }}>{s.name}</span>
                <span style={{ fontFamily: "var(--font-mono)", fontSize: 8, color: "var(--ink-faint)" }}>
                  D{s.display}
                </span>
                <button className="nbtn compact" style={{ padding: "2px 4px", fontSize: 8 }}
                  onClick={(e) => { e.stopPropagation(); upd(s.id, { enabled: !s.enabled }); }}>
                  {s.enabled ? "●" : "○"}
                </button>
              </div>
            ))}
          </div>

          {/* Reference image */}
          <div className="cap" style={{ marginTop: 6 }}>Reference Image</div>
          {refImage ? (
            <div style={{ padding: 6, background: "rgba(255,255,255,0.03)", borderRadius: 6,
              display: "flex", gap: 6, alignItems: "center" }}>
              <img src={refImage.src} style={{
                width: 38, height: 28, objectFit: "cover", borderRadius: 3,
                border: "1px solid rgba(255,255,255,0.1)",
              }} alt="" />
              <div style={{ flex: 1, minWidth: 0 }}>
                <div style={{ fontFamily: "var(--font-mono)", fontSize: 9, color: "var(--led-violet)",
                  whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>
                  {refImage.name}
                </div>
                <div style={{ fontFamily: "var(--font-mono)", fontSize: 8, color: "var(--ink-faint)" }}>
                  {refImage.w}×{refImage.h}
                </div>
              </div>
              <button className="nbtn compact danger" style={{ padding: "2px 5px", fontSize: 9 }}
                onClick={() => setRefImage(null)}>×</button>
            </div>
          ) : (
            <button className="nbtn compact" style={{ position: "relative" }}>
              <input type="file" accept="image/*" onChange={importReference}
                style={{ position: "absolute", inset: 0, opacity: 0, cursor: "pointer" }} />
              + Import Photo/Plan
            </button>
          )}
          {refImage && (
            <div style={{ display: "grid", gridTemplateColumns: "40px 1fr 36px", gap: 6, alignItems: "center" }}>
              <span className="cap" style={{ fontSize: 8 }}>Opacity</span>
              <HSlider value={refOpacity} onChange={setRefOpacity} min={0.05} max={1} />
              <span style={{ fontFamily: "var(--font-mono)", fontSize: 9, color: "var(--led-violet)" }}>
                {(refOpacity * 100).toFixed(0)}%
              </span>
            </div>
          )}

          {/* PRESETS */}
          <div className="cap" style={{ marginTop: 8, display: "flex", justifyContent: "space-between" }}>
            <span>Stage Presets</span>
            <span style={{ color: "var(--accent)" }}>{presets.length}</span>
          </div>
          <div style={{ display: "flex", gap: 4 }}>
            <input value={presetName} onChange={e => setPresetName(e.target.value)}
              placeholder="Preset name"
              style={{
                flex: 1, padding: "5px 7px", borderRadius: 4, border: "none", outline: "none",
                background: "linear-gradient(180deg,#0a0a0c,#14141a)",
                color: "var(--ink)", fontFamily: "var(--font-mono)", fontSize: 10,
                boxShadow: "var(--nshadow-in-sm)",
              }} />
            <button className="nbtn compact glow pressed" style={{ padding: "5px 10px" }} onClick={savePreset}>
              💾
            </button>
          </div>
          <div style={{ display: "flex", flexDirection: "column", gap: 3, maxHeight: 160, overflowY: "auto" }}>
            {presets.map(p => (
              <div key={p.id}
                style={{
                  display: "grid", gridTemplateColumns: "1fr auto auto auto", gap: 4, alignItems: "center",
                  padding: "4px 6px", borderRadius: 4,
                  background: "linear-gradient(180deg, var(--panel-hi), var(--panel))",
                  boxShadow: "var(--nshadow-out-sm)",
                }}>
                <span style={{
                  fontFamily: "var(--font-mono)", fontSize: 10, color: "var(--accent)",
                  whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis",
                }}>📐 {p.name}</span>
                <button className="nbtn compact" style={{ padding: "2px 5px", fontSize: 8 }}
                  onClick={() => loadPreset(p)} title="Load">↺</button>
                <button className="nbtn compact" style={{ padding: "2px 5px", fontSize: 8 }}
                  onClick={() => exportPreset(p)} title="Export">⇩</button>
                <button className="nbtn compact danger" style={{ padding: "2px 5px", fontSize: 8 }}
                  onClick={() => deletePreset(p.id)}>×</button>
              </div>
            ))}
            {presets.length === 0 && (
              <div style={{ fontSize: 9, color: "var(--ink-faint)", padding: 6, textAlign: "center" }}>
                Save a mapping setup<br/>to recall every show.
              </div>
            )}
          </div>
          <label className="nbtn compact" style={{ position: "relative", padding: "4px 8px", fontSize: 9 }}>
            <input type="file" accept=".json" onChange={importPreset}
              style={{ position: "absolute", inset: 0, opacity: 0, cursor: "pointer" }} />
            ⬆ Import JSON
          </label>
        </div>

        {/* CENTER: canvas */}
        <div style={{
          position: "relative", background: "#000",
          display: "grid", placeItems: "center", overflow: "hidden", padding: 20,
        }}>
          <canvas ref={canvasRef}
            onMouseDown={onCanvasMouseDown}
            onMouseMove={onCanvasMouseMove}
            onMouseUp={onCanvasMouseUp}
            onMouseLeave={onCanvasMouseUp}
            style={{ cursor: dragRef.current ? "grabbing" : (editorMode === "dst" ? "grab" : "crosshair") }} />
          <div className="screen-corner" style={{ top: 10, left: 14 }}>
            ● MAPPING · {outW}×{outH} · {slices.filter(s => s.enabled).length}/{slices.length} active · MODE: {editorMode.toUpperCase()}
          </div>
          <div className="screen-corner" style={{ top: 10, right: 14 }}>
            DISPLAYS: {[...new Set(slices.filter(s => s.enabled).map(s => s.display))].sort().map(d => `D${d}`).join(" · ")}
          </div>
          <div className="screen-corner" style={{ bottom: 10, left: 14 }}>
            {editorMode === "dst" && "Drag slice body to move · drag corners to resize"}
            {editorMode === "warp" && (sel?.warpEnabled ? "Drag cyan dots to warp perspective" : "Enable Perspective Warp on selected slice")}
            {editorMode === "mesh" && (sel?.meshEnabled ? "Drag green grid points — video wraps onto curved/organic surfaces" : "Enable Mesh Warp in the inspector")}
            {editorMode === "mask" && (sel?.mask ? "Drag dots to reshape · Alt+click adds point" : "Click 'Enable Mask' to start shaping")}
            {editorMode === "reference" && (pickCorners ? "PERSPECTIVE PICK: click the surface corners on the photo (TL → TR → BR → BL)" : "Reference photo underneath — Venue Photo Tools: auto-mask + perspective pick")}
          </div>
        </div>

        {/* RIGHT: inspector */}
        <div style={{
          padding: 10, background: "linear-gradient(180deg, #14141a, #0a0a0c)",
          borderLeft: "1px solid rgba(255,255,255,0.04)",
          display: "flex", flexDirection: "column", gap: 8, overflowY: "auto", minHeight: 0,
        }}>
          {!sel ? (
            <div style={{ padding: 16, textAlign: "center", color: "var(--ink-faint)", fontSize: 11, lineHeight: 1.6 }}>
              <div className="cap" style={{ marginBottom: 10 }}>Slice Inspector</div>
              <p>Click a slice on canvas or in the list to edit.</p>
              <p style={{ fontSize: 10, marginTop: 8, opacity: 0.8 }}>
                Use modes above: Move/Resize · Perspective warp · Mask shape · Reference photo overlay.
              </p>
            </div>
          ) : (
            <>
              <div className="dock-section" style={{ padding: 8, borderLeft: `3px solid ${sel.color}` }}>
                <input value={sel.name}
                  onChange={e => updSel({ name: e.target.value })}
                  style={{
                    width: "100%", padding: "6px 8px",
                    background: "linear-gradient(180deg,#0a0a0c,#14141a)",
                    border: "none", borderRadius: 4, outline: "none",
                    color: sel.color, fontFamily: "var(--font-display)", fontSize: 12, fontWeight: 700,
                    boxShadow: "var(--nshadow-in-sm)",
                  }} />
                <div style={{ display: "flex", gap: 6, marginTop: 6 }}>
                  <div style={{ display: "flex", alignItems: "center", gap: 6, flex: 1 }}>
                    <span className="cap" style={{ fontSize: 8 }}>Enabled</span>
                    <Flip value={sel.enabled} onChange={v => updSel({ enabled: v })} />
                  </div>
                </div>
              </div>

              {/* PHYSICAL DISPLAY */}
              <div className="dock-section" style={{ padding: 8 }}>
                <div className="dock-label">Physical Display</div>
                <div style={{ display: "grid", gridTemplateColumns: "auto 1fr", gap: 6, alignItems: "center", marginTop: 6 }}>
                  <span className="cap" style={{ fontSize: 8 }}>Output</span>
                  <select value={sel.display} onChange={e => updSel({ display: Number(e.target.value) })}
                    style={inpStyle}>
                    {[1,2,3,4,5,6,7,8].map(d => <option key={d} value={d}>Display {d}</option>)}
                  </select>
                </div>
                <div style={{ display: "grid", gridTemplateColumns: "auto 1fr 1fr", gap: 6, alignItems: "center", marginTop: 6 }}>
                  <span className="cap" style={{ fontSize: 8 }}>Pixels</span>
                  <input type="number" value={sel.displayPx[0]}
                    onChange={e => updSel({ displayPx: [Number(e.target.value), sel.displayPx[1]] })}
                    style={inpStyle} />
                  <input type="number" value={sel.displayPx[1]}
                    onChange={e => updSel({ displayPx: [sel.displayPx[0], Number(e.target.value)] })}
                    style={inpStyle} />
                </div>
                <div style={{ display: "grid", gridTemplateColumns: "auto 1fr", gap: 6, alignItems: "center", marginTop: 6 }}>
                  <span className="cap" style={{ fontSize: 8 }}>Orient</span>
                  <div className="chips" style={{ padding: 2 }}>
                    {["landscape", "portrait"].map(o => (
                      <button key={o}
                        className={`chip ${sel.displayOrient === o ? "active" : ""}`}
                        onClick={() => updSel({ displayOrient: o })}
                        style={{ padding: "3px 6px", fontSize: 9 }}>{o}</button>
                    ))}
                  </div>
                </div>
                <div style={{ display: "grid", gridTemplateColumns: "auto 1fr", gap: 6, alignItems: "center", marginTop: 6 }}>
                  <span className="cap" style={{ fontSize: 8 }}
                    title="Which part of the output space this projector window shows">Region</span>
                  <div className="chips" style={{ padding: 2, flexWrap: "wrap" }}>
                    {[["Full", [0, 0, 1, 1]], ["L½", [0, 0, 0.5, 1]], ["R½", [0.5, 0, 0.5, 1]],
                      ["T½", [0, 0, 1, 0.5]], ["B½", [0, 0.5, 1, 0.5]]].map(([l, r]) => {
                      const cur = window.LuminaMapping ? window.LuminaMapping.region(sel.display) : [0, 0, 1, 1];
                      const is = cur.join() === r.join();
                      return (
                        <button key={l} className={`chip ${is ? "active" : ""}`}
                          onClick={() => {
                            const ST = window.LuminaMapping;
                            if (!ST) return;
                            ST.set({ displayRegions: { ...ST.displayRegions, [sel.display]: r } }, "ui");
                            setProjTick(t => t + 1);
                          }}
                          style={{ padding: "3px 6px", fontSize: 9 }}>{l}</button>
                      );
                    })}
                  </div>
                </div>
                <div style={{ fontSize: 8.5, color: "var(--ink-faint)", marginTop: 4, lineHeight: 1.4 }}>
                  The projector window for display {sel.display} shows this region of the
                  output space, scaled fullscreen.
                </div>
              </div>

              {/* RECT */}
              <div className="dock-section" style={{ padding: 8 }}>
                <div className="dock-label">Destination (output position)</div>
                <RectEditor rect={sel.dst} onChange={dst => updSel({ dst })} />
              </div>
              <div className="dock-section" style={{ padding: 8 }}>
                <div className="dock-label">Source (input crop)</div>
                <RectEditor rect={sel.src} onChange={src => updSel({ src })} />
                <button className="nbtn compact" style={{ marginTop: 6, width: "100%", padding: "4px", fontSize: 9 }}
                  title="Match the crop aspect to the warped surface — no stretching on the wall"
                  onClick={() => {
                    const [dx, dy, dw, dh] = sel.dst;
                    const cor = (sel.warpEnabled ? sel.warp : [[0, 0], [1, 0], [1, 1], [0, 1]])
                      .map(([u, v]) => [(dx + u * dw) * outW, (dy + v * dh) * outH]);
                    const wA = (Math.hypot(cor[1][0] - cor[0][0], cor[1][1] - cor[0][1])
                              + Math.hypot(cor[2][0] - cor[3][0], cor[2][1] - cor[3][1])) / 2;
                    const hA = (Math.hypot(cor[3][0] - cor[0][0], cor[3][1] - cor[0][1])
                              + Math.hypot(cor[2][0] - cor[1][0], cor[2][1] - cor[1][1])) / 2;
                    if (wA < 1 || hA < 1) return;
                    const quadAspect = wA / hA;
                    const [sx, sy, sw, sh] = sel.src;
                    const cx2 = sx + sw / 2, cy2 = sy + sh / 2;
                    let nw2 = sw, nh2 = nw2 * (outW / outH) / quadAspect;
                    if (nh2 > 1) { nw2 *= 1 / nh2; nh2 = 1; }
                    if (nw2 > 1) { nh2 *= 1 / nw2; nw2 = 1; }
                    const nx2 = Math.max(0, Math.min(1 - nw2, cx2 - nw2 / 2));
                    const ny2 = Math.max(0, Math.min(1 - nh2, cy2 - nh2 / 2));
                    updSel({ src: [nx2, ny2, nw2, nh2] });
                  }}>
                  ▣ Auto Crop — fit surface aspect
                </button>
              </div>

              {/* MESH WARP */}
              <div className="dock-section" style={{ padding: 8 }}>
                <div className="dock-label" style={{ display: "flex", justifyContent: "space-between" }}>
                  Mesh Warp (curved / organic)
                  <Flip value={!!sel.meshEnabled} onChange={v => {
                    if (v) {
                      updSel({ mesh: sel.mesh || window.LuminaMapping.makeMesh(3, 3), meshEnabled: true, warpEnabled: false });
                      setEditorMode("mesh");
                    } else updSel({ meshEnabled: false });
                  }} />
                </div>
                {sel.meshEnabled && (
                  <>
                    <div style={{ display: "grid", gridTemplateColumns: "repeat(4, 1fr)", gap: 3, marginTop: 6 }}>
                      {[[2, 2], [3, 3], [4, 4], [6, 4]].map(([cc, rr]) => (
                        <button key={cc + "x" + rr}
                          className={`nbtn compact ${sel.mesh && sel.mesh.cols === cc && sel.mesh.rows === rr ? "glow pressed" : ""}`}
                          onClick={() => updSel({ mesh: window.LuminaMapping.makeMesh(cc, rr) })}
                          style={{ padding: "4px 0", fontSize: 9 }}>{cc}×{rr}</button>
                      ))}
                    </div>
                    <button className="nbtn compact" style={{ marginTop: 4, width: "100%", padding: "3px", fontSize: 9 }}
                      onClick={() => sel.mesh && updSel({ mesh: window.LuminaMapping.makeMesh(sel.mesh.cols, sel.mesh.rows) })}>
                      Reset grid
                    </button>
                    <div style={{ fontSize: 8.5, color: "var(--ink-faint)", marginTop: 4 }}>
                      Drag the green grid points — wraps video onto vaults, columns, rocks.
                    </div>
                  </>
                )}
              </div>

              {/* VENUE PHOTO TOOLS */}
              <div className="dock-section" style={{ padding: 8 }}>
                <div className="dock-label">Venue Photo Tools</div>
                {!refImage ? (
                  <div style={{ fontSize: 9, color: "var(--ink-faint)", marginTop: 4, lineHeight: 1.5 }}>
                    Import a venue photo (left panel) to unlock silhouette recognition
                    and perspective pick.
                  </div>
                ) : (
                  <>
                    <button className="nbtn compact glow pressed" style={{ marginTop: 6, width: "100%", padding: "5px", fontSize: 9 }}
                      title="Click the 4 corners of the surface on the photo (TL → TR → BR → BL) — the slice warps to match the real perspective"
                      onClick={() => { setPickCorners([]); setEditorMode("reference"); }}>
                      ◱ Pick 4 corners on photo → warp
                    </button>
                    <button className="nbtn compact" style={{ marginTop: 4, width: "100%", padding: "5px", fontSize: 9 }}
                      title="Detects the dominant shape in the photo and builds the mask polygon automatically"
                      onClick={() => {
                        if (!window.LuminaMapping || !window.LuminaMapping.traceSilhouette) return;
                        const poly = window.LuminaMapping.traceSilhouette(refImage.el, { threshold: autoThr, invert: autoInv });
                        if (!poly) { alert("No clear silhouette found — adjust threshold / invert."); return; }
                        const sr = refImage.w / refImage.h, tr = outW / outH;
                        const fw = sr > tr ? 1 : sr / tr;
                        const fh = sr > tr ? tr / sr : 1;
                        const ox = (1 - fw) / 2, oy = (1 - fh) / 2;
                        const [dx, dy, dw, dh] = sel.dst;
                        const mask = poly.map(([u, v]) => [(ox + u * fw - dx) / dw, (oy + v * fh - dy) / dh]);
                        updSel({ mask, maskEnabled: true, feather: sel.feather || 6 });
                        setEditorMode("mask");
                      }}>
                      ⚙ Auto-mask — trace silhouette
                    </button>
                    <SI label="Thr" value={autoThr} onChange={setAutoThr} min={0.05} max={0.95} step={0.01} />
                    <div style={{ display: "flex", alignItems: "center", gap: 6, marginTop: 4 }}>
                      <span className="cap" style={{ fontSize: 8 }}>Trace dark</span>
                      <Flip value={autoInv} onChange={setAutoInv} />
                    </div>
                    <div style={{ fontSize: 8.5, color: "var(--ink-faint)", marginTop: 4, lineHeight: 1.5 }}>
                      Silhouette → mask polygon on the selected slice.
                      Adjust Thr until the building pops from the sky.
                    </div>
                  </>
                )}
              </div>

              {/* PERSPECTIVE */}
              <div className="dock-section" style={{ padding: 8 }}>
                <div className="dock-label" style={{ display: "flex", justifyContent: "space-between" }}>
                  Perspective Warp
                  <Flip value={sel.warpEnabled} onChange={v => updSel({ warpEnabled: v })} />
                </div>
                {sel.warpEnabled && (
                  <>
                    <div style={{ fontSize: 9, color: "var(--ink-faint)", marginTop: 4, marginBottom: 6 }}>
                      Drag the 4 cyan corner dots on canvas to warp.
                    </div>
                    <button className="nbtn compact"
                      onClick={() => updSel({ warp: [[0,0],[1,0],[1,1],[0,1]] })}>
                      Reset corners
                    </button>
                  </>
                )}
              </div>

              {/* MASK */}
              <div className="dock-section" style={{ padding: 8 }}>
                <div className="dock-label" style={{ display: "flex", justifyContent: "space-between" }}>
                  Mask Shape
                  <button className="nbtn compact" onClick={enableMask} style={{ padding: "2px 8px", fontSize: 9 }}>
                    {sel.maskEnabled ? "Disable" : sel.mask ? "Enable" : "Create"}
                  </button>
                </div>
                <div style={{ display: "grid", gridTemplateColumns: "repeat(5, 1fr)", gap: 3, marginTop: 6 }}>
                  {[["□","rect"],["△","tri"],["◇","diamond"],["⬡","hex"],["○","circle"]].map(([g, k]) => (
                    <button key={k} className="nbtn compact" onClick={() => presetShape(k)}
                      style={{ padding: "5px 0", fontSize: 13 }}>{g}</button>
                  ))}
                </div>
                <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 3, marginTop: 4 }}>
                  <button className="nbtn compact" onClick={() => presetShape("star")}
                    style={{ padding: "4px", fontSize: 9 }}>★ Star</button>
                  <button className="nbtn compact" onClick={() => presetShape("arch")}
                    title="Round arch — historic façade openings"
                    style={{ padding: "4px", fontSize: 9 }}>⌒ Arch</button>
                </div>
                {sel.mask && (
                  <div style={{ display: "flex", gap: 4, marginTop: 6 }}>
                    <button className="nbtn compact" style={{ flex: 1, padding: "3px", fontSize: 9 }}
                      onClick={addMaskPoint}>+ Point</button>
                    <button className="nbtn compact" style={{ flex: 1, padding: "3px", fontSize: 9 }}
                      onClick={removeMaskPoint}>− Point</button>
                  </div>
                )}
                {sel.mask && (
                  <SI label="Fthr" value={sel.feather || 0} onChange={v => updSel({ feather: v })}
                    min={0} max={80} step={1} unit="px" />
                )}
                {sel.mask && (
                  <div style={{ fontSize: 8.5, color: "var(--ink-faint)", marginTop: 4 }}>
                    {sel.mask.length} vertices · Alt+click adds · Feather softens the edge
                    against stone/brick.
                  </div>
                )}
              </div>

              {/* EDGE BLEND */}
              <div className="dock-section" style={{ padding: 8 }}>
                <div className="dock-label">Edge Blend (overlap multi-projector)</div>
                <SI label="←" value={sel.blend.left} onChange={v => updSel({ blend: {...sel.blend, left: v} })} min={0} max={0.5} step={0.001} />
                <SI label="→" value={sel.blend.right} onChange={v => updSel({ blend: {...sel.blend, right: v} })} min={0} max={0.5} step={0.001} />
                <SI label="↑" value={sel.blend.top} onChange={v => updSel({ blend: {...sel.blend, top: v} })} min={0} max={0.5} step={0.001} />
                <SI label="↓" value={sel.blend.bottom} onChange={v => updSel({ blend: {...sel.blend, bottom: v} })} min={0} max={0.5} step={0.001} />
                <SI label="γ" value={blendGamma} onChange={setBlendGamma} min={1} max={3} step={0.01} />
                <div style={{ fontSize: 8.5, color: "var(--ink-faint)", marginTop: 3 }}>
                  γ = photometric ramp (all slices) — tune until the overlap zone
                  matches the rest in brightness.
                </div>
              </div>

              {/* COLOR */}
              <div className="dock-section" style={{ padding: 8 }}>
                <div className="dock-label">Color Match</div>
                <SI label="Bri" value={sel.brightness} onChange={v => updSel({ brightness: v })} min={0} max={2} step={0.01} />
                <SI label="Con" value={sel.contrast} onChange={v => updSel({ contrast: v })} min={0} max={2} step={0.01} />
                <SI label="Sat" value={sel.saturation} onChange={v => updSel({ saturation: v })} min={0} max={2} step={0.01} />
              </div>

              {/* TRANSFORM */}
              <div className="dock-section" style={{ padding: 8 }}>
                <div className="dock-label">Transform</div>
                <SI label="Rot" value={sel.rotation || 0} onChange={v => updSel({ rotation: v })} min={-180} max={180} step={1} unit="°" />
                <div style={{ display: "flex", gap: 4, marginTop: 4 }}>
                  <button className={`nbtn compact ${sel.flipH ? "glow pressed" : ""}`}
                    style={{ flex: 1, padding: "3px", fontSize: 9 }}
                    onClick={() => updSel({ flipH: !sel.flipH })}>⇋ Flip H</button>
                  <button className={`nbtn compact ${sel.flipV ? "glow pressed" : ""}`}
                    style={{ flex: 1, padding: "3px", fontSize: 9 }}
                    onClick={() => updSel({ flipV: !sel.flipV })}>⇅ Flip V</button>
                </div>
              </div>

              <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 4 }}>
                <button className="nbtn compact" onClick={dupSlice}>Duplicate</button>
                <button className="nbtn compact danger" onClick={() => removeSlice(sel.id)}>Delete</button>
              </div>
            </>
          )}
        </div>
      </div>

      <div className="stage-strip" style={{ gap: 10, flexWrap: "wrap" }}>
        <span className="cap">PROJECTORS:</span>
        {[1,2,3,4,5,6,7,8].map(d => {
          const active = slices.some(s => s.display === d && s.enabled);
          const used = slices.some(s => s.display === d);
          if (!used && d > 4) return null;
          const open = window.LuminaMapping ? window.LuminaMapping.isProjectorOpen(d) : false;
          return (
            <button key={d} className="nbtn compact"
              onClick={() => {
                const ST = window.LuminaMapping;
                if (!ST) return;
                if (open) ST.closeProjector(d); else ST.openProjector(d);
              }}
              title={open
                ? `Close projector window D${d}`
                : `Open the real output window for display ${d} — auto-placed on a physical screen when the browser allows it · double-click inside = fullscreen`}
              style={{
                display: "flex", alignItems: "center", gap: 5, padding: "3px 9px",
                background: open ? "rgba(255,59,59,0.14)" : active ? "rgba(75,230,255,0.08)" : "transparent",
                boxShadow: open ? "0 0 10px rgba(255,59,59,0.45)" : "var(--nshadow-out-sm)",
              }}>
              <Led on={open ? true : active} color={open ? "red" : "cyan"} size={6} />
              <span style={{ fontSize: 9, fontFamily: "var(--font-mono)", fontWeight: open ? 800 : 400,
                color: open ? "var(--led-red)" : active ? "var(--led-cyan)" : "var(--ink-faint)" }}>
                D{d}{open ? " · LIVE" : ""}
              </span>
            </button>
          );
        })}
        <button className="nbtn compact" style={{ padding: "3px 9px", fontSize: 9 }}
          onClick={() => window.LuminaMapping && window.LuminaMapping.closeAllProjectors()}>
          ■ Close all
        </button>
        <div style={{ flex: 1 }} />
        <span className="cap" style={{ color: "var(--ink-faint)" }}>
          Click a D-button to open its projector window · ▦ Test to align · presets recall each show.
        </span>
      </div>
    </div>
  );
}

function MappedOutput({ slices, outW, outH, onClose }) {
  const cnvRef = useRefMap(null);
  const srcRef = useRefMap(null);
  useEffectMap(() => {
    const onKey = (e) => { if (e.key === "Escape") onClose(); };
    window.addEventListener("keydown", onKey);
    const cnv = cnvRef.current;
    const ctx = cnv.getContext("2d");
    let raf;
    const draw = () => {
      const W = cnv.width = window.innerWidth;
      const H = cnv.height = window.innerHeight;
      if (window.LuminaMapping) {
        // shared renderer — identical pipeline to the projector windows
        window.LuminaMapping.renderOutput(ctx, W, H, performance.now() / 1000, {});
      } else {
        ctx.fillStyle = "#000"; ctx.fillRect(0, 0, W, H);
      }
      raf = requestAnimationFrame(draw);
    };
    raf = requestAnimationFrame(draw);
    return () => { cancelAnimationFrame(raf); window.removeEventListener("keydown", onKey); };
  }, [slices]);

  return (
    <div style={{ position: "fixed", inset: 0, zIndex: 99999, background: "#000" }}>
      <canvas ref={cnvRef} style={{ display: "block", width: "100vw", height: "100vh" }} />
      <button data-lum-close="1" onClick={onClose}
        style={{
          position: "fixed", top: 14, right: 14, zIndex: 100000,
          padding: "8px 14px", border: "1px solid rgba(255,255,255,0.2)", borderRadius: 8,
          background: "rgba(0,0,0,0.6)", color: "#fff", cursor: "pointer",
          fontFamily: "var(--font-mono)", fontSize: 11, letterSpacing: "0.1em",
        }}>✕ EXIT OUTPUT (Esc)</button>
      <div style={{
        position: "fixed", bottom: 14, left: 14, zIndex: 100000,
        fontFamily: "var(--font-mono)", fontSize: 10, color: "rgba(255,255,255,0.4)",
        pointerEvents: "none",
      }}>LUMINA · mapped output {outW}×{outH} · drag this window onto the projector display</div>
    </div>
  );
}

function RectEditor({ rect, onChange }) {
  const [x, y, w, h] = rect;
  const upd = (i, v) => { const next = [...rect]; next[i] = v; onChange(next); };
  return (
    <div style={{ display: "flex", flexDirection: "column", gap: 4, marginTop: 6 }}>
      <SI label="X" value={x} onChange={v => upd(0, v)} min={0} max={1} step={0.001} />
      <SI label="Y" value={y} onChange={v => upd(1, v)} min={0} max={1} step={0.001} />
      <SI label="W" value={w} onChange={v => upd(2, v)} min={0.01} max={1} step={0.001} />
      <SI label="H" value={h} onChange={v => upd(3, v)} min={0.01} max={1} step={0.001} />
    </div>
  );
}
function SI({ label, value, onChange, min, max, step, unit = "" }) {
  return (
    <div style={{ display: "grid", gridTemplateColumns: "26px 1fr 44px", gap: 6, alignItems: "center", marginTop: 3 }}>
      <span className="cap" style={{ fontSize: 8 }}>{label}</span>
      <HSlider value={value} onChange={onChange} min={min} max={max} />
      <span style={{ fontFamily: "var(--font-mono)", fontSize: 9, color: "var(--accent)",
        textShadow: "0 0 3px var(--accent-glow)", textAlign: "right" }}>
        {step >= 1 ? value.toFixed(0) : value.toFixed(3)}{unit}
      </span>
    </div>
  );
}
const inpStyle = {
  padding: "4px 6px", borderRadius: 4, border: "none", outline: "none",
  background: "linear-gradient(180deg,#0a0a0c,#14141a)",
  color: "var(--led-cyan)", textShadow: "0 0 3px var(--led-cyan-glow)",
  fontFamily: "var(--font-mono)", fontSize: 10, boxShadow: "var(--nshadow-in-sm)", width: "100%",
};
function TemplatePreview({ slices }) {
  return (
    <svg viewBox="0 0 40 22" width="40" height="22" style={{ background: "#0a0a0c", borderRadius: 3, marginBottom: 2 }}>
      <rect width="40" height="22" fill="none" stroke="rgba(255,255,255,0.1)" />
      {slices.map((s, i) => (
        <rect key={i}
          x={s.dst[0] * 40 + 0.5} y={s.dst[1] * 22 + 0.5}
          width={s.dst[2] * 40 - 1} height={s.dst[3] * 22 - 1}
          fill={SLICE_COLORS[i % SLICE_COLORS.length]} fillOpacity="0.5"
          stroke={SLICE_COLORS[i % SLICE_COLORS.length]} strokeWidth="0.5" />
      ))}
    </svg>
  );
}

window.MappingWorkspace = MappingWorkspace;
