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>
227 lines
8.1 KiB
C#
227 lines
8.1 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using UnityEngine;
|
|
|
|
namespace MyInfoMate.Vr.Menu
|
|
{
|
|
/// <summary>
|
|
/// Une pile de pages qu'on feuillette — la vue commune du <b>Slider</b>, de la
|
|
/// <b>Map</b> et de l'<b>Event</b>, item <b>E8</b> du lot XR-4.
|
|
///
|
|
/// <b>Pourquoi une seule vue pour trois types.</b> Une galerie de photos, une liste de
|
|
/// points d'intérêt et un programme de la journée sont trois contenus différents dans
|
|
/// le CMS, mais dans le casque ce sont les mêmes gestes : une chose à la fois, grande,
|
|
/// devant soi, et de quoi passer à la suivante. Les trois renderers ne se distinguent
|
|
/// que par <b>ce qu'ils mettent dans les pages</b> — c'est le rôle de
|
|
/// <see cref="SectionPages"/>.
|
|
///
|
|
/// Les pages s'ajoutent au fil du chargement, et la première s'affiche dès qu'elle
|
|
/// arrive : sur vingt photos, attendre la vingtième ferait passer le chargement pour
|
|
/// une panne.
|
|
/// </summary>
|
|
public class PagedView : MonoBehaviour
|
|
{
|
|
const string CommandPrevious = "__previous";
|
|
const string CommandNext = "__next";
|
|
const string CommandBack = "__back";
|
|
|
|
const float DistanceMeters = 2.5f;
|
|
const float ImageWidthMeters = 1.6f;
|
|
const float ImageHeightMeters = 0.9f;
|
|
const float CenterHeight = 1.5f;
|
|
|
|
public class Page
|
|
{
|
|
/// <summary>Le titre de la page. Toujours présent.</summary>
|
|
public string Heading;
|
|
|
|
/// <summary>Description, horaire, adresse… Peut être vide.</summary>
|
|
public string Body;
|
|
|
|
/// <summary>Nulle sur une page sans média — le texte tient tout seul.</summary>
|
|
public Texture2D Image;
|
|
}
|
|
|
|
readonly List<Page> _pages = new List<Page>();
|
|
|
|
Material _imageMaterial;
|
|
Transform _imageQuad;
|
|
Ui.Label _text;
|
|
MenuItemPanel _previous;
|
|
MenuItemPanel _next;
|
|
int _index;
|
|
|
|
/// <summary>Le visiteur veut revenir au menu.</summary>
|
|
public event Action Closed;
|
|
|
|
/// <summary>
|
|
/// N'importe quelle action du visiteur. Sert au mode borne : feuilleter est une
|
|
/// activité, même si la tête ne bouge presque pas.
|
|
/// </summary>
|
|
public event Action Interacted;
|
|
|
|
public int PageCount => _pages.Count;
|
|
|
|
public static PagedView Create(Transform head)
|
|
{
|
|
var root = new GameObject("PagedView");
|
|
var view = root.AddComponent<PagedView>();
|
|
|
|
if (head != null)
|
|
{
|
|
var forward = head.forward;
|
|
forward.y = 0f;
|
|
if (forward.sqrMagnitude < 0.01f) forward = Vector3.forward;
|
|
|
|
root.transform.position = new Vector3(head.position.x, 0f, head.position.z);
|
|
root.transform.rotation = Quaternion.LookRotation(forward.normalized);
|
|
}
|
|
|
|
var selector = root.AddComponent<AimSelector>();
|
|
selector.Configure(head);
|
|
selector.Selected += view.OnSelected;
|
|
|
|
view.Build();
|
|
return view;
|
|
}
|
|
|
|
void Build()
|
|
{
|
|
var quad = GameObject.CreatePrimitive(PrimitiveType.Quad);
|
|
quad.name = "Image";
|
|
quad.transform.SetParent(transform, false);
|
|
quad.transform.localPosition = new Vector3(0f, CenterHeight, DistanceMeters);
|
|
quad.transform.localScale = new Vector3(ImageWidthMeters, ImageHeightMeters, 1f);
|
|
|
|
// L'image n'est pas une cible de visée : ce sont les trois commandes qui le
|
|
// sont, et un collider plein cadre les masquerait.
|
|
Destroy(quad.GetComponent<Collider>());
|
|
|
|
_imageMaterial = new Material(Shader.Find("Universal Render Pipeline/Unlit"));
|
|
_imageMaterial.SetFloat("_Cull", 0f);
|
|
_imageMaterial.color = new Color(0.08f, 0.10f, 0.13f, 1f);
|
|
quad.GetComponent<Renderer>().material = _imageMaterial;
|
|
_imageQuad = quad.transform;
|
|
|
|
_text = Ui.Label.Create(transform, "Text", Ui.Label.Anchor.TopCenter, 0.048f, Color.white);
|
|
_text.transform.localPosition = new Vector3(
|
|
0f, CenterHeight - ImageHeightMeters * 0.62f, DistanceMeters);
|
|
|
|
_previous = Command(CommandPrevious, "◀", -ImageWidthMeters * 0.62f, CenterHeight);
|
|
_next = Command(CommandNext, "▶", ImageWidthMeters * 0.62f, CenterHeight);
|
|
Command(CommandBack, "Retour au menu", 0f, CenterHeight - ImageHeightMeters * 0.95f);
|
|
|
|
UpdateCommands();
|
|
}
|
|
|
|
MenuItemPanel Command(string id, string label, float x, float y)
|
|
{
|
|
var panel = MenuItemPanel.Create(id, label, transform);
|
|
panel.transform.localPosition = new Vector3(x, y, DistanceMeters - 0.02f);
|
|
panel.transform.localScale = Vector3.one * 0.55f;
|
|
return panel;
|
|
}
|
|
|
|
public void AddPage(Page page)
|
|
{
|
|
_pages.Add(page);
|
|
if (_pages.Count == 1) Display(0);
|
|
else UpdateCommands();
|
|
}
|
|
|
|
/// <summary>Quand il n'y a rien à montrer, le dire — pas laisser un cadre vide.</summary>
|
|
public void ShowEmpty(string message)
|
|
{
|
|
_text.Text = Wrap(message, 46);
|
|
UpdateCommands();
|
|
}
|
|
|
|
void Display(int index)
|
|
{
|
|
if (index < 0 || index >= _pages.Count) return;
|
|
|
|
_index = index;
|
|
var page = _pages[index];
|
|
|
|
if (page.Image != null)
|
|
{
|
|
_imageMaterial.mainTexture = page.Image;
|
|
_imageMaterial.color = Color.white;
|
|
_imageQuad.gameObject.SetActive(true);
|
|
}
|
|
else
|
|
{
|
|
// Sans image, le cadre reste — mais sombre : il porte le texte, qui
|
|
// devient alors le contenu principal.
|
|
_imageMaterial.mainTexture = null;
|
|
_imageMaterial.color = new Color(0.08f, 0.10f, 0.13f, 1f);
|
|
}
|
|
|
|
var counter = _pages.Count > 1 ? $"\n{index + 1} / {_pages.Count}" : "";
|
|
var body = string.IsNullOrEmpty(page.Body) ? "" : $"\n{page.Body}";
|
|
|
|
_text.Text = Wrap(page.Heading, 46) + Wrap(body, 46) + counter;
|
|
|
|
UpdateCommands();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Les flèches disparaissent aux extrémités plutôt que de ne rien faire : un
|
|
/// bouton présent qui ne réagit pas se lit comme une panne.
|
|
/// </summary>
|
|
void UpdateCommands()
|
|
{
|
|
if (_previous != null) _previous.gameObject.SetActive(_index > 0);
|
|
if (_next != null) _next.gameObject.SetActive(_index < _pages.Count - 1);
|
|
}
|
|
|
|
void OnSelected(IAimTarget target)
|
|
{
|
|
if (!(target is MenuItemPanel panel)) return;
|
|
|
|
Interacted?.Invoke();
|
|
|
|
switch (panel.Id)
|
|
{
|
|
case CommandPrevious: Display(_index - 1); break;
|
|
case CommandNext: Display(_index + 1); break;
|
|
case CommandBack: Closed?.Invoke(); break;
|
|
}
|
|
}
|
|
|
|
static string Wrap(string text, int columns)
|
|
{
|
|
if (string.IsNullOrEmpty(text)) return "";
|
|
|
|
var wrapped = new System.Text.StringBuilder();
|
|
|
|
// Les retours à la ligne du contenu sont respectés : c'est la mise en forme
|
|
// voulue par le gestionnaire, on ne recolle pas ses paragraphes.
|
|
foreach (var paragraph in text.Split('\n'))
|
|
{
|
|
if (wrapped.Length > 0) wrapped.Append('\n');
|
|
|
|
var line = 0;
|
|
foreach (var word in paragraph.Split(' '))
|
|
{
|
|
if (line + word.Length > columns)
|
|
{
|
|
wrapped.Append('\n');
|
|
line = 0;
|
|
}
|
|
else if (line > 0)
|
|
{
|
|
wrapped.Append(' ');
|
|
line++;
|
|
}
|
|
|
|
wrapped.Append(word);
|
|
line += word.Length;
|
|
}
|
|
}
|
|
|
|
return wrapped.ToString();
|
|
}
|
|
}
|
|
}
|