// gp-mesure.jsx — "Mesure sur plan" focused dark takeoff workspace
// ───────────────────────────────────────────────────────────────
const { Icon: ZIcon, moduleById: zModById, ESTIMATION_MODULES: ZMODS, fmt: zFmt } = window;

// outils de relevé (take-off sur plan existant)
const MZ_TOOLS = [
{ id: 'plan', n: 'file', l: 'Plan' },
{ id: 'echelle', n: 'scale', l: 'Échelle' },
{ id: 'ligne', n: 'line', l: 'Ligne' },
{ id: 'surface', n: 'surface', l: 'Surf.' },
{ id: 'ouvert', n: 'window', l: 'Ouvert.' },
{ id: 'pan', n: 'pan', l: 'Pan' },
{ id: 'angle', n: 'angle', l: 'Angle' },
{ id: 'select', n: 'cursor', l: 'Sélect.' }];

// outils de dessin (création d'un plan sans document de départ)
const MZ_DRAW_TOOLS = [
{ id: 'select', n: 'cursor', l: 'Sélect.' },
{ id: 'dline', n: 'line', l: 'Ligne' },
{ id: 'rect', n: 'square', l: 'Rect.' },
{ id: 'poly', n: 'polygon', l: 'Polyg.' },
{ id: 'circ', n: 'circle', l: 'Cercle' },
{ id: 'dim', n: 'dim', l: 'Cote' },
{ id: 'text', n: 'textT', l: 'Texte' }];

const MZ_PAGES = [
{ n: 'Page 1', ct: 2 }, { n: 'Page 2', ct: 4 }, { n: 'Page 3' }, { n: 'Page 4' }, { n: 'Page 5' },
{ n: 'Page 6', ct: 2 }, { n: 'Page 7' }, { n: 'Page 8' }, { n: 'Page 9' }];

// composants de ventilation — outils traçables (barre de gauche)
const VENT_COMPONENTS = [
{ id: 'conduit', n: 'duct', l: 'Conduit', full: 'Conduit', line: true },
{ id: 'trappe', n: 'box', l: 'Trappe', full: 'Trappe d’accès' },
{ id: 'grille', n: 'grille', l: 'Grille', full: 'Grille / diffuseur' },
{ id: 'vrc', n: 'fan', l: 'VRC', full: 'VRC / équipement' },
{ id: 'prise', n: 'surface', l: 'Prise', full: 'Prise d’air' },
{ id: 'sortie', n: 'roof', l: 'Sortie', full: 'Sortie de toit' }];
const VENT_BY_ID = Object.fromEntries(VENT_COMPONENTS.map((c) => [c.id, c]));
// outils du mode Ventilation (sélection + un outil par composant)
const VENT_TOOLS = [{ id: 'select', n: 'cursor', l: 'Sélect.' }, ...VENT_COMPONENTS.map((c) => ({ id: c.id, n: c.n, l: c.l }))];
// dimensions réelles approximatives de la feuille (pour estimer les longueurs)
const SHEET_FT = { w: 50, h: 36 };
const conduitLen = (pts) => {
  let s = 0;
  for (let i = 1; i < pts.length; i++) {
    const dx = (pts[i].x - pts[i - 1].x) / 100 * SHEET_FT.w;
    const dy = (pts[i].y - pts[i - 1].y) / 100 * SHEET_FT.h;
    s += Math.hypot(dx, dy);
  }
  return +s.toFixed(1);
};

// Raccourcis clavier & actions rapides, regroupés
const MZ_SHORTCUTS = [
{ group: 'Outils', items: [
  ['V', 'Sélection'], ['L', 'Ligne'], ['S', 'Surface'], ['O', 'Ouverture'],
  ['E', 'Échelle'], ['P', 'Déplacer (pan)'], ['A', 'Angle']] },
{ group: 'Navigation', items: [
  ['Espace + glisser', 'Déplacer le plan'], ['+ / −', 'Zoom avant / arrière'],
  ['⌘ 0', 'Ajuster à l’écran'], ['← / →', 'Page précédente / suivante']] },
{ group: 'Relevé & dessin', items: [
  ['Entrée', 'Terminer la forme'], ['Échap', 'Annuler la forme'],
  ['⌘ Z', 'Annuler'], ['⌘ ⇧ Z', 'Rétablir'], ['Suppr', 'Supprimer la sélection'],
  ['⌘ D', 'Dupliquer']] },
{ group: 'Assistant & aide', items: [
  ['I', 'Ouvrir l’assistant IA'], ['?', 'Afficher les raccourcis'],
  ['⌘ Entrée', 'Envoyer au module cible']] }];

// Actions rapides de l'assistant IA (simulées)
const MZ_AI_ACTIONS = [
{ id: 'surfaces', icon: 'surface', label: 'Détecter les surfaces',
  reply: 'J’ai repéré 2 surfaces fermées sur le plan actif.',
  suggest: [
  { name: 'Pan de toit — détecté', kind: 'surface', val: 547.4, unit: 'pi²', target: 'toiture' },
  { name: 'Débord de toit — détecté', kind: 'surface', val: 96.0, unit: 'pi²', target: 'toiture' }] },
{ id: 'echelle', icon: 'scale', label: 'Caler l’échelle',
  reply: 'Cartouche détecté : échelle 1/4″ = 1′-0″ proposée (confiance 92 %).', suggest: [] },
{ id: 'ouvertures', icon: 'window', label: 'Repérer les ouvertures',
  reply: 'J’ai détecté 3 ouvertures probables sur cette page.',
  suggest: [
  { name: 'Fenêtre détectée', kind: 'ouvert', val: 1, unit: 'u', target: 'fenestration' },
  { name: 'Porte détectée', kind: 'ouvert', val: 1, unit: 'u', target: 'fenestration' }] },
{ id: 'verif', icon: 'check', label: 'Vérifier la cohérence',
  reply: 'Les périmètres relevés concordent avec les surfaces (écart < 2 %). Aucune anomalie.', suggest: [] }];


const defaultTool = (m, ctx) =>
m === 'dessin' ? 'rect' : m === 'ventil' ? 'conduit' :
ctx === 'fenestration' ? 'ouvert' : ctx === 'fondation' ? 'ligne' : 'surface';

