Skip to main content

draco_gltf/
packed.rs

1//! Materialized primitive geometry shared by glTF read and write operations.
2
3use std::collections::BTreeSet;
4
5#[cfg(feature = "draco-decode")]
6use draco_core::draco_types::DataType;
7#[cfg(feature = "draco-decode")]
8use draco_core::mesh::Mesh;
9use thiserror::Error as ThisError;
10
11use crate::{ComponentType, ValidationProfile};
12#[cfg(feature = "draco-decode")]
13use crate::{Error, Result};
14
15/// glTF primitive topology mode.
16#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
17#[repr(u32)]
18pub enum PrimitiveMode {
19    /// Independent points.
20    Points = 0,
21    /// Independent line segments.
22    Lines = 1,
23    /// Closed line loop.
24    LineLoop = 2,
25    /// Connected line strip.
26    LineStrip = 3,
27    /// Independent triangles.
28    #[default]
29    Triangles = 4,
30    /// Connected triangle strip.
31    TriangleStrip = 5,
32    /// Connected triangle fan.
33    TriangleFan = 6,
34}
35
36impl PrimitiveMode {
37    /// Converts a glTF primitive mode code to its typed representation.
38    pub fn from_gltf(value: u32) -> Option<Self> {
39        Some(match value {
40            0 => Self::Points,
41            1 => Self::Lines,
42            2 => Self::LineLoop,
43            3 => Self::LineStrip,
44            4 => Self::Triangles,
45            5 => Self::TriangleStrip,
46            6 => Self::TriangleFan,
47            _ => return None,
48        })
49    }
50
51    /// Returns the glTF numeric mode code.
52    pub const fn to_gltf(self) -> u32 {
53        self as u32
54    }
55}
56
57/// Validation errors for materialized primitive geometry.
58#[derive(Clone, Debug, ThisError, PartialEq, Eq)]
59pub enum GeometryError {
60    /// A byte-size calculation overflowed the addressable range.
61    #[error("packed geometry byte size overflow")]
62    ByteSizeOverflow,
63    /// A byte payload does not match its declared accessor layout.
64    #[error("packed {kind} has {actual} bytes, expected {expected}")]
65    ByteLength {
66        /// Kind of payload being checked.
67        kind: &'static str,
68        /// Actual byte length.
69        actual: usize,
70        /// Expected byte length.
71        expected: usize,
72    },
73    /// An attribute semantic occurs more than once.
74    #[error("duplicate packed attribute semantic {0:?}")]
75    DuplicateSemantic(String),
76    /// Vertex attributes do not agree on their element count.
77    #[error("attribute {semantic:?} has count {actual}, expected {expected}")]
78    AttributeCount {
79        /// Attribute semantic.
80        semantic: String,
81        /// Actual element count.
82        actual: usize,
83        /// Expected element count.
84        expected: usize,
85    },
86    /// A primitive has no POSITION attribute.
87    #[error("packed geometry is missing POSITION")]
88    MissingPosition,
89    /// A primitive has no vertices.
90    #[error("packed geometry has no vertices")]
91    EmptyGeometry,
92    /// A well-known attribute has the wrong number of components.
93    #[error("{semantic:?} has {actual} components; expected {expected}")]
94    AttributeComponents {
95        /// Attribute semantic.
96        semantic: String,
97        /// Actual component count.
98        actual: u8,
99        /// Required component count.
100        expected: &'static str,
101    },
102    /// A well-known attribute uses storage forbidden by the selected profile.
103    #[error("invalid {component_type:?}/normalized={normalized} for {semantic:?} in {profile:?}")]
104    AttributeComponentType {
105        /// Attribute semantic.
106        semantic: String,
107        /// Rejected scalar storage type.
108        component_type: ComponentType,
109        /// Whether normalized integer interpretation was requested.
110        normalized: bool,
111        /// Active validation profile.
112        profile: ValidationProfile,
113    },
114    /// A floating-point POSITION value cannot be represented in JSON bounds.
115    #[cfg(feature = "write")]
116    #[error("POSITION contains a non-finite floating-point value")]
117    NonFinitePosition,
118    /// The number of vertices or indices is invalid for the primitive mode.
119    #[error("invalid {mode:?} element count {count}")]
120    InvalidElementCount {
121        /// Primitive topology.
122        mode: PrimitiveMode,
123        /// Number of indexed or non-indexed elements.
124        count: usize,
125    },
126    /// A primitive mode code is outside the glTF core range.
127    #[error("primitive mode {0} is not supported")]
128    InvalidPrimitiveMode(u32),
129    /// A component count is not valid for primitive attributes.
130    #[error("packed attribute component count {0} is not supported")]
131    InvalidComponents(u8),
132    /// An index accessor uses a component type forbidden by the profile.
133    #[error("index component type {0:?} is not permitted")]
134    InvalidIndexType(ComponentType),
135    /// An index references a vertex outside the attribute range.
136    #[error("index {index} is outside vertex count {vertex_count}")]
137    IndexOutOfRange {
138        /// Invalid vertex index.
139        index: u64,
140        /// Number of vertices in the primitive.
141        vertex_count: usize,
142    },
143    /// Decoded Draco topology disagrees with its glTF accessor metadata.
144    #[error("decoded Draco {semantic} count {decoded} does not match accessor count {declared}")]
145    DracoAccessorCount {
146        /// Attribute semantic or `indices`.
147        semantic: String,
148        /// Count materialized from the Draco stream.
149        decoded: u64,
150        /// Count declared by the glTF accessor.
151        declared: u64,
152    },
153    /// A component type is outside the selected validation profile.
154    #[error("component type {component_type:?} is not permitted by {profile:?}")]
155    ComponentTypeProfile {
156        /// Rejected component type.
157        component_type: ComponentType,
158        /// Active validation profile.
159        profile: ValidationProfile,
160    },
161    /// Draco cannot represent the supplied geometry without conversion.
162    #[error("Draco encoding does not support {0}")]
163    UnsupportedDraco(String),
164    /// Existing morph targets would become invalid after replacement.
165    #[cfg(feature = "write")]
166    #[error("replacement vertex count {actual} does not match morph target count {expected}")]
167    MorphTargetCount {
168        /// Existing morph-target element count.
169        expected: usize,
170        /// Replacement vertex count.
171        actual: usize,
172    },
173}
174
175/// One materialized, tightly packed vertex attribute.
176///
177/// ```
178/// use draco_gltf::{ComponentType, PackedAttribute};
179///
180/// let position = PackedAttribute::new(
181///     "POSITION",
182///     1,
183///     3,
184///     ComponentType::F32,
185///     false,
186///     vec![0; 12],
187/// )?;
188/// assert_eq!(position.count(), 1);
189/// # Ok::<(), draco_gltf::GeometryError>(())
190/// ```
191#[derive(Clone, Debug, Eq)]
192pub struct PackedAttribute {
193    semantic: String,
194    count: usize,
195    components: u8,
196    component_type: ComponentType,
197    normalized: bool,
198    bytes: Vec<u8>,
199    source_accessor: Option<usize>,
200}
201
202/// Equality is over the vertex data, not over where it was read from.
203///
204/// `source_accessor` records provenance: the same bytes materialized from a
205/// different document, or re-materialized after a write, are the same
206/// attribute, and a round-trip that renumbers accessors has lost nothing.
207impl PartialEq for PackedAttribute {
208    fn eq(&self, other: &Self) -> bool {
209        self.semantic == other.semantic
210            && self.count == other.count
211            && self.components == other.components
212            && self.component_type == other.component_type
213            && self.normalized == other.normalized
214            && self.bytes == other.bytes
215    }
216}
217
218impl PackedAttribute {
219    /// Creates and validates a tightly packed vertex attribute.
220    pub fn new(
221        semantic: impl Into<String>,
222        count: usize,
223        components: u8,
224        component_type: ComponentType,
225        normalized: bool,
226        bytes: Vec<u8>,
227    ) -> std::result::Result<Self, GeometryError> {
228        if !(1..=4).contains(&components) {
229            return Err(GeometryError::InvalidComponents(components));
230        }
231        validate_byte_len("attribute", count, components, component_type, bytes.len())?;
232        Ok(Self {
233            semantic: semantic.into(),
234            count,
235            components,
236            component_type,
237            normalized,
238            bytes,
239            source_accessor: None,
240        })
241    }
242
243    /// Records which document accessor these bytes were materialized from.
244    ///
245    /// Primitives routinely share one accessor — a mesh split by material is
246    /// the usual case — and a consumer that rebuilds its own buffers has no
247    /// other way to notice, since the bytes arrive already materialized. Left
248    /// unset for compressed geometry, whose bytes come from the codec stream
249    /// rather than from the accessor the attribute names.
250    #[must_use]
251    pub fn with_source_accessor(mut self, accessor: usize) -> Self {
252        self.source_accessor = Some(accessor);
253        self
254    }
255
256    /// Returns the document accessor these bytes came from, when known.
257    pub const fn source_accessor(&self) -> Option<usize> {
258        self.source_accessor
259    }
260
261    /// Returns the glTF attribute semantic.
262    pub fn semantic(&self) -> &str {
263        &self.semantic
264    }
265
266    /// Returns the number of attribute elements.
267    pub const fn count(&self) -> usize {
268        self.count
269    }
270
271    /// Returns the number of scalar components in each element.
272    pub const fn components(&self) -> u8 {
273        self.components
274    }
275
276    /// Returns the scalar storage type.
277    pub const fn component_type(&self) -> ComponentType {
278        self.component_type
279    }
280
281    /// Returns whether integer values use normalized interpretation.
282    pub const fn normalized(&self) -> bool {
283        self.normalized
284    }
285
286    /// Borrows the tightly packed row-major bytes.
287    pub fn bytes(&self) -> &[u8] {
288        &self.bytes
289    }
290}
291
292/// One materialized, tightly packed primitive index stream.
293#[derive(Clone, Debug, Eq)]
294pub struct PackedIndices {
295    count: usize,
296    component_type: ComponentType,
297    bytes: Vec<u8>,
298    source_accessor: Option<usize>,
299}
300
301/// Equality is over the index data; see [`PackedAttribute`]'s implementation.
302impl PartialEq for PackedIndices {
303    fn eq(&self, other: &Self) -> bool {
304        self.count == other.count
305            && self.component_type == other.component_type
306            && self.bytes == other.bytes
307    }
308}
309
310impl PackedIndices {
311    /// Creates and validates a tightly packed scalar index stream.
312    pub fn new(
313        count: usize,
314        component_type: ComponentType,
315        bytes: Vec<u8>,
316    ) -> std::result::Result<Self, GeometryError> {
317        if !matches!(
318            component_type,
319            ComponentType::U8 | ComponentType::U16 | ComponentType::U32
320        ) {
321            return Err(GeometryError::InvalidIndexType(component_type));
322        }
323        validate_byte_len("indices", count, 1, component_type, bytes.len())?;
324        Ok(Self {
325            count,
326            component_type,
327            bytes,
328            source_accessor: None,
329        })
330    }
331
332    /// Records which document accessor these indices were materialized from.
333    ///
334    /// See [`PackedAttribute::with_source_accessor`]; the same sharing applies.
335    #[must_use]
336    pub fn with_source_accessor(mut self, accessor: usize) -> Self {
337        self.source_accessor = Some(accessor);
338        self
339    }
340
341    /// Returns the document accessor these indices came from, when known.
342    pub const fn source_accessor(&self) -> Option<usize> {
343        self.source_accessor
344    }
345
346    /// Returns the number of indices.
347    pub const fn count(&self) -> usize {
348        self.count
349    }
350
351    /// Returns the scalar index storage type.
352    pub const fn component_type(&self) -> ComponentType {
353        self.component_type
354    }
355
356    /// Borrows the tightly packed little-endian index bytes.
357    pub fn bytes(&self) -> &[u8] {
358        &self.bytes
359    }
360}
361
362/// Materialized primitive geometry with contiguous attribute and index buffers.
363///
364/// ```
365/// use draco_gltf::{ComponentType, PackedAttribute, PackedGeometry, PrimitiveMode};
366///
367/// let position = PackedAttribute::new(
368///     "POSITION", 1, 3, ComponentType::F32, false, vec![0; 12],
369/// )?;
370/// let geometry = PackedGeometry::new(PrimitiveMode::Points, vec![position], None)?;
371/// assert_eq!(geometry.vertex_count(), 1);
372/// # Ok::<(), draco_gltf::GeometryError>(())
373/// ```
374#[derive(Clone, Debug, PartialEq, Eq)]
375pub struct PackedGeometry {
376    mode: PrimitiveMode,
377    indices: Option<PackedIndices>,
378    attributes: Vec<PackedAttribute>,
379}
380
381impl PackedGeometry {
382    /// Creates and validates one materialized primitive.
383    pub fn new(
384        mode: PrimitiveMode,
385        attributes: Vec<PackedAttribute>,
386        indices: Option<PackedIndices>,
387    ) -> std::result::Result<Self, GeometryError> {
388        let geometry = Self {
389            mode,
390            indices,
391            attributes,
392        };
393        geometry.validate(ValidationProfile::Gltf21Draft)?;
394        Ok(geometry)
395    }
396
397    /// Returns the primitive topology.
398    pub const fn mode(&self) -> PrimitiveMode {
399        self.mode
400    }
401
402    /// Returns the shared vertex count.
403    pub fn vertex_count(&self) -> usize {
404        self.attributes.first().map_or(0, PackedAttribute::count)
405    }
406
407    /// Borrows the packed vertex attributes in document order.
408    pub fn attributes(&self) -> &[PackedAttribute] {
409        &self.attributes
410    }
411
412    /// Borrows the optional packed index stream.
413    pub fn indices(&self) -> Option<&PackedIndices> {
414        self.indices.as_ref()
415    }
416
417    /// Validates the geometry against a glTF profile.
418    pub fn validate(&self, profile: ValidationProfile) -> std::result::Result<(), GeometryError> {
419        let mut semantics = BTreeSet::new();
420        let mut vertex_count = None;
421        for attribute in &self.attributes {
422            validate_component_profile(attribute.component_type, profile)?;
423            validate_attribute_components(attribute)?;
424            validate_attribute_profile(attribute, profile)?;
425            if !semantics.insert(attribute.semantic.as_str()) {
426                return Err(GeometryError::DuplicateSemantic(attribute.semantic.clone()));
427            }
428            match vertex_count {
429                None => vertex_count = Some(attribute.count),
430                Some(expected) if expected != attribute.count => {
431                    return Err(GeometryError::AttributeCount {
432                        semantic: attribute.semantic.clone(),
433                        actual: attribute.count,
434                        expected,
435                    })
436                }
437                _ => {}
438            }
439        }
440        if !semantics.contains("POSITION") {
441            return Err(GeometryError::MissingPosition);
442        }
443        let vertex_count = vertex_count.unwrap_or(0);
444        if vertex_count == 0 {
445            return Err(GeometryError::EmptyGeometry);
446        }
447        if let Some(indices) = &self.indices {
448            validate_component_profile(indices.component_type, profile)?;
449            for index in index_values(indices) {
450                let index = index?;
451                if index >= vertex_count as u64 {
452                    return Err(GeometryError::IndexOutOfRange {
453                        index,
454                        vertex_count,
455                    });
456                }
457            }
458        }
459        validate_element_count(
460            self.mode,
461            self.indices
462                .as_ref()
463                .map_or(vertex_count, PackedIndices::count),
464        )?;
465        Ok(())
466    }
467
468    #[cfg(feature = "draco-decode")]
469    pub(crate) fn from_draco_mesh(
470        mode: PrimitiveMode,
471        mesh: &Mesh,
472        attributes: &[(String, u32)],
473        normalized: &std::collections::BTreeMap<String, bool>,
474    ) -> Result<Self> {
475        let attributes = attributes
476            .iter()
477            .map(|(semantic, unique_id)| {
478                let attribute = mesh.attribute_by_unique_id(*unique_id).ok_or_else(|| {
479                    Error::Geometry(GeometryError::UnsupportedDraco(format!(
480                        "decoded attribute {unique_id} is missing"
481                    )))
482                })?;
483                PackedAttribute::new(
484                    semantic.clone(),
485                    mesh.num_points(),
486                    attribute.num_components(),
487                    component_type_for_data_type(attribute.data_type())?,
488                    // The glTF accessor is authoritative here; the decoded
489                    // Draco attribute carries its own flag, which encoders
490                    // leave unset even for normalized colours and weights.
491                    normalized
492                        .get(semantic.as_str())
493                        .copied()
494                        .unwrap_or_else(|| attribute.normalized()),
495                    packed_draco_attribute_bytes(mesh, *unique_id)?,
496                )
497                .map_err(Error::Geometry)
498            })
499            .collect::<Result<Vec<_>>>()?;
500        let count = mesh
501            .num_faces()
502            .checked_mul(3)
503            .ok_or(Error::Geometry(GeometryError::ByteSizeOverflow))?;
504        let indices =
505            PackedIndices::new(count, ComponentType::U32, packed_draco_index_bytes(mesh)?)
506                .map_err(Error::Geometry)?;
507        Self::new(mode, attributes, Some(indices)).map_err(Error::Geometry)
508    }
509}
510
511fn validate_element_count(
512    mode: PrimitiveMode,
513    count: usize,
514) -> std::result::Result<(), GeometryError> {
515    let valid = match mode {
516        PrimitiveMode::Points => count >= 1,
517        PrimitiveMode::Lines => count >= 2 && count.is_multiple_of(2),
518        PrimitiveMode::LineLoop | PrimitiveMode::LineStrip => count >= 2,
519        PrimitiveMode::Triangles => count >= 3 && count.is_multiple_of(3),
520        PrimitiveMode::TriangleStrip | PrimitiveMode::TriangleFan => count >= 3,
521    };
522    if !valid {
523        return Err(GeometryError::InvalidElementCount { mode, count });
524    }
525    Ok(())
526}
527
528fn validate_attribute_components(
529    attribute: &PackedAttribute,
530) -> std::result::Result<(), GeometryError> {
531    let expected = if attribute.semantic == "POSITION" || attribute.semantic == "NORMAL" {
532        Some("3")
533    } else if attribute.semantic == "TANGENT"
534        || attribute.semantic.starts_with("JOINTS_")
535        || attribute.semantic.starts_with("WEIGHTS_")
536    {
537        Some("4")
538    } else if attribute.semantic.starts_with("TEXCOORD_") {
539        Some("2")
540    } else if attribute.semantic.starts_with("COLOR_") && !matches!(attribute.components, 3 | 4) {
541        Some("3 or 4")
542    } else {
543        None
544    };
545    if let Some(expected) = expected {
546        let valid = match expected {
547            "2" => attribute.components == 2,
548            "3" => attribute.components == 3,
549            "4" => attribute.components == 4,
550            "3 or 4" => matches!(attribute.components, 3 | 4),
551            _ => unreachable!("known component requirement"),
552        };
553        if !valid {
554            return Err(GeometryError::AttributeComponents {
555                semantic: attribute.semantic.clone(),
556                actual: attribute.components,
557                expected,
558            });
559        }
560    }
561    Ok(())
562}
563
564fn validate_attribute_profile(
565    attribute: &PackedAttribute,
566    profile: ValidationProfile,
567) -> std::result::Result<(), GeometryError> {
568    if profile != ValidationProfile::Gltf20 {
569        return Ok(());
570    }
571    let float = attribute.component_type == ComponentType::F32 && !attribute.normalized;
572    let normalized_unsigned = matches!(
573        attribute.component_type,
574        ComponentType::U8 | ComponentType::U16
575    ) && attribute.normalized;
576    let valid = if matches!(
577        attribute.semantic.as_str(),
578        "POSITION" | "NORMAL" | "TANGENT"
579    ) {
580        float
581    } else if attribute.semantic.starts_with("TEXCOORD_")
582        || attribute.semantic.starts_with("COLOR_")
583        || attribute.semantic.starts_with("WEIGHTS_")
584    {
585        float || normalized_unsigned
586    } else if attribute.semantic.starts_with("JOINTS_") {
587        matches!(
588            attribute.component_type,
589            ComponentType::U8 | ComponentType::U16
590        ) && !attribute.normalized
591    } else {
592        true
593    };
594    if !valid {
595        return Err(GeometryError::AttributeComponentType {
596            semantic: attribute.semantic.clone(),
597            component_type: attribute.component_type,
598            normalized: attribute.normalized,
599            profile,
600        });
601    }
602    Ok(())
603}
604
605fn validate_component_profile(
606    component_type: ComponentType,
607    profile: ValidationProfile,
608) -> std::result::Result<(), GeometryError> {
609    if profile == ValidationProfile::Gltf20
610        && !matches!(
611            component_type,
612            ComponentType::I8
613                | ComponentType::U8
614                | ComponentType::I16
615                | ComponentType::U16
616                | ComponentType::U32
617                | ComponentType::F32
618        )
619    {
620        return Err(GeometryError::ComponentTypeProfile {
621            component_type,
622            profile,
623        });
624    }
625    Ok(())
626}
627
628fn validate_byte_len(
629    kind: &'static str,
630    count: usize,
631    components: u8,
632    component_type: ComponentType,
633    actual: usize,
634) -> std::result::Result<(), GeometryError> {
635    let expected = count
636        .checked_mul(components as usize)
637        .and_then(|value| value.checked_mul(component_type.byte_width()))
638        .ok_or(GeometryError::ByteSizeOverflow)?;
639    if actual != expected {
640        return Err(GeometryError::ByteLength {
641            kind,
642            actual,
643            expected,
644        });
645    }
646    Ok(())
647}
648
649fn index_values(
650    indices: &PackedIndices,
651) -> impl Iterator<Item = std::result::Result<u64, GeometryError>> + '_ {
652    let width = indices.component_type.byte_width();
653    indices.bytes.chunks_exact(width).map(move |bytes| {
654        Ok(match indices.component_type {
655            ComponentType::U8 => bytes[0] as u64,
656            ComponentType::U16 => u16::from_le_bytes(bytes.try_into().unwrap()) as u64,
657            ComponentType::U32 => u32::from_le_bytes(bytes.try_into().unwrap()) as u64,
658            _ => return Err(GeometryError::InvalidIndexType(indices.component_type)),
659        })
660    })
661}
662
663#[cfg(feature = "draco-decode")]
664fn component_type_for_data_type(data_type: DataType) -> Result<ComponentType> {
665    match data_type {
666        DataType::Int8 => Ok(ComponentType::I8),
667        DataType::Uint8 => Ok(ComponentType::U8),
668        DataType::Int16 => Ok(ComponentType::I16),
669        DataType::Uint16 => Ok(ComponentType::U16),
670        DataType::Int32 => Ok(ComponentType::I32),
671        DataType::Uint32 => Ok(ComponentType::U32),
672        DataType::Float32 => Ok(ComponentType::F32),
673        DataType::Int64 => Ok(ComponentType::I64),
674        DataType::Uint64 => Ok(ComponentType::U64),
675        DataType::Float64 => Ok(ComponentType::F64),
676        other => Err(Error::Geometry(GeometryError::UnsupportedDraco(format!(
677            "component type {other:?}"
678        )))),
679    }
680}
681
682#[cfg(feature = "draco-decode")]
683fn packed_draco_attribute_bytes(mesh: &Mesh, unique_id: u32) -> Result<Vec<u8>> {
684    let attribute = mesh.attribute_by_unique_id(unique_id).ok_or_else(|| {
685        Error::Geometry(GeometryError::UnsupportedDraco(format!(
686            "decoded attribute {unique_id} is missing"
687        )))
688    })?;
689    let stride = usize::try_from(attribute.byte_stride()).map_err(|_| {
690        Error::Geometry(GeometryError::UnsupportedDraco(
691            "decoded attribute stride is invalid".into(),
692        ))
693    })?;
694    let byte_len = mesh
695        .num_points()
696        .checked_mul(stride)
697        .ok_or(Error::Geometry(GeometryError::ByteSizeOverflow))?;
698    let mut out = vec![0; byte_len];
699    let mut row = vec![0; stride];
700    for point in 0..mesh.num_points() {
701        let index = attribute.mapped_index(draco_core::PointIndex(point as u32));
702        if !attribute
703            .buffer()
704            .try_read(index.0 as usize * stride, &mut row)
705        {
706            return Err(Error::Geometry(GeometryError::UnsupportedDraco(
707                "decoded attribute is out of bounds".into(),
708            )));
709        }
710        out[point * stride..(point + 1) * stride].copy_from_slice(&row);
711    }
712    Ok(out)
713}
714
715#[cfg(feature = "draco-decode")]
716fn packed_draco_index_bytes(mesh: &Mesh) -> Result<Vec<u8>> {
717    let byte_len = mesh
718        .num_faces()
719        .checked_mul(3)
720        .and_then(|value| value.checked_mul(4))
721        .ok_or(Error::Geometry(GeometryError::ByteSizeOverflow))?;
722    let mut out = Vec::with_capacity(byte_len);
723    for face in 0..mesh.num_faces() {
724        for index in mesh.face(draco_core::FaceIndex(face as u32)) {
725            out.extend_from_slice(&index.0.to_le_bytes());
726        }
727    }
728    Ok(out)
729}