using System; using System.Collections.Generic; using UnityEngine; namespace MyInfoMate.Vr.Menu { /// /// Une pile de pages qu'on feuillette — la vue commune du Slider, de la /// Map et de l'Event, item E8 du lot XR-4. /// /// Pourquoi une seule vue pour trois types. 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 ce qu'ils mettent dans les pages — c'est le rôle de /// . /// /// 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. /// 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 { /// Le titre de la page. Toujours présent. public string Heading; /// Description, horaire, adresse… Peut être vide. public string Body; /// Nulle sur une page sans média — le texte tient tout seul. public Texture2D Image; } readonly List _pages = new List(); Material _imageMaterial; Transform _imageQuad; Ui.Label _text; MenuItemPanel _previous; MenuItemPanel _next; int _index; /// Le visiteur veut revenir au menu. public event Action Closed; /// /// N'importe quelle action du visiteur. Sert au mode borne : feuilleter est une /// activité, même si la tête ne bouge presque pas. /// public event Action Interacted; public int PageCount => _pages.Count; public static PagedView Create(Transform head) { var root = new GameObject("PagedView"); var view = root.AddComponent(); 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(); 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()); _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().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(); } /// Quand il n'y a rien à montrer, le dire — pas laisser un cadre vide. 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(); } /// /// 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. /// 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(); } } }