function Mesure({ onClose, measures = [], setMeasures, ctx, projectId, projectName, onExport, onImport }) {
  const [mode, setMode] = React.useState('releve'); // 'releve' | 'dessin' | 'ventil'
  const [tool, setTool] = React.useState(defaultTool('releve', ctx));
  const [page, setPage] = React.useState(5);
  const [zoom, setZoom] = React.useState(23);
  const [target, setTarget] = React.useState(ctx || 'toiture');
  const [aiOpen, setAiOpen] = React.useState(false);
  const [shortcutsOpen, setShortcutsOpen] = React.useState(false);
  const [aiMsgs, setAiMsgs] = React.useState([
  { from: 'ia', text: 'Bonjour ! Je peux détecter des surfaces, caler l’échelle ou repérer les ouvertures sur ce plan. Que souhaitez-vous ?' }]);
  const [aiSug, setAiSug] = React.useState([]);
  const [draft, setDraft] = React.useState(null); // tracé de conduit en cours
  const [cursor, setCursor] = React.useState(null); // position curseur (aperçu segment)
  const canvasRef = React.useRef(null);

  // ─────────────────────────────────────────────────────────────
  //  TAKE-OFF RÉEL (mode Relevé) — plan importé + échelle + dessin
  //  Contrat hérité du take-off ProfibCell (GPM_) :
  //    échelle  : pxParPi = distancePx(2 points) / longueurRéelle(pi)
  //    longueur : Σ hypot(dx,dy) / pxParPi
  //    surface  : aire du lacet (shoelace) / pxParPi²
  //  Coordonnées stockées en PIXELS NATURELS de l'image (stables au zoom).
  // ─────────────────────────────────────────────────────────────
  const [planSrc, setPlanSrc] = React.useState(null);     // dataURL de l'image importée
  const [imgNat, setImgNat] = React.useState({ w: 0, h: 0 }); // dimensions naturelles
  const [pxPerFt, setPxPerFt] = React.useState(null);     // échelle (px naturels / pied)
  const [dpts, setDpts] = React.useState([]);             // points du tracé en cours (px naturels)
  const [dhover, setDhover] = React.useState(null);       // curseur live (px naturels)
  const [scalePend, setScalePend] = React.useState(null); // {a,b} en attente de saisie de longueur
  const imgRef = React.useRef(null);

  const hasPlan = !!planSrc;
  // outils de take-off réel disponibles une fois le plan chargé
  const TO_TOOLS = [
    { id: 'echelle', n: 'scale', l: 'Échelle' },
    { id: 'ligne', n: 'line', l: 'Ligne' },
    { id: 'surface', n: 'surface', l: 'Surf.' },
    { id: 'select', n: 'cursor', l: 'Sélect.' }];

  // applique un plan chargé (dataURL + dimensions) et réinitialise le dessin
  const applyPlan = (src, w, h) => {
    setImgNat({ w, h }); setPlanSrc(src);
    setPxPerFt(null); setDpts([]); setDhover(null); setScalePend(null); setTool('echelle');
  };
  // import d'un plan : image (PNG/JPG) OU PDF (1re page rendue via pdf.js)
  const onPlanFile = (file) => {
    if (!file) return;
    const isPdf = file.type === 'application/pdf' || /\.pdf$/i.test(file.name);
    if (isPdf) {
      if (!window.pdfjsLib) { alert('Lecteur PDF non disponible (pdf.js non chargé).'); return; }
      const reader = new FileReader();
      reader.onload = (e) => {
        window.pdfjsLib.getDocument({ data: new Uint8Array(e.target.result) }).promise
          .then((pdf) => pdf.getPage(1))
          .then((pageObj) => {
            const vp = pageObj.getViewport({ scale: 2 }); // 2× pour une bonne résolution de relevé
            const c = document.createElement('canvas');
            c.width = Math.round(vp.width); c.height = Math.round(vp.height);
            return pageObj.render({ canvasContext: c.getContext('2d'), viewport: vp }).promise
              .then(() => applyPlan(c.toDataURL('image/png'), c.width, c.height));
          })
          .catch((err) => alert('PDF illisible : ' + (err && err.message || err)));
      };
      reader.onerror = () => alert('Lecture du PDF impossible.');
      reader.readAsArrayBuffer(file);
      return;
    }
    if (/^image\//.test(file.type)) {
      const reader = new FileReader();
      reader.onload = (e) => {
        const img = new Image();
        img.onload = () => applyPlan(e.target.result, img.naturalWidth, img.naturalHeight);
        img.onerror = () => alert('Image illisible.');
        img.src = e.target.result;
      };
      reader.onerror = () => alert('Lecture du fichier impossible.');
      reader.readAsDataURL(file);
      return;
    }
    alert('Format non supporté : importez une image (PNG/JPG) ou un PDF.');
  };

  // conversion clic écran → pixels naturels de l'image
  const toNat = (e) => {
    const el = imgRef.current; if (!el || !imgNat.w) return null;
    const r = el.getBoundingClientRect();
    return {
      x: +((e.clientX - r.left) / r.width * imgNat.w).toFixed(2),
      y: +((e.clientY - r.top) / r.height * imgNat.h).toFixed(2) };
  };
  // math de mesure (pixels naturels → pieds)
  const segFt = (a, b) => Math.hypot(b.x - a.x, b.y - a.y) / pxPerFt;
  const polyFt = (pts) => { let d = 0; for (let i = 1; i < pts.length; i++) d += Math.hypot(pts[i].x - pts[i - 1].x, pts[i].y - pts[i - 1].y); return d / pxPerFt; };
  const areaFt2 = (pts) => { let a = 0; for (let i = 0; i < pts.length; i++) { const j = (i + 1) % pts.length; a += pts[i].x * pts[j].y - pts[j].x * pts[i].y; } return Math.abs(a / 2) / (pxPerFt * pxPerFt); };

  // ajoute une mesure RÉELLE issue du plan (valeur calculée, jamais aléatoire)
  const addPlanMeasure = (kind, val, unit, planPts) => {
    const tMod = zModById(target);
    const n = measures.filter((m) => m.target === target).length + 1;
    setMeasures((ms) => [...ms, {
      id: 'me' + Date.now() + Math.round(Math.random() * 999),
      name: `${tMod ? tMod.name : 'Forme'} ${n}`,
      kind, val: +val.toFixed(kind === 'ligne' ? 1 : 0), unit,
      target, page: page + 1, source: 'plan', planPts }]);
  };

  // clic sur le plan selon l'outil actif
  const onPlanClick = (e) => {
    const p = toNat(e); if (!p) return;
    if (tool === 'echelle') {
      const next = [...dpts, p];
      if (next.length === 2) { setScalePend({ a: next[0], b: next[1] }); setDpts([]); setDhover(null); }
      else setDpts(next);
      return;
    }
    if (tool === 'ligne' || tool === 'surface') {
      if (!pxPerFt) { alert('Calez d’abord l’échelle (outil Échelle).'); setTool('echelle'); return; }
      setDpts((d) => [...d, p]);
    }
  };
  const onPlanMove = (e) => { if (tool === 'echelle' || tool === 'ligne' || tool === 'surface') { const p = toNat(e); if (p) setDhover(p); } };
  const finishDraw = () => {
    if (tool === 'ligne' && dpts.length >= 2) { addPlanMeasure('ligne', polyFt(dpts), 'pi lin.', dpts); setDpts([]); setDhover(null); }
    else if (tool === 'surface' && dpts.length >= 3) { addPlanMeasure('surface', areaFt2(dpts), 'pi²', dpts); setDpts([]); setDhover(null); }
  };
  // validation de l'échelle (longueur réelle saisie en pieds)
  const confirmScale = (feet) => {
    const f = parseFloat(String(feet).replace(',', '.'));
    if (!scalePend || !(f > 0)) { setScalePend(null); return; }
    const distPx = Math.hypot(scalePend.b.x - scalePend.a.x, scalePend.b.y - scalePend.a.y);
    setPxPerFt(distPx / f);
    setScalePend(null);
    setTool('ligne');
  };
  // Quand 2 points d'échelle sont posés, demander la longueur réelle.
  React.useEffect(() => {
    if (!scalePend) return;
    const v = window.prompt('Longueur réelle de la ligne tracée (en pieds) :', '10');
    if (v == null) { setScalePend(null); return; }
    confirmScale(v);
  }, [scalePend]);

  const ctxMod = zModById(ctx);
  const tools = mode === 'dessin' ? MZ_DRAW_TOOLS : mode === 'ventil' ? VENT_TOOLS : (hasPlan ? TO_TOOLS : MZ_TOOLS);
  const isVentLine = mode === 'ventil' && tool === 'conduit';
  const isVentPoint = mode === 'ventil' && VENT_BY_ID[tool] && !VENT_BY_ID[tool].line;
  const ventActive = isVentLine || isVentPoint;

  const switchMode = (m) => {
    setMode(m);
    setTool(defaultTool(m, ctx));
    setDraft(null);setCursor(null);
    if (m === 'ventil') setTarget('ventilation');
  };

  // efface le tracé de conduit en cours si on quitte l'outil Conduit
  React.useEffect(() => {
    if (tool !== 'conduit') {setDraft(null);setCursor(null);}
  }, [tool]);

  // raccourcis clavier
  React.useEffect(() => {
    const onKey = (e) => {
      if (e.target && /INPUT|TEXTAREA|SELECT/.test(e.target.tagName)) return;
      if (e.key === '?') {setShortcutsOpen((o) => !o);} else
      if (e.key === 'i' || e.key === 'I') {setAiOpen((o) => !o);} else
      if (e.key === 'Escape') {setShortcutsOpen(false);setAiOpen(false);setDraft(null);setCursor(null);} else
      if (e.key === 'Enter') {if (draft) finishConduit();} else
      {const map = mode === 'dessin' ?
        { v: 'select', l: 'dline', r: 'rect', p: 'poly', c: 'circ', d: 'dim', t: 'text' } :
        mode === 'ventil' ?
        { v: 'select', c: 'conduit', t: 'trappe', g: 'grille', d: 'vrc', p: 'prise', s: 'sortie' } :
        { v: 'select', l: 'ligne', s: 'surface', o: 'ouvert', e: 'echelle', p: 'pan', a: 'angle' };
        if (map[e.key.toLowerCase()]) setTool(map[e.key.toLowerCase()]);}
    };
    window.addEventListener('keydown', onKey);
    return () => window.removeEventListener('keydown', onKey);
  }, [mode, draft]);

  // ajoute une forme / composant relevé — persiste dans l'état partagé
  const addShape = (extra = {}) => {
    if (!setMeasures) return;
    const tMod = zModById(extra.target || target);
    const isLine = extra.kind ? extra.kind === 'ligne' : tool === 'ligne' || tool === 'dline' || ctx === 'fondation';
    const tgt = extra.target || target;
    const n = measures.filter((m) => m.target === tgt).length + 1;
    setMeasures((ms) => [...ms, {
      id: 'me' + Date.now() + Math.round(Math.random() * 999),
      name: extra.name || `${tMod ? tMod.name : 'Forme'} ${n}`,
      kind: extra.kind || (isLine ? 'ligne' : 'surface'),
      val: extra.val != null ? extra.val : 0,
      unit: extra.unit || (isLine ? 'pi lin.' : 'pi²'),
      target: tgt, page: page + 1, source: extra.source,
      comp: extra.comp, vx: extra.vx, vy: extra.vy, points: extra.points }]);
  };
  const delShape = (id) => setMeasures && setMeasures((ms) => ms.filter((m) => m.id !== id));

  // ── tracé de ventilation directement sur le plan ──
  const sheetXY = (e) => {
    const r = e.currentTarget.getBoundingClientRect();
    return {
      x: +((e.clientX - r.left) / r.width * 100).toFixed(1),
      y: +((e.clientY - r.top) / r.height * 100).toFixed(1) };
  };
  const placePoint = (compId, x, y) => {
    const c = VENT_BY_ID[compId];
    const n = measures.filter((m) => m.comp === compId).length + 1;
    addShape({ kind: 'composant', comp: compId, name: `${c.full} ${n}`, unit: 'u', val: 1, target: 'ventilation', vx: x, vy: y });
  };
  const finishConduit = () => {
    setDraft((d) => {
      if (!d) return null;
      const pts = d.points.filter((p, i, a) => i === 0 || Math.hypot(p.x - a[i - 1].x, p.y - a[i - 1].y) > 1.2);
      if (pts.length >= 2) {
        const n = measures.filter((m) => m.comp === 'conduit').length + 1;
        addShape({ kind: 'composant', comp: 'conduit', name: `Conduit ${n}`, unit: 'pi lin.', val: conduitLen(pts), target: 'ventilation', points: pts });
      }
      return null;
    });
    setCursor(null);
  };
  const onSheetClick = (e) => {
    const { x, y } = sheetXY(e);
    if (isVentPoint) placePoint(tool, x, y);else
    if (isVentLine) setDraft((d) => d ? { points: [...d.points, { x, y }] } : { points: [{ x, y }] });
  };
  const onSheetMove = (e) => {if (isVentLine && draft) setCursor(sheetXY(e));};
  const onSheetDbl = () => {if (isVentLine && draft && draft.points.length >= 2) finishConduit();};

  // assistant IA
  const runAiAction = (a) => {
    setAiMsgs((m) => [...m, { from: 'user', text: a.label }, { from: 'ia', text: a.reply }]);
    if (a.suggest && a.suggest.length) setAiSug((s) => [...s, ...a.suggest.map((x) => ({ ...x, _k: 'k' + Date.now() + Math.random() }))]);
  };
  const acceptSug = (s) => {addShape({ ...s, source: 'ia' });setAiSug((list) => list.filter((x) => x._k !== s._k));};
  const ignoreSug = (s) => setAiSug((list) => list.filter((x) => x._k !== s._k));

  return (
    <div className="mz fade-in">
      <div className="mz-top">
        <div className="mz-brand"><span className="lg">G</span> GO·PASSIF</div>
        <span className="mz-tag">Mesure sur plan</span>
        {ctxMod && <span className="mz-ctx"><ZIcon n="link" style={{ width: 13, height: 13 }} /> {ctxMod.name}</span>}
        <div className="mz-mode">
          <button className={mode === 'releve' ? 'on' : ''} onClick={() => switchMode('releve')}><ZIcon n="ruler" style={{ width: 13, height: 13 }} /> Relevé</button>
          <button className={mode === 'dessin' ? 'on' : ''} onClick={() => switchMode('dessin')}><ZIcon n="square" style={{ width: 13, height: 13 }} /> Dessin</button>
          <button className={mode === 'ventil' ? 'on' : ''} onClick={() => switchMode('ventil')}><ZIcon n="fan" style={{ width: 13, height: 13 }} /> Ventil.</button>
          <button disabled title="Modélisation 3D — à venir"><ZIcon n="cube" style={{ width: 13, height: 13 }} /> 3D <span className="soon">à venir</span></button>
        </div>
        <div className="mz-pages">
          {MZ_PAGES.map((p, i) => {
            const cnt = measures.filter((m) => m.page === i + 1).length;
            return (
              <button key={i} className={'mz-page' + (i === page ? ' on' : '')} onClick={() => setPage(i)}>
                <span className="pg-dot" /> {p.n}
                {cnt > 0 && <span className="pg-ct">{cnt}</span>}
              </button>);

          })}
        </div>
        <div className="mz-io">
          <label className="btn accent mz-io-btn" title="Importer un plan (image PNG/JPG)">
            <ZIcon n="upload" style={{ width: 13, height: 13 }} /> {hasPlan ? 'Changer de plan' : 'Importer un plan'}
            <input type="file" accept="image/*,application/pdf" style={{ display: 'none' }}
              onChange={(e) => { if (e.target.files[0]) { onPlanFile(e.target.files[0]); e.target.value = ''; } }} />
          </label>
          {onExport &&
            <button className="btn ghost mz-io-btn" onClick={onExport} title="Exporter les relevés en JSON">
              <ZIcon n="file" style={{ width: 13, height: 13 }} /> Exporter
            </button>}
          {onImport &&
            <label className="btn ghost mz-io-btn" title="Importer des relevés depuis un fichier JSON">
              <ZIcon n="upload" style={{ width: 13, height: 13 }} /> Importer
              <input type="file" accept=".json" style={{ display: 'none' }}
                onChange={(e) => { if (e.target.files[0]) { onImport(e.target.files[0]); e.target.value = ''; } }} />
            </label>}
        </div>
        <button className="btn accent mz-close" onClick={onClose}><ZIcon n="check" /> {ctxMod ? 'Retour au module' : 'Vers le calculateur'}</button>
      </div>

      <div className="mz-rail">
        <div className="mz-rail-tools">
          {tools.map((t) =>
          <button key={t.id} className={'mz-tool' + (tool === t.id ? ' on' : '')} onClick={() => setTool(t.id)}>
              <ZIcon n={t.n} /><span>{t.l}</span>
            </button>
          )}
        </div>
        <div className="mz-rail-foot">
          <button className={'mz-tool mz-tool-ia' + (aiOpen ? ' on' : '')} onClick={() => setAiOpen((o) => !o)} title="Assistant IA (I)">
            <ZIcon n="sparkles" /><span>IA</span>
          </button>
          <button className={'mz-tool' + (shortcutsOpen ? ' on' : '')} onClick={() => setShortcutsOpen((o) => !o)} title="Raccourcis (?)">
            <ZIcon n="keyboard" /><span>Touches</span>
          </button>
        </div>
      </div>

      <div className="mz-canvas" ref={canvasRef}>
        {hasPlan ?
        <div className="mz-sheet" style={{ position: 'relative', width: imgNat.w * zoom / 100 + 'px', maxWidth: 'none', background: '#fff' }}>
          <img ref={imgRef} src={planSrc} alt="plan importé" draggable={false}
            style={{ display: 'block', width: '100%', height: 'auto', userSelect: 'none' }} />
          {/* overlay de dessin en coordonnées naturelles de l'image */}
          <svg viewBox={`0 0 ${imgNat.w} ${imgNat.h}`} preserveAspectRatio="none"
            style={{ position: 'absolute', inset: 0, width: '100%', height: '100%',
              cursor: mode === 'releve' && (tool === 'echelle' || tool === 'ligne' || tool === 'surface') ? 'crosshair' : 'default' }}
            onClick={mode === 'releve' ? onPlanClick : undefined}
            onMouseMove={mode === 'releve' ? onPlanMove : undefined}
            onDoubleClick={mode === 'releve' ? finishDraw : undefined}>
            {/* mesures déjà relevées sur cette page */}
            {measures.filter((m) => m.page === page + 1 && m.planPts && m.planPts.length).map((m) => {
              const pts = m.planPts.map((p) => `${p.x},${p.y}`).join(' ');
              return m.kind === 'surface' ?
                <polygon key={m.id} points={pts} fill="rgba(58,107,74,.16)" stroke="#3a6b4a" strokeWidth="2" vectorEffect="non-scaling-stroke" /> :
                <polyline key={m.id} points={pts} fill="none" stroke="#3a6b4a" strokeWidth="2" vectorEffect="non-scaling-stroke" />;
            })}
            {/* tracé en cours */}
            {dpts.length > 0 && (() => {
              const live = dhover ? [...dpts, dhover] : dpts;
              const pts = live.map((p) => `${p.x},${p.y}`).join(' ');
              return tool === 'surface' ?
                <polygon points={pts} fill="rgba(245,158,11,.15)" stroke="#f59e0b" strokeWidth="2" strokeDasharray="6 4" vectorEffect="non-scaling-stroke" /> :
                <polyline points={pts} fill="none" stroke="#f59e0b" strokeWidth="2" strokeDasharray="6 4" vectorEffect="non-scaling-stroke" />;
            })()}
            {dpts.map((p, i) => <circle key={i} cx={p.x} cy={p.y} r="4" fill="#f59e0b" vectorEffect="non-scaling-stroke" />)}
            {scalePend && <line x1={scalePend.a.x} y1={scalePend.a.y} x2={scalePend.b.x} y2={scalePend.b.y} stroke="#fbbf24" strokeWidth="2.5" vectorEffect="non-scaling-stroke" />}
          </svg>
          <VentOverlay measures={measures} page={page + 1} draft={draft} cursor={cursor} />
          {ventActive &&
          <div className="mz-clicklayer" onClick={onSheetClick} onMouseMove={onSheetMove} onDoubleClick={onSheetDbl}
          style={{ cursor: isVentLine ? 'crosshair' : 'copy' }} />}
        </div> :
        <div className="mz-empty-plan" style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', gap: 14, height: '100%', color: '#9aa3ad', textAlign: 'center', padding: 40 }}>
          <ZIcon n="upload" style={{ width: 40, height: 40, opacity: .6 }} />
          <div style={{ fontSize: 16, fontWeight: 600, color: '#cfd6df' }}>Aucun plan importé</div>
          <div style={{ fontSize: 13, maxWidth: 360, lineHeight: 1.5 }}>Importez un plan (image PNG/JPG), calez l’échelle sur une cote connue, puis relevez vos surfaces et longueurs réelles.</div>
          <label className="btn accent" style={{ marginTop: 6 }}><ZIcon n="upload" style={{ width: 14, height: 14 }} /> Importer un plan
            <input type="file" accept="image/*,application/pdf" style={{ display: 'none' }}
              onChange={(e) => { if (e.target.files[0]) { onPlanFile(e.target.files[0]); e.target.value = ''; } }} />
          </label>
        </div>}

        {mode === 'ventil' && hasPlan &&
        <div className="mz-draw-note"><ZIcon n="fan" /> Mode ventilation — choisissez un outil à gauche, puis tracez sur le plan</div>}

        {hasPlan &&
        <div className="mz-hint">
          {!pxPerFt ?
          <><b>Échelle requise</b> — outil <b>Échelle</b> : cliquez 2 points sur une cote connue, puis entrez sa longueur réelle.</> :
          mode === 'ventil' && isVentLine ?
          <>Cliquez pour router le conduit. <b>Double-clic</b> pour terminer.</> :
          tool === 'surface' ?
          <>Cliquez chaque sommet de la surface. <b>Double-clic</b> pour fermer et calculer l’aire.</> :
          tool === 'ligne' ?
          <>Cliquez chaque point de la ligne. <b>Double-clic</b> pour terminer et calculer la longueur.</> :
          <>Échelle calée : <b>{pxPerFt.toFixed(1)} px/pi</b>. Choisissez Ligne ou Surface à gauche.</>}
        </div>}
        <div className="mz-zoom">
          <button onClick={() => setZoom((z) => Math.max(8, z - 4))}>−</button>
          <span>{zoom}%</span>
          <button onClick={() => setZoom((z) => Math.min(200, z + 4))}>+</button>
        </div>

        {aiOpen && <AIPanel
          actions={MZ_AI_ACTIONS} msgs={aiMsgs} sug={aiSug}
          onAction={runAiAction} onAccept={acceptSug} onIgnore={ignoreSug}
          onClose={() => setAiOpen(false)} zModById={zModById} containerRef={canvasRef} />}
      </div>

      <div className="mz-panel">
        <div className="mzp-lbl">Plan actif</div>
        <input className="mzp-field" defaultValue={mode === 'dessin' ? 'Esquisse — ' + MZ_PAGES[page].n : 'Page ' + (page + 1)} style={{ marginBottom: 16 }} />

        <div className="mzp-lbl">Échelle</div>
        <div className="mzp-scale" style={{ marginBottom: 16, color: pxPerFt ? undefined : '#cf8a2c' }}>
          <ZIcon n={pxPerFt ? 'check' : 'scale'} style={{ width: 14, height: 14 }} />{' '}
          {!hasPlan ? 'Importez un plan' : pxPerFt ? `Calée — ${pxPerFt.toFixed(1)} px/pi` : 'À caler — outil Échelle'}
        </div>

        {mode === 'ventil' ?
        <div className="mz-vent" style={{ marginTop: 0, paddingTop: 0, borderTop: 'none' }}>
          <div className="mz-vent-head"><ZIcon n="fan" /> <span>Réseau de ventilation</span></div>
          <div className="mz-vent-hint">Chaque outil de gauche trace un élément réel sur le plan. Son libellé est une propriété de l’objet, pas un texte affiché au centre.</div>
          <div className="mz-vent-active">
            <span className="ic"><ZIcon n={(VENT_BY_ID[tool] || { n: 'cursor' }).n} /></span>
            <div><b>{tool === 'select' ? 'Sélection' : (VENT_BY_ID[tool] || {}).full}</b>
              <span>{isVentLine ? 'Cliquez pour router, double-clic pour finir' : isVentPoint ? 'Cliquez pour placer sur le plan' : 'Choisissez un outil de tracé'}</span></div>
          </div>
          {isVentLine && draft &&
          <button className="mz-add" onClick={finishConduit}>
            <ZIcon n="check" style={{ width: 15, height: 15 }} /> Terminer le conduit ({draft.points.length} pt{draft.points.length > 1 ? 's' : ''})
          </button>}
        </div> :
        <>
          <div className="mzp-lbl">Module cible de la forme</div>
          <select className="mzp-field" value={target} onChange={(e) => setTarget(e.target.value)} style={{ marginBottom: 10 }}>
            {ZMODS.map((m) => <option key={m.id} value={m.id}>{m.name}</option>)}
          </select>
          {/* Tracé en cours sur le plan → bouton Terminer */}
          {hasPlan && (tool === 'ligne' || tool === 'surface') && dpts.length > 0 &&
          <button className="mz-add" onClick={finishDraw}
            disabled={tool === 'surface' ? dpts.length < 3 : dpts.length < 2}>
            <ZIcon n="check" style={{ width: 15, height: 15 }} /> Terminer {tool === 'surface' ? 'la surface' : 'la ligne'} ({dpts.length} pt{dpts.length > 1 ? 's' : ''})
          </button>}
          {/* Saisie manuelle (sans plan, ou complément) — valeur réelle saisie, jamais aléatoire */}
          <button className="mz-add" style={{ marginTop: hasPlan && dpts.length ? 8 : 0, background: hasPlan ? 'var(--surface-2, #1b222c)' : undefined }}
            onClick={() => {
              const isLine = ctx === 'fondation';
              const raw = window.prompt(`Valeur ${isLine ? 'de longueur (pi lin.)' : 'de surface (pi²)'} à saisir manuellement :`, '');
              if (raw == null) return;
              const v = parseFloat(String(raw).replace(',', '.'));
              if (!(v > 0)) { alert('Entrez un nombre positif.'); return; }
              addShape({ kind: isLine ? 'ligne' : 'surface', val: +v.toFixed(isLine ? 1 : 0), unit: isLine ? 'pi lin.' : 'pi²', source: 'manuel' });
            }}>
            <ZIcon n="plus" style={{ width: 15, height: 15 }} /> Saisir une mesure manuelle
          </button>
        </>}

        <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', margin: '18px 0 12px' }}>
          <span className="mzp-lbl" style={{ margin: 0 }}>Mesures du projet</span>
          <span style={{ fontFamily: 'var(--font-mono)', fontSize: 12, color: '#67717f' }}>{measures.length}</span>
        </div>
        {measures.map((m) => {
          const mMod = zModById(m.target);
          const dim = ctx && m.target !== ctx;
          const isComp = m.kind === 'composant';
          return (
            <div className={'mz-meas' + (dim ? ' dim' : '')} key={m.id}>
              <div className="mz-meas-h">
                <span className="d" style={isComp ? { background: '#9af0c0' } : null} /><b>{m.name}</b>
                <button className="mz-meas-x" onClick={() => delShape(m.id)}><ZIcon n="trash" style={{ width: 12, height: 12 }} /></button>
              </div>
              <div className="mz-meas-v">{isComp ? '1 composant' : `${zFmt(m.val, m.kind === 'ligne' ? 1 : 0)} ${m.unit}`}</div>
              <div className="mz-meas-tags">
                <span className="mz-meas-t">{mMod ? mMod.name : m.target}</span>
                {m.source === 'ia' && <span className="mz-meas-ia"><ZIcon n="sparkles" style={{ width: 10, height: 10 }} /> IA</span>}
                {m.source === 'dessin' && <span className="mz-meas-ia" style={{ background: 'color-mix(in srgb,var(--accent) 26%,#11151b)', color: '#a9c2ff' }}><ZIcon n="square" style={{ width: 10, height: 10 }} /> Dessin</span>}
              </div>
              <div className="mz-meas-ok"><ZIcon n="check" style={{ width: 12, height: 12 }} /> liée · page {m.page}</div>
            </div>);

        })}
      </div>

      {shortcutsOpen && <ShortcutsModal onClose={() => setShortcutsOpen(false)} />}
    </div>);

}

