using System;
using UnityEngine;
using UnityEngine.XR;
namespace MyInfoMate.Vr.Kiosk
{
///
/// Le mode borne — item E10 du lot XR-4.
///
/// Ce qui sépare une démo d'une borne : une démo, quelqu'un la lance et la
/// range ; une borne tourne 8 h par jour, passe de main en main, et personne ne la
/// surveille. Le seul état acceptable pour le visiteur suivant est le menu, devant
/// lui, comme si l'app venait de démarrer — pas la galerie que le précédent avait
/// laissée ouverte, pas un menu dans son dos.
///
/// Deux déclencheurs, et le second est le vrai :
///
/// - Le casque est reposé — le signal le plus fiable qui existe : la
/// visite est finie, tout de suite, sans attendre.
/// - Plus rien ne bouge pendant un moment — le repli, pour le casque
/// posé sur une table sans que personne ne l'ait retiré de la tête.
///
///
/// Sans SDK Meta. La présence du porteur se lit par CommonUsages.userPresence
/// d'Unity XR, alimenté par OpenXR : pas besoin d'un OVRManager dans la scène,
/// donc rien à configurer et rien à oublier de configurer.
///
public class KioskSession : MonoBehaviour
{
///
/// 90 s : assez long pour qu'un visiteur qui lit un panneau ne soit pas renvoyé
/// au menu, assez court pour que le suivant ne trouve pas l'écran du précédent.
///
[SerializeField] float idleSeconds = 90f;
/// Au-delà, la tête a bougé : quelqu'un est là.
const float MovementThresholdDegrees = 3f;
Transform _head;
Quaternion _lastRotation;
float _idle;
bool _wasWorn = true;
/// La visite est finie — revenir au menu et repartir de zéro.
public event Action Ended;
public static KioskSession Attach(GameObject host, Transform head)
{
var session = host.AddComponent();
session._head = head;
session._lastRotation = head != null ? head.rotation : Quaternion.identity;
// Une borne ne doit pas s'éteindre toute seule pendant qu'un visiteur lit.
Screen.sleepTimeout = SleepTimeout.NeverSleep;
return session;
}
///
/// À appeler sur toute action délibérée. Le mouvement de tête suffit à détecter
/// une présence, mais pas à prouver l'attention : quelqu'un qui vise et choisit
/// est actif même s'il bouge à peine.
///
public void NotifyActivity() => _idle = 0f;
void Update()
{
if (IsWorn())
{
if (!_wasWorn)
{
// Le casque vient d'être remis : c'est un nouveau visiteur, même si
// le précédent l'a reposé il y a dix secondes.
_wasWorn = true;
_idle = 0f;
Ended?.Invoke();
}
}
else if (_wasWorn)
{
_wasWorn = false;
Ended?.Invoke();
return;
}
if (!_wasWorn) return;
if (_head != null)
{
if (Quaternion.Angle(_head.rotation, _lastRotation) > MovementThresholdDegrees)
{
_lastRotation = _head.rotation;
_idle = 0f;
}
}
_idle += Time.deltaTime;
if (_idle < idleSeconds) return;
_idle = 0f;
Ended?.Invoke();
}
///
/// Vrai quand le casque est porté. Un runtime qui ne sait pas répondre — le
/// simulateur, l'éditeur — renvoie « porté » : sur un poste de développement, une
/// borne qui se réinitialise toutes les 90 s serait intenable.
///
static bool IsWorn()
{
var device = InputDevices.GetDeviceAtXRNode(XRNode.Head);
if (!device.isValid) return true;
return !device.TryGetFeatureValue(CommonUsages.userPresence, out var present) || present;
}
}
}