Skip to main content

draco_io/
gltf_geometry.rs

1//! Reader-agnostic glTF geometry decoding.
2//!
3//! This module holds the parts of glTF geometry handling that do **not** depend
4//! on `draco-io`'s own glTF reader: the shared error type, the
5//! [`AccessorSource`] seam, and [`decode_geometry`], which builds a
6//! [`draco_core::Mesh`] (faces, deduplication, attribute typing, multi-set
7//! semantics) from whatever accessor data a source yields.
8//!
9//! It is compiled whenever the glTF reader **or** writer is enabled, so the
10//! document-preserving compressor ([`crate::compress_gltf_value`]) and external
11//! front ends (e.g. a `gltf-rs` document) can reuse the same decode logic with
12//! only the encoder, never linking the reader.
13
14use std::io;
15
16use draco_core::draco_types::DataType;
17use draco_core::geometry_attribute::{GeometryAttributeType, PointAttribute};
18use draco_core::mesh::Mesh;
19use thiserror::Error;
20
21/// Errors that can occur when reading or decoding glTF geometry.
22#[derive(Error, Debug)]
23pub enum GltfError {
24    /// Filesystem or stream I/O failed.
25    #[error("IO error: {0}")]
26    Io(#[from] io::Error),
27
28    /// glTF JSON parsing failed.
29    #[error("JSON parse error: {0}")]
30    Json(#[from] serde_json::Error),
31
32    /// Binary GLB structure is invalid.
33    #[error("Invalid GLB: {0}")]
34    InvalidGlb(String),
35
36    /// glTF JSON or accessor/buffer structure is invalid.
37    #[error("Invalid glTF: {0}")]
38    InvalidGltf(String),
39
40    /// Embedded Draco payload failed to decode.
41    #[error("Draco decode error: {0}")]
42    DracoDecode(#[source] draco_core::DracoError),
43
44    /// Draco encoding failed with a typed codec error.
45    #[error("Draco encode error: {0}")]
46    DracoEncode(#[source] draco_core::DracoError),
47
48    /// The asset uses a glTF feature outside this crate's geometry scope.
49    #[error("Unsupported feature: {0}")]
50    Unsupported(String),
51
52    /// An external resource was rejected by policy or confinement.
53    #[error("External resource denied: {0}")]
54    ExternalResourceDenied(String),
55
56    /// A configured resource quota or a checked allocation was exceeded.
57    #[error("Resource limit exceeded: {0}")]
58    ResourceLimitExceeded(String),
59
60    /// Unknown extension JSON contains binary references that cannot be remapped safely.
61    #[error("Opaque binary reference: {0}")]
62    OpaqueBinaryReference(String),
63
64    /// Compression options are outside their supported range.
65    #[error("Invalid compression options: {0}")]
66    InvalidOptions(String),
67}
68
69/// Result type used by glTF readers and the geometry decoder.
70pub type Result<T> = std::result::Result<T, GltfError>;
71
72pub(crate) const GLTF_MODE_POINTS: u32 = 0;
73pub(crate) const GLTF_MODE_TRIANGLES: u32 = 4;
74pub(crate) const GLTF_COMPONENT_BYTE: u32 = 5120;
75pub(crate) const GLTF_COMPONENT_UNSIGNED_BYTE: u32 = 5121;
76pub(crate) const GLTF_COMPONENT_SHORT: u32 = 5122;
77pub(crate) const GLTF_COMPONENT_UNSIGNED_SHORT: u32 = 5123;
78// Index component type, used only by the reader's index decoding.
79#[cfg(feature = "gltf-reader")]
80pub(crate) const GLTF_COMPONENT_UNSIGNED_INT: u32 = 5125;
81pub(crate) const GLTF_COMPONENT_FLOAT: u32 = 5126;
82
83/// One glTF accessor decoded to a tight, row-major byte block plus its layout.
84///
85/// This is the reader-agnostic unit the geometry decoder consumes: an
86/// [`AccessorSource`] produces these, and [`decode_geometry`] turns them into a
87/// [`Mesh`]. It carries the *original* component type / normalized flag, so the
88/// output accessors (which the compressor preserves) keep matching layout.
89pub struct DecodedAccessor {
90    count: usize,
91    num_components: u8,
92    data_type: DataType,
93    normalized: bool,
94    bytes: Vec<u8>,
95}
96
97impl DecodedAccessor {
98    /// Builds a decoded accessor from already-extracted values. `bytes` must be
99    /// `count * num_components * data_type.byte_length()` long, row-major.
100    pub fn new(
101        count: usize,
102        num_components: u8,
103        data_type: DataType,
104        normalized: bool,
105        bytes: Vec<u8>,
106    ) -> Result<Self> {
107        let expected = count
108            .checked_mul(num_components as usize)
109            .and_then(|count| count.checked_mul(data_type.byte_length()))
110            .ok_or_else(|| GltfError::InvalidGltf("accessor byte size overflow".into()))?;
111        if bytes.len() != expected {
112            return Err(GltfError::InvalidGltf(format!(
113                "accessor has {} bytes, expected {expected}",
114                bytes.len()
115            )));
116        }
117        Ok(Self {
118            count,
119            num_components,
120            data_type,
121            normalized,
122            bytes,
123        })
124    }
125
126    fn gather(&self, indices: &[u32]) -> Result<Self> {
127        let stride = (self.num_components as usize)
128            .checked_mul(self.data_type.byte_length())
129            .ok_or_else(|| GltfError::InvalidGltf("accessor stride overflow".into()))?;
130        let byte_len = indices
131            .len()
132            .checked_mul(stride)
133            .ok_or_else(|| GltfError::InvalidGltf("gathered accessor size overflow".into()))?;
134        let mut bytes = Vec::new();
135        bytes.try_reserve_exact(byte_len).map_err(|_| {
136            GltfError::ResourceLimitExceeded("gathered accessor allocation failed".into())
137        })?;
138
139        for &index in indices {
140            let index = index as usize;
141            if index >= self.count {
142                return Err(GltfError::InvalidGltf(format!(
143                    "Accessor index {} out of bounds for {} values",
144                    index, self.count
145                )));
146            }
147            let offset = index
148                .checked_mul(stride)
149                .ok_or_else(|| GltfError::InvalidGltf("accessor offset overflow".into()))?;
150            let end = offset
151                .checked_add(stride)
152                .filter(|end| *end <= self.bytes.len())
153                .ok_or_else(|| GltfError::InvalidGltf("accessor bytes are truncated".into()))?;
154            bytes.extend_from_slice(&self.bytes[offset..end]);
155        }
156
157        Ok(Self {
158            count: indices.len(),
159            num_components: self.num_components,
160            data_type: self.data_type,
161            normalized: self.normalized,
162            bytes,
163        })
164    }
165}
166
167/// Source of raw accessor data for [`decode_geometry`].
168///
169/// This is the seam that lets the geometry decoder run against different glTF
170/// front ends: `draco-io`'s own accessor reader implements it over the parsed
171/// glTF document, but a caller that already holds a parsed scene (e.g. a
172/// `gltf-rs` document) can implement it over that instead and reuse the exact
173/// same decode logic, without linking `draco-io`'s glTF reader.
174///
175/// Implementors only have to locate and copy out bytes; all of the geometry
176/// model building (faces, deduplication, attribute typing, multi-set semantics)
177/// lives once in [`decode_geometry`].
178pub trait AccessorSource {
179    /// Reads one attribute accessor, validating its glTF type against
180    /// `expected_types` (e.g. `["VEC3"]`) and component type against
181    /// `allowed_component_types` (glTF component-type constants).
182    fn read_attribute(
183        &self,
184        accessor: usize,
185        expected_types: &[&str],
186        allowed_component_types: &[u32],
187    ) -> Result<DecodedAccessor>;
188
189    /// Reads a `SCALAR` index accessor as `u32` values.
190    fn read_indices(&self, accessor: usize) -> Result<Vec<u32>>;
191}
192
193#[derive(Clone, Copy)]
194pub(crate) struct SemanticSpec {
195    pub(crate) attribute_type: GeometryAttributeType,
196    pub(crate) expected_accessor_types: &'static [&'static str],
197    pub(crate) allowed_component_types: &'static [u32],
198    normalization: NormalizationPolicy,
199}
200
201#[derive(Clone, Copy)]
202enum NormalizationPolicy {
203    Forbidden,
204    RequiredForInteger,
205    Generic,
206}
207
208const FLOAT_ONLY: &[u32] = &[GLTF_COMPONENT_FLOAT];
209const TEXCOORD_COMPONENT_TYPES: &[u32] = &[
210    GLTF_COMPONENT_FLOAT,
211    GLTF_COMPONENT_UNSIGNED_BYTE,
212    GLTF_COMPONENT_UNSIGNED_SHORT,
213];
214const COLOR_COMPONENT_TYPES: &[u32] = &[
215    GLTF_COMPONENT_FLOAT,
216    GLTF_COMPONENT_UNSIGNED_BYTE,
217    GLTF_COMPONENT_UNSIGNED_SHORT,
218];
219const JOINT_COMPONENT_TYPES: &[u32] =
220    &[GLTF_COMPONENT_UNSIGNED_BYTE, GLTF_COMPONENT_UNSIGNED_SHORT];
221const WEIGHT_COMPONENT_TYPES: &[u32] = &[
222    GLTF_COMPONENT_FLOAT,
223    GLTF_COMPONENT_UNSIGNED_BYTE,
224    GLTF_COMPONENT_UNSIGNED_SHORT,
225];
226const GENERIC_COMPONENT_TYPES: &[u32] = &[
227    GLTF_COMPONENT_BYTE,
228    GLTF_COMPONENT_UNSIGNED_BYTE,
229    GLTF_COMPONENT_SHORT,
230    GLTF_COMPONENT_UNSIGNED_SHORT,
231    GLTF_COMPONENT_FLOAT,
232];
233
234/// Decodes a non-Draco primitive's geometry into a [`Mesh`] plus its
235/// `(glTF semantic, Draco unique id)` mapping, reading attribute and index data
236/// through any [`AccessorSource`].
237///
238/// `mode` is the glTF primitive mode (only `POINTS` = 0 and `TRIANGLES` = 4 are
239/// supported), `attributes` maps each glTF semantic to its accessor index, and
240/// `indices` is the optional index accessor. This is the single place the
241/// geometry model is built — faces, deduplication, attribute typing, multi-set
242/// `TEXCOORD_n`/`COLOR_n`, `TANGENT`/`JOINTS_n`/`WEIGHTS_n`, and custom `_*`
243/// attributes — so different glTF front ends share it by implementing only
244/// [`AccessorSource`], never duplicating this logic.
245///
246/// The returned attribute ids equal the Draco unique ids referenced by the
247/// `KHR_draco_mesh_compression` attributes map.
248pub fn decode_geometry<S: AccessorSource>(
249    src: &S,
250    mode: u32,
251    attributes: &[(String, usize)],
252    indices: Option<usize>,
253) -> Result<(Mesh, Vec<(String, u32)>)> {
254    if mode != GLTF_MODE_TRIANGLES && mode != GLTF_MODE_POINTS {
255        return Err(GltfError::Unsupported(format!(
256            "Primitive mode {} not supported (only POINTS=0 and TRIANGLES=4)",
257            mode
258        )));
259    }
260
261    // POSITION is required.
262    let pos_accessor_idx = attributes
263        .iter()
264        .find(|(semantic, _)| semantic == "POSITION")
265        .map(|(_, accessor)| *accessor)
266        .ok_or_else(|| GltfError::InvalidGltf("primitive has no POSITION attribute".into()))?;
267
268    let positions = src.read_attribute(pos_accessor_idx, &["VEC3"], &[GLTF_COMPONENT_FLOAT])?;
269    validate_decoded_semantic("POSITION", &positions)?;
270
271    let mut mesh = Mesh::new();
272    let point_indices = if mode == GLTF_MODE_POINTS {
273        indices.map(|idx| src.read_indices(idx)).transpose()?
274    } else {
275        None
276    };
277    let positions = if let Some(idx) = &point_indices {
278        positions.gather(idx)?
279    } else {
280        positions
281    };
282    mesh.set_num_points(positions.count);
283
284    let mut semantics: Vec<(String, u32)> = Vec::new();
285    semantics.try_reserve_exact(attributes.len()).map_err(|_| {
286        GltfError::ResourceLimitExceeded("attribute semantic table allocation failed".into())
287    })?;
288    let pos_id = add_decoded_attribute(&mut mesh, GeometryAttributeType::Position, positions)?;
289    semantics.push(("POSITION".to_string(), pos_id as u32));
290
291    if mode == GLTF_MODE_TRIANGLES {
292        if let Some(indices_accessor_idx) = indices {
293            let indices = src.read_indices(indices_accessor_idx)?;
294            if indices.len() % 3 != 0 {
295                return Err(GltfError::InvalidGltf(
296                    "Index count not divisible by 3 for triangles".into(),
297                ));
298            }
299            for &index in &indices {
300                if index as usize >= mesh.num_points() {
301                    return Err(GltfError::InvalidGltf(format!(
302                        "Triangle index {} out of bounds for {} points",
303                        index,
304                        mesh.num_points()
305                    )));
306                }
307            }
308            let num_faces = indices.len() / 3;
309            mesh.try_set_num_faces(num_faces)
310                .map_err(GltfError::DracoEncode)?;
311            for (face_id, face) in indices.chunks_exact(3).enumerate() {
312                mesh.set_face_from_indices(face_id, [face[0], face[1], face[2]]);
313            }
314        } else {
315            // Non-indexed: generate sequential triangle faces.
316            if !mesh.num_points().is_multiple_of(3) {
317                return Err(GltfError::InvalidGltf(
318                    "Non-indexed primitive point count not divisible by 3".into(),
319                ));
320            }
321            let num_faces = mesh.num_points() / 3;
322            mesh.try_set_num_faces(num_faces)
323                .map_err(GltfError::DracoEncode)?;
324            for face_id in 0..num_faces {
325                let base = face_id
326                    .checked_mul(3)
327                    .and_then(|base| u32::try_from(base).ok())
328                    .ok_or_else(|| {
329                        GltfError::InvalidGltf(
330                            "Non-indexed primitive exceeds Draco's u32 point-id limit".into(),
331                        )
332                    })?;
333                let second = base
334                    .checked_add(1)
335                    .ok_or_else(|| GltfError::InvalidGltf("Triangle point-id overflow".into()))?;
336                let third = base
337                    .checked_add(2)
338                    .ok_or_else(|| GltfError::InvalidGltf("Triangle point-id overflow".into()))?;
339                mesh.set_face_from_indices(face_id, [base, second, third]);
340            }
341        }
342    }
343
344    // Optionally read NORMAL.
345    if let Some(normal_idx) = attributes
346        .iter()
347        .find(|(semantic, _)| semantic == "NORMAL")
348        .map(|(_, accessor)| *accessor)
349    {
350        let spec = supported_semantic_spec("NORMAL")?;
351        let normal_id = read_and_add_standard_attribute(
352            &mut mesh,
353            src,
354            normal_idx,
355            "NORMAL",
356            spec,
357            point_indices.as_deref(),
358        )?;
359        semantics.push(("NORMAL".to_string(), normal_id as u32));
360    }
361
362    // Read every remaining semantic in sorted order. Draco can carry multiple
363    // attributes with the same semantic type (extra TEXCOORD_n/COLOR_n), plus
364    // TANGENT, JOINTS_n, WEIGHTS_n, and custom `_*`.
365    let mut sorted: Vec<&(String, usize)> = Vec::new();
366    sorted.try_reserve_exact(attributes.len()).map_err(|_| {
367        GltfError::ResourceLimitExceeded("attribute sort table allocation failed".into())
368    })?;
369    sorted.extend(attributes.iter());
370    sorted.sort_by(|(left, _), (right, _)| left.cmp(right));
371    for (semantic, accessor_idx) in sorted {
372        if semantic == "POSITION" || semantic == "NORMAL" {
373            continue;
374        }
375        let attribute_spec = supported_semantic_spec(semantic)?;
376        let att_id = read_and_add_standard_attribute(
377            &mut mesh,
378            src,
379            *accessor_idx,
380            semantic,
381            attribute_spec,
382            point_indices.as_deref(),
383        )?;
384        semantics.push((semantic.clone(), att_id as u32));
385    }
386
387    // Match C++ Draco: deduplicate point IDs in face-traversal order for binary
388    // compatibility. Remapping does not change attribute ids, so `semantics`
389    // stays valid. (Draco-compressed meshes don't need this.)
390    mesh.deduplicate_point_ids();
391
392    Ok((mesh, semantics))
393}
394
395/// Reads the attribute for `semantic` from `src` and adds it to `mesh`,
396/// returning its attribute id. Used by the reader for side attributes that
397/// accompany a Draco stream but are not carried inside it.
398#[cfg(feature = "gltf-reader")]
399pub(crate) fn add_named_attribute<S: AccessorSource>(
400    mesh: &mut Mesh,
401    src: &S,
402    semantic: &str,
403    accessor_idx: usize,
404    point_indices: Option<&[u32]>,
405) -> Result<i32> {
406    let spec = supported_semantic_spec(semantic)?;
407    read_and_add_standard_attribute(mesh, src, accessor_idx, semantic, spec, point_indices)
408}
409
410fn read_and_add_standard_attribute<S: AccessorSource>(
411    mesh: &mut Mesh,
412    src: &S,
413    accessor_idx: usize,
414    semantic: &str,
415    spec: SemanticSpec,
416    point_indices: Option<&[u32]>,
417) -> Result<i32> {
418    let decoded = src.read_attribute(
419        accessor_idx,
420        spec.expected_accessor_types,
421        spec.allowed_component_types,
422    )?;
423    validate_decoded_semantic(semantic, &decoded)?;
424    let decoded = if let Some(indices) = point_indices {
425        decoded.gather(indices)?
426    } else {
427        decoded
428    };
429    add_decoded_attribute(mesh, spec.attribute_type, decoded)
430}
431
432/// Adds a decoded attribute to the mesh, returning the new attribute id (which
433/// equals its Draco unique id).
434fn add_decoded_attribute(
435    mesh: &mut Mesh,
436    attribute_type: GeometryAttributeType,
437    decoded: DecodedAccessor,
438) -> Result<i32> {
439    if decoded.count != mesh.num_points() {
440        return Err(GltfError::InvalidGltf(format!(
441            "Attribute {:?} has {} values but primitive has {} points",
442            attribute_type,
443            decoded.count,
444            mesh.num_points()
445        )));
446    }
447
448    let mut attribute = PointAttribute::new();
449    attribute
450        .try_init(
451            attribute_type,
452            decoded.num_components,
453            decoded.data_type,
454            decoded.normalized,
455            decoded.count,
456        )
457        .map_err(GltfError::DracoEncode)?;
458    if !attribute.buffer_mut().try_write(0, &decoded.bytes) {
459        return Err(GltfError::DracoEncode(draco_core::DracoError::BufferError(
460            "Decoded glTF attribute does not fit its Draco buffer".into(),
461        )));
462    }
463    Ok(mesh.add_attribute(attribute))
464}
465
466pub(crate) fn supported_semantic_spec(semantic: &str) -> Result<SemanticSpec> {
467    let spec = if semantic == "POSITION" {
468        SemanticSpec {
469            attribute_type: GeometryAttributeType::Position,
470            expected_accessor_types: &["VEC3"],
471            allowed_component_types: FLOAT_ONLY,
472            normalization: NormalizationPolicy::Forbidden,
473        }
474    } else if semantic == "NORMAL" {
475        SemanticSpec {
476            attribute_type: GeometryAttributeType::Normal,
477            expected_accessor_types: &["VEC3"],
478            allowed_component_types: FLOAT_ONLY,
479            normalization: NormalizationPolicy::Forbidden,
480        }
481    } else if semantic == "TANGENT" {
482        SemanticSpec {
483            attribute_type: GeometryAttributeType::Generic,
484            expected_accessor_types: &["VEC4"],
485            allowed_component_types: FLOAT_ONLY,
486            normalization: NormalizationPolicy::Forbidden,
487        }
488    } else if indexed_semantic(semantic, "TEXCOORD_") {
489        SemanticSpec {
490            attribute_type: GeometryAttributeType::TexCoord,
491            expected_accessor_types: &["VEC2"],
492            allowed_component_types: TEXCOORD_COMPONENT_TYPES,
493            normalization: NormalizationPolicy::RequiredForInteger,
494        }
495    } else if indexed_semantic(semantic, "COLOR_") {
496        SemanticSpec {
497            attribute_type: GeometryAttributeType::Color,
498            expected_accessor_types: &["VEC3", "VEC4"],
499            allowed_component_types: COLOR_COMPONENT_TYPES,
500            normalization: NormalizationPolicy::RequiredForInteger,
501        }
502    } else if indexed_semantic(semantic, "JOINTS_") {
503        SemanticSpec {
504            attribute_type: GeometryAttributeType::Generic,
505            expected_accessor_types: &["VEC4"],
506            allowed_component_types: JOINT_COMPONENT_TYPES,
507            normalization: NormalizationPolicy::Forbidden,
508        }
509    } else if indexed_semantic(semantic, "WEIGHTS_") {
510        SemanticSpec {
511            attribute_type: GeometryAttributeType::Generic,
512            expected_accessor_types: &["VEC4"],
513            allowed_component_types: WEIGHT_COMPONENT_TYPES,
514            normalization: NormalizationPolicy::RequiredForInteger,
515        }
516    } else if semantic.starts_with('_') && semantic.len() > 1 {
517        // Application-specific semantics are carried as generic Draco
518        // attributes. The semantic name remains in primitive.attributes and in
519        // the KHR_draco_mesh_compression attribute map.
520        SemanticSpec {
521            attribute_type: GeometryAttributeType::Generic,
522            expected_accessor_types: &["SCALAR", "VEC2", "VEC3", "VEC4"],
523            allowed_component_types: GENERIC_COMPONENT_TYPES,
524            normalization: NormalizationPolicy::Generic,
525        }
526    } else {
527        return Err(GltfError::InvalidGltf(format!(
528            "invalid glTF attribute semantic {semantic}"
529        )));
530    };
531
532    Ok(spec)
533}
534
535fn indexed_semantic(semantic: &str, prefix: &str) -> bool {
536    semantic
537        .strip_prefix(prefix)
538        .is_some_and(|index| !index.is_empty() && index.bytes().all(|byte| byte.is_ascii_digit()))
539}
540
541pub(crate) fn validate_semantic_accessor(
542    semantic: &str,
543    accessor_type: &str,
544    component_type: u32,
545    normalized: bool,
546) -> Result<SemanticSpec> {
547    let spec = supported_semantic_spec(semantic)?;
548    if !spec.expected_accessor_types.contains(&accessor_type) {
549        return Err(GltfError::InvalidGltf(format!(
550            "{semantic} accessor type {accessor_type} is invalid"
551        )));
552    }
553    if !spec.allowed_component_types.contains(&component_type) {
554        return Err(GltfError::InvalidGltf(format!(
555            "{semantic} accessor componentType {component_type} is invalid"
556        )));
557    }
558    let integer = component_type != GLTF_COMPONENT_FLOAT;
559    let valid_normalized = match spec.normalization {
560        NormalizationPolicy::Forbidden => !normalized,
561        NormalizationPolicy::RequiredForInteger => normalized == integer,
562        NormalizationPolicy::Generic => !normalized || integer,
563    };
564    if !valid_normalized {
565        return Err(GltfError::InvalidGltf(format!(
566            "{semantic} accessor normalized={normalized} is invalid for componentType {component_type}"
567        )));
568    }
569    Ok(spec)
570}
571
572fn validate_decoded_semantic(semantic: &str, accessor: &DecodedAccessor) -> Result<()> {
573    validate_semantic_accessor(
574        semantic,
575        gltf_type_for_num_components(accessor.num_components)?,
576        component_type_for_data_type(accessor.data_type)?,
577        accessor.normalized,
578    )?;
579    Ok(())
580}
581
582pub(crate) fn gltf_type_for_num_components(num_components: u8) -> Result<&'static str> {
583    match num_components {
584        1 => Ok("SCALAR"),
585        2 => Ok("VEC2"),
586        3 => Ok("VEC3"),
587        4 => Ok("VEC4"),
588        _ => Err(GltfError::InvalidGltf(format!(
589            "Invalid accessor component count: {num_components}"
590        ))),
591    }
592}
593
594pub(crate) fn component_type_for_data_type(data_type: DataType) -> Result<u32> {
595    match data_type {
596        DataType::Int8 => Ok(GLTF_COMPONENT_BYTE),
597        DataType::Uint8 => Ok(GLTF_COMPONENT_UNSIGNED_BYTE),
598        DataType::Int16 => Ok(GLTF_COMPONENT_SHORT),
599        DataType::Uint16 => Ok(GLTF_COMPONENT_UNSIGNED_SHORT),
600        #[cfg(feature = "gltf-reader")]
601        DataType::Uint32 => Ok(GLTF_COMPONENT_UNSIGNED_INT),
602        DataType::Float32 => Ok(GLTF_COMPONENT_FLOAT),
603        _ => Err(GltfError::Unsupported(format!(
604            "Unsupported Draco attribute data type for glTF: {data_type:?}"
605        ))),
606    }
607}