// ── Assistant IA — flottant, déplaçable, redimensionnable, ancrable ──
const AI_GEO_KEY = 'gp_ai_panel_geo';
const AI_ANCHORS = ['tl', 'tc', 'tr', 'ml', 'cc', 'mr', 'bl', 'bc', 'br'];

function AIPanel({ actions, msgs, sug, onAction, onAccept, onIgnore, onClose, zModById, containerRef }) {
  const [input, setInput] = React.useState('');
  const [picker, setPicker] = React.useState(false);
  const [rz, setRz] = React.useState(0); // recompute trigger on container resize
  const [geo, setGeo] = React.useState(() => {
    try {const s = JSON.parse(localStorage.getItem(AI_GEO_KEY));if (s && typeof s.w === 'number') return s;} catch (e) {}
    return { anchor: 'tr', x: null, y: null, w: 330, h: 560 };
  });
  const bodyRef = React.useRef(null);

  React.useEffect(() => {if (bodyRef.current) bodyRef.current.scrollTop = bodyRef.current.scrollHeight;}, [msgs.length, sug.length]);
  React.useEffect(() => {try {localStorage.setItem(AI_GEO_KEY, JSON.stringify(geo));} catch (e) {}}, [geo]);

  const cont = () => containerRef && containerRef.current;
  const M = 14;
  const xyForAnchor = (anchor, w, h) => {
    const p = cont();if (!p) return { x: M, y: M };
    const cw = p.clientWidth,ch = p.clientHeight;
    const xs = { l: M, c: Math.max(M, (cw - w) / 2), r: Math.max(M, cw - w - M) };
    const ys = { t: M, c: Math.max(M, (ch - h) / 2), b: Math.max(M, ch - h - M) };
    const col = { tl: 'l', tc: 'c', tr: 'r', ml: 'l', cc: 'c', mr: 'r', bl: 'l', bc: 'c', br: 'r' }[anchor];
    const row = { tl: 't', tc: 't', tr: 't', ml: 'c', cc: 'c', mr: 'c', bl: 'b', bc: 'b', br: 'b' }[anchor];
    return { x: xs[col], y: ys[row] };
  };

  // (re)positionne quand l'ancrage / la taille / le conteneur changent
  React.useLayoutEffect(() => {
    const p = cont();if (!p) return;
    const cw = p.clientWidth,ch = p.clientHeight;
    const w = Math.min(geo.w, cw - 2 * M),h = Math.min(geo.h, ch - 2 * M);
    let x, y;
    if (geo.anchor !== 'free') {({ x, y } = xyForAnchor(geo.anchor, w, h));} else
    {x = Math.max(M, Math.min(geo.x == null ? M : geo.x, cw - w - M));
      y = Math.max(M, Math.min(geo.y == null ? M : geo.y, ch - h - M));}
    setGeo((g) => g.x === x && g.y === y && g.w === w && g.h === h ? g : { ...g, x, y, w, h });
  }, [geo.anchor, geo.w, geo.h, rz]);

  React.useEffect(() => {
    const onR = () => setRz((n) => n + 1);
    window.addEventListener('resize', onR);
    return () => window.removeEventListener('resize', onR);
  }, []);

  const startDrag = (e) => {
    if (e.target.closest('button')) return;
    e.preventDefault();
    const p = cont();if (!p) return;
    const sx = e.clientX,sy = e.clientY,ox = geo.x || M,oy = geo.y || M;
    const move = (ev) => {
      const cw = p.clientWidth,ch = p.clientHeight;
      let nx = Math.max(M, Math.min(ox + (ev.clientX - sx), cw - geo.w - M));
      let ny = Math.max(M, Math.min(oy + (ev.clientY - sy), ch - geo.h - M));
      setGeo((g) => ({ ...g, anchor: 'free', x: nx, y: ny }));
    };
    const up = () => {window.removeEventListener('pointermove', move);window.removeEventListener('pointerup', up);};
    window.addEventListener('pointermove', move);window.addEventListener('pointerup', up);
  };

  const startResize = (dir) => (e) => {
    e.preventDefault();e.stopPropagation();
    const p = cont();if (!p) return;
    const sx = e.clientX,sy = e.clientY,ow = geo.w,oh = geo.h,ox = geo.x || M,oy = geo.y || M;
    const move = (ev) => {
      const cw = p.clientWidth,ch = p.clientHeight;
      const dx = ev.clientX - sx,dy = ev.clientY - sy;
      let nx = ox,ny = oy,nw = ow,nh = oh;
      if (dir.includes('e')) nw = Math.max(296, Math.min(ow + dx, cw - ox - M));
      if (dir.includes('s')) nh = Math.max(300, Math.min(oh + dy, ch - oy - M));
      if (dir.includes('w')) {const x = Math.min(Math.max(M, ox + dx), ox + ow - 296);nw = ow + (ox - x);nx = x;}
      if (dir.includes('n')) {const y = Math.min(Math.max(M, oy + dy), oy + oh - 300);nh = oh + (oy - y);ny = y;}
      setGeo((g) => ({ ...g, anchor: 'free', x: nx, y: ny, w: nw, h: nh }));
    };
    const up = () => {window.removeEventListener('pointermove', move);window.removeEventListener('pointerup', up);};
    window.addEventListener('pointermove', move);window.addEventListener('pointerup', up);
  };

  const style = { left: geo.x == null ? 14 : geo.x, top: geo.y == null ? 14 : geo.y, width: geo.w, height: geo.h };

  return (
    <div className="mz-ai" style={style}>
      <div className="mz-ai-head" onPointerDown={startDrag}>
        <span className="mz-ai-title"><ZIcon n="sparkles" style={{ width: 16, height: 16 }} /> Assistant IA</span>
        <div className="mz-ai-tools">
          <button className={'mz-ai-anchor' + (picker ? ' on' : '')} title="Ancrage & disposition" onClick={() => setPicker((o) => !o)}><ZIcon n="grid" style={{ width: 15, height: 15 }} /></button>
          <button className="mz-ai-x" onClick={onClose}>×</button>
        </div>
      </div>

      {picker &&
      <div className="mz-ai-picker">
        <div className="mz-ai-pick-lbl">Ancrer le panneau</div>
        <div className="mz-ai-pick-grid">
          {AI_ANCHORS.map((a) =>
          <button key={a} className={'mz-ai-pick' + (geo.anchor === a ? ' on' : '')}
          onClick={() => {setGeo((g) => ({ ...g, anchor: a }));setPicker(false);}}><i /></button>)}
        </div>
      </div>}

      <div className="mz-ai-actions">
        {actions.map((a) =>
        <button key={a.id} className="mz-ai-chip" onClick={() => onAction(a)}>
            <ZIcon n={a.icon} style={{ width: 13, height: 13 }} /> {a.label}
          </button>)}
      </div>
      <div className="mz-ai-body" ref={bodyRef}>
        {msgs.map((m, i) =>
        <div key={i} className={'mz-ai-msg ' + m.from}>{m.text}</div>)}
        {sug.length > 0 &&
        <div className="mz-ai-sug-wrap">
            <div className="mz-ai-sug-lbl">Suggestions détectées</div>
            {sug.map((s) => {
            const mod = zModById(s.target);
            return (
              <div className="mz-ai-sug" key={s._k}>
                  <div className="mz-ai-sug-tx">
                    <div className="mz-ai-sug-name">{s.name}</div>
                    <div className="mz-ai-sug-meta">{s.val} {s.unit} · {mod ? mod.name : s.target}</div>
                  </div>
                  <button className="mz-ai-accept" onClick={() => onAccept(s)}><ZIcon n="check" style={{ width: 12, height: 12 }} /></button>
                  <button className="mz-ai-ignore" onClick={() => onIgnore(s)}>×</button>
                </div>);
          })}
          </div>}
      </div>
      <form className="mz-ai-input" onSubmit={(e) => {e.preventDefault();if (!input.trim()) return;
        onAction({ id: 'q', label: input, reply: 'Bonne question — dans la version connectée, j’analyserai le plan pour vous répondre. (démo)', suggest: [] });setInput('');}}>
        <input value={input} onChange={(e) => setInput(e.target.value)} placeholder="Demander à l’assistant…" />
        <button type="submit"><ZIcon n="chev" style={{ width: 15, height: 15 }} /></button>
      </form>

      <div className="mz-ai-rz mz-ai-rz-e" onPointerDown={startResize('e')} title="Redimensionner" />
      <div className="mz-ai-rz mz-ai-rz-s" onPointerDown={startResize('s')} title="Redimensionner" />
      <div className="mz-ai-rz mz-ai-rz-w" onPointerDown={startResize('w')} title="Redimensionner" />
      <div className="mz-ai-rz mz-ai-rz-sw" onPointerDown={startResize('sw')} title="Redimensionner" />
      <div className="mz-ai-resize" onPointerDown={startResize('se')} title="Redimensionner" />
    </div>);
}

