using System;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
using MyInfoMate.Vr.Net;
using UnityEngine;
namespace MyInfoMate.Vr.Menu
{
///
/// Le menu flottant — le hub de l'app, item E5 du lot XR-4.
///
/// C'est le rendu de la SectionMenu du CMS, et le seul type de section dont
/// le plan dit qu'il est « nécessaire » : tout le reste s'atteint depuis lui.
///
/// Disposition en arc, pas en grille plate. Une grille plate oblige à tourner
/// les yeux vers ses bords, qui sont alors vus de biais ; un arc centré sur le
/// visiteur présente chaque panneau de face, à distance constante. C'est le même
/// principe qu'un pupitre incurvé.
///
public class FloatingMenu : MonoBehaviour
{
/// 2,2 m : au-delà le texte devient petit, en deçà l'œil doit converger.
const float RadiusMeters = 2.2f;
const float HeightMeters = 1.45f;
/// Écart angulaire entre deux panneaux, assez large pour que le regard tranche.
const float StepDegrees = 16f;
/// Au-delà, on passe à une seconde rangée plutôt que d'encercler le visiteur.
const int MaxPerRow = 5;
readonly List _panels = new List();
AimSelector _selector;
/// L'id de la section choisie.
public event Action SectionChosen;
public static FloatingMenu Create(Transform head)
{
var root = new GameObject("FloatingMenu");
var menu = root.AddComponent();
menu._selector = root.AddComponent();
menu._selector.Configure(head);
menu._selector.Selected += menu.OnSelected;
menu._head = head;
menu.Recenter();
return menu;
}
Transform _head;
///
/// Pose le menu devant le visiteur. Appelé au démarrage et à chaque nouvelle
/// visite (mode borne) : le visiteur suivant n'arrive pas forcément orienté comme
/// le précédent, et un menu resté dans son dos est un menu introuvable.
///
/// Entre deux recentrages, le menu ne suit pas la tête : un menu qui suit
/// est impossible à viser, et donne le mal de cœur.
///
public void Recenter()
{
if (_head == null) return;
var forward = _head.forward;
forward.y = 0f;
if (forward.sqrMagnitude < 0.01f) forward = Vector3.forward;
transform.position = new Vector3(_head.position.x, 0f, _head.position.z);
transform.rotation = Quaternion.LookRotation(forward.normalized);
}
public void Build(ConfigurationExport export, string language)
{
foreach (var panel in _panels) Destroy(panel.gameObject);
_panels.Clear();
var sections = export.RootSections();
for (var i = 0; i < sections.Count; i++)
{
var title = ConfigurationExport.Translate(sections[i].Title, language)
?? sections[i].Label;
var panel = MenuItemPanel.Create(sections[i].Id, title, transform);
Place(panel.transform, i, sections.Count);
// La cascade : 40 ms d'écart entre deux panneaux. Assez pour qu'on voie
// le menu se construire, assez peu pour que le dernier soit là avant
// qu'on ait fini de tourner la tête.
panel.PlayAppear(i * 0.04f);
_panels.Add(panel);
}
}
///
/// Les vignettes arrivent après coup : le menu doit être utilisable avant que
/// le premier octet d'image ne soit téléchargé.
///
public async Task LoadImagesAsync(ConfigurationExport export, ApiClient client)
{
var sections = export.RootSections();
for (var i = 0; i < sections.Count && i < _panels.Count; i++)
{
var source = sections[i].ImageSource;
if (string.IsNullOrEmpty(source)) continue;
var path = await ContentCache.MediaPathAsync(client, source);
if (path == null || _panels[i] == null) continue;
var texture = new Texture2D(2, 2);
if (texture.LoadImage(File.ReadAllBytes(path)))
_panels[i].SetImage(texture);
}
}
void Place(Transform panel, int index, int total)
{
var row = index / MaxPerRow;
var inRow = index % MaxPerRow;
var countInRow = Mathf.Min(total - row * MaxPerRow, MaxPerRow);
var angle = (inRow - (countInRow - 1) / 2f) * StepDegrees;
var radians = angle * Mathf.Deg2Rad;
var height = HeightMeters - row * (MenuItemPanel.HeightMeters + 0.06f);
panel.localPosition = new Vector3(
Mathf.Sin(radians) * RadiusMeters,
height,
Mathf.Cos(radians) * RadiusMeters);
// Chaque panneau pivote vers le centre de l'arc — donc vers le visiteur.
panel.localRotation = Quaternion.Euler(0f, angle, 0f);
}
void OnSelected(IAimTarget target)
{
if (target is MenuItemPanel panel) SectionChosen?.Invoke(panel.Id);
}
}
}