// FloorCanvas — measure / guides / align / export pack / presentation

const fcExUid = () => Math.random().toString(36).slice(2, 9);

// ─────────────────────────────────────────────────────────────────────
// Persistent Tape Measure shape — like dimension but with angle + delta
// ─────────────────────────────────────────────────────────────────────
function MeasureShape({ s, selected }) {
  const dx = s.b.x - s.a.x, dy = s.b.y - s.a.y;
  const len = Math.hypot(dx, dy);
  const mid = { x: (s.a.x + s.b.x) / 2, y: (s.a.y + s.b.y) / 2 };
  const angle = Math.atan2(dy, dx) * 180 / Math.PI;
  const norm = Math.abs(angle) > 90 ? angle + 180 : angle;
  const ft = Math.floor(len);
  const inches = Math.round((len - ft) * 12);
  const txt = `${ft}'-${inches}"  ·  ${Math.abs(angle).toFixed(1)}°`;
  const sw = window.COLORS;
  return (
    <g>
      <line x1={s.a.x} y1={s.a.y} x2={s.b.x} y2={s.b.y} stroke={sw.red} strokeWidth={1.2} vectorEffect="non-scaling-stroke" />
      <circle cx={s.a.x} cy={s.a.y} r={0.18} fill={sw.yellow} stroke={sw.ink} strokeWidth={1} vectorEffect="non-scaling-stroke" />
      <circle cx={s.b.x} cy={s.b.y} r={0.18} fill={sw.yellow} stroke={sw.ink} strokeWidth={1} vectorEffect="non-scaling-stroke" />
      <g transform={`translate(${mid.x},${mid.y}) rotate(${norm})`}>
        <rect x={-txt.length * 0.15} y={-0.36} width={txt.length * 0.3} height={0.72} fill={sw.yellow} stroke={sw.ink} strokeWidth={0.8} vectorEffect="non-scaling-stroke" />
        <text x={0} y={0} textAnchor="middle" dominantBaseline="middle" fontFamily="JetBrains Mono, monospace" fontSize={0.42} fill={sw.ink} fontWeight="700">{txt}</text>
      </g>
      {selected && (
        <>
          <circle cx={s.a.x} cy={s.a.y} r={0.32} fill="none" stroke={sw.blue} strokeWidth={1.5} vectorEffect="non-scaling-stroke" strokeDasharray="0.2 0.15" />
          <circle cx={s.b.x} cy={s.b.y} r={0.32} fill="none" stroke={sw.blue} strokeWidth={1.5} vectorEffect="non-scaling-stroke" strokeDasharray="0.2 0.15" />
        </>
      )}
    </g>
  );
}

// ─────────────────────────────────────────────────────────────────────
// Guide line — horizontal or vertical, spans the entire viewport
// ─────────────────────────────────────────────────────────────────────
function GuideShape({ s, selected, viewBox }) {
  const [minX, minY, w, h] = viewBox;
  const sw = window.COLORS;
  const color = selected ? sw.blue : sw.red;
  if (s.axis === 'h') {
    return (
      <g style={{ cursor: 'ns-resize' }}>
        <line x1={minX - 10} y1={s.pos} x2={minX + w + 10} y2={s.pos} stroke={color} strokeWidth={0.8} vectorEffect="non-scaling-stroke" strokeDasharray="0.3 0.2" opacity={0.7} />
        <rect x={minX + 0.2} y={s.pos - 0.3} width={1.6} height={0.6} fill={sw.paper} stroke={color} strokeWidth={0.8} vectorEffect="non-scaling-stroke" />
        <text x={minX + 1} y={s.pos} textAnchor="middle" dominantBaseline="middle" fontFamily="JetBrains Mono, monospace" fontSize={0.32} fill={color}>{s.pos.toFixed(1)}'</text>
      </g>
    );
  }
  return (
    <g style={{ cursor: 'ew-resize' }}>
      <line x1={s.pos} y1={minY - 10} x2={s.pos} y2={minY + h + 10} stroke={color} strokeWidth={0.8} vectorEffect="non-scaling-stroke" strokeDasharray="0.3 0.2" opacity={0.7} />
      <rect x={s.pos - 0.8} y={minY + 0.2} width={1.6} height={0.6} fill={sw.paper} stroke={color} strokeWidth={0.8} vectorEffect="non-scaling-stroke" />
      <text x={s.pos} y={minY + 0.5} textAnchor="middle" dominantBaseline="middle" fontFamily="JetBrains Mono, monospace" fontSize={0.32} fill={color}>{s.pos.toFixed(1)}'</text>
    </g>
  );
}