// ── Modale des raccourcis ─────────────────────────────────────
function ShortcutsModal({ onClose }) {
  return (
    <div className="mz-kbd-backdrop" onClick={onClose}>
      <div className="mz-kbd" onClick={(e) => e.stopPropagation()}>
        <div className="mz-kbd-head">
          <span><ZIcon n="keyboard" style={{ width: 18, height: 18 }} /> Raccourcis & actions rapides</span>
          <button className="mz-ai-x" onClick={onClose}>×</button>
        </div>
        <div className="mz-kbd-grid">
          {MZ_SHORTCUTS.map((g) =>
          <div className="mz-kbd-group" key={g.group}>
              <div className="mz-kbd-glbl">{g.group}</div>
              {g.items.map(([k, label], i) =>
            <div className="mz-kbd-row" key={i}>
                  <span className="mz-kbd-lbl">{label}</span>
                  <span className="mz-kbd-keys">{k.split(' ').map((part, j) =>
                part === '+' || part === '/' ? <span key={j} className="mz-kbd-plus">{part}</span> : <kbd key={j}>{part}</kbd>)}</span>
                </div>)}
            </div>)}
        </div>
        <div className="mz-kbd-foot">Astuce : appuyez sur <kbd>?</kbd> à tout moment pour ouvrir ce panneau.</div>
      </div>
    </div>);
}

