- 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
170 lines
11 KiB
Markdown
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.
|