From d8470682883fc316a5775da21f1655f40fe6a248 Mon Sep 17 00:00:00 2001 From: Antoine Jacquin Date: Sun, 13 Sep 2026 23:17:11 +0200 Subject: [PATCH] =?UTF-8?q?Ajouter=20des=20pr=C3=A9configurations=20de=20c?= =?UTF-8?q?ouches=20(couches,=20transparences,=20ordre)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit L'onglet Couches propose un sélecteur de presets : cinq presets intégrés (Relief, Pentes, Cavités, Rugosité, Tout) définissent les couches visibles, leurs transparences et l'ordre de pile. 💾 enregistre l'état actuel comme preset personnel (persisté en localStorage), ✕ le supprime depuis la liste « Mes presets ». --- lidar_pipeline/index.py | 126 +++++++++++++++++++++++++++++ lidar_pipeline/tests/test_index.py | 7 ++ 2 files changed, 133 insertions(+) diff --git a/lidar_pipeline/index.py b/lidar_pipeline/index.py index e462e36..f5acf9e 100644 --- a/lidar_pipeline/index.py +++ b/lidar_pipeline/index.py @@ -1235,6 +1235,13 @@ _HTML_TEMPLATE = """ +
+ + + +
@@ -1536,6 +1543,12 @@ select, input[type="checkbox"] { accent-color: var(--accent); } } .layer-sub-actions { display: inline-flex; align-items: center; gap: 4px; } .layer-sub-actions .icon-btn { font-size: 10.5px; padding: 1px 6px; } +.preset-row { + display: flex; align-items: center; gap: 4px; + padding: 4px 6px; +} +.preset-row .gen-select { flex: 1; min-width: 0; } +.preset-row .icon-btn { font-size: 10.5px; padding: 1px 6px; } /* Sliders d'opacité : piste fine dans une zone cliquable haute de 18 px — le pouce reste saisissable au drag (l'élément lui-même fait toute la @@ -2228,6 +2241,119 @@ document.getElementById('osmRow').querySelector('.layer-name') document.getElementById('osmRow').querySelector('.layer-name').title = 'Cliquer : fond de carte clair / assombri'; +// ============================================================ +// Préconfigurations de couches : un preset définit les couches +// visibles, leurs transparences et l'ordre de pile (bas → haut). +// Les presets intégrés couvrent les couches du panneau ; les presets +// personnels (couches + transparences + ordre) sont persistés en +// localStorage et se suppriment depuis la liste. +// ============================================================ +const PRESETS_LS = 'lidarPresets_v1'; +const BUILTIN_PRESETS = [ + { label: 'Relief', on: ['aspect', 'positive_openness'], + opacity: { aspect: 1, positive_openness: 0.65 } }, + { label: 'Pentes', on: ['aspect', 'slope'], + opacity: { aspect: 0.5, slope: 1 } }, + { label: 'Cavités', on: ['slope', 'positive_openness'], + opacity: { slope: 0.5, positive_openness: 1 } }, + { label: 'Rugosité', on: ['roughness'], opacity: {} }, + { label: 'Tout', on: ['slope', 'aspect', 'positive_openness', 'roughness'], + opacity: { slope: 0.8, aspect: 0.8, positive_openness: 0.55, roughness: 0.8 } }, +]; + +function loadCustomPresets() { + try { + const p = JSON.parse(localStorage.getItem(PRESETS_LS) || '[]'); + return Array.isArray(p) ? p : []; + } catch (e) { return []; } +} +function saveCustomPresets(list) { + try { localStorage.setItem(PRESETS_LS, JSON.stringify(list)); } catch (e) { /* privé */ } +} + +function applyPreset(p) { + const want = (p.on || []).filter(k => VIZ_META[k]); + const L = state.layers; + const off = L.order.filter(k => !want.includes(k)); + L.order = want.slice(); // ordre du preset (bas → haut) + L.order.push(...off); // les autres gardent leur ordre relatif + for (const k of L.order) { + const on = want.includes(k); + L.on[k] = on; + if (on) L.opacity[k] = (p.opacity && k in p.opacity) ? Number(p.opacity[k]) : 1; + } + saveLayers(); + renderLayerPanel(); + syncTileLayers(); +} + +const presetSelectEl = document.getElementById('presetSelect'); +const presetSaveEl = document.getElementById('presetSave'); +const presetDeleteEl = document.getElementById('presetDelete'); + +function renderPresetSelect(selectedValue) { + presetSelectEl.innerHTML = ''; + const none = document.createElement('option'); + none.value = ''; + none.textContent = 'Préconfigurations…'; + presetSelectEl.appendChild(none); + for (const p of BUILTIN_PRESETS) { + const o = document.createElement('option'); + o.value = 'builtin:' + p.label; + o.textContent = p.label; + presetSelectEl.appendChild(o); + } + const customs = loadCustomPresets(); + if (customs.length) { + const g = document.createElement('optgroup'); + g.label = 'Mes presets'; + for (const p of customs) { + const o = document.createElement('option'); + o.value = 'custom:' + p.id; + o.textContent = p.label; + g.appendChild(o); + } + presetSelectEl.appendChild(g); + } + if (selectedValue) presetSelectEl.value = selectedValue; + presetDeleteEl.hidden = !presetSelectEl.value.startsWith('custom:'); +} + +presetSelectEl.addEventListener('change', () => { + const v = presetSelectEl.value; + if (!v) return; + let p = null; + if (v.startsWith('builtin:')) p = BUILTIN_PRESETS.find(x => x.label === v.slice(8)); + else if (v.startsWith('custom:')) p = loadCustomPresets().find(x => x.id === v.slice(7)); + if (p) applyPreset(p); +}); + +presetSaveEl.addEventListener('click', () => { + const name = prompt('Nom du preset :'); + if (!name || !name.trim()) return; + const enabled = state.layers.order.filter(k => state.layers.on[k]); + const p = { + id: Date.now().toString(36), + label: name.trim(), + on: enabled, + opacity: Object.fromEntries(enabled.map(k => [k, state.layers.opacity[k]])), + }; + const customs = loadCustomPresets(); + customs.push(p); + saveCustomPresets(customs); + renderPresetSelect('custom:' + p.id); +}); + +presetDeleteEl.addEventListener('click', () => { + const v = presetSelectEl.value; + if (!v.startsWith('custom:')) return; + if (!confirm('Supprimer ce preset ?')) return; + saveCustomPresets(loadCustomPresets().filter(p => p.id !== v.slice(7))); + renderPresetSelect(''); +}); + +renderPresetSelect(); + // Barre latérale : repli + onglets (Couches / Génération) const sidebar = document.getElementById('sidebar'); function setSidebarCollapsed(collapsed) { diff --git a/lidar_pipeline/tests/test_index.py b/lidar_pipeline/tests/test_index.py index 208ccd6..c4d4f48 100644 --- a/lidar_pipeline/tests/test_index.py +++ b/lidar_pipeline/tests/test_index.py @@ -274,6 +274,13 @@ def test_build_index_generates_html(tmp_path): # le ↻ resynchronise puis reconstruit la carte (mode webapp distante). assert 'id="layerRefresh"' in content assert "/api/layers" in app_js and "/api/sync" in app_js + # Préconfigurations de couches (onglet Couches) : le sélecteur applique + # des presets intégrés (couches visibles, transparences, ordre de pile) + # et 💾/✕ gèrent les presets personnels persistés en localStorage. + assert 'id="presetSelect"' in content + assert 'id="presetSave"' in content + assert "BUILTIN_PRESETS" in app_js and "applyPreset" in app_js + assert "lidarPresets_v1" in app_js # Partage de la vue : bouton « Partager » + lien contenant vue, couches # et transparences (hash #zoom,lat,lng&L=...&O=...) — le destinataire # reçoit l'état exact, indépendamment de son localStorage local.