DOCS/v2/vr-quest-unity-plan.md
2026-09-03 14:00:51 +02:00

372 lines
23 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# VR Meta Quest — app Unity + onglet XR dans manager-app — 📦 V2
> Relevé le 2026-08-31. Toutes les affirmations « existe / n'existe pas » ci-dessous ont été
> vérifiées **dans le code**, pas dans la doc. Ce fichier remplace la section VR de
> [../roadmap.md](../roadmap.md) comme référence technique — la roadmap garde le volet commercial.
>
> ⚠️ **Rien de ce document n'est V1.** La bascule Postgres et le backlog V1 passent avant.
---
## 0. Correction d'une affirmation de STATUS.md
STATUS.md §5 disait jusqu'ici : VR Meta Quest = « idée, **zéro ligne de code** dans tous les repos ».
C'est faux depuis au moins juillet 2026. **Le canal VR est déjà un citoyen de première classe du
modèle de données et de la navigation du back-office.** Ce qui manque, c'est l'app et l'écran
de flotte — pas les fondations.
---
## 1. Ce qui existe déjà
### 1.1 Backend (`manager-service`)
| Brique | Emplacement | État |
|---|---|---|
| `AppType.VR` | `Data/ApplicationInstance.cs``enum AppType { Mobile, Tablet, Web, VR, Voice }` | ✅ Valeur 3, persistée en base |
| `Instance.IsVR` | `Data/Instance.cs:35`, `DTOs/InstanceDTO.cs:16`, `InstanceController.cs:212` | ✅ Activable par instance, exposé au DTO, modifiable par l'API |
| Migration | `20250709151627_AddedApplicationInstanceAndMisc` | ✅ Colonne `IsVR` en base depuis juillet 2025 |
| `ApplicationInstance` VR | `Controllers/ApplicationInstanceController.cs` — CRUD complet + `application-link` | ✅ Une instance VR se crée déjà par l'API, sans une ligne de code neuve |
| `AppConfigurationLink` | `Data/AppConfigurationLink.cs` | ✅ Porte déjà `DeviceId` (nullable) — c'est exactement le mécanisme « une config par appareil » du kiosk |
| Entité `Device` | `Data/Device.cs` | ✅ `Identifier`, `Name`, `Connected`, `BatteryLevel`, `ConnectionLevel`, `AppVersion`, `LastSeen`, `InstanceId`, `ConfigurationId` |
| `DeviceController` | 7 endpoints (list, detail, create, update, mainInfos, delete) | ⚠️ Existe, mais **hardcodé sur `AppType.Tablet`** (`DeviceController.cs:155`) |
| Push MQTT « config changée » | `DeviceController.cs:303` → topic `player/{device.Id}` | ✅ Le mécanisme de rafraîchissement à distance est déjà là |
| Télémétrie | `Data/VisitEvent.cs:27``public AppType AppType` | ✅ Un event VR se stocke sans aucun changement de schéma |
| Export de contenu | `GET /api/configuration/{id}/export` (`ConfigurationController.cs:388`) | ✅ Configuration + sections + ressources en **un seul appel** — le contrat idéal pour un renderer tiers |
| Auth app cliente | `GET /api/instance/appKeyByPin` (`InstanceController.cs:347`) | ⚠️ Existe, mais `ApiKeyAppType` ne connaît que `{ VisitApp, TabletApp, Other }` |
### 1.2 manager-app
| Brique | Emplacement | État |
|---|---|---|
| `AppType.VR` côté client | `manager_api_new/lib/model/app_type.dart` + `app_type_extensions.dart` | ✅ Valeur 3 présente **et** nommée « VR » |
| Sous-menu « VR » | `lib/Screens/Main/main_screen.dart:65``if (instance.isVR == true)` | ✅ L'entrée de menu apparaît déjà si l'instance a le canal activé |
| Écran VR | `main_screen.dart:698-705``case 'vr'` | ❌ **`Text("TODO vr")`** — c'est littéralement le seul trou de la navigation |
| Écran de flotte à cloner | `lib/Screens/Kiosk_devices/` — 4 fichiers, 769 lignes | ✅ `kiosk_screen.dart` (grille d'appareils), `device_element.dart`, `dropDown_configuration.dart` (assignation de config), `change_device_info_modal.dart` |
| Filtre stats par canal | `lib/Screens/Statistics/statistics_screen.dart:81, 177` | ✅ Générique sur `AppType.values`, et `statsChannelVR` **existe déjà en i18n** — le canal VR apparaîtra tout seul dès le premier `VisitEvent` |
### 1.3 Ce qui n'existe nulle part
- Aucun projet Unity, aucun `.unity`, aucun package Meta XR dans les repos.
- Aucun rendu 360° : pas de `ResourceType.Panorama360` (planifié en V2, voir
[../todo-features.md](../todo-features.md) § Ressource 360°), pas de flag « vidéo equirectangulaire ».
- Aucun `Device` non-tablette : la table est peuplée exclusivement par `tablet-app`.
---
## 2. Ce qu'il reste à faire
### Lot V-1 — Le `Device` devient multi-canal (backend) — **2-3 j**
Aujourd'hui un appareil *est* une tablette : `DeviceController.Create` va chercher
`ApplicationInstances.FirstOrDefault(ai => ai.AppType == AppType.Tablet)` en dur. Un casque
enregistré par ce chemin serait rattaché à l'instance kiosk et apparaîtrait dans l'onglet Kiosk.
- [ ] `Device.AppType` (colonne `int`, **défaut `Tablet` = 1** pour que les lignes existantes ne bougent pas) + migration EF
- [ ] `DeviceDTO.appType` / `DeviceDetailDTO`, mappé dans `ToDTO` / `ToDetailDTO` / `FromDTO`
- [ ] `DeviceController.Create` : résoudre l'`ApplicationInstance` sur `newDevice.appType` au lieu de `AppType.Tablet`
- [ ] `DeviceController.GetAll` : paramètre de requête `appType` optionnel (sans lui, comportement actuel)
- [ ] `ApiKeyAppType` : ajouter `VrApp` **à la fin** de l'enum (`{ VisitApp, TabletApp, Other, VrApp }`) — l'enum est persisté en int, ne jamais insérer au milieu
- [ ] `DeviceControllerTests` : un cas « création d'un device VR » qui vérifie le rattachement à la bonne `ApplicationInstance`
⚠️ Le champ `Device.ConfigurationId` est marqué `[Required]` et le commentaire du code le dit
« OLD WAY → AppConfigurationLink ». Le casque doit passer par `AppConfigurationLink`, pas par ce
champ ; on le remplit quand même pour ne pas casser la contrainte.
### Lot V-2 — Onglet XR dans manager-app — **4-5 j**
C'est le lot le moins cher : l'écran kiosk est un patron directement réutilisable, la seule
différence de fond est l'`AppType` filtré.
- [ ] `lib/Screens/Vr_devices/vr_screen.dart` — clone de `kiosk_screen.dart` : grille de casques, pincode d'appairage en tête, état connecté/déconnecté
- [ ] Réutiliser tel quel `device_element.dart`, `dropDown_configuration.dart` (assignation d'une configuration par casque), `change_device_info_modal.dart`
- [ ] Brancher `main_screen.dart:704` : remplacer `Text("TODO vr")` par `VrScreen(applicationInstanceDTO: applicationInstanceVR)`
- [ ] Carte casque : ajouter niveau de batterie + `AppVersion` + `LastSeen` (colonnes déjà en base, jamais affichées pour le kiosk)
- [ ] `manager_api_new` : ajouter `appType` à `device_dto.dart` / `device_detail_dto.dart` et `VrApp` à `api_key_app_type.dart`**à la main**, la génération OpenAPI ne se relance pas sur ce repo
- [ ] i18n FR/EN/NL/DE : titre d'onglet, « casques », « aucun casque appairé », libellés batterie/version
Rien à faire pour le sous-menu ni pour les stats : les deux sont déjà branchés sur `isVR` / `AppType.VR`.
### Lot V-3 — Contrat de contenu pour un client non-Flutter — **1-2 j**
Unity ne peut pas consommer le client Dart généré. Deux options :
| Option | Coût | Verdict |
|---|---|---|
| Unity appelle les endpoints REST un par un, DTO C# écrits à la main | Élevé et récurrent — chaque évolution de section se répercute | ❌ |
| Unity appelle `GET /api/configuration/{id}/export` (un appel, tout le contenu) et désérialise `ExportConfigurationDTO` | Faible — le DTO d'export est déjà stable, c'est celui de l'import/export back-office | ✅ **retenu** |
- [ ] Vérifier que `Export` répond bien à une clé d'API d'app (aujourd'hui utilisé depuis le back-office authentifié)
- [ ] Documenter le JSON d'export comme contrat public versionné (c'est aussi ce qui servira au mode offline du casque)
- [ ] Décider si l'export multilingue (aujourd'hui `?language=`) doit renvoyer toutes les langues d'un coup pour la VR — un casque en borne change de langue à chaud
### Lot V-4 — L'app Unity + Meta XR SDK — **6-10 semaines** (POC utile : 3-4 semaines)
- [ ] **Socle** : Unity 6 LTS, URP, XR Plugin Management → OpenXR + *Meta XR feature group*, Meta XR Core SDK + Interaction SDK (raycast contrôleur **et** hand tracking)
- [ ] **Couche réseau C#** : appairage par pincode (clone du flux `tablet-app/lib/Screens/Configuration/config_view.dart:274`), récupération de la clé d'API, `POST /api/device` avec `appType = VR` et l'identifiant matériel du casque
- [ ] **Cache disque + offline-first** : une borne d'accueil perd le wifi ; l'export JSON et les médias se mettent en cache, l'app démarre sur le cache et se rafraîchit en tâche de fond
- [ ] **Renderer par type de section** — arbitrage tranché, voir §8
- [ ] **Lecture 360°** : dépend du lot « Ressource 360° » de todo-features.md — sans un type de ressource 360°, il n'y a rien à afficher en immersif
- [ ] **Mode borne** : verrouiller le casque sur l'app (Meta Horizon Managed Solutions ou un MDM tiers), relance auto au réveil, timeout de session, retour au menu à la repose du casque
- [ ] **Télémétrie** : `POST` de `VisitEvent` avec `AppType.VR` → les stats manager-app fonctionnent sans une ligne de plus
- [ ] **Distribution** : Meta Horizon Store (review) ou canal privé / sideload via MDM
### Lot V-5 — Supervision de la flotte — **1-2 semaines**
- [ ] Client MQTT dans Unity (MQTTnet) : heartbeat, batterie, abonnement à `player/{deviceId}` — le publish côté serveur existe déjà (`DeviceController.cs:303`)
- [ ] Alternative pour le POC : polling de l'export toutes les N minutes. Moins élégant, 10× moins cher, suffisant pour 1-3 casques
- [ ] Remontée batterie/version/dernier vu (colonnes déjà présentes sur `Device`)
---
## 3. Estimation
| Lot | Charge | Dépendances |
|---|---|---|
| V-1 — Device multi-canal (backend) | 2-3 j | — |
| V-2 — Onglet XR manager-app | 4-5 j | V-1 |
| V-3 — Contrat de contenu | 1-2 j | — |
| **Sous-total « back-office XR prêt »** | **8-12 j** | |
| V-4 — App Unity (POC : menu + vidéo 360 + une section) | 3-4 sem. | V-1, V-3, Ressource 360° |
| V-4 — App Unity (couverture complète) | 6-10 sem. | idem |
| V-5 — Supervision flotte | 1-2 sem. | V-4 |
| **Total avec app complète** | **~3-4 mois** d'un dev | |
Le découpage est volontaire : **les 8-12 jours du back-office XR se tiennent debout tout seuls**.
Ils rendent le canal VR administrable et démontrable (flotte, assignation de config, stats)
avant d'engager le vrai coût, qui est l'app Unity.
---
## 4. Risques
1. **Le contenu du CMS est 2D.** C'est le risque produit majeur, pas un risque technique. Un
Article affiché sur un panneau flottant en VR est *moins* bon qu'une tablette. La valeur du
canal vient du contenu 360°, que très peu de clients possèdent. Sans un client pilote qui a
déjà des vidéos 360°, le POC démontrera une régression.
2. **Nouvelle stack, zéro mutualisation.** Unity/C# XR ne réutilise ni Flutter, ni le client API
généré, ni `myinfomate_layout`. C'est un quatrième front à maintenir, pas une déclinaison.
3. ~~**Économie du MDM.**~~ **Rétrogradé le 2026-08-31.** Un MDM XR (~15-20 €/casque/mois) n'est
nécessaire qu'à l'échelle d'une flotte. À 1-3 casques, sideload adb + le mode appareil intégré
suffisent. Ne redevient un sujet qu'au-delà d'une dizaine de casques.
⚠️ Les noms et l'état des programmes business de Meta bougent vite — à revérifier avant de
l'écrire dans une offre.
4. **Exploitation en borne publique** : hygiène des casques, sessions courtes, présence de
personnel. C'est un frein opérationnel réel côté client, indépendant de la qualité de l'app.
5. **La landing promet déjà « Meta Quest — Bientôt »** en 4 langues
(`myinfomate-landing`, `translations.ts`, clé `mode4Desc`). L'écart annonce/réalité existe
depuis un moment ; il se creuse tant que rien n'est livré.
---
## 5. Recommandation de séquencement
1. **V-1 + V-2 + V-3** (~2 semaines) — peu cher, sans risque, rend le canal administrable, ferme le `TODO vr`.
2. **Ressource 360°** (déjà en V2, le plus petit des trois chantiers médias) — prérequis dur du POC.
3. **Décision go/no-go** conditionnée à **un client pilote avec du contenu 360° existant**.
Sans lui, ne pas démarrer V-4.
4. **POC Unity** (3-4 semaines) — menu immersif + vidéo 360 + une section riche.
5. V-4 complet et V-5 seulement après validation du POC en conditions réelles.
---
> **§6 à §10 ajoutés le 2026-08-31**, seconde passe. Tout ce qui suit est vérifié dans le code —
> c'est de l'analyse déjà faite, à ne pas refaire.
---
## 6. Pricing — add-on « Contenu immersif », à partir de 70 €/mois
**Arbitré par Thomas le 2026-08-31.** Les chiffres encore inscrits dans
[../roadmap.md](../roadmap.md) — `+30-50€/mois` (L113) et `+40€/mois` dans le bundle (L148-149) —
sont **périmés** et à réécrire.
> Historique de la décision : l'assistant avait proposé un **palier « Immersif » à 299 €/mois**
> en remplacement de l'add-on. Arbitrage retenu : **on garde un add-on**, revalorisé de 40 à
> **70 €/mois et plus**. Le raisonnement du palier est conservé ci-dessous parce que **son
> principe structurant, lui, est validé** — c'est le montant et le véhicule qui changent.
### Le principe qui reste vrai : décorréler du casque
Un add-on « VR » ne se vend que le jour où l'app Unity existe *et* où le client achète un casque.
Un add-on **« Contenu immersif »** vend le *contenu* — image 360, vidéo 360, modèles GLB — qui
s'affiche **déjà dans le web, le mobile et le kiosk**. Le casque devient le haut de gamme de
l'add-on, pas sa condition d'existence. Un client souscrit et en a pour son argent sans casque.
### Pourquoi l'add-on bat le palier — le point que l'assistant avait manqué
**Un add-on s'accroche à n'importe quel palier.** Le palier à 299 € forçait tout le monde à passer
par Premium ; l'add-on permet à un client **Pro à 99 € de prendre du 360 sans payer l'assistant IA
dont il ne veut pas** → 169 € au lieu de 299 €. Marché adressable nettement plus large.
Sur le total, les deux modèles convergeaient de toute façon : Premium 179 + 70 = **249 €**, soit
exactement le plancher de négociation cité pour le palier. Le désaccord portait sur la structure,
pas sur le montant.
Paliers de référence (source de vérité : `myinfomate-landing`, `HomeClient.tsx:534,583` +
`segments.ts`) : Essentiel 39 / Pro 99 / Premium 179 / Enterprise sur devis.
### Ce que l'add-on débloque
Ressources image 360, vidéo 360, modèles GLB avec POI cliquables texte/audio — et l'app Quest
quand elle sortira. L'assistant IA **n'est pas dedans** : il reste attaché à son propre add-on /
au palier Premium.
### ⚠️ Le stockage — plus aigu en add-on qu'en palier
C'est le seul point de vigilance qui **s'aggrave** avec ce modèle. Un client Pro à 99 € a un quota
dimensionné pour du Pro, et une vidéo 360 de 5 min en qualité correcte pèse des Go.
➡️ **L'add-on doit porter sa propre enveloppe de stockage**, explicite et finie, avec facturation
du dépassement. Sans ça, le premier client 360 sur un petit palier fait sauter la marge.
### Ce qui reste HORS de l'add-on
| Exclusion | Pourquoi |
|---|---|
| Modélisation 3D | Coût studio, 3 à 15 k€, variable — reste **sur devis** |
| Hardware casque | Achat client |
| Stockage au-delà de l'enveloppe | Voir ci-dessus — plafond **dur** + facturation du dépassement |
Onboarding one-shot : **1200-1500 €** quand il y a des casques (provisioning, config borne,
formation, journée sur site). Sans casque, l'add-on ne justifie pas de frais de mise en place
particuliers.
### Implémentation — une journée de backend
**Vérifié dans le code, et c'est la bonne surprise.** L'`Instance` **recopie localement** les
valeurs du plan au lieu de les lire à travers : `StorageQuotaBytes`, `AiTokensPerMonth`,
`HasStats`, `StatsHistoryDays`, `HasAdvancedStats` sont tous des colonnes de `Instance`
(`Data/Instance.cs:87-98`). Le `SubscriptionPlan` n'est qu'un **gabarit**.
Et `IsAssistant` (`Data/Instance.cs:37`) est déjà exactement ce patron : **un add-on = un `bool`
sur l'`Instance`**.
- [ ] `Instance.HasImmersiveContent` (bool) + migration
- [ ] Relèvement de `StorageQuotaBytes` à l'activation
- [ ] Exposition dans `InstanceDTO` + l'écran SuperAdmin
➡️ **Pas de nouvelle ligne de plan, pas de table de jointure, pas de refonte de la facturation.**
**Et ça tombe sur une carte déjà planifiée** : « Écran SuperAdmin — add-on IA & canaux d'une
instance » (kanban, colonne Planifié), qui dit explicitement « à faire d'un bloc avec l'endpoint
de mise à jour d'`ApplicationInstance` : c'est le même écran ». L'add-on immersif y prend sa place
sans rien ajouter au backlog.
⚠️ **Réserve Stripe** : le webhook ne gère que `checkout.session.completed` et
`invoice.payment_failed` (`Controllers/StripeWebhookController.cs:62-65`), rien sur les lignes
d'abonnement. Facturer l'add-on **automatiquement** demandera du travail. Pour 1-3 clients
pilotes, une ligne ajoutée à la main dans Stripe suffit.
---
## 7. Assistant IA dans le casque — possible, et quasi gratuit
**Vérifié dans le code.** `POST /api/Ai/chat` (`Controllers/AiController.cs:367`) prend déjà un
`AppType` dans sa requête (`DTOs/AiChatDTO.cs:10`) et se contente de vérifier que
l'`ApplicationInstance` de cet `AppType` a `IsAssistant = true` (`AiController.cs:387`).
➡️ Créer l'`ApplicationInstance` VR avec le flag suffit. **Zéro ligne de backend.**
Deux nuances :
- **Pas de fallback pour VR.** Le fallback sur Mobile n'existe que pour `AppType.Voice`
(commentaire `AiController.cs:387`). Sans `ApplicationInstance` VR, c'est un 403.
- **Le TTS est côté client.** L'instance ne stocke qu'un nom de voix Gemini (`Data/Instance.cs:54`),
la synthèse est faite par l'app. Unity devra appeler Gemini TTS lui-même et gérer le micro pour
le STT. L'architecture est prouvée par le POC Ray-Ban (11 implémentations de moteurs, branche
`Meta-Rayban-Test`) mais c'est du **Dart** : réimplémentation en C#, pas un portage.
---
## 8. Types de section en VR — arbitrage tranché
Sur les 13 types existants :
| Verdict | Types | Pourquoi |
|---|---|---|
| **Nécessaire** | Menu | C'est le hub flottant lui-même |
| **Gagne vraiment** | Video (360 en skybox), Slider (galerie immersive), Map (le plan posé devant soi comme une maquette — c'est là qu'arrive le GLB), Parcours (chaque étape = un lieu autour de soi) | La VR ajoute une dimension que l'écran n'a pas |
| **Utile en borne** | Event / Agenda | « Ce qui se passe aujourd'hui » en écran d'attente d'office de tourisme — c'est même l'usage principal |
| **Correct** | Quiz | Jouable au regard ou au contrôleur, bon pour l'engagement |
| **À ne pas porter** | Article, PDF, Web, Weather, Game | Un mur de texte flottant est *moins* bon qu'une tablette. Une WebView en VR est frustrante |
**Scope du POC** : Menu + Video 360 + Slider + Map/GLB, avec Event en écran d'attente.
Quatre types, et c'est exactement la démo.
---
## 9. POI sur modèle GLB — le modèle de données existe déjà à 80 %
**La plus grosse économie du chantier, repérée le 2026-08-31.**
`GeoPoint` (`Data/SubSection/SectionMap.cs:85`) porte déjà `Title`, `Description`, `ImageResource`,
et surtout **`Contents`** — une `List<Content>` où chaque `Content` (`Data/SubSection/Content.cs`)
est titre + description + **`Resource`**, donc **audio**. Le tout multilingue via `TranslationDTO`.
➡️ Un POI sur GLB, c'est **le même objet avec une position locale (x,y,z) au lieu d'une lat/lon**.
- [ ] `GeoPoint.Model3DPosition` nullable (x,y,z + rotation) à côté du `Geometry` PostGIS existant
- [ ] `SectionModel3D` référençant une ressource GLB
- [ ] `ResourceType.Model3D`**en fin d'enum**, il est persisté en int
- [ ] Éditeur de placement de POI en 3D dans manager-app — **le seul morceau non trivial**
Le back-office réutilise l'éditeur de POI existant ; le multilingue et l'audio suivent gratuitement.
---
## 10. Unity ou WebXR — la question tranchée
### Unity n'est PAS une nécessité technique
La distinction qui compte : **le mode kiosk ne vient pas du moteur, il vient de la gestion
d'appareil.** Un MDM ou le mode appareil de Meta verrouille le casque sur *n'importe quelle* app
installée — build Unity, APK Godot, ou navigateur épinglé sur une URL. Le choix du moteur est
**orthogonal** au verrouillage kiosk.
Routes possibles vers un APK installable sur Quest : Unity + Meta XR SDK, Unreal, Godot 4 + OpenXR,
ou du WebXR empaqueté (Meta supporte l'installation de PWA sur Horizon OS).
⚠️ Sur ce dernier point : hors date de connaissance de l'assistant, **à revérifier** avant
tout engagement.
### Ce qui plaide pour Unity — l'exploitation, pas la technique
- **Média offline** : plusieurs Go de vidéo 360 en cache navigateur se heurtent aux quotas de
stockage. En natif on écrit sur le disque.
- **Décodage vidéo** : une 360 en 8K veut le décodeur matériel et le contrôle dessus.
- **Prévisibilité** — *l'argument le plus fort* : une borne tourne 8 h/jour sans personne. Un build
Unity est figé ; le navigateur du casque se met à jour tout seul et peut casser la borne un mardi
matin.
- Accessoirement : MQTT en tâche de fond, hand tracking riche, porte du Horizon Store.
### Ce qui plaide pour WebXR — à ne pas balayer
`visitapp-web` rend **déjà les 13 types de section** en React 19 / Next 16
(`src/components/sections/`, 13 fichiers, aucune dépendance 3D pour l'instant). Une couche WebXR
par-dessus **attaque frontalement le risque n°2** (« nouvelle stack, zéro mutualisation ») :
c'est potentiellement un quatrième front en moins à maintenir.
### Position retenue
**WebXR pour la maquette et la spec visuelle, Unity pour la borne en production.**
Pas parce que le web ne peut pas, mais parce qu'une borne publique non surveillée pardonne mal.
➡️ **À rouvrir si** l'usage devient « le visiteur met le casque 5 minutes avec un agent à côté » —
là, le web redevient le bon choix.
### Ce que l'assistant peut et ne peut pas produire
**Peut** : le projet Unity complet — scripts C#, client API, script d'éditeur qui construit la scène
par code (plutôt que du YAML `.unity` écrit à la main, illisible et cassant), `manifest.json` avec
les packages Meta XR, menu flottant, swap de skybox 360, chargement GLB, raycast sur POI avec audio.
**Ne peut pas** : ⚠️ **Unity n'est pas installé sur cette machine** — vérifié le 2026-08-31, ni
`Program Files/Unity`, ni Unity Hub. Donc pas de compilation, pas d'APK, pas de rendu, pas de
casque de test. Le livrable est un projet *plausible* : il faut l'ouvrir et itérer sur les erreurs
de compilation. **Compter 2 à 4 allers-retours**, surtout sur le SDK Meta dont les API bougent
entre versions.
Pour *voir* le rendu, la maquette WebXR/three.js est le chemin court : visible sur écran ou dans le
navigateur du Quest en immersif, itérable en secondes. Précédent dans le repo : la démo villa
d'Echternach (`visitapp-web/src/app/demo/villa-echternach/scene.ts`), rendu procédural fait
exactement pour montrer au client.