// ─────────────────────────────────────────────────────────────────────
// Rulers (top + left edges of canvas) — drag from ruler to drop guide
// ─────────────────────────────────────────────────────────────────────
function Rulers({ view, addGuide, gridSize }) {
  const [hovering, setHovering] = React.useState(null); // {axis, pos}
  const onMouseDown = (axis, e) => {
    e.preventDefault();
    const stage = document.querySelector('.stage');
    if (!stage) return;
    const r = stage.getBoundingClientRect();
    const move = (ev) => {
      const x = (ev.clientX - r.left - view.tx) / view.scale;
      const y = (ev.clientY - r.top - view.ty) / view.scale;
      setHovering({ axis, pos: axis === 'h' ? y : x });
    };
    const up = (ev) => {
      const x = (ev.clientX - r.left - view.tx) / view.scale;
      const y = (ev.clientY - r.top - view.ty) / view.scale;
      const pos = axis === 'h' ? y : x;
      const snapped = Math.round(pos / gridSize) * gridSize;
      addGuide(axis, snapped);
      setHovering(null);
      window.removeEventListener('mousemove', move);
      window.removeEventListener('mouseup', up);
    };
    window.addEventListener('mousemove', move);
    window.addEventListener('mouseup', up);
  };
  const tickSpacing = view.scale < 12 ? 10 : view.scale < 30 ? 5 : 1;
  const stage = document.querySelector('.stage');
  const r = stage ? stage.getBoundingClientRect() : { width: 1000, height: 700 };
  const worldXStart = -view.tx / view.scale;
  const worldXEnd = worldXStart + r.width / view.scale;
  const worldYStart = -view.ty / view.scale;
  const worldYEnd = worldYStart + r.height / view.scale;
  const xTicks = [];
  for (let i = Math.floor(worldXStart / tickSpacing) * tickSpacing; i <= worldXEnd; i += tickSpacing) xTicks.push(i);
  const yTicks = [];
  for (let i = Math.floor(worldYStart / tickSpacing) * tickSpacing; i <= worldYEnd; i += tickSpacing) yTicks.push(i);
  return (
    <>
      {/* horizontal ruler (top) */}
      <div
        className="fc-ruler fc-ruler-h"
        onMouseDown={(e) => onMouseDown('h', e)}
        style={{ position: 'absolute', top: 0, left: 16, right: 0, height: 16, background: 'var(--paper-2)', borderBottom: '1.5px solid var(--ink)', cursor: 'ns-resize', zIndex: 5, userSelect: 'none', overflow: 'hidden' }}
      >
        {xTicks.map((tx) => {
          const px = tx * view.scale + view.tx - 16;
          return (
            <div key={tx} style={{ position: 'absolute', left: px, top: 0, height: '100%', borderLeft: '1px solid var(--ink-5)', fontFamily: 'var(--font-mono)', fontSize: 9, color: 'var(--ink-4)', paddingLeft: 2, lineHeight: '16px' }}>{tx}'</div>
          );
        })}
      </div>
      {/* vertical ruler (left) */}
      <div
        className="fc-ruler fc-ruler-v"
        onMouseDown={(e) => onMouseDown('v', e)}
        style={{ position: 'absolute', top: 16, left: 0, bottom: 0, width: 16, background: 'var(--paper-2)', borderRight: '1.5px solid var(--ink)', cursor: 'ew-resize', zIndex: 5, userSelect: 'none', overflow: 'hidden' }}
      >
        {yTicks.map((ty) => {
          const py = ty * view.scale + view.ty - 16;
          return (
            <div key={ty} style={{ position: 'absolute', top: py, left: 0, width: '100%', borderTop: '1px solid var(--ink-5)', fontFamily: 'var(--font-mono)', fontSize: 9, color: 'var(--ink-4)', paddingLeft: 2 }}>{ty}'</div>
          );
        })}
      </div>
      {/* corner box */}
      <div style={{ position: 'absolute', top: 0, left: 0, width: 16, height: 16, background: 'var(--ink)', zIndex: 6 }} />
      {hovering && (
        <div style={{ position: 'absolute', ...(hovering.axis === 'h' ? { left: 20, top: hovering.pos * view.scale + view.ty } : { top: 20, left: hovering.pos * view.scale + view.tx }), background: 'var(--red)', color: 'var(--paper)', fontFamily: 'var(--font-mono)', fontSize: 10, padding: '1px 4px', pointerEvents: 'none', zIndex: 10 }}>
          {hovering.pos.toFixed(1)}'
        </div>
      )}
    </>
  );
}