// abstract drafting sketch (no literal building art — schematic plan blocks)
function PlanSketch() {
  return (
    <svg viewBox="0 0 600 440" width="100%" height="100%" preserveAspectRatio="xMidYMid meet"
    style={{ display: 'block' }}>
      <rect width="600" height="440" fill="#f7f7f4" />
      {Array.from({ length: 24 }).map((_, i) => <line key={'v' + i} x1={i * 26} y1="0" x2={i * 26} y2="440" stroke="#e7e7e1" strokeWidth="1" />)}
      {Array.from({ length: 18 }).map((_, i) => <line key={'h' + i} x1="0" y1={i * 26} x2="600" y2={i * 26} stroke="#e7e7e1" strokeWidth="1" />)}
      <g fill="#f7d7e3" stroke="#d98aa6" strokeWidth="1.4">
        <rect x="150" y="60" width="120" height="320" />
        <rect x="290" y="60" width="120" height="320" />
      </g>
      <line x1="150" y1="220" x2="410" y2="220" stroke="#d98aa6" strokeWidth="1.4" strokeDasharray="5 4" />
      <g stroke="#f7941d" strokeWidth="2.4" fill="#f7941d">
        <line x1="430" y1="130" x2="540" y2="200" />
        <circle cx="430" cy="130" r="4" />
        <circle cx="540" cy="200" r="4" />
      </g>
      <g fill="#9aa0a6" fontFamily="monospace" fontSize="11">
        <text x="200" y="46">42′-6″</text>
        <text x="330" y="46">42′-6″</text>
        <text x="468" y="150">24′-8″</text>
      </g>
      <text x="470" y="410" fill="#7a8088" fontFamily="monospace" fontSize="13" fontWeight="700">PLAN — TOITURE</text>
    </svg>);

}

