/** * mp-welcome-modal.js — Composant autonome MonPermisIA * Modale "Démarrage Graphique Intelligent" * Compatible : editeur-plan.html · architecte-ia.html * Dépend de : mp-core.js (MPC) — à charger avant ce script * ───────────────────────────────────────────────────────── * Déclenchement : automatique au chargement si geometry_state.batiment vide * Expose : window.MPWelcome.show() / .hide() */ (function () { 'use strict'; /* ───────────────────── CONSTANTES ───────────────────── */ const WORKER = 'https://anthropicapi.venceslas.workers.dev'; const FONT_SYNE = 'https://fonts.googleapis.com/css2?family=Syne:wght@700;900&display=swap'; /* ───────────────────── CSS GLOBAL ───────────────────── */ const CSS = ` @import url('${FONT_SYNE}'); #mpwm-overlay { position: fixed; inset: 0; z-index: 99999; background: rgba(5,6,10,.82); backdrop-filter: blur(6px); display: flex; align-items: center; justify-content: center; padding: 1rem; animation: mpwm-fadein .22s ease; } @keyframes mpwm-fadein { from { opacity:0; transform:scale(.97) } to { opacity:1; transform:scale(1) } } #mpwm-modal { background: #0D0E11; border: 1px solid rgba(255,255,255,.08); border-radius: 20px; width: 100%; max-width: 820px; max-height: 92vh; overflow-y: auto; box-shadow: 0 32px 80px rgba(0,0,0,.7); padding: 2rem 2rem 1.5rem; box-sizing: border-box; scrollbar-width: thin; scrollbar-color: rgba(255,255,255,.12) transparent; } #mpwm-header { display: flex; align-items: flex-start; justify-content: space-between; margin-bottom: 1.5rem; } #mpwm-title { font-family: 'Syne', sans-serif; font-weight: 900; font-size: 1.35rem; color: #fff; line-height: 1.2; } #mpwm-sub { font-size: .78rem; color: rgba(255,255,255,.4); margin-top: .3rem; font-family: 'DM Sans', sans-serif; } #mpwm-close { background: rgba(255,255,255,.06); border: 1px solid rgba(255,255,255,.1); color: rgba(255,255,255,.5); width: 34px; height: 34px; border-radius: 50%; cursor: pointer; font-size: 1rem; display: flex; align-items: center; justify-content: center; flex-shrink: 0; margin-left: 1rem; transition: background .15s, color .15s; } #mpwm-close:hover { background: rgba(255,255,255,.12); color: #fff; } #mpwm-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: .75rem; } @media (max-width: 600px) { #mpwm-grid { grid-template-columns: repeat(2, 1fr); } } .mpwm-card { background: #141419; border: 1.5px solid rgba(255,255,255,.07); border-radius: 14px; padding: 1.2rem 1rem 1rem; cursor: pointer; transition: border-color .18s, background .18s, transform .15s; display: flex; flex-direction: column; align-items: flex-start; gap: .55rem; min-height: 130px; } .mpwm-card:hover { border-color: #2563EB; background: #16171e; transform: translateY(-2px); } .mpwm-card.--ai:hover { border-color: #7C3AED; } .mpwm-card.--plu:hover { border-color: #059669; } .mpwm-card.--scan:hover{ border-color: #D97706; } .mpwm-icon { width: 40px; height: 40px; border-radius: 10px; display: flex; align-items: center; justify-content: center; flex-shrink: 0; } .mpwm-icon.--blue { background: rgba(37,99,235,.15); } .mpwm-icon.--purple { background: rgba(124,58,237,.15); } .mpwm-icon.--green { background: rgba(5,150,105,.15); } .mpwm-icon.--slate { background: rgba(100,116,139,.13); } .mpwm-icon.--amber { background: rgba(217,119,6,.15); } .mpwm-card-title { font-family: 'Syne', sans-serif; font-weight: 700; font-size: .88rem; color: #fff; line-height: 1.3; } .mpwm-card-desc { font-size: .72rem; color: rgba(255,255,255,.38); line-height: 1.45; font-family: 'DM Sans', sans-serif; } /* ── Panneau secondaire ── */ #mpwm-panel { display: none; margin-top: 1rem; background: #141419; border: 1px solid rgba(255,255,255,.09); border-radius: 14px; padding: 1.25rem 1.2rem; } #mpwm-panel.--visible { display: block; animation: mpwm-fadein .18s ease; } .mpwm-panel-title { font-family: 'Syne', sans-serif; font-weight: 700; font-size: .88rem; color: #fff; margin-bottom: .75rem; } .mpwm-row { display: flex; gap: .5rem; align-items: center; flex-wrap: wrap; margin-bottom: .6rem; } .mpwm-input { flex: 1; min-width: 140px; background: #0D0E11; border: 1.5px solid rgba(255,255,255,.12); border-radius: 8px; color: #fff; font-size: .82rem; padding: .45rem .7rem; outline: none; font-family: 'DM Mono', monospace; transition: border-color .15s; } .mpwm-input:focus { border-color: #2563EB; } .mpwm-label { font-size: .72rem; color: rgba(255,255,255,.5); font-family: 'DM Sans', sans-serif; white-space: nowrap; } .mpwm-btn { padding: .5rem 1.1rem; border-radius: 8px; font-size: .8rem; font-weight: 700; cursor: pointer; border: none; transition: opacity .15s, transform .1s; font-family: 'DM Sans', sans-serif; } .mpwm-btn:active { transform: scale(.97); } .mpwm-btn.--primary { background: #2563EB; color: #fff; } .mpwm-btn.--purple { background: #7C3AED; color: #fff; } .mpwm-btn.--green { background: #059669; color: #fff; } .mpwm-btn.--amber { background: #D97706; color: #fff; } .mpwm-btn.--ghost { background: rgba(255,255,255,.07); color: rgba(255,255,255,.6); } .mpwm-btn:disabled { opacity: .4; cursor: not-allowed; } .mpwm-textarea { width: 100%; box-sizing: border-box; background: #0D0E11; border: 1.5px solid rgba(255,255,255,.12); border-radius: 8px; color: #fff; font-size: .82rem; padding: .6rem .8rem; outline: none; resize: vertical; min-height: 80px; font-family: 'DM Sans', sans-serif; line-height: 1.5; transition: border-color .15s; } .mpwm-textarea:focus { border-color: #7C3AED; } #mpwm-ai-status { font-size: .72rem; color: rgba(255,255,255,.4); font-family: 'DM Sans', sans-serif; margin-top: .4rem; min-height: 1.1rem; } /* loader dots */ .mpwm-dots::after { content: ''; animation: mpwm-dots 1.2s infinite; } @keyframes mpwm-dots { 0% { content: '.'; } 33% { content: '..'; } 66% { content: '...'; } } /* gabarit shape-picker */ .mpwm-shapes { display: flex; gap: .5rem; flex-wrap: wrap; margin-bottom: .75rem; } .mpwm-shape-btn { padding: .35rem .9rem; border-radius: 8px; border: 1.5px solid rgba(255,255,255,.13); background: transparent; color: rgba(255,255,255,.65); font-size: .78rem; font-weight: 700; cursor: pointer; transition: border-color .15s, background .15s, color .15s; font-family: 'Syne', sans-serif; } .mpwm-shape-btn.--active, .mpwm-shape-btn:hover { border-color: #2563EB; background: rgba(37,99,235,.12); color: #fff; } /* upload zone */ .mpwm-upload-zone { border: 2px dashed rgba(255,255,255,.15); border-radius: 10px; padding: 1.5rem; text-align: center; cursor: pointer; transition: border-color .15s, background .15s; } .mpwm-upload-zone:hover, .mpwm-upload-zone.--drag { border-color: #D97706; background: rgba(217,119,6,.05); } .mpwm-upload-zone input { display: none; } .mpwm-upload-text { font-size: .78rem; color: rgba(255,255,255,.4); font-family: 'DM Sans', sans-serif; line-height: 1.5; } /* footer modal */ #mpwm-footer { display: flex; align-items: center; justify-content: flex-end; margin-top: 1.2rem; gap: .5rem; } #mpwm-skip { font-size: .72rem; color: rgba(255,255,255,.3); cursor: pointer; font-family: 'DM Sans', sans-serif; text-decoration: underline; background: none; border: none; padding: .2rem .4rem; transition: color .15s; } #mpwm-skip:hover { color: rgba(255,255,255,.6); } `; /* ───────────────────── HTML ───────────────────── */ function buildHTML() { return `
`; } /* ─────────────── GÉNÉRATEURS DE FORMES ─────────────── */ /** Retourne les points (px) d'un rectangle centré sur le canvas */ function _rectPts(cx, cy, wM, dM, scale) { const wPx = wM * scale, hPx = dM * scale; return [ { x: cx - wPx / 2, y: cy - hPx / 2 }, { x: cx + wPx / 2, y: cy - hPx / 2 }, { x: cx + wPx / 2, y: cy + hPx / 2 }, { x: cx - wPx / 2, y: cy + hPx / 2 }, ]; } function shapeSquare(w, d, cx, cy, scale) { return { batiment: { x: cx - (w * scale) / 2, y: cy - (d * scale) / 2, w: w * scale, h: d * scale, rot: 0 }, rooms: [], label: `Carré ${w}×${d} m` }; } function shapeL(w, d, aileW, aileD, cx, cy, scale) { // Corps principal + aile gauche en haut const bx = cx - (w * scale) / 2, by = cy - (d * scale) / 2; return { batiment: { x: bx, y: by, w: w * scale, h: d * scale, rot: 0 }, rooms: [ { id: 'r1', nom: 'Aile principale', x: 0, y: aileD / d, w: 1, h: (d - aileD) / d }, { id: 'r2', nom: 'Aile transversale', x: 0, y: 0, w: aileW / w, h: aileD / d }, ], label: `L ${w}×${d} m (aile ${aileW}×${aileD})` }; } function shapeT(w, d, aileW, aileD, cx, cy, scale) { return { batiment: { x: cx - (w * scale) / 2, y: cy - (d * scale) / 2, w: w * scale, h: d * scale, rot: 0 }, rooms: [ { id: 'r1', nom: 'Corps principal', x: 0, y: aileD / d, w: 1, h: (d - aileD) / d }, { id: 'r2', nom: 'Aile centrale', x: (w / 2 - aileW / 2) / w, y: 0, w: aileW / w, h: aileD / d }, ], label: `T ${w}×${d} m (aile centrale ${aileW}×${aileD})` }; } function shapeU(w, d, aileW, aileD, cx, cy, scale) { return { batiment: { x: cx - (w * scale) / 2, y: cy - (d * scale) / 2, w: w * scale, h: d * scale, rot: 0 }, rooms: [ { id: 'r1', nom: 'Fond', x: 0, y: 0, w: 1, h: aileD / d }, { id: 'r2', nom: 'Aile gauche', x: 0, y: aileD / d, w: aileW / w, h: (d - aileD) / d }, { id: 'r3', nom: 'Aile droite', x: (w - aileW) / w, y: aileD / d, w: aileW / w, h: (d - aileD) / d }, ], label: `U ${w}×${d} m` }; } /* ─────────── EMPRISE PLU ─────────── */ function computePLUEmprise(scale, cx, cy) { const d = (typeof MPC !== 'undefined') ? MPC.load() : {}; const rv = d.plu_rules?.rv_min ?? 5; const rl = d.plu_rules?.rl_min ?? 3; const parcSurf = d.plu_rules?.parcelle_surface ?? 500; const cos = d.plu_rules?.cos ?? null; // Estimation simple de la parcelle (rectangle) depuis surface const parcW = Math.sqrt(parcSurf * 1.5); const parcD = parcSurf / parcW; const bW = Math.max(4, parcW - 2 * rl); const bD = Math.max(4, parcD - rv - 3); // recul avant + recul postérieur minimal 3m const bWpx = bW * scale; const bDpx = bD * scale; return { batiment: { x: cx - bWpx / 2, y: cy - bDpx / 2, w: bWpx, h: bDpx, rot: 0 }, rooms: [], meta: { rv, rl, bW: bW.toFixed(1), bD: bD.toFixed(1) } }; } /* ───── APPLY SHAPE TO PAGE STATE ───── */ function applyShape(shape) { // editeur-plan.html — variables globales if (typeof batiment !== 'undefined') { batiment.x = shape.batiment.x; batiment.y = shape.batiment.y; batiment.w = shape.batiment.w; batiment.h = shape.batiment.h; } if (typeof pieces !== 'undefined' && shape.rooms) { pieces = shape.rooms.map(r => ({ id: r.id || ('r' + Math.random().toString(36).slice(2)), nom: r.nom, x: shape.batiment.x + r.x * shape.batiment.w, y: shape.batiment.y + r.y * shape.batiment.h, w: r.w * shape.batiment.w, h: r.h * shape.batiment.h, color: 'rgba(37,99,235,.22)' })); } // Persister via MPC if (typeof MPC !== 'undefined') { MPC.updateGeometry({ batiment: { x: 0, y: 0, w: shape.batiment.w / (typeof SCALE !== 'undefined' ? SCALE : 20), d: shape.batiment.h / (typeof SCALE !== 'undefined' ? SCALE : 20), h: 0, rot: 0 }, rooms: shape.rooms || [] }, 'welcome_modal'); } if (typeof render === 'function') render(); if (typeof renderPlan === 'function') renderPlan(); } /* ───────────────────── INIT MODAL ───────────────────── */ function inject() { // Styles const style = document.createElement('style'); style.textContent = CSS; document.head.appendChild(style); // HTML const wrap = document.createElement('div'); wrap.innerHTML = buildHTML(); document.body.appendChild(wrap.firstElementChild); bindEvents(); } /* ─────────────── BINDING EVENTS ─────────────── */ function bindEvents() { const overlay = document.getElementById('mpwm-overlay'); const panel = document.getElementById('mpwm-panel'); // Fermeture document.getElementById('mpwm-close').addEventListener('click', hide); document.getElementById('mpwm-skip').addEventListener('click', hide); overlay.addEventListener('click', e => { if (e.target === overlay) hide(); }); document.addEventListener('keydown', e => { if (e.key === 'Escape') hide(); }); // Cartes document.querySelectorAll('.mpwm-card').forEach(card => { card.addEventListener('click', () => { const action = card.dataset.action; document.querySelectorAll('.mpwm-card').forEach(c => c.style.borderColor = ''); card.style.borderColor = '#2563EB'; openPanel(action); }); }); } /* ─────────────── PANNEAUX SECONDAIRES ─────────────── */ function openPanel(action) { const panel = document.getElementById('mpwm-panel'); panel.innerHTML = ''; panel.classList.remove('--visible'); const handlers = { vide, ia, plu, gabarit, cadastre, scan }; if (handlers[action]) { handlers[action](panel); panel.classList.add('--visible'); panel.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); } } /* 1. Plan Vide */ function vide(panel) { panel.innerHTML = `
Partir d'un canevas vide

Un espace neutre sans aucune forme prédéfinie. Dessinez librement avec les outils de la barre latérale.

`; document.getElementById('mpwm-do-vide').addEventListener('click', () => { if (typeof MPC !== 'undefined') MPC.updateGeometry({ batiment: { x: 0, y: 0, w: 0, d: 0, h: 0, rot: 0 }, rooms: [] }, 'welcome_modal'); if (typeof render === 'function') render(); hide(); }); } /* 2. Configuration libre IA */ function ia(panel) { panel.innerHTML = `
✦ Configuration libre par IA
`; const examples = [ 'Maison en V avec deux ailes de 10×5m formant un angle de 45°, orientation plein sud', 'Plan en U ouvert sur le sud, fond de 14×6m, ailes latérales de 4×8m', 'Volume octogonal de 10m de diamètre, un côté plat vers la rue', 'Corps principal rectangulaire 12×8m avec une véranda octogonale de 5m en façade sud', ]; document.getElementById('mpwm-ia-ex').addEventListener('click', () => { document.getElementById('mpwm-ia-prompt').value = examples[Math.floor(Math.random() * examples.length)]; }); document.getElementById('mpwm-ia-gen').addEventListener('click', async () => { const prompt = document.getElementById('mpwm-ia-prompt').value.trim(); if (!prompt) return; const btn = document.getElementById('mpwm-ia-gen'); const status = document.getElementById('mpwm-ai-status'); btn.disabled = true; status.innerHTML = 'Génération en cours'; const systemPrompt = `Tu es un générateur de formes architecturales pour un éditeur de plan 2D. À partir de la description utilisateur, génère un objet JSON valide (et UNIQUEMENT du JSON, sans markdown) avec cette structure : { "batiment": { "w": , "h": , "rot": }, "rooms": [ { "id": "r1", "nom": "Nom pièce", "x": <0-1>, "y": <0-1>, "w": <0-1>, "h": <0-1> } ], "label": "Description courte" } - x,y,w,h des rooms sont des fractions normalisées (0–1) du bâtiment. - Pour les formes complexes (L, T, U, V…), décompose en sous-volumes (rooms). - Pour les angles biaisés, utilise la rotation globale du batiment. - Si la description est trop vague, génère quand même un volume raisonnable avec une note dans label. - Réponds UNIQUEMENT avec le JSON, sans aucun texte autour.`; try { const res = await fetch(WORKER, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ model: 'claude-sonnet-4-20250514', max_tokens: 1000, system: systemPrompt, messages: [{ role: 'user', content: prompt }] }) }); const data = await res.json(); const raw = data.content?.[0]?.text || ''; const cleaned = raw.replace(/```json|```/g, '').trim(); const shape = JSON.parse(cleaned); // Centrer sur le canvas const cx = (typeof canvas !== 'undefined') ? canvas.width / 2 : 400; const cy = (typeof canvas !== 'undefined') ? canvas.height / 2 : 300; const scale = (typeof SCALE !== 'undefined') ? SCALE : 20; shape.batiment.x = cx - (shape.batiment.w * scale) / 2; shape.batiment.y = cy - (shape.batiment.h * scale) / 2; shape.batiment.w *= scale; shape.batiment.h *= scale; applyShape(shape); status.textContent = `✓ ${shape.label || 'Plan généré'}`; status.style.color = '#6EE7B7'; setTimeout(hide, 1200); } catch (err) { console.error('[MPWelcome IA]', err); status.textContent = '✗ Erreur — vérifiez votre connexion et réessayez.'; status.style.color = '#F87171'; btn.disabled = false; } }); } /* 3. Emprise PLU */ function plu(panel) { const d = (typeof MPC !== 'undefined') ? MPC.load() : {}; const rv = d.plu_rules?.rv_min ?? 5; const rl = d.plu_rules?.rl_min ?? 3; const surf = d.plu_rules?.parcelle_surface ?? 500; panel.innerHTML = `
Emprise maximale PLU

Calcul automatique du polygone constructible à partir de vos reculs enregistrés dans le dossier.

Recul avant (rv) m Recul latéral (rl) m Surface parcelle
`; function previewPLU() { const rvV = parseFloat(document.getElementById('mpwm-rv').value) || 5; const rlV = parseFloat(document.getElementById('mpwm-rl').value) || 3; const surfV = parseFloat(document.getElementById('mpwm-surf').value) || 500; const parcW = Math.sqrt(surfV * 1.5); const parcD = surfV / parcW; const bW = Math.max(4, parcW - 2 * rlV); const bD = Math.max(4, parcD - rvV - 3); document.getElementById('mpwm-plu-result').textContent = `→ Emprise calculée : ${bW.toFixed(1)} m × ${bD.toFixed(1)} m = ${(bW * bD).toFixed(0)} m²`; } ['mpwm-rv', 'mpwm-rl', 'mpwm-surf'].forEach(id => document.getElementById(id).addEventListener('input', previewPLU) ); previewPLU(); document.getElementById('mpwm-plu-apply').addEventListener('click', () => { const rvV = parseFloat(document.getElementById('mpwm-rv').value) || 5; const rlV = parseFloat(document.getElementById('mpwm-rl').value) || 3; const surfV = parseFloat(document.getElementById('mpwm-surf').value) || 500; if (typeof MPC !== 'undefined') { MPC.updatePLU({ rv_min: rvV, rl_min: rlV, parcelle_surface: surfV }); } const cx = (typeof canvas !== 'undefined') ? canvas.width / 2 : 400; const cy = (typeof canvas !== 'undefined') ? canvas.height / 2 : 300; const scale = (typeof SCALE !== 'undefined') ? SCALE : 20; const shape = computePLUEmprise(scale, cx, cy); // Recalcul avec valeurs du form const parcW = Math.sqrt(surfV * 1.5); const parcD = surfV / parcW; const bW = Math.max(4, parcW - 2 * rlV); const bD = Math.max(4, parcD - rvV - 3); shape.batiment = { x: cx - (bW * scale) / 2, y: cy - (bD * scale) / 2, w: bW * scale, h: bD * scale, rot: 0 }; shape.rooms = []; applyShape(shape); hide(); }); } /* 4. Gabarits paramétriques */ let _selectedShape = 'carre'; function gabarit(panel) { _selectedShape = 'carre'; panel.innerHTML = `
Gabarits paramétriques
`; document.querySelectorAll('.mpwm-shape-btn').forEach(btn => { btn.addEventListener('click', () => { document.querySelectorAll('.mpwm-shape-btn').forEach(b => b.classList.remove('--active')); btn.classList.add('--active'); _selectedShape = btn.dataset.shape; renderGabaritFields(); }); }); function renderGabaritFields() { const f = document.getElementById('mpwm-gab-fields'); const s = _selectedShape; const row = (label, id, val, unit='m') => ` ${label} ${unit} `; if (s === 'carre') { f.innerHTML = `
${row('Largeur (L)', 'g-w', 10)}${row('Profondeur (P)', 'g-d', 8)}
`; } else if (s === 'l') { f.innerHTML = `
${row('Largeur totale', 'g-w', 12)}${row('Profondeur totale', 'g-d', 10)}
${row('Largeur aile', 'g-aw', 6)}${row('Profondeur aile', 'g-ad', 5)}
`; } else if (s === 't') { f.innerHTML = `
${row('Largeur totale', 'g-w', 14)}${row('Profondeur totale', 'g-d', 10)}
${row('Largeur aile centrale', 'g-aw', 6)}${row('Profondeur aile', 'g-ad', 5)}
`; } else if (s === 'u') { f.innerHTML = `
${row('Largeur totale', 'g-w', 14)}${row('Profondeur totale', 'g-d', 12)}
${row('Largeur des ailes', 'g-aw', 4)}${row('Profondeur du fond', 'g-ad', 5)}
`; } f.querySelectorAll('input').forEach(i => i.addEventListener('input', updatePreview)); updatePreview(); } function updatePreview() { const val = id => parseFloat(document.getElementById(id)?.value || 0); const s = _selectedShape; let surf = 0; if (s === 'carre') { surf = val('g-w') * val('g-d'); } else if (s === 'l') { surf = val('g-w') * val('g-d') - (val('g-w') - val('g-aw')) * val('g-ad'); } else if (s === 't') { surf = val('g-w') * (val('g-d') - val('g-ad')) + val('g-aw') * val('g-ad'); } else if (s === 'u') { surf = val('g-w') * val('g-ad') + 2 * val('g-aw') * (val('g-d') - val('g-ad')); } document.getElementById('mpwm-gab-preview').textContent = `→ Surface estimée ≈ ${Math.max(0, surf).toFixed(0)} m²`; } renderGabaritFields(); document.getElementById('mpwm-gab-apply').addEventListener('click', () => { const val = id => parseFloat(document.getElementById(id)?.value || 0); const cx = (typeof canvas !== 'undefined') ? canvas.width / 2 : 400; const cy = (typeof canvas !== 'undefined') ? canvas.height / 2 : 300; const scale = (typeof SCALE !== 'undefined') ? SCALE : 20; const s = _selectedShape; let shape; if (s === 'carre') shape = shapeSquare(val('g-w'), val('g-d'), cx, cy, scale); else if (s === 'l') shape = shapeL(val('g-w'), val('g-d'), val('g-aw'), val('g-ad'), cx, cy, scale); else if (s === 't') shape = shapeT(val('g-w'), val('g-d'), val('g-aw'), val('g-ad'), cx, cy, scale); else if (s === 'u') shape = shapeU(val('g-w'), val('g-d'), val('g-aw'), val('g-ad'), cx, cy, scale); if (shape) { applyShape(shape); hide(); } }); } /* 5. Cadastre IGN */ function cadastre(panel) { panel.innerHTML = `
Importer un cadastre IGN/Apicarto
`; document.getElementById('mpwm-cad-load').addEventListener('click', () => { const addr = document.getElementById('mpwm-cad-addr').value.trim(); if (!addr) return; const status = document.getElementById('mpwm-cad-status'); status.textContent = 'Recherche en cours…'; // Brancher sur la fonction native de editeur-plan.html si disponible if (typeof chargerParcelle === 'function') { document.getElementById('inAdresse') && (document.getElementById('inAdresse').value = addr); chargerParcelle(addr); status.textContent = '✓ Chargement lancé — fermeture de la modale.'; setTimeout(hide, 1000); } else if (typeof chargerParcelleDemo === 'function') { chargerParcelleDemo(addr); status.textContent = '✓ Démo chargée.'; setTimeout(hide, 800); } else { // Fallback : copie l'adresse dans le champ de la page et ferme const inp = document.getElementById('inAdresse'); if (inp) { inp.value = addr; inp.dispatchEvent(new Event('input')); } status.textContent = '✓ Adresse transmise à l\'éditeur.'; setTimeout(hide, 800); } }); document.getElementById('mpwm-cad-addr').addEventListener('keydown', e => { if (e.key === 'Enter') document.getElementById('mpwm-cad-load').click(); }); } /* 6. Scanner croquis */ function scan(panel) { panel.innerHTML = `
Scanner un croquis à la main
`; const zone = document.getElementById('mpwm-scan-zone'); zone.addEventListener('dragover', e => { e.preventDefault(); zone.classList.add('--drag'); }); zone.addEventListener('dragleave', () => zone.classList.remove('--drag')); zone.addEventListener('drop', e => { e.preventDefault(); zone.classList.remove('--drag'); processImage(e.dataTransfer.files[0]); }); document.getElementById('mpwm-scan-file').addEventListener('change', e => processImage(e.target.files[0])); async function processImage(file) { if (!file) return; const status = document.getElementById('mpwm-scan-status'); status.innerHTML = 'Analyse IA du croquis'; const reader = new FileReader(); reader.onload = async (ev) => { const b64 = ev.target.result.split(',')[1]; const mime = file.type || 'image/jpeg'; try { const res = await fetch(WORKER, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ model: 'claude-sonnet-4-20250514', max_tokens: 1000, system: `Tu analyses un croquis architectural dessiné à la main. Retourne UNIQUEMENT un JSON (sans markdown) avec la structure : { "batiment": { "w": , "h": , "rot": 0 }, "rooms": [{ "id": "r1", "nom": "Pièce", "x": 0, "y": 0, "w": 1, "h": 1 }], "label": "Description détectée" } Si tu détectes plusieurs volumes (L, T, U…), décompose en rooms. Estime les dimensions en mètres selon les proportions visuelles (suppose une habitation standard de 8-15m).`, messages: [{ role: 'user', content: [ { type: 'image', source: { type: 'base64', media_type: mime, data: b64 } }, { type: 'text', text: 'Analyse ce croquis et génère le JSON de forme architecturale.' } ] }] }) }); const data = await res.json(); const raw = data.content?.[0]?.text || ''; const shape = JSON.parse(raw.replace(/```json|```/g, '').trim()); const cx = (typeof canvas !== 'undefined') ? canvas.width / 2 : 400; const cy = (typeof canvas !== 'undefined') ? canvas.height / 2 : 300; const scale = (typeof SCALE !== 'undefined') ? SCALE : 20; shape.batiment.x = cx - (shape.batiment.w * scale) / 2; shape.batiment.y = cy - (shape.batiment.h * scale) / 2; shape.batiment.w *= scale; shape.batiment.h *= scale; applyShape(shape); status.textContent = `✓ ${shape.label || 'Croquis vectorisé'}`; status.style.color = '#6EE7B7'; setTimeout(hide, 1200); } catch (err) { console.error('[MPWelcome scan]', err); status.textContent = '✗ Analyse échouée — réessayez avec une image plus nette.'; status.style.color = '#F87171'; } }; reader.readAsDataURL(file); } } /* ─────────────── SHOW / HIDE ─────────────── */ function show() { const existing = document.getElementById('mpwm-overlay'); if (existing) { existing.style.display = 'flex'; return; } inject(); } function hide() { const overlay = document.getElementById('mpwm-overlay'); if (!overlay) return; overlay.style.animation = 'mpwm-fadein .18s ease reverse'; setTimeout(() => overlay.remove(), 180); } /* ─────────────── AUTO-TRIGGER ─────────────── */ function shouldShow() { try { const raw = JSON.parse(localStorage.getItem('mpDossier') || '{}'); // Si un dossier est actif avec données (nom ou parcelle), ne pas afficher la modale // La bannière de reprise gère déjà ce cas dans assistant.html if (raw.savedAt && (raw.nom || raw.pRef)) return false; const bat = raw.geometry_state?.batiment; // Vide si w=0 ou absent return !bat || (!bat.w && !bat.d); } catch { return true; } } function autoInit() { if (shouldShow()) show(); } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', autoInit); } else { // Léger délai pour laisser la page finir son propre init setTimeout(autoInit, 400); } /* ─────────────── API PUBLIQUE ─────────────── */ window.MPWelcome = { show, hide }; })();