/**
 * Fichier  : assets/js/pages/plans.js
 * Auteur   : KREMER Régis
 * Version  : 2.6.70
 * Date     : 2026-07-11
 * Description : JS — Plans d’emplacements / création avec unités, éditeur et symboles professionnels
 *
 * Changelog :
 *   v2.6.25 (2026-07-09) — Création de la page socle : liste, CRUD plan et consultation.
 *   v2.6.26 (2026-07-09) — PLAN 2 : éditeur SVG avec grille, zoom, pan, création, déplacement, redimensionnement et sauvegarde.
 *   v2.6.27 (2026-07-09) — PLAN 3 : bibliothèque composants, rendus SVG enrichis, multi-sélection, alignements et tailles rapides.
 *   v2.6.28 (2026-07-09) — PLAN 4 : rattachement aux emplacements existants, recherche et panneau métier.
 *   v2.6.29 (2026-07-09) — PLAN 5 : ouverture depuis une fiche produit, zoom automatique, surbrillance et fiche de localisation.
 *   v2.6.30 (2026-07-09) — PLAN 6 : impression/export PDF navigateur, légende, surbrillance imprimable et responsive final.
 *   v2.6.32 (2026-07-09) — PLANS UX1 : accueil guidé, consultation simple, mode édition admin et libellés accessibles.
 *   v2.6.33 (2026-07-09) — PLANS UX2 : page édition dédiée, drag & drop des composants et placement au clic fiabilisé.
 *   v2.6.34 (2026-07-09) — PLANS UX3 : création pleine page, formulaire dédié et suppression des interférences d’édition.
 *   v2.6.36 (2026-07-09) — Correctif : drop/clic de composants fiabilisé, rotation rapide et UX édition resserrée.
 *   v2.6.37 (2026-07-09) — Correctif : déplacement direct des objets, rendu SVG plus lisible, plan sans arrondi fort et thème sombre.
 *   v2.6.38 (2026-07-09) — PLANS DESIGN1 : objets premium, sélection fluide en édition, libellés longueur/largeur et légende allégée.
 *   v2.6.40 (2026-07-09) — PLANS DESIGN1 : rendu architectural renforcé, textes adaptatifs et consultation produit finie.
 *   v2.6.41 (2026-07-09) — PLANS EDITOR2 : zoom ancré, pan fluide, sélection avancée, guides et raccourcis clavier.
 *   v2.6.42 (2026-07-09) — PLANS LOCALISATION2 : ouverture produit en lecture forcée, impression enrichie et plan fini.
 *   v2.6.43 (2026-07-09) — PLANS PRINT3 : cartouche premium, légende compacte, orientation auto et impression localisation/PDF.
 *   v2.6.44 (2026-07-09) — PLANS ARCHITECTE1 : modes de conception, composants murs/ouvertures et rendu plan 2D professionnel.
 *   v2.6.45 (2026-07-09) — PLANS WALLS2 : dessin de murs par points, accrochage des ouvertures et détection visuelle des pièces fermées.
 *   v2.6.46 (2026-07-09) — PLANS ROOMS3 : murs chaînés, fermeture de pièce, surfaces, cotes et sélection de structure.
 *   v2.6.47 (2026-07-09) — PLANS OPENINGS4 : portes/fenêtres réelles, découpe des murs, sens d’ouverture et accroche renforcée.
 *   v2.6.51 (2026-07-09) — PLANS EDITOR PIXEL1 : shell plein écran pro, toolbar haute, bibliothèque gauche et inspecteur droit.
 *   v2.6.52 (2026-07-09) — PLANS RAIL FIX1 : recherche bibliothèque, filtre fonctionnel et catégories repliables.
 *   v2.6.53 (2026-07-09) — PLANS OPENINGS SYMBOLS1 : symboles placés pro pour portes, fenêtres et passages.
 *   v2.6.54 (2026-07-09) — PLANS OPENINGS SYMBOLS1 FIX : rendu réel des objets ouvrants dans le canvas.
 *   v2.6.55 (2026-07-09) — PLANS OPENINGS SYMBOLS2 : finition architecturale des ouvrants placés.
 *   v2.6.56 (2026-07-09) — PLANS OPENINGS SYMBOLS3 : gabarits et rendu architectural corrigés.
 *   v2.6.57 (2026-07-09) — PLANS WALL DRAWING1 : murs/cloisons par points, cloison grille et pan clic droit.
 *   v2.6.60 (2026-07-09) — PLANS STORAGE OBJECTS REALISM1 : dimensions réalistes et rendu premium des objets de stockage.
 *   v2.6.61 (2026-07-09) — PLANS STORAGE OBJECTS DETAIL2 : proportions, rotation, sélection et variantes visuelles des objets de stockage.
 *   v2.6.63 (2026-07-10) — PLANS OPENINGS_FIX5 : insertion réelle des ouvertures, anti-duplication et cloison grille adoucie.
 *   v2.6.64 (2026-07-10) — PLANS CREATE UX1 : dimensions en mètres, conversion canvas et aperçu proportionnel.
 *   v2.6.65 (2026-07-10) — PLANS SCALE FLOOR FRAME1 : métrique réelle, contour automatique et cloison grille premium.
 *   v2.6.66 (2026-07-10) — PLANS OBJECT BOUNDS ROTATE1 : placement aux bords, accroche verticale des ouvertures et UX rotation.
 *   v2.6.67 (2026-07-10) — PLANS VIEWER LOCALIZE UX1 : consultation plan et localisation produit avec rendu unifié.
 *   v2.6.70 (2026-07-11) — I18N CORE LOCALE CURRENCY1 : dates de génération selon la locale active.
 *   v2.6.58 (2026-07-09) — PLANS WALL DRAWING1 FIX : clic canvas fiabilisé et point de départ visible.
 */
