Skip to main content

draco_io/
gltf_reader.rs

1//! glTF/GLB reader with full scene graph and mesh decoding support.
2//!
3//! This module provides support for reading glTF 2.0 files. It supports:
4//! - Draco-compressed primitives via `KHR_draco_mesh_compression`
5//! - Standard (non-Draco) primitives with accessor-based geometry
6//! - Full scene graph parsing (scenes, nodes, transforms, hierarchy)
7//! - Both `.gltf` (JSON + separate `.bin`) and `.glb` (binary container) formats
8//!
9//! # Example
10//!
11//! ```ignore
12//! use draco_io::gltf_reader::GltfReader;
13//! use draco_io::SceneReader;
14//!
15//! let mut reader = GltfReader::open("model.glb")?;
16//!
17//! // Read all meshes (Draco and non-Draco)
18//! let meshes = reader.decode_all_meshes()?;
19//!
20//! // Or read the full scene graph with transforms
21//! let scene = reader.read_scene()?;
22//! for node in &scene.root_nodes {
23//!     println!(
24//!         "Node: {:?}, mesh instances: {}",
25//!         node.name,
26//!         node.mesh_instances.len()
27//!     );
28//! }
29//! ```
30
31use std::collections::HashMap;
32use std::fs;
33use std::io;
34use std::path::Path;
35
36use draco_core::decoder_buffer::DecoderBuffer;
37use draco_core::draco_types::DataType;
38use draco_core::geometry_attribute::PointAttribute;
39use draco_core::mesh::Mesh;
40use draco_core::mesh_decoder::MeshDecoder;
41#[cfg(feature = "point_cloud_decode")]
42use draco_core::point_cloud::PointCloud;
43#[cfg(feature = "point_cloud_decode")]
44use draco_core::point_cloud_decoder::PointCloudDecoder;
45use serde::Deserialize;
46
47use crate::traits::ReadFromBytes;
48
49// The error type, the reader-agnostic geometry decoder, and the glTF numeric
50// constants live in `gltf_geometry` so they are available with only the writer
51// feature (the compressor reuses them without linking this reader).
52use crate::gltf_geometry::{
53    add_named_attribute, decode_geometry, supported_semantic_spec, AccessorSource, DecodedAccessor,
54    GltfError, Result, GLTF_COMPONENT_BYTE, GLTF_COMPONENT_FLOAT, GLTF_COMPONENT_SHORT,
55    GLTF_COMPONENT_UNSIGNED_BYTE, GLTF_COMPONENT_UNSIGNED_INT, GLTF_COMPONENT_UNSIGNED_SHORT,
56    GLTF_MODE_TRIANGLES,
57};
58
59// ============================================================================
60// glTF JSON Schema (full scene graph support)
61// ============================================================================
62
63#[allow(dead_code)]
64#[derive(Debug, Deserialize)]
65#[serde(rename_all = "camelCase")]
66struct GltfRoot {
67    asset: Asset,
68    #[serde(default)]
69    accessors: Vec<Accessor>,
70    #[serde(default)]
71    buffer_views: Vec<BufferView>,
72    #[serde(default)]
73    buffers: Vec<Buffer>,
74    #[serde(default)]
75    meshes: Vec<GltfMesh>,
76    #[serde(default)]
77    nodes: Vec<GltfNode>,
78    #[serde(default)]
79    scenes: Vec<GltfScene>,
80    #[serde(default)]
81    skins: Vec<serde_json::Value>,
82    #[serde(default)]
83    animations: Vec<serde_json::Value>,
84    /// Default scene index (if present).
85    scene: Option<usize>,
86    #[serde(default)]
87    extensions_used: Vec<String>,
88    #[serde(default)]
89    extensions_required: Vec<String>,
90}
91
92#[allow(dead_code)]
93#[derive(Debug, Deserialize)]
94#[serde(rename_all = "camelCase")]
95struct Asset {
96    version: String,
97    min_version: Option<String>,
98}
99
100/// A glTF scene containing root node indices.
101#[allow(dead_code)]
102#[derive(Debug, Deserialize)]
103#[serde(rename_all = "camelCase")]
104struct GltfScene {
105    name: Option<String>,
106    #[serde(default)]
107    nodes: Vec<usize>,
108}
109
110/// A glTF node in the scene graph.
111#[allow(dead_code)]
112#[derive(Debug, Deserialize)]
113#[serde(rename_all = "camelCase")]
114struct GltfNode {
115    name: Option<String>,
116    /// Index into meshes array.
117    mesh: Option<usize>,
118    /// Child node indices.
119    #[serde(default)]
120    children: Vec<usize>,
121    /// 4x4 transformation matrix (column-major).
122    matrix: Option<[f32; 16]>,
123    /// Translation (T in TRS).
124    translation: Option<[f32; 3]>,
125    /// Rotation quaternion [x, y, z, w] (R in TRS).
126    rotation: Option<[f32; 4]>,
127    /// Scale (S in TRS).
128    scale: Option<[f32; 3]>,
129    skin: Option<usize>,
130}
131
132#[allow(dead_code)]
133#[derive(Debug, Deserialize)]
134#[serde(rename_all = "camelCase")]
135struct Accessor {
136    buffer_view: Option<usize>,
137    byte_offset: Option<usize>,
138    component_type: u32,
139    #[serde(default)]
140    normalized: bool,
141    count: usize,
142    #[serde(rename = "type")]
143    accessor_type: String,
144    #[serde(default)]
145    min: Vec<f64>,
146    #[serde(default)]
147    max: Vec<f64>,
148    sparse: Option<serde_json::Value>,
149}
150
151#[allow(dead_code)]
152#[derive(Debug, Deserialize)]
153#[serde(rename_all = "camelCase")]
154struct BufferView {
155    buffer: usize,
156    byte_offset: Option<usize>,
157    byte_length: usize,
158    byte_stride: Option<usize>,
159    target: Option<u32>,
160}
161
162#[allow(dead_code)]
163#[derive(Debug, Deserialize)]
164#[serde(rename_all = "camelCase")]
165struct Buffer {
166    byte_length: usize,
167    uri: Option<String>,
168}
169
170#[derive(Debug, Deserialize)]
171#[serde(rename_all = "camelCase")]
172struct GltfMesh {
173    name: Option<String>,
174    primitives: Vec<Primitive>,
175}
176
177#[allow(dead_code)]
178#[derive(Debug, Deserialize)]
179#[serde(rename_all = "camelCase")]
180struct Primitive {
181    #[serde(default)]
182    attributes: HashMap<String, usize>,
183    indices: Option<usize>,
184    mode: Option<u32>,
185    material: Option<usize>,
186    #[serde(default)]
187    targets: Vec<HashMap<String, usize>>,
188    extensions: Option<PrimitiveExtensions>,
189}
190
191#[derive(Debug, Deserialize)]
192#[serde(rename_all = "camelCase")]
193struct PrimitiveExtensions {
194    #[serde(rename = "KHR_draco_mesh_compression")]
195    khr_draco_mesh_compression: Option<DracoExtension>,
196}
197
198#[derive(Debug, Deserialize)]
199#[serde(rename_all = "camelCase")]
200struct DracoExtension {
201    buffer_view: usize,
202    #[serde(default)]
203    attributes: HashMap<String, usize>,
204}
205
206// ============================================================================
207// GLB Binary Format Constants
208// ============================================================================
209
210const GLB_MAGIC: u32 = 0x46546C67; // "glTF" in little-endian
211const GLB_VERSION: u32 = 2;
212const GLB_CHUNK_JSON: u32 = 0x4E4F534A; // "JSON"
213const GLB_CHUNK_BIN: u32 = 0x004E4942; // "BIN\0"
214const KHR_DRACO_MESH_COMPRESSION: &str = "KHR_draco_mesh_compression";
215
216// ============================================================================
217// GltfReader
218// ============================================================================
219
220/// A reader for glTF/GLB files with Draco mesh decompression support.
221pub struct GltfReader {
222    root: GltfRoot,
223    buffers: Vec<Vec<u8>>,
224}
225
226// `DecodedAccessor` and the `AccessorSource` trait now live in `gltf_geometry`;
227// `GltfAccessorReader` is this crate's implementation over a parsed `GltfRoot`.
228struct GltfAccessorReader<'a> {
229    accessors: &'a [Accessor],
230    buffer_views: &'a [BufferView],
231    buffers: &'a [Vec<u8>],
232}
233
234impl AccessorSource for GltfAccessorReader<'_> {
235    fn read_attribute(
236        &self,
237        accessor: usize,
238        expected_types: &[&str],
239        allowed_component_types: &[u32],
240    ) -> Result<DecodedAccessor> {
241        GltfAccessorReader::read_attribute(self, accessor, expected_types, allowed_component_types)
242    }
243
244    fn read_indices(&self, accessor: usize) -> Result<Vec<u32>> {
245        GltfAccessorReader::read_indices(self, accessor)
246    }
247}
248
249impl<'a> GltfAccessorReader<'a> {
250    fn new(root: &'a GltfRoot, buffers: &'a [Vec<u8>]) -> Self {
251        Self {
252            accessors: &root.accessors,
253            buffer_views: &root.buffer_views,
254            buffers,
255        }
256    }
257
258    fn read_attribute(
259        &self,
260        accessor_idx: usize,
261        expected_types: &[&str],
262        allowed_component_types: &[u32],
263    ) -> Result<DecodedAccessor> {
264        let accessor = self.accessor(accessor_idx)?;
265
266        if !expected_types
267            .iter()
268            .any(|expected| accessor.accessor_type == *expected)
269        {
270            return Err(GltfError::InvalidGltf(format!(
271                "Expected one of {:?} accessor, got {}",
272                expected_types, accessor.accessor_type
273            )));
274        }
275
276        if !allowed_component_types.contains(&accessor.component_type) {
277            return Err(GltfError::Unsupported(format!(
278                "Unsupported {} component type: {}",
279                accessor.accessor_type, accessor.component_type
280            )));
281        }
282
283        let num_components = accessor_num_components(&accessor.accessor_type)?;
284        let data_type = data_type_for_component_type(accessor.component_type)?;
285        let component_size = data_type.byte_length();
286        let row_size = num_components as usize * component_size;
287        let layout = self.accessor_layout(accessor, row_size, component_size, true, "Accessor")?;
288
289        let mut bytes = Vec::with_capacity(accessor.count * row_size);
290        for i in 0..accessor.count {
291            let offset = layout
292                .start
293                .checked_add(i * layout.stride)
294                .ok_or_else(|| GltfError::InvalidGltf("Accessor range overflow".into()))?;
295            if offset + row_size > layout.view_end {
296                return Err(GltfError::InvalidGltf(format!(
297                    "{} accessor out of bounds",
298                    accessor.accessor_type
299                )));
300            }
301            bytes.extend_from_slice(&layout.buffer[offset..offset + row_size]);
302        }
303
304        Ok(DecodedAccessor::new(
305            accessor.count,
306            num_components,
307            data_type,
308            accessor.normalized,
309            bytes,
310        ))
311    }
312
313    fn read_indices(&self, accessor_idx: usize) -> Result<Vec<u32>> {
314        let accessor = self.accessor(accessor_idx)?;
315
316        if accessor.accessor_type != "SCALAR" {
317            return Err(GltfError::InvalidGltf(format!(
318                "Expected SCALAR accessor for indices, got {}",
319                accessor.accessor_type
320            )));
321        }
322
323        let component_size = match accessor.component_type {
324            GLTF_COMPONENT_UNSIGNED_BYTE => 1,
325            GLTF_COMPONENT_UNSIGNED_SHORT => 2,
326            GLTF_COMPONENT_UNSIGNED_INT => 4,
327            _ => {
328                return Err(GltfError::Unsupported(format!(
329                    "Unsupported index component type: {}",
330                    accessor.component_type
331                )));
332            }
333        };
334        let layout = self.accessor_layout(
335            accessor,
336            component_size,
337            component_size,
338            false,
339            "Index accessor",
340        )?;
341        let mut result = Vec::with_capacity(accessor.count);
342
343        match accessor.component_type {
344            GLTF_COMPONENT_UNSIGNED_BYTE => {
345                for i in 0..accessor.count {
346                    let offset = layout.start + i * layout.stride;
347                    if offset + component_size > layout.view_end {
348                        return Err(GltfError::InvalidGltf(
349                            "Index accessor out of bounds".into(),
350                        ));
351                    }
352                    result.push(layout.buffer[offset] as u32);
353                }
354            }
355            GLTF_COMPONENT_UNSIGNED_SHORT => {
356                for i in 0..accessor.count {
357                    let offset = layout.start + i * layout.stride;
358                    if offset + component_size > layout.view_end {
359                        return Err(GltfError::InvalidGltf(
360                            "Index accessor out of bounds".into(),
361                        ));
362                    }
363                    let val =
364                        u16::from_le_bytes([layout.buffer[offset], layout.buffer[offset + 1]]);
365                    result.push(val as u32);
366                }
367            }
368            GLTF_COMPONENT_UNSIGNED_INT => {
369                for i in 0..accessor.count {
370                    let offset = layout.start + i * layout.stride;
371                    if offset + component_size > layout.view_end {
372                        return Err(GltfError::InvalidGltf(
373                            "Index accessor out of bounds".into(),
374                        ));
375                    }
376                    let val = u32::from_le_bytes([
377                        layout.buffer[offset],
378                        layout.buffer[offset + 1],
379                        layout.buffer[offset + 2],
380                        layout.buffer[offset + 3],
381                    ]);
382                    result.push(val);
383                }
384            }
385            _ => unreachable!(),
386        }
387
388        Ok(result)
389    }
390
391    fn accessor(&self, accessor_idx: usize) -> Result<&Accessor> {
392        self.accessors.get(accessor_idx).ok_or_else(|| {
393            GltfError::InvalidGltf(format!("Invalid accessor index: {}", accessor_idx))
394        })
395    }
396
397    fn accessor_layout(
398        &self,
399        accessor: &Accessor,
400        element_size: usize,
401        component_size: usize,
402        vertex_attribute: bool,
403        label: &str,
404    ) -> Result<AccessorLayout<'a>> {
405        if accessor.sparse.is_some() {
406            return Err(GltfError::Unsupported(
407                "Sparse accessors are not supported".into(),
408            ));
409        }
410        if accessor.count == 0 {
411            return Err(GltfError::InvalidGltf(format!(
412                "{} count must be greater than zero",
413                label
414            )));
415        }
416
417        let buffer_view_idx = accessor
418            .buffer_view
419            .ok_or_else(|| GltfError::InvalidGltf(format!("{} has no bufferView", label)))?;
420
421        let buffer_view = self.buffer_views.get(buffer_view_idx).ok_or_else(|| {
422            GltfError::InvalidGltf(format!("Invalid bufferView index: {}", buffer_view_idx))
423        })?;
424
425        let buffer = self.buffers.get(buffer_view.buffer).ok_or_else(|| {
426            GltfError::InvalidGltf(format!("Invalid buffer index: {}", buffer_view.buffer))
427        })?;
428
429        let view_offset = buffer_view.byte_offset.unwrap_or(0);
430        let accessor_offset = accessor.byte_offset.unwrap_or(0);
431        if !accessor_offset.is_multiple_of(component_size) {
432            return Err(GltfError::InvalidGltf(format!(
433                "{} byteOffset is not aligned to component size {}",
434                label, component_size
435            )));
436        }
437
438        let start = view_offset
439            .checked_add(accessor_offset)
440            .ok_or_else(|| GltfError::InvalidGltf("Accessor start overflow".into()))?;
441        if start % component_size != 0 {
442            return Err(GltfError::InvalidGltf(format!(
443                "{} absolute byte offset is not aligned to component size {}",
444                label, component_size
445            )));
446        }
447        if !vertex_attribute && buffer_view.byte_stride.is_some() {
448            return Err(GltfError::InvalidGltf(format!(
449                "{} bufferView must not define byteStride",
450                label
451            )));
452        }
453
454        let stride = buffer_view.byte_stride.unwrap_or(element_size);
455
456        if stride < element_size {
457            return Err(GltfError::InvalidGltf(format!(
458                "{} byteStride {} is smaller than element size {}",
459                label, stride, element_size
460            )));
461        }
462        if stride % component_size != 0 {
463            return Err(GltfError::InvalidGltf(format!(
464                "{} byteStride {} is not aligned to component size {}",
465                label, stride, component_size
466            )));
467        }
468        if let Some(byte_stride) = buffer_view.byte_stride {
469            if !(4..=252).contains(&byte_stride) {
470                return Err(GltfError::InvalidGltf(format!(
471                    "{} byteStride {} is outside glTF range 4..=252",
472                    label, byte_stride
473                )));
474            }
475            if vertex_attribute && byte_stride % 4 != 0 {
476                return Err(GltfError::InvalidGltf(format!(
477                    "{} byteStride {} is not 4-byte aligned",
478                    label, byte_stride
479                )));
480            }
481        }
482
483        let view_end = view_offset
484            .checked_add(buffer_view.byte_length)
485            .ok_or_else(|| GltfError::InvalidGltf("Buffer view range overflow".into()))?;
486        if start > view_end {
487            return Err(GltfError::InvalidGltf(format!(
488                "{} starts past bufferView end",
489                label
490            )));
491        }
492        let byte_len = stride
493            .checked_mul(accessor.count - 1)
494            .and_then(|prefix| prefix.checked_add(element_size))
495            .ok_or_else(|| GltfError::InvalidGltf("Accessor byte range overflow".into()))?;
496        let accessor_end = start
497            .checked_add(byte_len)
498            .ok_or_else(|| GltfError::InvalidGltf("Accessor byte range overflow".into()))?;
499        if accessor_end > view_end {
500            return Err(GltfError::InvalidGltf(format!(
501                "{} accessor does not fit its bufferView",
502                label
503            )));
504        }
505        if view_end > buffer.len() {
506            return Err(GltfError::InvalidGltf(
507                "Buffer view extends past buffer end".into(),
508            ));
509        }
510
511        Ok(AccessorLayout {
512            buffer,
513            start,
514            stride,
515            view_end,
516        })
517    }
518}
519
520struct AccessorLayout<'a> {
521    buffer: &'a [u8],
522    start: usize,
523    stride: usize,
524    view_end: usize,
525}
526
527struct GlbChunks<'a> {
528    json: &'a [u8],
529    bin: Option<&'a [u8]>,
530}
531
532/// Information about a Draco-compressed primitive within a glTF mesh.
533#[derive(Debug, Clone)]
534pub struct DracoPrimitiveInfo {
535    /// Index of the mesh in the glTF file.
536    pub mesh_index: usize,
537    /// Name of the mesh (if available).
538    pub mesh_name: Option<String>,
539    /// Index of the primitive within the mesh.
540    pub primitive_index: usize,
541    /// Buffer view index containing the Draco data.
542    pub buffer_view: usize,
543    /// Attribute mappings from glTF semantic to Draco attribute ID.
544    pub attributes: HashMap<String, usize>,
545}
546
547impl GltfReader {
548    /// Open a glTF or GLB file.
549    ///
550    /// The file type is detected automatically based on the magic bytes.
551    pub fn open<P: AsRef<Path>>(path: P) -> Result<Self> {
552        let path = path.as_ref();
553        let data = fs::read(path)?;
554
555        if data.len() >= 4 && read_u32_le(&data[0..4]) == GLB_MAGIC {
556            Self::from_glb_with_base_path(&data, path.parent())
557        } else {
558            let base_path = path.parent();
559            Self::from_gltf(&data, base_path)
560        }
561    }
562
563    /// Parse from GLB binary data.
564    pub fn from_glb(data: &[u8]) -> Result<Self> {
565        Self::from_glb_with_base_path(data, None)
566    }
567
568    /// Parse from glTF JSON or GLB binary data.
569    ///
570    /// The payload type is detected automatically from the GLB magic bytes.
571    /// For glTF JSON with external buffers, use [`Self::from_bytes_with_base_path`].
572    pub fn from_bytes(data: &[u8]) -> Result<Self> {
573        Self::from_bytes_with_base_path(data, None)
574    }
575
576    /// Parse from glTF JSON or GLB binary data with an optional base path for external buffers.
577    pub fn from_bytes_with_base_path(data: &[u8], base_path: Option<&Path>) -> Result<Self> {
578        if data.len() >= 4 && read_u32_le(&data[0..4]) == GLB_MAGIC {
579            Self::from_glb_with_base_path(data, base_path)
580        } else {
581            Self::from_gltf(data, base_path)
582        }
583    }
584
585    fn from_glb_with_base_path(data: &[u8], base_path: Option<&Path>) -> Result<Self> {
586        let chunks = parse_glb_chunks(data)?;
587        let root: GltfRoot = serde_json::from_slice(chunks.json)?;
588        validate_root_metadata(&root)?;
589        reject_unsupported_features(&root)?;
590        let buffers = load_buffers(&root, true, chunks.bin, base_path)?;
591
592        Ok(Self { root, buffers })
593    }
594
595    /// Parse from glTF JSON data with optional base path for external buffers.
596    pub fn from_gltf(json_data: &[u8], base_path: Option<&Path>) -> Result<Self> {
597        let root: GltfRoot = serde_json::from_slice(json_data)?;
598        validate_root_metadata(&root)?;
599        reject_unsupported_features(&root)?;
600        let buffers = load_buffers(&root, false, None, base_path)?;
601
602        Ok(Self { root, buffers })
603    }
604
605    /// Parse glTF/GLB bytes, decoding geometry even when the asset uses scene
606    /// features this crate does not model (skins, animations, morph targets).
607    ///
608    /// Unlike [`Self::from_bytes`], which rejects such assets, this reader
609    /// ignores those features and decodes only geometry. Use it to read meshes
610    /// out of skinned or animated assets, including the output of
611    /// [`crate::compress_gltf_bytes`] for those assets. Per-primitive decoding
612    /// still fails for unsupported attribute layouts.
613    pub fn from_bytes_lenient(data: &[u8]) -> Result<Self> {
614        Self::from_bytes_lenient_with_base_path(data, None)
615    }
616
617    /// Like [`Self::from_bytes_lenient`], with a base path for external buffers.
618    pub fn from_bytes_lenient_with_base_path(
619        data: &[u8],
620        base_path: Option<&Path>,
621    ) -> Result<Self> {
622        if data.len() >= 4 && read_u32_le(&data[0..4]) == GLB_MAGIC {
623            let chunks = parse_glb_chunks(data)?;
624            let root: GltfRoot = serde_json::from_slice(chunks.json)?;
625            validate_root_metadata(&root)?;
626            let buffers = load_buffers(&root, true, chunks.bin, base_path)?;
627            Ok(Self { root, buffers })
628        } else {
629            let root: GltfRoot = serde_json::from_slice(data)?;
630            validate_root_metadata(&root)?;
631            let buffers = load_buffers(&root, false, None, base_path)?;
632            Ok(Self { root, buffers })
633        }
634    }
635
636    /// Builds a lenient reader from an already-parsed glTF document (`doc`) and
637    /// its resolved buffer bytes.
638    ///
639    /// This lets a caller that already holds a parsed scene and its buffers
640    /// (for example a `gltf-rs` document and the bytes it resolved) decode
641    /// geometry through this reader without serializing back to glTF/GLB bytes
642    /// and re-resolving the buffers. The same lenient policy as
643    /// [`Self::from_bytes_lenient`] applies: skins, animations, and morph
644    /// targets are ignored (not rejected), and per-primitive decoding still
645    /// fails for unsupported attribute layouts.
646    ///
647    /// `buffers` must already be resolved and indexed by glTF buffer index; no
648    /// URI or BIN-chunk resolution is performed here.
649    pub fn from_value(doc: &serde_json::Value, buffers: Vec<Vec<u8>>) -> Result<Self> {
650        let root: GltfRoot = serde_json::from_value(doc.clone())?;
651        validate_root_metadata(&root)?;
652        Ok(Self { root, buffers })
653    }
654
655    /// Resolved buffer bytes, indexed by glTF buffer index. Used by the
656    /// byte-API compressor, which also requires the writer feature.
657    #[cfg(feature = "gltf-writer")]
658    pub(crate) fn buffers(&self) -> &[Vec<u8>] {
659        &self.buffers
660    }
661
662    /// Decode a single non-Draco primitive, returning the mesh and the
663    /// `(glTF semantic, Draco unique id)` mapping for its attributes.
664    ///
665    /// Used by the compressor to build the `KHR_draco_mesh_compression`
666    /// attributes map with the original glTF semantic names (including
667    /// `TANGENT`, `JOINTS_n`, `WEIGHTS_n`, extra `TEXCOORD_n`/`COLOR_n`, and
668    /// custom `_*` attributes), which the Draco attribute model alone cannot
669    /// preserve. Errors if the primitive is already Draco-compressed.
670    ///
671    /// This is the geometry-decode callback expected by
672    /// [`crate::compress_gltf_value`], so a caller holding a parsed scene can
673    /// drive the compressor: build a reader with [`Self::from_value`] and pass
674    /// `|mesh, prim| reader.decode_primitive_with_semantics(mesh, prim)`.
675    pub fn decode_primitive_with_semantics(
676        &self,
677        mesh_idx: usize,
678        prim_idx: usize,
679    ) -> Result<(Mesh, Vec<(String, u32)>)> {
680        let gltf_mesh = self.root.meshes.get(mesh_idx).ok_or_else(|| {
681            GltfError::InvalidGltf(format!("Mesh index {} out of range", mesh_idx))
682        })?;
683        let primitive = gltf_mesh.primitives.get(prim_idx).ok_or_else(|| {
684            GltfError::InvalidGltf(format!(
685                "Primitive index {}:{} out of range",
686                mesh_idx, prim_idx
687            ))
688        })?;
689        if primitive
690            .extensions
691            .as_ref()
692            .and_then(|ext| ext.khr_draco_mesh_compression.as_ref())
693            .is_some()
694        {
695            return Err(GltfError::Unsupported(
696                "primitive is already Draco-compressed".into(),
697            ));
698        }
699        self.decode_standard_primitive(mesh_idx, prim_idx, primitive)
700    }
701
702    /// Check if the glTF file uses Draco compression.
703    pub fn has_draco_extension(&self) -> bool {
704        self.root
705            .extensions_used
706            .iter()
707            .any(|ext| ext == KHR_DRACO_MESH_COMPRESSION)
708    }
709
710    /// Get information about all Draco-compressed primitives.
711    pub fn draco_primitives(&self) -> Vec<DracoPrimitiveInfo> {
712        let mut result = Vec::new();
713
714        for (mesh_idx, mesh) in self.root.meshes.iter().enumerate() {
715            for (prim_idx, primitive) in mesh.primitives.iter().enumerate() {
716                if let Some(ext) = &primitive.extensions {
717                    if let Some(draco) = &ext.khr_draco_mesh_compression {
718                        result.push(DracoPrimitiveInfo {
719                            mesh_index: mesh_idx,
720                            mesh_name: mesh.name.clone(),
721                            primitive_index: prim_idx,
722                            buffer_view: draco.buffer_view,
723                            attributes: draco.attributes.clone(),
724                        });
725                    }
726                }
727            }
728        }
729
730        result
731    }
732
733    /// Get the raw Draco-compressed data for a primitive.
734    pub fn get_draco_data(&self, info: &DracoPrimitiveInfo) -> Result<&[u8]> {
735        let buffer_view = self
736            .root
737            .buffer_views
738            .get(info.buffer_view)
739            .ok_or_else(|| {
740                GltfError::InvalidGltf(format!("Invalid buffer view index: {}", info.buffer_view))
741            })?;
742
743        let buffer = self.buffers.get(buffer_view.buffer).ok_or_else(|| {
744            GltfError::InvalidGltf(format!("Invalid buffer index: {}", buffer_view.buffer))
745        })?;
746
747        let offset = buffer_view.byte_offset.unwrap_or(0);
748        let end = offset
749            .checked_add(buffer_view.byte_length)
750            .ok_or_else(|| GltfError::InvalidGltf("Buffer view range overflow".into()))?;
751
752        if end > buffer.len() {
753            return Err(GltfError::InvalidGltf(
754                "Buffer view extends past buffer end".into(),
755            ));
756        }
757
758        Ok(&buffer[offset..end])
759    }
760
761    /// Decode a Draco-compressed primitive as a Mesh.
762    pub fn decode_draco_mesh(&self, info: &DracoPrimitiveInfo) -> Result<Mesh> {
763        let data = self.get_draco_data(info)?;
764        let mut decoder_buffer = DecoderBuffer::new(data);
765        let mut mesh = Mesh::new();
766        let mut decoder = MeshDecoder::new();
767
768        decoder
769            .decode(&mut decoder_buffer, &mut mesh)
770            .map_err(|e| GltfError::DracoDecode(format!("{:?}", e)))?;
771
772        let primitive = self.primitive_for_draco_info(info)?;
773        self.validate_draco_primitive_metadata(info, primitive, &mesh)?;
774        self.add_draco_side_attributes(&mut mesh, primitive, info)?;
775
776        Ok(mesh)
777    }
778
779    /// Decode a Draco-compressed primitive as a PointCloud.
780    #[cfg(feature = "point_cloud_decode")]
781    pub fn decode_draco_point_cloud(&self, info: &DracoPrimitiveInfo) -> Result<PointCloud> {
782        let data = self.get_draco_data(info)?;
783        let mut decoder_buffer = DecoderBuffer::new(data);
784        let mut point_cloud = PointCloud::new();
785        let mut decoder = PointCloudDecoder::new();
786
787        decoder
788            .decode(&mut decoder_buffer, &mut point_cloud)
789            .map_err(|e| GltfError::DracoDecode(format!("{:?}", e)))?;
790
791        Ok(point_cloud)
792    }
793
794    /// Decode all Draco-compressed primitives as meshes.
795    pub fn decode_all_draco_meshes(&self) -> Result<Vec<(DracoPrimitiveInfo, Mesh)>> {
796        let primitives = self.draco_primitives();
797        let mut result = Vec::with_capacity(primitives.len());
798
799        for info in primitives {
800            let mesh = self.decode_draco_mesh(&info)?;
801            result.push((info, mesh));
802        }
803
804        Ok(result)
805    }
806
807    // ========================================================================
808    // Non-Draco Mesh Decoding
809    // ========================================================================
810
811    /// Decode a non-Draco primitive from accessors/bufferViews.
812    ///
813    /// Returns the decoded mesh plus the `(glTF semantic, Draco unique id)`
814    /// mapping for each attribute, in attribute add order. The unique id equals
815    /// the attribute index, which is what the `KHR_draco_mesh_compression`
816    /// attributes map references.
817    fn decode_standard_primitive(
818        &self,
819        _mesh_idx: usize,
820        _prim_idx: usize,
821        primitive: &Primitive,
822    ) -> Result<(Mesh, Vec<(String, u32)>)> {
823        let mode = primitive.mode.unwrap_or(GLTF_MODE_TRIANGLES);
824        let attributes: Vec<(String, usize)> = primitive
825            .attributes
826            .iter()
827            .map(|(semantic, accessor)| (semantic.clone(), *accessor))
828            .collect();
829        decode_geometry(
830            &self.accessor_reader(),
831            mode,
832            &attributes,
833            primitive.indices,
834        )
835    }
836
837    fn accessor_reader(&self) -> GltfAccessorReader<'_> {
838        GltfAccessorReader::new(&self.root, &self.buffers)
839    }
840
841    fn decode_primitive_mesh(
842        &self,
843        mesh_idx: usize,
844        gltf_mesh: &GltfMesh,
845        prim_idx: usize,
846        primitive: &Primitive,
847    ) -> Result<Mesh> {
848        if let Some(draco) = primitive
849            .extensions
850            .as_ref()
851            .and_then(|ext| ext.khr_draco_mesh_compression.as_ref())
852        {
853            let info = DracoPrimitiveInfo {
854                mesh_index: mesh_idx,
855                mesh_name: gltf_mesh.name.clone(),
856                primitive_index: prim_idx,
857                buffer_view: draco.buffer_view,
858                attributes: draco.attributes.clone(),
859            };
860            self.decode_draco_mesh(&info)
861        } else {
862            self.decode_standard_primitive(mesh_idx, prim_idx, primitive)
863                .map(|(mesh, _)| mesh)
864        }
865    }
866
867    fn primitive_for_draco_info(&self, info: &DracoPrimitiveInfo) -> Result<&Primitive> {
868        let mesh = self.root.meshes.get(info.mesh_index).ok_or_else(|| {
869            GltfError::InvalidGltf(format!("Invalid mesh index: {}", info.mesh_index))
870        })?;
871        let primitive = mesh.primitives.get(info.primitive_index).ok_or_else(|| {
872            GltfError::InvalidGltf(format!(
873                "Invalid primitive index {} for mesh {}",
874                info.primitive_index, info.mesh_index
875            ))
876        })?;
877        let draco = primitive
878            .extensions
879            .as_ref()
880            .and_then(|ext| ext.khr_draco_mesh_compression.as_ref())
881            .ok_or_else(|| {
882                GltfError::InvalidGltf(format!(
883                    "Primitive {}:{} does not use {}",
884                    info.mesh_index, info.primitive_index, KHR_DRACO_MESH_COMPRESSION
885                ))
886            })?;
887        if draco.buffer_view != info.buffer_view || draco.attributes != info.attributes {
888            return Err(GltfError::InvalidGltf(
889                "Draco primitive info does not match source primitive".into(),
890            ));
891        }
892        Ok(primitive)
893    }
894
895    fn validate_draco_primitive_metadata(
896        &self,
897        info: &DracoPrimitiveInfo,
898        primitive: &Primitive,
899        mesh: &Mesh,
900    ) -> Result<()> {
901        let mode = primitive.mode.unwrap_or(GLTF_MODE_TRIANGLES);
902        if mode != GLTF_MODE_TRIANGLES {
903            return Err(GltfError::Unsupported(format!(
904                "{} supports only TRIANGLES=4 for Draco mesh primitives, got mode {}",
905                KHR_DRACO_MESH_COMPRESSION, mode
906            )));
907        }
908        if !info.attributes.contains_key("POSITION") {
909            return Err(GltfError::InvalidGltf(format!(
910                "{} primitive is missing POSITION in extension attributes",
911                KHR_DRACO_MESH_COMPRESSION
912            )));
913        }
914
915        for (semantic, &draco_attribute_id) in &info.attributes {
916            let Some(accessor_idx) = primitive.attributes.get(semantic) else {
917                return Err(GltfError::InvalidGltf(format!(
918                    "{} attribute {} is not present in primitive.attributes",
919                    KHR_DRACO_MESH_COMPRESSION, semantic
920                )));
921            };
922            let attribute_spec = supported_semantic_spec(semantic)?;
923            let attribute = mesh.try_attribute(draco_attribute_id as i32).map_err(|_| {
924                GltfError::InvalidGltf(format!(
925                    "Draco attribute id {} for {} is out of range",
926                    draco_attribute_id, semantic
927                ))
928            })?;
929            if attribute.attribute_type() != attribute_spec.attribute_type {
930                return Err(GltfError::InvalidGltf(format!(
931                    "Draco attribute {} has type {:?}, expected {:?}",
932                    semantic,
933                    attribute.attribute_type(),
934                    attribute_spec.attribute_type
935                )));
936            }
937            self.validate_accessor_matches_attribute(*accessor_idx, semantic, attribute)?;
938        }
939
940        if let Some(indices_accessor_idx) = primitive.indices {
941            let accessor = self
942                .root
943                .accessors
944                .get(indices_accessor_idx)
945                .ok_or_else(|| {
946                    GltfError::InvalidGltf(format!(
947                        "Invalid indices accessor index: {}",
948                        indices_accessor_idx
949                    ))
950                })?;
951            if accessor.sparse.is_some() {
952                return Err(GltfError::Unsupported(
953                    "Sparse index accessors are not supported".into(),
954                ));
955            }
956            if accessor.accessor_type != "SCALAR" {
957                return Err(GltfError::InvalidGltf(format!(
958                    "Expected SCALAR accessor for Draco indices, got {}",
959                    accessor.accessor_type
960                )));
961            }
962            if ![
963                GLTF_COMPONENT_UNSIGNED_BYTE,
964                GLTF_COMPONENT_UNSIGNED_SHORT,
965                GLTF_COMPONENT_UNSIGNED_INT,
966            ]
967            .contains(&accessor.component_type)
968            {
969                return Err(GltfError::Unsupported(format!(
970                    "Unsupported Draco index accessor component type: {}",
971                    accessor.component_type
972                )));
973            }
974            let expected_count = mesh.num_faces() * 3;
975            if accessor.count != expected_count {
976                return Err(GltfError::InvalidGltf(format!(
977                    "Draco indices accessor count {} does not match decoded index count {}",
978                    accessor.count, expected_count
979                )));
980            }
981        }
982
983        Ok(())
984    }
985
986    fn validate_accessor_matches_attribute(
987        &self,
988        accessor_idx: usize,
989        semantic: &str,
990        attribute: &PointAttribute,
991    ) -> Result<()> {
992        let accessor = self.root.accessors.get(accessor_idx).ok_or_else(|| {
993            GltfError::InvalidGltf(format!("Invalid accessor index: {}", accessor_idx))
994        })?;
995        if accessor.sparse.is_some() {
996            return Err(GltfError::Unsupported(format!(
997                "Sparse accessor for {} is not supported",
998                semantic
999            )));
1000        }
1001        let expected_accessor_type = gltf_type_for_num_components(attribute.num_components())?;
1002        if accessor.accessor_type != expected_accessor_type {
1003            return Err(GltfError::InvalidGltf(format!(
1004                "{} accessor type {} does not match decoded attribute type {}",
1005                semantic, accessor.accessor_type, expected_accessor_type
1006            )));
1007        }
1008        let expected_component_type = component_type_for_data_type(attribute.data_type())?;
1009        let attribute_spec = supported_semantic_spec(semantic)?;
1010        if !attribute_spec
1011            .allowed_component_types
1012            .contains(&expected_component_type)
1013        {
1014            return Err(GltfError::Unsupported(format!(
1015                "{} decoded component type {} is not supported by draco-io glTF",
1016                semantic, expected_component_type
1017            )));
1018        }
1019        if accessor.component_type != expected_component_type {
1020            return Err(GltfError::InvalidGltf(format!(
1021                "{} accessor componentType {} does not match decoded componentType {}",
1022                semantic, accessor.component_type, expected_component_type
1023            )));
1024        }
1025        if accessor.normalized != attribute.normalized() {
1026            return Err(GltfError::InvalidGltf(format!(
1027                "{} accessor normalized={} does not match decoded normalized={}",
1028                semantic,
1029                accessor.normalized,
1030                attribute.normalized()
1031            )));
1032        }
1033        if accessor.count != attribute.size() {
1034            return Err(GltfError::InvalidGltf(format!(
1035                "{} accessor count {} does not match decoded attribute count {}",
1036                semantic,
1037                accessor.count,
1038                attribute.size()
1039            )));
1040        }
1041        Ok(())
1042    }
1043
1044    fn add_draco_side_attributes(
1045        &self,
1046        mesh: &mut Mesh,
1047        primitive: &Primitive,
1048        info: &DracoPrimitiveInfo,
1049    ) -> Result<()> {
1050        let accessor_reader = self.accessor_reader();
1051        let mut attributes: Vec<_> = primitive.attributes.iter().collect();
1052        attributes.sort_by_key(|(left, _)| *left);
1053
1054        for (semantic, accessor_idx) in attributes {
1055            if info.attributes.contains_key(semantic) {
1056                continue;
1057            }
1058            add_named_attribute(mesh, &accessor_reader, semantic, *accessor_idx, None)?;
1059        }
1060        Ok(())
1061    }
1062
1063    /// Get the number of meshes in the glTF file.
1064    pub fn num_meshes(&self) -> usize {
1065        self.root.meshes.len()
1066    }
1067
1068    /// Get the number of buffers in the glTF file.
1069    pub fn num_buffers(&self) -> usize {
1070        self.buffers.len()
1071    }
1072
1073    /// Get the extensions used by this glTF file.
1074    pub fn extensions_used(&self) -> &[String] {
1075        &self.root.extensions_used
1076    }
1077
1078    /// Get the extensions required by this glTF file.
1079    pub fn extensions_required(&self) -> &[String] {
1080        &self.root.extensions_required
1081    }
1082}
1083
1084// ============================================================================
1085// Helper Functions
1086// ============================================================================
1087
1088fn read_u32_le(data: &[u8]) -> u32 {
1089    u32::from_le_bytes([data[0], data[1], data[2], data[3]])
1090}
1091
1092fn gltf_type_for_num_components(num_components: u8) -> Result<&'static str> {
1093    match num_components {
1094        1 => Ok("SCALAR"),
1095        2 => Ok("VEC2"),
1096        3 => Ok("VEC3"),
1097        4 => Ok("VEC4"),
1098        _ => Err(GltfError::InvalidGltf(format!(
1099            "Invalid accessor component count: {}",
1100            num_components
1101        ))),
1102    }
1103}
1104
1105fn component_type_for_data_type(data_type: DataType) -> Result<u32> {
1106    match data_type {
1107        DataType::Int8 => Ok(GLTF_COMPONENT_BYTE),
1108        DataType::Uint8 => Ok(GLTF_COMPONENT_UNSIGNED_BYTE),
1109        DataType::Int16 => Ok(GLTF_COMPONENT_SHORT),
1110        DataType::Uint16 => Ok(GLTF_COMPONENT_UNSIGNED_SHORT),
1111        DataType::Uint32 => Ok(GLTF_COMPONENT_UNSIGNED_INT),
1112        DataType::Float32 => Ok(GLTF_COMPONENT_FLOAT),
1113        _ => Err(GltfError::Unsupported(format!(
1114            "Unsupported Draco attribute data type for glTF: {:?}",
1115            data_type
1116        ))),
1117    }
1118}
1119
1120fn validate_root_metadata(root: &GltfRoot) -> Result<()> {
1121    if root.asset.version != "2.0" {
1122        return Err(GltfError::Unsupported(format!(
1123            "Unsupported glTF asset version: {}",
1124            root.asset.version
1125        )));
1126    }
1127    if let Some(min_version) = &root.asset.min_version {
1128        if min_version != "2.0" {
1129            return Err(GltfError::Unsupported(format!(
1130                "Unsupported glTF minimum version: {}",
1131                min_version
1132            )));
1133        }
1134    }
1135
1136    // glTF validity: every required extension must also be listed as used.
1137    // Whether an unknown required extension is *acceptable* is a scope decision
1138    // left to reject_unsupported_features (strict readers only); the lenient
1139    // document-preserving path tolerates them since it preserves, not
1140    // interprets, the rest of the document.
1141    for required in &root.extensions_required {
1142        if !root.extensions_used.iter().any(|used| used == required) {
1143            return Err(GltfError::InvalidGltf(format!(
1144                "Required extension {} is not listed in extensionsUsed",
1145                required
1146            )));
1147        }
1148    }
1149
1150    let mut has_draco_primitive = false;
1151    for mesh in &root.meshes {
1152        for primitive in &mesh.primitives {
1153            if primitive
1154                .extensions
1155                .as_ref()
1156                .and_then(|ext| ext.khr_draco_mesh_compression.as_ref())
1157                .is_some()
1158            {
1159                has_draco_primitive = true;
1160            }
1161        }
1162    }
1163    if has_draco_primitive
1164        && !root
1165            .extensions_used
1166            .iter()
1167            .any(|used| used == KHR_DRACO_MESH_COMPRESSION)
1168    {
1169        return Err(GltfError::InvalidGltf(format!(
1170            "Primitive uses {} but extensionsUsed does not list it",
1171            KHR_DRACO_MESH_COMPRESSION
1172        )));
1173    }
1174
1175    Ok(())
1176}
1177
1178/// Rejects glTF features outside this crate's geometry-decoding scope.
1179///
1180/// Used by the strict readers ([`GltfReader::from_bytes`] and friends). The
1181/// document-preserving compressor uses a lenient path instead: it never
1182/// interprets these features, it just carries them through untouched, so it
1183/// does not reject them here.
1184fn reject_unsupported_features(root: &GltfRoot) -> Result<()> {
1185    // The strict reader cannot faithfully load an asset that *requires* an
1186    // extension it does not implement. (KHR_draco_mesh_compression is the only
1187    // one this crate honors.) The lenient/compressor path skips this check.
1188    for required in &root.extensions_required {
1189        if required != KHR_DRACO_MESH_COMPRESSION {
1190            return Err(GltfError::Unsupported(format!(
1191                "Unsupported required extension: {}",
1192                required
1193            )));
1194        }
1195    }
1196
1197    for (mesh_idx, mesh) in root.meshes.iter().enumerate() {
1198        for (prim_idx, primitive) in mesh.primitives.iter().enumerate() {
1199            if !primitive.targets.is_empty() {
1200                return Err(GltfError::Unsupported(format!(
1201                    "Morph targets are not supported on primitive {}:{}",
1202                    mesh_idx, prim_idx
1203                )));
1204            }
1205        }
1206    }
1207
1208    if !root.skins.is_empty() {
1209        return Err(GltfError::Unsupported("Skins are not supported".into()));
1210    }
1211    if root.nodes.iter().any(|node| node.skin.is_some()) {
1212        return Err(GltfError::Unsupported(
1213            "Skinned nodes are not supported".into(),
1214        ));
1215    }
1216    if !root.animations.is_empty() {
1217        return Err(GltfError::Unsupported(
1218            "Animations are not supported".into(),
1219        ));
1220    }
1221
1222    Ok(())
1223}
1224
1225fn parse_glb_chunks(data: &[u8]) -> Result<GlbChunks<'_>> {
1226    if data.len() < 12 {
1227        return Err(GltfError::InvalidGlb(
1228            "File too small for GLB header".into(),
1229        ));
1230    }
1231
1232    let magic = read_u32_le(&data[0..4]);
1233    let version = read_u32_le(&data[4..8]);
1234    let length = read_u32_le(&data[8..12]) as usize;
1235
1236    if magic != GLB_MAGIC {
1237        return Err(GltfError::InvalidGlb("Invalid GLB magic".into()));
1238    }
1239    if version != GLB_VERSION {
1240        return Err(GltfError::InvalidGlb(format!(
1241            "Unsupported GLB version: {}",
1242            version
1243        )));
1244    }
1245    if length > data.len() {
1246        return Err(GltfError::InvalidGlb("File truncated".into()));
1247    }
1248    if length != data.len() {
1249        return Err(GltfError::InvalidGlb(
1250            "GLB header length does not match file length".into(),
1251        ));
1252    }
1253
1254    let mut offset = 12;
1255    let mut json_chunk: Option<&[u8]> = None;
1256    let mut bin_chunk: Option<&[u8]> = None;
1257    let mut chunk_index = 0usize;
1258    let mut seen_bin = false;
1259
1260    while offset + 8 <= length {
1261        let chunk_length = read_u32_le(&data[offset..offset + 4]) as usize;
1262        let chunk_type = read_u32_le(&data[offset + 4..offset + 8]);
1263        offset += 8;
1264
1265        if !chunk_length.is_multiple_of(4) {
1266            return Err(GltfError::InvalidGlb(
1267                "GLB chunk length is not 4-byte aligned".into(),
1268            ));
1269        }
1270        if offset + chunk_length > length {
1271            return Err(GltfError::InvalidGlb("Chunk extends past file end".into()));
1272        }
1273
1274        let chunk_data = &data[offset..offset + chunk_length];
1275        offset += chunk_length;
1276
1277        match chunk_type {
1278            GLB_CHUNK_JSON => {
1279                if chunk_index != 0 {
1280                    return Err(GltfError::InvalidGlb(
1281                        "JSON chunk must be the first GLB chunk".into(),
1282                    ));
1283                }
1284                if json_chunk.is_some() {
1285                    return Err(GltfError::InvalidGlb("Duplicate JSON chunk".into()));
1286                }
1287                json_chunk = Some(chunk_data);
1288            }
1289            GLB_CHUNK_BIN => {
1290                if chunk_index == 0 {
1291                    return Err(GltfError::InvalidGlb(
1292                        "BIN chunk must not precede JSON chunk".into(),
1293                    ));
1294                }
1295                if seen_bin {
1296                    return Err(GltfError::InvalidGlb("Duplicate BIN chunk".into()));
1297                }
1298                if chunk_index != 1 {
1299                    return Err(GltfError::InvalidGlb(
1300                        "BIN chunk must be the second GLB chunk".into(),
1301                    ));
1302                }
1303                seen_bin = true;
1304                bin_chunk = Some(chunk_data);
1305            }
1306            _ => {}
1307        }
1308        chunk_index += 1;
1309    }
1310
1311    if offset != length {
1312        return Err(GltfError::InvalidGlb(
1313            "GLB chunk table ended on a partial header".into(),
1314        ));
1315    }
1316
1317    Ok(GlbChunks {
1318        json: json_chunk.ok_or_else(|| GltfError::InvalidGlb("No JSON chunk".into()))?,
1319        bin: bin_chunk,
1320    })
1321}
1322
1323fn load_buffers(
1324    root: &GltfRoot,
1325    is_glb: bool,
1326    glb_bin_chunk: Option<&[u8]>,
1327    base_path: Option<&Path>,
1328) -> Result<Vec<Vec<u8>>> {
1329    let mut buffers = Vec::with_capacity(root.buffers.len());
1330    for (i, buffer) in root.buffers.iter().enumerate() {
1331        buffers.push(load_buffer(i, buffer, is_glb, glb_bin_chunk, base_path)?);
1332    }
1333    Ok(buffers)
1334}
1335
1336fn load_buffer(
1337    index: usize,
1338    buffer: &Buffer,
1339    is_glb: bool,
1340    glb_bin_chunk: Option<&[u8]>,
1341    base_path: Option<&Path>,
1342) -> Result<Vec<u8>> {
1343    if let Some(uri) = &buffer.uri {
1344        let data = if uri.starts_with("data:") {
1345            decode_data_uri(uri)?
1346        } else if let Some(base) = base_path {
1347            fs::read(base.join(uri))?
1348        } else {
1349            // No base path means in-memory loading from untrusted bytes (e.g.
1350            // `compress_gltf_bytes`). Refuse to resolve external resources so a
1351            // hostile `buffer.uri` cannot make us read an arbitrary local file;
1352            // only data URIs and the GLB BIN chunk are allowed here.
1353            return Err(GltfError::Unsupported(
1354                "External buffer URIs require a base path; cannot resolve from in-memory bytes"
1355                    .into(),
1356            ));
1357        };
1358        return trim_buffer_to_declared_length(index, buffer, data, false);
1359    }
1360
1361    if is_glb {
1362        if index == 0 {
1363            let bin = glb_bin_chunk.ok_or_else(|| {
1364                GltfError::InvalidGlb("Buffer 0 has no URI but no BIN chunk present".into())
1365            })?;
1366            return trim_buffer_to_declared_length(index, buffer, bin.to_vec(), true);
1367        }
1368        return Err(GltfError::InvalidGlb(format!(
1369            "Buffer {} has no URI and is not buffer 0",
1370            index
1371        )));
1372    }
1373
1374    Err(GltfError::InvalidGltf(
1375        "Buffer without URI in non-GLB file".into(),
1376    ))
1377}
1378
1379fn trim_buffer_to_declared_length(
1380    index: usize,
1381    buffer: &Buffer,
1382    data: Vec<u8>,
1383    is_glb_bin: bool,
1384) -> Result<Vec<u8>> {
1385    if data.len() < buffer.byte_length {
1386        return Err(GltfError::InvalidGltf(format!(
1387            "Buffer {} byteLength {} exceeds resource length {}",
1388            index,
1389            buffer.byte_length,
1390            data.len()
1391        )));
1392    }
1393    if is_glb_bin && data.len() > buffer.byte_length + 3 {
1394        return Err(GltfError::InvalidGlb(format!(
1395            "GLB BIN chunk length {} is more than 3 bytes larger than buffer[0].byteLength {}",
1396            data.len(),
1397            buffer.byte_length
1398        )));
1399    }
1400    Ok(data[..buffer.byte_length].to_vec())
1401}
1402
1403fn accessor_num_components(accessor_type: &str) -> Result<u8> {
1404    match accessor_type {
1405        "SCALAR" => Ok(1),
1406        "VEC2" => Ok(2),
1407        "VEC3" => Ok(3),
1408        "VEC4" => Ok(4),
1409        _ => Err(GltfError::Unsupported(format!(
1410            "Unsupported accessor type: {}",
1411            accessor_type
1412        ))),
1413    }
1414}
1415
1416fn data_type_for_component_type(component_type: u32) -> Result<DataType> {
1417    match component_type {
1418        GLTF_COMPONENT_BYTE => Ok(DataType::Int8),
1419        GLTF_COMPONENT_UNSIGNED_BYTE => Ok(DataType::Uint8),
1420        GLTF_COMPONENT_SHORT => Ok(DataType::Int16),
1421        GLTF_COMPONENT_UNSIGNED_SHORT => Ok(DataType::Uint16),
1422        GLTF_COMPONENT_UNSIGNED_INT => Ok(DataType::Uint32),
1423        GLTF_COMPONENT_FLOAT => Ok(DataType::Float32),
1424        _ => Err(GltfError::Unsupported(format!(
1425            "Unsupported component type: {}",
1426            component_type
1427        ))),
1428    }
1429}
1430
1431fn decode_data_uri(uri: &str) -> Result<Vec<u8>> {
1432    // Format: data:[<mediatype>][;base64],<data>
1433    let comma_pos = uri
1434        .find(',')
1435        .ok_or_else(|| GltfError::InvalidGltf("Invalid data URI: no comma".into()))?;
1436
1437    let header = &uri[5..comma_pos]; // Skip "data:"
1438    let data = &uri[comma_pos + 1..];
1439
1440    if header.contains(";base64") {
1441        decode_base64(data)
1442    } else {
1443        // URL-encoded data
1444        Ok(percent_decode(data))
1445    }
1446}
1447
1448// Implement the Reader trait for glTF/GLB files. Decodes all primitives
1449// (Draco-compressed and standard) and returns them as meshes.
1450impl crate::traits::Reader for GltfReader {
1451    fn open<P: AsRef<Path>>(path: P) -> std::io::Result<Self> {
1452        GltfReader::open(path).map_err(|e| std::io::Error::other(e.to_string()))
1453    }
1454
1455    fn read_meshes(&mut self) -> std::io::Result<Vec<draco_core::mesh::Mesh>> {
1456        self.decode_all_meshes()
1457            .map_err(|e| std::io::Error::other(e.to_string()))
1458    }
1459}
1460
1461impl ReadFromBytes for GltfReader {
1462    fn from_bytes(bytes: &[u8]) -> io::Result<Self> {
1463        GltfReader::from_bytes(bytes).map_err(|e| io::Error::other(e.to_string()))
1464    }
1465}
1466
1467impl GltfReader {
1468    /// Decode all primitives (both Draco and standard) as meshes.
1469    pub fn decode_all_meshes(&self) -> Result<Vec<Mesh>> {
1470        let mut result = Vec::new();
1471
1472        for (mesh_idx, gltf_mesh) in self.root.meshes.iter().enumerate() {
1473            for (prim_idx, primitive) in gltf_mesh.primitives.iter().enumerate() {
1474                let mesh = self.decode_primitive_mesh(mesh_idx, gltf_mesh, prim_idx, primitive)?;
1475                result.push(mesh);
1476            }
1477        }
1478
1479        Ok(result)
1480    }
1481
1482    /// Compute a node's local transform as a row-major 4x4 matrix.
1483    fn compute_node_transform(node: &GltfNode) -> Option<crate::scene::Transform> {
1484        if let Some(m) = &node.matrix {
1485            // glTF stores column-major; convert to row-major
1486            Some(crate::scene::Transform {
1487                matrix: [
1488                    [m[0], m[4], m[8], m[12]],
1489                    [m[1], m[5], m[9], m[13]],
1490                    [m[2], m[6], m[10], m[14]],
1491                    [m[3], m[7], m[11], m[15]],
1492                ],
1493            })
1494        } else if node.translation.is_some() || node.rotation.is_some() || node.scale.is_some() {
1495            // Compose T * R * S
1496            let t = node.translation.unwrap_or([0.0, 0.0, 0.0]);
1497            let r = node.rotation.unwrap_or([0.0, 0.0, 0.0, 1.0]); // [x, y, z, w]
1498            let s = node.scale.unwrap_or([1.0, 1.0, 1.0]);
1499
1500            // Quaternion to rotation matrix (row-major)
1501            let (qx, qy, qz, qw) = (r[0], r[1], r[2], r[3]);
1502            let xx = qx * qx;
1503            let yy = qy * qy;
1504            let zz = qz * qz;
1505            let xy = qx * qy;
1506            let xz = qx * qz;
1507            let yz = qy * qz;
1508            let wx = qw * qx;
1509            let wy = qw * qy;
1510            let wz = qw * qz;
1511
1512            // Rotation matrix (row-major)
1513            let rot = [
1514                [1.0 - 2.0 * (yy + zz), 2.0 * (xy - wz), 2.0 * (xz + wy)],
1515                [2.0 * (xy + wz), 1.0 - 2.0 * (xx + zz), 2.0 * (yz - wx)],
1516                [2.0 * (xz - wy), 2.0 * (yz + wx), 1.0 - 2.0 * (xx + yy)],
1517            ];
1518
1519            // Compose T * R * S into 4x4 row-major
1520            Some(crate::scene::Transform {
1521                matrix: [
1522                    [rot[0][0] * s[0], rot[0][1] * s[1], rot[0][2] * s[2], t[0]],
1523                    [rot[1][0] * s[0], rot[1][1] * s[1], rot[1][2] * s[2], t[1]],
1524                    [rot[2][0] * s[0], rot[2][1] * s[1], rot[2][2] * s[2], t[2]],
1525                    [0.0, 0.0, 0.0, 1.0],
1526                ],
1527            })
1528        } else {
1529            None
1530        }
1531    }
1532
1533    /// Recursively build a SceneNode from a glTF node index.
1534    fn build_scene_node(
1535        &self,
1536        node_idx: usize,
1537        visited: &mut Vec<bool>,
1538    ) -> Result<crate::scene::SceneNode> {
1539        if node_idx >= self.root.nodes.len() {
1540            return Err(GltfError::InvalidGltf(format!(
1541                "Invalid node index: {}",
1542                node_idx
1543            )));
1544        }
1545
1546        // Cycle detection
1547        if visited[node_idx] {
1548            return Err(GltfError::InvalidGltf(format!(
1549                "Cycle detected at node {}",
1550                node_idx
1551            )));
1552        }
1553        visited[node_idx] = true;
1554
1555        let gltf_node = &self.root.nodes[node_idx];
1556
1557        let mut scene_node = crate::scene::SceneNode::new(gltf_node.name.clone());
1558        scene_node.transform = Self::compute_node_transform(gltf_node);
1559
1560        // Attach meshes if this node references a mesh
1561        if let Some(mesh_idx) = gltf_node.mesh {
1562            if let Some(gltf_mesh) = self.root.meshes.get(mesh_idx) {
1563                for (prim_idx, primitive) in gltf_mesh.primitives.iter().enumerate() {
1564                    let mesh =
1565                        self.decode_primitive_mesh(mesh_idx, gltf_mesh, prim_idx, primitive)?;
1566
1567                    let mesh_instance_name = if gltf_mesh.primitives.len() > 1 {
1568                        gltf_mesh
1569                            .name
1570                            .as_ref()
1571                            .map(|n| format!("{}_{}", n, prim_idx))
1572                    } else {
1573                        gltf_mesh.name.clone()
1574                    };
1575
1576                    scene_node.mesh_instances.push(crate::scene::MeshInstance {
1577                        name: mesh_instance_name,
1578                        mesh,
1579                        transform: None, // Primitive-level transform is identity
1580                    });
1581                }
1582            }
1583        }
1584
1585        // Recursively build children
1586        for &child_idx in &gltf_node.children {
1587            let child_node = self.build_scene_node(child_idx, visited)?;
1588            scene_node.children.push(child_node);
1589        }
1590
1591        Ok(scene_node)
1592    }
1593
1594    fn root_node_indices_without_scenes(&self) -> Vec<usize> {
1595        let mut is_child = vec![false; self.root.nodes.len()];
1596        for node in &self.root.nodes {
1597            for &child_idx in &node.children {
1598                if child_idx < is_child.len() {
1599                    is_child[child_idx] = true;
1600                }
1601            }
1602        }
1603
1604        (0..self.root.nodes.len())
1605            .filter(|&i| !is_child[i])
1606            .collect()
1607    }
1608
1609    fn build_scene_from_roots(
1610        &self,
1611        name: Option<String>,
1612        root_node_indices: &[usize],
1613    ) -> Result<crate::scene::Scene> {
1614        let mut visited = vec![false; self.root.nodes.len()];
1615        let mut root_nodes = Vec::with_capacity(root_node_indices.len());
1616        for &node_idx in root_node_indices {
1617            root_nodes.push(self.build_scene_node(node_idx, &mut visited)?);
1618        }
1619
1620        Ok(crate::scene::Scene { name, root_nodes })
1621    }
1622
1623    fn read_scene_result(&self) -> Result<crate::scene::Scene> {
1624        let scene_idx = self.root.scene.or({
1625            if self.root.scenes.is_empty() {
1626                None
1627            } else {
1628                Some(0)
1629            }
1630        });
1631
1632        if let Some(idx) = scene_idx {
1633            let gltf_scene =
1634                self.root.scenes.get(idx).ok_or_else(|| {
1635                    GltfError::InvalidGltf(format!("Invalid scene index: {}", idx))
1636                })?;
1637            self.build_scene_from_roots(gltf_scene.name.clone(), &gltf_scene.nodes)
1638        } else {
1639            let roots = self.root_node_indices_without_scenes();
1640            self.build_scene_from_roots(None, &roots)
1641        }
1642    }
1643
1644    fn read_scenes_result(&self) -> Result<Vec<crate::scene::Scene>> {
1645        if self.root.scenes.is_empty() {
1646            let roots = self.root_node_indices_without_scenes();
1647            return self
1648                .build_scene_from_roots(None, &roots)
1649                .map(|scene| vec![scene]);
1650        }
1651
1652        self.root
1653            .scenes
1654            .iter()
1655            .map(|scene| self.build_scene_from_roots(scene.name.clone(), &scene.nodes))
1656            .collect()
1657    }
1658}
1659
1660impl crate::scene::SceneReader for GltfReader {
1661    fn read_scene(&mut self) -> std::io::Result<crate::scene::Scene> {
1662        self.read_scene_result()
1663            .map_err(|e| std::io::Error::other(e.to_string()))
1664    }
1665
1666    fn read_scenes(&mut self) -> std::io::Result<Vec<crate::scene::Scene>> {
1667        self.read_scenes_result()
1668            .map_err(|e| std::io::Error::other(e.to_string()))
1669    }
1670}
1671fn decode_base64(input: &str) -> Result<Vec<u8>> {
1672    // Simple base64 decoder (no external dependency)
1673    const DECODE_TABLE: [i8; 128] = [
1674        -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
1675        -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, -1,
1676        -1, 63, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -1, -1, -1, -1, 0, 1, 2, 3, 4,
1677        5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1,
1678        -1, -1, -1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45,
1679        46, 47, 48, 49, 50, 51, -1, -1, -1, -1, -1,
1680    ];
1681
1682    let input: Vec<u8> = input
1683        .bytes()
1684        .filter(|&b| b != b'\n' && b != b'\r')
1685        .collect();
1686    let mut output = Vec::with_capacity(input.len() * 3 / 4);
1687
1688    let chunks = input.chunks(4);
1689    for chunk in chunks {
1690        if chunk.is_empty() {
1691            break;
1692        }
1693
1694        let mut buf = [0u8; 4];
1695        let mut valid_count = 0;
1696
1697        for (i, &byte) in chunk.iter().enumerate() {
1698            if byte == b'=' {
1699                buf[i] = 0;
1700            } else if byte < 128 {
1701                let val = DECODE_TABLE[byte as usize];
1702                if val < 0 {
1703                    return Err(GltfError::InvalidGltf("Invalid base64 character".into()));
1704                }
1705                buf[i] = val as u8;
1706                valid_count = i + 1;
1707            } else {
1708                return Err(GltfError::InvalidGltf("Invalid base64 character".into()));
1709            }
1710        }
1711
1712        // Fill remaining slots with 0 if chunk is incomplete
1713        for item in buf.iter_mut().skip(chunk.len()) {
1714            *item = 0;
1715        }
1716
1717        let n = ((buf[0] as u32) << 18)
1718            | ((buf[1] as u32) << 12)
1719            | ((buf[2] as u32) << 6)
1720            | (buf[3] as u32);
1721
1722        output.push((n >> 16) as u8);
1723        if valid_count > 2 {
1724            output.push((n >> 8) as u8);
1725        }
1726        if valid_count > 3 {
1727            output.push(n as u8);
1728        }
1729    }
1730
1731    Ok(output)
1732}
1733
1734fn percent_decode(input: &str) -> Vec<u8> {
1735    let mut output = Vec::with_capacity(input.len());
1736    let bytes = input.as_bytes();
1737    let mut i = 0;
1738
1739    while i < bytes.len() {
1740        if bytes[i] == b'%' && i + 2 < bytes.len() {
1741            if let (Some(h), Some(l)) = (hex_digit(bytes[i + 1]), hex_digit(bytes[i + 2])) {
1742                output.push((h << 4) | l);
1743                i += 3;
1744                continue;
1745            }
1746        }
1747        output.push(bytes[i]);
1748        i += 1;
1749    }
1750
1751    output
1752}
1753
1754fn hex_digit(b: u8) -> Option<u8> {
1755    match b {
1756        b'0'..=b'9' => Some(b - b'0'),
1757        b'a'..=b'f' => Some(b - b'a' + 10),
1758        b'A'..=b'F' => Some(b - b'A' + 10),
1759        _ => None,
1760    }
1761}
1762
1763// ============================================================================
1764// Tests
1765// ============================================================================
1766
1767#[cfg(test)]
1768mod tests {
1769    use super::*;
1770    use draco_core::draco_types::DataType;
1771    use draco_core::geometry_attribute::GeometryAttributeType;
1772    #[cfg(feature = "gltf-writer")]
1773    use draco_core::geometry_attribute::PointAttribute;
1774    use draco_core::mesh::Mesh;
1775    use tempfile::tempdir;
1776
1777    fn build_glb(json: &str) -> Vec<u8> {
1778        let mut json_bytes = json.as_bytes().to_vec();
1779        while !json_bytes.len().is_multiple_of(4) {
1780            json_bytes.push(b' ');
1781        }
1782
1783        let total_len = 12 + 8 + json_bytes.len();
1784        let mut glb = Vec::with_capacity(total_len);
1785        glb.extend_from_slice(&GLB_MAGIC.to_le_bytes());
1786        glb.extend_from_slice(&GLB_VERSION.to_le_bytes());
1787        glb.extend_from_slice(&(total_len as u32).to_le_bytes());
1788        glb.extend_from_slice(&(json_bytes.len() as u32).to_le_bytes());
1789        glb.extend_from_slice(&GLB_CHUNK_JSON.to_le_bytes());
1790        glb.extend_from_slice(&json_bytes);
1791        glb
1792    }
1793
1794    fn build_glb_chunks(chunks: &[(u32, Vec<u8>)]) -> Vec<u8> {
1795        let total_len = 12 + chunks.iter().map(|(_, data)| 8 + data.len()).sum::<usize>();
1796        let mut glb = Vec::with_capacity(total_len);
1797        glb.extend_from_slice(&GLB_MAGIC.to_le_bytes());
1798        glb.extend_from_slice(&GLB_VERSION.to_le_bytes());
1799        glb.extend_from_slice(&(total_len as u32).to_le_bytes());
1800        for (chunk_type, data) in chunks {
1801            glb.extend_from_slice(&(data.len() as u32).to_le_bytes());
1802            glb.extend_from_slice(&chunk_type.to_le_bytes());
1803            glb.extend_from_slice(data);
1804        }
1805        glb
1806    }
1807
1808    fn padded_json(json: &str) -> Vec<u8> {
1809        let mut bytes = json.as_bytes().to_vec();
1810        while !bytes.len().is_multiple_of(4) {
1811            bytes.push(b' ');
1812        }
1813        bytes
1814    }
1815
1816    fn padded_bin(data: &[u8]) -> Vec<u8> {
1817        let mut bytes = data.to_vec();
1818        while !bytes.len().is_multiple_of(4) {
1819            bytes.push(0);
1820        }
1821        bytes
1822    }
1823
1824    fn triangle_positions() -> Vec<u8> {
1825        [
1826            0.0f32, 0.0, 0.0, //
1827            1.0, 0.0, 0.0, //
1828            0.0, 1.0, 0.0,
1829        ]
1830        .into_iter()
1831        .flat_map(f32::to_le_bytes)
1832        .collect()
1833    }
1834
1835    #[cfg(feature = "gltf-writer")]
1836    fn triangle_mesh() -> Mesh {
1837        let mut mesh = Mesh::new();
1838        mesh.set_num_points(3);
1839        mesh.add_face([
1840            draco_core::geometry_indices::PointIndex(0),
1841            draco_core::geometry_indices::PointIndex(1),
1842            draco_core::geometry_indices::PointIndex(2),
1843        ]);
1844
1845        let mut positions = PointAttribute::new();
1846        positions.init(
1847            GeometryAttributeType::Position,
1848            3,
1849            DataType::Float32,
1850            false,
1851            3,
1852        );
1853        positions.buffer_mut().write(0, &triangle_positions());
1854        mesh.add_attribute(positions);
1855        mesh
1856    }
1857
1858    #[cfg(feature = "gltf-writer")]
1859    fn writer_gltf_json_value() -> serde_json::Value {
1860        let mut writer = crate::gltf_writer::GltfWriter::new();
1861        writer
1862            .add_draco_mesh(&triangle_mesh(), Some("triangle"), None)
1863            .unwrap();
1864        serde_json::from_str(&writer.to_gltf_embedded().unwrap()).unwrap()
1865    }
1866
1867    fn read_attribute_bytes(mesh: &Mesh, attribute_type: GeometryAttributeType) -> Vec<u8> {
1868        mesh.named_attribute(attribute_type)
1869            .expect("missing attribute")
1870            .buffer()
1871            .data()
1872            .to_vec()
1873    }
1874
1875    #[test]
1876    fn test_base64_decode() {
1877        assert_eq!(decode_base64("SGVsbG8=").unwrap(), b"Hello");
1878        assert_eq!(decode_base64("SGVsbG8gV29ybGQ=").unwrap(), b"Hello World");
1879        assert_eq!(decode_base64("YQ==").unwrap(), b"a");
1880        assert_eq!(decode_base64("YWI=").unwrap(), b"ab");
1881        assert_eq!(decode_base64("YWJj").unwrap(), b"abc");
1882    }
1883
1884    #[test]
1885    fn test_percent_decode() {
1886        assert_eq!(percent_decode("Hello%20World"), b"Hello World");
1887        assert_eq!(percent_decode("%2F"), b"/");
1888        assert_eq!(percent_decode("test"), b"test");
1889    }
1890
1891    #[test]
1892    fn test_glb_magic() {
1893        // "glTF" in ASCII = 0x67, 0x6C, 0x54, 0x46
1894        // In little-endian u32: 0x46546C67
1895        let magic_bytes = b"glTF";
1896        let magic = u32::from_le_bytes(*magic_bytes);
1897        assert_eq!(magic, GLB_MAGIC);
1898    }
1899
1900    #[test]
1901    fn test_minimal_gltf_json() {
1902        let json = r#"{
1903            "asset": {"version": "2.0"},
1904            "meshes": [],
1905            "buffers": [],
1906            "bufferViews": [],
1907            "accessors": []
1908        }"#;
1909
1910        let root: GltfRoot = serde_json::from_str(json).unwrap();
1911        assert!(root.meshes.is_empty());
1912        assert!(root.buffers.is_empty());
1913    }
1914
1915    #[test]
1916    fn test_read_scenes_returns_all_gltf_scenes() {
1917        use crate::scene::SceneReader;
1918
1919        let json = r#"{
1920            "asset": {"version": "2.0"},
1921            "scene": 1,
1922            "scenes": [
1923                {"name": "Preview", "nodes": [0]},
1924                {"name": "Full", "nodes": [1, 2]}
1925            ],
1926            "nodes": [
1927                {"name": "PreviewRoot"},
1928                {"name": "FullRootA"},
1929                {"name": "FullRootB"}
1930            ]
1931        }"#;
1932
1933        let mut reader = GltfReader::from_gltf(json.as_bytes(), None).unwrap();
1934        let default_scene = reader.read_scene().unwrap();
1935        assert_eq!(default_scene.name, Some("Full".to_string()));
1936        assert_eq!(default_scene.root_nodes.len(), 2);
1937        assert_eq!(
1938            default_scene.root_nodes[0].name,
1939            Some("FullRootA".to_string())
1940        );
1941
1942        let scenes = reader.read_scenes().unwrap();
1943        assert_eq!(scenes.len(), 2);
1944        assert_eq!(scenes[0].name, Some("Preview".to_string()));
1945        assert_eq!(scenes[0].root_nodes.len(), 1);
1946        assert_eq!(
1947            scenes[0].root_nodes[0].name,
1948            Some("PreviewRoot".to_string())
1949        );
1950        assert_eq!(scenes[1].name, Some("Full".to_string()));
1951        assert_eq!(scenes[1].root_nodes.len(), 2);
1952    }
1953
1954    #[test]
1955    fn test_gltf_with_draco_extension() {
1956        let json = r#"{
1957            "asset": {"version": "2.0"},
1958            "extensionsUsed": ["KHR_draco_mesh_compression"],
1959            "meshes": [{
1960                "name": "TestMesh",
1961                "primitives": [{
1962                    "attributes": {"POSITION": 0},
1963                    "extensions": {
1964                        "KHR_draco_mesh_compression": {
1965                            "bufferView": 0,
1966                            "attributes": {"POSITION": 0}
1967                        }
1968                    }
1969                }]
1970            }],
1971            "buffers": [{"byteLength": 3, "uri": "data:application/octet-stream;base64,AAAA"}],
1972            "bufferViews": [{"buffer": 0, "byteLength": 3}],
1973            "accessors": []
1974        }"#;
1975
1976        let reader = GltfReader::from_gltf(json.as_bytes(), None).unwrap();
1977        assert!(reader.has_draco_extension());
1978        assert_eq!(reader.num_meshes(), 1);
1979
1980        let primitives = reader.draco_primitives();
1981        assert_eq!(primitives.len(), 1);
1982        assert_eq!(primitives[0].mesh_name, Some("TestMesh".to_string()));
1983        assert_eq!(primitives[0].buffer_view, 0);
1984    }
1985
1986    #[test]
1987    fn test_glb_rejects_header_length_mismatch() {
1988        let mut glb = build_glb(r#"{"asset":{"version":"2.0"}}"#);
1989        let shorter = (glb.len() as u32 - 1).to_le_bytes();
1990        glb[8..12].copy_from_slice(&shorter);
1991
1992        assert!(matches!(
1993            GltfReader::from_glb(&glb),
1994            Err(GltfError::InvalidGlb(_))
1995        ));
1996    }
1997
1998    #[test]
1999    fn test_glb_rejects_extra_trailing_bytes() {
2000        let mut glb = build_glb(r#"{"asset":{"version":"2.0"}}"#);
2001        glb.push(0);
2002
2003        assert!(matches!(
2004            GltfReader::from_glb(&glb),
2005            Err(GltfError::InvalidGlb(_))
2006        ));
2007    }
2008
2009    #[test]
2010    fn test_glb_rejects_json_not_first_duplicate_bin_and_unaligned_chunk() {
2011        let json = padded_json(r#"{"asset":{"version":"2.0"}}"#);
2012        let bin = padded_bin(&[1, 2, 3, 4]);
2013
2014        let bin_first =
2015            build_glb_chunks(&[(GLB_CHUNK_BIN, bin.clone()), (GLB_CHUNK_JSON, json.clone())]);
2016        assert!(matches!(
2017            GltfReader::from_glb(&bin_first),
2018            Err(GltfError::InvalidGlb(_))
2019        ));
2020
2021        let duplicate_bin = build_glb_chunks(&[
2022            (GLB_CHUNK_JSON, json),
2023            (GLB_CHUNK_BIN, bin.clone()),
2024            (GLB_CHUNK_BIN, bin),
2025        ]);
2026        assert!(matches!(
2027            GltfReader::from_glb(&duplicate_bin),
2028            Err(GltfError::InvalidGlb(_))
2029        ));
2030
2031        let unaligned = build_glb_chunks(&[(GLB_CHUNK_JSON, b"{}".to_vec())]);
2032        assert!(matches!(
2033            GltfReader::from_glb(&unaligned),
2034            Err(GltfError::InvalidGlb(_))
2035        ));
2036    }
2037
2038    #[test]
2039    fn test_rejects_unknown_required_extension() {
2040        let json = r#"{
2041            "asset": {"version": "2.0"},
2042            "extensionsUsed": ["EXT_required"],
2043            "extensionsRequired": ["EXT_required"]
2044        }"#;
2045
2046        assert!(matches!(
2047            GltfReader::from_gltf(json.as_bytes(), None),
2048            Err(GltfError::Unsupported(_))
2049        ));
2050    }
2051
2052    #[test]
2053    fn test_rejects_draco_primitive_missing_extensions_used() {
2054        let json = r#"{
2055            "asset": {"version": "2.0"},
2056            "meshes": [{
2057                "primitives": [{
2058                    "attributes": {"POSITION": 0},
2059                    "extensions": {
2060                        "KHR_draco_mesh_compression": {
2061                            "bufferView": 0,
2062                            "attributes": {"POSITION": 0}
2063                        }
2064                    }
2065                }]
2066            }],
2067            "buffers": [{"byteLength": 3, "uri": "data:application/octet-stream;base64,AAAA"}],
2068            "bufferViews": [{"buffer": 0, "byteLength": 3}],
2069            "accessors": []
2070        }"#;
2071
2072        assert!(matches!(
2073            GltfReader::from_gltf(json.as_bytes(), None),
2074            Err(GltfError::InvalidGltf(_))
2075        ));
2076    }
2077
2078    #[test]
2079    fn test_glb_open_loads_relative_external_buffer() {
2080        let dir = tempdir().unwrap();
2081        let bin_path = dir.path().join("mesh.bin");
2082        std::fs::write(&bin_path, triangle_positions()).unwrap();
2083
2084        let json = r#"{
2085            "asset": {"version": "2.0"},
2086            "buffers": [{"byteLength": 36, "uri": "mesh.bin"}],
2087            "bufferViews": [{"buffer": 0, "byteOffset": 0, "byteLength": 36}],
2088            "accessors": [{
2089                "bufferView": 0,
2090                "componentType": 5126,
2091                "count": 3,
2092                "type": "VEC3"
2093            }],
2094            "meshes": [{
2095                "primitives": [{
2096                    "attributes": {"POSITION": 0},
2097                    "mode": 4
2098                }]
2099            }]
2100        }"#;
2101        let glb = build_glb(json);
2102        let glb_path = dir.path().join("external.glb");
2103        std::fs::write(&glb_path, &glb).unwrap();
2104
2105        let reader = GltfReader::open(&glb_path).unwrap();
2106        let meshes = reader.decode_all_meshes().unwrap();
2107
2108        assert_eq!(meshes.len(), 1);
2109        assert_eq!(meshes[0].num_points(), 3);
2110        assert_eq!(meshes[0].num_faces(), 1);
2111        assert_eq!(
2112            read_attribute_bytes(&meshes[0], GeometryAttributeType::Position),
2113            triangle_positions()
2114        );
2115    }
2116
2117    #[test]
2118    fn test_from_glb_rejects_external_buffer_without_base_path() {
2119        let json = r#"{
2120            "asset": {"version": "2.0"},
2121            "buffers": [{"byteLength": 36, "uri": "mesh.bin"}],
2122            "bufferViews": [{"buffer": 0, "byteOffset": 0, "byteLength": 36}],
2123            "accessors": [],
2124            "meshes": []
2125        }"#;
2126
2127        let err = match GltfReader::from_glb(&build_glb(json)) {
2128            Ok(_) => panic!("external buffer unexpectedly loaded without a base path"),
2129            Err(err) => err,
2130        };
2131        assert!(matches!(err, GltfError::Unsupported(_)));
2132    }
2133
2134    #[test]
2135    fn test_texcoord_unsigned_short_normalized_vec2() {
2136        let mut bytes = triangle_positions();
2137        let texcoords = [0u16, 0, 65535, 0, 0, 65535];
2138        bytes.extend(texcoords.into_iter().flat_map(u16::to_le_bytes));
2139        let data_uri = format!(
2140            "data:application/octet-stream;base64,{}",
2141            base64_for_test(&bytes)
2142        );
2143        let json = format!(
2144            r#"{{
2145                "asset": {{"version": "2.0"}},
2146                "buffers": [{{"byteLength": {}, "uri": "{}"}}],
2147                "bufferViews": [
2148                    {{"buffer": 0, "byteOffset": 0, "byteLength": 36}},
2149                    {{"buffer": 0, "byteOffset": 36, "byteLength": 12}}
2150                ],
2151                "accessors": [
2152                    {{"bufferView": 0, "componentType": 5126, "count": 3, "type": "VEC3"}},
2153                    {{
2154                        "bufferView": 1,
2155                        "componentType": 5123,
2156                        "normalized": true,
2157                        "count": 3,
2158                        "type": "VEC2"
2159                    }}
2160                ],
2161                "meshes": [{{
2162                    "primitives": [{{
2163                        "attributes": {{"POSITION": 0, "TEXCOORD_0": 1}},
2164                        "mode": 4
2165                    }}]
2166                }}]
2167            }}"#,
2168            bytes.len(),
2169            data_uri
2170        );
2171
2172        let mesh = GltfReader::from_gltf(json.as_bytes(), None)
2173            .unwrap()
2174            .decode_all_meshes()
2175            .unwrap()
2176            .remove(0);
2177        let texcoord = mesh
2178            .named_attribute(GeometryAttributeType::TexCoord)
2179            .expect("missing texcoord");
2180
2181        assert_eq!(texcoord.data_type(), DataType::Uint16);
2182        assert!(texcoord.normalized());
2183        assert_eq!(texcoord.num_components(), 2);
2184        assert_eq!(texcoord.buffer().data(), &bytes[36..48]);
2185    }
2186
2187    #[test]
2188    fn test_color_unsigned_byte_normalized_vec3() {
2189        let mut bytes = triangle_positions();
2190        let colors = [255u8, 0, 0, 0, 255, 0, 0, 0, 255];
2191        bytes.extend(colors);
2192        let data_uri = format!(
2193            "data:application/octet-stream;base64,{}",
2194            base64_for_test(&bytes)
2195        );
2196        let json = format!(
2197            r#"{{
2198                "asset": {{"version": "2.0"}},
2199                "buffers": [{{"byteLength": {}, "uri": "{}"}}],
2200                "bufferViews": [
2201                    {{"buffer": 0, "byteOffset": 0, "byteLength": 36}},
2202                    {{"buffer": 0, "byteOffset": 36, "byteLength": 9}}
2203                ],
2204                "accessors": [
2205                    {{"bufferView": 0, "componentType": 5126, "count": 3, "type": "VEC3"}},
2206                    {{
2207                        "bufferView": 1,
2208                        "componentType": 5121,
2209                        "normalized": true,
2210                        "count": 3,
2211                        "type": "VEC3"
2212                    }}
2213                ],
2214                "meshes": [{{
2215                    "primitives": [{{
2216                        "attributes": {{"POSITION": 0, "COLOR_0": 1}},
2217                        "mode": 4
2218                    }}]
2219                }}]
2220            }}"#,
2221            bytes.len(),
2222            data_uri
2223        );
2224
2225        let mesh = GltfReader::from_gltf(json.as_bytes(), None)
2226            .unwrap()
2227            .decode_all_meshes()
2228            .unwrap()
2229            .remove(0);
2230        let color = mesh
2231            .named_attribute(GeometryAttributeType::Color)
2232            .expect("missing color");
2233
2234        assert_eq!(color.data_type(), DataType::Uint8);
2235        assert!(color.normalized());
2236        assert_eq!(color.num_components(), 3);
2237        assert_eq!(color.buffer().data(), &bytes[36..45]);
2238    }
2239
2240    #[test]
2241    fn test_points_primitive_decodes_without_faces() {
2242        let indices = [2u16, 0];
2243        let mut bytes = triangle_positions();
2244        bytes.extend(indices.into_iter().flat_map(u16::to_le_bytes));
2245        let data_uri = format!(
2246            "data:application/octet-stream;base64,{}",
2247            base64_for_test(&bytes)
2248        );
2249        let json = format!(
2250            r#"{{
2251                "asset": {{"version": "2.0"}},
2252                "buffers": [{{"byteLength": {}, "uri": "{}"}}],
2253                "bufferViews": [
2254                    {{"buffer": 0, "byteOffset": 0, "byteLength": 36}},
2255                    {{"buffer": 0, "byteOffset": 36, "byteLength": 4}}
2256                ],
2257                "accessors": [
2258                    {{"bufferView": 0, "componentType": 5126, "count": 3, "type": "VEC3"}},
2259                    {{"bufferView": 1, "componentType": 5123, "count": 2, "type": "SCALAR"}}
2260                ],
2261                "meshes": [{{
2262                    "primitives": [{{
2263                        "attributes": {{"POSITION": 0}},
2264                        "indices": 1,
2265                        "mode": 0
2266                    }}]
2267                }}]
2268            }}"#,
2269            bytes.len(),
2270            data_uri
2271        );
2272
2273        let mesh = GltfReader::from_gltf(json.as_bytes(), None)
2274            .unwrap()
2275            .decode_all_meshes()
2276            .unwrap()
2277            .remove(0);
2278
2279        assert_eq!(mesh.num_points(), 2);
2280        assert_eq!(mesh.num_faces(), 0);
2281        let positions = read_attribute_bytes(&mesh, GeometryAttributeType::Position);
2282        assert_eq!(&positions[0..12], &triangle_positions()[24..36]);
2283        assert_eq!(&positions[12..24], &triangle_positions()[0..12]);
2284    }
2285
2286    #[cfg(feature = "gltf-writer")]
2287    #[test]
2288    fn test_writer_glb_roundtrips_through_reader() {
2289        let dir = tempdir().unwrap();
2290        let path = dir.path().join("roundtrip.glb");
2291
2292        let mut writer = crate::gltf_writer::GltfWriter::new();
2293        writer
2294            .add_draco_mesh(&triangle_mesh(), Some("triangle"), None)
2295            .unwrap();
2296        writer.write_glb(&path).unwrap();
2297
2298        let reader = GltfReader::open(&path).unwrap();
2299        let primitives = reader.draco_primitives();
2300        assert_eq!(primitives.len(), 1);
2301        assert_eq!(primitives[0].attributes.get("POSITION"), Some(&0));
2302
2303        let decoded = reader.decode_all_meshes().unwrap().remove(0);
2304        let position = decoded
2305            .named_attribute(GeometryAttributeType::Position)
2306            .expect("missing position");
2307        assert_eq!(position.data_type(), DataType::Float32);
2308        assert_eq!(position.num_components(), 3);
2309        assert_eq!(decoded.num_faces(), 1);
2310    }
2311
2312    #[cfg(feature = "gltf-writer")]
2313    #[test]
2314    fn test_draco_decode_rejects_extension_attribute_not_in_primitive_attributes() {
2315        let mut value = writer_gltf_json_value();
2316        value["meshes"][0]["primitives"][0]["extensions"]["KHR_draco_mesh_compression"]
2317            ["attributes"]["TEXCOORD_0"] = serde_json::json!(0);
2318        let json = serde_json::to_string(&value).unwrap();
2319
2320        let err = GltfReader::from_gltf(json.as_bytes(), None)
2321            .unwrap()
2322            .decode_all_meshes()
2323            .unwrap_err();
2324        assert!(matches!(err, GltfError::InvalidGltf(_)));
2325    }
2326
2327    #[cfg(feature = "gltf-writer")]
2328    #[test]
2329    fn test_draco_decode_rejects_accessor_metadata_mismatch_and_sparse() {
2330        let mut count_mismatch = writer_gltf_json_value();
2331        let pos_accessor = count_mismatch["meshes"][0]["primitives"][0]["attributes"]["POSITION"]
2332            .as_u64()
2333            .unwrap() as usize;
2334        count_mismatch["accessors"][pos_accessor]["count"] = serde_json::json!(99);
2335        let json = serde_json::to_string(&count_mismatch).unwrap();
2336        let err = GltfReader::from_gltf(json.as_bytes(), None)
2337            .unwrap()
2338            .decode_all_meshes()
2339            .unwrap_err();
2340        assert!(matches!(err, GltfError::InvalidGltf(_)));
2341
2342        let mut sparse = writer_gltf_json_value();
2343        let pos_accessor = sparse["meshes"][0]["primitives"][0]["attributes"]["POSITION"]
2344            .as_u64()
2345            .unwrap() as usize;
2346        sparse["accessors"][pos_accessor]["sparse"] = serde_json::json!({
2347            "count": 1,
2348            "indices": {"bufferView": 0, "componentType": 5123},
2349            "values": {"bufferView": 0}
2350        });
2351        let json = serde_json::to_string(&sparse).unwrap();
2352        let err = GltfReader::from_gltf(json.as_bytes(), None)
2353            .unwrap()
2354            .decode_all_meshes()
2355            .unwrap_err();
2356        assert!(matches!(err, GltfError::Unsupported(_)));
2357    }
2358
2359    #[test]
2360    fn test_standard_triangle_rejects_out_of_bounds_indices() {
2361        let indices = [0u16, 1, 3];
2362        let mut bytes = triangle_positions();
2363        bytes.extend(indices.into_iter().flat_map(u16::to_le_bytes));
2364        let data_uri = format!(
2365            "data:application/octet-stream;base64,{}",
2366            base64_for_test(&bytes)
2367        );
2368        let json = format!(
2369            r#"{{
2370                "asset": {{"version": "2.0"}},
2371                "buffers": [{{"byteLength": {}, "uri": "{}"}}],
2372                "bufferViews": [
2373                    {{"buffer": 0, "byteOffset": 0, "byteLength": 36}},
2374                    {{"buffer": 0, "byteOffset": 36, "byteLength": 6}}
2375                ],
2376                "accessors": [
2377                    {{"bufferView": 0, "componentType": 5126, "count": 3, "type": "VEC3"}},
2378                    {{"bufferView": 1, "componentType": 5123, "count": 3, "type": "SCALAR"}}
2379                ],
2380                "meshes": [{{
2381                    "primitives": [{{
2382                        "attributes": {{"POSITION": 0}},
2383                        "indices": 1,
2384                        "mode": 4
2385                    }}]
2386                }}]
2387            }}"#,
2388            bytes.len(),
2389            data_uri
2390        );
2391
2392        let err = GltfReader::from_gltf(json.as_bytes(), None)
2393            .unwrap()
2394            .decode_all_meshes()
2395            .unwrap_err();
2396        assert!(matches!(err, GltfError::InvalidGltf(_)));
2397    }
2398
2399    #[cfg(feature = "legacy-bitstream-decode")]
2400    #[test]
2401    fn test_draco_legacy_bitstream_data_uri() {
2402        let draco_bytes =
2403            include_bytes!("../../../testdata/legacy_draco/cube_att.mesh_seq.1.1.0.drc");
2404        let data_uri = format!(
2405            "data:application/octet-stream;base64,{}",
2406            base64_for_test(draco_bytes)
2407        );
2408        let json = format!(
2409            r#"{{
2410                "asset": {{"version": "2.0"}},
2411                "extensionsUsed": ["KHR_draco_mesh_compression"],
2412                "buffers": [{{"byteLength": {}, "uri": "{}"}}],
2413                "bufferViews": [{{"buffer": 0, "byteOffset": 0, "byteLength": {}}}],
2414                "accessors": [
2415                    {{"componentType": 5126, "count": 24, "type": "VEC3"}}
2416                ],
2417                "meshes": [{{
2418                    "primitives": [{{
2419                        "attributes": {{"POSITION": 0}},
2420                        "mode": 4,
2421                        "extensions": {{
2422                            "KHR_draco_mesh_compression": {{
2423                                "bufferView": 0,
2424                                "attributes": {{"POSITION": 0}}
2425                            }}
2426                        }}
2427                    }}]
2428                }}]
2429            }}"#,
2430            draco_bytes.len(),
2431            data_uri,
2432            draco_bytes.len()
2433        );
2434
2435        let mesh = GltfReader::from_gltf(json.as_bytes(), None)
2436            .unwrap()
2437            .decode_all_draco_meshes()
2438            .unwrap()
2439            .remove(0)
2440            .1;
2441
2442        assert_eq!(mesh.num_faces(), 12);
2443        assert_eq!(mesh.num_points(), 24);
2444        assert_eq!(
2445            mesh.named_attribute(GeometryAttributeType::Position)
2446                .expect("missing position")
2447                .size(),
2448            24
2449        );
2450    }
2451
2452    fn base64_for_test(bytes: &[u8]) -> String {
2453        const TABLE: &[u8; 64] =
2454            b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
2455        let mut out = String::new();
2456
2457        for chunk in bytes.chunks(3) {
2458            let b0 = chunk[0];
2459            let b1 = *chunk.get(1).unwrap_or(&0);
2460            let b2 = *chunk.get(2).unwrap_or(&0);
2461            let n = ((b0 as u32) << 16) | ((b1 as u32) << 8) | b2 as u32;
2462
2463            out.push(TABLE[((n >> 18) & 0x3f) as usize] as char);
2464            out.push(TABLE[((n >> 12) & 0x3f) as usize] as char);
2465            if chunk.len() > 1 {
2466                out.push(TABLE[((n >> 6) & 0x3f) as usize] as char);
2467            } else {
2468                out.push('=');
2469            }
2470            if chunk.len() > 2 {
2471                out.push(TABLE[(n & 0x3f) as usize] as char);
2472            } else {
2473                out.push('=');
2474            }
2475        }
2476
2477        out
2478    }
2479}