// ─────────────────────────────────────────────────────────────────────
// Bounds helpers for align / distribute
// ─────────────────────────────────────────────────────────────────────
function shapeBounds(s) {
  if (s.type === 'wall' || s.type === 'dimension' || s.type === 'measure') {
    return { x: Math.min(s.a.x, s.b.x), y: Math.min(s.a.y, s.b.y), w: Math.abs(s.b.x - s.a.x), h: Math.abs(s.b.y - s.a.y) };
  }
  if (s.type === 'floor') return { x: s.x, y: s.y, w: s.w, h: s.h };
  if (s.type === 'furniture') return { x: s.x, y: s.y, w: s.w || (window.FURNITURE_MAP[s.kind]?.w || 2), h: s.h || (window.FURNITURE_MAP[s.kind]?.h || 2) };
  if (s.type === 'door' || s.type === 'window') return { x: s.x - 0.2, y: s.y - 0.3, w: (s.width || 3) + 0.4, h: 0.6 };
  if (s.type === 'label') return { x: s.x - 1.5, y: s.y - 0.5, w: 3, h: 1 };
  return { x: 0, y: 0, w: 0, h: 0 };
}

function translateShape(s, dx, dy) {
  if (s.type === 'wall' || s.type === 'dimension' || s.type === 'measure') {
    return { ...s, a: { x: s.a.x + dx, y: s.a.y + dy }, b: { x: s.b.x + dx, y: s.b.y + dy } };
  }
  return { ...s, x: s.x + dx, y: s.y + dy };
}

function alignShapes(shapes, ids, mode) {
  const targets = shapes.filter((s) => ids.includes(s.id));
  if (targets.length < 2) return shapes;
  const bs = targets.map(shapeBounds);
  let ref;
  if (mode === 'left')   ref = Math.min(...bs.map((b) => b.x));
  if (mode === 'right')  ref = Math.max(...bs.map((b) => b.x + b.w));
  if (mode === 'top')    ref = Math.min(...bs.map((b) => b.y));
  if (mode === 'bottom') ref = Math.max(...bs.map((b) => b.y + b.h));
  if (mode === 'cx')     ref = bs.reduce((a, b) => a + b.x + b.w / 2, 0) / bs.length;
  if (mode === 'cy')     ref = bs.reduce((a, b) => a + b.y + b.h / 2, 0) / bs.length;
  return shapes.map((s) => {
    if (!ids.includes(s.id)) return s;
    const b = shapeBounds(s);
    let dx = 0, dy = 0;
    if (mode === 'left')   dx = ref - b.x;
    if (mode === 'right')  dx = ref - (b.x + b.w);
    if (mode === 'top')    dy = ref - b.y;
    if (mode === 'bottom') dy = ref - (b.y + b.h);
    if (mode === 'cx')     dx = ref - (b.x + b.w / 2);
    if (mode === 'cy')     dy = ref - (b.y + b.h / 2);
    return translateShape(s, dx, dy);
  });
}

function distributeShapes(shapes, ids, axis) {
  const targets = shapes.filter((s) => ids.includes(s.id));
  if (targets.length < 3) return shapes;
  const withBounds = targets.map((s) => ({ s, b: shapeBounds(s) }));
  withBounds.sort((p, q) => axis === 'h' ? (p.b.x + p.b.w / 2) - (q.b.x + q.b.w / 2) : (p.b.y + p.b.h / 2) - (q.b.y + q.b.h / 2));
  const first = withBounds[0].b;
  const last = withBounds[withBounds.length - 1].b;
  const start = axis === 'h' ? first.x + first.w / 2 : first.y + first.h / 2;
  const end = axis === 'h' ? last.x + last.w / 2 : last.y + last.h / 2;
  const step = (end - start) / (withBounds.length - 1);
  const newPositions = new Map();
  withBounds.forEach((item, i) => {
    const targetCenter = start + step * i;
    const curCenter = axis === 'h' ? item.b.x + item.b.w / 2 : item.b.y + item.b.h / 2;
    newPositions.set(item.s.id, targetCenter - curCenter);
  });
  return shapes.map((s) => {
    if (!newPositions.has(s.id)) return s;
    const delta = newPositions.get(s.id);
    return translateShape(s, axis === 'h' ? delta : 0, axis === 'v' ? delta : 0);
  });
}

