Reorganise le formulaire de run selon les roles Ceil/Ground de CloudCompare

Regroupe chaque nuage avec son role dans le calcul de volume (Ceil = surface
superieure, Ground = plan Z constant ou second nuage), cote a cote comme dans
la boite de dialogue "Compute Volume" du GUI, au lieu d'un unique bloc de
parametres partages en dessous des deux fichiers. La hauteur de reference Z
(mode plan constant) est deplacee dans le panneau Ground plutot que noyee dans
les parametres avances.

Garde les reglages de grille/remplissage (grid step, empty fill, cell height,
direction) dans une section UNIQUE et partagee : cote CLI CloudCompare
(-PROJ/-EMPTY_FILL/-VERT_DIR), ce sont des reglages appliques a toute la
grille, pas un parametre par nuage - les dupliquer par role induirait en
erreur sur ce que le backend supporte reellement.

Ajoute un tooltip (title) sur chaque champ expliquant sa correspondance avec
le flag CLI CloudCompare concerne, et des labels reprenant la terminologie
du GUI (Ceil/Ground, Fill empty cells with, Cell height...).

Verifie que toutes les options exposees correspondent a des flags CLI
CloudCompare reellement supportes par le backend (aucune option inventee).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Nicolas Fryder 2026-07-10 18:50:25 +02:00
parent 9d6ebf5177
commit dfba8e7897

View file

