script-volume-auto/CLAUDE.md
Nicolas Fryder 79daa30a8a Implementation initiale : calcul de volume 2.5D CloudCompare et test de robustesse par decimation spatiale progressive
- Backend NestJS orchestrant CloudCompare CLI (stats, decimation x2 sur 5 etapes, volume, assemblage .bin)
- Scripts Python (laspy, scipy, matplotlib) pour la conversion LAS/LAZ et la generation d'images
- Frontend web simple (Tailwind CDN) avec historique des runs
- Image Docker (debian:trixie-slim + cloudcompare apt + xvfb) prete pour deploiement Coolify
- Lint/typecheck/tests unitaires integres comme portes de qualite au build Docker
- CLAUDE.md documentant les comportements CloudCompare CLI verifies empiriquement
2026-07-10 13:55:25 +02:00

170 lines
11 KiB
Markdown

# CLAUDE.md
Notes pour toute future session travaillant sur ce repo. Ce fichier documente des faits
verifies empiriquement sur CloudCompare 2.13.2 (Windows officiel ET paquet apt Debian trixie) —
pas de la documentation officielle recopiee, mais des comportements observes directement, souvent
non documentes ou contredisant la doc/le wiki.
## Contexte du projet
Pipeline : nuage de points (LAS/LAZ/COPC.laz/BIN) -> calcul de volume 2.5D CloudCompare -> decimation
spatiale progressive x2 sur 5 etapes (chainee, chaque niveau decime depuis le precedent, pas depuis
l'original) -> volume recalcule a chaque niveau -> assemblage des 6 nuages dans un seul `.bin` ->
rapport CSV/JSON + images. UI web (NestJS + Tailwind CDN) avec historique des runs (SQLite).
E57 volontairement hors perimetre.
## CloudCompare CLI — comportements verifies
### `-VOLUME` (calcul de volume 2.5D)
- Sur un nuage **unique**, utiliser `-CONST_HEIGHT <valeur>` comme plan de reference (le nuage charge
est le "ceiling", le plan constant est le "ground"). Sans `-CONST_HEIGHT`, la commande attend DEUX
nuages charges.
- Syntaxe complete : `-VOLUME -GRID_STEP <val> [-VERT_DIR 0/1/2] [-CONST_HEIGHT <val>] [-GROUND_IS_FIRST] [-OUTPUT_MESH]`.
- **Piege majeur non documente** : le rapport texte `VolumeCalculationReport_<date>.txt` n'est genere
QUE si `-AUTO_SAVE ON` (le defaut). Avec `-AUTO_SAVE OFF` (qu'on utilise partout ailleurs pour
eviter de polluer le dossier de travail avec des fichiers intermediaires), la commande `-VOLUME`
s'execute (log "[2.5D VOLUME CALCULATION] finished") mais **aucun rapport n'est ecrit, aucune
erreur n'est loggee**. Il faut explicitement repasser `-AUTO_SAVE ON` juste pour l'appel `-VOLUME`.
Voir `CcRunnerService.run(args, cwd, { autoSave: true })`.
- Avec `-AUTO_SAVE ON`, `-VOLUME` sauvegarde AUSSI automatiquement une grille de difference de hauteur
nommee `<nom>_HEIGHT_DIFFERENCE_<date>.bin` qu'on ne veut pas garder — on la detecte (meme technique
que pour le rapport, cf ci-dessous) et on la supprime juste apres (voir pipeline.service.ts).
- **CloudCompare ecrit ses fichiers de sortie automatiques (rapport, grilles) relativement au dossier
du fichier charge via `-O`, pas au `cwd` du process.** Si on charge `-O levels/L0_full.bin`, le
rapport atterrit dans `levels/VolumeCalculationReport_*.txt`, pas a la racine du `cwd`. D'ou la
necessite d'une recherche **recursive** (`CcRunnerService.findLatestFile`) plutot qu'un simple
`readdir` du dossier de travail.
- Format du rapport texte (verifie, stable) — cle:valeur, une ligne par info, parsable par regex :
```
Volume: 7.834176
Surface: 100.774998
----------------------
Added volume: (+)7.975798
Removed volume: (-)0.141622
----------------------
Matching cells: 99.8%
Non-matching cells:
ground = 0.2%
ceil = 0.0%
Average neighbors per cell: 8.0 / 8.0
```
- **`Matching cells: X%`** est le meilleur signal natif de robustesse : c'est le % de cellules de la
grille de calcul ou les deux surfaces sont effectivement comparables. Il s'effondre des que la
decimation produit un nuage plus clairseme que la resolution de grille utilisee pour le calcul de
volume — exactement l'indicateur cherche pour ce projet (voir `matchingCellsWarnThreshold` dans
`config.ts`, defaut 90%).
- Le `-GRID_STEP` du calcul de volume doit rester **fixe** (le meme pour les 6 niveaux) pour que les
volumes restent comparables entre eux — sinon on mele l'effet de la decimation avec l'effet d'un
changement de resolution de calcul. On le derive une seule fois du pas spatial initial (voir plus
bas) et on le reutilise identique partout.
### `-SS SPATIAL` / `-SS RANDOM` (sous-echantillonnage)
- `-SS SPATIAL <distance>` : distance minimale entre points, PAS un ratio de points cible. Le nombre
de points obtenu depend de la densite reelle du nuage — jamais garanti a un pourcentage exact.
- `-SS RANDOM <n>` : sous-echantillonne a `n` points. Si `n` >= nombre de points du nuage, ne plante
pas (comportement observe : garde le nuage tel quel).
- Log de sortie a parser : `[SUBSAMPLE]` puis `Result: <N> points` (regex utilisee :
`/\[SUBSAMPLE\][\s\S]*?Result:\s*(\d+)\s*points/`).
- `-AUTO_SAVE OFF` fonctionne correctement ici (contrairement a `-VOLUME`) : pas de fichier auto-sauve
parasite, seul `-SAVE_CLOUDS FILE "nom.bin"` explicite produit un fichier, avec le nom voulu.
### `-SAVE_CLOUDS`
- `-SAVE_CLOUDS FILE "nom.ext"` : nom de sortie explicite (fonctionne, contrairement a ce que
certains threads du forum CloudCompare laissent penser).
- `-SAVE_CLOUDS ALL_AT_ONCE FILE "nom.bin"` avec **plusieurs nuages charges** (plusieurs `-O`) sans
`-MERGE_CLOUDS` : sauvegarde tous les nuages comme entites **distinctes** dans un seul `.bin` (le
format `.bin` de CloudCompare supporte nativement une hierarchie de plusieurs nuages). Verifie en
rouvrant le fichier : chaque nuage retrouve son point count exact, sans fusion de geometrie. C'est
la technique utilisee pour assembler les 6 niveaux de decimation dans un seul fichier de sortie.
**`-MERGE_CLOUDS` fusionnerait la geometrie en un seul nuage — a ne PAS utiliser ici.**
- Pas de commande `-RENAME_CLOUDS` (n'existe pas, testee = "Unknown or misplaced command"). Le nom de
chaque nuage dans le `.bin` fusionne est derive du nom de fichier source charge — on nomme donc les
fichiers intermediaires de facon descriptive (`L0_full.bin`, `L1_step0.05.bin`, ...) plutot que de
chercher a renommer les entites apres coup.
### Autres commandes
- `-RASTERIZE -GRID_STEP <val> -OUTPUT_RASTER_Z` : export GeoTIFF. **Fonctionne sur le build Windows
officiel, mais PLANTE (assertion `false` dans `ccRasterizeTool.cpp:ExportGeoTiff`, core dump) sur le
paquet apt Debian trixie**, faute de support GDAL compile dans ce paquet. Ne pas utiliser cette
voie dans l'image Docker — voir section "Paquet apt Debian" ci-dessous pour la solution retenue.
- Les commandes s'appliquent comme une **machine a etats sequentielle sur UN SEUL process** : on ne
peut pas "reprendre" une session precedente. Chaque etape logique du pipeline (stats, conversion,
decimation, volume, export) = un appel CLI independant, avec son propre `-O` de rechargement.
- `-SILENT` doit etre le tout premier argument (ou juste apres `-VERBOSITY`).
- `-PREC <n>` controle la precision decimale des exports ASCII (`-C_EXPORT_FMT ASC`).
## Paquet apt Debian `cloudcompare` (trixie, 2.13.2) — limitations et contournements
Choix d'archi : `debian:trixie-slim` + `apt-get install cloudcompare` plutot qu'une compilation depuis
les sources (des heures de build, fragile). Debian trixie est la seule distro testee avec une version
recente (2.13.2) directement en apt `main` (Ubuntu jammy/noble n'ont que 2.11.3 en `universe`).
Mais ce paquet est **allege par rapport au build officiel Windows** :
1. **Pas de plugin LAS/LAZ** (`dpkg -L cloudcompare` ne montre que
`libQCORE_IO_PLUGIN.so`, aucun `QLAS_IO_PLUGIN`). Ouvrir un `.las`/`.laz` donne :
`[Load] Can't guess file format: unhandled file extension 'las'`.
**Solution retenue** : conversion LAS/LAZ/COPC -> ASCII XYZ via `laspy` (Python, `python/las_to_xyz.py`)
*avant* tout traitement CloudCompare. L'import ASCII XYZ, lui, fonctionne nativement sans aucun
plugin (verifie : `CloudCompare -O fichier.xyz` marche directement sur ce paquet). Un `.copc.laz`
est lu par laspy comme un LAZ standard (l'indexation octree COPC est ignoree, seuls les points
comptent ici).
2. **Export GeoTIFF cassé** (voir `-RASTERIZE` ci-dessus). **Solution retenue** : les cartes de hauteur
sont generees nous-memes (`python/render_images.py heightmap`) via export ASCII du nuage +
binning `scipy.stats.binned_statistic_2d` + rendu `matplotlib`, sans passer par le rasterizer
CloudCompare.
3. **CloudCompare reste une appli Qt, meme en `-SILENT`** : plante avec `QXcbConnection: Could not
connect to display` sans serveur X. Necessite `xvfb`. On demarre UN Xvfb persistant dans
`docker/entrypoint.sh` (pas un `xvfb-run` par appel CLI, plus rapide et evite les conflits de
lockfile entre appels concurrents).
4. Si un futur besoin necessite E57 : le meme probleme de plugin manquant se posera probablement
(a verifier — `QE57_IO_PLUGIN` n'apparaissait pas non plus dans `dpkg -L cloudcompare`).
**Si CloudCompare est mis a jour dans une future version du paquet Debian et regagne GDAL/LAS**, ces
contournements resteront fonctionnels sans rien casser (ils n'utilisent jamais les fonctionnalites
manquantes), mais pourraient etre simplifies.
## Pieges d'implementation generaux (au-dela de CloudCompare)
- **Chemins `/c/...` (style MSYS/git-bash) vs chemins Windows natifs** : un `python.exe` natif Windows
ne comprend PAS `/c/Users/...` — seulement `C:/Users/...` ou `C:\Users\...`. `curl`/`cat`/`ls`
(MSYS) acceptent les deux, mais un process Windows natif lance depuis bash avec un chemin `/c/...`
en argument echouera silencieusement (`FileNotFoundError`) si ce chemin est passe tel quel a une
fonction Python (`open(...)`) plutot que d'etre resolu par le shell. Toujours utiliser des chemins
style `C:/...` quand on passe un chemin en argument a un outil natif Windows depuis bash.
- **TypeScript + closures + `let` mutable** : `tsc` peut perdre le narrowing d'une variable `let`
mutee dans une closure asynchrone appelee avant un `return` conditionnel (erreur `Property 'x' does
not exist on type 'never'`). Contournement : accumuler dans un tableau (`push`) plutot que de muter
une variable `best` capturee, puis trier/reduire a la fin.
- **Nest CLI + fichiers de test** : `nest build` doit utiliser `tsconfig.build.json` (qui exclut
`**/*.spec.ts`) pour ne pas emettre les tests dans `dist/`. Le `tsconfig.json` de base (sans
exclude) reste utilise par `tsc --noEmit` (script `typecheck`) et par `ts-jest`, pour que les tests
soient bien type-checkes.
- **`better-sqlite3`** necessite des outils de build natifs (python3/make/g++) — presents par defaut
dans l'image `node:20-bookworm` utilisee pour le stage de build, absents volontairement du stage
final (juste `node_modules` deja compile est copie).
## Deploiement (Coolify)
- Image construite en deux etapes : build (Node, avec lint+typecheck+test+compilation) puis runtime
(Debian + CloudCompare + xvfb + Python). Le build Docker **echoue** si lint, typecheck ou tests
echouent (`RUN npm run lint|typecheck|test` avant `RUN npm run build` dans le Dockerfile) — sert de
garde-fou avant tout deploiement automatique via webhook.
- `docker-compose.yml` utilise un **volume nomme** (`app-data:/data`), pas un bind mount vers `./data` :
Coolify re-clone le repo a chaque deploiement, un bind mount relatif au checkout perdrait les
donnees (uploads, resultats, base sqlite) a chaque redeploy.
- `HEALTHCHECK` dans le Dockerfile (`curl` sur `/`) : Coolify l'utilise pour determiner si le
deploiement a reussi.
- Le port d'ecoute est configurable via `PORT` (`config.ts` lit `process.env.PORT`, defaut 3000).
## Constantes du pipeline (voir `src/config.ts`)
- `decimationFactor = 2`, `decimationSteps = 5` (fixes par la spec, pas exposes en config utilisateur).
- `gridStepMultiplier = 2` : le grid step de `-VOLUME` = pas spatial initial x2.
- `statsSampleCap = 50000` : nb de points echantillonnes pour estimer la distance mediane au plus
proche voisin (KD-tree scipy) qui sert de pas spatial initial.
- `minPointsToContinue = 25` : la decimation s'arrete si un niveau tombe en dessous.
- `matchingCellsWarnThreshold = 90` : seuil d'affichage "a verifier" dans l'UI.