/* global React, ReactDOM */
/* ===========================================================
   VOLTLAB · CAD — Desenho técnico 2D (engine refatorado v2)
   ───────────────────────────────────────────────────────────
   • Pan/Zoom desacoplados do React (requestAnimationFrame +
     transform imperativo via ref; estado só atualiza no fim)
   • Snap inteligente com CACHE (WeakMap) + pré-filtro por bbox
   • Atualizações de mousemove coalescidas em 1 frame (rAF)
   • Histórico em ref (zero re-render) com limite (cap)
   • Autosave em localStorage (debounced, à prova de falha)
   • Exportação SVG limpa: viewBox do desenho inteiro, sem
     estilos de seleção e independente do pan/zoom atual
   • Listener de wheel nativo non-passive (preventDefault
     confiável em todos os browsers)
   • Restrição ortogonal (Shift → 0/45/90°) · Espaço = mão
   • Entidades memoizadas (React.memo) + non-scaling-stroke
=========================================================== */
const { useState, useRef, useEffect, useMemo, useCallback } = React;

/* ---------- ICONS ---------- */
const CIcon = ({ name, size = 16 }) => {
  const p = { width: size, height: size, viewBox: "0 0 24 24", fill: "none",
              stroke: "currentColor", strokeWidth: 1.5, strokeLinecap: "round", strokeLinejoin: "round" };
  const M = {
    cursor:  <path d="M5 3l14 7-6 2-2 6z"/>,
    pan:     <><path d="M9 11V5a2 2 0 014 0v6"/><path d="M13 8h2a2 2 0 012 2v6a5 5 0 01-5 5h-1a5 5 0 01-4-2l-3-5a2 2 0 013-2l2 2"/></>,
    line:    <><circle cx="5" cy="19" r="1.5"/><circle cx="19" cy="5" r="1.5"/><line x1="6" y1="18" x2="18" y2="6"/></>,
    rect:    <rect x="4" y="6" width="16" height="12"/>,
    circle:  <circle cx="12" cy="12" r="8"/>,
    poly:    <><circle cx="4" cy="4" r="1.2"/><circle cx="20" cy="6" r="1.2"/><circle cx="18" cy="18" r="1.2"/><circle cx="6" cy="20" r="1.2"/><polyline points="4 4 20 6 18 18 6 20"/></>,
    text:    <><path d="M5 6V4h14v2"/><line x1="12" y1="4" x2="12" y2="20"/><line x1="9" y1="20" x2="15" y2="20"/></>,
    measure: <><path d="M3 17L17 3l4 4L7 21z"/><path d="M7 13l2 2M10 10l2 2M13 7l2 2"/></>,
    grid:    <><rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/><rect x="14" y="14" width="7" height="7"/></>,
    snap:    <><circle cx="12" cy="12" r="3"/><line x1="12" y1="3" x2="12" y2="6"/><line x1="12" y1="18" x2="12" y2="21"/><line x1="3" y1="12" x2="6" y2="12"/><line x1="18" y1="12" x2="21" y2="12"/></>,
    ortho:   <><path d="M5 19V5h14"/><path d="M5 19l7-7"/></>,
    zoomIn:  <><circle cx="11" cy="11" r="7"/><line x1="21" y1="21" x2="16.5" y2="16.5"/><line x1="8" y1="11" x2="14" y2="11"/><line x1="11" y1="8" x2="11" y2="14"/></>,
    zoomOut: <><circle cx="11" cy="11" r="7"/><line x1="21" y1="21" x2="16.5" y2="16.5"/><line x1="8" y1="11" x2="14" y2="11"/></>,
    fit:     <><polyline points="4 9 4 4 9 4"/><polyline points="20 9 20 4 15 4"/><polyline points="20 15 20 20 15 20"/><polyline points="4 15 4 20 9 20"/></>,
    undo:    <><polyline points="9 14 4 9 9 4"/><path d="M20 20v-7a4 4 0 00-4-4H4"/></>,
    redo:    <><polyline points="15 14 20 9 15 4"/><path d="M4 20v-7a4 4 0 014-4h12"/></>,
    trash:   <><polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14a2 2 0 01-2 2H8a2 2 0 01-2-2L5 6"/></>,
    download:<><path d="M12 3v12"/><polyline points="7 10 12 15 17 10"/><line x1="4" y1="21" x2="20" y2="21"/></>,
    plus:    <><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></>,
    eye:     <><path d="M2 12s3.5-7 10-7 10 7 10 7-3.5 7-10 7S2 12 2 12z"/><circle cx="12" cy="12" r="3"/></>,
    eyeOff:  <><path d="M17.94 17.94A10 10 0 0112 19c-6 0-10-7-10-7a18 18 0 014.06-4.94"/><path d="M9.9 5A9 9 0 0112 5c6 0 10 7 10 7a18 18 0 01-2.16 3.19"/><line x1="1" y1="1" x2="23" y2="23"/></>,
    lock:    <><rect x="5" y="11" width="14" height="10" rx="2"/><path d="M8 11V8a4 4 0 018 0v3"/></>,
    unlock:  <><rect x="5" y="11" width="14" height="10" rx="2"/><path d="M8 11V8a4 4 0 0110.7-2"/></>,
  };
  return <svg {...p}>{M[name]}</svg>;
};

/* ---------- ELECTRICAL SYMBOL LIBRARY ---------- */
const SYMBOLS = [
  { id: "motor",   group: "Máquinas", name: "Motor 3~", w: 60, h: 60,
    draw: <><circle cx="30" cy="30" r="22"/><text x="30" y="34" textAnchor="middle" fontSize="16" fontFamily="Space Grotesk" fontWeight="600" stroke="none" fill="currentColor">M</text><text x="30" y="46" textAnchor="middle" fontSize="7" fontFamily="JetBrains Mono" stroke="none" fill="currentColor">3~</text></> },
  { id: "trafo",   group: "Máquinas", name: "Transformador", w: 80, h: 60,
    draw: <><circle cx="28" cy="30" r="16"/><circle cx="52" cy="30" r="16"/></> },
  { id: "gen",     group: "Máquinas", name: "Gerador", w: 60, h: 60,
    draw: <><circle cx="30" cy="30" r="22"/><text x="30" y="36" textAnchor="middle" fontSize="14" fontFamily="Space Grotesk" fontWeight="600" stroke="none" fill="currentColor">G</text></> },
  { id: "breaker", group: "Proteção", name: "Disjuntor", w: 40, h: 60,
    draw: <><line x1="20" y1="2" x2="20" y2="18"/><rect x="10" y="18" width="20" height="24"/><line x1="14" y1="24" x2="26" y2="36"/><line x1="20" y1="42" x2="20" y2="58"/></> },
  { id: "fuse",    group: "Proteção", name: "Fusível", w: 30, h: 50,
    draw: <><line x1="15" y1="2" x2="15" y2="12"/><rect x="7" y="12" width="16" height="26"/><line x1="15" y1="38" x2="15" y2="48"/></> },
  { id: "ground",  group: "Proteção", name: "Terra", w: 40, h: 40,
    draw: <><line x1="20" y1="2" x2="20" y2="22"/><line x1="8" y1="22" x2="32" y2="22"/><line x1="12" y1="28" x2="28" y2="28"/><line x1="16" y1="34" x2="24" y2="34"/></> },
  { id: "contact", group: "Manobra", name: "Contator", w: 40, h: 50,
    draw: <><line x1="20" y1="2" x2="20" y2="14"/><line x1="14" y1="14" x2="28" y2="22"/><line x1="20" y1="22" x2="20" y2="36"/><rect x="14" y="36" width="12" height="10"/><line x1="20" y1="46" x2="20" y2="48"/></> },
  { id: "relay",   group: "Manobra", name: "Relé Térmico", w: 40, h: 50,
    draw: <><line x1="20" y1="2" x2="20" y2="14"/><rect x="10" y="14" width="20" height="22"/><path d="M14 18 L26 30"/><line x1="20" y1="36" x2="20" y2="48"/></> },
  { id: "switch",  group: "Manobra", name: "Chave", w: 30, h: 50,
    draw: <><line x1="15" y1="2" x2="15" y2="14"/><line x1="15" y1="14" x2="26" y2="32"/><circle cx="15" cy="14" r="1.5"/><circle cx="15" cy="34" r="1.5"/><line x1="15" y1="34" x2="15" y2="48"/></> },
  { id: "lamp",    group: "Cargas", name: "Lâmpada", w: 50, h: 50,
    draw: <><circle cx="25" cy="25" r="14"/><line x1="15" y1="15" x2="35" y2="35"/><line x1="35" y1="15" x2="15" y2="35"/></> },
  { id: "outlet",  group: "Cargas", name: "Tomada 2P+T", w: 40, h: 40,
    draw: <><circle cx="20" cy="20" r="14"/><circle cx="14" cy="20" r="1.5"/><circle cx="26" cy="20" r="1.5"/><line x1="20" y1="12" x2="20" y2="14"/></> },
  { id: "cap",     group: "Componentes", name: "Capacitor", w: 30, h: 50,
    draw: <><line x1="15" y1="2" x2="15" y2="20"/><line x1="6" y1="20" x2="24" y2="20"/><line x1="6" y1="26" x2="24" y2="26"/><line x1="15" y1="26" x2="15" y2="48"/></> },
  { id: "resist",  group: "Componentes", name: "Resistor", w: 60, h: 30,
    draw: <><line x1="2" y1="15" x2="12" y2="15"/><rect x="12" y="9" width="36" height="12"/><line x1="48" y1="15" x2="58" y2="15"/></> },
  { id: "ind",     group: "Componentes", name: "Indutor", w: 60, h: 24,
    draw: <><path d="M2 18 a4 4 0 0 1 8 0 a4 4 0 0 1 8 0 a4 4 0 0 1 8 0 a4 4 0 0 1 8 0 a4 4 0 0 1 8 0 a4 4 0 0 1 8 0"/></> },
  { id: "battery", group: "Cargas", name: "Bateria", w: 40, h: 40,
    draw: <><line x1="20" y1="4" x2="20" y2="10"/><line x1="6" y1="10" x2="34" y2="10"/><line x1="10" y1="14" x2="30" y2="14"/><line x1="6" y1="20" x2="34" y2="20"/><line x1="10" y1="24" x2="30" y2="24"/><line x1="20" y1="24" x2="20" y2="36"/></> },
  { id: "ammeter", group: "Medição", name: "Amperímetro", w: 50, h: 50,
    draw: <><circle cx="25" cy="25" r="18"/><text x="25" y="30" textAnchor="middle" fontSize="14" fontFamily="Space Grotesk" fontWeight="600" stroke="none" fill="currentColor">A</text></> },
  { id: "voltmeter", group: "Medição", name: "Voltímetro", w: 50, h: 50,
    draw: <><circle cx="25" cy="25" r="18"/><text x="25" y="30" textAnchor="middle" fontSize="14" fontFamily="Space Grotesk" fontWeight="600" stroke="none" fill="currentColor">V</text></> },
];
const SYM_BY_ID = Object.fromEntries(SYMBOLS.map(s => [s.id, s]));
const SYM_GROUPS = [...new Set(SYMBOLS.map(s => s.group))];

