vr-app/viewer/README.md
Thomas Fransolet 0310d28b5e Canal VR Meta Quest : projet Unity, viewer web et documentation
Premier commit du cinquième front. Trois parties :

- `unity/MyInfoMateVR/` : le projet Unity (6000.0.83f1, URP, Meta XR SDK 205),
  un APK unique pour tous les clients. Menu flottant à sélection au regard,
  appairage, chargement de scène GLB, POI, cache de contenu, télémétrie.
- `unity-overlay/` : les mêmes scripts à recopier sur un projet Unity neuf, avec
  les pièges rencontrés consignés dans son README.
- `viewer/` : viewer et éditeur de scène web autonome (Vite, TypeScript,
  three.js), partagé avec les autres fronts.
- `docs/` : état des lieux, setup Unity, décisions d'architecture et plan
  d'exécution en 9 étapes.

La scène est décrite par un `scene.json` poussé par `adb push` : l'app le
préfère à celui embarqué dans l'APK.

Les binaires (GLB, textures de l'échantillon Sponza, DLL Meta XR) passent par
Git LFS dès ce premier commit — les y faire entrer après coup demanderait de
réécrire l'historique. Les artefacts régénérés par l'éditeur et par CMake
(`Library/`, `.utmp/`, Burst debug) sont ignorés.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-16 15:26:07 +02:00

93 lines
4.4 KiB
Markdown

# viewer — le viewer et l'éditeur de scène 3D
Application web **autonome** (décision D3 de [`../docs/02-decisions.md`](../docs/02-decisions.md)).
Elle affiche une scène 3D décrite par un manifeste, et permet de composer ce manifeste à la souris.
C'est l'étape **S3** du plan. Elle ne contient **aucun Flutter et aucun appel au backend** — c'est
ce qui la rend faisable pendant que le casque est bloqué.
## Démarrer
```bash
npm install # copie aussi les décodeurs Draco/KTX2 dans public/libs/
npm run dev # http://localhost:5180
npm run build # typecheck + bundle statique dans dist/
```
Puis **glisser-déposer** dans la fenêtre le `scene.json` **et** les `.glb` qu'il référence,
ensemble. Le fichier d'exemple est celui de S2 :
`../unity/MyInfoMateVR/Assets/StreamingAssets/scene.json`, avec `world.glb`, `calibration.glb` et
`persona.glb` du même dossier.
## Les gestes
| Geste | Effet |
|---|---|
| clic | sélectionner |
| glisser | déplacer au sol (XZ) |
| **Alt** + glisser | régler la hauteur (Y) — indispensable pour un point d'intérêt |
| **Maj** + glisser | pivoter (lacet) |
| Échap | désélectionner |
| Ctrl+Z / Ctrl+Maj+Z | annuler / refaire |
Les valeurs numériques du panneau sont éditables et passent par le même undo : poser à la souris est
confortable mais imprécis, « ce panneau à exactement 1,40 m » se tape.
## Le test qui compte
C'est le critère de validation de S3, et c'est **la seule preuve valable de la convention d'axes** :
1. placer trois objets dans le viewer, à des endroits **asymétriques** ;
2. exporter le `scene.json` ;
3. le pousser sur le casque (`adb push … /sdcard/…`, ou le remettre dans `StreamingAssets`) ;
4. les trois objets sont **exactement là** où ils ont été mis.
Une scène symétrique ne prouve rien : un miroir d'axes y est invisible. C'est précisément le bug que
`GltfSpace` et `calibration.glb` existent pour attraper.
## Ce que ce viewer n'est pas
Un **aperçu indicatif**, et le mot est dans l'interface — pas seulement ici. Sont alignés sur Unity :
tone mapping ACES, exposition du manifeste, environnement neutre, PBR core glTF. Ne sont **pas**
garantis : ombres, post-traitement, matériaux exotiques. Sans cet avertissement, un client valide une
couleur qu'il ne retrouvera pas au casque.
Ce n'est pas non plus un éditeur 3D : pas de gizmos, pas de hiérarchie, pas de matériaux. Le client
compose, il ne modélise pas (§8 de la conception).
## Les trois règles de D3, et pourquoi elles sont déjà là
Elles ne coûtent rien maintenant et coûtent une réécriture au moment du portage.
1. **Fichiers statiques.** `base: './'`, décodeurs Draco/KTX2 copiés en local, aucun CDN. Le bundle
de `dist/` est embarquable dans les assets d'une app Flutter et servable depuis `file://`.
2. **Aucune URL d'API en dur.** Toutes les URL viennent de `assets[].url`. Un asset se désigne par
son identifiant dans l'interface, jamais par une URL.
3. **Le manifeste est une entrée, pas un fetch.** Trois chemins : `postMessage` (le cas
`manager-app` en S5, et le cas offline), `?manifest=<url>` (le cas S4), glisser-déposer (le cas
d'aujourd'hui). Le glisser-déposer réécrit `assets[].url` en `blob:` — exactement ce que fera le
pipeline offline avec des chemins locaux, donc ce scénario est testé tous les jours.
## Où il ira ensuite
| Front | Intégration | Étape |
|---|---|---|
| `manager-app` (Flutter Web) | iframe `HtmlElementView` + `postMessage` | S5 |
| `visitapp-web` (Next.js) | composant, lecture seule (`mode: 'view'`) | hors V1 |
| `mymuseum-visitapp` (Flutter) | WebView, bundle embarqué, hors ligne | hors V1 |
Le protocole `postMessage` est déjà écrit ([`src/bridge.ts`](src/bridge.ts)) alors que son premier
consommateur n'existe pas : c'est la règle 3 ci-dessus.
## Les fichiers
| Fichier | Rôle |
|---|---|
| `src/manifest.ts` | Les types du manifeste et sa validation. **Miroir de `SceneManifest.cs`** — modifier l'un sans l'autre casse le test croisé |
| `src/Viewer.ts` | three.js : scène, caméra, chargement glTF, tone mapping aligné sur Unity |
| `src/Editor.ts` | Sélection, placement, rotation, undo, état « non enregistré » |
| `src/Bounds.ts` | Le cercle de navigation et la grille au mètre, à l'échelle |
| `src/Panel.ts` | Listes, valeurs numériques, contenu multilingue des points d'intérêt, budget |
| `src/bridge.ts` | Le protocole `postMessage` |
| `src/main.ts` | Le câblage, l'export, le glisser-déposer |