@ -110,101 +110,124 @@ async function deleteRun(id) {
}
// ---------------- Nouveau run ----------------
// Layout inspire de la boite de dialogue "Compute Volume" de CloudCompare : les nuages sont
// presentes selon leurs 2 roles dans le calcul (Ceil = surface superieure, Ground = reference
// inferieure - un plan Z constant ou un second nuage), cote a cote comme dans le GUI CC. A la
// difference de CC (qui liste les nuages deja charges dans des combobox), on utilise des boutons
// de selection de fichier puisque l'app fonctionne par upload. Les parametres de grille/
// remplissage restent dans UNE section partagee : cote CLI CloudCompare, -PROJ/-EMPTY_FILL/
// -VERT_DIR sont des reglages uniques appliques a toute la grille (pas un reglage par nuage,
// verifie dans les sources - voir CLAUDE.md), donc les dupliquer par role induirait en erreur.
function renderNew() {
const tip = (text) => `title="${text.replace(/"/g, '&quot;')}"`;
app.innerHTML = `
<h1 class="text-xl font-semibold mb-6">Nouveau run</h1>
<form id="upload-form" class="space-y-5 max-w-xl">
<form id="upload-form" class="space-y-5 max-w-2xl">
<div>
<label class="block text-sm font-medium mb-2">Type de comparaison</label>
<div class="flex gap-4 text-sm">
<label class="flex items-center gap-2 cursor-pointer">
<div class="flex flex-col sm:flex-row gap-3 sm:gap-4 text-sm">
<label class="flex items-center gap-2 cursor-pointer" ${tip('Compare le nuage a un plan horizontal (Z constant). CloudCompare : -CONST_HEIGHT. Ne donne un volume correct QUE si le nuage ne contient que lobjet mesure, sans terrain environnant.')}>
<input type="radio" name="mode" value="const_height" checked />
Plan de reference Z constant
</label>
<label class="flex items-center gap-2 cursor-pointer">
<label class="flex items-center gap-2 cursor-pointer" ${tip('Compare 2 nuages entre eux (ex: surface dun amas vs son socle/plan dajustement). Les densites sont appariees avant de commencer la decimation.')}>
<input type="radio" name="mode" value="cloud_compare" />
Comparaison a un second nuage
</label>
</div>
</div>
<div id="mode-const-height">
<label class="block text-sm font-medium mb-1">Nuage de points (.las, .laz, .copc.laz, .bin)</label>
<div id="mode-const-height" class="border border-slate-800 rounded-lg overflow-hidden">
<div class="grid sm:grid-cols-2 divide-y sm:divide-y-0 divide-slate-800 sm:divide-x">
<div class="p-4">
<p class="text-[11px] uppercase tracking-wide text-slate-500 mb-1">Ceil <span class="text-slate-600 normal-case">(surface superieure)</span></p>
<label class="block text-sm font-medium mb-1" ${tip('Nuage charge par -O et compare au plan Ground. Cest ce nuage, jamais decime, qui constitue le niveau 0.')}>Nuage de points</label>
<input type="file" name="file" accept=".las,.laz,.bin"
class="block w-full text-sm text-slate-300 border border-slate-700 rounded-lg cursor-pointer bg-slate-900 focus:outline-none p-2" />
<p class="text-xs text-slate-500 mt-1">Le volume est calcule entre le nuage et un plan horizontal (altitude min par defaut).</p>
<p class="text-xs text-slate-500 mt-1">.las, .laz, .copc.laz ou .bin</p>
</div>
<div class="p-4">
<p class="text-[11px] uppercase tracking-wide text-slate-500 mb-1">Ground <span class="text-slate-600 normal-case">(plan de reference)</span></p>
<label class="block text-sm font-medium mb-1" ${tip('Hauteur (m) du plan Z constant utilise comme Ground (CloudCompare : -CONST_HEIGHT). Auto = 1er percentile des altitudes du nuage : plus robuste quun minimum brut, insensible a un point aberrant isole.')}>Hauteur constante (m)</label>
<input type="number" step="any" name="zrefOverride" placeholder="auto = 1er percentile de Z"
class="w-full bg-slate-900 border border-slate-700 rounded-lg p-2 text-sm" />
</div>
</div>
</div>
<div id="mode-cloud-compare" class="hidden space-y-4">
<div>
<label class="block text-sm font-medium mb-1">Surface superieure (sommet de l'amas)</label>
<div id="mode-cloud-compare" class="hidden border border-slate-800 rounded-lg overflow-hidden">
<div class="grid sm:grid-cols-2 divide-y sm:divide-y-0 divide-slate-800 sm:divide-x">
<div class="p-4">
<p class="text-[11px] uppercase tracking-wide text-slate-500 mb-1">Ceil <span class="text-slate-600 normal-case">(surface superieure)</span></p>
<label class="block text-sm font-medium mb-1" ${tip('Nuage charge en premier -> role Ceil dans le calcul de volume CloudCompare (-VOLUME).')}>Sommet de l'amas</label>
<input type="file" name="fileTop" accept=".las,.laz,.bin"
class="block w-full text-sm text-slate-300 border border-slate-700 rounded-lg cursor-pointer bg-slate-900 focus:outline-none p-2" />
<p class="text-xs text-slate-500 mt-1">.las, .laz, .copc.laz ou .bin</p>
</div>
<div>
<label class="block text-sm font-medium mb-1">Limite inferieure (base / socle de l'amas)</label>
<div class="p-4">
<p class="text-[11px] uppercase tracking-wide text-slate-500 mb-1">Ground <span class="text-slate-600 normal-case">(limite inferieure)</span></p>
<label class="block text-sm font-medium mb-1" ${tip('Nuage charge en second -> role Ground dans le calcul de volume CloudCompare (-VOLUME). Base, socle ou plan dajustement sous lamas.')}>Base / socle de l'amas</label>
<input type="file" name="fileBottom" accept=".las,.laz,.bin"
class="block w-full text-sm text-slate-300 border border-slate-700 rounded-lg cursor-pointer bg-slate-900 focus:outline-none p-2" />
<p class="text-xs text-slate-500 mt-1">.las, .laz, .copc.laz ou .bin</p>
</div>
<p class="text-xs text-slate-500">Les densites des deux nuages sont d'abord appariees (le plus dense est decime pour matcher l'autre), puis les deux sont decimes ensemble a chaque etape.</p>
</div>
<p class="text-xs text-slate-500 p-3 border-t border-slate-800">Les densites des deux nuages sont d'abord appariees (le plus dense est decime pour matcher l'autre), puis les deux sont decimes ensemble a chaque etape.</p>
</div>
<details class="border border-slate-800 rounded-lg p-3" open>
<summary class="cursor-pointer text-sm text-slate-400">Parametres CloudCompare (mirroir de la boite "Compute Volume")</summary>
<summary class="cursor-pointer text-sm text-slate-400">Grille &amp; remplissage (mirroir de la boite "Compute Volume" de CloudCompare)</summary>
<p class="text-xs text-slate-500 mt-2">Ces reglages s'appliquent a la grille de calcul entiere, identiquement pour Ceil et Ground (CloudCompare n'a pas de reglage separe par nuage pour la grille).</p>
<div class="mt-3 space-y-4">
<div id="zref-override-wrap">
<label class="block text-sm mb-1">Plan de reference Z / hauteur par defaut (override, mode Z constant uniquement)</label>
<input type="number" step="any" name="zrefOverride" placeholder="auto = Zmin de l'echantillon"
class="w-full bg-slate-900 border border-slate-700 rounded-lg p-2 text-sm" />
</div>
<div>
<label class="block text-sm mb-1">Pas spatial initial en metres (override)</label>
<label class="block text-sm mb-1" ${tip('Distance minimale entre points au niveau 0 (avant toute decimation). Auto = distance mediane au plus proche voisin (mode Z constant) ou la plus grande des 2 densites Ceil/Ground (mode comparaison, pour aligner les nuages).')}>Pas spatial initial (m)</label>
<input type="number" step="any" name="initialStepOverride" placeholder="auto = distance mediane au plus proche voisin"
class="w-full bg-slate-900 border border-slate-700 rounded-lg p-2 text-sm" />
</div>
<div>
<label class="block text-sm mb-1">Multiplicateur de grille (grid step = pas spatial x ce nombre)</label>
<label class="block text-sm mb-1" ${tip('Cell size de la grille de calcul (CloudCompare : -GRID_STEP) = pas spatial du niveau x ce nombre. Recalcule a chaque niveau pour rester adapte a la densite du nuage decime.')}>Multiplicateur de grille (grid step)</label>
<input type="number" step="any" min="0.1" name="gridStepMultiplierOverride" placeholder="auto = 2"
class="w-full bg-slate-900 border border-slate-700 rounded-lg p-2 text-sm" />
</div>
<div>
<label class="block text-sm mb-1">Cellules vides (Empty cells)</label>
<label class="block text-sm mb-1" ${tip('Strategie de comblement des cellules sans point avant le calcul de volume (CloudCompare : -EMPTY_FILL). -VOLUME na pas cette option nativement : le nuage est dabord rasterise (-RASTERIZE) puis comble.')}>Remplissage des cellules vides (Fill empty cells with)</label>
<select name="emptyCellStrategy" class="w-full bg-slate-900 border border-slate-700 rounded-lg p-2 text-sm">
<option value="interpolate" selected>Interpoler (Delaunay, borne)</option>
<option value="leave_empty">Laisser vide</option>
<option value="min_height">Hauteur minimum</option>
<option value="max_height">Hauteur maximum</option>
<option value="custom_height">Hauteur personnalisee</option>
<option value="kriging">Krigeage</option>
<option value="interpolate" selected>Interpolation Delaunay, bornee (INTERP)</option>
<option value="leave_empty">Laisser vide (LEAVE_EMPTY)</option>
<option value="min_height">Hauteur minimum des voisins (MIN_H)</option>
<option value="max_height">Hauteur maximum des voisins (MAX_H)</option>
<option value="custom_height">Hauteur personnalisee (CUSTOM_H)</option>
<option value="kriging">Krigeage (KRIGING)</option>
</select>
</div>
<div id="max-edge-wrap">
<label class="block text-sm mb-1">Distance max de remplissage / Delaunay (m)</label>
<input type="number" step="any" name="maxEdgeLengthOverride" placeholder="auto = grille du niveau x3"
<label class="block text-sm mb-1" ${tip('Longueur darete maximale de linterpolation Delaunay (CloudCompare : -MAX_EDGE_LENGTH). Bornee pour ne pas combler les concavites reelles du contour de lamas.')}>Distance max de remplissage / Delaunay (m)</label>
<input type="number" step="any" name="maxEdgeLengthOverride" placeholder="auto = grille du niveau 0 x3, fixe sur tous les niveaux"
class="w-full bg-slate-900 border border-slate-700 rounded-lg p-2 text-sm" />
<p class="text-xs text-slate-500 mt-1">Trop grand = comble aussi les creux du contour de l'amas (a eviter). Trop petit = les trous de scan restent vides.</p>
</div>
<div id="custom-height-wrap" class="hidden">
<label class="block text-sm mb-1">Hauteur personnalisee (m)</label>
<label class="block text-sm mb-1" ${tip('Valeur (m) utilisee pour combler les cellules vides (CloudCompare : -CUSTOM_HEIGHT).')}>Hauteur personnalisee (m)</label>
<input type="number" step="any" name="customHeightValue" placeholder="ex: 0"
class="w-full bg-slate-900 border border-slate-700 rounded-lg p-2 text-sm" />
</div>
<div id="kriging-knn-wrap" class="hidden">
<label class="block text-sm mb-1">Krigeage : nombre de voisins (KNN)</label>
<label class="block text-sm mb-1" ${tip('Nombre de plus proches voisins utilises par le krigeage (CloudCompare : -KRIGING_KNN).')}>Krigeage : nombre de voisins (KNN)</label>
<input type="number" step="1" min="1" name="krigingKnn" placeholder="auto = 8"
class="w-full bg-slate-900 border border-slate-700 rounded-lg p-2 text-sm" />
</div>
<div>
<label class="block text-sm mb-1">Hauteur de cellule (projection)</label>
<label class="block text-sm mb-1" ${tip('Comment la hauteur dune cellule est calculee a partir des points quelle contient (CloudCompare : -PROJ, "Cell height" dans le GUI).')}>Hauteur de cellule (projection)</label>
<select name="projectionType" class="w-full bg-slate-900 border border-slate-700 rounded-lg p-2 text-sm">
<option value="avg" selected>Hauteur moyenne</option>
<option value="min">Hauteur minimum</option>
<option value="max">Hauteur maximum</option>
<option value="med">Hauteur mediane</option>
<option value="avg" selected>Hauteur moyenne (AVG)</option>
<option value="min">Hauteur minimum (MIN)</option>
<option value="max">Hauteur maximum (MAX)</option>
<option value="med">Hauteur mediane (MED)</option>
</select>
</div>
<div>
<label class="block text-sm mb-1">Direction de projection</label>
<label class="block text-sm mb-1" ${tip('Axe le long duquel la grille 2.5D est projetee (CloudCompare : -VERT_DIR). Z par defaut = vue de dessus.')}>Direction de projection</label>
<select name="vertDir" class="w-full bg-slate-900 border border-slate-700 rounded-lg p-2 text-sm">
<option value="2" selected>Z</option>
<option value="0">X</option>
@ -220,7 +243,6 @@ function renderNew() {
const form = document.getElementById('upload-form');
const constDiv = document.getElementById('mode-const-height');
const compareDiv = document.getElementById('mode-cloud-compare');
const zrefWrap = document.getElementById('zref-override-wrap');
const constFileInput = form.querySelector('input[name=file]');
const topFileInput = form.querySelector('input[name=fileTop]');
const bottomFileInput = form.querySelector('input[name=fileBottom]');
@ -234,7 +256,6 @@ function renderNew() {
const isCompare = form.mode.value === 'cloud_compare';
constDiv.classList.toggle('hidden', isCompare);
compareDiv.classList.toggle('hidden', !isCompare);
zrefWrap.classList.toggle('hidden', isCompare);
constFileInput.required = !isCompare;
topFileInput.required = isCompare;
bottomFileInput.required = isCompare;