using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using MyInfoMate.Vr.Manifest;
using MyInfoMate.Vr.Net;
using MyInfoMate.Vr.Scene;
using UnityEngine;
namespace MyInfoMate.Vr.Menu
{
///
/// Une maquette 3D et ses points d'intérêt — item E7 du lot XR-4.
///
/// Ce fichier ne dessine rien. Le moteur existe déjà :
/// charge un GLB, place les hotspots, gère leur visée et leur audio spatialisé —
/// c'est tout le travail de la piste Scène 3D. Ce qui manquait, c'est la
/// source de données : le manifeste venait d'un fichier poussé à la main,
/// il vient maintenant de la section.
///
/// La traduction tient en trois lignes parce que les deux modèles ont été pensés
/// l'un pour l'autre : un SceneHotspot porte déjà un GeoPointId, et
/// GeoPoint.LocalTransform est dans la convention du manifeste — pas celle
/// d'Unity. La conversion d'axes reste donc au seul endroit qui la connaît.
///
public class Scene3DView : MonoBehaviour
{
const string ModelAssetId = "model";
SceneBuilder _builder;
MenuItemPanel _back;
Transform _head;
public event Action Closed;
public event Action Interacted;
public static Scene3DView Create(Transform head)
{
var root = new GameObject("Scene3DView");
var view = root.AddComponent();
view._head = head;
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();
selector.Configure(head);
selector.Selected += view.OnSelected;
view._back = MenuItemPanel.Create("__back", "Retour au menu", root.transform);
view._back.transform.localPosition = new Vector3(0f, 0.9f, 2.2f);
view._back.transform.localScale = Vector3.one * 0.6f;
return view;
}
public async Task LoadAsync(ConfigurationExport.SectionSummary section,
ConfigurationExport export,
string language)
{
var manifest = BuildManifest(section, export, language);
if (manifest == null) return false;
_builder = gameObject.AddComponent();
// ⚠️ Le rig n'est passé qu'en mode **décor**. En mode objet, on regarde la
// chose de l'extérieur : lui donner une zone de navigation la ferait parcourir
// comme un lieu, et le visiteur se retrouverait *dans* l'épée.
var rig = section.Scene3DMode == ConfigurationExport.Scene3DMode.Scene
? (_head != null ? _head.root : null)
: null;
return await _builder.BuildAsync(manifest, _head, rig, language);
}
///
/// Où poser le modèle. Un objet se présente devant le visiteur, à portée de
/// regard ; un décor l'entoure, donc il reste à l'origine et c'est le visiteur qui
/// est dedans.
///
static TransformManifest WorldTransformFor(ConfigurationExport.Scene3DMode mode)
{
if (mode == ConfigurationExport.Scene3DMode.Scene) return new TransformManifest();
// 1,20 m de haut, 1,50 m devant : la hauteur d'un objet posé sur un socle,
// à la distance où l'on tend la main sans avancer.
return new TransformManifest
{
Position = new[] { 0f, 1.2f, 1.5f },
Rotation = new[] { 0f, 0f, 0f, 1f },
Scale = new[] { 1f, 1f, 1f }
};
}
///
/// Le manifeste que le moteur attend, construit depuis la section. Le modèle
/// devient le décor : c'est bien ce qu'il est ici, la scène entière.
///
SceneManifest BuildManifest(ConfigurationExport.SectionSummary section,
ConfigurationExport export,
string language)
{
var model = export?.FindResource(section.Model3DResourceId);
var url = model?.Url ?? section.Model3DSource;
if (string.IsNullOrEmpty(url))
{
Debug.LogError($"[Model3D] {section.Id} : aucun modèle à afficher.");
return null;
}
var manifest = new SceneManifest
{
ManifestVersion = SceneManifest.SupportedManifestVersion,
SceneId = section.Id,
CoordinateSystem = SceneManifest.ExpectedCoordinateSystem,
DefaultLanguage = language,
World = new WorldManifest
{
Kind = "mesh",
AssetId = ModelAssetId,
Transform = WorldTransformFor(section.Scene3DMode)
},
Assets = new List
{
new SceneAsset
{
Id = ModelAssetId,
ResourceId = section.Model3DResourceId,
Url = url,
MimeType = "model/gltf-binary"
}
},
Hotspots = new List()
};
foreach (var point in section.Points)
{
if (point.LocalTransform == null) continue;
manifest.Hotspots.Add(new SceneHotspot
{
Id = $"poi-{point.Id}",
GeoPointId = point.Id ?? 0,
Transform = TransformOf(point.LocalTransform),
Title = Localized(point.Title),
Description = Localized(point.Description),
Contents = ContentsOf(point, manifest)
});
}
return manifest;
}
static TransformManifest TransformOf(ConfigurationExport.Position3D position)
{
var yaw = (position.RotationY ?? 0f) * Mathf.Deg2Rad / 2f;
return new TransformManifest
{
Position = new[] { position.X, position.Y, position.Z },
Rotation = new[] { 0f, Mathf.Sin(yaw), 0f, Mathf.Cos(yaw) },
Scale = new[] { 1f, 1f, 1f }
};
}
static List Localized(List texts)
{
var result = new List();
foreach (var text in texts ?? new List())
result.Add(new LocalizedText { Language = text.Language, Value = text.Value });
return result;
}
///
/// Les contenus d'un point portent leurs médias — c'est de là que vient le
/// commentaire audio. Chaque ressource entre aussi dans Assets : le
/// hotspot ne connaît qu'un identifiant, le manifeste seul porte les URL.
///
static List ContentsOf(ConfigurationExport.GeoPoint point,
SceneManifest manifest)
{
var contents = new List();
foreach (var content in point.Contents ?? new List())
{
var resource = content.Resource;
string assetId = null;
if (!string.IsNullOrEmpty(resource?.Url))
{
assetId = $"asset-{resource.Id}";
manifest.Assets.Add(new SceneAsset
{
Id = assetId,
ResourceId = resource.Id,
Url = resource.Url
});
}
contents.Add(new HotspotContent
{
Order = content.Order ?? 0,
Title = Localized(content.Title),
Description = Localized(content.Description),
AssetId = assetId
});
}
return contents;
}
void OnSelected(IAimTarget target)
{
if (!(target is MenuItemPanel panel) || panel.Id != "__back") return;
Interacted?.Invoke();
Closed?.Invoke();
}
}
}