/* ---------- TOOLS ---------- */
const TOOLS = [
  { id: "select",  label: "Selecionar (V)",  icon: "cursor",  shortcut: "v" },
  { id: "pan",     label: "Mão (H)",          icon: "pan",     shortcut: "h" },
  { id: "line",    label: "Linha (L)",        icon: "line",    shortcut: "l" },
  { id: "rect",    label: "Retângulo (R)",    icon: "rect",    shortcut: "r" },
  { id: "circle",  label: "Círculo (C)",      icon: "circle",  shortcut: "c" },
  { id: "poly",    label: "Polilinha (P)",    icon: "poly",    shortcut: "p" },
  { id: "text",    label: "Texto (T)",        icon: "text",    shortcut: "t" },
  { id: "measure", label: "Cota (M)",         icon: "measure", shortcut: "m" },
];

const SNAP_LABEL = { end: "fim", mid: "médio", center: "centro", vertex: "vértice", quad: "quadrante", grid: "grade" };

/* ---------- CONSTANTS ---------- */
const MAX_HISTORY = 100;                 // cap do histórico (memória O(1) amortizada)
const PERSIST_KEY = "vl_cad_drawing_v1"; // autosave
const TEXT_CHAR_W = 0.5;                 // largura ≈ size·0.5 por caractere (mono)

/* ---------- HELPERS ---------- */
let __uidSeq = 0;
const uid = () => Date.now().toString(36) + "-" + (__uidSeq++).toString(36) + Math.random().toString(36).slice(2, 5);
const snapGrid = (v, step) => Math.round(v / step) * step;
const fmt = (v) => Math.abs(v) < 0.01 ? "0" : v.toFixed(2).replace(/\.?0+$/, "");
const clampZoom = (z) => Math.max(0.1, Math.min(16, z));

function distPtToSeg(p, a, b) {
  const dx = b.x - a.x, dy = b.y - a.y;
  if (dx === 0 && dy === 0) return Math.hypot(p.x - a.x, p.y - a.y);
  const t = Math.max(0, Math.min(1, ((p.x - a.x) * dx + (p.y - a.y) * dy) / (dx * dx + dy * dy)));
  return Math.hypot(p.x - (a.x + t * dx), p.y - (a.y + t * dy));
}
function textBox(e) {
  const size = e.size || 14;
  return { x1: e.x, y1: e.y - size, x2: e.x + e.text.length * size * TEXT_CHAR_W, y2: e.y + size * 0.3 };
}
function computeBBox(e) {
  if (e.kind === "line" || e.kind === "measure")
    return { x1: Math.min(e.x1, e.x2), y1: Math.min(e.y1, e.y2), x2: Math.max(e.x1, e.x2), y2: Math.max(e.y1, e.y2) };
  if (e.kind === "rect")   return { x1: e.x, y1: e.y, x2: e.x + e.w, y2: e.y + e.h };
  if (e.kind === "circle") return { x1: e.cx - e.r, y1: e.cy - e.r, x2: e.cx + e.r, y2: e.cy + e.r };
  if (e.kind === "poly") {
    // loop simples (sem spread): não estoura a pilha em polilinhas longas
    let x1 = Infinity, y1 = Infinity, x2 = -Infinity, y2 = -Infinity;
    for (const p of e.points) {
      if (p.x < x1) x1 = p.x; if (p.x > x2) x2 = p.x;
      if (p.y < y1) y1 = p.y; if (p.y > y2) y2 = p.y;
    }
    return { x1, y1, x2, y2 };
  }
  if (e.kind === "text")   return textBox(e);
  if (e.kind === "symbol") return { x1: e.x - e.w / 2, y1: e.y - e.h / 2, x2: e.x + e.w / 2, y2: e.y + e.h / 2 };
  return { x1: 0, y1: 0, x2: 0, y2: 0 };
}
/* cache de bbox e snap points — entidades são imutáveis, então
   o WeakMap nunca serve dado obsoleto e é coletado pelo GC sozinho */
const __bboxCache = new WeakMap();
const bbox = (e) => {
  let b = __bboxCache.get(e);
  if (!b) { b = computeBBox(e); __bboxCache.set(e, b); }
  return b;
};
function translateEnt(e, dx, dy) {
  if (e.kind === "line" || e.kind === "measure")
    return { ...e, x1: e.x1 + dx, y1: e.y1 + dy, x2: e.x2 + dx, y2: e.y2 + dy };
  if (e.kind === "rect")   return { ...e, x: e.x + dx, y: e.y + dy };
  if (e.kind === "circle") return { ...e, cx: e.cx + dx, cy: e.cy + dy };
  if (e.kind === "poly")   return { ...e, points: e.points.map(p => ({ x: p.x + dx, y: p.y + dy })) };
  if (e.kind === "text" || e.kind === "symbol") return { ...e, x: e.x + dx, y: e.y + dy };
  return e;
}
/* notable snap points for a single entity (cacheado) */
function computeNotablePoints(e) {
  const pts = [];
  const add = (x, y, type) => pts.push({ x, y, type });
  if (e.kind === "line" || e.kind === "measure") {
    add(e.x1, e.y1, "end"); add(e.x2, e.y2, "end");
    add((e.x1 + e.x2) / 2, (e.y1 + e.y2) / 2, "mid");
  } else if (e.kind === "rect") {
    const { x, y, w, h } = e;
    add(x, y, "vertex"); add(x + w, y, "vertex"); add(x, y + h, "vertex"); add(x + w, y + h, "vertex");
    add(x + w / 2, y, "mid"); add(x + w / 2, y + h, "mid"); add(x, y + h / 2, "mid"); add(x + w, y + h / 2, "mid");
    add(x + w / 2, y + h / 2, "center");
  } else if (e.kind === "circle") {
    add(e.cx, e.cy, "center");
    add(e.cx + e.r, e.cy, "quad"); add(e.cx - e.r, e.cy, "quad");
    add(e.cx, e.cy + e.r, "quad"); add(e.cx, e.cy - e.r, "quad");
  } else if (e.kind === "poly") {
    e.points.forEach(p => add(p.x, p.y, "vertex"));
    for (let i = 0; i < e.points.length - 1; i++)
      add((e.points[i].x + e.points[i + 1].x) / 2, (e.points[i].y + e.points[i + 1].y) / 2, "mid");
  } else if (e.kind === "symbol") {
    add(e.x, e.y, "center");
    add(e.x - e.w / 2, e.y - e.h / 2, "vertex"); add(e.x + e.w / 2, e.y - e.h / 2, "vertex");
    add(e.x - e.w / 2, e.y + e.h / 2, "vertex"); add(e.x + e.w / 2, e.y + e.h / 2, "vertex");
  }
  return pts;
}
const __snapCache = new WeakMap();
const notablePoints = (e) => {
  let p = __snapCache.get(e);
  if (!p) { p = computeNotablePoints(e); __snapCache.set(e, p); }
  return p;
};
/* resize handles (world coords) */
function getHandles(e) {
  if (e.kind === "rect") {
    const { x, y, w, h } = e;
    return [
      { id: "nw", x, y }, { id: "n", x: x + w / 2, y }, { id: "ne", x: x + w, y },
      { id: "e", x: x + w, y: y + h / 2 }, { id: "se", x: x + w, y: y + h },
      { id: "s", x: x + w / 2, y: y + h }, { id: "sw", x, y: y + h }, { id: "w", x, y: y + h / 2 },
    ];
  }
  if (e.kind === "circle") {
    return [
      { id: "e", x: e.cx + e.r, y: e.cy }, { id: "w", x: e.cx - e.r, y: e.cy },
      { id: "s", x: e.cx, y: e.cy + e.r }, { id: "n", x: e.cx, y: e.cy - e.r },
    ];
  }
  if (e.kind === "line" || e.kind === "measure") {
    return [{ id: "p1", x: e.x1, y: e.y1 }, { id: "p2", x: e.x2, y: e.y2 }];
  }
  return [];
}
/* apply ortho/45° constraint from anchor a to point p */
function applyOrtho(a, p) {
  const dx = p.x - a.x, dy = p.y - a.y;
  const len = Math.hypot(dx, dy);
  if (len < 1e-6) return p;
  const step = Math.PI / 4;
  const ang = Math.round(Math.atan2(dy, dx) / step) * step;
  return { x: a.x + Math.cos(ang) * len, y: a.y + Math.sin(ang) * len };
}

