/**
* 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 `
Plan vide
Canevas blanc prêt à dessiner — liberté totale.
Configuration libre par IA
Décrivez votre projet (villa en U, aile biaisée, octogone…) — Claude génère le polygone.
Emprise maximale PLU
Calcule automatiquement le polygone constructible optimal depuis vos reculs (rv, rl).
Gabarits paramétriques
Carré, L, T, U — avec vos propres dimensions saisies avant génération.
Importer un cadastre
Interrogation automatique IGN/Apicarto — saisissez l'adresse pour récupérer la parcelle.
Scanner un croquis main
Uploadez une photo de votre esquisse — l'IA la vectorise en lignes 2D.
`;
}
/* ─────────────── 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
m²
`;
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 };
})();