Skip to main content

draco_gltf/
document.rs

1//! Lossless glTF document model and typed object views.
2//!
3//! The JSON DOM is authoritative: typed views deliberately never own a second
4//! schema copy, so draft fields and unknown extensions survive edits.
5
6use std::marker::PhantomData;
7
8use crate::json::Value;
9
10use crate::{Error, Result};
11
12macro_rules! index {
13    ($name:ident) => {
14        #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
15        #[doc = concat!("Typed index into the glTF `", stringify!($name), "[]` array.")]
16        pub struct $name(pub usize);
17        impl $name {
18            /// Returns the zero-based array position represented by this index.
19            pub const fn index(self) -> usize {
20                self.0
21            }
22        }
23    };
24}
25
26index!(AccessorIndex);
27index!(AnimationIndex);
28index!(BufferIndex);
29index!(BufferViewIndex);
30index!(CameraIndex);
31index!(ExternalAssetIndex);
32index!(FileIndex);
33index!(ImageIndex);
34index!(MaterialIndex);
35index!(MeshIndex);
36index!(NodeIndex);
37index!(SamplerIndex);
38index!(SceneIndex);
39index!(ShapeIndex);
40index!(SkinIndex);
41index!(TextureIndex);
42
43/// Specification profile used for strict validation and transformations.
44#[derive(Clone, Copy, Debug, PartialEq, Eq)]
45pub enum ValidationProfile {
46    /// Stable glTF 2.0 core.
47    Gltf20,
48    /// Pinned glTF 2.1 draft described in `GLTF_2_1_SNAPSHOT.md`.
49    Gltf21Draft,
50}
51
52/// Core accessor component type definitions, including the glTF 2.1 draft.
53#[derive(Clone, Copy, Debug, PartialEq, Eq)]
54pub enum ComponentType {
55    /// Signed 8-bit integer.
56    I8 = 5120,
57    /// Unsigned 8-bit integer.
58    U8 = 5121,
59    /// Signed 16-bit integer.
60    I16 = 5122,
61    /// Unsigned 16-bit integer.
62    U16 = 5123,
63    /// Unsigned 32-bit integer.
64    U32 = 5125,
65    /// 32-bit floating-point value.
66    F32 = 5126,
67    /// Signed 32-bit integer.
68    I32 = 5124,
69    /// 16-bit floating-point value from the draft profile.
70    F16 = 5131,
71    /// 64-bit floating-point value from the draft profile.
72    F64 = 5130,
73    /// Signed 64-bit integer from the draft profile.
74    I64 = 5134,
75    /// Unsigned 64-bit integer from the draft profile.
76    U64 = 5135,
77}
78
79impl ComponentType {
80    /// Converts a glTF numeric component type code to its typed representation.
81    pub fn from_gltf(value: u64) -> Option<Self> {
82        Some(match value {
83            5120 => Self::I8,
84            5121 => Self::U8,
85            5122 => Self::I16,
86            5123 => Self::U16,
87            5125 => Self::U32,
88            5126 => Self::F32,
89            5124 => Self::I32,
90            5131 => Self::F16,
91            5130 => Self::F64,
92            5134 => Self::I64,
93            5135 => Self::U64,
94            _ => return None,
95        })
96    }
97
98    /// Returns the on-disk width of one scalar component in bytes.
99    pub const fn byte_width(self) -> usize {
100        match self {
101            Self::I8 | Self::U8 => 1,
102            Self::I16 | Self::U16 | Self::F16 => 2,
103            Self::I32 | Self::U32 | Self::F32 => 4,
104            Self::F64 | Self::I64 | Self::U64 => 8,
105        }
106    }
107
108    /// Returns the numeric glTF component type code.
109    pub const fn to_gltf(self) -> u32 {
110        self as u32
111    }
112}
113
114/// Typed location of a primitive nested in one mesh.
115#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
116pub struct PrimitiveIndex {
117    /// Mesh containing the primitive.
118    pub mesh: MeshIndex,
119    /// Zero-based primitive position inside the mesh.
120    pub primitive: usize,
121}
122
123impl PrimitiveIndex {
124    /// Creates a typed nested primitive location.
125    pub const fn new(mesh: MeshIndex, primitive: usize) -> Self {
126        Self { mesh, primitive }
127    }
128}
129
130/// Semantically lossless glTF JSON document.
131#[derive(Clone, Debug)]
132pub struct Document {
133    root: Value,
134    original_json: Option<Vec<u8>>,
135}
136
137impl Document {
138    /// Parses a JSON document, retaining the exact source bytes until mutation.
139    ///
140    /// ```
141    /// # use draco_gltf::Document;
142    /// let bytes = br#"{"asset":{"version":"2.0"},"meshes":[]}"#;
143    /// let document = Document::from_json_bytes(bytes)?;
144    /// assert_eq!(document.meshes().len(), 0);
145    /// # Ok::<(), draco_gltf::Error>(())
146    /// ```
147    pub fn from_json_bytes(bytes: &[u8]) -> Result<Self> {
148        let root = Value::parse(bytes).map_err(Error::Json)?;
149        if !root.is_object() {
150            return Err(Error::Validation(vec!["glTF root is not an object".into()]));
151        }
152        Ok(Self {
153            root,
154            original_json: Some(bytes.to_vec()),
155        })
156    }
157
158    /// Creates a document from a JSON value.
159    pub fn from_value(root: Value) -> Result<Self> {
160        if !root.is_object() {
161            return Err(Error::Validation(vec!["glTF root is not an object".into()]));
162        }
163        Ok(Self {
164            root,
165            original_json: None,
166        })
167    }
168
169    /// Returns the complete lossless JSON value.
170    pub fn as_value(&self) -> &Value {
171        &self.root
172    }
173
174    /// Returns mutable JSON and marks the source representation dirty.
175    pub fn as_value_mut(&mut self) -> &mut Value {
176        self.original_json = None;
177        &mut self.root
178    }
179
180    /// Serializes JSON, preserving original bytes when the document is untouched.
181    ///
182    /// ```
183    /// # use draco_gltf::Document;
184    /// let source = br#"{ "asset": { "version": "2.0" } }"#;
185    /// let document = Document::from_json_bytes(source)?;
186    /// assert_eq!(document.to_json_bytes()?, source);
187    /// # Ok::<(), draco_gltf::Error>(())
188    /// ```
189    pub fn to_json_bytes(&self) -> Result<Vec<u8>> {
190        match &self.original_json {
191            Some(bytes) => Ok(bytes.clone()),
192            None => Ok(self.root.to_vec()),
193        }
194    }
195
196    /// Serializes the current DOM without insignificant JSON whitespace.
197    ///
198    /// Unlike [`Document::to_json_bytes`], this always serializes the parsed
199    /// value, even when the document has not been changed. Object order and
200    /// number lexemes are retained; keys and numbers are not normalized.
201    ///
202    /// ```
203    /// # use draco_gltf::Document;
204    /// let document = Document::from_json_bytes(
205    ///     br#"{ "asset": { "version": "2.0" } }"#,
206    /// )?;
207    /// assert_eq!(
208    ///     document.to_minified_json_bytes(),
209    ///     br#"{"asset":{"version":"2.0"}}"#,
210    /// );
211    /// # Ok::<(), draco_gltf::Error>(())
212    /// ```
213    pub fn to_minified_json_bytes(&self) -> Vec<u8> {
214        self.root.to_vec()
215    }
216
217    /// Performs basic structural checks, plus strict graph checks when enabled.
218    ///
219    /// Enable the `strict-validation` feature to additionally check core
220    /// references, node trees, and finite ordered `POSITION` bounds. Compact
221    /// readers retain local bounds checks while materializing individual data.
222    pub fn validate(&self, profile: ValidationProfile) -> Result<()> {
223        let asset = self
224            .root
225            .get("asset")
226            .ok_or_else(|| Error::Validation(vec!["asset is missing or not an object".into()]))?;
227        let version = asset
228            .get("version")
229            .and_then(Value::as_str)
230            .ok_or_else(|| {
231                Error::Validation(vec!["asset.version is missing or not a string".into()])
232            })?;
233        match profile {
234            ValidationProfile::Gltf20 if !version.starts_with("2.0") => {
235                return Err(Error::Validation(vec![format!(
236                    "asset.version {version:?} is not glTF 2.0"
237                )]))
238            }
239            ValidationProfile::Gltf21Draft if !version.starts_with("2.") => {
240                return Err(Error::Validation(vec![format!(
241                    "asset.version {version:?} is not glTF 2.x"
242                )]))
243            }
244            _ => {}
245        }
246        for name in [
247            "accessors",
248            "animations",
249            "buffers",
250            "bufferViews",
251            "cameras",
252            "externalAssets",
253            "files",
254            "images",
255            "materials",
256            "meshes",
257            "nodes",
258            "samplers",
259            "scenes",
260            "shapes",
261            "skins",
262            "textures",
263        ] {
264            if let Some(value) = self.root.get(name) {
265                let array = value
266                    .as_array()
267                    .ok_or_else(|| Error::Validation(vec![format!("{name} is not an array")]))?;
268                if array.iter().any(|object| !object.is_object()) {
269                    return Err(Error::Validation(vec![format!(
270                        "{name} contains a non-object entry"
271                    )]));
272                }
273            }
274        }
275        if profile == ValidationProfile::Gltf20
276            && (self.root.get("externalAssets").is_some()
277                || self.root.get("files").is_some()
278                || self.root.get("shapes").is_some())
279        {
280            return Err(Error::Validation(vec![
281                "glTF 2.1 fields require the draft profile".into(),
282            ]));
283        }
284        #[cfg(feature = "strict-validation")]
285        validate_references(&self.root, profile)?;
286        Ok(())
287    }
288
289    /// Iterates over accessor objects in document order.
290    pub fn accessors(&self) -> Objects<'_, AccessorIndex> {
291        self.objects("accessors")
292    }
293    /// Returns one accessor view by typed index.
294    pub fn accessor(&self, index: AccessorIndex) -> Option<Accessor<'_>> {
295        self.accessors().get(index).map(Accessor)
296    }
297    /// Iterates over animation objects in document order.
298    pub fn animations(&self) -> Objects<'_, AnimationIndex> {
299        self.objects("animations")
300    }
301    /// Returns one animation view by typed index.
302    pub fn animation(&self, index: AnimationIndex) -> Option<Animation<'_>> {
303        self.animations().get(index).map(Animation)
304    }
305    /// Iterates over buffer objects in document order.
306    pub fn buffers(&self) -> Objects<'_, BufferIndex> {
307        self.objects("buffers")
308    }
309    /// Returns one buffer view by typed index.
310    pub fn buffer(&self, index: BufferIndex) -> Option<Buffer<'_>> {
311        self.buffers().get(index).map(Buffer)
312    }
313    /// Iterates over buffer-view objects in document order.
314    pub fn buffer_views(&self) -> Objects<'_, BufferViewIndex> {
315        self.objects("bufferViews")
316    }
317    /// Returns one buffer-view by typed index.
318    pub fn buffer_view(&self, index: BufferViewIndex) -> Option<BufferView<'_>> {
319        self.buffer_views().get(index).map(BufferView)
320    }
321    /// Iterates over camera objects in document order.
322    pub fn cameras(&self) -> Objects<'_, CameraIndex> {
323        self.objects("cameras")
324    }
325    /// Iterates over draft external-asset declarations.
326    pub fn external_assets(&self) -> Objects<'_, ExternalAssetIndex> {
327        self.objects("externalAssets")
328    }
329    /// Returns one external-asset declaration by typed index.
330    pub fn external_asset(&self, index: ExternalAssetIndex) -> Option<ExternalAsset<'_>> {
331        self.external_assets().get(index).map(ExternalAsset)
332    }
333    /// Returns one camera view by typed index.
334    pub fn camera(&self, index: CameraIndex) -> Option<Camera<'_>> {
335        self.cameras().get(index).map(Camera)
336    }
337    /// Iterates over draft packaged-file declarations.
338    pub fn files(&self) -> Objects<'_, FileIndex> {
339        self.objects("files")
340    }
341    /// Returns one packaged-file view by typed index.
342    pub fn file(&self, index: FileIndex) -> Option<File<'_>> {
343        self.files().get(index).map(File)
344    }
345    /// Iterates over image objects in document order.
346    pub fn images(&self) -> Objects<'_, ImageIndex> {
347        self.objects("images")
348    }
349    /// Returns one image view by typed index.
350    pub fn image(&self, index: ImageIndex) -> Option<Image<'_>> {
351        self.images().get(index).map(Image)
352    }
353    /// Iterates over material objects in document order.
354    pub fn materials(&self) -> Objects<'_, MaterialIndex> {
355        self.objects("materials")
356    }
357    /// Returns one material view by typed index.
358    pub fn material(&self, index: MaterialIndex) -> Option<Material<'_>> {
359        self.materials().get(index).map(Material)
360    }
361    /// Iterates over mesh objects in document order.
362    pub fn meshes(&self) -> Objects<'_, MeshIndex> {
363        self.objects("meshes")
364    }
365    /// Returns one mesh view by typed index.
366    pub fn mesh(&self, index: MeshIndex) -> Option<Mesh<'_>> {
367        self.meshes().get(index).map(Mesh)
368    }
369    /// Iterates over node objects in document order.
370    pub fn nodes(&self) -> Objects<'_, NodeIndex> {
371        self.objects("nodes")
372    }
373    /// Returns one node view by typed index.
374    pub fn node(&self, index: NodeIndex) -> Option<Node<'_>> {
375        self.nodes().get(index).map(Node)
376    }
377    /// Iterates over sampler objects in document order.
378    pub fn samplers(&self) -> Objects<'_, SamplerIndex> {
379        self.objects("samplers")
380    }
381    /// Returns one sampler view by typed index.
382    pub fn sampler(&self, index: SamplerIndex) -> Option<Sampler<'_>> {
383        self.samplers().get(index).map(Sampler)
384    }
385    /// Iterates over scene objects in document order.
386    pub fn scenes(&self) -> Objects<'_, SceneIndex> {
387        self.objects("scenes")
388    }
389    /// Returns one scene view by typed index.
390    pub fn scene(&self, index: SceneIndex) -> Option<Scene<'_>> {
391        self.scenes().get(index).map(Scene)
392    }
393    /// Returns the document's preferred scene index, if declared.
394    pub fn default_scene(&self) -> Option<SceneIndex> {
395        index_value(&self.root, "scene").map(SceneIndex)
396    }
397    /// Returns the optional draft thumbnail image declared by `asset.thumbnail`.
398    pub fn thumbnail(&self) -> Option<ImageIndex> {
399        self.root
400            .get("asset")
401            .and_then(|asset| index_value(asset, "thumbnail"))
402            .map(ImageIndex)
403    }
404    /// Iterates over draft shape declarations.
405    pub fn shapes(&self) -> Objects<'_, ShapeIndex> {
406        self.objects("shapes")
407    }
408    /// Returns one shape view by typed index.
409    pub fn shape(&self, index: ShapeIndex) -> Option<Shape<'_>> {
410        self.shapes().get(index).map(Shape)
411    }
412    /// Iterates over skin objects in document order.
413    pub fn skins(&self) -> Objects<'_, SkinIndex> {
414        self.objects("skins")
415    }
416    /// Returns one skin view by typed index.
417    pub fn skin(&self, index: SkinIndex) -> Option<Skin<'_>> {
418        self.skins().get(index).map(Skin)
419    }
420    /// Iterates over texture objects in document order.
421    pub fn textures(&self) -> Objects<'_, TextureIndex> {
422        self.objects("textures")
423    }
424    /// Returns one texture view by typed index.
425    pub fn texture(&self, index: TextureIndex) -> Option<Texture<'_>> {
426        self.textures().get(index).map(Texture)
427    }
428
429    /// Returns a primitive addressed by stable mesh and primitive indices.
430    pub fn primitive(&self, mesh: MeshIndex, primitive: usize) -> Option<PrimitiveRef<'_>> {
431        self.meshes()
432            .get(mesh)?
433            .value()
434            .get("primitives")?
435            .as_array()?
436            .get(primitive)?;
437        Some(PrimitiveRef {
438            document: self,
439            mesh,
440            primitive,
441        })
442    }
443
444    fn objects<I>(&self, key: &'static str) -> Objects<'_, I> {
445        Objects {
446            values: self.root.get(key).and_then(Value::as_array).unwrap_or(&[]),
447            marker: PhantomData,
448        }
449    }
450}
451
452#[cfg(feature = "strict-validation")]
453fn validate_references(root: &Value, profile: ValidationProfile) -> Result<()> {
454    let len = |name: &str| -> usize {
455        root.get(name)
456            .and_then(Value::as_array)
457            .map_or(0, <[Value]>::len)
458    };
459    let check = |value: &Value, field: &str, target: &str| -> Result<()> {
460        if let Some(raw) = value.get(field) {
461            let index = raw
462                .as_u64()
463                .ok_or_else(|| Error::Validation(vec![format!("{field} is not an index")]))?;
464            let index = usize::try_from(index).map_err(|_| {
465                Error::Validation(vec![format!("{field} does not fit the platform index")])
466            })?;
467            if index >= len(target) {
468                return Err(Error::Validation(vec![format!(
469                    "{field} references missing {target}[{index}]"
470                )]));
471            }
472        }
473        Ok(())
474    };
475    let required_index = |value: &Value, field: &str, target: &str| -> Result<()> {
476        if value.get(field).is_none() {
477            return Err(Error::Validation(vec![format!("{field} is missing")]));
478        }
479        check(value, field, target)
480    };
481    for view in root
482        .get("bufferViews")
483        .and_then(Value::as_array)
484        .unwrap_or(&[])
485    {
486        check(view, "buffer", "buffers")?;
487    }
488    for accessor in root
489        .get("accessors")
490        .and_then(Value::as_array)
491        .unwrap_or(&[])
492    {
493        check(accessor, "bufferView", "bufferViews")?;
494        let component = accessor
495            .get("componentType")
496            .and_then(Value::as_u64)
497            .ok_or_else(|| Error::Validation(vec!["accessor componentType is missing".into()]))?;
498        let component = ComponentType::from_gltf(component).ok_or_else(|| {
499            Error::Validation(vec![format!(
500                "unsupported accessor componentType {component}"
501            )])
502        })?;
503        if profile == ValidationProfile::Gltf20
504            && !matches!(
505                component,
506                ComponentType::I8
507                    | ComponentType::U8
508                    | ComponentType::I16
509                    | ComponentType::U16
510                    | ComponentType::U32
511                    | ComponentType::F32
512            )
513        {
514            return Err(Error::Validation(vec![format!(
515                "accessor componentType {component:?} requires the glTF 2.1 draft profile"
516            )]));
517        }
518        let kind = accessor
519            .get("type")
520            .and_then(Value::as_str)
521            .ok_or_else(|| Error::Validation(vec!["accessor type is missing".into()]))?;
522        if !matches!(
523            kind,
524            "SCALAR" | "VEC2" | "VEC3" | "VEC4" | "MAT2" | "MAT3" | "MAT4"
525        ) {
526            return Err(Error::Validation(vec![format!(
527                "unsupported accessor type {kind:?}"
528            )]));
529        }
530        if accessor.get("count").and_then(Value::as_u64).is_none() {
531            return Err(Error::Validation(vec![
532                "accessor count is missing or invalid".into(),
533            ]));
534        }
535    }
536    for image in root.get("images").and_then(Value::as_array).unwrap_or(&[]) {
537        check(image, "bufferView", "bufferViews")?;
538    }
539    if let Some(asset) = root.get("asset") {
540        check(asset, "thumbnail", "images")?;
541    }
542    for file in root.get("files").and_then(Value::as_array).unwrap_or(&[]) {
543        if file.get("mimeType").and_then(Value::as_str).is_none() {
544            return Err(Error::Validation(vec![
545                "file mimeType is missing or not a string".into(),
546            ]));
547        }
548        let has_uri = match file.get("uri") {
549            Some(value) if value.as_str().is_some() => true,
550            Some(_) => {
551                return Err(Error::Validation(vec!["file uri is not a string".into()]));
552            }
553            None => false,
554        };
555        let has_buffer_view = match file.get("bufferView") {
556            Some(value) if value.as_u64().is_some() => {
557                check(file, "bufferView", "bufferViews")?;
558                true
559            }
560            Some(_) => {
561                return Err(Error::Validation(vec![
562                    "file bufferView is not an index".into()
563                ]));
564            }
565            None => false,
566        };
567        if has_uri == has_buffer_view {
568            return Err(Error::Validation(vec![
569                "file must contain exactly one of uri or bufferView".into(),
570            ]));
571        }
572    }
573    for asset in root
574        .get("externalAssets")
575        .and_then(Value::as_array)
576        .unwrap_or(&[])
577    {
578        if asset.get("file").and_then(Value::as_u64).is_none() {
579            return Err(Error::Validation(vec![
580                "external asset file is missing or not an index".into(),
581            ]));
582        }
583        check(asset, "file", "files")?;
584    }
585    for texture in root
586        .get("textures")
587        .and_then(Value::as_array)
588        .unwrap_or(&[])
589    {
590        check(texture, "sampler", "samplers")?;
591        check(texture, "source", "images")?;
592    }
593    for mesh in root.get("meshes").and_then(Value::as_array).unwrap_or(&[]) {
594        for primitive in mesh
595            .get("primitives")
596            .and_then(Value::as_array)
597            .unwrap_or(&[])
598        {
599            check(primitive, "indices", "accessors")?;
600            check(primitive, "material", "materials")?;
601            if let Some(attributes) = primitive.get("attributes").and_then(Value::as_object) {
602                for (semantic, index) in attributes {
603                    let index = index.as_u64().ok_or_else(|| {
604                        Error::Validation(vec![format!(
605                            "attribute {semantic} is not an accessor index"
606                        )])
607                    })?;
608                    if usize::try_from(index)
609                        .ok()
610                        .is_none_or(|index| index >= len("accessors"))
611                    {
612                        return Err(Error::Validation(vec![format!(
613                            "attribute {semantic} references missing accessors[{index}]"
614                        )]));
615                    }
616                    if semantic == "POSITION" {
617                        validate_position_accessor(
618                            root,
619                            usize::try_from(index).expect("accessor index was range checked"),
620                        )?;
621                    }
622                }
623            }
624        }
625    }
626    for node in root.get("nodes").and_then(Value::as_array).unwrap_or(&[]) {
627        for field in ["camera", "mesh", "skin"] {
628            let target = match field {
629                "camera" => "cameras",
630                "mesh" => "meshes",
631                _ => "skins",
632            };
633            check(node, field, target)?;
634        }
635        check(node, "externalAsset", "externalAssets")?;
636        if let Some(volume) = node.get("boundingVolume") {
637            if !volume.is_object() {
638                return Err(Error::Validation(vec![
639                    "node boundingVolume is not an object".into(),
640                ]));
641            }
642            if volume.get("shape").and_then(Value::as_u64).is_none() {
643                return Err(Error::Validation(vec![
644                    "node boundingVolume shape is missing or not an index".into(),
645                ]));
646            }
647            check(volume, "shape", "shapes")?;
648        }
649        if let Some(children) = node.get("children").and_then(Value::as_array) {
650            for child in children {
651                if child.as_u64().is_none_or(|index| {
652                    usize::try_from(index)
653                        .ok()
654                        .is_none_or(|index| index >= len("nodes"))
655                }) {
656                    return Err(Error::Validation(vec![
657                        "node child references a missing node".into(),
658                    ]));
659                }
660            }
661        }
662    }
663    for scene in root.get("scenes").and_then(Value::as_array).unwrap_or(&[]) {
664        if let Some(nodes) = scene.get("nodes").and_then(Value::as_array) {
665            for node in nodes {
666                if node.as_u64().is_none_or(|index| {
667                    usize::try_from(index)
668                        .ok()
669                        .is_none_or(|index| index >= len("nodes"))
670                }) {
671                    return Err(Error::Validation(vec![
672                        "scene references a missing node".into()
673                    ]));
674                }
675            }
676        }
677    }
678    validate_node_hierarchy(root)?;
679    check(root, "scene", "scenes")?;
680    for skin in root.get("skins").and_then(Value::as_array).unwrap_or(&[]) {
681        check(skin, "inverseBindMatrices", "accessors")?;
682        check(skin, "skeleton", "nodes")?;
683        if let Some(joints) = skin.get("joints").and_then(Value::as_array) {
684            for joint in joints {
685                if joint.as_u64().is_none_or(|index| {
686                    usize::try_from(index)
687                        .ok()
688                        .is_none_or(|index| index >= len("nodes"))
689                }) {
690                    return Err(Error::Validation(vec![
691                        "skin references a missing joint node".into(),
692                    ]));
693                }
694            }
695        }
696    }
697    for mesh in root.get("meshes").and_then(Value::as_array).unwrap_or(&[]) {
698        for primitive in mesh
699            .get("primitives")
700            .and_then(Value::as_array)
701            .unwrap_or(&[])
702        {
703            for target in primitive
704                .get("targets")
705                .and_then(Value::as_array)
706                .unwrap_or(&[])
707            {
708                if let Some(attributes) = target.as_object() {
709                    for (semantic, index) in attributes {
710                        if index.as_u64().is_none_or(|index| {
711                            usize::try_from(index)
712                                .ok()
713                                .is_none_or(|index| index >= len("accessors"))
714                        }) {
715                            return Err(Error::Validation(vec![format!(
716                                "morph target attribute {semantic} references a missing accessor"
717                            )]));
718                        }
719                    }
720                }
721            }
722        }
723    }
724    for animation in root
725        .get("animations")
726        .and_then(Value::as_array)
727        .unwrap_or(&[])
728    {
729        let samplers = animation
730            .get("samplers")
731            .and_then(Value::as_array)
732            .unwrap_or(&[]);
733        for sampler in samplers {
734            required_index(sampler, "input", "accessors")?;
735            required_index(sampler, "output", "accessors")?;
736            if let Some(interpolation) = sampler.get("interpolation").and_then(Value::as_str) {
737                if !matches!(interpolation, "LINEAR" | "STEP" | "CUBICSPLINE") {
738                    return Err(Error::Validation(vec![format!(
739                        "animation sampler interpolation {interpolation:?} is invalid"
740                    )]));
741                }
742            }
743        }
744        for channel in animation
745            .get("channels")
746            .and_then(Value::as_array)
747            .unwrap_or(&[])
748        {
749            let sampler = channel.get("sampler").and_then(Value::as_u64);
750            if sampler.is_none_or(|index| {
751                usize::try_from(index)
752                    .ok()
753                    .is_none_or(|index| index >= samplers.len())
754            }) {
755                return Err(Error::Validation(vec![
756                    "animation channel references a missing sampler".into(),
757                ]));
758            }
759            if let Some(target) = channel.get("target") {
760                check(target, "node", "nodes")?;
761                let path = target.get("path").and_then(Value::as_str).ok_or_else(|| {
762                    Error::Validation(vec!["animation channel target path is missing".into()])
763                })?;
764                // `pointer` is the path KHR_animation_pointer defines, and it
765                // targets a JSON pointer rather than a node, which is why the
766                // node index is absent on such a channel. The extension is
767                // ratified, so a file using it is valid whether or not a
768                // reader animates it: rejecting the document would lose the
769                // geometry over an animation the reader was free to skip.
770                let pointed = path == "pointer"
771                    && target
772                        .get("extensions")
773                        .and_then(|extensions| extensions.get("KHR_animation_pointer"))
774                        .is_some();
775                if !pointed && !matches!(path, "translation" | "rotation" | "scale" | "weights") {
776                    return Err(Error::Validation(vec![format!(
777                        "animation channel target path {path:?} is invalid"
778                    )]));
779                }
780            } else {
781                return Err(Error::Validation(vec![
782                    "animation channel target is missing".into(),
783                ]));
784            }
785        }
786    }
787    validate_draco_extension(root, &check)?;
788    if profile == ValidationProfile::Gltf21Draft {
789        validate_shapes(root)?;
790        validate_uids(root)?;
791    }
792    Ok(())
793}
794
795#[cfg(feature = "strict-validation")]
796fn validate_position_accessor(root: &Value, index: usize) -> Result<()> {
797    let accessor = root
798        .get("accessors")
799        .and_then(Value::as_array)
800        .and_then(|accessors| accessors.get(index))
801        .ok_or_else(|| Error::Validation(vec![format!("POSITION accessor {index} is missing")]))?;
802    if accessor.get("type").and_then(Value::as_str) != Some("VEC3") {
803        return Err(Error::Validation(vec![format!(
804            "POSITION accessor {index} must have type VEC3"
805        )]));
806    }
807
808    let mut bounds = Vec::with_capacity(2);
809    for field in ["min", "max"] {
810        let values = accessor
811            .get(field)
812            .and_then(Value::as_array)
813            .filter(|values| values.len() == 3)
814            .ok_or_else(|| {
815                Error::Validation(vec![format!(
816                    "POSITION accessor {index} must define three-component {field} bounds"
817                )])
818            })?;
819        let values = values
820            .iter()
821            .map(|value| value.as_f64().filter(|value| value.is_finite()))
822            .collect::<Option<Vec<_>>>()
823            .ok_or_else(|| {
824                Error::Validation(vec![format!(
825                    "POSITION accessor {index} {field} bounds must be finite numbers"
826                )])
827            })?;
828        bounds.push(values);
829    }
830    if bounds[0].iter().zip(&bounds[1]).any(|(min, max)| min > max) {
831        return Err(Error::Validation(vec![format!(
832            "POSITION accessor {index} min bounds exceed max bounds"
833        )]));
834    }
835    Ok(())
836}
837
838#[cfg(feature = "strict-validation")]
839fn validate_node_hierarchy(root: &Value) -> Result<()> {
840    let nodes = root.get("nodes").and_then(Value::as_array).unwrap_or(&[]);
841    let mut parents = vec![None; nodes.len()];
842    let mut edges = vec![Vec::new(); nodes.len()];
843
844    for (parent, node) in nodes.iter().enumerate() {
845        for child in node
846            .get("children")
847            .and_then(Value::as_array)
848            .unwrap_or(&[])
849        {
850            let child = child
851                .as_u64()
852                .and_then(|value| usize::try_from(value).ok())
853                .filter(|child| *child < nodes.len())
854                .ok_or_else(|| {
855                    Error::Validation(vec!["node child references a missing node".into()])
856                })?;
857            if let Some(existing) = parents[child] {
858                let message = if existing == parent {
859                    format!("nodes[{parent}] lists child nodes[{child}] more than once")
860                } else {
861                    format!(
862                        "nodes[{child}] has multiple parents: nodes[{existing}] and nodes[{parent}]"
863                    )
864                };
865                return Err(Error::Validation(vec![message]));
866            }
867            parents[child] = Some(parent);
868            edges[parent].push(child);
869        }
870    }
871
872    // Iterative depth-first traversal avoids consuming the call stack on large
873    // scenes while still detecting back edges in disconnected node trees.
874    let mut state = vec![0u8; nodes.len()];
875    for start in 0..nodes.len() {
876        if state[start] != 0 {
877            continue;
878        }
879        state[start] = 1;
880        let mut stack = vec![(start, 0usize)];
881        while let Some((node, next_child)) = stack.last_mut() {
882            if *next_child == edges[*node].len() {
883                state[*node] = 2;
884                stack.pop();
885                continue;
886            }
887            let child = edges[*node][*next_child];
888            *next_child += 1;
889            match state[child] {
890                0 => {
891                    state[child] = 1;
892                    stack.push((child, 0));
893                }
894                1 => {
895                    return Err(Error::Validation(vec![format!(
896                        "node hierarchy contains a cycle through nodes[{child}]"
897                    )]))
898                }
899                _ => {}
900            }
901        }
902    }
903
904    for (scene_index, scene) in root
905        .get("scenes")
906        .and_then(Value::as_array)
907        .unwrap_or(&[])
908        .iter()
909        .enumerate()
910    {
911        for root_node in scene.get("nodes").and_then(Value::as_array).unwrap_or(&[]) {
912            let root_node = root_node
913                .as_u64()
914                .and_then(|value| usize::try_from(value).ok())
915                .filter(|node| *node < nodes.len())
916                .ok_or_else(|| Error::Validation(vec!["scene references a missing node".into()]))?;
917            if let Some(parent) = parents[root_node] {
918                return Err(Error::Validation(vec![format!(
919                    "scenes[{scene_index}] uses nodes[{root_node}] as a root, but it is a child of nodes[{parent}]"
920                )]));
921            }
922        }
923    }
924
925    Ok(())
926}
927
928#[cfg(feature = "strict-validation")]
929fn validate_draco_extension(
930    root: &Value,
931    check: &impl Fn(&Value, &str, &str) -> Result<()>,
932) -> Result<()> {
933    const NAME: &str = crate::KHR_DRACO_MESH_COMPRESSION;
934    let listed = |field: &str| {
935        root.get(field)
936            .and_then(Value::as_array)
937            .is_some_and(|values| values.iter().any(|value| value.as_str() == Some(NAME)))
938    };
939    let required = listed("extensionsRequired");
940    for mesh in root.get("meshes").and_then(Value::as_array).unwrap_or(&[]) {
941        for primitive in mesh
942            .get("primitives")
943            .and_then(Value::as_array)
944            .unwrap_or(&[])
945        {
946            let Some(extension) = primitive
947                .get("extensions")
948                .and_then(|value| value.get(NAME))
949            else {
950                continue;
951            };
952            if !listed("extensionsUsed") {
953                return Err(Error::Validation(vec![
954                    "KHR_draco_mesh_compression is missing from extensionsUsed".into(),
955                ]));
956            }
957            let mode = primitive.get("mode").and_then(Value::as_u64).unwrap_or(4);
958            if !matches!(mode, 4 | 5) {
959                return Err(Error::Validation(vec![
960                    "KHR_draco_mesh_compression requires TRIANGLES or TRIANGLE_STRIP".into(),
961                ]));
962            }
963            check(extension, "bufferView", "bufferViews")?;
964            let attributes = extension
965                .get("attributes")
966                .and_then(Value::as_object)
967                .ok_or_else(|| {
968                    Error::Validation(vec!["Draco extension attributes is not an object".into()])
969                })?;
970            let primitive_attributes = primitive
971                .get("attributes")
972                .and_then(Value::as_object)
973                .ok_or_else(|| {
974                    Error::Validation(vec!["Draco primitive attributes is not an object".into()])
975                })?;
976            let mut unique_ids = std::collections::BTreeSet::new();
977            for (semantic, unique_id) in attributes {
978                let unique_id = unique_id
979                    .as_u64()
980                    .and_then(|value| u32::try_from(value).ok())
981                    .ok_or_else(|| {
982                        Error::Validation(vec![format!(
983                            "Draco attribute {semantic:?} unique id is not a u32"
984                        )])
985                    })?;
986                if !unique_ids.insert(unique_id) {
987                    return Err(Error::Validation(vec![format!(
988                        "Draco unique id {unique_id} is mapped more than once"
989                    )]));
990                }
991                if primitive_attributes
992                    .iter()
993                    .all(|(name, _)| name != semantic)
994                {
995                    return Err(Error::Validation(vec![format!(
996                        "Draco attribute {semantic:?} is absent from primitive attributes"
997                    )]));
998                }
999            }
1000            if required {
1001                for (semantic, _) in attributes {
1002                    let accessor = primitive_attributes
1003                        .iter()
1004                        .find(|(name, _)| name == semantic)
1005                        .and_then(|(_, value)| value.as_u64())
1006                        .and_then(|value| usize::try_from(value).ok())
1007                        .and_then(|index| {
1008                            root.get("accessors").and_then(Value::as_array)?.get(index)
1009                        })
1010                        .ok_or_else(|| {
1011                            Error::Validation(vec!["Draco accessor is invalid".into()])
1012                        })?;
1013                    if accessor.get("bufferView").is_some() || accessor.get("sparse").is_some() {
1014                        return Err(Error::Validation(vec![
1015                            "Draco-only accessor must not retain raw buffer data".into(),
1016                        ]));
1017                    }
1018                }
1019                if let Some(index) = primitive.get("indices") {
1020                    let accessor = index
1021                        .as_u64()
1022                        .and_then(|value| usize::try_from(value).ok())
1023                        .and_then(|index| {
1024                            root.get("accessors").and_then(Value::as_array)?.get(index)
1025                        })
1026                        .ok_or_else(|| {
1027                            Error::Validation(vec!["Draco index accessor is invalid".into()])
1028                        })?;
1029                    if accessor.get("bufferView").is_some() || accessor.get("sparse").is_some() {
1030                        return Err(Error::Validation(vec![
1031                            "Draco-only index accessor must not retain raw buffer data".into(),
1032                        ]));
1033                    }
1034                }
1035            }
1036        }
1037    }
1038    Ok(())
1039}
1040
1041#[cfg(feature = "strict-validation")]
1042fn validate_shapes(root: &Value) -> Result<()> {
1043    const CORE_TYPES: [&str; 5] = ["box", "capsule", "cylinder", "plane", "sphere"];
1044    for shape in root.get("shapes").and_then(Value::as_array).unwrap_or(&[]) {
1045        let kind = shape.get("type").and_then(Value::as_str).ok_or_else(|| {
1046            Error::Validation(vec!["shape type is missing or not a string".into()])
1047        })?;
1048        if CORE_TYPES.contains(&kind) && !shape.get(kind).is_some_and(Value::is_object) {
1049            return Err(Error::Validation(vec![format!(
1050                "shape {kind:?} is missing its {kind:?} definition object"
1051            )]));
1052        }
1053    }
1054    Ok(())
1055}
1056
1057#[cfg(feature = "strict-validation")]
1058fn validate_uids(root: &Value) -> Result<()> {
1059    use std::collections::BTreeMap;
1060
1061    let mut names = BTreeMap::new();
1062    let mut uids = BTreeMap::new();
1063    for kind in [
1064        "accessors",
1065        "animations",
1066        "buffers",
1067        "bufferViews",
1068        "cameras",
1069        "externalAssets",
1070        "files",
1071        "images",
1072        "materials",
1073        "meshes",
1074        "nodes",
1075        "samplers",
1076        "scenes",
1077        "shapes",
1078        "skins",
1079        "textures",
1080    ] {
1081        for (index, value) in root
1082            .get(kind)
1083            .and_then(Value::as_array)
1084            .unwrap_or(&[])
1085            .iter()
1086            .enumerate()
1087        {
1088            let location = format!("{kind}[{index}]");
1089            if let Some(name) = value.get("name").and_then(Value::as_str) {
1090                names.insert(name, location.clone());
1091            }
1092            if let Some(uid) = value.get("uid") {
1093                let uid = uid.as_str().ok_or_else(|| {
1094                    Error::Validation(vec![format!("{location}.uid is not a string")])
1095                })?;
1096                if let Some(previous) = uids.insert(uid, location.clone()) {
1097                    return Err(Error::Validation(vec![format!(
1098                        "{location}.uid duplicates {previous}.uid"
1099                    )]));
1100                }
1101            }
1102        }
1103    }
1104    for (uid, location) in &uids {
1105        if let Some(named) = names.get(uid) {
1106            if named != location {
1107                return Err(Error::Validation(vec![format!(
1108                    "{location}.uid conflicts with {named}.name"
1109                )]));
1110            }
1111        }
1112    }
1113    Ok(())
1114}
1115
1116/// Typed view of a root-level glTF object.
1117#[derive(Clone, Copy)]
1118pub struct ObjectRef<'a, I> {
1119    index: I,
1120    value: &'a Value,
1121}
1122impl<'a, I: Copy> ObjectRef<'a, I> {
1123    /// Returns the typed index of this object.
1124    pub fn index(self) -> I {
1125        self.index
1126    }
1127    /// Returns the underlying lossless JSON object.
1128    pub fn value(self) -> &'a Value {
1129        self.value
1130    }
1131    /// Returns the optional glTF object name.
1132    pub fn name(self) -> Option<&'a str> {
1133        self.value.get("name").and_then(Value::as_str)
1134    }
1135    /// Returns the optional draft UID.
1136    pub fn uid(self) -> Option<&'a str> {
1137        self.value.get("uid").and_then(Value::as_str)
1138    }
1139    /// Returns the object's unknown or extension fields.
1140    pub fn extensions(self) -> Option<&'a [(String, Value)]> {
1141        self.value.get("extensions").and_then(Value::as_object)
1142    }
1143    /// Returns the object's application-defined extras value.
1144    pub fn extras(self) -> Option<&'a Value> {
1145        self.value.get("extras")
1146    }
1147}
1148
1149macro_rules! typed_object {
1150    ($name:ident, $index:ident) => {
1151        #[derive(Clone, Copy)]
1152        #[doc = concat!("Typed view of a glTF `", stringify!($name), "` object.")]
1153        pub struct $name<'a>(ObjectRef<'a, $index>);
1154        impl<'a> $name<'a> {
1155            /// Returns the typed index of this object.
1156            pub fn index(self) -> $index {
1157                self.0.index()
1158            }
1159            /// Returns the underlying lossless JSON object.
1160            pub fn value(self) -> &'a Value {
1161                self.0.value()
1162            }
1163            /// Returns the optional glTF object name.
1164            pub fn name(self) -> Option<&'a str> {
1165                self.0.name()
1166            }
1167            /// Returns the optional draft UID.
1168            pub fn uid(self) -> Option<&'a str> {
1169                self.0.uid()
1170            }
1171            /// Returns the object's application-defined extras value.
1172            pub fn extras(self) -> Option<&'a Value> {
1173                self.0.extras()
1174            }
1175            /// Returns the object's unknown or extension fields.
1176            pub fn extensions(self) -> Option<&'a [(String, Value)]> {
1177                self.0.extensions()
1178            }
1179        }
1180    };
1181}
1182
1183typed_object!(Accessor, AccessorIndex);
1184typed_object!(Animation, AnimationIndex);
1185typed_object!(Buffer, BufferIndex);
1186typed_object!(BufferView, BufferViewIndex);
1187typed_object!(Camera, CameraIndex);
1188typed_object!(ExternalAsset, ExternalAssetIndex);
1189typed_object!(File, FileIndex);
1190typed_object!(Image, ImageIndex);
1191typed_object!(Material, MaterialIndex);
1192typed_object!(Mesh, MeshIndex);
1193typed_object!(Node, NodeIndex);
1194typed_object!(Sampler, SamplerIndex);
1195typed_object!(Scene, SceneIndex);
1196typed_object!(Shape, ShapeIndex);
1197typed_object!(Skin, SkinIndex);
1198typed_object!(Texture, TextureIndex);
1199
1200impl<'a> Buffer<'a> {
1201    /// Returns the declared buffer length.
1202    pub fn byte_length(self) -> Option<u64> {
1203        self.value().get("byteLength").and_then(Value::as_u64)
1204    }
1205    /// Returns the optional external buffer URI.
1206    pub fn uri(self) -> Option<&'a str> {
1207        self.value().get("uri").and_then(Value::as_str)
1208    }
1209}
1210
1211impl<'a> BufferView<'a> {
1212    /// Returns the referenced buffer index.
1213    pub fn buffer(self) -> Option<BufferIndex> {
1214        index_value(self.value(), "buffer").map(BufferIndex)
1215    }
1216    /// Returns the byte offset, defaulting to zero when omitted.
1217    pub fn byte_offset(self) -> u64 {
1218        self.value()
1219            .get("byteOffset")
1220            .and_then(Value::as_u64)
1221            .unwrap_or(0)
1222    }
1223    /// Returns the declared view length.
1224    pub fn byte_length(self) -> Option<u64> {
1225        self.value().get("byteLength").and_then(Value::as_u64)
1226    }
1227    /// Returns the optional interleaved byte stride.
1228    pub fn byte_stride(self) -> Option<u64> {
1229        self.value().get("byteStride").and_then(Value::as_u64)
1230    }
1231}
1232
1233impl<'a> Accessor<'a> {
1234    /// Returns the optional source buffer-view index.
1235    pub fn buffer_view(self) -> Option<BufferViewIndex> {
1236        index_value(self.value(), "bufferView").map(BufferViewIndex)
1237    }
1238    /// Returns the number of accessor elements.
1239    pub fn count(self) -> Option<u64> {
1240        self.value().get("count").and_then(Value::as_u64)
1241    }
1242    /// Returns the typed component format.
1243    pub fn component_type(self) -> Option<ComponentType> {
1244        self.value()
1245            .get("componentType")
1246            .and_then(Value::as_u64)
1247            .and_then(ComponentType::from_gltf)
1248    }
1249    /// Returns the accessor shape such as `SCALAR` or `VEC3`.
1250    pub fn accessor_type(self) -> Option<&'a str> {
1251        self.value().get("type").and_then(Value::as_str)
1252    }
1253    /// Returns whether integer values are normalized on read.
1254    pub fn normalized(self) -> bool {
1255        matches!(self.value().get("normalized"), Some(Value::Bool(true)))
1256    }
1257}
1258
1259impl<'a> Image<'a> {
1260    /// Returns the optional image URI.
1261    pub fn uri(self) -> Option<&'a str> {
1262        self.value().get("uri").and_then(Value::as_str)
1263    }
1264    /// Returns the optional image buffer-view index.
1265    pub fn buffer_view(self) -> Option<BufferViewIndex> {
1266        index_value(self.value(), "bufferView").map(BufferViewIndex)
1267    }
1268}
1269
1270impl<'a> Texture<'a> {
1271    /// Returns the optional source image index.
1272    pub fn source(self) -> Option<ImageIndex> {
1273        index_value(self.value(), "source").map(ImageIndex)
1274    }
1275    /// Returns the optional sampler index.
1276    pub fn sampler(self) -> Option<SamplerIndex> {
1277        index_value(self.value(), "sampler").map(SamplerIndex)
1278    }
1279}
1280
1281impl<'a> Node<'a> {
1282    /// Returns the optional mesh index.
1283    pub fn mesh(self) -> Option<MeshIndex> {
1284        index_value(self.value(), "mesh").map(MeshIndex)
1285    }
1286    /// Returns the optional camera index.
1287    pub fn camera(self) -> Option<CameraIndex> {
1288        index_value(self.value(), "camera").map(CameraIndex)
1289    }
1290    /// Returns the optional skin index.
1291    pub fn skin(self) -> Option<SkinIndex> {
1292        index_value(self.value(), "skin").map(SkinIndex)
1293    }
1294    /// Returns the optional external-asset index.
1295    pub fn external_asset(self) -> Option<ExternalAssetIndex> {
1296        index_value(self.value(), "externalAsset").map(ExternalAssetIndex)
1297    }
1298    /// Returns the optional node bounding-volume view.
1299    pub fn bounding_volume(self) -> Option<BoundingVolume<'a>> {
1300        self.value()
1301            .get("boundingVolume")
1302            .filter(|value| value.is_object())
1303            .map(BoundingVolume)
1304    }
1305    /// Iterates over child node indexes.
1306    pub fn children(self) -> impl Iterator<Item = NodeIndex> + 'a {
1307        self.value()
1308            .get("children")
1309            .and_then(Value::as_array)
1310            .unwrap_or(&[])
1311            .iter()
1312            .filter_map(Value::as_u64)
1313            .filter_map(|index| usize::try_from(index).ok())
1314            .map(NodeIndex)
1315    }
1316}
1317
1318impl<'a> Scene<'a> {
1319    /// Iterates over the scene's root node indexes.
1320    pub fn nodes(self) -> impl Iterator<Item = NodeIndex> + 'a {
1321        self.value()
1322            .get("nodes")
1323            .and_then(Value::as_array)
1324            .unwrap_or(&[])
1325            .iter()
1326            .filter_map(Value::as_u64)
1327            .filter_map(|index| usize::try_from(index).ok())
1328            .map(NodeIndex)
1329    }
1330}
1331
1332impl<'a> File<'a> {
1333    /// Returns the declared file MIME type.
1334    pub fn mime_type(self) -> Option<&'a str> {
1335        self.value().get("mimeType").and_then(Value::as_str)
1336    }
1337    /// Returns the optional external file URI.
1338    pub fn uri(self) -> Option<&'a str> {
1339        self.value().get("uri").and_then(Value::as_str)
1340    }
1341    /// Returns the optional embedded buffer-view index.
1342    pub fn buffer_view(self) -> Option<BufferViewIndex> {
1343        index_value(self.value(), "bufferView").map(BufferViewIndex)
1344    }
1345}
1346
1347impl<'a> ExternalAsset<'a> {
1348    /// Returns the packaged file index backing this asset.
1349    pub fn file(self) -> Option<FileIndex> {
1350        index_value(self.value(), "file").map(FileIndex)
1351    }
1352}
1353
1354/// Typed view of a node's draft bounding-volume object.
1355#[derive(Clone, Copy)]
1356pub struct BoundingVolume<'a>(&'a Value);
1357impl<'a> BoundingVolume<'a> {
1358    /// Returns the underlying bounding-volume JSON object.
1359    pub fn value(self) -> &'a Value {
1360        self.0
1361    }
1362    /// Returns the referenced draft shape index.
1363    pub fn shape(self) -> Option<ShapeIndex> {
1364        index_value(self.0, "shape").map(ShapeIndex)
1365    }
1366}
1367
1368impl<'a> Shape<'a> {
1369    /// Returns the shape discriminator.
1370    pub fn shape_type(self) -> Option<&'a str> {
1371        self.value().get("type").and_then(Value::as_str)
1372    }
1373    /// Returns the shape definition under its discriminator key.
1374    pub fn definition(self) -> Option<&'a Value> {
1375        self.shape_type().and_then(|kind| self.value().get(kind))
1376    }
1377}
1378
1379impl<'a> Mesh<'a> {
1380    /// Returns the number of primitives in the mesh.
1381    pub fn primitive_count(self) -> usize {
1382        self.value()
1383            .get("primitives")
1384            .and_then(Value::as_array)
1385            .map_or(0, <[Value]>::len)
1386    }
1387}
1388
1389fn index_value(value: &Value, name: &str) -> Option<usize> {
1390    value
1391        .get(name)
1392        .and_then(Value::as_u64)
1393        .and_then(|index| usize::try_from(index).ok())
1394}
1395
1396/// Typed iterator over a root-level glTF array.
1397pub struct Objects<'a, I> {
1398    values: &'a [Value],
1399    marker: PhantomData<I>,
1400}
1401impl<'a, I: From<usize> + Into<usize> + Copy> Objects<'a, I> {
1402    /// Returns the number of objects in the array.
1403    pub fn len(&self) -> usize {
1404        self.values.len()
1405    }
1406    /// Returns whether the array contains no objects.
1407    pub fn is_empty(&self) -> bool {
1408        self.values.is_empty()
1409    }
1410    /// Returns an untyped object view for a typed array index.
1411    pub fn get(&self, index: I) -> Option<ObjectRef<'a, I>> {
1412        let index = index.into();
1413        self.values.get(index).map(|value| ObjectRef {
1414            index: I::from(index),
1415            value,
1416        })
1417    }
1418}
1419impl<'a, I: From<usize> + Into<usize> + Copy> IntoIterator for Objects<'a, I> {
1420    type Item = ObjectRef<'a, I>;
1421    type IntoIter = std::iter::Map<
1422        std::iter::Enumerate<std::slice::Iter<'a, Value>>,
1423        fn((usize, &'a Value)) -> ObjectRef<'a, I>,
1424    >;
1425    fn into_iter(self) -> Self::IntoIter {
1426        fn make<I: From<usize> + Into<usize> + Copy>(
1427            (index, value): (usize, &Value),
1428        ) -> ObjectRef<'_, I> {
1429            ObjectRef {
1430                index: I::from(index),
1431                value,
1432            }
1433        }
1434        self.values.iter().enumerate().map(make::<I>)
1435    }
1436}
1437
1438macro_rules! index_conversions { ($($name:ident),+ $(,)?) => { $(impl From<usize> for $name { fn from(value: usize) -> Self { Self(value) } } impl From<$name> for usize { fn from(value: $name) -> Self { value.0 } })+ }; }
1439index_conversions!(
1440    AccessorIndex,
1441    AnimationIndex,
1442    BufferIndex,
1443    BufferViewIndex,
1444    CameraIndex,
1445    ExternalAssetIndex,
1446    FileIndex,
1447    ImageIndex,
1448    MaterialIndex,
1449    MeshIndex,
1450    NodeIndex,
1451    SamplerIndex,
1452    SceneIndex,
1453    ShapeIndex,
1454    SkinIndex,
1455    TextureIndex
1456);
1457
1458/// Stable reference to a primitive in the lossless document.
1459#[derive(Clone, Copy)]
1460pub struct PrimitiveRef<'a> {
1461    document: &'a Document,
1462    mesh: MeshIndex,
1463    primitive: usize,
1464}
1465impl<'a> PrimitiveRef<'a> {
1466    /// Returns the mesh containing this primitive.
1467    pub fn mesh_index(self) -> MeshIndex {
1468        self.mesh
1469    }
1470    /// Returns the primitive's zero-based position within its mesh.
1471    pub fn primitive_index(self) -> usize {
1472        self.primitive
1473    }
1474    /// Returns the primitive's lossless JSON object.
1475    pub fn value(self) -> &'a Value {
1476        &self.document.as_value()["meshes"][self.mesh.0]["primitives"][self.primitive]
1477    }
1478    /// Returns the primitive attribute map.
1479    pub fn attributes(self) -> Option<&'a [(String, Value)]> {
1480        self.value().get("attributes").and_then(Value::as_object)
1481    }
1482    /// Iterates over named primitive attribute accessor indexes.
1483    pub fn attribute_indices(self) -> impl Iterator<Item = (&'a str, AccessorIndex)> + 'a {
1484        self.attributes()
1485            .unwrap_or(&[])
1486            .iter()
1487            .filter_map(|(semantic, value)| {
1488                value
1489                    .as_u64()
1490                    .and_then(|index| usize::try_from(index).ok())
1491                    .map(|index| (semantic.as_str(), AccessorIndex(index)))
1492            })
1493    }
1494    /// Returns the optional index accessor.
1495    pub fn indices(self) -> Option<AccessorIndex> {
1496        index_value(self.value(), "indices").map(AccessorIndex)
1497    }
1498    /// Returns the optional material index.
1499    pub fn material(self) -> Option<MaterialIndex> {
1500        index_value(self.value(), "material").map(MaterialIndex)
1501    }
1502    /// Returns the primitive mode, defaulting to TRIANGLES (4).
1503    pub fn mode(self) -> u32 {
1504        self.value()
1505            .get("mode")
1506            .and_then(Value::as_u64)
1507            .and_then(|mode| u32::try_from(mode).ok())
1508            .unwrap_or(4)
1509    }
1510    /// Iterates over morph-target attribute maps.
1511    pub fn morph_targets(self) -> impl Iterator<Item = &'a [(String, Value)]> + 'a {
1512        self.value()
1513            .get("targets")
1514            .and_then(Value::as_array)
1515            .unwrap_or(&[])
1516            .iter()
1517            .filter_map(Value::as_object)
1518    }
1519    /// Returns a named primitive extension payload.
1520    pub fn extension(self, name: &str) -> Option<&'a Value> {
1521        self.value().get("extensions")?.get(name)
1522    }
1523}