// esquisse en cours de dessin (création de plan sans document de départ)
function DrawSketch() {
  return (
    <svg viewBox="0 0 600 440" width="100%" height="100%" preserveAspectRatio="xMidYMid meet" style={{ display: 'block' }}>
      <rect width="600" height="440" fill="#fbfbf9" />
      {Array.from({ length: 24 }).map((_, i) => <line key={'v' + i} x1={i * 26} y1="0" x2={i * 26} y2="440" stroke="#ececec" strokeWidth="1" />)}
      {Array.from({ length: 18 }).map((_, i) => <line key={'h' + i} x1="0" y1={i * 26} x2="600" y2={i * 26} stroke="#ececec" strokeWidth="1" />)}
      {/* contour tracé par l'utilisateur */}
      <path d="M120 110 L430 110 L430 230 L330 230 L330 340 L120 340 Z" fill="rgba(47,107,237,.07)" stroke="#2f6bed" strokeWidth="2.4" strokeLinejoin="round" />
      {/* segment en cours (pointillé vers le curseur) */}
      <line x1="330" y1="340" x2="430" y2="300" stroke="#2f6bed" strokeWidth="1.6" strokeDasharray="4 4" />
      {/* nœuds */}
      <g fill="#fff" stroke="#2f6bed" strokeWidth="2">
        {[[120, 110], [430, 110], [430, 230], [330, 230], [330, 340], [120, 340]].map(([x, y], i) =>
        <rect key={i} x={x - 4} y={y - 4} width="8" height="8" />)}
      </g>
      <circle cx="430" cy="300" r="5" fill="#2f6bed" />
      {/* cotes esquissées */}
      <g fill="#9aa0a6" fontFamily="monospace" fontSize="11">
        <text x="255" y="100">— à coter —</text>
        <text x="95" y="230" transform="rotate(-90 95 230)">— à coter —</text>
      </g>
      <text x="380" y="410" fill="#9aa0a6" fontFamily="monospace" fontSize="13" fontWeight="700">ESQUISSE — RDC</text>
    </svg>);

}