/* ---------- persistence (à prova de falha: nunca quebra o app) ---------- */
function loadPersisted() {
  try {
    const raw = localStorage.getItem(PERSIST_KEY);
    if (!raw) return null;
    const d = JSON.parse(raw);
    return d && typeof d === "object" ? d : null;
  } catch { return null; }
}

/* ===========================================================
   ENTITY RENDERING (memoized)
=========================================================== */
function entityEl(e, strokeC, strokeW, dash) {
  const common = { stroke: strokeC, strokeWidth: strokeW, fill: "none",
                   vectorEffect: "non-scaling-stroke", strokeDasharray: dash || undefined };
  switch (e.kind) {
    case "line":   return <line {...common} x1={e.x1} y1={e.y1} x2={e.x2} y2={e.y2}/>;
    case "rect":   return <rect {...common} x={e.x} y={e.y} width={e.w} height={e.h}/>;
    case "circle": return <circle {...common} cx={e.cx} cy={e.cy} r={e.r}/>;
    case "poly":   return <polyline {...common} points={e.points.map(p => `${p.x},${p.y}`).join(" ")}/>;
    case "text":   return <text x={e.x} y={e.y} fill={strokeC} fontSize={e.size || 14} fontFamily="JetBrains Mono">{e.text}</text>;
    case "measure": {
      const dx = e.x2 - e.x1, dy = e.y2 - e.y1;
      const len = Math.hypot(dx, dy), ang = Math.atan2(dy, dx), off = 14;
      const ox = -Math.sin(ang) * off, oy = Math.cos(ang) * off;
      return (<g>
        <line x1={e.x1} y1={e.y1} x2={e.x1 + ox} y2={e.y1 + oy} stroke={strokeC} strokeWidth={strokeW} vectorEffect="non-scaling-stroke"/>
        <line x1={e.x2} y1={e.y2} x2={e.x2 + ox} y2={e.y2 + oy} stroke={strokeC} strokeWidth={strokeW} vectorEffect="non-scaling-stroke"/>
        <line x1={e.x1 + ox} y1={e.y1 + oy} x2={e.x2 + ox} y2={e.y2 + oy} stroke={strokeC} strokeWidth={strokeW} vectorEffect="non-scaling-stroke"/>
        <text x={(e.x1 + e.x2) / 2 + ox * 1.4} y={(e.y1 + e.y2) / 2 + oy * 1.4 + 4} textAnchor="middle"
              fill={strokeC} fontSize={11} fontFamily="JetBrains Mono" stroke="none" style={{ fontSize: "11px" }}>
          {fmt(len / 10)} m
        </text>
      </g>);
    }
    case "symbol": {
      const sym = SYM_BY_ID[e.symId];
      if (!sym) return null;
      // style.color → letras "M/G/A/V" (currentColor) acompanham a cor da entidade
      return (
        <g transform={`translate(${e.x - sym.w / 2}, ${e.y - sym.h / 2})`}
           stroke={strokeC} strokeWidth={strokeW} fill="none" vectorEffect="non-scaling-stroke"
           style={{ color: strokeC }}>
          {sym.draw}
        </g>
      );
    }
    default: return null;
  }
}

const EntShape = React.memo(function EntShape({ e, sel, hover, color }) {
  const strokeC = sel ? "var(--accent)" : hover ? "var(--accent-2)" : color;
  const strokeW = e.stroke || 1.5;
  return <g>{entityEl(e, strokeC, strokeW, sel ? "5 4" : "")}</g>;
});

const EntitiesLayer = React.memo(function EntitiesLayer({ ents, selKey, hoverId, layers }) {
  const selSet = useMemo(() => new Set(selKey ? selKey.split("|") : []), [selKey]);
  const lmap = useMemo(() => Object.fromEntries(layers.map(l => [l.id, l])), [layers]);
  return (
    <>
      {ents.map(e => {
        const L = lmap[e.layer];
        if (L && L.visible === false) return null;
        return <EntShape key={e.id} e={e} sel={selSet.has(e.id)} hover={hoverId === e.id}
                         color={e.color || (L && L.color) || "#3D8BFF"}/>;
      })}
    </>
  );
});

