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>
152 lines
5.2 KiB
C#
152 lines
5.2 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);
|
|
|
|
_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();
|
|
}
|
|
}
|
|
}
|