using System.Collections.Generic; using Newtonsoft.Json; using UnityEngine; namespace MyInfoMate.Vr.Manifest { /// /// Miroir exact du schéma §2.2 de la conception. Ce fichier et /// viewer/src/manifest.ts décrivent la même chose : modifier l'un /// sans l'autre casse le test croisé de S3. /// /// Les coordonnées sont ici telles qu'écrites dans le manifeste, donc en /// convention glTF. Elles ne deviennent des coordonnées Unity qu'en passant par /// — nulle part ailleurs. /// public class SceneManifest { public const int SupportedManifestVersion = 1; public const string ExpectedCoordinateSystem = "gltf/y-up/right-handed/meters"; /// Version du format. Distincte de . public int ManifestVersion = 1; public string SceneId; public string InstanceId; public string ConfigurationId; /// Version du contenu : +1 à chaque publication. C'est elle que le /// cache compare pour savoir qu'il y a du nouveau. public int Version; public string PublishedAt; public List Languages = new List(); public string DefaultLanguage; public string CoordinateSystem; public NavigationManifest Navigation = new NavigationManifest(); public EnvironmentManifest Environment = new EnvironmentManifest(); public WorldManifest World; public List Objects = new List(); public List Personas = new List(); public List Hotspots = new List(); public List Assets = new List(); public BudgetManifest Budget; public ProvenanceManifest Provenance; [JsonIgnore] Dictionary _assetsById; /// /// assets[] est la seule liste que le téléchargeur parcourt (§2.3) : tout /// le reste ne porte que des identifiants. D'où cet index. /// public SceneAsset FindAsset(string assetId) { if (string.IsNullOrEmpty(assetId)) return null; if (_assetsById == null) { _assetsById = new Dictionary(); foreach (var asset in Assets) if (asset != null && !string.IsNullOrEmpty(asset.Id)) _assetsById[asset.Id] = asset; } return _assetsById.TryGetValue(assetId, out var found) ? found : null; } } /// /// Position + rotation + échelle en convention glTF. Les accesseurs Unity passent /// tous par : c'est ce qui garantit qu'il n'existe /// qu'une conversion dans le projet. /// public class TransformManifest { public float[] Position = { 0f, 0f, 0f }; public float[] Rotation = { 0f, 0f, 0f, 1f }; public float[] Scale = { 1f, 1f, 1f }; public Vector3 UnityPosition => Scene.GltfSpace.Position(Position ?? new[] { 0f, 0f, 0f }); public Quaternion UnityRotation => Scene.GltfSpace.Rotation(Rotation ?? new[] { 0f, 0f, 0f, 1f }); public Vector3 UnityScale => Scene.GltfSpace.Scale(Scale ?? new[] { 1f, 1f, 1f }); public void ApplyTo(Transform target) { target.localPosition = UnityPosition; target.localRotation = UnityRotation; target.localScale = UnityScale; } } public class NavigationManifest { /// Le plafond dur du §2.2, appliqué serveur et client. public const float MaxRadiusMeters = 3f; public TransformManifest Spawn = new TransformManifest(); public float RadiusMeters = MaxRadiusMeters; public bool ShowBoundary = true; public float ClampedRadiusMeters => Mathf.Clamp(RadiusMeters, 0.5f, MaxRadiusMeters); } public class EnvironmentManifest { public string LightingPreset = "neutral-indoor"; public string HdriAssetId; public float Exposure = 1f; } /// /// kind est la couture V2 : la V1 ne lit que et /// refuse le reste explicitement. Un refus lisible vaut mieux qu'une scène /// vide (§2.3). /// public class WorldManifest { public string Kind = "mesh"; public string AssetId; public TransformManifest Transform = new TransformManifest(); public World3DKind ParsedKind => Kind switch { "mesh" => World3DKind.Mesh, "splat" => World3DKind.Splat, "panorama" => World3DKind.Panorama, _ => World3DKind.Unknown }; } public enum World3DKind { Mesh, Splat, Panorama, Unknown } public class SceneObject { public string Id; public string Label; public string AssetId; public TransformManifest Transform = new TransformManifest(); } public class ScenePersona { public string Id; /// Null en V1 : la couture vers l'entité Persona du lot 7 Studio. public string PersonaId; public string Name; public string AssetId; public TransformManifest Transform = new TransformManifest(); public AnimationManifest Animation = new AnimationManifest(); public bool GazeAtVisitor = true; public List Audio = new List(); public List Script = new List(); } public class AnimationManifest { public string IdleClip = "Idle"; public bool Loop = true; } public class SceneHotspot { public string Id; /// Un hotspot est un GeoPoint côté serveur (§3.2). public int GeoPointId; public TransformManifest Transform = new TransformManifest(); public List Title = new List(); public List Description = new List(); public List Contents = new List(); } public class HotspotContent { public int Order; public List Title = new List(); public List Description = new List(); public string AssetId; } /// /// Toutes les langues arrivent d'un coup : un casque en borne change de langue à /// chaud, l'export mono-langue existant ne suffit pas (§2.3). /// public class LocalizedText { public string Language; public string Value; } public class LocalizedAsset { public string Language; public string AssetId; } public class SceneAsset { public string Id; public string ResourceId; public string Url; public string MimeType; public long SizeBytes; /// Le seul moyen d'un delta réel côté casque (§2.4). public string Sha256; /// KTX2 / Meshopt viendront ici, sans casser le contrat. public List Variants = new List(); } public class AssetVariant { public string Kind; public string Url; public string MimeType; public long SizeBytes; public string Sha256; } public class BudgetManifest { public long TotalBytes; public long LimitBytes; } public class ProvenanceManifest { public bool AiGenerated; public string RightsHolder; public List Sources = new List(); } }