// ── Réseau de ventilation rendu sur le plan (conduits tracés + symboles) ──
function VentOverlay({ measures, page, draft, cursor }) {
  const items = measures.filter((m) => m.kind === 'composant' && m.page === page && m.target === 'ventilation');
  const conduits = items.filter((m) => m.comp === 'conduit' && m.points);
  const points = items.filter((m) => m.vx != null && m.comp !== 'conduit');
  const draftPts = draft ? [...draft.points, ...(cursor ? [cursor] : [])] : [];
  return (
    <>
      <svg className="mz-vlayer" viewBox="0 0 100 100" preserveAspectRatio="none">
        {conduits.map((c) =>
        <polyline key={c.id} className="mz-vline" vectorEffect="non-scaling-stroke"
        points={c.points.map((p) => `${p.x},${p.y}`).join(' ')} />)}
        {draftPts.length > 1 &&
        <polyline className="mz-vline draft" vectorEffect="non-scaling-stroke"
        points={draftPts.map((p) => `${p.x},${p.y}`).join(' ')} />}
      </svg>

      {conduits.map((c) => {
        const mid = c.points[Math.floor((c.points.length - 1) / 2)];
        return (
          <React.Fragment key={c.id}>
            {c.points.map((p, i) => <span key={i} className="mz-vnode" style={{ left: p.x + '%', top: p.y + '%' }} />)}
            <span className="mz-vlabel conduit" style={{ left: mid.x + '%', top: mid.y + '%' }}>{c.name} · {zFmt(c.val, 1)} pi</span>
          </React.Fragment>);

      })}
      {draft && draft.points.map((p, i) => <span key={'d' + i} className="mz-vnode draft" style={{ left: p.x + '%', top: p.y + '%' }} />)}

      {points.map((p) => {
        const c = VENT_BY_ID[p.comp] || { n: 'box', l: p.name };
        return (
          <span className="mz-vpt" key={p.id} style={{ left: p.vx + '%', top: p.vy + '%' }} title={p.name}>
            <span className="mz-vpt-ic"><ZIcon n={c.n} /></span>
            <span className="mz-vpt-lbl">{c.l}</span>
          </span>);

      })}
    </>);

}

Object.assign(window, { Mesure });