// ─────────────────────────────────────────────────────────────────────
// Align/Distribute toolbar (shows when 2+ selected)
// ─────────────────────────────────────────────────────────────────────
function AlignBar({ count, onAlign, onDistribute, onClear }) {
  const btn = (title, mode, kind, icon) => (
    <button
      title={title}
      onClick={() => kind === 'align' ? onAlign(mode) : onDistribute(mode)}
      style={{ width: 32, height: 32, border: '1.5px solid var(--ink)', borderRight: 'none', background: 'var(--paper)', cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center' }}
    >
      <svg width="18" height="18" viewBox="0 0 18 18" stroke="var(--ink)" strokeWidth="1.5" fill="none">{icon}</svg>
    </button>
  );
  return (
    <div style={{ position: 'absolute', top: 24, left: '50%', transform: 'translateX(-50%)', display: 'flex', background: 'var(--paper)', border: 'none', boxShadow: '0 2px 0 var(--ink)', zIndex: 8 }}>
      <div style={{ padding: '0 10px', height: 32, display: 'flex', alignItems: 'center', borderTop: '1.5px solid var(--ink)', borderLeft: '1.5px solid var(--ink)', borderBottom: '1.5px solid var(--ink)', fontFamily: 'var(--font-narrow)', fontWeight: 700, textTransform: 'uppercase', letterSpacing: '0.12em', fontSize: 11, background: 'var(--ink)', color: 'var(--paper)' }}>
        {count} selected
      </div>
      {btn('Align left', 'left', 'align', <g><line x1="2" y1="2" x2="2" y2="16" /><rect x="4" y="4" width="10" height="3" /><rect x="4" y="11" width="7" height="3" /></g>)}
      {btn('Align center X', 'cx', 'align', <g><line x1="9" y1="2" x2="9" y2="16" /><rect x="4" y="4" width="10" height="3" /><rect x="5.5" y="11" width="7" height="3" /></g>)}
      {btn('Align right', 'right', 'align', <g><line x1="16" y1="2" x2="16" y2="16" /><rect x="4" y="4" width="10" height="3" /><rect x="7" y="11" width="7" height="3" /></g>)}
      {btn('Align top', 'top', 'align', <g><line x1="2" y1="2" x2="16" y2="2" /><rect x="4" y="4" width="3" height="10" /><rect x="11" y="4" width="3" height="7" /></g>)}
      {btn('Align center Y', 'cy', 'align', <g><line x1="2" y1="9" x2="16" y2="9" /><rect x="4" y="4" width="3" height="10" /><rect x="11" y="5.5" width="3" height="7" /></g>)}
      {btn('Align bottom', 'bottom', 'align', <g><line x1="2" y1="16" x2="16" y2="16" /><rect x="4" y="4" width="3" height="10" /><rect x="11" y="7" width="3" height="7" /></g>)}
      <div style={{ width: 4 }} />
      {btn('Distribute horizontal', 'h', 'dist', <g><rect x="1" y="4" width="3" height="10" /><rect x="7.5" y="4" width="3" height="10" /><rect x="14" y="4" width="3" height="10" /></g>)}
      {btn('Distribute vertical', 'v', 'dist', <g><rect x="4" y="1" width="10" height="3" /><rect x="4" y="7.5" width="10" height="3" /><rect x="4" y="14" width="10" height="3" /></g>)}
      <button onClick={onClear} title="Clear selection" style={{ width: 32, height: 32, border: '1.5px solid var(--ink)', background: 'var(--red)', color: 'var(--paper)', cursor: 'pointer', fontFamily: 'var(--font-narrow)', fontWeight: 700, fontSize: 14 }}>×</button>
    </div>
  );
}

// ─────────────────────────────────────────────────────────────────────
// Title block + scale bar + north arrow (rendered inside SVG, world coords)
// ─────────────────────────────────────────────────────────────────────
function TitleBlock({ x, y, w = 28, h = 6, project, sheet, scale = '1/4" = 1\'-0"', date, author = 'FloorCanvas' }) {
  const today = date || new Date().toISOString().slice(0, 10);
  return (
    <g transform={`translate(${x},${y})`}>
      <rect x={0} y={0} width={w} height={h} fill="var(--paper)" stroke="var(--ink)" strokeWidth={1.5} vectorEffect="non-scaling-stroke" />
      <line x1={0} y1={1.8} x2={w} y2={1.8} stroke="var(--ink)" strokeWidth={1} vectorEffect="non-scaling-stroke" />
      <line x1={w * 0.55} y1={1.8} x2={w * 0.55} y2={h} stroke="var(--ink)" strokeWidth={1} vectorEffect="non-scaling-stroke" />
      <line x1={w * 0.78} y1={1.8} x2={w * 0.78} y2={h} stroke="var(--ink)" strokeWidth={1} vectorEffect="non-scaling-stroke" />
      <line x1={w * 0.55} y1={(h + 1.8) / 2} x2={w} y2={(h + 1.8) / 2} stroke="var(--ink)" strokeWidth={0.5} vectorEffect="non-scaling-stroke" />
      <text x={0.5} y={1.2} fontFamily="Archivo Black, sans-serif" fontSize={1.1} fill="var(--ink)" style={{ textTransform: 'uppercase' }}>{project}</text>
      <text x={0.5} y={2.8} fontFamily="Oswald, sans-serif" fontSize={0.5} fill="var(--ink-4)" style={{ textTransform: 'uppercase', letterSpacing: '0.18em' }} fontWeight="700">drawn by</text>
      <text x={0.5} y={3.6} fontFamily="JetBrains Mono, monospace" fontSize={0.55} fill="var(--ink)">{author}</text>
      <text x={0.5} y={4.6} fontFamily="Oswald, sans-serif" fontSize={0.5} fill="var(--ink-4)" style={{ textTransform: 'uppercase', letterSpacing: '0.18em' }} fontWeight="700">scale</text>
      <text x={0.5} y={5.4} fontFamily="JetBrains Mono, monospace" fontSize={0.55} fill="var(--ink)">{scale}</text>
      <text x={w * 0.56} y={2.6} fontFamily="Oswald, sans-serif" fontSize={0.4} fill="var(--ink-4)" style={{ textTransform: 'uppercase', letterSpacing: '0.18em' }} fontWeight="700">date</text>
      <text x={w * 0.56} y={3.5} fontFamily="JetBrains Mono, monospace" fontSize={0.55} fill="var(--ink)">{today}</text>
      <text x={w * 0.56} y={5.0} fontFamily="Oswald, sans-serif" fontSize={0.4} fill="var(--ink-4)" style={{ textTransform: 'uppercase', letterSpacing: '0.18em' }} fontWeight="700">project</text>
      <text x={w * 0.56} y={5.8} fontFamily="JetBrains Mono, monospace" fontSize={0.45} fill="var(--ink-5)">FC · phase 0</text>
      <text x={w * 0.79} y={2.6} fontFamily="Oswald, sans-serif" fontSize={0.4} fill="var(--ink-4)" style={{ textTransform: 'uppercase', letterSpacing: '0.18em' }} fontWeight="700">sheet</text>
      <text x={(w * 0.78 + w) / 2} y={4.8} fontFamily="Archivo Black, sans-serif" fontSize={2.0} fill="var(--ink)" textAnchor="middle">{sheet}</text>
      {/* bauhaus accents */}
      <rect x={w - 1.4} y={0.4} width={0.5} height={1.0} fill="var(--red)" />
      <circle cx={w - 0.7} cy={0.9} r={0.4} fill="var(--blue)" />
    </g>
  );
}

function ScaleBar({ x, y }) {
  const segs = [0, 1, 2, 3, 4, 5];
  return (
    <g transform={`translate(${x},${y})`}>
      <text x={0} y={-0.4} fontFamily="Oswald, sans-serif" fontSize={0.42} fill="var(--ink-4)" style={{ textTransform: 'uppercase', letterSpacing: '0.18em' }} fontWeight="700">scale (ft)</text>
      {segs.slice(0, -1).map((i) => (
        <rect key={i} x={i} y={0} width={1} height={0.4} fill={i % 2 === 0 ? 'var(--ink)' : 'var(--paper)'} stroke="var(--ink)" strokeWidth={1} vectorEffect="non-scaling-stroke" />
      ))}
      {segs.map((i) => (
        <g key={'t' + i}>
          <line x1={i} y1={0.4} x2={i} y2={0.7} stroke="var(--ink)" strokeWidth={1} vectorEffect="non-scaling-stroke" />
          <text x={i} y={1.2} textAnchor="middle" fontFamily="JetBrains Mono, monospace" fontSize={0.35} fill="var(--ink)">{i}</text>
        </g>
      ))}
    </g>
  );
}

function NorthArrow({ x, y, r = 1.6 }) {
  return (
    <g transform={`translate(${x},${y})`}>
      <circle cx={0} cy={0} r={r} fill="var(--paper)" stroke="var(--ink)" strokeWidth={1.2} vectorEffect="non-scaling-stroke" />
      <circle cx={0} cy={0} r={r - 0.25} fill="none" stroke="var(--ink)" strokeWidth={0.4} vectorEffect="non-scaling-stroke" />
      <path d={`M 0 ${-r + 0.3} L ${r * 0.35} ${r - 0.4} L 0 ${r * 0.45} Z`} fill="var(--ink)" />
      <path d={`M 0 ${-r + 0.3} L ${-r * 0.35} ${r - 0.4} L 0 ${r * 0.45} Z`} fill="var(--paper)" stroke="var(--ink)" strokeWidth={1} vectorEffect="non-scaling-stroke" />
      <text x={0} y={-r - 0.4} textAnchor="middle" fontFamily="Archivo Black, sans-serif" fontSize={0.7} fill="var(--ink)">N</text>
    </g>
  );
}

// ─────────────────────────────────────────────────────────────────────
// PNG export — rasterize the .stage svg at chosen DPI
// ─────────────────────────────────────────────────────────────────────
async function exportPNG({ multiplier = 2, filename = 'floorcanvas', bounds = null, titleBlockProps = null }) {
  const stage = document.querySelector('.stage svg');
  if (!stage) return;
  const clone = stage.cloneNode(true);
  // remove crosshair / ghost cursors
  clone.querySelectorAll('.ghost').forEach((g) => g.remove());

  // compute bounds
  let viewBox;
  if (bounds) {
    viewBox = bounds;
  } else {
    const bbox = stage.getBBox();
    const pad = 6;
    viewBox = [bbox.x - pad, bbox.y - pad, bbox.width + pad * 2, bbox.height + pad * 2];
  }
  const [minX, minY, vw, vh] = viewBox;
  clone.setAttribute('viewBox', `${minX} ${minY} ${vw} ${vh}`);
  clone.setAttribute('xmlns', 'http://www.w3.org/2000/svg');

  // inline resolved CSS variables so the export reads cleanly
  const cs = getComputedStyle(document.documentElement);
  const vars = ['--paper', '--paper-2', '--paper-3', '--paper-4', '--ink', '--ink-4', '--ink-5', '--ink-6', '--yellow', '--red', '--blue'];
  const style = vars.map((v) => `${v}: ${cs.getPropertyValue(v).trim()};`).join(' ');
  clone.setAttribute('style', style);

  const aspect = vw / vh;
  const baseDPI = 96;
  const widthPx = Math.round(vw * baseDPI / 4 * multiplier); // 1/4" = 1' base scale → 24px/ft at 1x
  const heightPx = Math.round(widthPx / aspect);

  const svgStr = new XMLSerializer().serializeToString(clone);
  const blob = new Blob([svgStr], { type: 'image/svg+xml' });
  const url = URL.createObjectURL(blob);

  return new Promise((resolve) => {
    const img = new Image();
    img.onload = () => {
      const canvas = document.createElement('canvas');
      canvas.width = widthPx; canvas.height = heightPx;
      const ctx = canvas.getContext('2d');
      ctx.fillStyle = cs.getPropertyValue('--paper').trim() || '#f1ebe1';
      ctx.fillRect(0, 0, widthPx, heightPx);
      ctx.drawImage(img, 0, 0, widthPx, heightPx);
      canvas.toBlob((pngBlob) => {
        const a = document.createElement('a');
        a.href = URL.createObjectURL(pngBlob);
        a.download = `${filename}.png`;
        document.body.appendChild(a); a.click(); document.body.removeChild(a);
        URL.revokeObjectURL(url);
        resolve();
      }, 'image/png');
    };
    img.onerror = () => { URL.revokeObjectURL(url); resolve(); };
    img.src = url;
  });
}

// ─────────────────────────────────────────────────────────────────────
// Export menu
// ─────────────────────────────────────────────────────────────────────
function ExportMenu({ onClose, projectName, activeFloorName, exportSVG, saveJSON, loadJSON, presentation, setPresentation }) {
  const safeName = `${projectName.replace(/\s+/g, '-').toLowerCase()}-${activeFloorName.toLowerCase()}`;
  const opt = (label, sub, onClick) => (
    <button onClick={() => { onClick(); onClose(); }} style={{ width: '100%', textAlign: 'left', padding: '10px 12px', border: 'none', borderBottom: '1px solid var(--ink)', background: 'var(--paper)', cursor: 'pointer', display: 'flex', flexDirection: 'column', gap: 2 }}>
      <span style={{ fontFamily: 'var(--font-narrow)', fontWeight: 700, textTransform: 'uppercase', letterSpacing: '0.12em', fontSize: 11, color: 'var(--ink)' }}>{label}</span>
      <span style={{ fontFamily: 'var(--font-mono)', fontSize: 10, color: 'var(--ink-5)' }}>{sub}</span>
    </button>
  );
  return (
    <div style={{ position: 'absolute', top: 56, right: 12, width: 240, background: 'var(--paper)', border: '2px solid var(--ink)', boxShadow: '4px 4px 0 var(--ink)', zIndex: 30 }}>
      <div style={{ padding: '8px 12px', borderBottom: '1.5px solid var(--ink)', background: 'var(--ink)', color: 'var(--paper)', fontFamily: 'var(--font-display)', fontSize: 13, textTransform: 'uppercase', display: 'flex', justifyContent: 'space-between' }}>
        Export <span onClick={onClose} style={{ cursor: 'pointer' }}>×</span>
      </div>
      {opt('PNG · 1×', 'screen resolution', () => exportPNG({ multiplier: 1, filename: safeName }))}
      {opt('PNG · 2×', 'retina display', () => exportPNG({ multiplier: 2, filename: safeName }))}
      {opt('PNG · Print', '300 DPI sheet', () => exportPNG({ multiplier: 4, filename: safeName + '-print' }))}
      {opt('SVG', 'vector source', exportSVG)}
      {saveJSON && opt('Save JSON', 'project file (all floors)', saveJSON)}
      {loadJSON && opt('Load JSON', 'replace current project', loadJSON)}
      <div style={{ padding: '10px 12px', display: 'flex', justifyContent: 'space-between', alignItems: 'center', background: 'var(--paper-2)' }}>
        <span style={{ fontFamily: 'var(--font-narrow)', fontWeight: 700, textTransform: 'uppercase', letterSpacing: '0.12em', fontSize: 10, color: 'var(--ink)' }}>Presentation view</span>
        <button onClick={() => setPresentation(!presentation)} style={{ width: 38, height: 22, border: '1.5px solid var(--ink)', background: presentation ? 'var(--ink)' : 'var(--paper)', cursor: 'pointer', position: 'relative' }}>
          <span style={{ position: 'absolute', top: 1, left: presentation ? 18 : 1, width: 16, height: 16, background: presentation ? 'var(--yellow)' : 'var(--ink)' }} />
        </button>
      </div>
    </div>
  );
}

// ─────────────────────────────────────────────────────────────────────
// Sheet overlay — adds title block, scale bar, north arrow into svg
// ─────────────────────────────────────────────────────────────────────
function SheetFrame({ projectName, activeFloorName, sheet, viewBox }) {
  if (!viewBox) return null;
  const [minX, minY, w, h] = viewBox;
  return (
    <g pointerEvents="none">
      <NorthArrow x={minX + w - 3} y={minY + 3} />
      <ScaleBar x={minX + 2} y={minY + h - 2.5} />
      <TitleBlock x={minX + w - 30} y={minY + h - 7} w={28} h={6}
        project={projectName} sheet={sheet || 'A-101'} />
      <text x={minX + 2} y={minY + 1.6} fontFamily="Archivo Black, sans-serif" fontSize={1.1} fill="var(--ink)" style={{ textTransform: 'uppercase' }}>{activeFloorName} · plan</text>
    </g>
  );
}

Object.assign(window, {
  MeasureShape, GuideShape, Rulers, AlignBar, ExportMenu, SheetFrame,
  TitleBlock, ScaleBar, NorthArrow,
  exportPNG, alignShapes, distributeShapes, shapeBounds, fcExUid,
});
