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>
124 lines
4.6 KiB
C#
124 lines
4.6 KiB
C#
using MyInfoMate.Vr.Net;
|
|
using MyInfoMate.Vr.Scene;
|
|
using UnityEngine;
|
|
|
|
namespace MyInfoMate.Vr.Boot
|
|
{
|
|
/// <summary>
|
|
/// Items <b>E2 et E3</b> du lot XR-4 : le casque s'appaire, puis lit le contenu
|
|
/// de sa configuration. Rien d'immersif ici — c'est la preuve que la chaîne
|
|
/// réseau tient, affichée en une phrase dans le casque.
|
|
///
|
|
/// ⚠️ <b>Ne pas confondre avec <see cref="S1Bootstrap"/> et <see cref="S2Bootstrap"/></b> :
|
|
/// ceux-là sont la piste Scène 3D (S0-S8), celui-ci le canal VR (E0-E11). Deux
|
|
/// numérotations, deux plans.
|
|
///
|
|
/// <b>Le code PIN ne se saisit pas encore</b> : sans l'Interaction SDK (item E1),
|
|
/// il n'y a ni pointeur ni clavier. Il se pose donc dans l'inspecteur avant le
|
|
/// build ; l'écran d'appairage arrive avec E5.
|
|
/// </summary>
|
|
public class PairingBootstrap : MonoBehaviour
|
|
{
|
|
[Header("Serveur")]
|
|
[SerializeField] string baseUrl = "https://api.mymuseum.be";
|
|
|
|
[Header("Appairage — code PIN de l'instance")]
|
|
[SerializeField] string pinCode = "";
|
|
|
|
[Header("Nom affiché dans l'onglet XR du manager")]
|
|
[SerializeField] string headsetName = "Quest";
|
|
|
|
[Header("Langue d'affichage — l'export les porte toutes")]
|
|
[SerializeField] string language = "FR";
|
|
|
|
[Header("Réappairer même si ce casque l'est déjà")]
|
|
[SerializeField] bool forgetPairing;
|
|
|
|
async void Start()
|
|
{
|
|
var head = Camera.main != null ? Camera.main.transform : null;
|
|
if (head == null)
|
|
Debug.LogError("[Pairing] Aucune caméra active : le rig VR n'est pas dans la " +
|
|
"scène. Meta → Tools → Building Blocks → Camera Rig.");
|
|
|
|
if (forgetPairing) PairingService.Forget();
|
|
|
|
var pairing = PairingService.Restore();
|
|
|
|
if (pairing == null)
|
|
{
|
|
if (string.IsNullOrEmpty(pinCode))
|
|
{
|
|
SceneMessage.Show("Ce casque n'est pas appairé. Renseignez le code PIN " +
|
|
"du lieu dans l'inspecteur, puis relancez.", head);
|
|
return;
|
|
}
|
|
|
|
var result = await new PairingService()
|
|
.PairAsync(baseUrl, pinCode, headsetName);
|
|
|
|
if (!result.Ok)
|
|
{
|
|
SceneMessage.Show(result.Error, head);
|
|
return;
|
|
}
|
|
|
|
pairing = result.Value;
|
|
Debug.Log($"[Pairing] Casque appairé — device {pairing.DeviceId}, " +
|
|
$"instance {pairing.InstanceId}");
|
|
}
|
|
|
|
var client = new ApiClient(pairing.BaseUrl, pairing.ApiKey);
|
|
var content = new ContentService(client, pairing.ConfigurationId);
|
|
|
|
var loaded = await content.LoadAsync();
|
|
|
|
if (!loaded.Ok)
|
|
{
|
|
SceneMessage.Show(loaded.Error, head);
|
|
return;
|
|
}
|
|
|
|
Show(loaded.Export, head, loaded.FromCache ? loaded.CachedAt : null);
|
|
|
|
// E4 : le cache a déjà été affiché, le réseau ne fait plus attendre personne.
|
|
// Un échec ici ne se voit pas — c'est le comportement voulu pour une borne.
|
|
if (loaded.FromCache)
|
|
{
|
|
var fresh = await content.RefreshAsync();
|
|
if (fresh != null)
|
|
{
|
|
Debug.Log("[Content] Contenu mis à jour depuis le serveur.");
|
|
Show(fresh, head, null);
|
|
}
|
|
}
|
|
}
|
|
|
|
SceneMessage _message;
|
|
|
|
void Show(ConfigurationExport export, Transform head, System.DateTime? cachedAt)
|
|
{
|
|
// Le rafraîchissement de fond réaffiche : sans ça, deux panneaux se
|
|
// superposent et le texte devient illisible.
|
|
if (_message != null) Destroy(_message.gameObject);
|
|
|
|
var roots = export.RootSections();
|
|
var lines = $"{export.Label} — {roots.Count} sections";
|
|
|
|
if (cachedAt.HasValue)
|
|
lines += $"\n(hors ligne — contenu du {cachedAt.Value.ToLocalTime():dd/MM HH:mm})";
|
|
|
|
for (var i = 0; i < roots.Count; i++)
|
|
{
|
|
var title = ConfigurationExport.Translate(roots[i].Title, language) ?? roots[i].Label;
|
|
Debug.Log($"[Export] {roots[i].Type} — {title}");
|
|
|
|
// Le message flottant reste lisible à cinq lignes ; la console a le reste.
|
|
if (i < 5) lines += $"\n{title} ({roots[i].Type})";
|
|
}
|
|
|
|
_message = SceneMessage.Show(lines, head);
|
|
}
|
|
}
|
|
}
|