Les tuiles suivent maintenant les mêmes colSpan × rowSpan que les autres canaux, sur six colonnes — environ 83° d'ouverture, dans le confort du regard — avec un écart serré assumé : espacées, les tuiles se lisent comme des panneaux sans rapport plutôt que comme une planche. Le pas angulaire se déduit de la largeur de cellule et du rayon de l'arc, plutôt que d'être posé en dur. S'y ajoute le sélecteur de langue avec ses drapeaux, et les libellés de l'interface sortis du code. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
137 lines
5.8 KiB
C#
137 lines
5.8 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using MyInfoMate.Vr.Ui;
|
|
using UnityEngine;
|
|
|
|
namespace MyInfoMate.Vr.Menu
|
|
{
|
|
/// <summary>
|
|
/// Le choix de la langue, posé <b>avant le menu</b>.
|
|
///
|
|
/// <b>Pourquoi le demander, et pourquoi à chaque visite.</b> Un casque en borne passe
|
|
/// de main en main : la langue n'est pas un réglage de l'appareil, c'est une propriété
|
|
/// du visiteur en cours. Elle vit donc le temps d'une visite et rien de plus — rien
|
|
/// n'est écrit sur le disque, et le visiteur suivant est de nouveau accueilli par cet
|
|
/// écran. C'est la même logique que la session de statistiques (voir
|
|
/// <see cref="Kiosk.KioskSession"/>), et pour la même raison.
|
|
///
|
|
/// <b>Changer de langue ne coûte aucun appel réseau</b> : l'export porte toutes les
|
|
/// traductions d'un coup — c'est explicitement l'arbitrage de
|
|
/// <c>ConfigurationExport.FetchAsync</c>. Le menu se reconstruit simplement avec
|
|
/// l'autre langue, y compris hors ligne.
|
|
///
|
|
/// <b>Ce sont les mêmes tuiles que le menu</b>, avec un drapeau à la place de la
|
|
/// photo de section : c'est le premier écran de la visite, et il doit annoncer la
|
|
/// grammaire de ce qui suit plutôt que d'être un formulaire posé devant. Les drapeaux
|
|
/// sont ceux de l'app mobile, à l'identique.
|
|
///
|
|
/// L'écran ne s'affiche pas quand la visite n'est offerte que dans une langue : un
|
|
/// choix à une possibilité n'est pas un choix, c'est un écran de plus avant le contenu.
|
|
/// </summary>
|
|
public class LanguagePicker : MonoBehaviour
|
|
{
|
|
const float RadiusMeters = 2.2f;
|
|
|
|
/// <summary>Hauteur du centre de la rangée de tuiles.</summary>
|
|
const float HeightMeters = 1.35f;
|
|
|
|
/// <summary>Le même écart que la grille du menu, pour la même raison.</summary>
|
|
const float GapMeters = 0.03f;
|
|
|
|
static float StepDegrees =>
|
|
(MenuItemPanel.WidthMeters + GapMeters) / RadiusMeters * Mathf.Rad2Deg;
|
|
|
|
/// <summary>Le code de la langue choisie, tel qu'il part dans les traductions.</summary>
|
|
public event Action<string> Chosen;
|
|
|
|
public static LanguagePicker Create(Transform head, IList<string> languages)
|
|
{
|
|
var root = new GameObject("LanguagePicker");
|
|
var picker = root.AddComponent<LanguagePicker>();
|
|
|
|
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 += picker.OnSelected;
|
|
|
|
picker.Build(languages);
|
|
return picker;
|
|
}
|
|
|
|
void Build(IList<string> languages)
|
|
{
|
|
// La consigne est écrite dans <b>chacune</b> des langues proposées : celui
|
|
// qui ne lit pas le français doit pouvoir comprendre l'écran qui lui demande
|
|
// s'il lit le français.
|
|
var prompts = new List<string>();
|
|
foreach (var language in languages)
|
|
{
|
|
var prompt = Strings.Get(Strings.ChooseLanguage, language);
|
|
if (!prompts.Contains(prompt)) prompts.Add(prompt);
|
|
}
|
|
|
|
var heading = Label.Create(transform, "Heading", Label.Anchor.MiddleCenter,
|
|
0.075f, Color.white);
|
|
heading.Text = string.Join("\n", prompts);
|
|
heading.transform.localPosition = new Vector3(
|
|
0f,
|
|
HeightMeters + MenuItemPanel.HeightMeters * 0.5f + 0.13f + prompts.Count * 0.055f,
|
|
RadiusMeters);
|
|
|
|
for (var i = 0; i < languages.Count; i++)
|
|
{
|
|
var panel = MenuItemPanel.Create(
|
|
languages[i], Strings.NameOf(languages[i]), transform);
|
|
|
|
// Une langue sans drapeau garde la tuile nue, avec son nom au centre :
|
|
// c'est déjà lisible, et c'est ce que fait une tuile de section sans photo.
|
|
var flag = Flag(languages[i]);
|
|
if (flag != null) panel.SetImage(flag);
|
|
|
|
Place(panel.transform, i, languages.Count);
|
|
panel.PlayAppear(i * 0.06f);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Le drapeau d'une langue, ou null. ⚠️ La texture vient de <c>Resources</c> et est
|
|
/// <b>partagée</b> : contrairement aux vignettes de section, elle ne se détruit pas
|
|
/// avec la tuile — Unity la garde pour toute la vie de l'app, et la détruire ferait
|
|
/// un drapeau vide à la visite suivante.
|
|
/// </summary>
|
|
static Texture2D Flag(string language) =>
|
|
Resources.Load<Texture2D>("Flags/" + Strings.Normalize(language).ToLowerInvariant());
|
|
|
|
/// <summary>
|
|
/// Les tuiles sur l'arc du menu, centrées sur le regard : c'est la même géométrie
|
|
/// que <see cref="FloatingMenu"/>, à une rangée.
|
|
/// </summary>
|
|
void Place(Transform panel, int index, int count)
|
|
{
|
|
var angle = (index - (count - 1) * 0.5f) * StepDegrees;
|
|
var radians = angle * Mathf.Deg2Rad;
|
|
|
|
panel.localPosition = new Vector3(
|
|
Mathf.Sin(radians) * RadiusMeters,
|
|
HeightMeters,
|
|
Mathf.Cos(radians) * RadiusMeters);
|
|
|
|
panel.localRotation = Quaternion.Euler(0f, angle, 0f);
|
|
}
|
|
|
|
void OnSelected(IAimTarget target)
|
|
{
|
|
if (target is MenuItemPanel panel) Chosen?.Invoke(panel.Id);
|
|
}
|
|
}
|
|
}
|