/* ===========================================================
   ROOT
=========================================================== */
const CADModule = () => {
  const containerRef = useRef(null);
  const svgRef = useRef(null);
  const worldRef = useRef(null);        // transformed <g>
  const minorRef = useRef(null);
  const majorRef = useRef(null);
  const minorPathRef = useRef(null);
  const majorPathRef = useRef(null);
  const axisXRef = useRef(null);
  const axisYRef = useRef(null);
  const snapMarkerRef = useRef(null);
  const crosshairRef = useRef(null);
  const crossHRef = useRef(null);
  const crossVRef = useRef(null);
  const crossBoxRef = useRef(null);
  const coordXRef = useRef(null);
  const coordYRef = useRef(null);
  const zoomLabelRef = useRef(null);
  const exportRef = useRef(null);

  /* estado inicial restaurado do autosave (uma única leitura) */
  const initRef = useRef(null);
  if (initRef.current === null) initRef.current = loadPersisted() || {};

  const [size, setSize] = useState({ w: 1000, h: 700 });
  const [zoom, setZoom] = useState(1);
  const [offset, setOffset] = useState({ x: 200, y: 200 });
  // live view (ahead of React state during gestures)
  const viewRef = useRef({ zoom: 1, offset: { x: 200, y: 200 } });

  const [tool, setTool] = useState("select");
  const [color, setColor] = useState("#3D8BFF");
  const [stroke, setStroke] = useState(1.5);
  const [snapOn, setSnapOn] = useState(true);
  const [gridOn, setGridOn] = useState(true);
  const [gridStep, setGridStep] = useState(20);

  const [ents, setEnts] = useState(() => Array.isArray(initRef.current.ents) ? initRef.current.ents : []);
  const [draft, setDraft] = useState(null);
  const [selected, setSelected] = useState(new Set());
  const [hover, setHover] = useState(null);
  const [exporting, setExporting] = useState(null); // {vb,w,h} durante a exportação

  const [layers, setLayers] = useState(() =>
    Array.isArray(initRef.current.layers) && initRef.current.layers.length
      ? initRef.current.layers
      : [
          { id: "L1", name: "Geral",    color: "#3D8BFF", visible: true, locked: false },
          { id: "L2", name: "Símbolos", color: "#5BE38C", visible: true, locked: false },
          { id: "L3", name: "Cotas",    color: "#F2B043", visible: true, locked: false },
        ]);
  const [activeLayer, setActiveLayer] = useState("L1");
  const [symGroup, setSymGroup] = useState(SYM_GROUPS[0]);

  // mirrors for use inside imperative handlers (avoid stale closures during gestures)
  const entsRef = useRef(ents);          useEffect(() => { entsRef.current = ents; }, [ents]);
  const layersRef = useRef(layers);
  const layerMapRef = useRef({});
  useEffect(() => {
    layersRef.current = layers;
    layerMapRef.current = Object.fromEntries(layers.map(l => [l.id, l])); // O(1) lookup nos hot paths
  }, [layers]);
  const toolRef = useRef(tool);          useEffect(() => { toolRef.current = tool; }, [tool]);
  const snapOnRef = useRef(snapOn);      useEffect(() => { snapOnRef.current = snapOn; }, [snapOn]);
  const gridStepRef = useRef(gridStep);  useEffect(() => { gridStepRef.current = gridStep; }, [gridStep]);
  const selectedRef = useRef(selected);  useEffect(() => { selectedRef.current = selected; }, [selected]);
  const draftRef = useRef(draft);        useEffect(() => { draftRef.current = draft; }, [draft]);
  const shiftRef = useRef(false);
  const spaceRef = useRef(false);

  useEffect(() => { viewRef.current = { zoom, offset }; }, [zoom, offset]);

  /* ---------- coalescência de setState em mousemove (1 update/frame) ---------- */
  const pendingRef = useRef({});
  const flushRafRef = useRef(0);
  const scheduleState = useCallback((patch) => {
    // refs atualizam imediatamente → lógica subsequente nunca lê estado velho
    if ("ents" in patch)  entsRef.current  = patch.ents;
    if ("draft" in patch) draftRef.current = patch.draft;
    Object.assign(pendingRef.current, patch);
    if (flushRafRef.current) return;
    flushRafRef.current = requestAnimationFrame(() => {
      flushRafRef.current = 0;
      const p = pendingRef.current; pendingRef.current = {};
      if ("ents" in p)  setEnts(p.ents);
      if ("draft" in p) setDraft(p.draft);
    });
  }, []);
  /* updates discretos: cancelam pendências do rAF para não "ressuscitar" estado antigo */
  const setEntsNow = useCallback((arr) => { delete pendingRef.current.ents; entsRef.current = arr; setEnts(arr); }, []);
  const setDraftNow = useCallback((d) => { delete pendingRef.current.draft; draftRef.current = d; setDraft(d); }, []);

  /* ---------- resize observer ---------- */
  useEffect(() => {
    if (!containerRef.current) return;
    const ro = new ResizeObserver(([e]) => setSize({ w: e.contentRect.width, h: e.contentRect.height }));
    ro.observe(containerRef.current);
    return () => ro.disconnect();
  }, []);

  /* ---------- imperative view application (rAF) ---------- */
  const rafRef = useRef(0);
  const applyView = useCallback((v) => {
    const step = gridStepRef.current;
    if (worldRef.current)
      worldRef.current.setAttribute("transform", `translate(${v.offset.x} ${v.offset.y}) scale(${v.zoom})`);
    const setPat = (ref, pathRef, mult) => {
      if (!ref.current) return;
      const tile = step * mult * v.zoom;
      ref.current.setAttribute("x", v.offset.x);
      ref.current.setAttribute("y", v.offset.y);
      ref.current.setAttribute("width", tile);
      ref.current.setAttribute("height", tile);
      // keep the tile's grid path in sync with the tile size, otherwise the
      // lines stay at the previous scale and the grid "collapses" mid-gesture
      if (pathRef.current) pathRef.current.setAttribute("d", `M ${tile} 0 L 0 0 0 ${tile}`);
    };
    setPat(minorRef, minorPathRef, 1);
    setPat(majorRef, majorPathRef, 5);
    if (axisXRef.current) { axisXRef.current.setAttribute("y1", v.offset.y); axisXRef.current.setAttribute("y2", v.offset.y); }
    if (axisYRef.current) { axisYRef.current.setAttribute("x1", v.offset.x); axisYRef.current.setAttribute("x2", v.offset.x); }
    if (zoomLabelRef.current) zoomLabelRef.current.textContent = (v.zoom * 100).toFixed(0) + "%";
  }, []);
  const scheduleApply = useCallback(() => {
    if (rafRef.current) return;
    rafRef.current = requestAnimationFrame(() => { rafRef.current = 0; applyView(viewRef.current); });
  }, [applyView]);

  /* commit live view → React state (debounced for wheel) */
  const commitTimer = useRef(0);
  const commitViewSoon = useCallback(() => {
    clearTimeout(commitTimer.current);
    commitTimer.current = setTimeout(() => {
      setZoom(viewRef.current.zoom);
      setOffset({ ...viewRef.current.offset });
    }, 140);
  }, []);
  const commitViewNow = useCallback(() => {
    clearTimeout(commitTimer.current);
    setZoom(viewRef.current.zoom);
    setOffset({ ...viewRef.current.offset });
  }, []);

  /* ---------- limpeza total no unmount (sem vazamentos) ---------- */
  useEffect(() => () => {
    cancelAnimationFrame(rafRef.current);
    cancelAnimationFrame(flushRafRef.current);
    clearTimeout(commitTimer.current);
  }, []);

  /* ---------- autosave (debounced, nunca lança) ---------- */
  const saveTimer = useRef(0);
  useEffect(() => {
    clearTimeout(saveTimer.current);
    saveTimer.current = setTimeout(() => {
      try { localStorage.setItem(PERSIST_KEY, JSON.stringify({ ents, layers })); } catch { /* quota/priv. mode */ }
    }, 600);
    return () => clearTimeout(saveTimer.current);
  }, [ents, layers]);

  /* ---------- coordinate / screen helpers (live view) ---------- */
  const toWorld = (sx, sy) => {
    const v = viewRef.current;
    return { x: (sx - v.offset.x) / v.zoom, y: (sy - v.offset.y) / v.zoom };
  };
  const toScreen = (x, y) => {
    const v = viewRef.current;
    return { x: x * v.zoom + v.offset.x, y: y * v.zoom + v.offset.y };
  };
  const localXY = (e) => {
    const r = svgRef.current.getBoundingClientRect();
    return { sx: e.clientX - r.left, sy: e.clientY - r.top };
  };

  /* ---------- CROSSHAIR (follows cursor, native pointer hidden) ---------- */
  const positionCross = (sx, sy) => {
    if (crossHRef.current) { crossHRef.current.setAttribute("y1", sy); crossHRef.current.setAttribute("y2", sy); }
    if (crossVRef.current) { crossVRef.current.setAttribute("x1", sx); crossVRef.current.setAttribute("x2", sx); }
    if (crossBoxRef.current) { crossBoxRef.current.setAttribute("x", sx - 6.5); crossBoxRef.current.setAttribute("y", sy - 6.5); }
  };
  const showCross = (e) => {
    const { sx, sy } = localXY(e);
    positionCross(sx, sy);
    if (crosshairRef.current) crosshairRef.current.style.display = "";
  };
  const hideCross = () => { if (crosshairRef.current) crosshairRef.current.style.display = "none"; };

  /* ---------- SNAP ENGINE ----------
     O(N) com pré-filtro por bbox em cache; pontos notáveis em
     cache por entidade (WeakMap). O toggle SNAP agora desliga
     TAMBÉM o snap a pontos notáveis (consistente com o título
     do botão: "Snap a pontos notáveis e grade"). */
  const findSnap = (sx, sy, excludeIds) => {
    const v = viewRef.current;
    const world = toWorld(sx, sy);
    if (!snapOnRef.current) return { x: world.x, y: world.y, type: "free" };
    const tolScreen = 11;
    const tol = tolScreen / v.zoom;
    let best = null, bestD = tol;
    const arr = entsRef.current;
    const lmap = layerMapRef.current;
    const exclude = excludeIds && excludeIds.length ? new Set(excludeIds) : null;
    for (let i = arr.length - 1; i >= 0; i--) {
      const e = arr[i];
      if (exclude && exclude.has(e.id)) continue;
      const L = lmap[e.layer];
      if (L && (L.visible === false || L.locked)) continue;
      // pré-filtro: descarta entidades cujo bbox expandido não contém o cursor
      const b = bbox(e);
      if (world.x < b.x1 - tol || world.x > b.x2 + tol || world.y < b.y1 - tol || world.y > b.y2 + tol) continue;
      const pts = notablePoints(e);
      for (const pt of pts) {
        const d = Math.hypot(pt.x - world.x, pt.y - world.y);
        if (d < bestD) { bestD = d; best = pt; }
      }
    }
    if (best) return best;
    const step = gridStepRef.current;
    return { x: snapGrid(world.x, step), y: snapGrid(world.y, step), type: "grid" };
  };
  const showSnapMarker = (pt) => {
    const g = snapMarkerRef.current;
    if (!g) return;
    if (!pt || pt.type === "free") { g.style.display = "none"; return; }
    const s = toScreen(pt.x, pt.y);
    g.style.display = "";
    g.setAttribute("transform", `translate(${s.x} ${s.y})`);
    const col = pt.type === "grid" ? "var(--fg-mute)" : "var(--accent-2)";
    const box = g.querySelector(".snap-box");
    const lab = g.querySelector(".snap-lab");
    if (box) box.setAttribute("stroke", col);
    if (lab) { lab.textContent = SNAP_LABEL[pt.type] || ""; lab.setAttribute("fill", col); }
  };

  /* ---------- HIT TEST (com pré-filtro por bbox + lookup O(1) de camada) ---------- */
  const hitTest = (world) => {
    const v = viewRef.current;
    const tol = 6 / v.zoom;
    const arr = entsRef.current;
    const lmap = layerMapRef.current;
    for (let i = arr.length - 1; i >= 0; i--) {
      const e = arr[i];
      const L = lmap[e.layer];
      if (L && (!L.visible || L.locked)) continue;
      const b = bbox(e);
      if (world.x < b.x1 - tol || world.x > b.x2 + tol || world.y < b.y1 - tol || world.y > b.y2 + tol) continue;
      if (e.kind === "line" || e.kind === "measure") {
        if (distPtToSeg(world, { x: e.x1, y: e.y1 }, { x: e.x2, y: e.y2 }) < tol) return e.id;
      } else if (e.kind === "rect") {
        const onX = Math.abs(world.x - e.x) < tol || Math.abs(world.x - (e.x + e.w)) < tol;
        const onY = Math.abs(world.y - e.y) < tol || Math.abs(world.y - (e.y + e.h)) < tol;
        const inX = world.x >= e.x - tol && world.x <= e.x + e.w + tol;
        const inY = world.y >= e.y - tol && world.y <= e.y + e.h + tol;
        if ((onX && inY) || (onY && inX)) return e.id;
      } else if (e.kind === "circle") {
        if (Math.abs(Math.hypot(world.x - e.cx, world.y - e.cy) - e.r) < tol) return e.id;
      } else if (e.kind === "poly") {
        for (let k = 0; k < e.points.length - 1; k++)
          if (distPtToSeg(world, e.points[k], e.points[k + 1]) < tol) return e.id;
      } else if (e.kind === "text") {
        const tb = textBox(e);
        if (world.x >= tb.x1 && world.x <= tb.x2 && world.y >= tb.y1 && world.y <= tb.y2) return e.id;
      } else if (e.kind === "symbol") {
        if (world.x >= e.x - e.w / 2 && world.x <= e.x + e.w / 2 && world.y >= e.y - e.h / 2 && world.y <= e.y + e.h / 2) return e.id;
      }
    }
    return null;
  };
  const handleHitTest = (sx, sy) => {
    const sel = selectedRef.current;
    if (sel.size !== 1) return null;
    const id = [...sel][0];
    const e = entsRef.current.find(x => x.id === id);
    if (!e) return null;
    for (const h of getHandles(e)) {
      const s = toScreen(h.x, h.y);
      if (Math.hypot(s.x - sx, s.y - sy) < 9) return { handle: h.id, entId: id, orig: e };
    }
    return null;
  };

  /* ---------- history ----------
     100% em ref: zero re-render por commit, sem closures velhas,
     com cap (MAX_HISTORY). Snapshots compartilham referências de
     entidades não alteradas (structural sharing) → memória baixa. */
  const histRef = useRef(null);
  if (histRef.current === null) histRef.current = { stack: [entsRef.current], idx: 0 };
  const commit = useCallback((next) => {
    setEntsNow(next);
    const h = histRef.current;
    h.stack = h.stack.slice(0, h.idx + 1);
    h.stack.push(next);
    if (h.stack.length > MAX_HISTORY) h.stack.splice(0, h.stack.length - MAX_HISTORY);
    h.idx = h.stack.length - 1;
  }, [setEntsNow]);
  const doUndo = useCallback(() => {
    const h = histRef.current;
    if (h.idx <= 0) return;
    h.idx -= 1;
    setEntsNow(h.stack[h.idx]);
    setSelected(new Set());
  }, [setEntsNow]);
  const doRedo = useCallback(() => {
    const h = histRef.current;
    if (h.idx >= h.stack.length - 1) return;
    h.idx += 1;
    setEntsNow(h.stack[h.idx]);
  }, [setEntsNow]);

  /* ---------- keyboard (registrado UMA única vez) ---------- */
  useEffect(() => {
    const onKey = (e) => {
      if (e.key === "Shift") shiftRef.current = true;
      const tag = e.target.tagName;
      if (tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT" || e.target.isContentEditable) return;
      if (e.code === "Space") { spaceRef.current = true; e.preventDefault(); return; } // espaço = mão (já anunciado na barra de ajuda)
      if (e.metaKey || e.ctrlKey) {
        if (e.key === "z") { e.preventDefault(); e.shiftKey ? doRedo() : doUndo(); return; }
        if (e.key === "y") { e.preventDefault(); doRedo(); return; }
      }
      const t = TOOLS.find(t => t.shortcut === e.key.toLowerCase());
      if (t) { setTool(t.id); return; }
      if (e.key === "Escape") { setDraftNow(null); setSelected(new Set()); }
      if (e.key === "Delete" || e.key === "Backspace") {
        const sel = selectedRef.current;
        if (sel.size) { commit(entsRef.current.filter(en => !sel.has(en.id))); setSelected(new Set()); }
      }
    };
    const onKeyUp = (e) => {
      if (e.key === "Shift") shiftRef.current = false;
      if (e.code === "Space") spaceRef.current = false;
    };
    window.addEventListener("keydown", onKey);
    window.addEventListener("keyup", onKeyUp);
    return () => { window.removeEventListener("keydown", onKey); window.removeEventListener("keyup", onKeyUp); };
  }, [commit, doUndo, doRedo, setDraftNow]);

  /* ---------- pointer interactions ---------- */
  const dragRef = useRef(null);

  const onMouseDown = (e) => {
    const { sx, sy } = localXY(e);
    // pan: middle/right button, pan tool, ou espaço pressionado
    if (e.button === 1 || e.button === 2 || (e.button === 0 && (toolRef.current === "pan" || spaceRef.current))) {
      dragRef.current = { type: "pan", sx, sy, off: { ...viewRef.current.offset } };
      e.preventDefault();
      return;
    }
    if (e.button !== 0) return;

    if (tool === "select") {
      // handle drag first
      const hh = handleHitTest(sx, sy);
      if (hh) { dragRef.current = { type: "resize", ...hh, changed: false }; return; }
      const world = toWorld(sx, sy);
      const hit = hitTest(world);
      if (hit) {
        let nextSel;
        if (e.shiftKey) { nextSel = new Set(selected); nextSel.has(hit) ? nextSel.delete(hit) : nextSel.add(hit); }
        else nextSel = new Set([hit]);
        setSelected(nextSel);
        dragRef.current = { type: "move", startWorld: world, moved: false,
          originals: entsRef.current.filter(en => nextSel.has(en.id)) };
      } else {
        setSelected(new Set());
        dragRef.current = { type: "marquee", x1: world.x, y1: world.y };
        setDraftNow({ type: "marquee", x1: world.x, y1: world.y, x2: world.x, y2: world.y });
      }
      return;
    }

    // drawing tools — use snap point
    const snap = findSnap(sx, sy);
    const p = { x: snap.x, y: snap.y };
    if (tool === "line") {
      setDraftNow({ kind: "line", x1: p.x, y1: p.y, x2: p.x, y2: p.y, color, stroke, layer: activeLayer });
      dragRef.current = { type: "draw" };
    } else if (tool === "rect") {
      setDraftNow({ kind: "rect", x: p.x, y: p.y, w: 0, h: 0, color, stroke, layer: activeLayer });
      dragRef.current = { type: "draw", start: p };
    } else if (tool === "circle") {
      setDraftNow({ kind: "circle", cx: p.x, cy: p.y, r: 0, color, stroke, layer: activeLayer });
      dragRef.current = { type: "draw", start: p };
    } else if (tool === "poly") {
      const d = draftRef.current;
      if (!d || d.kind !== "poly") setDraftNow({ kind: "poly", points: [p, { ...p }], color, stroke, layer: activeLayer });
      else { const pts = [...d.points]; pts[pts.length - 1] = p; pts.push({ ...p }); setDraftNow({ ...d, points: pts }); }
    } else if (tool === "text") {
      const txt = prompt("Texto:", "Texto");
      if (txt) commit([...entsRef.current, { id: uid(), kind: "text", x: p.x, y: p.y, text: txt, color, layer: activeLayer, size: 14 }]);
    } else if (tool === "measure") {
      setDraftNow({ kind: "measure", x1: p.x, y1: p.y, x2: p.x, y2: p.y, color: "#F2B043", stroke: 1, layer: "L3" });
      dragRef.current = { type: "draw" };
    }
  };

  const onMouseMove = (e) => {
    const { sx, sy } = localXY(e);
    positionCross(sx, sy);
    if (crosshairRef.current && crosshairRef.current.style.display === "none") crosshairRef.current.style.display = "";
    const d = dragRef.current;

    /* PAN — fully imperative */
    if (d && d.type === "pan") {
      viewRef.current = { ...viewRef.current, offset: { x: d.off.x + (sx - d.sx), y: d.off.y + (sy - d.sy) } };
      scheduleApply();
      return;
    }

    // live coordinate readout (imperative — no re-render)
    const rawWorld = toWorld(sx, sy);
    if (coordXRef.current) coordXRef.current.textContent = rawWorld.x.toFixed(0);
    if (coordYRef.current) coordYRef.current.textContent = rawWorld.y.toFixed(0);

    /* RESIZE — coalescido em 1 update/frame */
    if (d && d.type === "resize") {
      const snap = findSnap(sx, sy, [d.orig.id]);
      const p = { x: snap.x, y: snap.y };
      showSnapMarker(snap);
      const o = d.orig;
      let updated = o;
      if (o.kind === "rect") {
        let x1 = o.x, y1 = o.y, x2 = o.x + o.w, y2 = o.y + o.h;
        if (d.handle.includes("w")) x1 = p.x;
        if (d.handle.includes("e")) x2 = p.x;
        if (d.handle.includes("n")) y1 = p.y;
        if (d.handle.includes("s")) y2 = p.y;
        updated = { ...o, x: Math.min(x1, x2), y: Math.min(y1, y2), w: Math.abs(x2 - x1), h: Math.abs(y2 - y1) };
      } else if (o.kind === "circle") {
        updated = { ...o, r: Math.max(0.5, Math.hypot(p.x - o.cx, p.y - o.cy)) };
      } else if (o.kind === "line" || o.kind === "measure") {
        const anchor = d.handle === "p1" ? { x: o.x2, y: o.y2 } : { x: o.x1, y: o.y1 };
        const pp = shiftRef.current ? applyOrtho(anchor, p) : p;
        updated = d.handle === "p1" ? { ...o, x1: pp.x, y1: pp.y } : { ...o, x2: pp.x, y2: pp.y };
      }
      d.changed = true;
      scheduleState({ ents: entsRef.current.map(en => en.id === o.id ? updated : en) });
      return;
    }

    /* MOVE selected — coalescido */
    if (d && d.type === "move") {
      const snap = findSnap(sx, sy, d.originals.map(o => o.id));
      showSnapMarker(snap);
      const dx = snap.x - d.startWorld.x, dy = snap.y - d.startWorld.y;
      d.moved = true;
      const origById = d.origById || (d.origById = Object.fromEntries(d.originals.map(o => [o.id, o])));
      scheduleState({
        ents: entsRef.current.map(en => {
          const orig = origById[en.id];
          return orig ? translateEnt(orig, dx, dy) : en;
        })
      });
      return;
    }

    /* MARQUEE */
    if (d && d.type === "marquee") {
      const w = toWorld(sx, sy);
      const dr = draftRef.current;
      if (dr) scheduleState({ draft: { ...dr, x2: w.x, y2: w.y } });
      return;
    }

    /* DRAWING — coalescido */
    if (d && d.type === "draw") {
      const snap = findSnap(sx, sy);
      let p = { x: snap.x, y: snap.y };
      showSnapMarker(snap);
      const dr = draftRef.current;
      if (!dr) return;
      if (dr.kind === "line" || dr.kind === "measure") {
        if (shiftRef.current) p = applyOrtho({ x: dr.x1, y: dr.y1 }, p);
        scheduleState({ draft: { ...dr, x2: p.x, y2: p.y } });
      } else if (dr.kind === "rect") {
        const s = d.start;
        let w = p.x - s.x, h = p.y - s.y;
        if (shiftRef.current) { const m = Math.max(Math.abs(w), Math.abs(h)); w = Math.sign(w || 1) * m; h = Math.sign(h || 1) * m; }
        scheduleState({ draft: { ...dr, x: Math.min(s.x, s.x + w), y: Math.min(s.y, s.y + h), w: Math.abs(w), h: Math.abs(h) } });
      } else if (dr.kind === "circle") {
        scheduleState({ draft: { ...dr, r: Math.hypot(p.x - d.start.x, p.y - d.start.y) } });
      }
      return;
    }

    /* IDLE — hover highlight + snap preview */
    const snap = findSnap(sx, sy);
    showSnapMarker(toolRef.current === "select" ? (snap.type === "grid" ? null : snap) : snap);
    if (toolRef.current === "select") setHover(hitTest(rawWorld));
    else if (draftRef.current && draftRef.current.kind === "poly") {
      // rubber-band the last poly point while moving (click-to-add mode)
      const dd = draftRef.current; const pts = [...dd.points];
      let p = { x: snap.x, y: snap.y };
      if (shiftRef.current && pts.length >= 2) p = applyOrtho(pts[pts.length - 2], p);
      pts[pts.length - 1] = p; scheduleState({ draft: { ...dd, points: pts } });
    }
  };

  const onMouseUp = () => {
    const d = dragRef.current;
    if (!d) return;
    if (d.type === "pan") { commitViewNow(); dragRef.current = null; return; }
    if (d.type === "resize") {
      // só grava no histórico se houve alteração real (evita snapshot duplicado)
      if (d.changed) commit(entsRef.current);
      dragRef.current = null; showSnapMarker(null); return;
    }
    if (d.type === "move") { if (d.moved) commit(entsRef.current); dragRef.current = null; showSnapMarker(null); return; }
    if (d.type === "marquee") {
      const m = draftRef.current;
      const x1 = Math.min(m.x1, m.x2), x2 = Math.max(m.x1, m.x2);
      const y1 = Math.min(m.y1, m.y2), y2 = Math.max(m.y1, m.y2);
      const inside = new Set();
      const lmap = layerMapRef.current;
      entsRef.current.forEach(en => {
        const L = lmap[en.layer];
        if (L && (!L.visible || L.locked)) return; // marquee respeita camadas ocultas/travadas
        const b = bbox(en);
        if (b.x1 >= x1 && b.x2 <= x2 && b.y1 >= y1 && b.y2 <= y2) inside.add(en.id);
      });
      setSelected(inside); setDraftNow(null); dragRef.current = null; return;
    }
    if (d.type === "draw") {
      const dr = draftRef.current;
      if (dr && (dr.kind === "line" || dr.kind === "measure")) {
        if (Math.hypot(dr.x2 - dr.x1, dr.y2 - dr.y1) > 0.5) commit([...entsRef.current, { ...dr, id: uid() }]);
        setDraftNow(null);
      } else if (dr && dr.kind === "rect") {
        if (dr.w > 0.5 && dr.h > 0.5) commit([...entsRef.current, { ...dr, id: uid() }]);
        setDraftNow(null);
      } else if (dr && dr.kind === "circle") {
        if (dr.r > 0.5) commit([...entsRef.current, { ...dr, id: uid() }]);
        setDraftNow(null);
      }
      // poly stays open (finalized on dblclick)
      showSnapMarker(null);
      dragRef.current = null;
    }
  };

  const onDoubleClick = () => {
    const dr = draftRef.current;
    if (dr && dr.kind === "poly") {
      const pts = dr.points.slice(0, -1);
      if (pts.length >= 2) commit([...entsRef.current, { ...dr, points: pts, id: uid() }]);
      setDraftNow(null);
    }
  };

  /* ---------- WHEEL — listener NATIVO non-passive ----------
     (React/Chrome registram wheel como passive em alguns níveis e
     preventDefault() falha silenciosamente → a página rola junto
     com o zoom; o listener nativo elimina esse comportamento) */
  useEffect(() => {
    const el = svgRef.current;
    if (!el) return;
    const onWheelNative = (e) => {
      e.preventDefault();
      const r = el.getBoundingClientRect();
      const sx = e.clientX - r.left, sy = e.clientY - r.top;
      const v = viewRef.current;
      const factor = e.deltaY > 0 ? 0.9 : 1.1;
      const nz = clampZoom(v.zoom * factor);
      const wx = (sx - v.offset.x) / v.zoom, wy = (sy - v.offset.y) / v.zoom;
      viewRef.current = { zoom: nz, offset: { x: sx - wx * nz, y: sy - wy * nz } };
      scheduleApply();
      commitViewSoon();
    };
    el.addEventListener("wheel", onWheelNative, { passive: false });
    return () => el.removeEventListener("wheel", onWheelNative);
  }, [scheduleApply, commitViewSoon]);

  /* ---------- viewport buttons ---------- */
  const zoomBtn = (factor) => {
    const v = viewRef.current;
    const cx = size.w / 2, cy = size.h / 2;
    const nz = clampZoom(v.zoom * factor);
    const wx = (cx - v.offset.x) / v.zoom, wy = (cy - v.offset.y) / v.zoom;
    viewRef.current = { zoom: nz, offset: { x: cx - wx * nz, y: cy - wy * nz } };
    applyView(viewRef.current); commitViewNow();
  };
  const drawingBBox = (arr) => {
    let x1 = Infinity, y1 = Infinity, x2 = -Infinity, y2 = -Infinity;
    arr.forEach(e => { const b = bbox(e); if (b.x1 < x1) x1 = b.x1; if (b.y1 < y1) y1 = b.y1; if (b.x2 > x2) x2 = b.x2; if (b.y2 > y2) y2 = b.y2; });
    return { x1, y1, x2, y2 };
  };
  const zoomFit = () => {
    if (ents.length === 0) {
      viewRef.current = { zoom: 1, offset: { x: size.w / 2, y: size.h / 2 } };
    } else {
      const { x1, y1, x2, y2 } = drawingBBox(ents);
      const pad = 70;
      const z = clampZoom(Math.min((size.w - pad * 2) / (x2 - x1 + 1), (size.h - pad * 2) / (y2 - y1 + 1)));
      viewRef.current = { zoom: z, offset: { x: size.w / 2 - (x1 + x2) / 2 * z, y: size.h / 2 - (y1 + y2) / 2 * z } };
    }
    applyView(viewRef.current); commitViewNow();
  };

  const dropSymbol = (sym) => {
    const v = viewRef.current;
    const cx = (size.w / 2 - v.offset.x) / v.zoom;
    const cy = (size.h / 2 - v.offset.y) / v.zoom;
    const p = snapOn ? { x: snapGrid(cx, gridStep), y: snapGrid(cy, gridStep) } : { x: cx, y: cy };
    commit([...entsRef.current, { id: uid(), kind: "symbol", symId: sym.id, x: p.x, y: p.y, w: sym.w, h: sym.h, rot: 0,
      color: (layerMapRef.current["L2"] || {}).color || "#5BE38C", stroke: 1.5, layer: "L2" }]);
  };

  const clearAll = () => { if (confirm("Limpar todo o desenho?")) { commit([]); setSelected(new Set()); } };

  /* ---------- EXPORT SVG limpo ----------
     Renderiza um SVG oculto SOMENTE com as entidades (sem grid,
     sem seleção/hover, sem crosshair), com viewBox calculado do
     desenho INTEIRO → independente do pan/zoom e sem recorte. */
  const exportSVG = () => {
    const arr = entsRef.current;
    let vb;
    if (arr.length === 0) vb = { x: 0, y: 0, w: size.w, h: size.h };
    else {
      const { x1, y1, x2, y2 } = drawingBBox(arr);
      const pad = 40;
      vb = { x: x1 - pad, y: y1 - pad, w: (x2 - x1) + pad * 2, h: (y2 - y1) + pad * 2 };
    }
    ReactDOM.flushSync(() => setExporting(vb));
    const node = exportRef.current;
    if (node) {
      const clone = node.cloneNode(true);
      clone.removeAttribute("style");
      const xml = new XMLSerializer().serializeToString(clone);
      const blob = new Blob([xml], { type: "image/svg+xml" });
      const url = URL.createObjectURL(blob);
      const a = document.createElement("a"); a.href = url; a.download = "voltlab-desenho.svg"; a.click();
      URL.revokeObjectURL(url);
    }
    setExporting(null);
  };

  /* selection signature (stable string) so EntitiesLayer memo holds across draft updates */
  const selKey = useMemo(() => [...selected].sort().join("|"), [selected]);
  const singleSel = selected.size === 1 ? ents.find(e => e.id === [...selected][0]) : null;
  const handles = singleSel ? getHandles(singleSel) : [];

  /* contagem de entidades por camada calculada UMA vez por render (O(N)) */
  const layerCounts = useMemo(() => {
    const c = {};
    for (const e of ents) c[e.layer] = (c[e.layer] || 0) + 1;
    return c;
  }, [ents]);

  /* draft entity preview color resolution */
  const draftColor = (draft && draft.color) || color;

  return (
    <div className="vl-cad">
      <div className="vl-cad-grid">
        {/* LEFT TOOLBAR */}
        <aside className="vl-cad-tools">
          {TOOLS.map(t => (
            <button key={t.id} className={"vl-cad-tool " + (tool === t.id ? "is-active" : "")}
                    title={t.label} onClick={() => setTool(t.id)}>
              <CIcon name={t.icon} size={16}/>
            </button>
          ))}
          <div className="vl-cad-tool-sep"/>
          <button className="vl-cad-tool" title="Desfazer (Ctrl+Z)" onClick={doUndo}><CIcon name="undo"/></button>
          <button className="vl-cad-tool" title="Refazer" onClick={doRedo}><CIcon name="redo"/></button>
          <div className="vl-cad-tool-sep"/>
          <button className="vl-cad-tool" title="Zoom +" onClick={() => zoomBtn(1.25)}><CIcon name="zoomIn"/></button>
          <button className="vl-cad-tool" title="Zoom −" onClick={() => zoomBtn(0.8)}><CIcon name="zoomOut"/></button>
          <button className="vl-cad-tool" title="Ajustar à tela" onClick={zoomFit}><CIcon name="fit"/></button>
          <div className="vl-cad-tool-sep"/>
          <button className="vl-cad-tool is-danger" title="Limpar tudo" onClick={clearAll}><CIcon name="trash"/></button>
        </aside>

        {/* CANVAS */}
        <div className="vl-cad-stage" ref={containerRef}>
          <div className="vl-cad-topbar">
            <div className="vl-cad-topbar-l">
              <span className="vl-cad-pill">
                <span className="status-dot status-ok"/>
                {(TOOLS.find(t => t.id === tool) || {}).label}
              </span>
              <button className={"vl-cad-toggle " + (snapOn ? "is-on" : "")} onClick={() => setSnapOn(s => !s)} title="Snap a pontos notáveis e grade">
                <CIcon name="snap" size={13}/> SNAP
              </button>
              <button className={"vl-cad-toggle " + (gridOn ? "is-on" : "")} onClick={() => setGridOn(s => !s)} title="Grade">
                <CIcon name="grid" size={13}/> GRID
              </button>
              <div className="vl-cad-step">
                STEP
                <select value={gridStep} onChange={e => setGridStep(Number(e.target.value))}>
                  {[5, 10, 20, 50, 100].map(s => <option key={s} value={s}>{s}px</option>)}
                </select>
              </div>
            </div>
            <div className="vl-cad-topbar-r">
              <span className="vl-cad-coord">X <b ref={coordXRef}>0</b> · Y <b ref={coordYRef}>0</b></span>
              <span className="vl-cad-coord">ZOOM <b ref={zoomLabelRef}>{(zoom * 100).toFixed(0)}%</b></span>
              <button className="btn btn-ghost btn-sm" onClick={exportSVG}>
                <CIcon name="download" size={13}/> Exportar SVG
              </button>
            </div>
          </div>

          <svg
            ref={svgRef}
            className="vl-cad-svg"
            onMouseDown={onMouseDown}
            onMouseMove={onMouseMove}
            onMouseUp={onMouseUp}
            onMouseEnter={showCross}
            onMouseLeave={(e) => { onMouseUp(); hideCross(); }}
            onDoubleClick={onDoubleClick}
            onContextMenu={e => e.preventDefault()}
            style={{ cursor: "none" }}
          >
            <defs>
              <pattern id="cadGridMinor" ref={minorRef} x={offset.x} y={offset.y} width={gridStep * zoom} height={gridStep * zoom} patternUnits="userSpaceOnUse">
                <path ref={minorPathRef} d={`M ${gridStep * zoom} 0 L 0 0 0 ${gridStep * zoom}`} fill="none" stroke="oklch(28% 0.012 240)" strokeWidth="0.5"/>
              </pattern>
              <pattern id="cadGridMajor" ref={majorRef} x={offset.x} y={offset.y} width={gridStep * 5 * zoom} height={gridStep * 5 * zoom} patternUnits="userSpaceOnUse">
                <path ref={majorPathRef} d={`M ${gridStep * 5 * zoom} 0 L 0 0 0 ${gridStep * 5 * zoom}`} fill="none" stroke="oklch(35% 0.014 240)" strokeWidth="0.8"/>
              </pattern>
            </defs>

            {gridOn && (
              <g className="cad-ui-only">
                <rect width={size.w} height={size.h} fill="url(#cadGridMinor)"/>
                <rect width={size.w} height={size.h} fill="url(#cadGridMajor)"/>
                <line ref={axisXRef} x1="0" y1={offset.y} x2={size.w} y2={offset.y} stroke="var(--accent)" strokeWidth="0.6" opacity="0.5"/>
                <line ref={axisYRef} x1={offset.x} y1="0" x2={offset.x} y2={size.h} stroke="var(--accent)" strokeWidth="0.6" opacity="0.5"/>
              </g>
            )}

            {/* World group — transform driven by React state, overridden imperatively during gestures */}
            <g ref={worldRef} transform={`translate(${offset.x}, ${offset.y}) scale(${zoom})`}>
              <EntitiesLayer ents={ents} selKey={selKey} hoverId={hover} layers={layers}/>
              {draft && draft.kind && (
                <g opacity="0.9">{entityEl(draft, draftColor, draft.stroke || stroke, "4 4")}</g>
              )}
            </g>

            {/* Marquee (screen space) */}
            {draft && draft.type === "marquee" && (() => {
              const a = toScreen(draft.x1, draft.y1), b = toScreen(draft.x2, draft.y2);
              return <rect className="cad-ui-only" x={Math.min(a.x, b.x)} y={Math.min(a.y, b.y)}
                           width={Math.abs(b.x - a.x)} height={Math.abs(b.y - a.y)}
                           fill="var(--accent-soft)" stroke="var(--accent)" strokeDasharray="4 4" strokeWidth="1"/>;
            })()}

            {/* Resize handles (screen space, constant size) */}
            {handles.length > 0 && tool === "select" && (
              <g className="cad-ui-only">
                {handles.map(h => {
                  const s = toScreen(h.x, h.y);
                  return <rect key={h.id} x={s.x - 4} y={s.y - 4} width="8" height="8"
                               fill="var(--bg-0)" stroke="var(--accent)" strokeWidth="1.5" rx="1"/>;
                })}
              </g>
            )}

            {/* Cursor crosshair (screen space, follows pointer) */}
            <g ref={crosshairRef} className="cad-ui-only" style={{ display: "none", pointerEvents: "none" }}>
              <line ref={crossHRef} x1="0" y1="0" x2={size.w} y2="0" stroke="var(--accent-2)" strokeWidth="0.7" opacity="0.5"/>
              <line ref={crossVRef} x1="0" y1="0" x2="0" y2={size.h} stroke="var(--accent-2)" strokeWidth="0.7" opacity="0.5"/>
              <rect ref={crossBoxRef} x="-6.5" y="-6.5" width="13" height="13" fill="none" stroke="var(--accent-2)" strokeWidth="1" opacity="0.85"/>
            </g>

            {/* Snap marker (imperative) */}
            <g ref={snapMarkerRef} className="cad-ui-only" style={{ display: "none", pointerEvents: "none" }}>
              <rect className="snap-box" x="-6" y="-6" width="12" height="12" fill="none" stroke="var(--accent-2)" strokeWidth="1.5"/>
              <line x1="-9" y1="0" x2="9" y2="0" stroke="var(--accent-2)" strokeWidth="0.6" opacity="0.5"/>
              <line x1="0" y1="-9" x2="0" y2="9" stroke="var(--accent-2)" strokeWidth="0.6" opacity="0.5"/>
              <text className="snap-lab" x="10" y="-8" fontSize="9" fontFamily="JetBrains Mono" fill="var(--accent-2)" stroke="none"></text>
            </g>
          </svg>

          {/* SVG oculto usado apenas durante a exportação (fora da tela, sem UI) */}
          {exporting && (
            <svg ref={exportRef} xmlns="http://www.w3.org/2000/svg"
                 viewBox={`${exporting.x} ${exporting.y} ${exporting.w} ${exporting.h}`}
                 width={exporting.w} height={exporting.h}
                 style={{ position: "fixed", left: "-100000px", top: 0 }}>
              <EntitiesLayer ents={ents} selKey="" hoverId={null} layers={layers}/>
            </svg>
          )}

          <div className="vl-cad-bottombar">
            <span>OBJETOS · <b>{ents.length}</b></span>
            <span>SELECIONADOS · <b>{selected.size}</b></span>
            <span>CAMADA ATIVA · <b>{(layers.find(l => l.id === activeLayer) || {}).name}</b></span>
            <span className="vl-cad-help">L·R·C·P·T = ferramentas · Shift = orto/45° · arraste alças p/ redimensionar · scroll = zoom · espaço/H = mão</span>
          </div>
        </div>

        {/* RIGHT PANEL */}
        <aside className="vl-cad-side">
          <div className="vl-cad-side-section">
            <div className="vl-cad-side-eyebrow">PROPRIEDADES</div>
            <div className="vl-cad-prop-row">
              <label>Cor</label>
              <div className="vl-cad-swatches">
                {["#3D8BFF", "#3DD6E6", "#5BE38C", "#F2B043", "#F26B6B", "#FFFFFF"].map(c => (
                  <button key={c} className={"vl-cad-sw " + (color === c ? "is-sel" : "")}
                          style={{ background: c }} onClick={() => setColor(c)}/>
                ))}
              </div>
            </div>
            <div className="vl-cad-prop-row">
              <label>Espessura</label>
              <div className="vl-cad-stroke-picker">
                {[0.5, 1, 1.5, 2, 3].map(s => (
                  <button key={s} className={"vl-cad-stroke-opt " + (stroke === s ? "is-sel" : "")} onClick={() => setStroke(s)}>
                    <span style={{ height: s + "px", background: color }}/>
                  </button>
                ))}
              </div>
            </div>
          </div>

          <div className="vl-cad-side-section">
            <div className="vl-cad-side-head">
              <span className="vl-cad-side-eyebrow">CAMADAS</span>
              <button className="vl-cad-side-add" onClick={() => {
                const id = "L" + (layers.length + 1);
                setLayers([...layers, { id, name: "Camada " + (layers.length + 1), color: "#9aa5b8", visible: true, locked: false }]);
              }}><CIcon name="plus" size={12}/></button>
            </div>
            <div className="vl-cad-layers">
              {layers.map(L => (
                <div key={L.id} className={"vl-cad-layer " + (activeLayer === L.id ? "is-active" : "")} onClick={() => setActiveLayer(L.id)}>
                  <button className="vl-cad-layer-vis" onClick={(e) => { e.stopPropagation(); setLayers(layers.map(l => l.id === L.id ? { ...l, visible: !l.visible } : l)); }}>
                    <CIcon name={L.visible ? "eye" : "eyeOff"} size={13}/>
                  </button>
                  <button className="vl-cad-layer-lock" onClick={(e) => { e.stopPropagation(); setLayers(layers.map(l => l.id === L.id ? { ...l, locked: !l.locked } : l)); }}>
                    <CIcon name={L.locked ? "lock" : "unlock"} size={13}/>
                  </button>
                  <span className="vl-cad-layer-color" style={{ background: L.color }}/>
                  <span className="vl-cad-layer-name">{L.name}</span>
                  <span className="vl-cad-layer-count">{layerCounts[L.id] || 0}</span>
                </div>
              ))}
            </div>
          </div>

          <div className="vl-cad-side-section vl-cad-symbols">
            <div className="vl-cad-side-head">
              <span className="vl-cad-side-eyebrow">SÍMBOLOS · CLIQUE PARA INSERIR</span>
            </div>
            <div className="vl-cad-sym-tabs">
              {SYM_GROUPS.map(g => (
                <button key={g} className={"vl-cad-sym-tab " + (symGroup === g ? "is-active" : "")} onClick={() => setSymGroup(g)}>{g}</button>
              ))}
            </div>
            <div className="vl-cad-sym-grid">
              {SYMBOLS.filter(s => s.group === symGroup).map(s => (
                <button key={s.id} className="vl-cad-sym" onClick={() => dropSymbol(s)} title={s.name}>
                  <svg viewBox={`0 0 ${s.w} ${s.h}`} width="56" height="56"
                       stroke="var(--accent)" strokeWidth="1.6" fill="none" strokeLinecap="round" strokeLinejoin="round">{s.draw}</svg>
                  <em>{s.name}</em>
                </button>
              ))}
            </div>
          </div>
        </aside>
      </div>
    </div>
  );
};

window.VL_CAD = CADModule;