function _planEsc(s){ return String(s ?? '').replace(/[&<>"']/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#039;'}[c])); }
function _planAttr(s){ return _planEsc(s).replace(/`/g, '&#096;'); }
function _planClamp(n, min, max){ n = Number(n); if(!Number.isFinite(n)) n = min; return Math.max(min, Math.min(max, n)); }
function _planShortLabel(s, max=24){
  const value = String(s ?? '').replace(/\s+/g, ' ').trim();
  if(value.length <= max) return value;
  return value.slice(0, Math.max(4, max - 1)).trimEnd() + '…';
}
function _planLabelLines(s, maxChars=18, maxLines=2){
  const clean = String(s ?? '').replace(/\s+/g, ' ').trim();
  if(!clean) return [];
  const words = clean.split(' ');
  const lines = [];
  let current = '';
  words.forEach(word => {
    const next = current ? current + ' ' + word : word;
    if(next.length <= maxChars || !current){
      current = next;
      return;
    }
    lines.push(current);
    current = word;
  });
  if(current) lines.push(current);
  if(!lines.length) lines.push(clean);
  if(lines.length > maxLines){
    const kept = lines.slice(0, maxLines);
    kept[maxLines - 1] = _planShortLabel(kept[maxLines - 1] + ' ' + lines.slice(maxLines).join(' '), maxChars);
    return kept;
  }
  return lines.map((line, idx) => idx === maxLines - 1 ? _planShortLabel(line, maxChars) : line);
}

const PlansPage = {
  rows: [],
  warehouses: [],
  locations: [],
  locationById: {},
  locationStockById: {},
  schema: { objectTypes:{} },
  objects: [],
  selectedPlan: null,
  selectedObjectId: null,
  selectedObjectIds: [],
  highlightObjectId: null,
  localization: null,
  filter: { warehouseId:'', q:'', locationQ:'' },
  componentQ: '',
  componentCategoryFilter: 'all',
  libraryFiltersOpen: false,
  collapsedComponentCategories: {},
  isAdmin: false,
  dirty: false,
  tool: 'select',
  snapEnabled: true,
  wallAngleMode: '15',
  zoom: 1,
  pan: { x: 20, y: 20 },
  interaction: null,
  _resizeTimer: null,
  showLegend: true,
  editMode: false,
  isDedicatedEditor: false,
  isCreateFlow: false,
  dragComponentType: '',
  guideLines: [],
  spacePan: false,
  localizationReadOnly: false,
  architectMode: 'build',
  wallDraft: null,
  wallPreviewPoint: null,
  editorFullscreen: false,
  fullscreenUserSet: false,

  async init(){
    if(!document.getElementById('pgPlans')) return;
    this.isAdmin = (window.SM_ROLE === 'admin' || window.USER_ROLE === 'admin');
    const root = document.getElementById('pgPlans');
    this.isDedicatedEditor = String(root?.dataset?.planPage || '') === 'plans-edit';
    this.isCreateFlow = this.isDedicatedEditor && String(root?.dataset?.planCreate || '') === '1';
    this.showLegend = !this.isDedicatedEditor;
    this.el = {
      root,
      rows: document.getElementById('planRows'),
      panel: document.querySelector('.plans-panel'),
      wh: document.getElementById('planWarehouseFilter'),
      search: document.getElementById('planSearch'),
      createScreen: document.getElementById('planCreateScreen'),
      createForm: document.getElementById('planCreateForm'),
      createName: document.getElementById('planCreateName'),
      createWarehouse: document.getElementById('planCreateWarehouse'),
      createType: document.getElementById('planCreateType'),
      createWidth: document.getElementById('planCreateWidth'),
      createHeight: document.getElementById('planCreateHeight'),
      createPreviewBox: document.getElementById('planCreatePreviewBox'),
      createPreviewShape: document.getElementById('planCreatePreviewShape'),
      createPreviewLong: document.getElementById('planCreatePreviewLong'),
      createPreviewWide: document.getElementById('planCreatePreviewWide'),
      createSurface: document.getElementById('planCreateSurface'),
      createCanvasUnits: document.getElementById('planCreateCanvasUnits'),
      createGridText: document.getElementById('planCreateGridText'),
      createScaleText: document.getElementById('planCreateScaleText'),
      createFrame: document.getElementById('planCreateFrame'),
      createFrameType: document.getElementById('planCreateFrameType'),
      createFrameThickness: document.getElementById('planCreateFrameThickness'),
      createSubmit: document.getElementById('btnPlanCreateSubmit'),
      add: document.getElementById('btnPlanAdd'),
      schema: document.getElementById('btnPlansSchema'),
      statActive: document.getElementById('planStatActive'),
      statWarehouses: document.getElementById('planStatWarehouses'),
      statObjects: document.getElementById('planStatObjects'),
      statComponents: document.getElementById('planStatComponents'),
      title: document.getElementById('planEditorTitle'),
      sub: document.getElementById('planEditorSub'),
      modeLabel: document.getElementById('planModeLabel'),
      dirtyState: document.getElementById('planDirtyState'),
      welcome: document.getElementById('plansWelcome'),
      workbench: document.getElementById('plansWorkbench'),
      toolbar: document.getElementById('planToolbar'),
      save: document.getElementById('btnPlanSave'),
      edit: document.getElementById('btnPlanEdit'),
      cancelEdit: document.getElementById('btnPlanCancelEdit'),
      searchFocus: document.getElementById('btnPlanSearchFocus'),
      print: document.getElementById('btnPlanPrint'),
      pdf: document.getElementById('btnPlanPdf'),
      legend: document.getElementById('btnPlanLegend'),
      fit: document.getElementById('btnPlanFit'),
      zoomIn: document.getElementById('btnPlanZoomIn'),
      zoomOut: document.getElementById('btnPlanZoomOut'),
      snap: document.getElementById('btnPlanSnap'),
      duplicate: document.getElementById('btnPlanDuplicate'),
      delete: document.getElementById('btnPlanDelete'),
      canvasWrap: document.getElementById('planCanvasWrap'),
      canvasHost: document.getElementById('planCanvasHost'),
      canvasEmpty: document.getElementById('planCanvasEmpty'),
      badges: document.getElementById('planCanvasBadges'),
      inspector: document.getElementById('planInspector'),
      componentLibrary: document.getElementById('planComponentLibrary'),
      componentSearch: document.getElementById('planComponentSearch'),
      libraryFilter: document.getElementById('planLibraryFilter') || document.querySelector('.plans-library-filter'),
      wallAngleSnap: document.getElementById('planWallAngleSnap'),
      libraryFilters: document.getElementById('planLibraryFilters'),
      libraryHead: document.getElementById('planLibraryHead'),
      locationPanel: document.getElementById('planLocationSearchPanel'),
      locationSearch: document.getElementById('planLocationSearch'),
      locationResults: document.getElementById('planLocationResults'),
      backToList: document.getElementById('btnPlanBackToList'),
      rail: document.getElementById('planEditorRail'),
      fullscreen: document.getElementById('btnPlanFullscreen'),
    };
    document.querySelectorAll('[data-plan-admin]').forEach(node => { node.hidden = !this.isAdmin; });
    if(this.el.add) this.el.add.hidden = !this.isAdmin;
    if(this.el.backToList) this.el.backToList.hidden = !this.isDedicatedEditor;
    this.el.root?.classList.toggle('plans-page--localization', false);
    if(this.el.save) this.el.save.hidden = true;
    this._bind();
    await this.load();
  },

  _bind(){
    this.el.search?.addEventListener('input', () => { this.filter.q = (this.el.search.value || '').trim().toLowerCase(); this.renderList(); });
    this.el.componentSearch?.addEventListener('input', () => { this.componentQ = (this.el.componentSearch.value || '').trim().toLowerCase(); this.renderComponentLibrary(); });
    this.el.componentSearch?.addEventListener('keydown', (e) => { if(e.key === 'Escape' && this.el.componentSearch.value){ e.stopPropagation(); this.el.componentSearch.value = ''; this.componentQ = ''; this.renderComponentLibrary(); } });
    this.el.libraryFilter?.addEventListener('click', (e) => { e.preventDefault(); this.toggleLibraryFilters(); });
    this.el.libraryFilters?.addEventListener('click', (e) => {
      const btn = e.target.closest('[data-library-filter]');
      if(!btn) return;
      this.componentCategoryFilter = btn.dataset.libraryFilter || 'all';
      this.libraryFiltersOpen = false;
      this.renderComponentLibrary();
    });
    document.addEventListener('click', (e) => {
      if(!this.libraryFiltersOpen) return;
      if(e.target.closest('#planLibraryFilters, #planLibraryFilter, .plans-library-filter')) return;
      this.libraryFiltersOpen = false;
      this.renderLibraryFilterPanel();
    });
    this.el.wh?.addEventListener('change', async () => { this.filter.warehouseId = this.el.wh.value || ''; await this.loadPlansOnly(); });
    this.el.add?.addEventListener('click', () => this.goCreate());
    this.el.schema?.addEventListener('click', () => this.openSchemaModal());
    this.el.save?.addEventListener('click', () => this.saveObjects());
    this.el.edit?.addEventListener('click', () => this.selectedPlan ? this.goEditor(this.selectedPlan.id) : this.enterEditMode());
    this.el.cancelEdit?.addEventListener('click', () => this.leaveEditMode());
    this.el.searchFocus?.addEventListener('click', () => this.focusLocationSearch());
    document.querySelectorAll('[data-plan-action="create"]').forEach(btn => btn.addEventListener('click', () => this.goCreate()));
    document.querySelectorAll('[data-plan-action="focus-search"]').forEach(btn => btn.addEventListener('click', () => this.focusPlanSearch()));
    this.el.createForm?.addEventListener('submit', (e) => { e.preventDefault(); this.savePlanFromCreateScreen(); });
    document.querySelectorAll('[data-plan-size]').forEach(btn => btn.addEventListener('click', () => this.applyCreateSize(btn.dataset.planSize || 'standard')));
    this.el.createWidth?.addEventListener('input', () => { this.markCreateCustomPreset(); this.updateCreatePreview(); });
    this.el.createHeight?.addEventListener('input', () => { this.markCreateCustomPreset(); this.updateCreatePreview(); });
    this.el.createType?.addEventListener('change', () => this.updateCreatePreview());
    this.el.createFrame?.addEventListener('change', () => this.updateCreatePreview());
    this.el.createFrameType?.addEventListener('change', () => { if(this.el.createFrameThickness){ const t=this.el.createFrameType.value; this.el.createFrameThickness.value = t === 'mur' ? '22' : (t === 'cloison' ? '10' : '8'); } this.updateCreatePreview(); });
    this.el.createFrameThickness?.addEventListener('input', () => this.updateCreatePreview());
    this.updateCreatePreview();
    this.el.print?.addEventListener('click', () => this.printPlan({ autoPrint:false, mode:'print' }));
    this.el.pdf?.addEventListener('click', () => this.printPlan({ autoPrint:true, mode:'pdf' }));
    this.el.legend?.addEventListener('click', () => { this.showLegend = !this.showLegend; this.el.legend.classList.toggle('active', this.showLegend); this.drawSvg(); });
    this.el.fit?.addEventListener('click', () => this.fitCanvas());
    this.el.fullscreen?.addEventListener('click', () => this.toggleEditorFullscreen());
    this.el.zoomIn?.addEventListener('click', () => this.setZoom(this.zoom * 1.18));
    this.el.zoomOut?.addEventListener('click', () => this.setZoom(this.zoom / 1.18));
    this.el.snap?.addEventListener('click', () => { this.snapEnabled = !this.snapEnabled; this.el.snap.classList.toggle('active', this.snapEnabled); this.drawSvg(); });
    this.el.wallAngleSnap?.addEventListener('change', () => { this.wallAngleMode = this.el.wallAngleSnap.value || '15'; if(this.wallDraft){ this.drawSvg(); } });
    this.el.duplicate?.addEventListener('click', () => this.duplicateSelected());
    this.el.delete?.addEventListener('click', () => this.deleteSelected());
    this.el.toolbar?.addEventListener('click', (e) => {
      const btn = e.target.closest('[data-tool],[data-add],[data-align],[data-resize],[data-rotate],[data-editor-act],[data-editor-mode]');
      if(!btn) return;
      if(btn.dataset.tool){
        const t = btn.dataset.tool;
        if(t.startsWith('add:') && this.isWallType(t.split(':')[1])) this.beginWallDrawingMode(t.split(':')[1]);
        else this.setTool(t);
      }
      if(btn.dataset.add) this.setTool('add:' + btn.dataset.add);
      if(btn.dataset.align) this.alignSelection(btn.dataset.align);
      if(btn.dataset.resize) this.resizeSelection(btn.dataset.resize);
      if(btn.dataset.rotate) this.rotateSelection(btn.dataset.rotate);
      if(btn.dataset.editorAct === 'selectAll') this.selectAllObjects();
      if(btn.dataset.editorAct === 'focusSelection') this.focusSelection();
      if(btn.dataset.editorAct === 'finishWall') this.finishWallChain();
      if(btn.dataset.editorMode) this.setArchitectMode(btn.dataset.editorMode);
    });
    this.el.componentLibrary?.addEventListener('click', (e) => {
      const toggle = e.target.closest('[data-library-toggle]');
      if(toggle){
        const cat = toggle.dataset.libraryToggle || '';
        this.collapsedComponentCategories[cat] = !this.collapsedComponentCategories[cat];
        this.renderComponentLibrary();
        return;
      }
      const dropHelp = e.target.closest('[data-library-help]');
      if(dropHelp){
        this.el.componentSearch?.focus();
        toast('Choisissez un objet dans la bibliothèque, puis cliquez dans le plan ou glissez-le sur la grille.', 'info');
        return;
      }
      const btn = e.target.closest('[data-component-add]');
      if(!btn || !this.canEdit()) return;
      const type = btn.dataset.componentAdd || '';
      if(this.isWallType(type)){
        this.beginWallDrawingMode(type);
        return;
      }
      this.beginComponentPlacement(type, btn.dataset.componentLabel || 'composant');
    });
    this.el.componentLibrary?.addEventListener('dragstart', (e) => {
      const btn = e.target.closest('[data-component-add]');
      if(!btn || !this.canEdit()) return;
      const type = btn.dataset.componentAdd || '';
      if(this.isWallType(type)){
        e.preventDefault();
        this.beginWallDrawingMode(type);
        return;
      }
      this.dragComponentType = type;
      e.dataTransfer.effectAllowed = 'copy';
      e.dataTransfer.setData('text/plain', this.dragComponentType);
      e.dataTransfer.setData('application/x-magestock-plan-component', this.dragComponentType);
      this.markComponentActive(this.dragComponentType);
    });
    this.el.componentLibrary?.addEventListener('pointerdown', (e) => {
      const btn = e.target.closest('[data-component-add]');
      if(!btn || !this.canEdit()) return;
      const type = btn.dataset.componentAdd || '';
      this.dragComponentType = this.isWallType(type) ? '' : type;
    });
    this.el.componentLibrary?.addEventListener('dragend', () => {
      this.dragComponentType = '';
      this.el.canvasWrap?.classList.remove('is-drop-ready');
      this.markComponentActive(this.tool.startsWith('add:') ? this.tool.split(':')[1] : '');
    });
    this.el.canvasWrap?.addEventListener('dragenter', (e) => this.onCanvasDragOver(e));
    this.el.canvasWrap?.addEventListener('dragover', (e) => this.onCanvasDragOver(e));
    this.el.canvasWrap?.addEventListener('dragleave', (e) => { if(e.currentTarget === this.el.canvasWrap && !this.el.canvasWrap.contains(e.relatedTarget)) this.el.canvasWrap.classList.remove('is-drop-ready'); });
    this.el.canvasWrap?.addEventListener('drop', (e) => this.onCanvasDrop(e));
    this.el.canvasWrap?.addEventListener('pointerdown', (e) => this.onCanvasPointerDown(e));
    this.el.canvasWrap?.addEventListener('contextmenu', (e) => { if(this.selectedPlan && (e.target?.closest?.('#planCanvasSvg') || e.target === this.el.canvasWrap || e.target === this.el.canvasHost)) e.preventDefault(); });
    this.el.canvasWrap?.addEventListener('wheel', (e) => this.onCanvasWheel(e), { passive:false });
    this.el.canvasWrap?.addEventListener('dblclick', (e) => { if(this.wallDraft){ e.preventDefault(); this.finishWallChain(); return; } if(this.selectedPlan && !this.canvasActionTarget(e)) this.fitCanvas(); });
    this.el.locationSearch?.addEventListener('input', () => {
      this.filter.locationQ = (this.el.locationSearch.value || '').trim().toLowerCase();
      this.renderLocationSearchResults();
    });
    this.el.locationResults?.addEventListener('click', (e) => {
      const btn = e.target.closest('[data-location-act]');
      if(!btn) return;
      const id = btn.dataset.locationId || '';
      if(btn.dataset.locationAct === 'focus') this.focusLocation(id);
      if(btn.dataset.locationAct === 'select') this.selectLinkedLocationCandidate(id);
    });
    window.addEventListener('pointermove', (e) => this.onPointerMove(e), { passive:false });
    window.addEventListener('pointerup', (e) => this.endInteraction(e));
    window.addEventListener('pointercancel', (e) => this.endInteraction(e));
    window.addEventListener('resize', () => {
      clearTimeout(this._resizeTimer);
      this._resizeTimer = setTimeout(() => { if(this.selectedPlan) this.drawSvg(); }, 160);
    });
    document.addEventListener('keydown', (e) => this.onKey(e));
    document.addEventListener('keyup', (e) => { if(e.code === 'Space'){ this.spacePan = false; this.drawSvg(); } });
  },

  canEdit(){
    return !!(this.isAdmin && this.editMode && this.selectedPlan && !this.localizationReadOnly);
  },

  enterEditMode(){
    if(!this.selectedPlan || !this.isAdmin) return;
    this.editMode = true;
    this.renderEditor();
    toast('Mode édition activé. Choisissez un élément à ajouter ou sélectionnez un objet existant.', 'info');
  },

  async leaveEditMode(){
    if(this.dirty){
      if(!confirm('Des modifications ne sont pas enregistrées. Revenir à la dernière version enregistrée ?')) return;
      const plan = this.selectedPlan;
      this.editMode = false;
      await this.openPlan(plan, { editMode:false, toast:false });
      return;
    }
    this.editMode = false;
    this.clearSelection();
    this.setTool('select', false);
    this.renderEditor();
  },

  focusPlanSearch(){
    this.el.search?.focus();
    this.el.search?.select?.();
  },

  focusLocationSearch(){
    if(!this.selectedPlan){ this.focusPlanSearch(); return; }
    this.el.locationSearch?.focus();
    this.el.locationSearch?.select?.();
    this.el.locationPanel?.scrollIntoView?.({ behavior:'smooth', block:'center' });
  },

  async load(){
    try{
      const [warehouses, schema] = await Promise.all([Api.warehouses(), Api.planSchema()]);
      this.warehouses = Array.isArray(warehouses) ? warehouses : [];
      this.schema = schema || { objectTypes:{} };
      this.renderWarehouseFilter();
      await this.loadPlansOnly();
      await this.handleInitialLocationRequest();
      await this.handleDedicatedEditorRequest();
      if(!this.selectedPlan) this.renderEditor();
    }catch(e){
      console.error(e);
      if(this.el.rows) this.el.rows.innerHTML = `<div style="padding:14px;color:var(--red)">${_planEsc(e.message || 'Erreur de chargement')}</div>`;
      toast(e.message || 'Erreur de chargement des plans.', 'error');
    }
  },

  async loadPlansOnly(){
    const params = {};
    if(this.filter.warehouseId) params.warehouseId = this.filter.warehouseId;
    this.rows = await Api.plans(params);
    if(!Array.isArray(this.rows)) this.rows = [];
    this.renderList();
    if(this.selectedPlan){
      const refreshed = this.rows.find(r => String(r.id) === String(this.selectedPlan.id));
      if(refreshed) this.selectedPlan = refreshed;
      else this.closeEditor();
    }
  },

  renderWarehouseFilter(){
    const warehouseOptions = this.warehouses.map(w => `<option value="${_planAttr(w.id)}">${_planEsc(w.name || ('Magasin #' + w.id))}</option>`).join('');
    if(this.el.wh){
      const current = this.el.wh.value || '';
      this.el.wh.innerHTML = '<option value="">Tous les magasins</option>' + warehouseOptions;
      this.el.wh.value = current;
    }
    if(this.el.createWarehouse){
      const currentCreate = this.el.createWarehouse.value || '';
      this.el.createWarehouse.innerHTML = '<option value="">Sélectionner un magasin</option>' + warehouseOptions;
      if(currentCreate) this.el.createWarehouse.value = currentCreate;
      else if(this.warehouses.length === 1) this.el.createWarehouse.value = String(this.warehouses[0].id || '');
    }
  },

  whName(id){
    const w = this.warehouses.find(x => String(x.id) === String(id));
    return w?.name || (id ? ('Magasin #' + id) : 'Non rattaché');
  },

  typeLabel(type){
    return this.schema?.objectTypes?.[type]?.label || type || 'Objet';
  },

  typeColor(type){
    return this.schema?.objectTypes?.[type]?.color || '#dbeafe';
  },

  filteredRows(){
    const q = this.filter.q;
    let rows = this.rows.slice();
    if(q){
      rows = rows.filter(r => `${r.name || ''} ${r.type || ''} ${this.whName(r.warehouseId)}`.toLowerCase().includes(q));
    }
    return rows.sort((a,b) => String(a.name || '').localeCompare(String(b.name || ''), 'fr'));
  },

  renderList(){
    const rows = this.filteredRows();
    const active = this.rows.filter(r => r.active !== false).length;
    const whCount = new Set(this.rows.map(r => String(r.warehouseId || '')).filter(Boolean)).size;
    const linkedCount = this.selectedPlan ? this.objects.filter(o => o.linkedLocationId).length : 0;
    if(this.el.statActive) this.el.statActive.textContent = String(active);
    if(this.el.statWarehouses) this.el.statWarehouses.textContent = String(whCount);
    if(this.el.statComponents) this.el.statComponents.textContent = String(linkedCount);
    if(this.el.statObjects) this.el.statObjects.textContent = String(this.selectedPlan ? (this.objects.length || 0) : 0);

    if(!this.el.rows) return;
    if(!rows.length){
      this.el.rows.innerHTML = `<div class="plans-list-empty"><strong>Aucun plan trouvé</strong><span>${this.isAdmin ? 'Créez un premier plan ou changez le filtre magasin.' : 'Aucun plan consultable pour vos magasins.'}</span></div>`;
      return;
    }
    this.el.rows.innerHTML = rows.map(r => this.planTileHtml(r)).join('');
    this.el.rows.querySelectorAll('[data-act]').forEach(btn => {
      btn.addEventListener('click', (e) => {
        e.stopPropagation();
        const row = this.rows.find(x => String(x.id) === String(btn.dataset.id));
        if(!row) return;
        if(btn.dataset.act === 'open') this.openPlan(row);
        if(btn.dataset.act === 'edit') this.openPlanModal(row);
        if(btn.dataset.act === 'editDesign') this.goEditor(row.id);
        if(btn.dataset.act === 'delete') this.deletePlan(row);
      });
    });
    this.el.rows.querySelectorAll('.plan-tile').forEach(tile => {
      tile.addEventListener('click', () => {
        const row = this.rows.find(x => String(x.id) === String(tile.dataset.id));
        if(row) this.openPlan(row);
      });
    });
  },

  planTileHtml(r){
    const active = String(this.selectedPlan?.id || '') === String(r.id);
    const dims = `Long. ${parseInt(r.width || 0, 10)} × Larg. ${parseInt(r.height || 0, 10)}`;
    const adminBtns = this.isAdmin ? `
      <button class="btn btn-secondary btn-sm" data-act="edit" data-id="${_planAttr(r.id)}">Paramètres</button>
      <button class="btn btn-primary btn-sm" data-act="editDesign" data-id="${_planAttr(r.id)}">Modifier</button>
      <button class="btn btn-danger btn-sm" data-act="delete" data-id="${_planAttr(r.id)}">Supprimer</button>` : '';
    return `<button type="button" class="plan-tile${active ? ' active' : ''}" data-id="${_planAttr(r.id)}">
      <span class="plan-tile__top">
        <span>
          <span class="plan-tile__name">${_planEsc(r.name || 'Plan sans nom')}</span>
          <span class="plan-tile__meta"><span>${_planEsc(this.whName(r.warehouseId))}</span><span>${_planEsc(r.type || 'dépôt')}</span><span>${_planEsc(dims)}</span></span>
        </span>
        <span class="plan-status-dot${r.active === false ? ' off' : ''}" aria-hidden="true"></span>
      </span>
      <span class="plan-tile__actions">
        <span class="btn btn-primary btn-sm" data-act="open" data-id="${_planAttr(r.id)}">Ouvrir</span>
        ${adminBtns}
      </span>
    </button>`;
  },

  async openPlan(row, options={}){
    if(this.dirty && !confirm('Des modifications ne sont pas encore enregistrées. Changer de plan sans sauvegarder ?')) return;
    try{
      const [full, objects, locationPayload] = await Promise.all([
        Api.planGet(row.id),
        Api.planObjects(row.id),
        Api.planLocations({ warehouseId: row.warehouseId, withStock: 1 })
      ]);
      this.selectedPlan = full || row;
      this.setLocations(locationPayload?.locations || []);
      this.objects = Array.isArray(objects) ? objects.map(o => this.normalizeClientObject(o)) : [];
      this.selectedObjectId = null;
      this.selectedObjectIds = [];
      this.highlightObjectId = options.highlightObjectId || null;
      this.localization = options.localization || null;
      this.localizationReadOnly = !!options.localizationReadOnly || !!this.localization || this.localizationReadOnly;
      this.editMode = this.localizationReadOnly ? false : !!(options.editMode && this.isAdmin);
      this.showLegend = typeof options.showLegend === 'boolean' ? options.showLegend : !this.editMode;
      this.dirty = false;
      this.setTool('select', false);
      this.fitCanvas();
      this.renderEditor();
      this.renderList();
      const focusId = options.focusObjectId || (options.locationId ? this.linkedObjectForLocation(options.locationId)?.id : null);
      if(focusId) this.focusObject(focusId, { highlight: options.highlight !== false, localization: options.localization || this.localization, toast: options.toast !== false });
    }catch(e){
      toast(e.message || 'Impossible d’ouvrir ce plan.', 'error');
    }
  },

  closeEditor(){
    this.selectedPlan = null;
    this.objects = [];
    this.locations = [];
    this.locationById = {};
    this.locationStockById = {};
    this.selectedObjectId = null;
    this.selectedObjectIds = [];
    this.highlightObjectId = null;
    this.localization = null;
    this.localizationReadOnly = false;
    this.dirty = false;
    this.editMode = false;
    this.renderEditor();
    this.renderList();
  },

  normalizeClientObject(o){
    return {
      id: o.id || ('tmp_' + Date.now() + '_' + Math.random().toString(16).slice(2)),
      planId: o.planId || this.selectedPlan?.id || '',
      type: o.type || 'emplacement',
      label: o.label || '',
      x: Number(o.x || 0),
      y: Number(o.y || 0),
      width: Number(o.width || 160),
      height: Number(o.height || 80),
      rotation: Number(o.rotation || 0),
      color: o.color || this.typeColor(o.type || 'emplacement'),
      icon: o.icon || this.schema?.objectTypes?.[o.type || 'emplacement']?.icon || '',
      linkedWarehouseId: o.linkedWarehouseId ?? null,
      linkedLocationId: o.linkedLocationId ?? null,
      linkedZone: o.linkedZone ?? '',
      linkedProductId: o.linkedProductId ?? null,
      metadata: this.normalizeObjectMetadata(o.metadata),
      createdAt: o.createdAt,
      updatedAt: o.updatedAt,
    };
  },

  normalizeObjectMetadata(metadata){
    const out = (metadata && typeof metadata === 'object' && !Array.isArray(metadata)) ? { ...metadata } : {};
    if(Array.isArray(out.points)){
      out.points = out.points.slice(0, 2).map(pt => ({ x:Number(pt?.x || 0), y:Number(pt?.y || 0) }));
    }
    if(out.thickness !== undefined) out.thickness = Math.max(4, Number(out.thickness || 0));
    if(out.length !== undefined) out.length = Math.max(0, Math.round(Number(out.length || 0)));
    if(out.angle !== undefined) out.angle = Math.round(Number(out.angle || 0));
    if(out.wallAngle !== undefined) out.wallAngle = Math.round(Number(out.wallAngle || 0));
    if(out.wallOffset !== undefined) out.wallOffset = Number(out.wallOffset || 0);
    if(out.swing !== undefined) out.swing = ['right','left','double','double-reverse'].includes(String(out.swing)) ? String(out.swing) : 'right';
    if(out.openingKind !== undefined) out.openingKind = ['door','doubleDoor','window','passage'].includes(String(out.openingKind)) ? String(out.openingKind) : '';
    return out;
  },

  renderEditor(){
    const hasPlan = !!this.selectedPlan;
    const creating = !!(this.isCreateFlow && !hasPlan);
    const editing = this.canEdit();
    const localizationMode = !!this.localizationReadOnly;
    if(this.el.title) this.el.title.textContent = creating ? 'Créer un plan' : (hasPlan ? (editing ? 'Modifier : ' + this.selectedPlan.name : this.selectedPlan.name) : (this.isDedicatedEditor ? 'Créer ou choisir un plan à modifier' : 'Choisissez ou créez un plan'));
    if(this.el.sub){
      this.el.sub.textContent = creating
        ? 'Renseignez le magasin et la taille du plan. Le dessin s’ouvrira ensuite sur une page dédiée.'
        : (hasPlan
          ? (localizationMode ? `${this.whName(this.selectedPlan.warehouseId)} · plan fini de localisation · emplacement surligné` : `${this.whName(this.selectedPlan.warehouseId)} · Longueur ${this.formatLengthUnits(this.selectedPlan.width)} · Largeur ${this.formatLengthUnits(this.selectedPlan.height)} · ${editing ? 'édition admin' : 'consultation'}`)
          : 'La consultation reste simple. L’édition complète est réservée aux administrateurs.');
    }
    if(this.el.modeLabel) this.el.modeLabel.textContent = creating ? 'Création' : (hasPlan ? (localizationMode ? 'Localisation' : (editing ? 'Page édition' : 'Mode consultation')) : (this.isDedicatedEditor ? 'Édition' : 'Accueil'));
    if(this.el.createScreen) this.el.createScreen.hidden = !creating;
    if(this.el.welcome) this.el.welcome.hidden = hasPlan || creating;
    if(this.el.workbench) this.el.workbench.hidden = !hasPlan || creating;
    if(this.el.toolbar) this.el.toolbar.hidden = !editing || creating;
    if(this.el.componentLibrary){
      this.el.componentLibrary.hidden = !editing || creating;
    }
    if(this.el.libraryHead) this.el.libraryHead.hidden = !editing || creating;
    if(this.el.libraryFilters) this.el.libraryFilters.hidden = !editing || creating || !this.libraryFiltersOpen;
    if(this.el.locationPanel) this.el.locationPanel.hidden = !hasPlan || creating || localizationMode;
    this.renderComponentLibrary();
    this.renderLocationSearchResults();
    [this.el.fit, this.el.zoomIn, this.el.zoomOut, this.el.print, this.el.pdf, this.el.legend, this.el.searchFocus].forEach(btn => { if(btn) btn.disabled = !hasPlan || creating; });
    if(this.el.backToList) this.el.backToList.hidden = !this.isDedicatedEditor;
    if(this.el.edit){ this.el.edit.hidden = !this.isAdmin || !hasPlan || editing || this.isDedicatedEditor || creating || localizationMode; this.el.edit.disabled = !hasPlan || localizationMode; }
    if(this.el.cancelEdit) this.el.cancelEdit.hidden = !editing || creating;
    if(this.el.legend) this.el.legend.classList.toggle('active', this.showLegend);
    if(this.el.save){ this.el.save.disabled = !editing || !this.dirty || creating; this.el.save.hidden = !editing || creating; }
    const linkedCount = this.objects.filter(o => o.linkedLocationId).length;
    if(this.el.statObjects) this.el.statObjects.textContent = String(hasPlan ? this.objects.length : 0);
    if(this.el.statComponents) this.el.statComponents.textContent = String(hasPlan ? linkedCount : 0);
    this.el.root?.classList.toggle('plans-page--localization', localizationMode);
    this.el.root?.classList.toggle('plans-page--viewer', !!hasPlan && !editing && !creating);
    if(this.el.fullscreen) this.el.fullscreen.disabled = !hasPlan && !creating;
    if(editing || creating){
      if(!this.fullscreenUserSet && !this.editorFullscreen && editing) this.setEditorFullscreen(true);
    }else{
      this.fullscreenUserSet = false;
      if(this.editorFullscreen) this.setEditorFullscreen(false);
    }
    this.updateDirtyState();
    this.updateToolbarState();
    this.drawSvg();
    this.renderInspector();
  },

  setEditorFullscreen(on){
    const active = !!on;
    this.editorFullscreen = active;
    this.el.root?.classList.toggle('plans-fs', active);
    if(document.body) document.body.classList.toggle('plans-fs-lock', active);
    if(this.el.fullscreen){
      this.el.fullscreen.classList.toggle('active', active);
      this.el.fullscreen.setAttribute('aria-pressed', active ? 'true' : 'false');
      this.el.fullscreen.innerHTML = '<span class="plan-tool-icon" aria-hidden="true">⛶</span><span>Plein écran</span>';
      this.el.fullscreen.title = active ? 'Quitter le plein écran' : 'Basculer l’éditeur en plein écran';
    }
    if(typeof requestAnimationFrame === 'function'){
      requestAnimationFrame(() => { this.drawSvg(); });
    }else{
      this.drawSvg();
    }
  },

  toggleEditorFullscreen(){
    this.fullscreenUserSet = true;
    this.setEditorFullscreen(!this.editorFullscreen);
  },

  updateDirtyState(){
    if(!this.el.dirtyState) return;
    if(!this.selectedPlan){
      this.el.dirtyState.className = 'plan-readonly-pill';
      this.el.dirtyState.textContent = 'Aucun plan ouvert';
      return;
    }
    if(this.localizationReadOnly){
      this.el.dirtyState.className = 'plan-readonly-pill plan-readonly-pill--localization';
      this.el.dirtyState.textContent = 'Localisation produit';
      return;
    }
    if(!this.isAdmin || !this.editMode){
      this.el.dirtyState.className = 'plan-readonly-pill';
      this.el.dirtyState.textContent = 'Consultation';
      return;
    }
    this.el.dirtyState.className = this.dirty ? 'plan-dirty-pill' : 'plan-clean-pill';
    this.el.dirtyState.textContent = this.dirty ? 'Modifications à enregistrer' : 'Édition à jour';
  },

  updateToolbarState(){
    const editing = this.canEdit();
    const selected = this.selectedObjects();
    const hasSelection = selected.length > 0;
    const hasMulti = selected.length > 1;
    if(this.el.toolbar){
      this.el.toolbar.querySelectorAll('[data-align],[data-resize],[data-rotate],#btnPlanDuplicate,#btnPlanDelete,[data-editor-act="focusSelection"]').forEach(btn => {
        btn.disabled = !editing || !hasSelection;
        btn.classList.toggle('is-disabled', btn.disabled);
      });
      this.el.toolbar.querySelectorAll('[data-align="distributeX"]').forEach(btn => {
        btn.disabled = !editing || !hasMulti || selected.length < 3;
        btn.classList.toggle('is-disabled', btn.disabled);
      });
      this.el.toolbar.querySelectorAll('[data-editor-act="selectAll"]').forEach(btn => {
        btn.disabled = !editing || this.objects.length === 0;
        btn.classList.toggle('is-disabled', btn.disabled);
      });
      this.el.toolbar.querySelectorAll('[data-tool="pan"]').forEach(btn => {
        btn.classList.toggle('active', this.tool === 'pan');
      });
    }
  },

  setDirty(v=true){
    this.dirty = !!v;
    this.updateDirtyState();
    if(this.el.save) this.el.save.disabled = !this.canEdit() || !this.dirty;
  },

  setTool(tool, redraw=true){
    const nextTool = tool || 'select';
    if(this.wallDraft && !this.isWallTool(nextTool)){
      this.cancelWallDraft(false);
    }
    this.tool = nextTool;
    if(this.el.toolbar){
      this.el.toolbar.querySelectorAll('.plan-tool-btn[data-tool], .plan-tool-btn[data-add]').forEach(btn => {
        const key = btn.dataset.tool ? btn.dataset.tool : ('add:' + btn.dataset.add);
        btn.classList.toggle('active', key === this.tool);
      });
    }
    this.markComponentActive(this.tool.startsWith('add:') ? this.tool.split(':')[1] : '');
    this.updateToolbarState();
    if(redraw) this.drawSvg();
  },

  appUrl(page, params={}){
    const runtime = window.MageStockRuntime || {};
    const base = String(runtime.basePath || '').replace(/\/$/, '');
    const query = new URLSearchParams(Object.entries(params).filter(([,v]) => v !== undefined && v !== null && v !== ''));
    return base + '/' + page + (query.toString() ? ('?' + query.toString()) : '');
  },

  goCreate(){
    if(!this.isAdmin){ toast('La création des plans est réservée aux administrateurs.', 'warning'); return; }
    window.location.href = this.appUrl('plans-edit', { create: '1' });
  },

  goEditor(planId){
    if(!this.isAdmin){ toast('La modification des plans est réservée aux administrateurs.', 'warning'); return; }
    window.location.href = this.appUrl('plans-edit', { planId: planId || '', edit: '1' });
  },

  async handleDedicatedEditorRequest(){
    if(!this.isDedicatedEditor) return;
    if(!this.isAdmin){ window.location.href = this.appUrl('plans'); return; }
    const params = new URLSearchParams(window.location.search || '');
    const planId = params.get('planId') || params.get('id') || '';
    if(planId){
      if(this.selectedPlan && String(this.selectedPlan.id || '') === String(planId)){
        this.editMode = true;
        this.renderEditor();
        return;
      }
      const row = this.rows.find(r => String(r.id) === String(planId)) || { id: planId };
      await this.openPlan(row, { editMode:true, toast:false });
      return;
    }
    if(!planId && params.get('create') === '1'){
      this.isCreateFlow = true;
      this.renderEditor();
    }
  },

  createSizePresets(){
    return {
      compact: { widthM: 6, heightM: 4, label: 'Petit local' },
      atelier: { widthM: 10, heightM: 6, label: 'Atelier' },
      standard: { widthM: 16, heightM: 10, label: 'Dépôt standard' },
      large: { widthM: 30, heightM: 20, label: 'Grand entrepôt' }
    };
  },

  parseCreateMeters(input, fallback){
    const raw = String(input?.value ?? fallback).replace(',', '.').trim();
    const n = Number.parseFloat(raw);
    if(!Number.isFinite(n)) return fallback;
    return Math.min(200, Math.max(1, n));
  },

  createMetersToCanvas(valueM){
    return Math.round(Math.min(200, Math.max(1, valueM)) * 100);
  },

  formatMeters(value){
    const n = Number(value);
    if(!Number.isFinite(n)) return '0';
    const fixed = Math.abs(n) >= 10 ? n.toFixed(1) : n.toFixed(2);
    return fixed.replace(/\.?0+$/, '').replace('.', ',');
  },

  unitsToMeters(units){
    const n = Number(units || 0);
    return (n * this.planScale()) / 100;
  },

  metersToUnits(meters){
    const n = Number(meters || 0);
    return Math.round((n * 100) / Math.max(0.01, this.planScale()));
  },

  formatLengthUnits(units, opts={}){
    const m = this.unitsToMeters(units);
    if(!Number.isFinite(m)) return '—';
    const abs = Math.abs(m);
    const decimals = opts.decimals ?? (abs < 10 ? 2 : 1);
    let txt = m.toFixed(decimals).replace(/\.?0+$/, '').replace('.', ',');
    return opts.unit === false ? txt : `${txt} m`;
  },

  gridStepsForPlan(planW=null, planH=null){
    const w = Number(planW || this.selectedPlan?.width || 1600);
    const h = Number(planH || this.selectedPlan?.height || 1000);
    const max = Math.max(w, h);
    const major = max <= 700 ? 100 : (max <= 2600 ? 200 : 500);
    return { major, minor: Math.max(10, major / 5) };
  },

  applyCreateSize(size){
    const presets = this.createSizePresets();
    const next = presets[size];
    if(next){
      if(this.el.createWidth) this.el.createWidth.value = String(next.widthM);
      if(this.el.createHeight) this.el.createHeight.value = String(next.heightM);
    }
    document.querySelectorAll('[data-plan-size]').forEach(btn => btn.classList.toggle('active', btn.dataset.planSize === size));
    this.updateCreatePreview();
  },

  markCreateCustomPreset(){
    const widthM = this.parseCreateMeters(this.el.createWidth, 16);
    const heightM = this.parseCreateMeters(this.el.createHeight, 10);
    const presets = this.createSizePresets();
    let matched = 'custom';
    Object.entries(presets).forEach(([key, preset]) => {
      if(Math.abs(widthM - preset.widthM) < 0.05 && Math.abs(heightM - preset.heightM) < 0.05) matched = key;
    });
    document.querySelectorAll('[data-plan-size]').forEach(btn => btn.classList.toggle('active', btn.dataset.planSize === matched));
  },

  updateCreatePreview(){
    if(!this.el.createPreviewShape) return;
    const widthM = this.parseCreateMeters(this.el.createWidth, 16);
    const heightM = this.parseCreateMeters(this.el.createHeight, 10);
    const widthUnits = this.createMetersToCanvas(widthM);
    const heightUnits = this.createMetersToCanvas(heightM);
    const surface = widthM * heightM;
    const ratio = widthM / Math.max(1, heightM);
    const maxW = 260;
    const maxH = 170;
    let previewW = maxW;
    let previewH = maxW / Math.max(.35, ratio);
    if(previewH > maxH){ previewH = maxH; previewW = maxH * ratio; }
    previewW = Math.max(78, Math.min(maxW, previewW));
    previewH = Math.max(58, Math.min(maxH, previewH));
    this.el.createPreviewShape.style.width = `${previewW}px`;
    this.el.createPreviewShape.style.height = `${previewH}px`;
    if(this.el.createPreviewLong) this.el.createPreviewLong.textContent = `${this.formatMeters(widthM)} m`;
    if(this.el.createPreviewWide) this.el.createPreviewWide.textContent = `${this.formatMeters(heightM)} m`;
    if(this.el.createSurface) this.el.createSurface.textContent = `${surface >= 100 ? Math.round(surface) : surface.toFixed(1).replace('.', ',')} m²`;
    if(this.el.createCanvasUnits) this.el.createCanvasUnits.textContent = `${widthUnits} × ${heightUnits} unités`;
    if(this.el.createGridText) this.el.createGridText.textContent = widthUnits <= 700 || heightUnits <= 700 ? '1 grand carreau = 1 m' : '1 grand carreau = 2 m';
    if(this.el.createScaleText) this.el.createScaleText.textContent = '1 unité canvas = 1 cm';
    const frameOn = !!this.el.createFrame?.checked;
    this.el.createPreviewBox?.classList.toggle('has-floor-frame', frameOn);
    this.el.createPreviewBox?.setAttribute('data-frame-type', frameOn ? (this.el.createFrameType?.value || 'mur') : 'none');
  },


  createFloorFrameObjects(plan, opts={}){
    if(!plan?.id || !opts.enabled) return [];
    const type = this.isWallType(opts.type) ? opts.type : 'mur';
    const planW = Math.max(100, Number(plan.width || 0));
    const planH = Math.max(100, Number(plan.height || 0));
    if(planW < 60 || planH < 60) return [];
    const thickness = _planClamp(Number(opts.thickness || this.wallThickness(type)), 6, type === 'mur' ? 45 : 24);
    const inset = Math.max(3, thickness / 2);
    const color = type === 'cloison_grille' ? '#8a96a8' : (type === 'cloison' ? '#7b8798' : '#334155');
    const icon = type === 'cloison_grille' ? 'partition-grid' : (type === 'cloison' ? 'partition' : 'wall');
    const shape = this.wallTypeShape(type);
    const labelBase = type === 'mur' ? 'Mur périphérique' : (type === 'cloison' ? 'Cloison périphérique' : 'Cloison grille périphérique');
    const segments = [
      ['haut', {x:inset, y:inset}, {x:planW - inset, y:inset}],
      ['droite', {x:planW - inset, y:inset}, {x:planW - inset, y:planH - inset}],
      ['bas', {x:planW - inset, y:planH - inset}, {x:inset, y:planH - inset}],
      ['gauche', {x:inset, y:planH - inset}, {x:inset, y:inset}],
    ];
    return segments.map(([side, p1, p2]) => {
      const pad = Math.max(6, thickness / 2 + 4);
      const bounds = {
        x: Math.max(0, Math.round(Math.min(p1.x, p2.x) - pad)),
        y: Math.max(0, Math.round(Math.min(p1.y, p2.y) - pad)),
        width: Math.max(24, Math.round(Math.abs(p2.x - p1.x) + pad * 2)),
        height: Math.max(20, Math.round(Math.abs(p2.y - p1.y) + pad * 2)),
      };
      return {
        planId: plan.id,
        type,
        label: `${labelBase} ${side}`,
        x: bounds.x,
        y: bounds.y,
        width: bounds.width,
        height: bounds.height,
        rotation: 0,
        color,
        icon,
        linkedWarehouseId: plan.warehouseId || null,
        linkedLocationId: null,
        linkedZone: '',
        linkedProductId: null,
        metadata: {
          shape,
          componentLabel: labelBase,
          generatedBy: 'create-floor-frame',
          frameSide: side,
          points: [p1, p2],
          thickness,
          length: Math.round(this.distance(p1, p2)),
          angle: Math.round(this.segmentAngle(p1, p2)),
        }
      };
    });
  },

  async savePlanFromCreateScreen(){
    if(!this.isAdmin) return;
    const name = (this.el.createName?.value || '').trim();
    const warehouseId = this.el.createWarehouse?.value || '';
    if(!name){ this.el.createName?.focus(); toast('Indiquez un nom de plan clair.', 'warning'); return; }
    if(!warehouseId){ this.el.createWarehouse?.focus(); toast('Sélectionnez le magasin concerné.', 'warning'); return; }
    const widthM = this.parseCreateMeters(this.el.createWidth, 16);
    const heightM = this.parseCreateMeters(this.el.createHeight, 10);
    if(widthM < 1 || heightM < 1){ toast('Les dimensions doivent être d’au moins 1 m.', 'warning'); return; }
    const width = this.createMetersToCanvas(widthM);
    const height = this.createMetersToCanvas(heightM);
    const payload = {
      name,
      warehouseId,
      type: this.el.createType?.value || 'depot',
      width,
      height,
      scale: 1,
      active: true,
      metadata: {
        unit: 'cm',
        displayUnit: 'm',
        widthM,
        heightM,
        scaleLabel: '1 unité canvas = 1 cm'
      }
    };
    try{
      if(this.el.createSubmit) this.el.createSubmit.disabled = true;
      const saved = await Api.planCreate(payload);
      if(saved?.id){
        const frameObjects = this.createFloorFrameObjects(saved, {
          enabled: !!this.el.createFrame?.checked,
          type: this.el.createFrameType?.value || 'mur',
          thickness: Number(this.el.createFrameThickness?.value || 0)
        });
        if(frameObjects.length){
          try{ await Api.planObjectsSave(saved.id, frameObjects); }
          catch(frameErr){ toast(frameErr.message || 'Plan créé, mais le contour automatique n’a pas pu être ajouté.', 'warning'); }
        }
      }
      toast(`Plan créé : ${this.formatMeters(widthM)} m × ${this.formatMeters(heightM)} m. Ouverture de l’éditeur.`, 'success');
      if(saved?.id){
        const url = this.appUrl('plans-edit', { planId: saved.id, edit: '1' });
        window.history.replaceState({}, '', url);
        this.el.root?.classList.remove('plans-page--create');
        this.el.root?.setAttribute('data-plan-create', '0');
        this.isCreateFlow = false;
        await this.loadPlansOnly();
        await this.openPlan(saved, { editMode:true, toast:false });
      }
    }catch(e){
      toast(e.message || 'Erreur pendant la création du plan.', 'error');
    }finally{
      if(this.el.createSubmit) this.el.createSubmit.disabled = false;
    }
  },

  setArchitectMode(mode='build'){
    const allowed = ['build','arrange','stock','link'];
    this.architectMode = allowed.includes(mode) ? mode : 'build';
    if(this.wallDraft && this.architectMode !== 'build') this.cancelWallDraft(false);
    document.querySelectorAll('[data-editor-mode]').forEach(btn => {
      const active = btn.dataset.editorMode === this.architectMode;
      btn.classList.toggle('active', active);
      btn.setAttribute('aria-pressed', active ? 'true' : 'false');
    });
    this.renderComponentLibrary();
    this.renderInspector();
  },

  beginComponentPlacement(type, label='composant'){
    if(!this.canEdit()) return;
    if(!type) return;
    if(this.isWallType(type)){
      this.beginWallDrawingMode(type);
      return;
    }
    this.setTool('add:' + type);
    this.markComponentActive(type);
    this.el.canvasWrap?.scrollIntoView?.({ behavior:'smooth', block:'center' });
    if(this.isOpeningType(type)) toast('Cliquez près d’un mur : l’ouverture sera automatiquement alignée dessus.', 'info');
    else toast('Glissez “' + label + '” sur le plan ou cliquez dans le canevas pour le placer.', 'info');
  },

  beginWallDrawingMode(type='mur'){
    if(!this.canEdit()) return;
    const wallType = this.isWallType(type) ? type : 'mur';
    this.cancelWallDraft(false);
    this.dragComponentType = '';
    this.setTool('add:' + wallType);
    this.markComponentActive(wallType);
    this.el.canvasWrap?.scrollIntoView?.({ behavior:'smooth', block:'center' });
    toast(this.wallTypeLabel(wallType) + ' : cliquez le premier point, puis chaque point suivant. Entrée termine, Échap annule.', 'info');
  },

  wallTypeLabel(type){
    return ({ mur:'Mur', cloison:'Cloison', cloison_grille:'Cloison grille' })[String(type || '')] || 'Mur';
  },

  wallTypeShape(type){
    return ({ mur:'wallSegment', cloison:'partitionSegment', cloison_grille:'gridPartitionSegment' })[String(type || '')] || 'wallSegment';
  },

  markComponentActive(type){
    if(!this.el.componentLibrary) return;
    this.el.componentLibrary.querySelectorAll('[data-component-add]').forEach(btn => {
      const active = !!type && btn.dataset.componentAdd === type;
      btn.classList.toggle('active', active);
      btn.setAttribute('aria-pressed', active ? 'true' : 'false');
    });
  },

  fitCanvas(){
    if(!this.selectedPlan) return;
    const wrap = this.el.canvasWrap;
    const rect = wrap?.getBoundingClientRect?.() || { width: 900, height: 560 };
    const w = Math.max(320, rect.width || 900);
    const h = Math.max(320, rect.height || 560);
    const { width: planW, height: planH } = this.planSize();
    this.zoom = _planClamp(Math.min((w - 42) / planW, (h - 42) / planH), 0.18, 2.6);
    this.pan.x = Math.round((w - planW * this.zoom) / 2);
    this.pan.y = Math.round((h - planH * this.zoom) / 2);
    this.guideLines = [];
    this.drawSvg();
  },

  setZoom(next, anchor=null){
    if(!this.selectedPlan) return;
    const oldZoom = this.zoom || 1;
    const nextZoom = _planClamp(next, 0.18, 3.5);
    if(Math.abs(nextZoom - oldZoom) < 0.001) return;
    if(anchor && Number.isFinite(anchor.clientX) && Number.isFinite(anchor.clientY)){
      const before = this.screenToWorld({ clientX:anchor.clientX, clientY:anchor.clientY });
      this.zoom = nextZoom;
      const svg = document.getElementById('planCanvasSvg');
      const rect = svg?.getBoundingClientRect?.() || { left:0, top:0, width:1, height:1 };
      const viewBox = (svg?.getAttribute?.('viewBox') || '').split(/\s+/).map(Number);
      const scaleX = viewBox.length === 4 && rect.width ? viewBox[2] / rect.width : 1;
      const scaleY = viewBox.length === 4 && rect.height ? viewBox[3] / rect.height : 1;
      this.pan.x = Math.round((anchor.clientX - rect.left) * scaleX - before.x * this.zoom);
      this.pan.y = Math.round((anchor.clientY - rect.top) * scaleY - before.y * this.zoom);
    }else{
      const wrap = this.el.canvasWrap?.getBoundingClientRect?.() || { width:900, height:560 };
      const anchorClient = { clientX: wrap.left + wrap.width / 2, clientY: wrap.top + wrap.height / 2 };
      const before = this.screenToWorld(anchorClient);
      this.zoom = nextZoom;
      this.pan.x = Math.round((wrap.width || 900) / 2 - before.x * this.zoom);
      this.pan.y = Math.round((wrap.height || 560) / 2 - before.y * this.zoom);
    }
    this.drawSvg();
  },

  onCanvasWheel(e){
    if(!this.selectedPlan) return;
    e.preventDefault();
    const direction = e.deltaY > 0 ? 0.88 : 1.12;
    this.setZoom(this.zoom * direction, { clientX:e.clientX, clientY:e.clientY });
  },

  snap(v, step=20){
    const n = Number(v || 0);
    return this.snapEnabled ? Math.round(n / step) * step : Math.round(n);
  },

  screenR(px){
    // Rayon (unités-monde) qui rend ~px pixels à l'écran quel que soit le zoom,
    // pour des poignées/extrémités toujours saisissables.
    const z = this.zoom || 1;
    return +(Number(px || 6) / z).toFixed(2);
  },

  soberColor(o){
    // Rendu premium : si la couleur est un ancien défaut pastel de palette, on
    // affiche une teinte architecte sobre. Les couleurs personnalisées sont
    // conservées. Aucune donnée n'est réécrite.
    const MAP = {
      '#e0f2fe':'#eef2f7', '#dbeafe':'#e9eef4', '#ede9fe':'#ecebf4', '#fef3c7':'#f2eee4',
      '#dcfce7':'#e9f1ec', '#f8fafc':'#f4f6f9', '#f1f5f9':'#eef2f7', '#e2e8f0':'#e7ebf1',
      '#c7d2fe':'#dfe5ef', '#a5b4fc':'#d6deec', '#bfdbfe':'#dde5f0', '#bae6fd':'#dbe4ef',
      '#fed7aa':'#ecdcc4', '#bbf7d0':'#e0ebe1', '#fde68a':'#f0e6c8', '#dbeafe ':'#e9eef4'
    };
    const c = String(o.color || '').trim().toLowerCase();
    if(!c) return this.typeColor(o.type);
    return MAP[c] || o.color;
  },

  screenToWorld(e){
    const svg = document.getElementById('planCanvasSvg');
    if(svg?.createSVGPoint && svg?.getScreenCTM){
      const pt = svg.createSVGPoint();
      pt.x = e.clientX;
      pt.y = e.clientY;
      const ctm = svg.getScreenCTM();
      if(ctm){
        const mapped = pt.matrixTransform(ctm.inverse());
        return {
          x: (mapped.x - this.pan.x) / this.zoom,
          y: (mapped.y - this.pan.y) / this.zoom,
        };
      }
    }
    const rect = svg?.getBoundingClientRect?.() || { left:0, top:0 };
    const viewBox = (svg?.getAttribute?.('viewBox') || '').split(/\s+/).map(Number);
    const scaleX = viewBox.length === 4 && rect.width ? viewBox[2] / rect.width : 1;
    const scaleY = viewBox.length === 4 && rect.height ? viewBox[3] / rect.height : 1;
    return {
      x: ((e.clientX - rect.left) * scaleX - this.pan.x) / this.zoom,
      y: ((e.clientY - rect.top) * scaleY - this.pan.y) / this.zoom,
    };
  },

  draggedComponentType(e){
    const dt = e?.dataTransfer;
    let type = this.dragComponentType || '';
    try{ type = type || dt?.getData?.('application/x-magestock-plan-component') || dt?.getData?.('text/plain') || ''; }catch(_err){}
    if(type && this.schema?.objectTypes?.[type]) return type;
    return '';
  },

  drawSvg(){
    if(!this.el.canvasHost || !this.el.canvasEmpty) return;
    const hasPlan = !!this.selectedPlan;
    this.el.canvasEmpty.style.display = hasPlan ? 'none' : 'grid';
    this.el.canvasHost.style.display = hasPlan ? 'block' : 'none';
    if(this.el.badges) this.el.badges.hidden = !hasPlan;
    if(!hasPlan){
      this.el.canvasHost.innerHTML = '';
      if(this.el.badges) this.el.badges.innerHTML = '';
      return;
    }

    const wrapRect = this.el.canvasWrap?.getBoundingClientRect?.() || { width: 900, height: 560 };
    const svgW = Math.max(320, Math.round(wrapRect.width || 900));
    const svgH = Math.max(360, Math.round(wrapRect.height || 560));
    const { width: planW, height: planH } = this.planSize();
    const gridSteps = this.gridStepsForPlan(planW, planH);
    const stageOverlaySvg = this.stageArchitectSvg(planW, planH);
    const roomsSvg = this.closedRoomsSvg(planW, planH);
    const objectsSvg = this.sortedObjectsForRender().map(o => this.objectSvg(o)).join('');
    const wallDraftSvg = this.wallDraftSvg();
    const guidesSvg = this.guidesSvg(planW, planH);
    const overlaysSvg = this.canvasOverlaysSvg(svgW, svgH, planW, planH);
    const adding = this.tool.startsWith('add:');
    const dragging = !!this.interaction;
    const panning = this.tool === 'pan' || this.spacePan;
    this.el.canvasHost.innerHTML = `<svg id="planCanvasSvg" class="plan-canvas-svg${adding ? ' is-adding' : ''}${dragging ? ' is-dragging' : ''}${panning ? ' is-panning-mode' : ''}" viewBox="0 0 ${svgW} ${svgH}" role="img" aria-label="Plan ${_planAttr(this.selectedPlan.name || '')}">
      <defs>
        <filter id="planShadow" x="-14%" y="-14%" width="128%" height="128%"><feDropShadow dx="0" dy="3" stdDeviation="3" flood-color="#0f172a" flood-opacity="0.08"/></filter>
        <filter id="planShadowStrong" x="-14%" y="-14%" width="128%" height="128%"><feDropShadow dx="0" dy="4" stdDeviation="4" flood-color="#2563eb" flood-opacity="0.14"/></filter>
        <pattern id="planGrid" width="${gridSteps.minor}" height="${gridSteps.minor}" patternUnits="userSpaceOnUse">
          <path d="M ${gridSteps.minor} 0 L 0 0 0 ${gridSteps.minor}" fill="none" stroke="rgba(100,116,139,.18)" stroke-width="1"/>
        </pattern>
        <pattern id="planGridBig" width="${gridSteps.major}" height="${gridSteps.major}" patternUnits="userSpaceOnUse">
          <path d="M ${gridSteps.major} 0 L 0 0 0 ${gridSteps.major}" fill="none" stroke="rgba(37,99,235,.18)" stroke-width="1.8"/>
        </pattern>
      </defs>
      <rect width="100%" height="100%" fill="transparent" data-bg="1"></rect>
      <g id="planStageGroup" transform="translate(${this.pan.x} ${this.pan.y}) scale(${this.zoom})">
        <rect class="plan-stage-bg" data-bg="1" x="0" y="0" width="${planW}" height="${planH}" rx="0"></rect>
        <rect class="plan-stage-grid" data-bg="1" x="0" y="0" width="${planW}" height="${planH}" rx="0" fill="url(#planGrid)"></rect>
        <rect class="plan-stage-grid plan-stage-grid--big" data-bg="1" x="0" y="0" width="${planW}" height="${planH}" rx="0" fill="url(#planGridBig)"></rect>
        ${stageOverlaySvg}
        ${roomsSvg}
        ${objectsSvg}
        ${wallDraftSvg}
        ${guidesSvg}
      </g>
      ${overlaysSvg}
      ${this.showLegend ? this.inlineSvgLegend(svgW, svgH) : ''}
    </svg>`;

    const svg = document.getElementById('planCanvasSvg');
    if(this.el.badges){
      const linkedCount = this.objects.filter(o => o.linkedLocationId).length;
      this.el.badges.innerHTML = `<span>Zoom ${Math.round(this.zoom * 100)}%</span><span>${this.objects.length} objet${this.objects.length > 1 ? 's' : ''}</span><span>${linkedCount} lié${linkedCount > 1 ? 's' : ''}</span><span>${this.canEdit() ? 'Édition' : 'Consultation'}</span><span>${this.localizationReadOnly ? 'Localisation produit' : (this.canEdit() ? this.architectModeLabel() : 'Plan fini')}</span>`;
    }
    document.querySelectorAll('.plans-zoom-value').forEach(node => { node.textContent = `${Math.round(this.zoom * 100)}%`; });
    const finishBtn = document.getElementById('btnPlanFinishWall');
    if(finishBtn) finishBtn.hidden = !(this.wallDraft && this.canEdit());
  },

  canvasOverlaysSvg(svgW, svgH, planW, planH){
    if(!this.selectedPlan) return '';
    const scaleX = 22;
    const scaleY = Math.max(34, svgH - 36);
    const targetPx = 150;
    const targetUnits = targetPx / Math.max(0.18, this.zoom || 1);
    const targetMeters = this.unitsToMeters(targetUnits);
    const candidates = [0.25,0.5,1,2,3,5,10,20,30,50,100];
    const chosenMeters = candidates.reduce((best, cur) => Math.abs(cur - targetMeters) < Math.abs(best - targetMeters) ? cur : best, candidates[0]);
    const chosenUnits = this.metersToUnits(chosenMeters);
    const scaleW = _planClamp(chosenUnits * Math.max(0.18, this.zoom || 1), 70, 210);
    const halfLabel = this.formatMeters(chosenMeters / 2);
    const fullLabel = this.formatMeters(chosenMeters) + ' m';
    const scaleLabel = `Échelle réelle · 1 unité = ${this.formatMeters(this.planScale())} cm`;
    const mmW = 154;
    const mmH = 102;
    const mmX = Math.max(16, svgW - mmW - 22);
    const mmY = Math.max(52, svgH - mmH - 22);
    const ratio = Math.min((mmW - 24) / Math.max(1, planW), (mmH - 24) / Math.max(1, planH));
    const pW = Math.max(20, planW * ratio);
    const pH = Math.max(14, planH * ratio);
    const pX = mmX + (mmW - pW) / 2;
    const pY = mmY + (mmH - pH) / 2;
    const viewW = Math.min(pW, pW * 0.55);
    const viewH = Math.min(pH, pH * 0.55);
    const viewX = _planClamp(pX + (-this.pan.x / Math.max(this.zoom, .1)) * ratio, pX, pX + pW - viewW);
    const viewY = _planClamp(pY + (-this.pan.y / Math.max(this.zoom, .1)) * ratio, pY, pY + pH - viewH);
    return `<g class="plan-scale-overlay" aria-hidden="true">
      <line x1="${scaleX}" y1="${scaleY}" x2="${scaleX + scaleW}" y2="${scaleY}" class="plan-scale-line"></line>
      <line x1="${scaleX}" y1="${scaleY}" x2="${scaleX}" y2="${scaleY - 8}" class="plan-scale-tick"></line>
      <line x1="${scaleX + scaleW / 2}" y1="${scaleY}" x2="${scaleX + scaleW / 2}" y2="${scaleY - 8}" class="plan-scale-tick"></line>
      <line x1="${scaleX + scaleW}" y1="${scaleY}" x2="${scaleX + scaleW}" y2="${scaleY - 8}" class="plan-scale-tick"></line>
      <text x="${scaleX}" y="${scaleY - 12}" class="plan-scale-text">0</text>
      <text x="${scaleX + scaleW / 2}" y="${scaleY - 12}" class="plan-scale-text" text-anchor="middle">${_planEsc(halfLabel)}</text>
      <text x="${scaleX + scaleW}" y="${scaleY - 12}" class="plan-scale-text" text-anchor="end">${_planEsc(fullLabel)}</text>
      <text x="${scaleX + scaleW + 14}" y="${scaleY - 2}" class="plan-scale-caption">${_planEsc(scaleLabel)}</text>
    </g>
    <g class="plan-minimap-overlay" aria-hidden="true">
      <rect x="${mmX}" y="${mmY}" width="${mmW}" height="${mmH}" rx="4" class="plan-minimap-bg"></rect>
      <rect x="${pX}" y="${pY}" width="${pW}" height="${pH}" rx="1" class="plan-minimap-plan"></rect>
      <rect x="${viewX}" y="${viewY}" width="${viewW}" height="${viewH}" rx="1" class="plan-minimap-view"></rect>
      <circle cx="${viewX}" cy="${viewY}" r="3" class="plan-minimap-handle"></circle>
      <circle cx="${viewX + viewW}" cy="${viewY}" r="3" class="plan-minimap-handle"></circle>
      <circle cx="${viewX}" cy="${viewY + viewH}" r="3" class="plan-minimap-handle"></circle>
      <circle cx="${viewX + viewW}" cy="${viewY + viewH}" r="3" class="plan-minimap-handle"></circle>
    </g>`;
  },

  architectModeLabel(){
    return ({build:'Construire le plan', arrange:'Aménager', stock:'Stockage', link:'Lier aux emplacements'})[this.architectMode || 'build'] || 'Plan 2D';
  },

  architectModeHint(){
    return ({
      build:'Murs, cloisons, portes, fenêtres et pièces : créez d’abord la structure lisible du local.',
      arrange:'Allées, circulations, zones et repères : rendez le parcours terrain compréhensible.',
      stock:'Armoires, racks, rayonnages, palettes et emplacements : placez le stockage réel.',
      link:'Sélectionnez un élément puis rattachez-le à un emplacement MageStock existant.'
    })[this.architectMode || 'build'] || 'Concevez le plan par étapes.';
  },

  stageArchitectSvg(planW, planH){
    const { major } = this.gridStepsForPlan(planW, planH);
    const ticksX = [];
    const ticksY = [];
    const addXLabel = (x, label) => {
      ticksX.push(`<line class="plan-stage-ruler-tick" x1="${x}" y1="-14" x2="${x}" y2="0"></line>`);
      ticksX.push(`<text class="plan-stage-ruler-text" x="${x}" y="-20" text-anchor="middle">${_planEsc(label)}</text>`);
    };
    const addYLabel = (y, label) => {
      ticksY.push(`<line class="plan-stage-ruler-tick" x1="-14" y1="${y}" x2="0" y2="${y}"></line>`);
      ticksY.push(`<text class="plan-stage-ruler-text" x="-20" y="${y + 3}" text-anchor="end">${_planEsc(label)}</text>`);
    };
    for(let x=0; x<=planW; x+=major){
      ticksX.push(`<line class="plan-stage-ruler-tick" x1="${x}" y1="-14" x2="${x}" y2="0"></line>`);
      if(x > 0 && x < planW) ticksX.push(`<text class="plan-stage-ruler-text" x="${x}" y="-20" text-anchor="middle">${_planEsc(this.formatLengthUnits(x))}</text>`);
    }
    for(let y=0; y<=planH; y+=major){
      ticksY.push(`<line class="plan-stage-ruler-tick" x1="-14" y1="${y}" x2="0" y2="${y}"></line>`);
      if(y > 0 && y < planH) ticksY.push(`<text class="plan-stage-ruler-text" x="-20" y="${y + 3}" text-anchor="end">${_planEsc(this.formatLengthUnits(y))}</text>`);
    }
    if(planW % major !== 0) addXLabel(planW, this.formatLengthUnits(planW));
    if(planH % major !== 0) addYLabel(planH, this.formatLengthUnits(planH));
    return `<g class="plan-stage-architect" aria-hidden="true">
      <rect class="plan-stage-paper-edge" x="0" y="0" width="${planW}" height="${planH}"></rect>
      <g class="plan-stage-ruler plan-stage-ruler--top">${ticksX.join('')}<line x1="0" y1="-1" x2="${planW}" y2="-1"></line></g>
      <g class="plan-stage-ruler plan-stage-ruler--left">${ticksY.join('')}<line x1="-1" y1="0" x2="-1" y2="${planH}"></line></g>
      <text class="plan-stage-dimension plan-stage-dimension--top" x="${planW / 2}" y="${-46}" text-anchor="middle">Longueur ${_planEsc(this.formatLengthUnits(planW))}</text>
      <text class="plan-stage-dimension plan-stage-dimension--left" transform="translate(${-52} ${planH / 2}) rotate(-90)" text-anchor="middle">Largeur ${_planEsc(this.formatLengthUnits(planH))}</text>
    </g>`;
  },

  guidesSvg(planW, planH){
    if(!this.canEdit() || !Array.isArray(this.guideLines) || !this.guideLines.length) return '';
    return this.guideLines.map(g => {
      if(g.axis === 'x') return `<line class="plan-align-guide plan-align-guide--x" x1="${g.value}" y1="0" x2="${g.value}" y2="${planH}"></line>`;
      if(g.axis === 'y') return `<line class="plan-align-guide plan-align-guide--y" x1="0" y1="${g.value}" x2="${planW}" y2="${g.value}"></line>`;
      return '';
    }).join('');
  },

  objectSvg(o){
    const selectedIds = this.selectedObjectIds.length ? this.selectedObjectIds.map(String) : (this.selectedObjectId ? [String(this.selectedObjectId)] : []);
    const selected = selectedIds.includes(String(o.id));
    const highlighted = String(o.id) === String(this.highlightObjectId || '');
    const primary = String(o.id) === String(this.selectedObjectId || selectedIds[0] || '');
    const x = Number(o.x || 0), y = Number(o.y || 0), w = Math.max(10, Number(o.width || 160)), h = Math.max(10, Number(o.height || 80));
    const color = this.soberColor(o);
    const loc = this.locationById[String(o.linkedLocationId || '')] || null;
    const rawLabel = (o.label || loc?.code || this.typeLabel(o.type));
    const structural = ['depot','local','piece','atelier','zone'].includes(o.type);
    const architectural = ['mur','cloison','cloison_grille','ouverture','fenetre','porte','porte_double','escalier'].includes(o.type);
    const wallLike = this.isWallType(o.type);
    const circulation = ['allee','circulation','escalier'].includes(o.type);
    const storage = ['armoire','armoire_double','rack','rayonnage','etagere','palette','emplacement'].includes(o.type);
    const storageSymbolOnly = ['armoire','armoire_double','rack','rayonnage','etagere','palette'].includes(o.type);
    const showLabel = !storageSymbolOnly && !['porte','porte_double','mur','cloison','cloison_grille','ouverture','fenetre'].includes(o.type) && w >= 86 && h >= 38;
    const lineLimit = (w >= 220 && h >= 96) ? 2 : 1;
    const labelLimit = Math.max(8, Math.min(24, Math.floor(w / 14)));
    const labelLines = showLabel ? _planLabelLines(rawLabel, labelLimit, lineLimit) : [];
    const showMeta = !!loc && w >= 190 && h >= 82;
    const meta = showMeta ? _planShortLabel(this.locationTitle(loc), Math.max(10, Math.min(24, Math.floor(w / 14)))) : '';
    const rx = wallLike ? 0 : (['porte','porte_double','ouverture','fenetre'].includes(o.type) ? 1 : (structural || circulation ? 1.5 : 3));
    const dash = (structural || circulation) && !architectural ? ' stroke-dasharray="14 10"' : '';
    const font = _planClamp(Math.min(10.8, Math.max(7.6, Math.min(w / 17, h / 7.2))), 7.6, 10.8);
    const metaFont = _planClamp(font - 1.6, 6.8, 9.2);
    const labelX = structural ? x + Math.max(10, Math.min(18, w * 0.045)) : x + Math.max(8, Math.min(14, w * 0.065));
    const firstLineY = structural ? y + 16 : y + Math.max(14, Math.min(20, h * 0.24));
    const lineStep = font + 2.4;
    const metaY = Math.min(y + h - 8, firstLineY + lineStep * Math.max(1, labelLines.length));
    const deco = this.objectDecorSvg(o, x, y, w, h);
    const linkedBadge = loc ? `<g class="plan-object-linked" data-id="${_planAttr(o.id)}" data-action="move" aria-hidden="true"><circle cx="${x + w - 10}" cy="${y + 10}" r="6"></circle><text x="${x + w - 10}" y="${y + 12.8}" text-anchor="middle">✓</text></g>` : '';
    const handle = (selected && primary && this.canEdit()) ? ['nw','ne','sw','se'].map(pos => {
      const hx = pos.includes('w') ? x : x + w;
      const hy = pos.includes('n') ? y : y + h;
      return `<circle class="plan-object-handle plan-object-handle--${pos}" data-action="resize" data-handle="${pos}" data-id="${_planAttr(o.id)}" cx="${hx}" cy="${hy}" r="${this.screenR(7)}"></circle>`;
    }).join('') : '';
    const rotateHandle = (selected && primary && this.canEdit()) ? `<g class="plan-object-rotate-control" data-action="rotate" data-id="${_planAttr(o.id)}" aria-label="Faire pivoter"><title>Faire pivoter</title><line data-action="rotate" data-id="${_planAttr(o.id)}" x1="${x + w / 2}" y1="${y}" x2="${x + w / 2}" y2="${y - this.screenR(22)}"></line><path class="plan-object-rotate-cue" data-action="rotate" data-id="${_planAttr(o.id)}" d="M ${x + w / 2 - this.screenR(8)} ${y - this.screenR(24)} A ${this.screenR(8)} ${this.screenR(8)} 0 1 1 ${x + w / 2 + this.screenR(6)} ${y - this.screenR(28)}"></path><path class="plan-object-rotate-arrow" data-action="rotate" data-id="${_planAttr(o.id)}" d="M ${x + w / 2 + this.screenR(6)} ${y - this.screenR(28)} l ${this.screenR(4)} ${this.screenR(1)} l ${-this.screenR(2)} ${this.screenR(4)}"></path><circle class="plan-object-rotate-dot" data-action="rotate" data-id="${_planAttr(o.id)}" cx="${x + w / 2}" cy="${y - this.screenR(24)}" r="${this.screenR(5.8)}"></circle></g>` : '';
    const selectFrame = selected ? `<rect class="plan-object-select-frame${primary ? ' is-primary' : ''}" data-id="${_planAttr(o.id)}" data-action="move" x="${x - 5}" y="${y - 5}" width="${w + 10}" height="${h + 10}" rx="${rx + 5}"></rect>` : '';
    const highlightFrame = highlighted ? `<rect class="plan-object-highlight-frame" x="${x - 10}" y="${y - 10}" width="${w + 20}" height="${h + 20}" rx="${rx + 7}"></rect>` : '';
    const labelBg = labelLines.length && !structural && !circulation && h >= 48 ? `<rect class="plan-object-label-bg" x="${x + 7}" y="${y + 7}" width="${Math.max(0, w - 14)}" height="${Math.max(14, Math.min(24, 8 + lineStep * labelLines.length))}" rx="3"></rect>` : '';
    const storageHit = storage ? `<rect class="plan-storage-hit" data-id="${_planAttr(o.id)}" data-action="move" x="${x}" y="${y}" width="${w}" height="${h}" rx="${rx}"></rect>` : '';
    if(this.isWallSegmentObject(o)) return this.wallSegmentObjectSvg(o, selected, primary, highlighted);
    if(this.isOpeningType(o)) return this.openingObjectSvg(o, selected, primary, highlighted);
    const labelSvg = labelLines.length ? `<text class="plan-object-text" data-id="${_planAttr(o.id)}" data-action="move" x="${labelX}" y="${firstLineY}" font-size="${font}">${labelLines.map((line, idx) => `<tspan x="${labelX}" dy="${idx === 0 ? 0 : lineStep}">${_planEsc(line)}</tspan>`).join('')}</text>` : '';
    const metaSvg = showMeta ? `<text class="plan-object-meta" data-id="${_planAttr(o.id)}" data-action="move" x="${labelX}" y="${metaY}" font-size="${metaFont}">${_planEsc(meta)}</text>` : '';
    return `<g class="plan-object plan-object--${_planAttr(o.type || 'objet')}${storage ? ' is-storage' : ''}${architectural ? ' is-architectural' : ''}${wallLike ? ' is-wall' : ''}${this.isWallSegmentObject(o) ? ' is-wall-segment' : ''}${this.isOpeningAttached(o) ? ' is-attached-opening' : ''}${structural ? ' is-structure' : ''}${circulation ? ' is-circulation' : ''}${o.linkedLocationId ? ' is-linked' : ''}${selected ? ' is-selected' : ''}${primary ? ' is-primary' : ''}${highlighted ? ' is-highlighted' : ''}" data-id="${_planAttr(o.id)}" data-action="move" transform="rotate(${Number(o.rotation || 0)} ${x + w / 2} ${y + h / 2})">
      ${highlightFrame}
      ${selectFrame}
      <rect class="plan-object-main" data-id="${_planAttr(o.id)}" data-action="move" x="${x}" y="${y}" width="${w}" height="${h}" rx="${rx}" fill="${_planAttr(color)}"${dash}></rect>
      ${deco}
      ${labelBg}
      ${storageHit}
      ${linkedBadge}
      ${labelSvg}
      ${metaSvg}
      ${rotateHandle}
      ${handle}
    </g>`;
  },
  objectDecorSvg(o, x, y, w, h){
    const INK = '#334155', INK2 = 'rgba(51,65,85,.55)', INK3 = 'rgba(51,65,85,.28)';
    const ve = 'vector-effect="non-scaling-stroke"';
    const L = (x1, y1, x2, y2, sw, st, dash) => `<line x1="${x1.toFixed(1)}" y1="${y1.toFixed(1)}" x2="${x2.toFixed(1)}" y2="${y2.toFixed(1)}" stroke="${st}" stroke-width="${sw}" stroke-linecap="round" ${dash ? `stroke-dasharray="${dash}" ` : ''}${ve}></line>`;
    const R = (rx, ry, rw, rh, st, sw, dash) => `<rect x="${rx.toFixed(1)}" y="${ry.toFixed(1)}" width="${Math.max(0, rw).toFixed(1)}" height="${Math.max(0, rh).toFixed(1)}" rx="2" fill="none" stroke="${st}" stroke-width="${sw}" ${dash ? `stroke-dasharray="${dash}" ` : ''}${ve}></rect>`;

    // Structures : double trait de mur léger
    if(['depot','local','piece','atelier'].includes(o.type)){
      let out = R(x + 6, y + 6, w - 12, h - 12, INK3, 1.1);
      if(o.type === 'atelier'){
        const s = Math.min(24, w * 0.2, h * 0.4);
        out += L(x + w - 6 - s, y + h - 6, x + w - 6, y + h - 6 - s, 1, INK3) + L(x + w - 6 - s * 0.6, y + h - 6, x + w - 6, y + h - 6 - s * 0.6, 1, INK3);
      }
      return out;
    }
    if(o.type === 'zone') return R(x + 6, y + 6, w - 12, h - 12, INK3, 1.1, '8 6');
    if(['allee','circulation'].includes(o.type)){
      const mid = y + h / 2;
      return L(x + 16, mid, x + w - 26, mid, 1.4, INK3, '2 9')
        + `<path d="M ${(x + w - 35).toFixed(1)} ${(mid - 6).toFixed(1)} L ${(x + w - 23).toFixed(1)} ${mid.toFixed(1)} L ${(x + w - 35).toFixed(1)} ${(mid + 6).toFixed(1)}" fill="none" stroke="${INK2}" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" ${ve}></path>`;
    }
    if(o.type === 'escalier'){
      const steps = Math.max(4, Math.min(11, Math.round(w / 22)));
      let out = R(x + 7, y + 7, w - 14, h - 14, INK2, 1.2);
      for(let i = 1; i < steps; i++){ const xx = x + 7 + i * ((w - 14) / steps); out += L(xx, y + 8, xx, y + h - 8, 1, INK3); }
      out += `<path d="M ${(x + 14).toFixed(1)} ${(y + h - 15).toFixed(1)} H ${(x + w - 24).toFixed(1)}" stroke="${INK2}" stroke-width="1.6" stroke-linecap="round" ${ve}></path><path d="M ${(x + w - 33).toFixed(1)} ${(y + h - 21).toFixed(1)} L ${(x + w - 22).toFixed(1)} ${(y + h - 15).toFixed(1)} L ${(x + w - 33).toFixed(1)} ${(y + h - 9).toFixed(1)}" fill="none" stroke="${INK2}" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" ${ve}></path>`;
      return out;
    }
    if(o.type === 'repere') return L(x + 14, y + 13, x + w - 14, y + 13, 1, INK3) + L(x + 14, y + h - 13, x + w - 14, y + h - 13, 1, INK3);

    // Stockage : symboles premium vus de dessus, basés sur des empreintes réelles en cm.
    // DETAIL2 : chaque famille possède une silhouette claire, une face avant et un détail lisible après rotation.
    if(['rack','rayonnage','etagere','armoire','armoire_double','palette','emplacement'].includes(o.type)){
      const fmt = (n) => Number(n || 0).toFixed(1);
      const inset = (min, max, ratio, ref) => Math.max(min, Math.min(max, ref * ratio));
      const bx = x + inset(4, 10, .035, w);
      const by = y + inset(4, 9, .055, h);
      const bw = Math.max(18, w - (bx - x) * 2);
      const bh = Math.max(14, h - (by - y) * 2);
      const R2 = (rx, ry, rw, rh, fill, stroke, sw=1, dash='', cls='') => `<rect${cls ? ` class="${cls}"` : ''} x="${fmt(rx)}" y="${fmt(ry)}" width="${fmt(Math.max(0, rw))}" height="${fmt(Math.max(0, rh))}" rx="1" fill="${fill}" stroke="${stroke}" stroke-width="${sw}" ${dash ? `stroke-dasharray="${dash}" ` : ''}${ve}></rect>`;
      const C2 = (cx, cy, r, fill, stroke='none', sw=1) => `<circle cx="${fmt(cx)}" cy="${fmt(cy)}" r="${fmt(r)}" fill="${fill}" stroke="${stroke}" stroke-width="${sw}" ${ve}></circle>`;
      const P2 = (d, fill, stroke, sw=1, dash='', cls='') => `<path${cls ? ` class="${cls}"` : ''} d="${d}" fill="${fill}" stroke="${stroke}" stroke-width="${sw}" ${dash ? `stroke-dasharray="${dash}" ` : ''}stroke-linecap="round" stroke-linejoin="round" ${ve}></path>`;
      const frontArrow = (stroke='rgba(37,99,235,.42)') => {
        if(bw < 40 || bh < 20) return '';
        const cy = by + bh - Math.max(4, Math.min(8, bh * .14));
        const cx = bx + bw / 2;
        const aw = Math.min(16, Math.max(8, bw * .08));
        return `<g class="plan-storage-front" aria-hidden="true">${L(cx-aw, cy, cx+aw, cy, 1, stroke)}${P2(`M ${fmt(cx+aw-4)} ${fmt(cy-3)} L ${fmt(cx+aw)} ${fmt(cy)} L ${fmt(cx+aw-4)} ${fmt(cy+3)}`, 'none', stroke, 1)}</g>`;
      };

      if(o.type === 'rack'){
        const rail = '#1f2937', upright = '#475569', detail = 'rgba(71,85,105,.40)';
        const fill = 'rgba(248,250,252,.78)';
        const bays = Math.max(2, Math.min(6, Math.round(bw / 70)));
        let out = R2(bx, by, bw, bh, fill, 'rgba(15,23,42,.20)', 1, '', 'plan-storage-surface plan-storage-surface--rack');
        out += R2(bx, by + 3, bw, 7, 'rgba(31,41,55,.92)', 'rgba(15,23,42,.28)', .8, '', 'plan-storage-beam');
        out += R2(bx, by + bh - 10, bw, 7, 'rgba(31,41,55,.92)', 'rgba(15,23,42,.28)', .8, '', 'plan-storage-beam');
        for(let i=0;i<=bays;i++){
          const xx = bx + bw * i / bays;
          out += R2(xx - 2, by + 1, 4, bh - 2, 'rgba(71,85,105,.82)', 'rgba(15,23,42,.22)', .8, '', 'plan-storage-upright');
        }
        const lanes = Math.max(1, Math.min(3, Math.round(bh / 38)));
        for(let j=1;j<lanes;j++){
          const yy = by + bh * j / lanes;
          out += L(bx + 3, yy, bx + bw - 3, yy, 1, detail, '9 6');
        }
        for(let i=0;i<bays;i++){
          const xx0 = bx + bw * i / bays + 5;
          const xx1 = bx + bw * (i+1) / bays - 5;
          if(xx1 - xx0 > 22) out += P2(`M ${fmt(xx0)} ${fmt(by + bh - 13)} L ${fmt(xx1)} ${fmt(by + 13)}`, 'none', 'rgba(71,85,105,.24)', 1, '8 6');
        }
        out += frontArrow('rgba(37,99,235,.38)');
        return out;
      }

      if(o.type === 'rayonnage'){
        const rail = '#475569', detail = 'rgba(51,65,85,.42)';
        const bays = Math.max(2, Math.min(5, Math.round(bw / 55)));
        const levels = Math.max(3, Math.min(5, Math.round(bh / 15)));
        let out = R2(bx, by, bw, bh, 'rgba(240,249,255,.70)', 'rgba(71,85,105,.45)', 1, '', 'plan-storage-surface plan-storage-surface--shelving');
        out += L(bx, by + 2, bx + bw, by + 2, 1.8, rail) + L(bx, by + bh - 2, bx + bw, by + bh - 2, 1.8, rail);
        out += L(bx + 1, by, bx + 1, by + bh, 1.25, rail) + L(bx + bw - 1, by, bx + bw - 1, by + bh, 1.25, rail);
        for(let i=1;i<bays;i++){ const xx = bx + bw * i / bays; out += L(xx, by + 3, xx, by + bh - 3, .9, detail); }
        for(let j=1;j<levels;j++){ const yy = by + bh * j / levels; out += L(bx + 3, yy, bx + bw - 3, yy, .9, detail); }
        out += frontArrow('rgba(14,116,144,.36)');
        return out;
      }

      if(o.type === 'etagere'){
        const rail = '#64748b', detail = 'rgba(71,85,105,.34)';
        const bays = Math.max(2, Math.min(4, Math.round(bw / 44)));
        let out = R2(bx, by, bw, bh, 'rgba(248,250,252,.88)', 'rgba(100,116,139,.48)', 1, '', 'plan-storage-surface plan-storage-surface--shelf');
        out += L(bx + 1, by + 2, bx + bw - 1, by + 2, 1.55, rail) + L(bx + 1, by + bh - 2, bx + bw - 1, by + bh - 2, 1.55, rail);
        out += L(bx + 2, by + bh / 2, bx + bw - 2, by + bh / 2, .9, detail, '6 5');
        for(let i=1;i<bays;i++){ const xx = bx + bw * i / bays; out += L(xx, by + 3, xx, by + bh - 3, .85, detail); }
        out += frontArrow('rgba(100,116,139,.34)');
        return out;
      }

      if(o.type === 'armoire' || o.type === 'armoire_double'){
        const body = o.type === 'armoire_double' ? 'rgba(226,232,240,.90)' : 'rgba(235,239,245,.92)';
        const edge = '#334155', detail = 'rgba(51,65,85,.42)', soft = 'rgba(255,255,255,.35)';
        let out = R2(bx, by, bw, bh, body, edge, 1.45, '', 'plan-storage-surface plan-storage-surface--cabinet');
        out += R2(bx + 3, by + 3, bw - 6, bh - 6, 'rgba(255,255,255,.18)', detail, .8, '', 'plan-storage-cabinet-panel');
        // Face avant et plinthe : permet de lire le sens après rotation.
        out += L(bx + 4, by + bh - 6, bx + bw - 4, by + bh - 6, 1.35, detail);
        out += L(bx + 4, by + 6, bx + bw - 4, by + 6, .8, 'rgba(255,255,255,.60)');
        if(o.type === 'armoire_double'){
          const mid = bx + bw / 2;
          out += L(mid, by + 4, mid, by + bh - 6, 1.15, edge);
          out += L(bx + 6, by + bh - 12, mid - 3, by + bh - 12, .9, detail, '5 4');
          out += L(mid + 3, by + bh - 12, bx + bw - 6, by + bh - 12, .9, detail, '5 4');
          out += C2(mid - 5, by + bh / 2, 1.9, soft, edge, .9) + C2(mid + 5, by + bh / 2, 1.9, soft, edge, .9);
        }else{
          out += L(bx + 7, by + 5, bx + 7, by + bh - 7, .9, 'rgba(51,65,85,.26)');
          out += C2(bx + bw - 9, by + bh / 2, 2, soft, edge, .9);
        }
        out += frontArrow('rgba(51,65,85,.36)');
        return out;
      }

      if(o.type === 'palette'){
        const wood = '#d5b176', dark = '#7c5f3b', line = 'rgba(124,95,59,.55)';
        let out = R2(bx, by, bw, bh, 'rgba(243,215,173,.44)', 'rgba(124,95,59,.54)', 1, '', 'plan-storage-surface plan-storage-surface--pallet');
        const deck = Math.max(3, Math.min(6, Math.round(bw / 24)));
        for(let i=0;i<deck;i++){
          const px = bx + 3 + i * ((bw - 6) / deck);
          const pw = Math.max(3, ((bw - 6) / deck) - 2);
          out += R2(px, by + 3, pw, bh - 6, wood, 'rgba(124,95,59,.32)', .7, '', 'plan-storage-pallet-plank');
        }
        [0.22,0.50,0.78].forEach(pp => out += L(bx + 4, by + bh * pp, bx + bw - 4, by + bh * pp, 1.25, dark));
        [0.16,0.50,0.84].forEach(pxv => [0.22,0.78].forEach(pyv => out += R2(bx + bw * pxv - 2.2, by + bh * pyv - 2.2, 4.4, 4.4, 'rgba(124,95,59,.34)', 'none', 0, '', 'plan-storage-pallet-block')));
        out += L(bx + 4, by + 5, bx + bw - 4, by + 5, .8, 'rgba(255,255,255,.35)');
        return out;
      }

      if(o.type === 'emplacement'){
        const edge = '#16a34a', soft = 'rgba(220,252,231,.36)';
        const corner = Math.min(16, Math.max(8, Math.min(bw, bh) * .18));
        let out = R2(bx, by, bw, bh, soft, 'rgba(22,163,74,.62)', 1.15, '8 5', 'plan-storage-surface plan-storage-surface--location');
        out += L(bx, by, bx + corner, by, 1.7, edge) + L(bx, by, bx, by + corner, 1.7, edge);
        out += L(bx + bw - corner, by, bx + bw, by, 1.7, edge) + L(bx + bw, by, bx + bw, by + corner, 1.7, edge);
        out += L(bx, by + bh - corner, bx, by + bh, 1.7, edge) + L(bx, by + bh, bx + corner, by + bh, 1.7, edge);
        out += L(bx + bw - corner, by + bh, bx + bw, by + bh, 1.7, edge) + L(bx + bw, by + bh - corner, bx + bw, by + bh, 1.7, edge);
        if(bw > 58 && bh > 32){ out += `<text class="plan-storage-location-mark" x="${fmt(bx + bw/2)}" y="${fmt(by + bh/2 + 3)}" text-anchor="middle" ${ve}>EMPL.</text>`; }
        return out;
      }
    }
    return '';
  },

  canvasActionTarget(e){
    if(!e) return null;
    let node = e.target?.closest?.('[data-id]') || null;
    if(node) return node;
    const path = typeof e.composedPath === 'function' ? e.composedPath() : [];
    return path.find(el => el?.dataset?.id) || null;
  },

  onCanvasPointerDown(e){
    if(!this.selectedPlan) return;
    if(e.defaultPrevented && Number(e.button || 0) !== 2) return;
    const target = e.target;
    const insideCanvas = !!(target?.closest?.('#planCanvasSvg') || target === this.el.canvasWrap || target === this.el.canvasHost);
    if(!insideCanvas) return;
    const isRightPan = Number(e.button || 0) === 2;
    if(isRightPan){
      e.preventDefault();
      this.interaction = { type:'pan', clientX:e.clientX, clientY:e.clientY, panX:this.pan.x, panY:this.pan.y, pointerId:e.pointerId, rightButton:true };
      try{ (e.currentTarget || document.getElementById('planCanvasSvg'))?.setPointerCapture?.(e.pointerId); }catch(_err){}
      this.el.canvasWrap?.classList.add('is-right-panning');
      this.drawSvg();
      return;
    }
    const actionEl = this.canvasActionTarget(e);
    const world = this.screenToWorld(e);
    const roomEl = e.target?.closest?.('[data-room-index]') || null;

    if(this.canEdit() && roomEl && !actionEl){
      e.preventDefault();
      this.selectRoomByIndex(Number(roomEl.dataset.roomIndex || -1));
      return;
    }

    if(this.canEdit() && this.isWallTool(this.tool)){
      e.preventDefault();
      this.handleWallToolClick(world, e);
      return;
    }

    if(actionEl){
      const id = String(actionEl.dataset.id || '');
      const multiKey = !!(e.shiftKey || e.ctrlKey || e.metaKey);
      if(multiKey){
        const ids = this.selectedObjectIds.length ? this.selectedObjectIds.slice() : (this.selectedObjectId ? [String(this.selectedObjectId)] : []);
        const index = ids.indexOf(id);
        if(index >= 0 && ids.length > 1) ids.splice(index, 1);
        else if(index < 0) ids.push(id);
        this.setSelection(ids, id);
      }else if(!this.selectedObjectIds.map(String).includes(id)){
        this.setSelection([id], id);
      }else{
        this.selectedObjectId = id;
      }
      this.renderInspector();
      if(!this.canEdit()){
        this.drawSvg();
        return;
      }
      const obj = this.objects.find(o => String(o.id) === id);
      if(!obj) return;
      e.preventDefault();
      const action = actionEl.dataset.action || 'move';
      try{ (e.currentTarget || document.getElementById('planCanvasSvg'))?.setPointerCapture?.(e.pointerId); }catch(_err){}
      const selected = this.selectedObjects();
      const center = { x:Number(obj.x || 0) + Number(obj.width || 0) / 2, y:Number(obj.y || 0) + Number(obj.height || 0) / 2 };
      this.interaction = {
        type: action === 'wall-endpoint' ? 'wall-endpoint' : (action === 'resize' ? 'resize' : (action === 'rotate' ? 'rotate' : 'move')),
        id,
        ids: selected.map(o => String(o.id)),
        startWorld: world,
        start: { x: obj.x, y: obj.y, width: obj.width, height: obj.height, rotation: obj.rotation || 0 },
        center,
        startAngle: Math.atan2(world.y - center.y, world.x - center.x) * 180 / Math.PI,
        handle: actionEl.dataset.handle || 'se',
        endpointIndex: Number(actionEl.dataset.endpoint || 0),
        pointerId: e.pointerId,
        starts: selected.reduce((acc, item) => {
          acc[String(item.id)] = { x:Number(item.x || 0), y:Number(item.y || 0), width:Number(item.width || 10), height:Number(item.height || 10), rotation:Number(item.rotation || 0), points:this.wallPoints(item) };
          return acc;
        }, {}),
      };
      this.el.canvasWrap?.classList.toggle('is-rotating-object', this.interaction.type === 'rotate');
      this.drawSvg();
      return;
    }

    if(this.canEdit() && this.tool.startsWith('add:')){
      e.preventDefault();
      const type = this.tool.split(':')[1] || 'emplacement';
      this.addObject(type, world.x, world.y);
      return;
    }

    e.preventDefault();
    const shouldKeepSelection = this.tool === 'pan' || this.spacePan || e.button === 1;
    if(!shouldKeepSelection) this.clearSelection();
    this.interaction = { type:'pan', clientX:e.clientX, clientY:e.clientY, panX:this.pan.x, panY:this.pan.y };
    this.renderInspector();
    this.drawSvg();
  },

  onCanvasDragOver(e){
    if(!this.canEdit()) return;
    const type = this.draggedComponentType(e);
    if(!type) return;
    e.preventDefault();
    if(e.dataTransfer) e.dataTransfer.dropEffect = 'copy';
    this.el.canvasWrap?.classList.add('is-drop-ready');
  },

  onCanvasDrop(e){
    if(!this.canEdit()) return;
    const type = this.draggedComponentType(e);
    if(!type) return;
    e.preventDefault();
    e.stopPropagation();
    const world = this.screenToWorld(e);
    const dropKey = `${type}|${Math.round(world.x)}|${Math.round(world.y)}|${Math.floor(Date.now() / 180)}`;
    if(this._lastPlanDropKey === dropKey) return;
    this._lastPlanDropKey = dropKey;
    if(this.isWallType(type)){
      this.beginWallDrawingMode(type);
      this.wallDraft = { type, start:this.snapWallPoint(world), chainStart:this.snapWallPoint(world), segments:0 };
      this.wallPreviewPoint = this.wallDraft.start;
      this.drawSvg();
      toast('Point de départ placé. Cliquez les points suivants pour tracer le mur.', 'info');
    }else{
      this.addObject(type, world.x, world.y);
    }
    this.dragComponentType = '';
    this.el.canvasWrap?.classList.remove('is-drop-ready');
  },

  onPointerMove(e){
    if(this.wallDraft && !this.interaction && this.canEdit() && this.selectedPlan){
      const world = this.screenToWorld(e);
      this.wallPreviewPoint = this.prepareWallEndPoint(this.wallDraft.start, world, e);
      this.drawSvg();
      return;
    }
    if(!this.interaction || !this.selectedPlan) return;
    e.preventDefault();
    if(this.interaction.type === 'pan'){
      this.pan.x = Math.round(this.interaction.panX + (e.clientX - this.interaction.clientX));
      this.pan.y = Math.round(this.interaction.panY + (e.clientY - this.interaction.clientY));
      this.drawSvg();
      return;
    }
    if(!this.canEdit()) return;
    const world = this.screenToWorld(e);
    const dx = world.x - this.interaction.startWorld.x;
    const dy = world.y - this.interaction.startWorld.y;
    if(this.interaction.type === 'move'){
      const ids = this.interaction.ids || [this.interaction.id];
      const moved = ids.map(id => {
        const start = this.interaction.starts?.[String(id)];
        if(!start) return null;
        return { id:String(id), x:start.x + dx, y:start.y + dy, width:start.width, height:start.height };
      }).filter(Boolean);
      const guide = this.applyMoveGuides(moved, ids.map(String));
      ids.forEach(id => {
        const obj = this.objects.find(o => String(o.id) === String(id));
        const start = this.interaction.starts?.[String(id)];
        if(!obj || !start) return;
        const rawX = start.x + dx + guide.dx;
        const rawY = start.y + dy + guide.dy;
        const nx = guide.dx ? Math.round(rawX) : this.snap(rawX);
        const ny = guide.dy ? Math.round(rawY) : this.snap(rawY);
        const clamped = this.clampObjectRect({ x:nx, y:ny, width:start.width, height:start.height, rotation:start.rotation });
        if(this.isWallSegmentObject(obj) && Array.isArray(start.points) && start.points.length >= 2){
          const pts = start.points.map(pt => ({ x:this.snap(pt.x + dx + guide.dx), y:this.snap(pt.y + dy + guide.dy) }));
          this.applyWallPoints(obj, pts[0], pts[1]);
        }else{
          obj.x = clamped.x;
          obj.y = clamped.y;
          if(this.isOpeningType(obj.type) && this.snapEnabled) this.snapOpeningObjectToWall(obj);
        }
      });
    }
    if(this.interaction.type === 'wall-endpoint'){
      const obj = this.objects.find(o => String(o.id) === String(this.interaction.id));
      if(!obj || !this.isWallSegmentObject(obj)) return;
      const pts = this.wallPoints(obj);
      const index = Number(this.interaction.endpointIndex || 0) === 1 ? 1 : 0;
      const other = pts[index === 0 ? 1 : 0];
      const next = this.prepareWallEndPoint(other, world, e);
      const p1 = index === 0 ? next : other;
      const p2 = index === 1 ? next : other;
      this.applyWallPoints(obj, p1, p2);
      this.guideLines = this.wallEndpointGuideLines(next, other);
    }
    if(this.interaction.type === 'resize'){
      const obj = this.objects.find(o => String(o.id) === String(this.interaction.id));
      if(!obj) return;
      const start = this.interaction.start;
      const handle = this.interaction.handle || 'se';
      let nx = Number(start.x || 0), ny = Number(start.y || 0), nw = Number(start.width || 24), nh = Number(start.height || 20);
      if(handle.includes('e')) nw = start.width + dx;
      if(handle.includes('s')) nh = start.height + dy;
      if(handle.includes('w')){ nx = start.x + dx; nw = start.width - dx; }
      if(handle.includes('n')){ ny = start.y + dy; nh = start.height - dy; }
      nw = Math.max(24, this.snap(nw));
      nh = Math.max(20, this.snap(nh));
      const { width: planW, height: planH } = this.planSize();
      if(handle.includes('w')) nx = Math.min(start.x + start.width - 24, nx);
      if(handle.includes('n')) ny = Math.min(start.y + start.height - 20, ny);
      const resizeClamp = this.clampObjectRect({ x:this.snap(nx), y:this.snap(ny), width:nw, height:nh, rotation:Number(obj.rotation || 0) });
      nx = resizeClamp.x;
      ny = resizeClamp.y;
      obj.x = nx; obj.y = ny; obj.width = nw; obj.height = nh;
      this.guideLines = this.edgeGuideLines({ x:nx, y:ny, width:nw, height:nh }, [String(obj.id)]).lines;
    }
    if(this.interaction.type === 'rotate'){
      const obj = this.objects.find(o => String(o.id) === String(this.interaction.id));
      if(!obj) return;
      const center = this.interaction.center;
      const angle = Math.atan2(world.y - center.y, world.x - center.x) * 180 / Math.PI;
      const delta = angle - this.interaction.startAngle;
      const raw = Number(this.interaction.start.rotation || 0) + delta;
      obj.rotation = ((this.snapEnabled ? Math.round(raw / 15) * 15 : raw) % 360 + 360) % 360;
      this.guideLines = [];
    }
    this.setDirty(true);
    this.drawSvg();
    this.renderInspector();
  },

  endInteraction(e=null){
    if(!this.interaction) return;
    try{ (this.el.canvasWrap || document.getElementById('planCanvasSvg'))?.releasePointerCapture?.(e?.pointerId || this.interaction.pointerId); }catch(_err){}
    this.el.canvasWrap?.classList.remove('is-right-panning');
    this.el.canvasWrap?.classList.remove('is-rotating-object');
    this.interaction = null;
    this.guideLines = [];
    this.drawSvg();
  },

  addObject(type, wx, wy){
    if(this.isWallType(type)){
      const start = this.snapWallPoint({ x:wx, y:wy });
      this.cancelWallDraft(false);
      this.setTool('add:' + type);
      this.wallDraft = { type, start, chainStart:start, segments:0 };
      this.wallPreviewPoint = start;
      this.drawSvg();
      toast('Point de départ placé. Cliquez le point suivant pour créer le premier segment.', 'info');
      return;
    }
    const size = this.defaultSize(type);
    const defaults = this.schema?.objectTypes?.[type] || {};
    const { width: planW, height: planH } = this.planSize();
    const openingPlacement = this.isOpeningType(type) ? this.openingPlacement(type, wx, wy) : null;
    const x = openingPlacement ? openingPlacement.x : _planClamp(this.snap(wx - size.width / 2), 0, Math.max(0, planW - size.width));
    const y = openingPlacement ? openingPlacement.y : _planClamp(this.snap(wy - size.height / 2), 0, Math.max(0, planH - size.height));
    const obj = this.normalizeClientObject({
      id: 'tmp_' + Date.now() + '_' + Math.random().toString(16).slice(2),
      planId: this.selectedPlan.id,
      type,
      label: defaults.label || 'Objet',
      x,
      y,
      width: openingPlacement?.width || size.width,
      height: openingPlacement?.height || size.height,
      rotation: openingPlacement?.rotation || 0,
      color: defaults.color || '#dbeafe',
      icon: defaults.icon || '',
      linkedWarehouseId: this.selectedPlan.warehouseId || null,
      metadata: { shape: defaults.shape || '', componentLabel: defaults.label || '', ...(this.isOpeningType(type) ? { openingKind:this.openingKind(type), swing:this.openingDefaultSwing(type) } : {}), ...(openingPlacement?.metadata || {}) },
    });
    this.objects.push(obj);
    this.setSelection([obj.id], obj.id);
    this.setTool('select', false);
    this.setDirty(true);
    this.renderEditor();
  },

  defaultSize(type){
    const schema = this.schema?.objectTypes?.[type] || {};
    const map = {
      mur:{width:520,height:22}, cloison:{width:420,height:10}, cloison_grille:{width:420,height:8}, ouverture:{width:150,height:34}, fenetre:{width:160,height:28}, porte:{width:130,height:100}, porte_double:{width:170,height:108},
      depot:{width:520,height:320}, local:{width:420,height:260}, piece:{width:380,height:220}, atelier:{width:440,height:260},
      zone:{width:300,height:160}, allee:{width:420,height:80}, circulation:{width:420,height:120}, escalier:{width:180,height:120},
      armoire:{width:80,height:55}, armoire_double:{width:160,height:55}, rack:{width:300,height:120}, rayonnage:{width:220,height:65},
      etagere:{width:140,height:45}, palette:{width:120,height:80}, emplacement:{width:120,height:70}, repere:{width:120,height:50},
    };
    const fallback = map[type] || { width:160, height:80 };
    return { width:Number(schema.defaultWidth || fallback.width), height:Number(schema.defaultHeight || fallback.height) };
  },

  isWallType(type){
    return ['mur','cloison','cloison_grille'].includes(String(type || ''));
  },

  isWallTool(tool){
    const value = String(tool || '');
    return value.startsWith('add:') && this.isWallType(value.split(':')[1]);
  },

  isOpeningType(type){
    const value = (type && typeof type === 'object') ? type.type : type;
    return ['porte','porte_double','fenetre','ouverture'].includes(String(value || ''));
  },

  isWallSegmentObject(obj){
    return !!(obj && this.isWallType(obj.type) && Array.isArray(obj.metadata?.points) && obj.metadata.points.length >= 2);
  },

  isOpeningAttached(obj){
    return !!(obj && this.isOpeningType(obj.type) && obj.metadata?.attachedWallId);
  },

  normalizePoint(pt){
    return { x:Number(pt?.x || 0), y:Number(pt?.y || 0) };
  },

  wallPoints(obj){
    if(obj?.metadata?.points && Array.isArray(obj.metadata.points) && obj.metadata.points.length >= 2){
      return [this.normalizePoint(obj.metadata.points[0]), this.normalizePoint(obj.metadata.points[1])];
    }
    const x = Number(obj?.x || 0), y = Number(obj?.y || 0), w = Number(obj?.width || 0), h = Number(obj?.height || 0);
    const p1 = { x:x, y:y + h / 2 };
    const p2 = { x:x + w, y:y + h / 2 };
    const angle = Number(obj?.rotation || 0);
    if(!angle) return [p1, p2];
    const c = { x:x + w / 2, y:y + h / 2 };
    return [this.rotatePoint(p1, c, angle), this.rotatePoint(p2, c, angle)];
  },

  rotatePoint(pt, center, angleDeg){
    const rad = Number(angleDeg || 0) * Math.PI / 180;
    const cos = Math.cos(rad), sin = Math.sin(rad);
    const dx = pt.x - center.x, dy = pt.y - center.y;
    return { x:center.x + dx * cos - dy * sin, y:center.y + dx * sin + dy * cos };
  },

  wallThickness(type){
    const fallback = type === 'cloison_grille' ? 8 : (type === 'cloison' ? 10 : 22);
    return Math.max(6, Number(this.defaultSize(type).height || fallback));
  },

  wallDisplayThickness(type, storedThickness=null){
    const raw = Number(storedThickness || this.wallThickness(type));
    if(type === 'cloison_grille') return Math.max(5, Math.min(8, raw));
    if(type === 'cloison') return Math.max(8, Math.min(11, raw));
    return Math.max(20, raw);
  },

  wallBoundsFromPoints(p1, p2, thickness=18){
    const pad = Math.max(6, thickness / 2 + 4);
    const x = Math.min(p1.x, p2.x) - pad;
    const y = Math.min(p1.y, p2.y) - pad;
    const width = Math.abs(p2.x - p1.x) + pad * 2;
    const height = Math.abs(p2.y - p1.y) + pad * 2;
    const plan = this.planSize();
    return {
      x:_planClamp(Math.round(x), 0, plan.width),
      y:_planClamp(Math.round(y), 0, plan.height),
      width:Math.max(24, Math.round(width)),
      height:Math.max(20, Math.round(height)),
    };
  },

  applyWallPoints(obj, p1, p2){
    if(!obj) return;
    const thickness = this.wallThickness(obj.type);
    const plan = this.planSize();
    const a = { x:_planClamp(Number(p1.x || 0), 0, plan.width), y:_planClamp(Number(p1.y || 0), 0, plan.height) };
    const b = { x:_planClamp(Number(p2.x || 0), 0, plan.width), y:_planClamp(Number(p2.y || 0), 0, plan.height) };
    const bounds = this.wallBoundsFromPoints(a, b, thickness);
    obj.x = bounds.x;
    obj.y = bounds.y;
    obj.width = bounds.width;
    obj.height = bounds.height;
    obj.rotation = 0;
    obj.metadata = { ...(obj.metadata || {}), shape:this.wallTypeShape(obj.type), points:[a,b], thickness, length:Math.round(this.distance(a,b)), angle:Math.round(this.segmentAngle(a,b)) };
  },

  distance(a,b){
    return Math.hypot(Number(b.x || 0) - Number(a.x || 0), Number(b.y || 0) - Number(a.y || 0));
  },

  segmentAngle(a,b){
    return Math.atan2(Number(b.y || 0) - Number(a.y || 0), Number(b.x || 0) - Number(a.x || 0)) * 180 / Math.PI;
  },

  clampPoint(pt){
    const plan = this.planSize();
    return { x:_planClamp(Number(pt.x || 0), 0, plan.width), y:_planClamp(Number(pt.y || 0), 0, plan.height) };
  },

  wallAngleStep(evt=null){
    if(evt?.shiftKey) return 90;
    if(!this.snapEnabled) return 0;
    const mode = String(this.wallAngleMode || this.el?.wallAngleSnap?.value || '15');
    if(mode === 'free' || mode === 'libre') return 0;
    const step = parseInt(mode, 10);
    return [15,30,45,90].includes(step) ? step : 15;
  },

  nearestAnglePoint(start, pt, evt=null){
    const stepDeg = this.wallAngleStep(evt);
    if(!stepDeg) return pt;
    const dx = pt.x - start.x;
    const dy = pt.y - start.y;
    const len = Math.max(1, Math.hypot(dx, dy));
    const angle = Math.atan2(dy, dx);
    const step = stepDeg * Math.PI / 180;
    const snapped = Math.round(angle / step) * step;
    return { x:start.x + Math.cos(snapped) * len, y:start.y + Math.sin(snapped) * len };
  },

  wallEndpoints(skipIds=[]){
    const skip = new Set((skipIds || []).map(String));
    const pts = [];
    this.objects.forEach(o => {
      if(skip.has(String(o.id)) || !this.isWallType(o.type)) return;
      this.wallPoints(o).forEach((pt, index) => pts.push({ ...pt, objectId:String(o.id), endpoint:index }));
    });
    return pts;
  },

  snapWallEndpointOnly(pt, skipIds=[]){
    let next = this.clampPoint(pt);
    if(!this.snapEnabled) return next;
    const tol = Math.max(14, 20 / Math.max(0.35, this.zoom || 1));
    let best = null;
    this.wallEndpoints(skipIds).forEach(candidate => {
      const dist = this.distance(next, candidate);
      if(dist <= tol && (!best || dist < best.dist)) best = { dist, candidate };
    });
    if(best) next = { x:best.candidate.x, y:best.candidate.y };
    return next;
  },

  snapWallPoint(pt, skipIds=[]){
    const gridPoint = this.snapEnabled ? { x:this.snap(pt.x), y:this.snap(pt.y) } : pt;
    return this.snapWallEndpointOnly(gridPoint, skipIds);
  },

  prepareWallEndPoint(start, pt, evt=null){
    const base = this.snapEnabled ? this.clampPoint({ x:this.snap(pt.x), y:this.snap(pt.y) }) : this.clampPoint(pt);
    const angled = this.nearestAnglePoint(start, base, evt);
    return this.snapWallEndpointOnly(angled);
  },

  handleWallToolClick(world, evt=null){
    const type = this.tool.split(':')[1] || 'mur';
    if(!this.wallDraft){
      const start = this.snapWallPoint(world);
      this.wallDraft = { type, start, chainStart:start, segments:0, startedAt:Date.now() };
      this.wallPreviewPoint = start;
      this.setSelection([], null);
      this.drawSvg();
      toast('Point de départ placé. Cliquez un second point pour créer le premier segment.', 'info');
      return;
    }
    const draft = this.wallDraft;
    let end = this.prepareWallEndPoint(draft.start, world, evt);
    const closeTolerance = Math.max(22, 28 / Math.max(0.35, this.zoom || 1));
    const canClose = (Number(draft.segments || 0) >= 2) && draft.chainStart && this.distance(end, draft.chainStart) <= closeTolerance;
    if(canClose) end = { x:draft.chainStart.x, y:draft.chainStart.y };
    if(this.distance(draft.start, end) < 40){
      toast('Segment trop court. Cliquez plus loin, ou Échap pour annuler le tracé.', 'warning');
      return;
    }
    const created = this.addWallSegment(draft.type, draft.start, end, { silent:true, prepared:true });
    if(!created) return;
    if(canClose){
      this.wallDraft = null;
      this.wallPreviewPoint = null;
      this.setTool('select', false);
      this.setSelection([created.id], created.id);
      this.setDirty(true);
      this.renderEditor();
      toast('Contour fermé détecté. La pièce reste modifiable point par point.', 'success');
      return;
    }
    this.wallDraft = { type:draft.type, start:end, chainStart:draft.chainStart || draft.start, segments:Number(draft.segments || 0) + 1 };
    this.wallPreviewPoint = end;
    this.setSelection([created.id], created.id);
    this.setDirty(true);
    this.drawSvg();
    this.renderInspector();
  },

  finishWallChain(){
    if(!this.wallDraft) return;
    this.wallDraft = null;
    this.wallPreviewPoint = null;
    this.setTool('select');
    toast('Tracé terminé. La structure reste modifiable point par point.', 'info');
  },

  addWallSegment(type, start, end, options={}){
    const defaults = this.schema?.objectTypes?.[type] || {};
    const thickness = this.wallThickness(type);
    const p1 = this.snapWallPoint(start);
    const p2 = options?.prepared ? this.clampPoint(end) : this.prepareWallEndPoint(p1, end, options?.event || null);
    if(this.distance(p1, p2) < 40) return null;
    const bounds = this.wallBoundsFromPoints(p1, p2, thickness);
    const obj = this.normalizeClientObject({
      id:'tmp_' + Date.now() + '_' + Math.random().toString(16).slice(2),
      planId:this.selectedPlan.id,
      type,
      label:defaults.label || this.wallTypeLabel(type),
      x:bounds.x,
      y:bounds.y,
      width:bounds.width,
      height:bounds.height,
      rotation:0,
      color:defaults.color || (type === 'cloison_grille' ? '#8a96a8' : (type === 'cloison' ? '#7b8798' : '#334155')),
      icon:defaults.icon || '',
      linkedWarehouseId:this.selectedPlan.warehouseId || null,
      metadata:{ shape:this.wallTypeShape(type), componentLabel:defaults.label || '', points:[p1,p2], thickness, length:Math.round(this.distance(p1,p2)), angle:Math.round(this.segmentAngle(p1,p2)) },
    });
    this.objects.push(obj);
    this.setSelection([obj.id], obj.id);
    this.setDirty(true);
    if(options?.silent){
      this.drawSvg();
      this.renderInspector();
    }else{
      this.renderEditor();
      toast(this.wallTypeLabel(type) + ' dessiné.', 'success');
    }
    return obj;
  },

  cancelWallDraft(redraw=true){
    this.wallDraft = null;
    this.wallPreviewPoint = null;
    if(redraw) this.drawSvg();
  },

  wallDraftSvg(){
    if(!this.wallDraft || !this.wallPreviewPoint) return '';
    const start = this.wallDraft.start;
    const end = this.wallPreviewPoint;
    const first = this.wallDraft.chainStart || start;
    const thickness = this.wallThickness(this.wallDraft.type);
    const length = Math.round(this.distance(start, end));
    const mid = { x:(start.x + end.x) / 2, y:(start.y + end.y) / 2 };
    const closing = Number(this.wallDraft.segments || 0) >= 2 && this.distance(end, first) <= Math.max(24, 30 / Math.max(0.35, this.zoom || 1));
    const draftClass = this.wallDraft.type === 'cloison_grille' ? ' plan-wall-draft--grid' : (this.wallDraft.type === 'cloison' ? ' plan-wall-draft--partition' : '');
    const isStartOnly = this.distance(start, end) < 2;
    if(isStartOnly){
      return `<g class="plan-wall-draft plan-wall-draft--start${draftClass}" aria-hidden="true"><circle cx="${start.x}" cy="${start.y}" r="8"></circle><line x1="${start.x - 18}" y1="${start.y}" x2="${start.x + 18}" y2="${start.y}" stroke-width="2"></line><line x1="${start.x}" y1="${start.y - 18}" x2="${start.x}" y2="${start.y + 18}" stroke-width="2"></line><text x="${start.x + 14}" y="${start.y - 16}">point 1</text></g>`;
    }
    return `<g class="plan-wall-draft${draftClass}" aria-hidden="true"><line x1="${start.x}" y1="${start.y}" x2="${end.x}" y2="${end.y}" stroke-width="${thickness}"></line><circle cx="${start.x}" cy="${start.y}" r="6"></circle><circle cx="${end.x}" cy="${end.y}" r="6"></circle>${this.wallDraft.segments ? `<circle class="plan-wall-chain-start" cx="${first.x}" cy="${first.y}" r="8"></circle>` : ''}<text x="${mid.x}" y="${mid.y - 12}" text-anchor="middle">${closing ? 'Fermer' : _planEsc(this.formatLengthUnits(length))}</text></g>`;
  },

  wallEndpointGuideLines(pt, other){
    const lines = [];
    if(Math.abs(pt.x - other.x) <= 1) lines.push({ axis:'x', value:pt.x });
    if(Math.abs(pt.y - other.y) <= 1) lines.push({ axis:'y', value:pt.y });
    return lines;
  },

  getWallSegments(skipIds=[]){
    const skip = new Set((skipIds || []).map(String));
    return this.objects.filter(o => this.isWallType(o.type) && !skip.has(String(o.id))).map(o => {
      const [p1,p2] = this.wallPoints(o);
      const length = this.distance(p1,p2);
      return { object:o, id:String(o.id), type:o.type, p1, p2, length, angle:this.segmentAngle(p1,p2), thickness:this.wallThickness(o.type) };
    }).filter(seg => seg.length >= 25);
  },

  projectPointToSegment(pt, seg){
    const ax = seg.p1.x, ay = seg.p1.y, bx = seg.p2.x, by = seg.p2.y;
    const dx = bx - ax, dy = by - ay;
    const lenSq = dx * dx + dy * dy || 1;
    const t = _planClamp(((pt.x - ax) * dx + (pt.y - ay) * dy) / lenSq, 0, 1);
    const projected = { x:ax + dx * t, y:ay + dy * t };
    return { point:projected, t, distance:this.distance(pt, projected), angle:this.segmentAngle(seg.p1, seg.p2), segment:seg };
  },

  closestWall(pt, skipIds=[]){
    let best = null;
    this.getWallSegments(skipIds).forEach(seg => {
      const projected = this.projectPointToSegment(pt, seg);
      if(!best || projected.distance < best.distance) best = projected;
    });
    return best;
  },

  openingSnapDistance(type, segment=null){
    const size = this.defaultSize(type);
    const objectDepth = Math.max(Number(size.height || 0), Number(size.width || 0) * 0.32);
    const wallThickness = Number(segment?.thickness || 0);
    return Math.max(72, Math.min(150, objectDepth + wallThickness + 26));
  },

  openingPlacement(type, wx, wy){
    const size = this.defaultSize(type);
    const closest = this.closestWall({ x:wx, y:wy });
    if(!closest || closest.distance > this.openingSnapDistance(type, closest.segment)) return null;
    const angle = ((closest.angle % 360) + 360) % 360;
    const width = size.width;
    const height = Math.max(size.height, closest.segment.thickness + 12);
    const point = closest.point;
    return {
      x:Number((point.x - width / 2).toFixed(2)),
      y:Number((point.y - height / 2).toFixed(2)),
      width,
      height,
      rotation:Math.round(angle),
      metadata:{ attachedWallId:closest.segment.id, wallOffset:Number(closest.t.toFixed(4)), wallAngle:Math.round(angle), snapMode:'wall', openingKind:this.openingKind(type), swing:this.openingDefaultSwing(type) }
    };
  },

  snapOpeningObjectToWall(obj){
    if(!obj || !this.isOpeningType(obj.type)) return false;
    const cx = Number(obj.x || 0) + Number(obj.width || 0) / 2;
    const cy = Number(obj.y || 0) + Number(obj.height || 0) / 2;
    const closest = this.closestWall({ x:cx, y:cy }, [String(obj.id)]);
    if(!closest || closest.distance > this.openingSnapDistance(obj.type, closest?.segment)){
      obj.metadata = { ...(obj.metadata || {}), attachedWallId:'', snapMode:'' };
      return false;
    }
    const angle = ((closest.angle % 360) + 360) % 360;
    obj.x = Number((closest.point.x - Number(obj.width || 0) / 2).toFixed(2));
    obj.y = Number((closest.point.y - Number(obj.height || 0) / 2).toFixed(2));
    obj.rotation = Math.round(angle);
    obj.metadata = { ...(obj.metadata || {}), attachedWallId:closest.segment.id, wallOffset:Number(closest.t.toFixed(4)), wallAngle:Math.round(angle), snapMode:'wall', openingKind:obj.metadata?.openingKind || this.openingKind(obj.type), swing:obj.metadata?.swing || this.openingDefaultSwing(obj.type) };
    return true;
  },

  wallOpeningsForSegment(wallObj){
    const wallId = String(wallObj?.id || '');
    if(!wallId) return [];
    const [p1,p2] = this.wallPoints(wallObj);
    const wallLength = Math.max(1, this.distance(p1,p2));
    return this.objects.filter(o => this.isOpeningType(o.type) && String(o.metadata?.attachedWallId || '') === wallId).map(o => {
      const t = _planClamp(Number(o.metadata?.wallOffset || 0.5), 0.02, 0.98);
      const cutLength = Math.max(44, Math.min(wallLength * 0.72, Number(o.width || this.defaultSize(o.type).width || 120)));
      const half = (cutLength / wallLength) / 2;
      return { object:o, t, from:_planClamp(t - half, 0, 1), to:_planClamp(t + half, 0, 1), length:cutLength };
    }).sort((a,b) => a.from - b.from);
  },

  wallSegmentPieces(openings){
    const ranges = [];
    (openings || []).forEach(o => {
      if(o.to <= 0 || o.from >= 1) return;
      const last = ranges[ranges.length - 1];
      const from = _planClamp(o.from, 0, 1);
      const to = _planClamp(o.to, 0, 1);
      if(!last || from > last.to) ranges.push({ from, to });
      else last.to = Math.max(last.to, to);
    });
    const pieces = [];
    let cursor = 0;
    ranges.forEach(r => {
      if(r.from - cursor > 0.01) pieces.push({ from:cursor, to:r.from });
      cursor = Math.max(cursor, r.to);
    });
    if(1 - cursor > 0.01) pieces.push({ from:cursor, to:1 });
    return pieces.length ? pieces : [{ from:0, to:1 }];
  },

  segmentPoint(p1, p2, t){
    return { x:p1.x + (p2.x - p1.x) * t, y:p1.y + (p2.y - p1.y) * t };
  },

  normalForSegment(p1,p2){
    const dx = p2.x - p1.x, dy = p2.y - p1.y;
    const len = Math.max(1, Math.hypot(dx,dy));
    return { x:-dy / len, y:dx / len };
  },

  wallOpeningCutSvg(opening, p1, p2, thickness){
    const a = this.segmentPoint(p1, p2, opening.from);
    const b = this.segmentPoint(p1, p2, opening.to);
    const n = this.normalForSegment(p1, p2);
    const cap = Math.max(8, thickness / 2 + 3);
    return `<g class="plan-wall-cut" aria-hidden="true">
      <line x1="${a.x}" y1="${a.y}" x2="${b.x}" y2="${b.y}" stroke-width="${Math.max(10, thickness + 5)}"></line>
      <line class="plan-wall-cut-cap" x1="${a.x - n.x * cap}" y1="${a.y - n.y * cap}" x2="${a.x + n.x * cap}" y2="${a.y + n.y * cap}"></line>
      <line class="plan-wall-cut-cap" x1="${b.x - n.x * cap}" y1="${b.y - n.y * cap}" x2="${b.x + n.x * cap}" y2="${b.y + n.y * cap}"></line>
    </g>`;
  },

  openingObjectSvg(o, selected, primary, highlighted){
    const rawX = Number(o.x || 0), rawY = Number(o.y || 0);
    const w = Math.max(40, Number(o.width || 120));
    const sourceH = Math.max(24, Number(o.height || 42));
    const minH = this.openingRecommendedHeight(o.type, w);
    const h = Math.max(sourceH, minH);
    const x = rawX;
    const y = rawY - Math.max(0, h - sourceH) / 2;
    const cx = x + w / 2, cy = y + h / 2;
    const angle = Number(o.rotation || o.metadata?.wallAngle || 0);
    const attached = this.isOpeningAttached(o);
    const selectedCls = selected ? ' is-selected' : '';
    const primaryCls = primary ? ' is-primary' : '';
    const attachedCls = attached ? ' is-attached-opening' : ' is-unattached-opening';
    const highlightFrame = highlighted ? `<rect class="plan-object-highlight-frame" x="${x - 6}" y="${y - 6}" width="${w + 12}" height="${h + 12}" rx="1.5"></rect>` : '';
    const selectFrame = selected ? `<rect class="plan-object-select-frame${primary ? ' is-primary' : ''}" data-id="${_planAttr(o.id)}" data-action="move" x="${x - 3}" y="${y - 3}" width="${w + 6}" height="${h + 6}" rx="1.5"></rect>` : '';
    const handle = (selected && primary && this.canEdit()) ? ['nw','ne','sw','se'].map(pos => {
      const hx = pos.includes('w') ? x : x + w;
      const hy = pos.includes('n') ? y : y + h;
      return `<circle class="plan-object-handle plan-object-handle--${pos}" data-action="resize" data-handle="${pos}" data-id="${_planAttr(o.id)}" cx="${hx}" cy="${hy}" r="${this.screenR(4.2)}"></circle>`;
    }).join('') : '';
    const rotateHandle = (selected && primary && this.canEdit()) ? `<g class="plan-object-rotate-control" data-action="rotate" data-id="${_planAttr(o.id)}" aria-label="Faire pivoter"><title>Faire pivoter</title><line data-action="rotate" data-id="${_planAttr(o.id)}" x1="${cx}" y1="${y}" x2="${cx}" y2="${y - this.screenR(18)}"></line><path class="plan-object-rotate-cue" data-action="rotate" data-id="${_planAttr(o.id)}" d="M ${cx - this.screenR(7)} ${y - this.screenR(20)} A ${this.screenR(7)} ${this.screenR(7)} 0 1 1 ${cx + this.screenR(5)} ${y - this.screenR(23)}"></path><path class="plan-object-rotate-arrow" data-action="rotate" data-id="${_planAttr(o.id)}" d="M ${cx + this.screenR(5)} ${y - this.screenR(23)} l ${this.screenR(3.5)} ${this.screenR(.8)} l ${-this.screenR(1.8)} ${this.screenR(3.5)}"></path><circle class="plan-object-rotate-dot" data-action="rotate" data-id="${_planAttr(o.id)}" cx="${cx}" cy="${y - this.screenR(20)}" r="${this.screenR(5.2)}"></circle></g>` : '';
    const shape = this.openingShapeSvg(o, x, y, w, h);
    return `<g class="plan-object plan-opening plan-object--${_planAttr(o.type || 'ouverture')}${attachedCls}${selectedCls}${primaryCls}${highlighted ? ' is-highlighted' : ''}" data-id="${_planAttr(o.id)}" data-action="move" transform="rotate(${angle} ${cx} ${cy})">
      ${highlightFrame}${selectFrame}<rect class="plan-opening-hit" data-id="${_planAttr(o.id)}" data-action="move" x="${x - 5}" y="${y - 5}" width="${w + 10}" height="${h + 10}" rx="1.5"></rect>${shape}${rotateHandle}${handle}
    </g>`;
  },

  openingRecommendedHeight(type, width){
    const w = Math.max(40, Number(width || 0));
    if(type === 'porte') return Math.max(74, Math.min(118, w * 0.72));
    if(type === 'porte_double') return Math.max(82, Math.min(126, w * 0.64));
    if(type === 'fenetre') return Math.max(28, Math.min(38, w * 0.22));
    if(type === 'ouverture') return Math.max(34, Math.min(46, w * 0.28));
    return 24;
  },

  openingShapeSvg(o, x, y, w, h){
    const type = String(o.type || 'ouverture');
    const cx = x + w / 2;
    const cy = y + h / 2;
    const m = Math.max(8, Math.min(18, w * 0.085));
    const left = x + m;
    const right = x + w - m;
    const baseline = y + h / 2;
    const jambUp = Math.max(11, Math.min(22, h * 0.22));
    const jambDown = Math.max(7, Math.min(14, h * 0.12));
    const stub = Math.max(8, Math.min(18, w * 0.11));
    const attached = this.isOpeningAttached(o);
    const stubsAt = (yy) => attached ? '' : `<line class="plan-opening-wall-stub" x1="${x + 3}" y1="${yy}" x2="${left}" y2="${yy}"></line><line class="plan-opening-wall-stub" x1="${right}" y1="${yy}" x2="${x + w - 3}" y2="${yy}"></line>`;

    if(type === 'fenetre'){
      const top = cy - Math.max(5, Math.min(8, h * 0.24));
      const bottom = cy + Math.max(5, Math.min(8, h * 0.24));
      const glass = cy;
      const innerLeft = left + Math.max(4, Math.min(8, w * 0.035));
      const innerRight = right - Math.max(4, Math.min(8, w * 0.035));
      const mullions = w >= 120
        ? `<line class="plan-window-mullion" x1="${x + w / 3}" y1="${top + 1}" x2="${x + w / 3}" y2="${bottom - 1}"></line><line class="plan-window-mullion" x1="${x + (w * 2) / 3}" y1="${top + 1}" x2="${x + (w * 2) / 3}" y2="${bottom - 1}"></line>`
        : `<line class="plan-window-mullion" x1="${cx}" y1="${top + 1}" x2="${cx}" y2="${bottom - 1}"></line>`;
      return `<g class="plan-opening-shape plan-opening-window">
        ${stubsAt(cy)}
        <line class="plan-opening-jamb" x1="${left}" y1="${top - 3}" x2="${left}" y2="${bottom + 3}"></line>
        <line class="plan-opening-jamb" x1="${right}" y1="${top - 3}" x2="${right}" y2="${bottom + 3}"></line>
        <line class="plan-window-rail plan-window-rail--outer" x1="${innerLeft}" y1="${top}" x2="${innerRight}" y2="${top}"></line>
        <line class="plan-window-rail plan-window-rail--outer" x1="${innerLeft}" y1="${bottom}" x2="${innerRight}" y2="${bottom}"></line>
        <line class="plan-opening-glass" x1="${innerLeft + 2}" y1="${glass}" x2="${innerRight - 2}" y2="${glass}"></line>
        ${mullions}
      </g>`;
    }

    if(type === 'ouverture'){
      const top = cy - Math.max(10, Math.min(18, h * 0.36));
      const bottom = cy + Math.max(10, Math.min(18, h * 0.36));
      const ret = Math.max(6, Math.min(12, w * 0.06));
      return `<g class="plan-opening-shape plan-opening-passage">
        ${stubsAt(cy)}
        <line class="plan-opening-jamb" x1="${left}" y1="${top}" x2="${left}" y2="${bottom}"></line>
        <line class="plan-opening-jamb" x1="${right}" y1="${top}" x2="${right}" y2="${bottom}"></line>
        <line class="plan-passage-return" x1="${left}" y1="${top}" x2="${left + ret}" y2="${top}"></line>
        <line class="plan-passage-return" x1="${left}" y1="${bottom}" x2="${left + ret}" y2="${bottom}"></line>
        <line class="plan-passage-return" x1="${right - ret}" y1="${top}" x2="${right}" y2="${top}"></line>
        <line class="plan-passage-return" x1="${right - ret}" y1="${bottom}" x2="${right}" y2="${bottom}"></line>
      </g>`;
    }

    if(type === 'porte_double'){
      const reverse = o.metadata?.swing === 'double-reverse';
      const hingeY = baseline;
      const leafTop = reverse ? y + h - Math.max(16, h * 0.18) : y + Math.max(12, h * 0.11);
      const leafJoin = cx;
      const jambTop = hingeY - jambUp;
      const jambBottom = hingeY + jambDown;
      const controlY = reverse ? hingeY + h * 0.30 : hingeY - h * 0.42;
      return `<g class="plan-opening-shape plan-opening-door plan-opening-door--double">
        ${stubsAt(hingeY)}
        <line class="plan-opening-jamb" x1="${left}" y1="${jambTop}" x2="${left}" y2="${jambBottom}"></line>
        <line class="plan-opening-jamb" x1="${right}" y1="${jambTop}" x2="${right}" y2="${jambBottom}"></line>
        <line class="plan-door-leaf" x1="${left}" y1="${hingeY}" x2="${leafJoin}" y2="${leafTop}"></line>
        <line class="plan-door-leaf" x1="${right}" y1="${hingeY}" x2="${leafJoin}" y2="${leafTop}"></line>
        <path class="plan-door-arc" d="M ${cx} ${hingeY} Q ${left + (cx-left)*0.68} ${controlY} ${leafJoin} ${leafTop}"></path>
        <path class="plan-door-arc" d="M ${cx} ${hingeY} Q ${right - (right-cx)*0.68} ${controlY} ${leafJoin} ${leafTop}"></path>
        <circle class="plan-door-hinge" cx="${left}" cy="${hingeY}" r="1.45"></circle>
        <circle class="plan-door-hinge" cx="${right}" cy="${hingeY}" r="1.45"></circle>
      </g>`;
    }

    const hingeRight = o.metadata?.swing === 'left';
    const hingeX = hingeRight ? right : left;
    const stopX = hingeRight ? left : right;
    const dir = hingeRight ? -1 : 1;
    const hingeY = baseline;
    const jambTop = hingeY - jambUp;
    const jambBottom = hingeY + jambDown;
    const leafLen = Math.min(Math.abs(stopX - hingeX) * 0.78, h * 0.78);
    const leafTipX = hingeX + dir * Math.max(10, leafLen * 0.24);
    const leafTipY = hingeY - leafLen;
    const controlX = hingeX + dir * Math.abs(stopX - hingeX) * 0.74;
    const controlY = hingeY - Math.max(18, h * 0.44);
    return `<g class="plan-opening-shape plan-opening-door plan-opening-door--single">
      ${stubsAt(hingeY)}
      <line class="plan-opening-jamb" x1="${left}" y1="${jambTop}" x2="${left}" y2="${jambBottom}"></line>
      <line class="plan-opening-jamb" x1="${right}" y1="${jambTop}" x2="${right}" y2="${jambBottom}"></line>
      <line class="plan-door-leaf" x1="${hingeX}" y1="${hingeY}" x2="${leafTipX}" y2="${leafTipY}"></line>
      <path class="plan-door-arc" d="M ${stopX} ${hingeY} Q ${controlX} ${controlY} ${leafTipX} ${leafTipY}"></path>
      <circle class="plan-door-hinge" cx="${hingeX}" cy="${hingeY}" r="1.45"></circle>
    </g>`;
  },

  gridPartitionBarsSvg(p1, p2, pieces, thickness){
    const length = Math.max(1, this.distance(p1, p2));
    const step = Math.max(54, Math.min(88, length / 9));
    const angle = Math.atan2(p2.y - p1.y, p2.x - p1.x);
    const nx = -Math.sin(angle);
    const ny = Math.cos(angle);
    const half = Math.max(5, thickness * 0.72);
    const bars = [];
    (pieces || [{from:0,to:1}]).forEach(piece => {
      const from = Math.max(0.02, Number(piece.from || 0));
      const to = Math.min(0.98, Number(piece.to || 1));
      const start = Math.ceil((from * length) / step) * step;
      const end = to * length;
      for(let d = start; d <= end; d += step){
        const t = d / length;
        const c = this.segmentPoint(p1, p2, t);
        bars.push(`<line class="plan-wall-grid-bar" x1="${(c.x - nx * half).toFixed(1)}" y1="${(c.y - ny * half).toFixed(1)}" x2="${(c.x + nx * half).toFixed(1)}" y2="${(c.y + ny * half).toFixed(1)}"></line>`);
      }
    });
    return bars.join('');
  },

  wallSegmentObjectSvg(o, selected, primary, highlighted){
    const pts = this.wallPoints(o);
    const p1 = pts[0], p2 = pts[1];
    const storedThickness = Number(o.metadata?.thickness || this.wallThickness(o.type));
    const thickness = this.wallDisplayThickness(o.type, storedThickness);
    const bounds = this.wallBoundsFromPoints(p1, p2, thickness);
    const color = o.color || this.typeColor(o.type);
    const length = Math.round(this.distance(p1, p2));
    const mid = { x:(p1.x + p2.x) / 2, y:(p1.y + p2.y) / 2 };
    const lineClass = o.type === 'cloison_grille' ? 'plan-wall-segment-line plan-wall-segment-line--grid' : (o.type === 'cloison' ? 'plan-wall-segment-line plan-wall-segment-line--partition' : 'plan-wall-segment-line plan-wall-segment-line--mur');
    const openings = this.wallOpeningsForSegment(o);
    const pieces = this.wallSegmentPieces(openings);
    const wallLines = pieces.map(piece => { const a = this.segmentPoint(p1, p2, piece.from); const b = this.segmentPoint(p1, p2, piece.to); return `<line class="${lineClass}" data-id="${_planAttr(o.id)}" data-action="move" x1="${a.x}" y1="${a.y}" x2="${b.x}" y2="${b.y}" stroke="${_planAttr(color)}" stroke-width="${thickness}"></line>`; }).join('');
    const gridBars = o.type === 'cloison_grille' ? this.gridPartitionBarsSvg(p1, p2, pieces, thickness) : '';
    const axisClass = o.type === 'cloison_grille' ? 'plan-wall-segment-axis plan-wall-segment-axis--grid' : (o.type === 'cloison' ? 'plan-wall-segment-axis plan-wall-segment-axis--partition' : 'plan-wall-segment-axis plan-wall-segment-axis--mur');
    const axisLines = pieces.map(piece => { const a = this.segmentPoint(p1, p2, piece.from); const b = this.segmentPoint(p1, p2, piece.to); return `<line class="${axisClass}" data-id="${_planAttr(o.id)}" data-action="move" x1="${a.x}" y1="${a.y}" x2="${b.x}" y2="${b.y}"></line>`; }).join('');
    const cutMarks = openings.map(opening => this.wallOpeningCutSvg(opening, p1, p2, thickness)).join('');
    const selectFrame = selected ? `<rect class="plan-object-select-frame${primary ? ' is-primary' : ''}" data-id="${_planAttr(o.id)}" data-action="move" x="${bounds.x - 5}" y="${bounds.y - 5}" width="${bounds.width + 10}" height="${bounds.height + 10}" rx="2"></rect>` : '';
    const highlightFrame = highlighted ? `<rect class="plan-object-highlight-frame" x="${bounds.x - 10}" y="${bounds.y - 10}" width="${bounds.width + 20}" height="${bounds.height + 20}" rx="4"></rect>` : '';
    const endpoints = selected && primary && this.canEdit() ? `<circle class="plan-wall-endpoint" data-id="${_planAttr(o.id)}" data-action="wall-endpoint" data-endpoint="0" cx="${p1.x}" cy="${p1.y}" r="${this.screenR(7.5)}"></circle><circle class="plan-wall-endpoint" data-id="${_planAttr(o.id)}" data-action="wall-endpoint" data-endpoint="1" cx="${p2.x}" cy="${p2.y}" r="${this.screenR(7.5)}"></circle>` : '';
    const hitLine = `<line class="plan-wall-hit" data-id="${_planAttr(o.id)}" data-action="move" x1="${p1.x}" y1="${p1.y}" x2="${p2.x}" y2="${p2.y}" stroke="rgba(0,0,0,0.001)" stroke-width="${Math.max(thickness, this.screenR(18))}" stroke-linecap="round"></line>`;
    const lengthLabel = selected ? `<text class="plan-wall-length" data-id="${_planAttr(o.id)}" data-action="move" x="${mid.x}" y="${mid.y - Math.max(12, thickness)}" text-anchor="middle">${_planEsc(this.formatLengthUnits(length))}</text>` : '';
    return `<g class="plan-object plan-object--${_planAttr(o.type || 'mur')} is-architectural is-wall is-wall-segment${selected ? ' is-selected' : ''}${primary ? ' is-primary' : ''}${highlighted ? ' is-highlighted' : ''}" data-id="${_planAttr(o.id)}" data-action="move">
      ${highlightFrame}${selectFrame}
      ${hitLine}
      ${wallLines}
      ${gridBars}
      ${cutMarks}
      ${axisLines}
      ${lengthLabel}${endpoints}
    </g>`;
  },

  objectBounds(o){
    if(this.isWallSegmentObject(o)){
      const pts = this.wallPoints(o);
      return this.wallBoundsFromPoints(pts[0], pts[1], Number(o.metadata?.thickness || this.wallThickness(o.type)));
    }
    return { x:Number(o.x || 0), y:Number(o.y || 0), width:Number(o.width || 0), height:Number(o.height || 0), right:Number(o.x || 0) + Number(o.width || 0), bottom:Number(o.y || 0) + Number(o.height || 0) };
  },

  sortedObjectsForRender(){
    const selected = new Set(this.selectedObjectIds.length ? this.selectedObjectIds.map(String) : (this.selectedObjectId ? [String(this.selectedObjectId)] : []));
    const rank = o => {
      if(selected.has(String(o.id))) return 90;
      if(['depot','local','piece','atelier','zone'].includes(o.type)) return 5;
      if(this.isWallType(o.type)) return 12;
      if(this.isOpeningType(o.type)) return 18;
      if(['allee','circulation','escalier','repere'].includes(o.type)) return 25;
      if(['armoire','armoire_double','rack','rayonnage','etagere','palette','emplacement'].includes(o.type)) return 35;
      return 30;
    };
    return this.objects.slice().sort((a,b) => (rank(a) - rank(b)) || (Number(a.y || 0) - Number(b.y || 0)) || (Number(a.x || 0) - Number(b.x || 0)));
  },

  planScale(){
    const scale = Number(this.selectedPlan?.scale || 1);
    return Number.isFinite(scale) && scale > 0 ? scale : 1;
  },

  formatRoomArea(area){
    if(!Number.isFinite(area) || area <= 0) return '—';
    return area < 10 ? area.toFixed(1).replace('.', ',') + ' m²' : Math.round(area).toString() + ' m²';
  },

  roomAreaFromBounds(width, height){
    const scale = this.planScale();
    return Math.max(0, (Number(width || 0) * scale) * (Number(height || 0) * scale) / 10000);
  },

  closedRooms(){
    const tol = 18;
    const walls = this.getWallSegments().filter(seg => seg.type === 'mur');
    const horizontals = [];
    const verticals = [];
    walls.forEach(seg => {
      const dx = seg.p2.x - seg.p1.x;
      const dy = seg.p2.y - seg.p1.y;
      if(Math.abs(dy) <= tol && Math.abs(dx) > 80){
        horizontals.push({ y:(seg.p1.y + seg.p2.y) / 2, x1:Math.min(seg.p1.x, seg.p2.x), x2:Math.max(seg.p1.x, seg.p2.x), id:seg.id });
      }
      if(Math.abs(dx) <= tol && Math.abs(dy) > 80){
        verticals.push({ x:(seg.p1.x + seg.p2.x) / 2, y1:Math.min(seg.p1.y, seg.p2.y), y2:Math.max(seg.p1.y, seg.p2.y), id:seg.id });
      }
    });
    const rooms = [];
    const seen = new Set();
    for(let i=0;i<horizontals.length;i++) for(let j=i+1;j<horizontals.length;j++){
      const top = horizontals[i].y < horizontals[j].y ? horizontals[i] : horizontals[j];
      const bottom = top === horizontals[i] ? horizontals[j] : horizontals[i];
      const height = bottom.y - top.y;
      if(height < 90) continue;
      for(let a=0;a<verticals.length;a++) for(let b=a+1;b<verticals.length;b++){
        const left = verticals[a].x < verticals[b].x ? verticals[a] : verticals[b];
        const right = left === verticals[a] ? verticals[b] : verticals[a];
        const width = right.x - left.x;
        if(width < 90) continue;
        const topOk = top.x1 <= left.x + tol && top.x2 >= right.x - tol;
        const bottomOk = bottom.x1 <= left.x + tol && bottom.x2 >= right.x - tol;
        const leftOk = left.y1 <= top.y + tol && left.y2 >= bottom.y - tol;
        const rightOk = right.y1 <= top.y + tol && right.y2 >= bottom.y - tol;
        if(!topOk || !bottomOk || !leftOk || !rightOk) continue;
        const key = [Math.round(left.x/tol), Math.round(top.y/tol), Math.round(width/tol), Math.round(height/tol)].join(':');
        if(seen.has(key)) continue;
        seen.add(key);
        const wallIds = [top.id, right.id, bottom.id, left.id].map(String);
        rooms.push({
          index:rooms.length,
          name:'Pièce ' + (rooms.length + 1),
          x:left.x,
          y:top.y,
          width,
          height,
          right:right.x,
          bottom:bottom.y,
          center:{ x:left.x + width / 2, y:top.y + height / 2 },
          wallIds,
          area:this.roomAreaFromBounds(width, height),
        });
        if(rooms.length >= 18) return rooms;
      }
    }
    return rooms;
  },

  roomCotesSvg(r){
    const labelW = this.formatLengthUnits(r.width);
    const labelH = this.formatLengthUnits(r.height);
    const yTop = r.y - 18;
    const xLeft = r.x - 18;
    return `<g class="plan-room-cotes" aria-hidden="true"><line x1="${r.x}" y1="${yTop}" x2="${r.right}" y2="${yTop}"></line><line x1="${r.x}" y1="${yTop - 5}" x2="${r.x}" y2="${yTop + 5}"></line><line x1="${r.right}" y1="${yTop - 5}" x2="${r.right}" y2="${yTop + 5}"></line><text x="${r.center.x}" y="${yTop - 4}" text-anchor="middle">${_planEsc(labelW)}</text><line x1="${xLeft}" y1="${r.y}" x2="${xLeft}" y2="${r.bottom}"></line><line x1="${xLeft - 5}" y1="${r.y}" x2="${xLeft + 5}" y2="${r.y}"></line><line x1="${xLeft - 5}" y1="${r.bottom}" x2="${xLeft + 5}" y2="${r.bottom}"></line><text transform="translate(${xLeft - 5} ${r.center.y}) rotate(-90)" text-anchor="middle">${_planEsc(labelH)}</text></g>`;
  },

  closedRoomsSvg(){
    const rooms = this.closedRooms();
    if(!rooms.length) return '';
    const selected = new Set((this.selectedObjectIds || []).map(String));
    return `<g class="plan-closed-rooms">${rooms.map((r,idx) => {
      const showLabel = r.width > 150 && r.height > 85;
      const selectedRoom = r.wallIds.every(id => selected.has(String(id)));
      const label = `${r.name} · ${this.formatRoomArea(r.area)}`;
      const cotes = (selectedRoom && r.width > 180 && r.height > 120) ? this.roomCotesSvg(r) : '';
      return `<g class="plan-room${selectedRoom ? ' is-selected' : ''}" data-room-index="${idx}" data-room-walls="${_planAttr(r.wallIds.join(','))}"><rect class="plan-room-fill" data-room-index="${idx}" x="${r.x}" y="${r.y}" width="${r.width}" height="${r.height}"></rect>${cotes}${showLabel ? `<text class="plan-room-label" data-room-index="${idx}" x="${r.center.x}" y="${r.center.y - 4}" text-anchor="middle">${_planEsc(label)}</text><text class="plan-room-sub" data-room-index="${idx}" x="${r.center.x}" y="${r.center.y + 15}" text-anchor="middle">${_planEsc(this.formatLengthUnits(r.width))} × ${_planEsc(this.formatLengthUnits(r.height))}</text>` : ''}</g>`;
    }).join('')}</g>`;
  },

  selectRoomByIndex(index){
    const rooms = this.closedRooms();
    const room = rooms[index] || null;
    if(!room || !Array.isArray(room.wallIds) || !room.wallIds.length) return;
    this.setSelection(room.wallIds, room.wallIds[0]);
    this.renderEditor();
    toast(`${room.name} sélectionnée — ${this.formatRoomArea(room.area)}. Déplacez les murs ou les extrémités pour modifier la structure.`, 'info');
  },

  selectedRoomForWalls(selected){
    const ids = new Set((selected || []).map(o => String(o.id || '')));
    if(ids.size < 4) return null;
    return this.closedRooms().find(room => room.wallIds.every(id => ids.has(String(id)))) || null;
  },

  selectedObjects(){
    const ids = this.selectedObjectIds.length ? this.selectedObjectIds.map(String) : (this.selectedObjectId ? [String(this.selectedObjectId)] : []);
    return this.objects.filter(o => ids.includes(String(o.id)));
  },

  selectedObject(){
    const selected = this.selectedObjects();
    return selected[0] || null;
  },

  setSelection(ids, primary=null){
    const uniq = [];
    (Array.isArray(ids) ? ids : [ids]).forEach(id => {
      if(id !== null && id !== undefined && !uniq.includes(String(id))) uniq.push(String(id));
    });
    this.selectedObjectIds = uniq;
    this.updateToolbarState();
    this.selectedObjectId = primary !== null && primary !== undefined ? String(primary) : (uniq[0] || null);
  },

  clearSelection(){
    this.selectedObjectId = null;
    this.selectedObjectIds = [];
    this.updateToolbarState();
  },

  toggleLibraryFilters(){
    if(!this.el.libraryFilters || !this.el.libraryFilter) return;
    this.libraryFiltersOpen = !this.libraryFiltersOpen;
    this.renderLibraryFilterPanel();
  },

  libraryCategoryLabel(category){
    if(category === 'all') return 'Tous';
    return this.categoryLabel(category);
  },

  libraryGroupsForSearch(){
    const q = String(this.componentQ || '').toLowerCase();
    const groups = {};
    this.componentEntries().forEach(([key,item]) => {
      const haystack = `${key} ${item.label || ''} ${item.description || ''} ${item.category || ''}`.toLowerCase();
      if(q && !haystack.includes(q)) return;
      const cat = item.category || 'autres';
      if(!groups[cat]) groups[cat] = [];
      groups[cat].push([key,item]);
    });
    return groups;
  },

  renderLibraryFilterPanel(groups=null){
    if(!this.el.libraryFilters) return;
    const order = ['construire','ouvertures','structure','stockage','circulation','organisation','repere','autres'];
    const source = groups || this.libraryGroupsForSearch();
    const total = Object.values(source).reduce((sum, rows) => sum + (Array.isArray(rows) ? rows.length : 0), 0);
    const cats = order.filter(cat => source[cat]?.length);
    const current = this.componentCategoryFilter || 'all';
    const buttons = [['all', total]].concat(cats.map(cat => [cat, source[cat].length]));
    this.el.libraryFilters.innerHTML = buttons.map(([cat,count]) => `<button type="button" class="plans-library-filter-chip${current === cat ? ' active' : ''}" data-library-filter="${_planAttr(cat)}" aria-pressed="${current === cat ? 'true' : 'false'}"><span>${_planEsc(this.libraryCategoryLabel(cat))}</span><em>${Number(count || 0)}</em></button>`).join('');
    this.el.libraryFilters.hidden = !this.libraryFiltersOpen;
    if(this.el.libraryFilter){
      this.el.libraryFilter.classList.toggle('active', this.libraryFiltersOpen || current !== 'all');
      this.el.libraryFilter.setAttribute('aria-expanded', this.libraryFiltersOpen ? 'true' : 'false');
      this.el.libraryFilter.title = current === 'all' ? 'Filtrer la bibliothèque d’objets' : 'Filtre actif : ' + this.libraryCategoryLabel(current);
    }
  },

  componentEntries(){
    const preferred = ['mur','cloison','cloison_grille','ouverture','porte','porte_double','fenetre','depot','local','piece','atelier','zone','allee','circulation','escalier','armoire','armoire_double','rack','rayonnage','etagere','palette','emplacement','repere'];
    const types = this.schema?.objectTypes || {};
    const keys = preferred.filter(k => types[k]).concat(Object.keys(types).filter(k => !preferred.includes(k)));
    return keys.map(key => [key, types[key] || {}]);
  },

  categoryLabel(category){
    return ({
      construire:'Construction',
      structure:'Pièces & zones',
      ouvertures:'Portes & ouvertures',
      organisation:'Équipements',
      circulation:'Équipements',
      stockage:'Stockage',
      repere:'Marquages & sécurité',
      autres:'Autres'
    })[category || ''] || 'Autres';
  },

  componentCategoryHint(category){
    return ({
      construire:'Murs',
      ouvertures:'Portes, fenêtres et passages',
      structure:'Locaux, pièces et zones',
      circulation:'Circulation et parcours',
      stockage:'Armoires, racks, rayonnages',
      organisation:'Repères d’aménagement',
      repere:'Signalétique et notes'
    })[category || ''] || 'Objets disponibles';
  },

  visibleComponentCategories(order){
    return order;
  },

  renderComponentLibrary(){
    if(!this.el.componentLibrary) return;
    if(!this.selectedPlan || !this.canEdit()){
      this.el.componentLibrary.innerHTML = `<div class="plan-component-empty">Passez le plan en mode édition pour ajouter des éléments.</div>`;
      this.renderLibraryFilterPanel({});
      return;
    }
    const groups = this.libraryGroupsForSearch();
    const order = ['construire','ouvertures','structure','stockage','circulation','organisation','repere','autres'];
    this.renderLibraryFilterPanel(groups);
    const activeFilter = this.componentCategoryFilter || 'all';
    let visible = order.filter(cat => groups[cat]?.length);
    if(activeFilter !== 'all') visible = visible.filter(cat => cat === activeFilter);
    if(activeFilter !== 'all' && !order.includes(activeFilter)) this.componentCategoryFilter = 'all';
    if(!visible.length){
      const hasSearch = !!String(this.componentQ || '').trim();
      const msg = hasSearch ? 'Aucun objet ne correspond à cette recherche.' : 'Aucun objet disponible dans ce filtre.';
      this.el.componentLibrary.innerHTML = `<div class="plan-component-empty">${_planEsc(msg)}</div>`;
      return;
    }
    this.el.componentLibrary.innerHTML = visible.map((cat) => {
      const rows = groups[cat] || [];
      const collapsed = !!this.collapsedComponentCategories[cat];
      return `<section class="plan-component-group${collapsed ? ' is-collapsed' : ''}" data-component-category="${_planAttr(cat)}">
        <button type="button" class="plan-component-group__title" data-library-toggle="${_planAttr(cat)}" aria-expanded="${collapsed ? 'false' : 'true'}"><span>${_planEsc(this.categoryLabel(cat))}</span><em>${rows.length}</em></button>
        <div class="plan-component-group__body">
          <div class="plan-component-group__hint">${_planEsc(this.componentCategoryHint(cat))}</div>
          <div class="plan-component-grid">
            ${rows.map(([key,item]) => this.componentCardHtml(key, item)).join('')}
          </div>
        </div>
      </section>`;
    }).join('') + `<button type="button" class="plans-library-dropzone" data-library-help="1"><strong>Glisser-déposer un objet sur le plan</strong><span>ou cliquer pour le préparer</span></button>`;
    this.markComponentActive(this.tool.startsWith('add:') ? this.tool.split(':')[1] : '');
  },
  componentCardHtml(key, item){
    const label = item.label || key;
    const color = item.color || '#dbeafe';
    const dims = `Long. ${item.defaultWidth || this.defaultSize(key).width} × Larg. ${item.defaultHeight || this.defaultSize(key).height}`;
    const desc = item.description || 'Composant du plan';
    const wallTool = this.isWallType(key);
    const actionHint = wallTool ? 'Cliquer pour tracer par points' : dims;
    return `<button type="button" class="plan-component-card${wallTool ? ' is-wall-tool' : ''}" draggable="${wallTool ? 'false' : 'true'}" aria-pressed="false" data-component-add="${_planAttr(key)}" data-component-label="${_planAttr(label)}">
      <span class="plan-component-preview" style="--pc:${_planAttr(color)}">${this.componentPreviewSvg(key, color)}</span>
      <span class="plan-component-copy"><strong>${_planEsc(label)}</strong><small>${_planEsc(desc)}</small><em>${_planEsc(actionHint)}</em></span>
    </button>`;
  },

  componentPreviewSvg(type, color){
    const safe = _planAttr(color || '#dbeafe');
    const line = 'rgba(15,23,42,.42)';
    if(type === 'mur') return `<svg viewBox="0 0 56 36" aria-hidden="true"><rect x="7" y="15" width="42" height="7" rx="1" fill="#334155"/><line x1="10" y1="18.5" x2="46" y2="18.5" stroke="rgba(255,255,255,.45)" stroke-width="1"/></svg>`;
    if(type === 'cloison') return `<svg viewBox="0 0 56 36" aria-hidden="true"><rect x="8" y="16" width="40" height="3" rx="0" fill="#7b8798"/><line x1="10" y1="17.5" x2="46" y2="17.5" stroke="rgba(255,255,255,.55)" stroke-width="1" stroke-dasharray="7 5"/></svg>`;
    if(type === 'cloison_grille') return `<svg viewBox="0 0 56 36" aria-hidden="true"><line x1="8" y1="18" x2="48" y2="18" stroke="#8a96a8" stroke-width="2.2" stroke-linecap="square"/><path d="M12 11V25M20 11V25M28 11V25M36 11V25M44 11V25" stroke="#475569" stroke-width="1.35"/><line x1="8" y1="13" x2="48" y2="13" stroke="#cbd5e1" stroke-width="1"/><line x1="8" y1="23" x2="48" y2="23" stroke="#cbd5e1" stroke-width="1"/></svg>`;
    if(type === 'ouverture') return `<svg viewBox="0 0 64 48" aria-hidden="true"><path d="M6 24H19M45 24H58" stroke="#111827" stroke-width="3" stroke-linecap="square"/><path d="M19 12V36M45 12V36" stroke="#111827" stroke-width="2.2"/><path d="M19 12H28M19 36H28M36 12H45M36 36H45" stroke="#334155" stroke-width="1.35" stroke-linecap="square"/></svg>`;
    if(type === 'fenetre') return `<svg viewBox="0 0 64 48" aria-hidden="true"><path d="M6 24H15M49 24H58" stroke="#111827" stroke-width="3" stroke-linecap="square"/><path d="M15 16V32M49 16V32" stroke="#111827" stroke-width="2"/><path d="M19 20H45M19 28H45M21 24H43" stroke="#334155" stroke-width="1.55" stroke-linecap="square"/><path d="M28 20V28M36 20V28" stroke="#64748b" stroke-width="1"/></svg>`;
    if(type === 'escalier') return `<svg viewBox="0 0 56 36" aria-hidden="true"><rect x="10" y="8" width="36" height="20" fill="${safe}" stroke="${line}"/><path d="M16 9V27M22 9V27M28 9V27M34 9V27M40 9V27" stroke="${line}"/><path d="M15 25H41M36 21L41 25L36 29" fill="none" stroke="${line}" stroke-width="1.6"/></svg>`;
    if(type === 'repere') return `<svg viewBox="0 0 56 36" aria-hidden="true"><rect x="10" y="10" width="36" height="16" rx="3" fill="${safe}" stroke="${line}"/><path d="M16 16H40M16 21H32" stroke="${line}" stroke-width="1.2"/></svg>`;
    if(type === 'porte') return `<svg viewBox="0 0 64 56" aria-hidden="true"><path d="M6 45H17M51 45H58" stroke="#111827" stroke-width="3" stroke-linecap="square"/><path d="M17 27V49M51 27V49" stroke="#111827" stroke-width="2"/><path d="M17 45L26 12" stroke="#111827" stroke-width="2.2" stroke-linecap="square"/><path d="M51 45Q42 18 26 12" fill="none" stroke="#64748b" stroke-width="1.3" stroke-linecap="round"/><circle cx="17" cy="45" r="1.6" fill="#fff" stroke="#111827" stroke-width="1"/></svg>`;
    if(type === 'porte_double') return `<svg viewBox="0 0 64 56" aria-hidden="true"><path d="M6 45H17M47 45H58" stroke="#111827" stroke-width="3" stroke-linecap="square"/><path d="M17 27V49M47 27V49" stroke="#111827" stroke-width="2"/><path d="M17 45L32 12M47 45L32 12" stroke="#111827" stroke-width="2.1" stroke-linecap="square"/><path d="M32 45Q23 20 32 12M32 45Q41 20 32 12" fill="none" stroke="#64748b" stroke-width="1.25" stroke-linecap="round"/><circle cx="17" cy="45" r="1.45" fill="#fff" stroke="#111827" stroke-width="1"/><circle cx="47" cy="45" r="1.45" fill="#fff" stroke="#111827" stroke-width="1"/></svg>`;
    if(type === 'rack') return `<svg viewBox="0 0 72 42" aria-hidden="true"><rect x="7" y="9" width="58" height="24" rx="1" fill="#f8fafc" stroke="#cbd5e1"/><rect x="8" y="12" width="56" height="5" fill="#1f2937"/><rect x="8" y="25" width="56" height="5" fill="#1f2937"/><path d="M8 9V33M64 9V33M22 9V33M36 9V33M50 9V33" stroke="#64748b" stroke-width="1.4"/><path d="M12 30L22 12M26 30L36 12M40 30L50 12" stroke="rgba(71,85,105,.28)" stroke-width="1"/></svg>`;
    if(type === 'rayonnage') return `<svg viewBox="0 0 72 42" aria-hidden="true"><rect x="8" y="11" width="56" height="20" rx="1" fill="#f0f9ff" stroke="#64748b"/><path d="M10 15H62M10 20H62M10 25H62M10 30H62M22 11V31M36 11V31M50 11V31" stroke="rgba(51,65,85,.48)" stroke-width="1"/></svg>`;
    if(type === 'etagere') return `<svg viewBox="0 0 72 42" aria-hidden="true"><rect x="12" y="14" width="48" height="14" rx="1" fill="#f8fafc" stroke="#64748b"/><path d="M14 18H58M14 24H58M28 14V28M44 14V28" stroke="rgba(51,65,85,.42)" stroke-width="1"/><path d="M25 31H47" stroke="rgba(100,116,139,.45)"/></svg>`;
    if(type === 'armoire') return `<svg viewBox="0 0 72 42" aria-hidden="true"><rect x="21" y="9" width="30" height="24" rx="1" fill="#e8edf4" stroke="#334155" stroke-width="1.5"/><rect x="25" y="13" width="22" height="16" fill="rgba(255,255,255,.24)" stroke="rgba(51,65,85,.36)"/><path d="M25 29H47M27 13V29" stroke="rgba(51,65,85,.42)"/><circle cx="43" cy="21" r="1.8" fill="#fff" stroke="#334155"/></svg>`;
    if(type === 'armoire_double') return `<svg viewBox="0 0 72 42" aria-hidden="true"><rect x="12" y="9" width="48" height="24" rx="1" fill="#e2e8f0" stroke="#334155" stroke-width="1.5"/><rect x="16" y="13" width="40" height="16" fill="rgba(255,255,255,.22)" stroke="rgba(51,65,85,.36)"/><path d="M36 13V29M16 29H56" stroke="#334155" stroke-width="1.1"/><circle cx="32" cy="21" r="1.5" fill="#fff" stroke="#334155"/><circle cx="40" cy="21" r="1.5" fill="#fff" stroke="#334155"/></svg>`;
    if(type === 'palette') return `<svg viewBox="0 0 72 42" aria-hidden="true"><rect x="14" y="8" width="44" height="28" rx="1" fill="#f3d7ad" stroke="rgba(124,95,59,.55)"/><path d="M18 15H54M18 22H54M18 29H54" stroke="#7c5f3b" stroke-width="1.2"/><path d="M22 10V34M30 10V34M38 10V34M46 10V34M54 10V34" stroke="rgba(124,95,59,.45)" stroke-width="1"/><rect x="22" y="14" width="4" height="4" fill="rgba(124,95,59,.35)"/><rect x="34" y="14" width="4" height="4" fill="rgba(124,95,59,.35)"/><rect x="46" y="14" width="4" height="4" fill="rgba(124,95,59,.35)"/></svg>`;
    if(type === 'emplacement') return `<svg viewBox="0 0 72 42" aria-hidden="true"><rect x="12" y="10" width="48" height="22" rx="1" fill="#dcfce7" stroke="#16a34a" stroke-dasharray="6 4"/><path d="M12 17V10H20M52 10H60V17M12 25V32H20M52 32H60V25" fill="none" stroke="#16a34a" stroke-width="1.8"/><path d="M24 21H48" stroke="rgba(22,101,52,.45)" stroke-dasharray="4 4"/></svg>`;
    if(['allee','circulation'].includes(type)) return `<svg viewBox="0 0 56 36" aria-hidden="true"><rect x="6" y="12" width="44" height="12" rx="2" fill="${safe}" stroke="rgba(15,23,42,.22)" stroke-dasharray="5 4"/><path d="M16 18H40" stroke="${line}" stroke-width="1.8" stroke-linecap="round"/><path d="M34 14L40 18L34 22" fill="none" stroke="${line}" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/></svg>`;
    if(['depot','local','piece','atelier','zone'].includes(type)) return `<svg viewBox="0 0 56 36" aria-hidden="true"><rect x="7" y="6" width="42" height="24" rx="2" fill="${safe}" stroke="${line}" stroke-width="1.3" stroke-dasharray="6 4"/><rect x="12" y="11" width="32" height="14" rx="1" fill="none" stroke="rgba(255,255,255,.50)"/></svg>`;
    return `<svg viewBox="0 0 56 36" aria-hidden="true"><rect x="8" y="7" width="40" height="22" rx="3" fill="${safe}"/><rect x="12" y="11" width="32" height="14" rx="2" fill="none" stroke="rgba(255,255,255,.58)"/><circle cx="39" cy="16" r="3" fill="rgba(15,23,42,.14)"/></svg>`;
  },
  setLocations(rows){
    this.locations = Array.isArray(rows) ? rows : [];
    this.locationById = {};
    this.locationStockById = {};
    this.locations.forEach(loc => {
      const id = String(loc.id || '');
      if(!id) return;
      this.locationById[id] = loc;
      if(loc.stock) this.locationStockById[id] = loc.stock;
    });
  },

  locationTitle(loc){
    if(!loc) return 'Emplacement';
    const code = loc.code || ('#' + loc.id);
    const label = loc.label ? ' — ' + loc.label : '';
    return code + label;
  },

  locationOptionsHtml(selectedId){
    const selected = String(selectedId || '');
    const rows = this.locations.slice().sort((a,b) => String(a.zone || '').localeCompare(String(b.zone || ''), 'fr') || String(a.code || '').localeCompare(String(b.code || ''), 'fr'));
    return '<option value="">Aucun rattachement</option>' + rows.map(loc => {
      const title = `${loc.code || ('#' + loc.id)}${loc.label ? ' — ' + loc.label : ''}${loc.zone ? ' · ' + loc.zone : ''}`;
      return `<option value="${_planAttr(loc.id)}" ${String(loc.id) === selected ? 'selected' : ''}>${_planEsc(title)}</option>`;
    }).join('');
  },

  linkedObjectForLocation(locationId){
    return this.objects.find(o => String(o.linkedLocationId || '') === String(locationId || '')) || null;
  },

  locationBusinessHtml(loc, stock=null){
    if(!loc) return '<div class="plan-link-empty">Aucun emplacement métier sélectionné.</div>';
    const st = stock || loc.stock || {};
    const products = Array.isArray(st.products) ? st.products : [];
    return `<div class="plan-location-card">
      <div><strong>${_planEsc(this.locationTitle(loc))}</strong><span>${_planEsc(loc.zone || 'Zone non renseignée')}</span></div>
      <dl>
        <dt>Code</dt><dd>${_planEsc(loc.code || '—')}</dd>
        <dt>Type</dt><dd>${_planEsc(loc.type || '—')}</dd>
        <dt>Statut</dt><dd>${loc.active === false ? 'Inactif' : 'Actif'}</dd>
        <dt>Produits</dt><dd>${Number(st.productCount || 0)}</dd>
        <dt>Lignes stock</dt><dd>${Number(st.stockEntries || 0)}</dd>
      </dl>
      ${products.length ? `<div class="plan-location-products">${products.slice(0,5).map(p => `<span>${_planEsc(p.sku || p.barcode || '')} ${_planEsc(p.name || 'Produit')}</span>`).join('')}</div>` : '<div class="plan-link-empty">Aucun produit stocké directement sur cet emplacement.</div>'}
    </div>`;
  },

  productLocalizationHtml(obj){
    const loc = this.localization;
    if(!loc || !obj || String(loc.objectId || '') !== String(obj.id || '')) return '';
    const product = loc.product || {};
    const stock = loc.stock || {};
    const plan = loc.plan || this.selectedPlan || {};
    const location = loc.location || null;
    const matchLabel = loc.matchType === 'product' ? 'Lien direct produit' : 'Emplacement du stock';
    return `<div class="plan-product-location-card" role="note">
      <strong>Produit localisé</strong>
      <span>${_planEsc(product.name || 'Produit')} ${product.sku ? '· ' + _planEsc(product.sku) : ''}</span>
      <dl>
        <dt>Magasin</dt><dd>${_planEsc(plan.warehouseName || this.whName(plan.warehouseId || this.selectedPlan?.warehouseId) || '—')}</dd>
        <dt>Plan</dt><dd>${_planEsc(plan.name || this.selectedPlan?.name || '—')}</dd>
        <dt>Emplacement</dt><dd>${_planEsc(location ? this.locationTitle(location) : '—')}</dd>
        <dt>Zone</dt><dd>${_planEsc(location?.zone || '—')}</dd>
        <dt>Correspondance</dt><dd>${_planEsc(matchLabel)}</dd>
        <dt>Quantité ici</dt><dd>${Number(stock.quantity || 0)}</dd>
      </dl>
    </div>`;
  },

  async handleInitialLocationRequest(){
    const params = new URLSearchParams(window.location.search || '');
    const productId = params.get('productId') || '';
    const planId = params.get('planId') || '';
    const objectId = params.get('objectId') || '';
    const locationId = params.get('locationId') || '';
    if(productId){
      this.localizationReadOnly = true;
      this.editMode = false;
      await this.locateProductFromQuery(productId, { planId, objectId, locationId });
      return;
    }
    if(planId){
      const row = this.rows.find(r => String(r.id) === String(planId)) || { id:planId };
      await this.openPlan(row, { focusObjectId:objectId || null, locationId:locationId || null, highlight:!!objectId, toast:false });
    }
  },

  async locateProductFromQuery(productId, preferred={}){
    try{
      const payload = await Api.planProductLocation(productId);
      const matches = Array.isArray(payload?.matches) ? payload.matches : [];
      let match = null;
      if(preferred.objectId) match = matches.find(m => String(m.object?.id || '') === String(preferred.objectId));
      if(!match && preferred.planId) match = matches.find(m => String(m.plan?.id || '') === String(preferred.planId));
      if(!match && preferred.locationId) match = matches.find(m => String(m.location?.id || '') === String(preferred.locationId));
      if(!match) match = payload?.best || matches[0] || null;
      if(!match || !match.plan || !match.object){
        const unplaced = Array.isArray(payload?.unplacedLocations) ? payload.unplacedLocations : [];
        const msg = unplaced.length
          ? 'Produit trouvé dans un emplacement MageStock, mais cet emplacement n’est pas encore placé sur un plan.'
          : 'Aucun plan ne permet encore de localiser ce produit.';
        toast(msg, 'warning');
        return;
      }
      const row = this.rows.find(r => String(r.id) === String(match.plan.id)) || match.plan;
      await this.openPlan(row, {
        focusObjectId: match.object.id,
        highlightObjectId: match.object.id,
        highlight: true,
        toast: false,
        localizationReadOnly: true,
        localization: {
          product: payload.product || {},
          plan: match.plan || {},
          objectId: match.object.id,
          location: match.location || null,
          stock: match.stock || {},
          matchType: match.matchType || 'location',
        }
      });
      toast('Produit localisé sur le plan.', 'success');
    }catch(e){
      console.error(e);
      toast(e.message || 'Impossible de localiser ce produit sur un plan.', 'error');
    }
  },

  renderLocationSearchResults(){
    if(!this.el.locationResults) return;
    if(!this.selectedPlan){
      this.el.locationResults.innerHTML = '';
      return;
    }
    const q = (this.filter.locationQ || '').trim().toLowerCase();
    const linked = this.locations.map(loc => ({ loc, obj:this.linkedObjectForLocation(loc.id) })).filter(x => x.obj);
    let candidates = this.locations.map(loc => ({ loc, obj:this.linkedObjectForLocation(loc.id) }));
    if(q){
      candidates = candidates.filter(({loc}) => `${loc.code || ''} ${loc.label || ''} ${loc.zone || ''} ${loc.type || ''}`.toLowerCase().includes(q));
    }else{
      candidates = linked.slice(0, 8);
    }
    candidates = candidates.slice(0, 12);
    if(!candidates.length){
      this.el.locationResults.innerHTML = `<div class="plan-location-result-empty">${q ? 'Aucun emplacement correspondant dans ce magasin.' : 'Aucun emplacement rattaché sur ce plan.'}</div>`;
      return;
    }
    this.el.locationResults.innerHTML = candidates.map(({loc,obj}) => {
      const linkedLabel = obj ? `<span class="plan-location-result__status linked">Placée</span>` : `<span class="plan-location-result__status">Non placée</span>`;
      const action = obj ? 'focus' : 'select';
      const btnLabel = obj ? 'Voir sur le plan' : (this.canEdit() && this.selectedObject() ? 'Lier à la sélection' : 'Non placée');
      const disabled = (!obj && (!this.canEdit() || !this.selectedObject())) ? 'disabled' : ''; 
      return `<button type="button" class="plan-location-result" data-location-act="${action}" data-location-id="${_planAttr(loc.id)}" ${disabled}>
        <span><strong>${_planEsc(this.locationTitle(loc))}</strong><small>${_planEsc(loc.zone || this.whName(loc.warehouseId))}</small></span>
        ${linkedLabel}<em>${_planEsc(btnLabel)}</em>
      </button>`;
    }).join('');
  },

  focusObject(objectId, options={}){
    const obj = this.objects.find(o => String(o.id) === String(objectId || ''));
    if(!obj){
      toast('Objet introuvable sur le plan ouvert.', 'warning');
      return;
    }
    this.setSelection([obj.id], obj.id);
    if(options.highlight !== false) this.highlightObjectId = String(obj.id);
    if(options.localization !== undefined) this.localization = options.localization;
    const wrap = this.el.canvasWrap?.getBoundingClientRect?.() || { width:900, height:560 };
    const targetZoom = _planClamp(this.localizationReadOnly ? Math.max(this.zoom, 1.45) : Math.max(this.zoom, 1.25), 0.18, 3.5);
    this.zoom = targetZoom;
    this.pan.x = Math.round((wrap.width || 900) / 2 - (Number(obj.x || 0) + Number(obj.width || 0) / 2) * this.zoom);
    this.pan.y = Math.round((wrap.height || 560) / 2 - (Number(obj.y || 0) + Number(obj.height || 0) / 2) * this.zoom);
    this.renderEditor();
    if(options.toast !== false) toast(options.message || 'Emplacement centré sur le plan.', 'success');
  },

  focusLocation(locationId){
    const obj = this.linkedObjectForLocation(locationId);
    if(!obj){
      toast('Cet emplacement existe dans MageStock mais n’est pas encore placé sur ce plan.', 'warning');
      return;
    }
    this.focusObject(obj.id, { highlight:false, localization:null });
  },

  selectLinkedLocationCandidate(locationId){
    const obj = this.selectedObject();
    const loc = this.locationById[String(locationId || '')];
    if(!this.canEdit() || !obj || !loc) return;
    obj.linkedLocationId = loc.id;
    obj.linkedWarehouseId = loc.warehouseId || this.selectedPlan?.warehouseId || null;
    obj.linkedZone = loc.zone || '';
    if(!obj.label) obj.label = loc.code || loc.label || obj.label;
    this.setDirty(true);
    this.renderEditor();
    toast('Emplacement rattaché à l’objet sélectionné.', 'success');
  },


  visibleHighlightObject(){
    return this.objects.find(o => String(o.id) === String(this.highlightObjectId || '')) || this.selectedObject() || null;
  },

  inlineSvgLegend(svgW, svgH){
    if(!this.selectedPlan || !this.objects.length) return '';
    const counts = new Map();
    this.objects.forEach(o => counts.set(o.type || 'objet', (counts.get(o.type || 'objet') || 0) + 1));
    const sorted = Array.from(counts.entries()).sort((a,b) => this.typeLabel(a[0]).localeCompare(this.typeLabel(b[0]), 'fr')).slice(0, 6);
    const rows = sorted.map(([type, count], idx) => {
      const y = 36 + idx * 15;
      return `<g class="plan-legend-row-svg"><rect x="14" y="${y - 7}" width="8" height="8" rx="1.5" fill="${_planAttr(this.typeColor(type))}" stroke="rgba(15,23,42,.16)"></rect><text class="plan-legend-text" x="28" y="${y}">${_planEsc(_planShortLabel(this.typeLabel(type), 15))}</text><text class="plan-legend-count" x="166" y="${y}" text-anchor="end">${count}</text></g>`;
    }).join('');
    const linked = this.objects.filter(o => o.linkedLocationId).length;
    const x = Math.max(12, svgW - 190);
    const h = Math.max(64, 50 + Math.min(6, sorted.length) * 15);
    return `<g class="plan-svg-legend" transform="translate(${x} 14)">
      <rect x="0" y="0" width="176" height="${h}" rx="8"></rect>
      <text class="plan-legend-title" x="14" y="20">Objets du plan</text>
      ${rows}
      <text class="plan-legend-note" x="14" y="${h - 10}">${linked} emplacement${linked > 1 ? 's' : ''} lié${linked > 1 ? 's' : ''}</text>
    </g>`;
  },

  printPlan(opts={}){
    if(!this.selectedPlan){ toast('Ouvrez un plan avant impression.', 'warning'); return; }
    const mode = opts.mode === 'pdf' ? 'pdf' : 'print';
    const highlightObj = this.visibleHighlightObject();
    const orientation = this.planPrintOrientation(highlightObj);
    const bodyHtml = this.planPrintBodyHtml(highlightObj, mode, orientation);
    const planName = this.selectedPlan?.name || 'Plan MageStock';
    const title = highlightObj ? 'Fiche terrain de localisation' : 'Plan terrain imprimable';
    const subtitle = highlightObj
      ? `${planName} · ${highlightObj.label || this.typeLabel(highlightObj.type)}`
      : `${planName} · ${this.whName(this.selectedPlan?.warehouseId)}`;
    const ok = window.MageStockPrint?.open ? window.MageStockPrint.open({
      title,
      subtitle,
      orientation,
      autoPrint: opts.autoPrint === true,
      reference: this.selectedPlan.id ? ('Plan #' + this.selectedPlan.id) : '',
      meta: this.planPrintMeta(highlightObj),
      bodyHtml,
      footer: mode === 'pdf' ? 'Export PDF MageStock · plan terrain en thème clair' : 'Impression terrain MageStock · plan de localisation'
    }) : false;
    if(ok && mode === 'pdf') toast('Fenêtre PDF ouverte. Choisissez “Enregistrer au format PDF”.', 'info');
    if(!ok) toast('Impossible d’ouvrir la fenêtre d’impression. Autorisez les popups puis réessayez.', 'warning');
  },

  planPrintOrientation(highlightObj=null){
    const { width:w, height:h } = this.planSize();
    const ratio = w / Math.max(1, h);
    if(highlightObj) return ratio < 0.72 ? 'portrait' : 'landscape';
    return ratio < 0.84 ? 'portrait' : 'landscape';
  },

  planPrintMeta(highlightObj=null){
    const linked = this.objects.filter(o => o.linkedLocationId).length;
    const planW = Math.round(Number(this.selectedPlan?.width || 0));
    const planH = Math.round(Number(this.selectedPlan?.height || 0));
    return [
      { label:'Magasin', value:this.whName(this.selectedPlan?.warehouseId) },
      { label:'Plan', value:this.selectedPlan?.name || 'Plan' },
      { label:'Dimensions', value:`${this.formatLengthUnits(planW)} × ${this.formatLengthUnits(planH)}` },
      { label:'Échelle', value:'1 unité = 1 cm' },
      { label:'Objets', value:String(this.objects.length) },
      { label:'Sortie', value:highlightObj ? 'Localisation produit' : 'Plan complet' }
    ].concat(linked ? [{ label:'Emplacements liés', value:String(linked) }] : []);
  },

  planPrintBodyHtml(highlightObj=null, mode='print', orientation='landscape'){
    const overviewSvg = this.planPrintSvgHtml(highlightObj, { variant:'overview' });
    const focusSvg = ''; // Impression FIX2 : une seule carte pour tenir sur une page A4.
    const legend = this.planPrintLegendHtml(highlightObj);
    const info = highlightObj ? this.planPrintLocationHtml(highlightObj) : this.planPrintPlanInfoHtml();
    const cartouche = this.planPrintCartoucheHtml(highlightObj);
    const pdfNote = mode === 'pdf'
      ? '<div class="ms-plan-print-export-note"><strong>Export PDF.</strong><span>Choisir “Enregistrer au format PDF” dans la fenêtre d’impression. Mise en page A4 optimisée, sans menus ni outils d’édition.</span></div>'
      : '';
    const focusBlock = ''; // La vue rapprochée est supprimée en impression pour éviter les pages supplémentaires.
    return `<style>${this.planPrintCss(orientation, !!highlightObj)}</style>${pdfNote}<article class="ms-plan-print-document ${highlightObj ? 'is-location' : 'is-plan'}"><header class="ms-plan-print-topline"><div><span>${highlightObj ? 'Fiche terrain' : 'Plan terrain'}</span><strong>${_planEsc(this.selectedPlan?.name || 'Plan')}</strong></div><em>${highlightObj ? 'Localisation avec emplacement surligné' : 'Vue complète imprimable'}</em></header>${cartouche}<div class="ms-plan-print-layout"><main class="ms-plan-print-map-block"><div class="ms-plan-print-section-head"><strong>Plan complet</strong><span>${_planEsc(this.formatLengthUnits(this.selectedPlan?.width || 0))} × ${_planEsc(this.formatLengthUnits(this.selectedPlan?.height || 0))}</span></div>${overviewSvg}${focusBlock}</main><aside class="ms-plan-print-side">${info}${legend}${this.planPrintChecklistHtml(highlightObj)}</aside></div></article>`;
  },

  planPrintCartoucheHtml(highlightObj=null){
    const d = new Date();
    const date = Locale.formatDateTime(d);
    const linked = this.objects.filter(o => o.linkedLocationId).length;
    const target = highlightObj ? (highlightObj.label || this.typeLabel(highlightObj.type)) : 'Plan complet';
    const area = ((Number(this.selectedPlan?.width || 0) / 100) * (Number(this.selectedPlan?.height || 0) / 100));
    return `<section class="ms-plan-print-cartouche" aria-label="Informations d’impression du plan">
      <div><span>Plan</span><strong>${_planEsc(this.selectedPlan?.name || 'Plan')}</strong><em>${_planEsc(this.whName(this.selectedPlan?.warehouseId))}</em></div>
      <div><span>Dimensions réelles</span><strong>${_planEsc(this.formatLengthUnits(this.selectedPlan?.width || 0))} × ${_planEsc(this.formatLengthUnits(this.selectedPlan?.height || 0))}</strong><em>Surface ${Number.isFinite(area) ? Locale.formatNumber(area, {maximumFractionDigits:2}) : '—'} m²</em></div>
      <div><span>Échelle</span><strong>1 unité = 1 cm</strong><em>Repères et cotes en mètres</em></div>
      <div><span>${highlightObj ? 'Cible' : 'Contenu'}</span><strong>${_planEsc(_planShortLabel(target, 34))}</strong><em>${this.objects.length} objet${this.objects.length > 1 ? 's' : ''} · ${linked} lié${linked > 1 ? 's' : ''} · ${date}</em></div>
    </section>`;
  },

  planPrintPlanInfoHtml(){
    const linked = this.objects.filter(o => o.linkedLocationId).length;
    const zones = new Set(this.objects.map(o => String(o.linkedZone || '')).filter(Boolean));
    return `<section class="ms-plan-print-box ms-plan-print-box--info"><h2>Lecture terrain</h2><dl>
      <dt>Magasin</dt><dd>${_planEsc(this.whName(this.selectedPlan?.warehouseId))}</dd>
      <dt>Plan</dt><dd>${_planEsc(this.selectedPlan?.name || '—')}</dd>
      <dt>Dimensions</dt><dd>${_planEsc(this.formatLengthUnits(this.selectedPlan?.width || 0))} × ${_planEsc(this.formatLengthUnits(this.selectedPlan?.height || 0))}</dd>
      <dt>Échelle</dt><dd>1 unité = 1 cm</dd>
      <dt>Objets dessinés</dt><dd>${this.objects.length}</dd>
      <dt>Emplacements liés</dt><dd>${linked}</dd>
      <dt>Zones renseignées</dt><dd>${zones.size || '—'}</dd>
    </dl><p class="ms-plan-print-note">Document de consultation terrain. Les outils d’édition, panneaux web et éléments d’interface ne sont pas imprimés.</p></section>`;
  },

  planPrintRulersSvg(planW, planH, gridSteps){
    const major = Math.max(100, Number(gridSteps?.major || 100));
    const fmt = (u) => _planEsc(this.formatLengthUnits(u));
    let out = '<g class="ms-plan-print-rulers" aria-hidden="true">';
    for(let x = major; x < planW - 1; x += major){
      out += `<line x1="${x}" y1="0" x2="${x}" y2="12"></line><text x="${x}" y="29" text-anchor="middle">${fmt(x)}</text>`;
    }
    for(let y = major; y < planH - 1; y += major){
      out += `<line x1="0" y1="${y}" x2="12" y2="${y}"></line><text x="28" y="${y + 4}" text-anchor="start">${fmt(y)}</text>`;
    }
    out += `<text class="ms-plan-print-dim-x" x="${planW/2}" y="-14" text-anchor="middle">Longueur ${fmt(planW)}</text>`;
    out += `<text class="ms-plan-print-dim-y" x="-14" y="${planH/2}" text-anchor="middle" transform="rotate(-90 -14 ${planH/2})">Largeur ${fmt(planH)}</text>`;
    out += '</g>';
    return out;
  },

  planPrintScaleSvg(vbX, vbY, vbW, vbH, gridSteps){
    const units = Math.max(100, Number(gridSteps?.major || 100));
    const x = vbX + 24;
    const y = vbY + vbH - 28;
    const h = 9;
    return `<g class="ms-plan-print-scale-bar" aria-hidden="true"><text x="${x}" y="${y - 9}">Échelle · ${_planEsc(this.formatLengthUnits(units))}</text><line x1="${x}" y1="${y}" x2="${x + units}" y2="${y}"></line><line x1="${x}" y1="${y - h}" x2="${x}" y2="${y + h}"></line><line x1="${x + units/2}" y1="${y - h/1.6}" x2="${x + units/2}" y2="${y + h/1.6}"></line><line x1="${x + units}" y1="${y - h}" x2="${x + units}" y2="${y + h}"></line><text x="${x}" y="${y + 22}">0</text><text x="${x + units/2}" y="${y + 22}" text-anchor="middle">${_planEsc(this.formatLengthUnits(units/2))}</text><text x="${x + units}" y="${y + 22}" text-anchor="middle">${_planEsc(this.formatLengthUnits(units))}</text></g>`;
  },

  planPrintSvgHtml(highlightObj=null, options={}){
    const { width: planW, height: planH } = this.planSize();
    const isFocus = options?.variant === 'focus' && highlightObj;
    let vbX = 0, vbY = 0, vbW = planW, vbH = planH;
    if(isFocus){
      const pad = Math.max(80, Math.min(280, Math.max(Number(highlightObj.width || 120), Number(highlightObj.height || 80)) * 1.25));
      vbX = Math.max(-20, Number(highlightObj.x || 0) - pad);
      vbY = Math.max(-20, Number(highlightObj.y || 0) - pad);
      vbW = Math.min(planW - vbX + 20, Math.max(260, Number(highlightObj.width || 120) + pad * 2));
      vbH = Math.min(planH - vbY + 20, Math.max(200, Number(highlightObj.height || 80) + pad * 2));
    } else {
      const padX = Math.max(28, Math.min(80, planW * .045));
      const padY = Math.max(30, Math.min(90, planH * .065));
      vbX = -padX;
      vbY = -padY;
      vbW = planW + padX * 2;
      vbH = planH + padY * 2;
    }
    const oldHighlight = this.highlightObjectId;
    if(highlightObj) this.highlightObjectId = String(highlightObj.id);
    const objectsSvg = this.objects.map(o => this.objectSvg(o)).join('');
    this.highlightObjectId = oldHighlight;
    const uid = 'planPrint' + Math.random().toString(36).slice(2, 8);
    const gridSteps = this.gridStepsForPlan(planW, planH);
    const focusClass = isFocus ? ' ms-plan-print-svg--focus' : ' ms-plan-print-svg--overview';
    return `<svg class="ms-plan-print-svg${focusClass}" viewBox="${vbX} ${vbY} ${vbW} ${vbH}" role="img" aria-label="Plan ${_planAttr(this.selectedPlan?.name || '')}">
      <defs>
        <filter id="${uid}Shadow" x="-20%" y="-20%" width="140%" height="140%"><feDropShadow dx="0" dy="4" stdDeviation="4" flood-color="#0f172a" flood-opacity="0.10"/></filter>
        <filter id="planShadow" x="-20%" y="-20%" width="140%" height="140%"><feDropShadow dx="0" dy="4" stdDeviation="4" flood-color="#0f172a" flood-opacity="0.10"/></filter>
        <filter id="planShadowStrong" x="-20%" y="-20%" width="140%" height="140%"><feDropShadow dx="0" dy="6" stdDeviation="6" flood-color="#f59e0b" flood-opacity="0.22"/></filter>
        <pattern id="${uid}Grid" width="${gridSteps.minor}" height="${gridSteps.minor}" patternUnits="userSpaceOnUse"><path d="M ${gridSteps.minor} 0 L 0 0 0 ${gridSteps.minor}" fill="none" stroke="#e8eef6" stroke-width="1" vector-effect="non-scaling-stroke"/></pattern>
        <pattern id="${uid}GridBig" width="${gridSteps.major}" height="${gridSteps.major}" patternUnits="userSpaceOnUse"><path d="M ${gridSteps.major} 0 L 0 0 0 ${gridSteps.major}" fill="none" stroke="#cbdaf0" stroke-width="1.25" vector-effect="non-scaling-stroke"/></pattern>
      </defs>
      <rect class="ms-plan-print-page-bg" x="${vbX}" y="${vbY}" width="${vbW}" height="${vbH}"></rect>
      <rect class="plan-stage-bg" x="0" y="0" width="${planW}" height="${planH}" rx="0"></rect>
      <rect x="0" y="0" width="${planW}" height="${planH}" rx="0" fill="url(#${uid}Grid)"></rect>
      <rect x="0" y="0" width="${planW}" height="${planH}" rx="0" fill="url(#${uid}GridBig)"></rect>
      ${isFocus ? '' : this.planPrintRulersSvg(planW, planH, gridSteps)}
      ${objectsSvg}
      ${isFocus ? '' : this.planPrintScaleSvg(vbX, vbY, vbW, vbH, gridSteps)}
    </svg>`;
  },

  planPrintLegendHtml(highlightObj=null){
    const counts = new Map();
    this.objects.forEach(o => counts.set(o.type || 'objet', (counts.get(o.type || 'objet') || 0) + 1));
    const sorted = Array.from(counts.entries()).sort((a,b) => this.typeLabel(a[0]).localeCompare(this.typeLabel(b[0]), 'fr'));
    const rows = sorted.map(([type,count]) => `<div class="ms-plan-legend-row"><span style="background:${_planAttr(this.typeColor(type))}"></span><strong>${_planEsc(this.typeLabel(type))}</strong><em>${count}</em></div>`).join('');
    const linked = this.objects.filter(o => o.linkedLocationId).length;
    return `<section class="ms-plan-print-box ms-plan-print-box--legend"><h2>Légende</h2>${rows || '<p>Aucun objet dessiné.</p>'}<div class="ms-plan-print-legend-foot"><span><b></b>Objet rattaché</span><span class="target">${highlightObj ? 'Repère orange = cible' : linked + ' emplacement(s) lié(s)'}</span></div></section>`;
  },

  planPrintLocationHtml(obj){
    const loc = this.locationById[String(obj?.linkedLocationId || '')] || this.localization?.location || null;
    const product = this.localization?.product || null;
    const stock = this.localization?.stock || (loc ? (this.locationStockById[String(loc.id)] || loc.stock || null) : null);
    const plan = this.localization?.plan || this.selectedPlan || {};
    const qty = Number(stock?.quantity ?? stock?.totalQuantity ?? 0);
    const detailRow = Array.isArray(stock?.rows) ? stock.rows[0] : null;
    const detail = [detailRow?.picking, detailRow?.location, detailRow?.rack, detailRow?.shelf].filter(Boolean).join(' · ');
    const path = [this.whName(plan.warehouseId || this.selectedPlan?.warehouseId), loc?.zone, loc?.code || loc?.label || obj?.label].filter(Boolean).join(' → ');
    const productLine = product ? `<dt>Produit</dt><dd>${_planEsc(product.name || 'Produit')} ${product.sku ? '· ' + _planEsc(product.sku) : ''}</dd>` : '';
    return `<section class="ms-plan-print-box ms-plan-print-box--focus"><h2>Localisation terrain</h2><dl>
      ${productLine}
      <dt>Magasin</dt><dd>${_planEsc(this.whName(plan.warehouseId || this.selectedPlan?.warehouseId) || plan.warehouseName || '—')}</dd>
      <dt>Plan</dt><dd>${_planEsc(plan.name || this.selectedPlan?.name || '—')}</dd>
      <dt>Objet ciblé</dt><dd>${_planEsc(obj?.label || this.typeLabel(obj?.type) || 'Objet')}</dd>
      <dt>Type</dt><dd>${_planEsc(this.typeLabel(obj?.type))}</dd>
      ${loc ? `<dt>Emplacement</dt><dd>${_planEsc(this.locationTitle(loc))}</dd><dt>Zone</dt><dd>${_planEsc(loc.zone || '—')}</dd>` : '<dt>Emplacement</dt><dd>Objet non rattaché</dd>'}
      <dt>Quantité ici</dt><dd>${Number.isFinite(qty) ? qty + ' u' : '0 u'}</dd>
      ${detail ? `<dt>Détail terrain</dt><dd>${_planEsc(detail)}</dd>` : ''}
      ${path ? `<dt>Chemin terrain</dt><dd>${_planEsc(path)}</dd>` : ''}
    </dl><p class="ms-plan-print-note">La cible est encadrée et surlignée sur le plan. Vérifiez le chemin terrain avant déplacement.</p></section>`;
  },

  planPrintChecklistHtml(highlightObj=null){
    return `<section class="ms-plan-print-box ms-plan-print-box--check"><h2>Contrôle terrain</h2><ul><li>Identifier le magasin et le plan.</li><li>Contrôler les repères métriques et l’échelle.</li><li>${highlightObj ? 'Rejoindre l’emplacement surligné.' : 'Utiliser la légende pour repérer les équipements.'}</li></ul></section>`;
  },

  planPrintCss(orientation='landscape', hasLocation=false){
    const overviewMax = orientation === 'portrait' ? '188mm' : (hasLocation ? '142mm' : '154mm');
    const focusMax = orientation === 'portrait' ? '72mm' : '60mm';
    return `:root{color-scheme:light}.ms-plan-print-export-note{display:flex;gap:8px;align-items:flex-start;border:1px solid #bfdbfe;border-left:4px solid #2563eb;background:#eff6ff;color:#1e3a8a;padding:7px 9px;margin:0 0 9px;font-size:8.6px}.ms-plan-print-export-note strong{white-space:nowrap}.ms-plan-print-document{font-family:"DM Sans",Inter,Arial,sans-serif;color:#0f172a;background:#fff}.ms-plan-print-topline{display:flex;justify-content:space-between;gap:12px;align-items:flex-end;border:1px solid #dbe3ee;border-left:4px solid #1d4ed8;background:#f8fafc;padding:7px 9px;margin:0 0 8px;break-inside:avoid}.ms-plan-print-topline span{display:block;text-transform:uppercase;letter-spacing:.08em;font-size:7.2px;color:#2563eb;font-weight:1000}.ms-plan-print-topline strong{display:block;font-size:14px;letter-spacing:-.03em}.ms-plan-print-topline em{font-style:normal;color:#64748b;font-size:8.4px;font-weight:900}.ms-plan-print-cartouche{display:grid;grid-template-columns:1.25fr 1fr .9fr 1.25fr;gap:5px;margin:0 0 8px;break-inside:avoid}.ms-plan-print-cartouche>div{border:1px solid #dbe3ee;background:#fff;border-top:3px solid #1d4ed8;padding:6px 8px;min-width:0}.ms-plan-print-cartouche span{display:block;font-size:7.1px;line-height:1;text-transform:uppercase;letter-spacing:.08em;color:#64748b;font-weight:1000}.ms-plan-print-cartouche strong{display:block;margin-top:2px;font-size:10.4px;color:#0f172a;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.ms-plan-print-cartouche em{display:block;margin-top:1px;font-size:7.7px;color:#64748b;font-style:normal;font-weight:800;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.ms-plan-print-layout{display:grid;grid-template-columns:minmax(0,1fr) 66mm;gap:6mm;align-items:start}.ms-plan-print-map-block{min-width:0}.ms-plan-print-map-block,.ms-plan-print-focus{border:1px solid #dbe3ee;background:#f8fafc;padding:3mm;break-inside:avoid}.ms-plan-print-section-head{display:flex;align-items:center;justify-content:space-between;gap:8px;margin:0 0 2.5mm;color:#64748b;font-size:8px;font-weight:900}.ms-plan-print-section-head strong{font-size:10px;color:#0f172a}.ms-plan-print-side{display:grid;gap:3.2mm}.ms-plan-print-svg{display:block;width:100%;height:auto;background:#fff;border:1px solid #cbd5e1}.ms-plan-print-svg--overview{max-height:${overviewMax}}.ms-plan-print-svg--focus{max-height:${focusMax}}.ms-plan-print-focus{margin-top:4mm}.ms-plan-print-box{border:1px solid #dbe3ee;background:#fff;padding:3.4mm;break-inside:avoid}.ms-plan-print-box h2{margin:0 0 2.6mm;font-size:10.5px;color:#1d4ed8;letter-spacing:-.015em}.ms-plan-legend-row{display:grid;grid-template-columns:10px minmax(0,1fr) auto;gap:6px;align-items:center;margin:0 0 4px;font-size:8.2px}.ms-plan-legend-row span{width:10px;height:10px;border:1px solid #94a3b8;border-radius:1px}.ms-plan-legend-row strong{min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;color:#0f172a}.ms-plan-legend-row em{font-style:normal;color:#64748b;font-weight:1000}.ms-plan-print-legend-foot{border-top:1px solid #e2e8f0;margin-top:3mm;padding-top:2mm;display:grid;gap:2px;font-size:8px;color:#64748b}.ms-plan-print-legend-foot b{display:inline-block;width:9px;height:9px;border-radius:50%;background:#16a34a;margin-right:4px;vertical-align:-1px}.ms-plan-print-legend-foot .target{color:#92400e;font-weight:900}.ms-plan-print-note{margin:3mm 0 0;color:#64748b;font-size:8px;line-height:1.35}.ms-plan-print-box dl{display:grid;grid-template-columns:24mm minmax(0,1fr);gap:1.8mm 3mm;margin:0}.ms-plan-print-box dt{font-size:7.3px;color:#64748b;text-transform:uppercase;letter-spacing:.06em;font-weight:1000}.ms-plan-print-box dd{margin:0;font-size:8.8px;font-weight:900;color:#0f172a;word-break:break-word}.ms-plan-print-box--focus{border-color:#f59e0b;background:#fff7ed}.ms-plan-print-box--info{border-color:#bfdbfe}.ms-plan-print-box--check ul{margin:0;padding-left:14px;color:#475569;font-size:8.4px;line-height:1.45}.ms-plan-print-box--check li{margin:0 0 2px}.ms-plan-print-page-bg{fill:#f8fafc}.plan-stage-bg{fill:#fffefa;stroke:#0f172a;stroke-width:2.5}.ms-plan-print-rulers line{stroke:#94a3b8;stroke-width:1.1;vector-effect:non-scaling-stroke}.ms-plan-print-rulers text,.ms-plan-print-dim-x,.ms-plan-print-dim-y{font-size:11px;font-weight:900;fill:#334155}.ms-plan-print-dim-x,.ms-plan-print-dim-y{font-size:13px}.ms-plan-print-scale-bar line{stroke:#0f172a;stroke-width:3;vector-effect:non-scaling-stroke}.ms-plan-print-scale-bar text{font-size:10px;font-weight:900;fill:#0f172a}.plan-object-main{stroke:rgba(15,23,42,.42);stroke-width:1.5}.plan-object-gloss{fill:rgba(255,255,255,.25)}.plan-object-label-bg{fill:rgba(255,255,255,.58)}.plan-object-text{font-size:8.5px;font-weight:650;fill:#0f172a}.plan-object-meta{font-size:7.6px;font-weight:650;fill:#334155}.plan-object-linked circle{fill:#16a34a;stroke:#fff;stroke-width:2}.plan-object-linked text{font-size:12px;font-weight:950;fill:#fff}.plan-object-highlight-frame{fill:rgba(245,158,11,.07);stroke:#f59e0b;stroke-width:2.2;filter:none}.plan-object-select-frame,.plan-object-handle,.plan-object-rotate-control,.plan-object-click-hit,.plan-opening-hit{display:none!important}.plan-object--allee .plan-object-main,.plan-object--circulation .plan-object-main,.plan-object--zone .plan-object-main{stroke-dasharray:10 8}.plan-object--mur .plan-object-main{fill:#334155;stroke:#0f172a}.plan-object--cloison .plan-object-main{fill:#64748b;stroke:#0f172a}.plan-object--ouverture .plan-object-main,.plan-object--fenetre .plan-object-main{fill:#fff;stroke:#334155}.plan-wall-axis,.plan-partition-axis{stroke:rgba(255,255,255,.55);stroke-width:1.1}.plan-partition-axis{stroke-dasharray:8 5}.plan-opening-cut{fill:#fff;stroke:#f59e0b;stroke-width:1}.plan-object--porte .plan-object-main{fill:#fff7ed}.plan-object--escalier .plan-object-main{fill:#f8fafc;stroke:#475569}@media print{html,body{background:#fff!important;color:#0f172a!important}.ms-plan-print-svg--overview{max-height:${overviewMax}!important}.ms-plan-print-svg--focus{max-height:${focusMax}!important}.ms-plan-print-layout{grid-template-columns:minmax(0,1fr) 58mm;gap:4mm}.ms-plan-print-cartouche>div,.ms-plan-print-box,.ms-plan-print-map-block,.ms-plan-print-focus{box-shadow:none!important}}@media(max-width:760px){.ms-plan-print-cartouche,.ms-plan-print-layout{grid-template-columns:1fr}.ms-plan-print-side{grid-template-columns:1fr 1fr}.ms-plan-print-svg{max-height:none}.ms-plan-print-focus{margin-top:4mm}}`;
  },

  renderInspector(){
    if(!this.el.inspector) return;
    const selected = this.selectedObjects();
    const obj = selected[0] || null;
    const editing = this.canEdit();
    if(!this.selectedPlan){
      this.el.inspector.innerHTML = `<h3>Résumé du plan</h3><p>Ouvrez un plan pour afficher ses informations.</p>`;
      return;
    }
    const roomRows = this.closedRooms();
    const totalArea = roomRows.reduce((sum,r) => sum + Number(r.area || 0), 0);
    const summaryHtml = `<section class="plan-inspector-section plan-inspector-section--summary">
      <h3>Résumé du plan</h3>
      <dl class="plans-inspector-kv">
        <div><dt>Nom du plan</dt><dd>${_planEsc(this.selectedPlan.name || '')}</dd></div>
        <div><dt>Magasin</dt><dd>${_planEsc(this.whName(this.selectedPlan.warehouseId))}</dd></div>
        <div><dt>Surface détectée</dt><dd>${totalArea ? this.formatRoomArea(totalArea) : '—'}</dd></div>
        <div><dt>Dimensions</dt><dd>${_planEsc(this.formatLengthUnits(this.selectedPlan.width))} × ${_planEsc(this.formatLengthUnits(this.selectedPlan.height))}</dd></div>
        <div><dt>Éléments</dt><dd>${this.objects.length}</dd></div>
        <div><dt>Pièces</dt><dd>${roomRows.length}</dd></div>
        <div><dt>Mode</dt><dd>${editing ? 'Édition' : 'Consultation'}</dd></div>
      </dl>
    </section>`;
    if(!obj){
      this.el.inspector.innerHTML = summaryHtml + (editing
        ? `<section class="plan-inspector-section"><h3>Confort édition</h3><p>Molette : zoom précis · Espace + glisser : déplacer le plan · Shift + clic : sélection multiple · Flèches : déplacement fin.</p></section>`
        : (this.isAdmin ? `<section class="plan-inspector-section"><p>Pour modifier le dessin, utilisez le bouton <strong>Modifier le plan</strong>.</p></section>` : `<div class="plan-editor-disabled">Consultation uniquement.</div>`));
      return;
    }
    if(selected.length > 1){
      const bounds = this.selectionBounds(selected);
      this.el.inspector.innerHTML = summaryHtml + `<section class="plan-inspector-section">
        <h3>Objets sélectionnés</h3>
        <div class="plans-selected-line"><strong>${selected.length} éléments</strong><span>Actions groupées disponibles.</span></div>
        <dl class="plans-inspector-kv">
          <div><dt>X min.</dt><dd>${Math.round(bounds.x)}</dd></div>
          <div><dt>Y min.</dt><dd>${Math.round(bounds.y)}</dd></div>
          <div><dt>Longueur</dt><dd>${_planEsc(this.formatLengthUnits(bounds.width))}</dd></div>
          <div><dt>Largeur</dt><dd>${_planEsc(this.formatLengthUnits(bounds.height))}</dd></div>
        </dl>
        ${editing ? `<div class="plan-mini-actions"><button class="btn btn-secondary btn-sm" id="btnInspectorDuplicate">Dupliquer</button><button class="btn btn-danger btn-sm" id="btnInspectorDelete">Supprimer</button></div>` : ''}
      </section>`;
      if(editing){
        document.getElementById('btnInspectorDuplicate')?.addEventListener('click', () => this.duplicateSelected());
        document.getElementById('btnInspectorDelete')?.addEventListener('click', () => this.deleteSelected());
      }
      return;
    }
    const typeOptions = Object.keys(this.schema?.objectTypes || {}).map(t => `<option value="${_planAttr(t)}" ${obj.type === t ? 'selected' : ''}>${_planEsc(this.typeLabel(t))}</option>`).join('');
    const locationOptions = this.locationOptionsHtml(obj.linkedLocationId);
    const linkedLoc = this.locationById[String(obj.linkedLocationId || '')] || null;
    const linkedStock = this.locationStockById[String(obj.linkedLocationId || '')] || linkedLoc?.stock || null;
    const readonly = editing ? '' : 'readonly';
    const disabled = editing ? '' : 'disabled';
    const defaults = this.schema?.objectTypes?.[obj.type] || {};
    this.el.inspector.innerHTML = summaryHtml + `<section class="plan-inspector-section plan-inspector-section--selected">
      <h3>Objet sélectionné</h3>
      <div class="plans-selected-line"><span class="plans-selected-icon">${this.componentPreviewSvg(obj.type, obj.color || defaults.color || '#dbeafe')}</span><strong>${_planEsc(obj.label || linkedLoc?.code || this.typeLabel(obj.type))}</strong></div>
      <p>${_planEsc(this.typeLabel(obj.type))}${defaults.description ? ' · ' + _planEsc(defaults.description) : ''}</p>
    </section>
    ${this.geometryInfoHtml(obj)}
    ${this.openingControlsHtml(obj, editing)}
    <section class="plan-inspector-section">
      <h3>Propriétés</h3>
      <div class="plan-field-grid plan-field-grid--inspector">
        <label>Libellé<input class="inp" data-plan-field="label" value="${_planAttr(obj.label || '')}" ${readonly}></label>
        <label>Type<select class="sel" data-plan-field="type" ${disabled}>${typeOptions}</select></label>
        <label>Couleur<input class="inp" type="color" data-plan-field="color" value="${_planAttr(obj.color || '#dbeafe')}" ${disabled}></label>
      </div>
    </section>
    <section class="plan-inspector-section">
      <h3>Dimensions</h3>
      <div class="plan-field-grid plan-field-grid--dimensions">
        <label>X<input class="inp" type="number" data-plan-field="x" value="${_planAttr(Math.round(obj.x || 0))}" ${readonly}></label>
        <label>Y<input class="inp" type="number" data-plan-field="y" value="${_planAttr(Math.round(obj.y || 0))}" ${readonly}></label>
        <label>Longueur<input class="inp" type="number" min="10" data-plan-field="width" value="${_planAttr(Math.round(obj.width || 0))}" ${readonly}></label>
        <label>Largeur<input class="inp" type="number" min="10" data-plan-field="height" value="${_planAttr(Math.round(obj.height || 0))}" ${readonly}></label>
        <label>Angle<input class="inp" type="number" data-plan-field="rotation" value="${_planAttr(Math.round(obj.rotation || 0))}" ${readonly}></label>
      </div>
    </section>
    <section class="plan-inspector-section plan-link-panel">
      <h3>Emplacement lié</h3>
      <div class="plan-field-grid plan-field-grid--inspector">
        <label>Emplacement<select class="sel" data-plan-field="linkedLocationId" ${disabled}>${locationOptions}</select></label>
        <label>Zone<input class="inp" data-plan-field="linkedZone" value="${_planAttr(obj.linkedZone || linkedLoc?.zone || '')}" ${readonly}></label>
        <label>Magasin<input class="inp" value="${_planAttr(this.whName(obj.linkedWarehouseId || linkedLoc?.warehouseId || this.selectedPlan?.warehouseId))}" readonly></label>
      </div>
      ${this.locationBusinessHtml(linkedLoc, linkedStock)}
      ${this.productLocalizationHtml(obj)}
    </section>
    <section class="plan-inspector-section">
      <h3>Actions</h3>
      <div class="plan-mini-actions">
        ${linkedLoc ? '<button class="btn btn-secondary btn-sm" id="btnInspectorFocusLocation">Centrer</button>' : ''}
        ${editing ? '<button class="btn btn-secondary btn-sm" id="btnInspectorDuplicate">Dupliquer</button><button class="btn btn-danger btn-sm" id="btnInspectorDelete">Supprimer</button>' : ''}
      </div>
    </section>`;
    if(editing){
      this.el.inspector.querySelectorAll('[data-plan-field]').forEach(input => {
        input.addEventListener(input.type === 'color' ? 'input' : 'change', () => this.updateSelectedFromField(input));
        if(input.dataset.planField === 'label') input.addEventListener('input', () => this.updateSelectedFromField(input, true));
      });
      document.getElementById('btnInspectorDuplicate')?.addEventListener('click', () => this.duplicateSelected());
      document.getElementById('btnInspectorDelete')?.addEventListener('click', () => this.deleteSelected());
      this.el.inspector.querySelectorAll('[data-plan-meta]').forEach(input => {
        input.addEventListener('change', () => this.updateOpeningMeta(input));
      });
      document.getElementById('btnOpeningFlipSwing')?.addEventListener('click', () => this.flipOpeningSwing());
      document.getElementById('btnOpeningResnap')?.addEventListener('click', () => { const selected = this.selectedObject(); if(selected && this.snapOpeningObjectToWall(selected)){ this.setDirty(true); this.drawSvg(); this.renderInspector(); toast('Ouverture recalée sur le mur proche.', 'success'); } });
    }
    document.getElementById('btnInspectorFocusLocation')?.addEventListener('click', () => this.focusLocation(obj.linkedLocationId));
  },
  openingKind(type){
    if(type === 'porte_double') return 'doubleDoor';
    if(type === 'porte') return 'door';
    if(type === 'fenetre') return 'window';
    return 'passage';
  },

  openingDefaultSwing(type){
    if(type === 'porte_double') return 'double';
    if(type === 'porte') return 'right';
    return '';
  },

  openingControlsHtml(obj, editing){
    if(!this.isOpeningType(obj?.type)) return '';
    const meta = obj.metadata || {};
    const attached = this.isOpeningAttached(obj);
    const swing = meta.swing || this.openingDefaultSwing(obj.type);
    const door = obj.type === 'porte' || obj.type === 'porte_double';
    const swingOptions = obj.type === 'porte_double'
      ? `<option value="double" ${swing === 'double' ? 'selected' : ''}>Deux battants vers le haut</option><option value="double-reverse" ${swing === 'double-reverse' ? 'selected' : ''}>Deux battants inversés</option>`
      : `<option value="right" ${swing === 'right' ? 'selected' : ''}>Battant vers la droite</option><option value="left" ${swing === 'left' ? 'selected' : ''}>Battant vers la gauche</option>`;
    return `<div class="plan-opening-panel">
      <strong>Détails de l’ouverture</strong>
      <p>${attached ? 'Accrochée au mur : la découpe est visible dans la structure.' : 'Libre : déplacez-la près d’un mur pour l’accrocher.'}</p>
      <div class="plan-field-grid">
        <label>Type d’ouverture<input class="inp" value="${_planAttr(this.typeLabel(obj.type))}" readonly></label>
        <label>Position sur mur<input class="inp" value="${attached ? Math.round(Number(meta.wallOffset || 0) * 100) + ' %' : 'Libre'}" readonly></label>
        ${door ? `<label>Sens d’ouverture<select class="sel" data-plan-meta="swing" ${editing ? '' : 'disabled'}>${swingOptions}</select></label>` : ''}
      </div>
      ${editing ? `<div class="plan-mini-actions">${door ? '<button class="btn btn-secondary btn-sm" id="btnOpeningFlipSwing">Inverser le sens</button>' : ''}<button class="btn btn-secondary btn-sm" id="btnOpeningResnap">Recaler sur le mur</button></div>` : ''}
    </div>`;
  },

  updateOpeningMeta(input){
    const obj = this.selectedObject();
    if(!obj || !this.canEdit() || !this.isOpeningType(obj.type)) return;
    const field = input.dataset.planMeta;
    obj.metadata = { ...(obj.metadata || {}) };
    obj.metadata[field] = input.value;
    this.setDirty(true);
    this.drawSvg();
    this.renderInspector();
  },

  flipOpeningSwing(){
    const obj = this.selectedObject();
    if(!obj || !this.canEdit() || !this.isOpeningType(obj.type)) return;
    obj.metadata = { ...(obj.metadata || {}) };
    if(obj.type === 'porte_double') obj.metadata.swing = obj.metadata.swing === 'double-reverse' ? 'double' : 'double-reverse';
    else obj.metadata.swing = obj.metadata.swing === 'left' ? 'right' : 'left';
    this.setDirty(true);
    this.drawSvg();
    this.renderInspector();
  },

  geometryInfoHtml(obj){
    if(this.isWallSegmentObject(obj)){
      const [a,b] = this.wallPoints(obj);
      return `<div class="plan-geometry-note"><strong>Mur dessiné par points</strong><span>Départ ${Math.round(a.x)}, ${Math.round(a.y)} · Arrivée ${Math.round(b.x)}, ${Math.round(b.y)} · Longueur ${this.formatLengthUnits(this.distance(a,b))}</span><small>Déplacez le mur complet ou tirez les deux points bleus pour ajuster les extrémités.</small></div>`;
    }
    if(this.isOpeningType(obj)){
      const attached = this.isOpeningAttached(obj);
      return `<div class="plan-geometry-note ${attached ? 'is-ok' : 'is-warning'}"><strong>${attached ? 'Ouverture accrochée à un mur' : 'Ouverture libre'}</strong><span>${attached ? 'Le mur est découpé visuellement et l’ouverture suit son angle. Position sur mur : ' + Math.round(Number(obj.metadata?.wallOffset || 0) * 100) + ' %.' : 'Déplacez-la près d’un mur pour l’accrocher automatiquement.'}</span></div>`;
    }
    const rooms = this.closedRooms();
    if(['depot','local','piece','atelier','zone'].includes(obj.type) && rooms.length){
      return `<div class="plan-geometry-note is-ok"><strong>${rooms.length} pièce${rooms.length > 1 ? 's' : ''} fermée${rooms.length > 1 ? 's' : ''} détectée${rooms.length > 1 ? 's' : ''}</strong><span>Les murs fermés sont surlignés en fond de plan.</span></div>`;
    }
    return '';
  },

  updateSelectedFromField(input, quiet=false){
    const obj = this.selectedObject();
    if(!obj || !this.canEdit()) return;
    const field = input.dataset.planField;
    let value = input.value;
    if(['x','y','width','height','rotation'].includes(field)) value = Number(value || 0);
    if(field === 'linkedLocationId'){
      const loc = this.locationById[String(value || '')] || null;
      obj.linkedLocationId = loc ? loc.id : null;
      obj.linkedWarehouseId = loc ? loc.warehouseId : (this.selectedPlan?.warehouseId || null);
      obj.linkedZone = loc ? (loc.zone || '') : '';
      if(loc && !obj.label) obj.label = loc.code || loc.label || obj.label;
      this.setDirty(true);
      this.drawSvg();
      this.renderInspector();
      this.renderLocationSearchResults();
      return;
    }
    if(field === 'type'){
      const size = this.defaultSize(value);
      obj.type = value;
      obj.color = this.typeColor(value);
      obj.icon = this.schema?.objectTypes?.[value]?.icon || '';
      obj.metadata = { ...(obj.metadata || {}), shape:this.schema?.objectTypes?.[value]?.shape || '', componentLabel:this.typeLabel(value) };
      obj.width = Math.max(24, Number(size.width || obj.width || 160));
      obj.height = Math.max(20, Number(size.height || obj.height || 80));
      if(!obj.label) obj.label = this.typeLabel(value);
    }else{
      obj[field] = value;
    }
    if(field === 'width') obj.width = Math.max(10, Number(obj.width || 10));
    if(field === 'height') obj.height = Math.max(10, Number(obj.height || 10));
    if(field === 'rotation') obj.rotation = ((Number(obj.rotation || 0) % 360) + 360) % 360;
    this.setDirty(true);
    this.drawSvg();
    if(!quiet) this.renderInspector();
  },

  planSize(){
    return {
      width: Math.max(100, Number(this.selectedPlan?.width || 1600)),
      height: Math.max(100, Number(this.selectedPlan?.height || 1000)),
    };
  },

  rotatedRectExtents(width, height, rotation=0){
    const w = Math.max(1, Number(width || 1));
    const h = Math.max(1, Number(height || 1));
    const angle = (((Number(rotation || 0) % 360) + 360) % 360) * Math.PI / 180;
    const c = Math.abs(Math.cos(angle));
    const s = Math.abs(Math.sin(angle));
    return { width:w * c + h * s, height:w * s + h * c };
  },

  clampObjectRect(rect){
    const plan = this.planSize();
    const w = Math.max(24, Number(rect.width || 24));
    const h = Math.max(20, Number(rect.height || 20));
    const ext = this.rotatedRectExtents(w, h, Number(rect.rotation || 0));
    const minCx = Math.min(plan.width / 2, ext.width / 2);
    const maxCx = Math.max(minCx, plan.width - ext.width / 2);
    const minCy = Math.min(plan.height / 2, ext.height / 2);
    const maxCy = Math.max(minCy, plan.height - ext.height / 2);
    const rawCx = Number(rect.x || 0) + w / 2;
    const rawCy = Number(rect.y || 0) + h / 2;
    const cx = _planClamp(rawCx, minCx, maxCx);
    const cy = _planClamp(rawCy, minCy, maxCy);
    return {
      x: Number((cx - w / 2).toFixed(2)),
      y: Number((cy - h / 2).toFixed(2)),
      width:w,
      height:h,
    };
  },

  edgeGuideLines(bounds, skipIds=[]){
    const plan = this.planSize();
    const skip = new Set((skipIds || []).map(String));
    const tol = Math.max(5, 10 / Math.max(0.35, this.zoom || 1));
    const xTargets = [{ value:0 }, { value:plan.width / 2 }, { value:plan.width }];
    const yTargets = [{ value:0 }, { value:plan.height / 2 }, { value:plan.height }];
    this.objects.forEach(o => {
      if(skip.has(String(o.id))) return;
      const b = this.objectBounds(o);
      const ox = Number(b.x || 0), oy = Number(b.y || 0), ow = Number(b.width || 0), oh = Number(b.height || 0);
      xTargets.push({ value:ox }, { value:ox + ow / 2 }, { value:ox + ow });
      yTargets.push({ value:oy }, { value:oy + oh / 2 }, { value:oy + oh });
    });
    const xCandidates = [bounds.x, bounds.x + bounds.width / 2, bounds.x + bounds.width];
    const yCandidates = [bounds.y, bounds.y + bounds.height / 2, bounds.y + bounds.height];
    let bestX = { dist:Infinity, dx:0, value:null };
    let bestY = { dist:Infinity, dy:0, value:null };
    xTargets.forEach(t => xCandidates.forEach(c => {
      const d = t.value - c;
      if(Math.abs(d) < Math.abs(bestX.dist) && Math.abs(d) <= tol) bestX = { dist:d, dx:d, value:t.value };
    }));
    yTargets.forEach(t => yCandidates.forEach(c => {
      const d = t.value - c;
      if(Math.abs(d) < Math.abs(bestY.dist) && Math.abs(d) <= tol) bestY = { dist:d, dy:d, value:t.value };
    }));
    const lines = [];
    if(bestX.value !== null) lines.push({ axis:'x', value:bestX.value });
    if(bestY.value !== null) lines.push({ axis:'y', value:bestY.value });
    return { dx: bestX.value !== null ? bestX.dx : 0, dy: bestY.value !== null ? bestY.dy : 0, lines };
  },

  applyMoveGuides(moved, skipIds=[]){
    if(!this.snapEnabled || !moved.length){ this.guideLines = []; return { dx:0, dy:0 }; }
    const b = this.selectionBounds(moved);
    const guide = this.edgeGuideLines(b, skipIds);
    this.guideLines = guide.lines;
    return { dx:guide.dx, dy:guide.dy };
  },

  selectAllObjects(){
    if(!this.canEdit() || !this.objects.length) return;
    this.setSelection(this.objects.map(o => o.id));
    this.renderEditor();
    toast(this.objects.length + ' élément' + (this.objects.length > 1 ? 's sélectionnés.' : ' sélectionné.'), 'info');
  },

  focusSelection(){
    const selected = this.selectedObjects();
    if(!this.selectedPlan || !selected.length){ toast('Sélectionnez un élément à centrer.', 'warning'); return; }
    const bounds = this.selectionBounds(selected);
    const wrap = this.el.canvasWrap?.getBoundingClientRect?.() || { width:900, height:560 };
    const targetZoom = _planClamp(Math.min((wrap.width - 90) / Math.max(40, bounds.width), (wrap.height - 90) / Math.max(40, bounds.height), 2.2), 0.18, 3.5);
    this.zoom = targetZoom;
    this.pan.x = Math.round((wrap.width || 900) / 2 - (bounds.x + bounds.width / 2) * this.zoom);
    this.pan.y = Math.round((wrap.height || 560) / 2 - (bounds.y + bounds.height / 2) * this.zoom);
    this.drawSvg();
  },

  moveSelectionBy(dx, dy){
    if(!this.canEdit()) return;
    const selected = this.selectedObjects();
    if(!selected.length) return;
    selected.forEach(obj => {
      if(this.isWallSegmentObject(obj)){
        const pts = this.wallPoints(obj).map(pt => ({ x:pt.x + dx, y:pt.y + dy }));
        this.applyWallPoints(obj, pts[0], pts[1]);
        return;
      }
      const clamped = this.clampObjectRect({ x:Number(obj.x || 0) + dx, y:Number(obj.y || 0) + dy, width:Number(obj.width || 0), height:Number(obj.height || 0), rotation:Number(obj.rotation || 0) });
      obj.x = this.snapEnabled ? this.snap(clamped.x, 1) : Math.round(clamped.x);
      obj.y = this.snapEnabled ? this.snap(clamped.y, 1) : Math.round(clamped.y);
    });
    this.setDirty(true);
    this.drawSvg();
    this.renderInspector();
  },

  duplicateSelected(){
    if(!this.canEdit()) return;
    const selected = this.selectedObjects();
    if(!selected.length){ toast('Sélectionnez un objet à dupliquer.', 'warning'); return; }
    const copies = selected.map((obj, index) => this.normalizeClientObject({
      ...obj,
      id:'tmp_' + Date.now() + '_' + index + '_' + Math.random().toString(16).slice(2),
      x:Number(obj.x || 0) + 30,
      y:Number(obj.y || 0) + 30,
      metadata: this.isWallSegmentObject(obj) ? { ...(obj.metadata || {}), points:this.wallPoints(obj).map(pt => ({ x:pt.x + 30, y:pt.y + 30 })) } : { ...(obj.metadata || {}) },
      label:(obj.label || this.typeLabel(obj.type)) + ' copie',
      linkedLocationId:null,
      linkedZone:'',
      linkedProductId:null
    }));
    this.objects.push(...copies);
    this.setSelection(copies.map(o => o.id), copies[0]?.id || null);
    this.setDirty(true);
    this.renderEditor();
  },

  deleteSelected(){
    if(!this.canEdit()) return;
    const selected = this.selectedObjects();
    if(!selected.length){ toast('Sélectionnez un objet à supprimer.', 'warning'); return; }
    const needsConfirm = selected.length > 1 || selected.some(o => o.linkedLocationId);
    if(needsConfirm){
      const msg = selected.length > 1
        ? 'Supprimer les ' + selected.length + ' éléments sélectionnés ? Les rattachements MageStock seront retirés du plan.'
        : 'Supprimer cet élément et son rattachement MageStock sur le plan ?';
      if(!confirm(msg)) return;
    }
    const ids = selected.map(o => String(o.id));
    this.objects = this.objects.filter(o => !ids.includes(String(o.id)));
    this.clearSelection();
    this.setDirty(true);
    this.renderEditor();
  },

  selectionBounds(items=null){
    const rows = items || this.selectedObjects();
    if(!rows.length) return {x:0,y:0,width:0,height:0,right:0,bottom:0};
    const bounds = rows.map(o => this.objectBounds(o));
    const left = Math.min(...bounds.map(b => Number(b.x || 0)));
    const top = Math.min(...bounds.map(b => Number(b.y || 0)));
    const right = Math.max(...bounds.map(b => Number(b.x || 0) + Number(b.width || 0)));
    const bottom = Math.max(...bounds.map(b => Number(b.y || 0) + Number(b.height || 0)));
    return { x:left, y:top, width:right-left, height:bottom-top, right, bottom };
  },

  alignSelection(mode){
    if(!this.canEdit()) return;
    const selected = this.selectedObjects();
    if(!selected.length){ toast('Sélectionnez au moins un objet.', 'warning'); return; }
    const { width: planW, height: planH } = this.planSize();
    const b = this.selectionBounds(selected);
    if(mode === 'left') selected.forEach(o => { o.x = this.snap(b.x); });
    if(mode === 'top') selected.forEach(o => { o.y = this.snap(b.y); });
    if(mode === 'hcenter') selected.forEach(o => { o.x = this.snap((planW - Number(o.width || 0)) / 2); });
    if(mode === 'vcenter') selected.forEach(o => { o.y = this.snap((planH - Number(o.height || 0)) / 2); });
    if(mode === 'distributeX'){
      if(selected.length < 3){ toast('Sélectionnez au moins 3 objets pour répartir horizontalement.', 'warning'); return; }
      const sorted = selected.slice().sort((a,b) => Number(a.x || 0) - Number(b.x || 0));
      const minX = Number(sorted[0].x || 0);
      const maxRight = Math.max(...sorted.map(o => Number(o.x || 0) + Number(o.width || 0)));
      const totalW = sorted.reduce((sum,o) => sum + Number(o.width || 0), 0);
      const gap = Math.max(0, (maxRight - minX - totalW) / (sorted.length - 1));
      let cursor = minX;
      sorted.forEach(o => { o.x = this.snap(cursor); cursor += Number(o.width || 0) + gap; });
    }
    this.setDirty(true);
    this.renderEditor();
  },

  rotateSelection(mode){
    if(!this.canEdit()) return;
    const selected = this.selectedObjects();
    if(!selected.length){ toast('Sélectionnez au moins un objet à pivoter.', 'warning'); return; }
    const stepMap = { left:-90, right:90, slightLeft:-15, slightRight:15, reset:0 };
    selected.forEach(o => {
      const current = Number(o.rotation || 0);
      o.rotation = mode === 'reset' ? 0 : ((current + (stepMap[mode] ?? 90)) % 360 + 360) % 360;
    });
    this.setDirty(true);
    this.renderEditor();
  },

  resizeSelection(mode){
    if(!this.canEdit()) return;
    const selected = this.selectedObjects();
    if(!selected.length){ toast('Sélectionnez au moins un objet.', 'warning'); return; }
    selected.forEach(o => {
      const d = this.defaultSize(o.type);
      if(mode === 'default'){ o.width = d.width; o.height = d.height; }
      if(mode === 'compact'){ o.width = Math.max(50, Math.round(d.width * .72)); o.height = Math.max(34, Math.round(d.height * .72)); }
      if(mode === 'wide'){ o.width = Math.max(70, Math.round(d.width * 1.35)); o.height = Math.max(30, Math.round(d.height * .9)); }
      if(mode === 'tall'){ o.width = Math.max(50, Math.round(d.width * .86)); o.height = Math.max(50, Math.round(d.height * 1.35)); }
    });
    this.setDirty(true);
    this.renderEditor();
  },

  async saveObjects(){
    if(!this.canEdit()) return;
    try{
      const payload = this.objects.map(o => ({
        id: String(o.id || ''),
        planId: this.selectedPlan.id,
        type: o.type,
        label: o.label,
        x: Number(o.x || 0),
        y: Number(o.y || 0),
        width: Number(o.width || 10),
        height: Number(o.height || 10),
        rotation: Number(o.rotation || 0),
        color: o.color,
        icon: o.icon || '',
        linkedWarehouseId: o.linkedWarehouseId ?? null,
        linkedLocationId: o.linkedLocationId ?? null,
        linkedZone: o.linkedZone ?? '',
        linkedProductId: o.linkedProductId ?? null,
        metadata: o.metadata || {},
      }));
      const res = await Api.planObjectsSave(this.selectedPlan.id, payload);
      this.objects = Array.isArray(res?.objects) ? res.objects.map(o => this.normalizeClientObject(o)) : [];
      this.clearSelection();
      this.setDirty(false);
      this.renderEditor();
      toast('Plan enregistré.', 'success');
    }catch(e){
      toast(e.message || 'Erreur pendant la sauvegarde du plan.', 'error');
    }
  },

  onKey(e){
    if(!this.selectedPlan) return;
    const tag = String(e.target?.tagName || '').toLowerCase();
    if(['input','textarea','select'].includes(tag)) return;
    if(e.code === 'Space'){
      e.preventDefault();
      this.spacePan = true;
      this.drawSvg();
      return;
    }
    if(e.key === 'Escape'){
      if(this.wallDraft){
        e.preventDefault();
        this.cancelWallDraft(false);
        this.setTool('select');
        toast('Tracé du mur annulé.', 'info');
        return;
      }
      this.clearSelection();
      this.setTool('select');
      this.renderInspector();
      this.drawSvg();
    }
    if(e.key === 'Enter' && this.wallDraft){
      e.preventDefault();
      this.finishWallChain();
      return;
    }
    if((e.key === 'Delete' || e.key === 'Backspace') && this.canEdit() && this.selectedObjects().length){
      e.preventDefault();
      this.deleteSelected();
    }
    if((e.ctrlKey || e.metaKey) && String(e.key || '').toLowerCase() === 'd' && this.canEdit()){
      e.preventDefault();
      this.duplicateSelected();
    }
    if((e.ctrlKey || e.metaKey) && String(e.key || '').toLowerCase() === 'a' && this.canEdit()){
      e.preventDefault();
      this.selectAllObjects();
    }
    if((e.ctrlKey || e.metaKey) && (e.key === '0') && this.selectedPlan){
      e.preventDefault();
      this.fitCanvas();
    }
    if(String(e.key || '').toLowerCase() === 'r' && this.canEdit() && this.selectedObjects().length){
      e.preventDefault();
      this.rotateSelection(e.shiftKey ? 'left' : 'right');
    }
    if(['ArrowUp','ArrowDown','ArrowLeft','ArrowRight'].includes(e.key) && this.canEdit() && this.selectedObjects().length){
      e.preventDefault();
      const step = e.shiftKey ? 20 : 2;
      const dx = e.key === 'ArrowLeft' ? -step : (e.key === 'ArrowRight' ? step : 0);
      const dy = e.key === 'ArrowUp' ? -step : (e.key === 'ArrowDown' ? step : 0);
      this.moveSelectionBy(dx, dy);
    }
    if((e.key === '+' || e.key === '=') && this.selectedPlan){ e.preventDefault(); this.setZoom(this.zoom * 1.12); }
    if((e.key === '-' || e.key === '_') && this.selectedPlan){ e.preventDefault(); this.setZoom(this.zoom / 1.12); }
  },

  openPlanModal(row){
    if(!this.isAdmin){ toast('La modification des plans est réservée aux administrateurs.', 'warning'); return; }
    const isEdit = !!row;
    const whOpts = this.warehouses.map(w => `<option value="${_planAttr(w.id)}" ${String(row?.warehouseId || '') === String(w.id) ? 'selected' : ''}>${_planEsc(w.name || ('Magasin #' + w.id))}</option>`).join('');
    const type = row?.type || 'depot';
    const html = `<div class="form-grid">
      <label>Nom du plan<input class="inp" id="planFormName" value="${_planAttr(row?.name || '')}" placeholder="Ex. Dépôt principal"></label>
      <label>Magasin<select class="sel" id="planFormWarehouse">${whOpts}</select></label>
      <label>Type<select class="sel" id="planFormType">
        ${['depot','entrepot','atelier','local','magasin','piece','zone'].map(t => `<option value="${t}" ${type === t ? 'selected' : ''}>${t}</option>`).join('')}
      </select></label>
      <label>Longueur du canevas<input class="inp" id="planFormWidth" type="number" min="100" value="${_planAttr(row?.width || 1600)}"></label>
      <label>Largeur du canevas<input class="inp" id="planFormHeight" type="number" min="100" value="${_planAttr(row?.height || 1000)}"></label>
      <label>Échelle métier<input class="inp" id="planFormScale" type="number" min="0.01" step="0.01" value="${_planAttr(row?.scale || 1)}"></label>
      <label style="grid-column:1/-1;display:flex;align-items:center;gap:8px"><input id="planFormActive" type="checkbox" ${row?.active === false ? '' : 'checked'}> Plan actif</label>
      <p style="grid-column:1/-1;color:var(--text-muted);margin:0">Les dimensions définissent la surface logique du canevas. Les objets dessinés restent en coordonnées internes et seront conservés à l’enregistrement.</p>
    </div>`;
    Modal.show(isEdit ? 'Modifier le plan' : 'Créer un plan', html, [
      {label:'Annuler', cls:'btn-secondary', onclick:() => Modal.close()},
      {label:isEdit ? 'Enregistrer' : 'Créer', cls:'btn-primary', onclick:() => this.savePlan(row)}
    ], 'info');
  },

  async savePlan(row){
    const data = {
      name: document.getElementById('planFormName')?.value || '',
      warehouseId: document.getElementById('planFormWarehouse')?.value || '',
      type: document.getElementById('planFormType')?.value || 'depot',
      width: parseInt(document.getElementById('planFormWidth')?.value || '1600', 10),
      height: parseInt(document.getElementById('planFormHeight')?.value || '1000', 10),
      scale: parseFloat(document.getElementById('planFormScale')?.value || '1'),
      active: !!document.getElementById('planFormActive')?.checked,
    };
    try{
      let saved;
      if(row?.id) saved = await Api.planUpdate({ ...data, id: row.id });
      else saved = await Api.planCreate(data);
      Modal.close();
      toast(row?.id ? 'Plan mis à jour.' : 'Plan créé.', 'success');
      await this.loadPlansOnly();
      if(saved?.id){
        if(!row) { this.goEditor(saved.id); return; }
        await this.openPlan(saved, { editMode: this.isDedicatedEditor });
      }
    }catch(e){ toast(e.message || 'Erreur pendant la sauvegarde du plan.', 'error'); }
  },

  async deletePlan(row){
    if(!this.isAdmin) return;
    if(!confirm(`Supprimer le plan “${row.name || 'sans nom'}” ? Les objets dessinés rattachés seront aussi supprimés.`)) return;
    try{
      await Api.planDelete(row.id);
      toast('Plan supprimé.', 'success');
      if(String(this.selectedPlan?.id || '') === String(row.id)) this.closeEditor();
      await this.loadPlansOnly();
    }catch(e){ toast(e.message || 'Erreur pendant la suppression.', 'error'); }
  },

  openSchemaModal(){
    const types = Object.entries(this.schema?.objectTypes || {});
    const html = `<div>
      <p style="margin-top:0;color:var(--text-muted)">Les composants permettent de dessiner rapidement un dépôt, une pièce, une armoire ou un rack. Ils ne remplacent pas les emplacements MageStock : ils servent à les visualiser.</p>
      <div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:8px;max-height:420px;overflow:auto">
        ${types.map(([key,t]) => `<div style="padding:10px;border:1px solid var(--border);border-radius:12px"><span class="plan-tool-dot" style="background:${_planAttr(t.color || '#dbeafe')}"></span> <strong>${_planEsc(t.label || key)}</strong><div style="font-size:12px;color:var(--text-muted);margin-top:4px">${_planEsc(key)} · ${_planEsc(t.category || '')}</div></div>`).join('')}
      </div>
    </div>`;
    Modal.show('Aide rapide des composants', html, [{label:'Fermer', cls:'btn-primary', onclick:() => Modal.close()}], 'info');
  }
};

window.PlansPage = PlansPage;
