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>
161 lines
5.8 KiB
C#
161 lines
5.8 KiB
C#
using TMPro;
|
|
using UnityEngine;
|
|
|
|
namespace MyInfoMate.Vr.Ui
|
|
{
|
|
/// <summary>
|
|
/// Tout le texte affiché dans le casque passe par ici.
|
|
///
|
|
/// <b>Pourquoi TextMeshPro.</b> <c>TextMesh</c> rend une police bitmap : à 2,20 m
|
|
/// dans un casque, chaque titre bave dès qu'on s'en approche, et c'est le premier
|
|
/// truc qui fait « prototype ». TMP rend par champ de distance signée — net à
|
|
/// n'importe quelle distance, pour le même coût de rendu.
|
|
///
|
|
/// ⚠️ <b>TMP a besoin de ses ressources.</b> <i>Window → TextMeshPro → Import TMP
|
|
/// Essential Resources</i>, une fois pour le projet. Sans elles, <c>TMP_Settings</c>
|
|
/// n'a pas de police par défaut et un <c>TextMeshPro</c> n'affiche <b>rien</b> —
|
|
/// silencieusement, dans un build. D'où le repli sur <c>TextMesh</c> plus bas : un
|
|
/// texte laid reste lisible, un texte absent transforme une borne en casque cassé.
|
|
/// </summary>
|
|
public class Label : MonoBehaviour
|
|
{
|
|
public enum Anchor { TopCenter, MiddleCenter }
|
|
|
|
/// <summary>
|
|
/// TMP exprime sa taille en points, à raison de 10 points par unité de monde pour
|
|
/// un objet à l'échelle 1. <c>TextMesh</c>, lui, multiplie <c>fontSize</c> par
|
|
/// <c>characterSize</c> puis par 0,1. Les deux constantes ci-dessous n'existent
|
|
/// que pour que les appelants parlent en <b>mètres</b>, une seule fois.
|
|
/// </summary>
|
|
const float TmpPointsPerMeter = 10f;
|
|
|
|
const int LegacyFontSize = 96;
|
|
|
|
TMP_Text _tmp;
|
|
TextMesh _legacy;
|
|
|
|
public static Label Create(Transform parent, string name, Anchor anchor,
|
|
float lineHeightMeters, Color color)
|
|
{
|
|
var root = new GameObject(name);
|
|
root.transform.SetParent(parent, false);
|
|
|
|
var label = root.AddComponent<Label>();
|
|
label.Build(anchor, lineHeightMeters, color);
|
|
return label;
|
|
}
|
|
|
|
void Build(Anchor anchor, float lineHeightMeters, Color color)
|
|
{
|
|
if (TextMeshProAvailable)
|
|
{
|
|
var tmp = gameObject.AddComponent<TextMeshPro>();
|
|
tmp.fontSize = lineHeightMeters * TmpPointsPerMeter;
|
|
tmp.color = color;
|
|
tmp.alignment = anchor == Anchor.TopCenter
|
|
? TextAlignmentOptions.Top
|
|
: TextAlignmentOptions.Center;
|
|
|
|
// Le retour à la ligne est fait par Wrap, en amont : laisser TMP couper
|
|
// en plus donnerait des lignes coupées deux fois.
|
|
tmp.textWrappingMode = TextWrappingModes.NoWrap;
|
|
|
|
// Sans rect assez large, TMP tronque au lieu d'afficher.
|
|
tmp.rectTransform.sizeDelta = new Vector2(4f, 2f);
|
|
|
|
// ⚠️ Le pivot, sinon le texte part en l'air. TMP aligne dans son rect, et
|
|
// ce rect est centré sur l'origine : un texte aligné en haut d'un rect de
|
|
// 2 m s'affiche donc 1 m au-dessus du point où on a posé le Label. En
|
|
// remontant le pivot, le haut du rect est l'origine — c'est ce que fait
|
|
// TextMesh en UpperCenter, sur lequel les positions des appelants sont
|
|
// calées.
|
|
tmp.rectTransform.pivot = new Vector2(
|
|
0.5f, anchor == Anchor.TopCenter ? 1f : 0.5f);
|
|
|
|
_tmp = tmp;
|
|
return;
|
|
}
|
|
|
|
var text = gameObject.AddComponent<TextMesh>();
|
|
text.fontSize = LegacyFontSize;
|
|
text.characterSize = lineHeightMeters / (LegacyFontSize * 0.1f);
|
|
text.color = color;
|
|
text.anchor = anchor == Anchor.TopCenter
|
|
? TextAnchor.UpperCenter
|
|
: TextAnchor.MiddleCenter;
|
|
text.alignment = TextAlignment.Center;
|
|
|
|
_legacy = text;
|
|
}
|
|
|
|
static bool? _available;
|
|
|
|
static bool TextMeshProAvailable
|
|
{
|
|
get
|
|
{
|
|
if (_available.HasValue) return _available.Value;
|
|
|
|
_available = TMP_Settings.instance != null
|
|
&& TMP_Settings.defaultFontAsset != null;
|
|
|
|
if (!_available.Value)
|
|
Debug.LogError(
|
|
"[Ui] Ressources TextMeshPro absentes — repli sur TextMesh, le texte " +
|
|
"sera flou. Window → TextMeshPro → Import TMP Essential Resources.");
|
|
|
|
return _available.Value;
|
|
}
|
|
}
|
|
|
|
public string Text
|
|
{
|
|
set
|
|
{
|
|
if (_tmp != null) _tmp.text = value;
|
|
else if (_legacy != null) _legacy.text = value;
|
|
}
|
|
}
|
|
|
|
public Color Color
|
|
{
|
|
set
|
|
{
|
|
if (_tmp != null) _tmp.color = value;
|
|
else if (_legacy != null) _legacy.color = value;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Coupe à la largeur donnée, en mots entiers. Était recopié dans trois écrans ;
|
|
/// un seul texte mal coupé suffisait à ce qu'ils divergent.
|
|
/// </summary>
|
|
public static string Wrap(string text, int columns)
|
|
{
|
|
if (string.IsNullOrEmpty(text)) return "";
|
|
|
|
var wrapped = new System.Text.StringBuilder();
|
|
var line = 0;
|
|
|
|
foreach (var word in text.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();
|
|
}
|
|
}
|
|
}
|