Skip to main content

draco_core/
mesh_encoder.rs

1use crate::attribute_quantization_transform::AttributeQuantizationTransform;
2use crate::attribute_transform::AttributeTransform;
3use crate::compression_config::EncodedGeometryType;
4use crate::compression_config::MeshEncodingMethod;
5use crate::corner_table::CornerTable;
6use crate::draco_types::DataType;
7use crate::encoder_buffer::EncoderBuffer;
8use crate::encoder_options::EncoderOptions;
9use crate::geometry_attribute::{GeometryAttributeType, PointAttribute};
10use crate::geometry_indices::{
11    FaceIndex, PointIndex, INVALID_ATTRIBUTE_VALUE_INDEX, INVALID_CORNER_INDEX,
12};
13use crate::mesh::Mesh;
14use crate::mesh_edgebreaker_encoder::{
15    select_edgebreaker_traversal, EdgebreakerAttributeConnectivity, EdgebreakerTraversal,
16    MeshEdgebreakerEncoder,
17};
18use crate::metadata::METADATA_FLAG_MASK;
19use crate::point_cloud::PointCloud;
20use crate::point_cloud_encoder::GeometryEncoder;
21use crate::prediction_scheme::{
22    EntryToPointIdMap, PredictionSchemeMethod, PredictionSchemeTransformType,
23};
24use crate::sequential_attribute_encoder::{
25    select_sequential_encoder, SequentialAttributeEncoder, SequentialAttributeEncoderType,
26};
27use crate::sequential_integer_attribute_encoder::SequentialIntegerAttributeEncoder;
28use crate::sequential_normal_attribute_encoder::SequentialNormalAttributeEncoder;
29use crate::status::{DracoError, Status};
30use crate::version::{
31    has_header_flags, uses_varint_encoding, uses_varint_unique_id, DEFAULT_MESH_VERSION,
32};
33
34/// Picks EdgeBreaker or sequential connectivity, as C++ `ExpertEncoder` does.
35///
36/// Shared by `encode_header` and by the version validation that runs before it,
37/// so the version a stream is checked against is the one it is written with.
38/// The two used to derive it separately, which is how a check can pass for a
39/// coder the encoder then does not use.
40fn select_mesh_encoding_method(options: &EncoderOptions) -> i32 {
41    // C++ default: EdgeBreaker unless speed is 10, which asks for sequential.
42    match options.get_global_int("encoding_method", -1) {
43        -1 if options.get_speed() == 10 => 0,
44        -1 => 1,
45        1 => 1,
46        _ => 0,
47    }
48}
49
50/// `(min, max)` per-component position bounds, each present when computable.
51type PositionBounds = (Option<Vec<f64>>, Option<Vec<f64>>);
52
53/// Encoder for Draco triangle mesh bitstreams.
54///
55/// A `MeshEncoder` takes a [`Mesh`] plus [`EncoderOptions`] and writes a
56/// self-contained `.drc` bitstream (header, optional metadata, connectivity,
57/// and attributes) into an [`EncoderBuffer`]. The encoding method (EdgeBreaker or
58/// sequential), prediction schemes, and quantization are selected from the
59/// options, mirroring the C++ `MeshEncoder`/`ExpertEncoder` configuration.
60///
61/// [`encode`](MeshEncoder::encode) produces the bitstream and nothing else.
62/// A caller who also wants per-attribute and per-face details of what the
63/// encode did uses [`encode_with_info`](MeshEncoder::encode_with_info), which
64/// derives them; they are not a byproduct of encoding and are not computed
65/// for callers who do not ask.
66///
67/// # Examples
68///
69/// Build a single-triangle mesh, encode it, and decode it back:
70///
71/// ```
72/// use draco_core::{
73///     DataType, DecoderBuffer, EncoderBuffer, EncoderOptions, FaceIndex,
74///     GeometryAttributeType, Mesh, MeshDecoder, MeshEncoder, PointAttribute,
75/// };
76///
77/// // One triangle with a float32 position attribute (3 vertices).
78/// let mut mesh = Mesh::new();
79/// let mut position = PointAttribute::new();
80/// position.init(GeometryAttributeType::Position, 3, DataType::Float32, false, 3);
81/// let coords: [f32; 9] = [0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0, 0.0];
82/// for (i, value) in coords.iter().enumerate() {
83///     position.buffer_mut().write(i * 4, &value.to_le_bytes());
84/// }
85/// mesh.add_attribute(position);
86/// mesh.set_num_faces(1);
87/// mesh.set_face(FaceIndex(0), [0u32.into(), 1u32.into(), 2u32.into()]);
88///
89/// // Encode to a Draco bitstream.
90/// let mut encoder = MeshEncoder::new();
91/// encoder.set_mesh(mesh);
92/// let mut buffer = EncoderBuffer::new();
93/// encoder.encode(&EncoderOptions::new(), &mut buffer)?;
94///
95/// // Decode it back.
96/// let mut decoded = Mesh::new();
97/// MeshDecoder::new().decode(&mut DecoderBuffer::new(buffer.data()), &mut decoded)?;
98/// assert_eq!(decoded.num_faces(), 1);
99/// # Ok::<(), draco_core::DracoError>(())
100/// ```
101pub struct MeshEncoder {
102    mesh: Option<Mesh>,
103    options: EncoderOptions,
104    num_encoded_faces: usize,
105    corner_table: Option<CornerTable>,
106    point_ids: Vec<PointIndex>,
107    data_to_corner_map: Option<Vec<u32>>,
108    vertex_to_data_map: Option<Vec<i32>>,
109    edgebreaker_attribute_connectivity: Vec<EdgebreakerAttributeConnectivity>,
110    active_corner_table: Option<CornerTable>,
111    active_data_to_corner_map: Option<Vec<u32>>,
112    active_vertex_to_data_map: Option<Vec<i32>>,
113    /// What the per-attribute-connectivity encode found, kept for
114    /// [`Self::encode_with_info`] so the report does not walk every attribute
115    /// again: the values each attribute encoded, by attribute id, and
116    /// [`AttributeSeamCorners`] over the attributes with interior seams.
117    split_value_counts: Vec<(i32, usize)>,
118    attribute_seam_corners: AttributeSeamCorners,
119    /// Set by [`Self::encode_with_info`] for its encode alone, so a plain
120    /// encode does not mark seam corners nobody reads.
121    mark_attribute_seam_corners: bool,
122    /// Depth-first order for the non-position attribute groups, present only
123    /// when the position group uses a different one (speed 0).
124    #[allow(clippy::type_complexity)]
125    attribute_traversal: Option<(Vec<PointIndex>, Vec<u32>, Vec<i32>)>,
126    /// The parents: attributes other schemes predict from, each with the
127    /// portable copy it is read through. See [`ParentAttributes`].
128    parent_attributes: ParentAttributes,
129    /// Kept past `encode_edgebreaker_connectivity` for its corner order, which
130    /// an attribute with interior seams needs to walk its own corner table.
131    edgebreaker_encoder: Option<MeshEdgebreakerEncoder>,
132    method: i32,
133    /// Maps point indices to vertex indices in the corner table.
134    /// Used when position-based deduplication is enabled.
135    /// Whether we're using single connectivity (all attributes share same corner table).
136    use_single_connectivity: bool,
137    /// Prediction choices made by the attribute encoders, keyed by attribute
138    /// id. Collected as encoding runs because the encoders are built at their
139    /// use site and dropped there, and only they know what they settled on.
140    attribute_predictions: Vec<(i32, PredictionSchemeMethod, PredictionSchemeTransformType)>,
141    /// The quantization parameters each attribute was encoded with, keyed by
142    /// attribute id. Kept for the same reason as `attribute_predictions`: the
143    /// encoded-mesh-info pass runs after the attribute encoders are gone and
144    /// would otherwise recompute these, and recomputing means a second full
145    /// min/max sweep of the attribute -- a pass the reference never makes.
146    attribute_quantization: Vec<(i32, AttributeQuantizationTransform)>,
147}
148
149/// Geometry shape, encoder choices and attribute metadata produced by a
150/// successful mesh encode.
151///
152/// The encoder decides several things the caller does not state: the
153/// connectivity coder, the EdgeBreaker traversal, whether attributes share one
154/// connectivity, and a prediction scheme per attribute. Everything it resolved
155/// is reported here, so "what did this encode actually do" is answerable
156/// without re-deriving the selection rules or parsing the stream back.
157#[derive(Debug, Clone, PartialEq)]
158#[non_exhaustive]
159pub struct EncodedMeshInfo {
160    /// Numeric Draco mesh encoding method used for the output.
161    pub encoding_method: i32,
162    /// Bitstream version written, after the default was substituted for an
163    /// unset one.
164    pub bitstream_version: (u8, u8),
165    /// EdgeBreaker traversal written, or `None` for sequential connectivity.
166    pub traversal: Option<EdgebreakerTraversal>,
167    /// Speed the choices above were made at, after `encoding_speed` and
168    /// `decoding_speed` were resolved into one value.
169    pub speed: i32,
170    /// Whether every attribute shared the position's connectivity. When false,
171    /// attributes with seams were encoded against their own corner tables.
172    pub single_connectivity: bool,
173    /// Number of faces encoded into the bitstream.
174    pub num_encoded_faces: usize,
175    /// Number of points encoded into the bitstream.
176    pub num_encoded_points: usize,
177    /// Per-attribute information captured during encoding.
178    pub attributes: Vec<EncodedAttributeInfo>,
179}
180
181/// Attribute metadata produced by a successful mesh encode.
182#[derive(Debug, Clone, PartialEq)]
183#[non_exhaustive]
184pub struct EncodedAttributeInfo {
185    /// Source attribute id in the input mesh.
186    pub source_attribute_id: i32,
187    /// Semantic type of the encoded attribute.
188    pub attribute_type: GeometryAttributeType,
189    /// Scalar data type of the encoded attribute.
190    pub data_type: DataType,
191    /// Number of scalar components per encoded value.
192    pub num_components: u8,
193    /// Whether integer values are normalized.
194    pub normalized: bool,
195    /// Draco unique id assigned to the attribute.
196    pub unique_id: u32,
197    /// Number of unique values encoded for the attribute.
198    pub num_encoded_values: usize,
199    /// Per-attribute encoder the values went through, which is what decides
200    /// whether the two fields below are populated.
201    pub encoder_type: SequentialAttributeEncoderType,
202    /// Quantization bits applied, or `None` when the attribute was not
203    /// quantized. A `quantization_bits` option set on an integer or generic
204    /// attribute is ignored by the encoder and reported as `None` here.
205    pub quantization_bits: Option<i32>,
206    /// Prediction scheme and transform the encoder settled on, or `None` when
207    /// the attribute never reached the integer path. This is the resolved
208    /// choice, not the request: several schemes fall back to `Difference` when
209    /// the attribute or the mesh cannot support them.
210    pub prediction: Option<(PredictionSchemeMethod, PredictionSchemeTransformType)>,
211    /// Minimum position components when known for position attributes.
212    pub position_min: Option<Vec<f64>>,
213    /// Maximum position components when known for position attributes.
214    pub position_max: Option<Vec<f64>>,
215}
216
217/// The parents, and the copies they are read through, in one place.
218///
219/// Upstream splits this across the controller: `MarkParentAttribute` marks an
220/// attribute another scheme predicts from, `is_parent_encoder()` reads the
221/// mark when the point map is rebuilt, and `GetPortableAttribute` answers for
222/// the copy. Here the mark and the copy are one entry: [`Self::register`]
223/// takes the copy as an argument and presence in the map *is* the mark, so a
224/// parent without a registered copy is unrepresentable rather than a missed
225/// registration that answers as success. Which attributes are parents is
226/// decided by [`Self::is_prediction_parent`] and nowhere else.
227#[derive(Default)]
228struct ParentAttributes {
229    entries: Vec<(i32, PointAttribute)>,
230}
231
232impl ParentAttributes {
233    fn new() -> Self {
234        Self::default()
235    }
236
237    fn clear(&mut self) {
238        self.entries.clear();
239    }
240
241    /// Whether other schemes predict from `att`.
242    ///
243    /// Every parent-reading scheme declares a `Position` parent, and the
244    /// schemes are in play below speed 4 or when asked for by number -- the
245    /// conditions `position_is_a_prediction_parent` spells out. This is the
246    /// one derivation; the encode paths register from it and never re-derive.
247    #[cfg(feature = "encoder")]
248    fn is_prediction_parent(
249        att: &PointAttribute,
250        point_cloud: &PointCloud,
251        options: &EncoderOptions,
252    ) -> bool {
253        att.attribute_type() == GeometryAttributeType::Position
254            && position_is_a_prediction_parent(point_cloud, options)
255    }
256
257    /// Marks `att_id` a parent and gives it its portable copy, in one call.
258    ///
259    /// The copy is half of what a parent is: registering without one is not
260    /// expressible, and the copy arrives with the point map already rebuilt
261    /// into encoding order, which is what a predictor reading
262    /// `mapped_index(point_id)` needs.
263    fn register(&mut self, att_id: i32, copy: PointAttribute) {
264        match self.entries.iter_mut().find(|(id, _)| *id == att_id) {
265            Some((_, existing)) => *existing = copy,
266            None => self.entries.push((att_id, copy)),
267        }
268    }
269
270    /// The registered copy of `att_id`, when it is a parent.
271    ///
272    /// Upstream's `GetPortableAttribute` falls back to the attribute itself
273    /// when its encoder made no portable form. That fallback is not answered
274    /// here: a parent is its registered copy, and anything else is not one.
275    /// The binding sites that need the attribute itself hold it separately
276    /// and choose between the two where upstream's
277    /// `portable_attribute_ != nullptr ? portable : attribute()` sits, so the
278    /// choice is written where it is made rather than folded into the lookup.
279    fn get(&self, att_id: i32) -> Option<&PointAttribute> {
280        self.entries
281            .iter()
282            .find(|(id, _)| *id == att_id)
283            .map(|(_, att)| att)
284    }
285}
286
287impl GeometryEncoder for MeshEncoder {
288    fn point_cloud(&self) -> Option<&PointCloud> {
289        self.mesh.as_ref().map(|m| m as &PointCloud)
290    }
291
292    fn mesh(&self) -> Option<&Mesh> {
293        self.mesh.as_ref()
294    }
295
296    fn corner_table(&self) -> Option<&CornerTable> {
297        self.active_corner_table
298            .as_ref()
299            .or(self.corner_table.as_ref())
300    }
301
302    fn options(&self) -> &EncoderOptions {
303        &self.options
304    }
305
306    fn get_geometry_type(&self) -> EncodedGeometryType {
307        EncodedGeometryType::TriangularMesh
308    }
309
310    fn get_encoding_method(&self) -> Option<i32> {
311        Some(self.method)
312    }
313
314    fn get_data_to_corner_map(&self) -> Option<&[u32]> {
315        self.active_data_to_corner_map
316            .as_deref()
317            .or(self.data_to_corner_map.as_deref())
318    }
319
320    fn get_vertex_to_data_map(&self) -> Option<&[i32]> {
321        self.active_vertex_to_data_map
322            .as_deref()
323            .or(self.vertex_to_data_map.as_deref())
324    }
325
326    fn get_portable_attribute(&self, att_id: i32) -> Option<&PointAttribute> {
327        self.parent_attributes.get(att_id)
328    }
329}
330
331/// Rebuilds a portable attribute's point map, as upstream does in
332/// `SequentialIntegerAttributeEncoder::TransformAttributeToPortableFormat`.
333///
334/// The values were written in encoding order, but a prediction scheme reads its
335/// parent as `mapped_index(point_id)`; without this the lookup returns whichever
336/// vertex happens to sit at that index in the traversal, and encoder and decoder
337/// predict from different positions.
338#[cfg(feature = "encoder")]
339fn rebuild_parent_point_map(
340    attribute: &PointAttribute,
341    portable: &mut PointAttribute,
342    point_ids: &[PointIndex],
343    num_points: usize,
344) -> Status {
345    let mut value_to_value = vec![0u32; attribute.size().max(1)];
346    for (entry, &point_id) in point_ids.iter().enumerate() {
347        let src = attribute.mapped_index(point_id);
348        if (src.0 as usize) < value_to_value.len() {
349            value_to_value[src.0 as usize] = entry as u32;
350        }
351    }
352    portable.set_explicit_mapping(num_points);
353    for point in 0..num_points {
354        let src = attribute.mapped_index(PointIndex(point as u32));
355        let entry = value_to_value
356            .get(src.0 as usize)
357            .copied()
358            .unwrap_or_default();
359        portable.try_set_point_map_entry(
360            PointIndex(point as u32),
361            crate::geometry_indices::AttributeValueIndex(entry),
362        )?;
363    }
364    Ok(())
365}
366
367/// Whether a prediction scheme that reads the position as its parent will be
368/// used, so the position needs a portable form for the predictor to read.
369///
370/// Speed is upstream's own condition -- the tex-coords-portable and
371/// geometric-normal schemes are the two that declare a parent, and
372/// `SelectPredictionMethod` picks neither at speed 4 or above. An explicit
373/// `prediction_scheme` option does not go through that selection, though, so
374/// asking for one by number at any speed has to count as well: otherwise the
375/// scheme is built and the parent it reads is the original attribute, whose
376/// value order and point map are not what the decoder reconstructs.
377#[cfg(feature = "encoder")]
378fn position_is_a_prediction_parent(point_cloud: &PointCloud, options: &EncoderOptions) -> bool {
379    if options.get_speed() < 4 {
380        return true;
381    }
382    (0..point_cloud.num_attributes())
383        .any(|att_id| matches!(options.get_attribute_prediction_scheme(att_id), 3 | 5 | 6))
384}
385
386/// The portable form of an already-integral attribute: its values converted to
387/// `i32` in encoding order, which is the shape the decoder reconstructs.
388///
389/// Upstream builds one for *every* integer attribute --
390/// `SequentialIntegerAttributeEncoder::PrepareValues` calls
391/// `PreparePortableAttribute` before it looks at quantization at all -- and a
392/// prediction scheme reaching for a parent gets that, never the original. This
393/// port only built one where a quantization transform produced it, so a scheme
394/// predicting from an integer position read the original instead: its own value
395/// count and its own point map, both of which a deduplicated or seamed mesh
396/// makes different from what the decoder will hold. Encoder and decoder then
397/// predicted from different positions and disagreed about which entries carry
398/// an orientation bit, which the decoder reports as running out of them.
399#[cfg(feature = "encoder")]
400fn integral_portable_attribute(
401    attribute: &PointAttribute,
402    point_ids: &[PointIndex],
403) -> Result<PointAttribute, DracoError> {
404    let num_components = attribute.num_components();
405    let data_type = attribute.data_type();
406    let byte_stride = attribute.byte_stride() as usize;
407    let component_size = data_type.byte_length();
408
409    let mut portable = PointAttribute::default();
410    portable.try_init(
411        attribute.attribute_type(),
412        num_components,
413        crate::draco_types::DataType::Int32,
414        false,
415        point_ids.len(),
416    )?;
417
418    for (entry, &point_id) in point_ids.iter().enumerate() {
419        let src = attribute.mapped_index(point_id).0 as usize * byte_stride;
420        for component in 0..num_components as usize {
421            let value = crate::sequential_integer_attribute_encoder::read_value_as_i32(
422                attribute.buffer(),
423                src + component * component_size,
424                data_type,
425            );
426            let offset = (entry * num_components as usize + component) * 4;
427            portable.buffer_mut().write(offset, &value.to_le_bytes());
428        }
429    }
430    Ok(portable)
431}
432
433impl MeshEncoder {
434    /// Creates an encoder without an assigned mesh.
435    pub fn new() -> Self {
436        Self {
437            mesh: None,
438            options: EncoderOptions::default(),
439            num_encoded_faces: 0,
440            corner_table: None,
441            point_ids: Vec::new(),
442            data_to_corner_map: None,
443            vertex_to_data_map: None,
444            edgebreaker_attribute_connectivity: Vec::new(),
445            active_corner_table: None,
446            active_data_to_corner_map: None,
447            active_vertex_to_data_map: None,
448            split_value_counts: Vec::new(),
449            attribute_seam_corners: AttributeSeamCorners::default(),
450            mark_attribute_seam_corners: false,
451            attribute_traversal: None,
452            parent_attributes: ParentAttributes::new(),
453            edgebreaker_encoder: None,
454            method: 0,
455            use_single_connectivity: false,
456            attribute_predictions: Vec::new(),
457            attribute_quantization: Vec::new(),
458        }
459    }
460
461    /// Assigns the mesh to encode.
462    pub fn set_mesh(&mut self, mesh: Mesh) {
463        self.mesh = Some(mesh);
464    }
465
466    /// Drops everything the previous encode derived from its mesh.
467    ///
468    /// An encoder is reusable - `set_mesh` then `encode`, twice - and each
469    /// encode caches connectivity for the attribute stage to read back:
470    /// a corner table, a point order, corner and vertex maps, per-attribute
471    /// seam connectivity. Only some of that is rewritten by every path. The
472    /// sequential connectivity branch does not build a corner table, so after
473    /// an EdgeBreaker encode it inherited the previous mesh's one and wrote
474    /// attributes against topology the stream does not describe: encoding an
475    /// attributed mesh with EdgeBreaker and then a plain mesh sequentially
476    /// with the same encoder produced a stream this crate's own decoder
477    /// rejects. Resetting in one place is the fix that does not depend on
478    /// every future path remembering to.
479    fn reset_derived_state(&mut self) {
480        self.parent_attributes.clear();
481        self.edgebreaker_encoder = None;
482        self.num_encoded_faces = 0;
483        self.corner_table = None;
484        self.point_ids.clear();
485        self.data_to_corner_map = None;
486        self.vertex_to_data_map = None;
487        self.edgebreaker_attribute_connectivity.clear();
488        self.active_corner_table = None;
489        self.active_data_to_corner_map = None;
490        self.active_vertex_to_data_map = None;
491        self.split_value_counts.clear();
492        self.attribute_seam_corners = AttributeSeamCorners::default();
493        self.attribute_traversal = None;
494        self.method = 0;
495        self.use_single_connectivity = false;
496        self.attribute_predictions.clear();
497        self.attribute_quantization.clear();
498    }
499
500    /// Returns the assigned mesh, if any.
501    pub fn mesh(&self) -> Option<&Mesh> {
502        self.mesh.as_ref()
503    }
504
505    /// Returns the number of faces encoded by the last successful encode.
506    pub fn num_encoded_faces(&self) -> usize {
507        self.num_encoded_faces
508    }
509
510    /// Returns the corner table built during the last mesh encode, if any.
511    pub fn corner_table(&self) -> Option<&CornerTable> {
512        self.corner_table.as_ref()
513    }
514
515    /// Encodes the assigned mesh into an output buffer.
516    ///
517    /// A mesh must have been provided with [`set_mesh`](MeshEncoder::set_mesh)
518    /// first. On success the bitstream is appended to `out_buffer` and nothing
519    /// else is computed; use
520    /// [`encode_with_info`](MeshEncoder::encode_with_info) to also get a
521    /// description of the encode.
522    ///
523    /// # Errors
524    ///
525    /// Returns an error if no mesh was set, if the requested encoding method or
526    /// options are unsupported, or if attribute encoding fails.
527    pub fn encode(&mut self, options: &EncoderOptions, out_buffer: &mut EncoderBuffer) -> Status {
528        self.options = options.clone();
529        self.reset_derived_state();
530
531        if self.mesh.is_none() {
532            return Err(DracoError::general("Mesh not set".to_string()));
533        }
534        crate::point_cloud_encoder::validate_encodable_attributes(self.mesh.as_ref().unwrap())?;
535        let (major, minor) = self.options.get_version();
536        let target = if select_mesh_encoding_method(&self.options) == 1 {
537            crate::version::EncodeTarget::MeshEdgebreaker
538        } else {
539            crate::version::EncodeTarget::MeshSequential
540        };
541        crate::version::validate_encodable_version(major, minor, target)?;
542        Self::validate_face_indices(self.mesh.as_ref().unwrap())?;
543        self.validate_predictive_traversal()?;
544        self.validate_prediction_schemes(self.mesh.as_ref().unwrap())?;
545        self.validate_attribute_versions(self.mesh.as_ref().unwrap())?;
546
547        // 1. Encode Header
548        self.encode_header(out_buffer)?;
549        self.encode_metadata(out_buffer)?;
550
551        // 2. Encode geometry data (connectivity + attributes)
552        self.encode_geometry_data(out_buffer)?;
553
554        Ok(())
555    }
556
557    /// Encodes the assigned mesh and describes what the encode did.
558    ///
559    /// The description is derived from the encode rather than produced by it,
560    /// and deriving it costs a sweep of every position for its bounds, a pass
561    /// over the corners of each attribute with interior seams, and a walk
562    /// around every vertex for the point count. So it is the caller who
563    /// decides whether that work happens: [`encode`](MeshEncoder::encode) never
564    /// does it, and this does it exactly once, here, where it was asked for.
565    ///
566    /// # Errors
567    ///
568    /// The same errors as [`encode`](MeshEncoder::encode), plus a failure to
569    /// derive the description. The bitstream in `out_buffer` is complete and
570    /// valid in that last case; only the description is missing.
571    pub fn encode_with_info(
572        &mut self,
573        options: &EncoderOptions,
574        out_buffer: &mut EncoderBuffer,
575    ) -> Result<EncodedMeshInfo, DracoError> {
576        self.mark_attribute_seam_corners = true;
577        let encoded = self.encode(options, out_buffer);
578        self.mark_attribute_seam_corners = false;
579        let info = encoded.and_then(|()| self.build_encoded_mesh_info());
580        self.attribute_seam_corners = AttributeSeamCorners::default();
581        info
582    }
583
584    fn encode_metadata(&self, buffer: &mut EncoderBuffer) -> Status {
585        if let Some(metadata) = self
586            .mesh
587            .as_ref()
588            .and_then(|mesh| mesh.metadata())
589            .filter(|metadata| !metadata.is_empty())
590        {
591            metadata.encode(buffer)?;
592        }
593        Ok(())
594    }
595
596    /// Rejects a tex-coord prediction scheme forced onto an attribute that is
597    /// not a texture coordinate.
598    ///
599    /// Both tex-coord predictors work on two components and predict from the
600    /// position, so the encoder builds one for any attribute that presents two
601    /// components. A normal does, once the octahedron transform has folded it
602    /// from three - so a scheme meant for UVs was accepted for normals and
603    /// wrote values the normal decoder cannot read back. Three-component
604    /// attributes were already refused, which is why only normals slipped
605    /// through.
606    fn validate_prediction_schemes(&self, mesh: &Mesh) -> Status {
607        const TEX_COORDS_DEPRECATED: i32 = 3;
608        const TEX_COORDS_PORTABLE: i32 = 5;
609
610        for att_id in 0..mesh.num_attributes() {
611            let scheme = self.options.get_attribute_prediction_scheme(att_id);
612            if !matches!(scheme, TEX_COORDS_DEPRECATED | TEX_COORDS_PORTABLE) {
613                continue;
614            }
615            let attribute_type = mesh.attribute(att_id).attribute_type();
616            if attribute_type != GeometryAttributeType::TexCoord {
617                return Err(DracoError::general(format!(
618                    "Prediction scheme {scheme} predicts texture coordinates and cannot be used \
619                     for attribute {att_id}, which is a {attribute_type:?}"
620                )));
621            }
622        }
623        Ok(())
624    }
625
626    /// Whether attribute `att_id` already wrote its quantization parameters
627    /// ahead of its values, so the trailing pass must not write them again.
628    ///
629    /// Asks the same function the attribute encoder asks, so the parameters are
630    /// written exactly once whichever side of the 2.0 boundary the target is.
631    fn quantization_parameters_are_inline(&self, att_id: i32) -> bool {
632        #[cfg(feature = "legacy_bitstream_encode")]
633        {
634            let Some(mesh) = self.mesh.as_ref() else {
635                return false;
636            };
637            crate::sequential_integer_attribute_encoder::uses_inline_quantization_parameters(
638                mesh.attribute(att_id),
639                &self.options,
640                att_id,
641            )
642        }
643        #[cfg(not(feature = "legacy_bitstream_encode"))]
644        {
645            let _ = att_id;
646            false
647        }
648    }
649
650    /// The per-attribute encoder this encode will build, which is what decides
651    /// whether an attribute is subject to the version gates below.
652    ///
653    /// An attribute only meets a prediction scheme or a quantization transform
654    /// if it reaches the integer path; a float attribute with no quantization
655    /// goes to the generic encoder, where a requested prediction scheme is
656    /// simply never consulted. Asking the same function the encoder asks keeps
657    /// the two from disagreeing about which attributes a refusal covers.
658    fn attribute_encoder_type(&self, mesh: &Mesh, att_id: i32) -> SequentialAttributeEncoderType {
659        let quantization_bits = self
660            .options
661            .get_attribute_int(att_id, "quantization_bits", -1);
662        select_sequential_encoder(mesh.attribute(att_id), quantization_bits)
663    }
664
665    /// Rejects attribute coding a pre-2.2 target has no layout for *in this
666    /// build*.
667    ///
668    /// Every pre-2.2 attribute layout is written behind `legacy_bitstream_encode`
669    /// -- the quantization parameters that go inline below 2.0, and the rANS
670    /// size prefixes and mode bytes the prediction schemes carry below 2.2. With
671    /// the feature on there is nothing to refuse. With it off those writes are
672    /// compiled out, so an encode that reaches them silently produces a stream
673    /// this crate's own decoder cannot read, and the refusal takes their place.
674    ///
675    /// EdgeBreaker is not covered here because `encode_header` already refuses
676    /// every pre-2.2 EdgeBreaker target when the feature is off. What is left is
677    /// the sequential mesh at 1.3, which has no such gate.
678    #[cfg(not(feature = "legacy_bitstream_encode"))]
679    fn validate_attribute_versions(&self, mesh: &Mesh) -> Status {
680        let (mut major, mut minor) = self.options.get_version();
681        if major == 0 && minor == 0 {
682            (major, minor) = DEFAULT_MESH_VERSION;
683        }
684        if !crate::version::version_less_than(major, minor, (2, 2)) {
685            return Ok(());
686        }
687
688        for att_id in 0..mesh.num_attributes() {
689            // A generic attribute is copied out raw and meets neither a
690            // transform nor a prediction scheme, so no legacy layout applies.
691            if self.attribute_encoder_type(mesh, att_id) == SequentialAttributeEncoderType::Generic
692            {
693                continue;
694            }
695            return Err(DracoError::unsupported_version(format!(
696                "Attribute {att_id} needs the pre-2.2 layout for bitstream version \
697                 {major}.{minor}, which requires the legacy_bitstream_encode feature"
698            )));
699        }
700        Ok(())
701    }
702
703    /// With the legacy writer compiled in, every claimed version has a layout,
704    /// so there is nothing to refuse.
705    #[cfg(feature = "legacy_bitstream_encode")]
706    fn validate_attribute_versions(&self, _mesh: &Mesh) -> Status {
707        Ok(())
708    }
709
710    /// Rejects the legacy predictive traversal on a version that cannot carry
711    /// it.
712    ///
713    /// `force_predictive_traversal` round-trips pre-0.10.0 connectivity and
714    /// only belongs in a target version below 2.0, which the encoder's own
715    /// comment said and nothing checked. Set on a current-version encode, it
716    /// produced a type-1 traversal inside a 2.x stream - which the decoder
717    /// refuses, since 2.x connectivity has no predictive traversal to read.
718    fn validate_predictive_traversal(&self) -> Status {
719        if self.options.get_global_int("force_predictive_traversal", 0) == 0 {
720            return Ok(());
721        }
722        let (mut major, mut minor) = self.options.get_version();
723        if major == 0 && minor == 0 {
724            (major, minor) = DEFAULT_MESH_VERSION;
725        }
726        if !crate::version::version_less_than(major, minor, (2, 0)) {
727            return Err(DracoError::unsupported_feature(format!(
728                "force_predictive_traversal requires a target bitstream version below 2.0, \
729                 not {major}.{minor}"
730            )));
731        }
732        Ok(())
733    }
734
735    /// Rejects a face that references a point the mesh does not have.
736    ///
737    /// The index buffer is the other half of caller-supplied geometry, and
738    /// nothing between a file and this encoder re-checks it against the point
739    /// count. Both connectivity paths then use face indices to index
740    /// point-sized arrays directly - `point_to_vertex[face[j]]` in the corner
741    /// table build is the shortest route to it - so an out-of-range index is a
742    /// panic rather than an encode failure. One pass here answers for every
743    /// such use.
744    fn validate_face_indices(mesh: &Mesh) -> Status {
745        let num_points = mesh.num_points();
746        for face_id in 0..mesh.num_faces() {
747            let face = mesh.face(FaceIndex(face_id as u32));
748            for index in face {
749                if index.0 as usize >= num_points {
750                    return Err(DracoError::general(format!(
751                        "Face {face_id} references point {} but the mesh has {num_points} points",
752                        index.0
753                    )));
754                }
755            }
756        }
757        Ok(())
758    }
759
760    fn encode_header(&self, buffer: &mut EncoderBuffer) -> Status {
761        let (mut major, mut minor) = self.options.get_version();
762        if major == 0 && minor == 0 {
763            // Default to latest mesh version
764            (major, minor) = DEFAULT_MESH_VERSION;
765        }
766        let has_metadata = self
767            .mesh
768            .as_ref()
769            .and_then(|mesh| mesh.metadata())
770            .is_some_and(|metadata| !metadata.is_empty());
771
772        if has_metadata && !has_header_flags(major, minor) {
773            return Err(DracoError::unsupported_version(
774                "Metadata requires Draco bitstream version 1.3 or newer".to_string(),
775            ));
776        }
777
778        let method = select_mesh_encoding_method(&self.options);
779
780        #[cfg(not(feature = "legacy_bitstream_encode"))]
781        if method == 1 {
782            let bitstream_version = crate::version::bitstream_version(major, minor);
783            if bitstream_version < 0x0202 {
784                return Err(DracoError::unsupported_version(
785                    "EdgeBreaker mesh encoding before bitstream 2.2 requires the \
786                     legacy_bitstream_encode feature"
787                        .to_string(),
788                ));
789            }
790            if self.options.get_global_int("force_predictive_traversal", 0) != 0 {
791                return Err(DracoError::unsupported_feature(
792                    "force_predictive_traversal requires the legacy_bitstream_encode feature"
793                        .to_string(),
794                ));
795            }
796        }
797        #[cfg(not(feature = "legacy_bitstream_encode"))]
798        match self.options.get_prediction_scheme() {
799            2 | 3 => {
800                return Err(DracoError::unsupported_feature(
801                    "legacy prediction schemes require the legacy_bitstream_encode feature"
802                        .to_string(),
803                ));
804            }
805            _ => {}
806        }
807
808        buffer.encode_data(b"DRACO");
809
810        buffer.encode_u8(major);
811        buffer.encode_u8(minor);
812        buffer.set_version(major, minor);
813        buffer.encode_u8(self.get_geometry_type() as u8);
814        buffer.encode_u8(method as u8);
815
816        // The flags field is always present in the binary header (the decoder reads
817        // it unconditionally); only the metadata bit gains meaning at v1.3+, which
818        // is guarded by the metadata check above. Emitting it only for >= 1.3 left
819        // pre-1.3 streams two bytes short, misaligning the rest of the stream.
820        let flags = if has_metadata { METADATA_FLAG_MASK } else { 0 };
821        buffer.encode_u16(flags);
822        Ok(())
823    }
824
825    fn encode_geometry_data(&mut self, out_buffer: &mut EncoderBuffer) -> Status {
826        // First encode connectivity
827        self.encode_connectivity(out_buffer)?;
828
829        // Check if we should store the number of encoded faces
830        if self
831            .options
832            .get_global_int("store_number_of_encoded_faces", 0)
833            != 0
834        {
835            self.compute_number_of_encoded_faces();
836        }
837
838        // Then encode attributes
839        self.encode_attributes(out_buffer)?;
840
841        Ok(())
842    }
843
844    fn encode_connectivity(&mut self, out_buffer: &mut EncoderBuffer) -> Status {
845        let mesh = self
846            .mesh
847            .as_ref()
848            .expect("mesh must be set before encoding");
849
850        // Determine encoding method FIRST (before building corner table)
851        let method_int = self.options.get_global_int("encoding_method", -1);
852        let method = if method_int == -1 {
853            if self.options.get_speed() == 10 {
854                MeshEncodingMethod::MeshSequentialEncoding
855            } else {
856                MeshEncodingMethod::MeshEdgebreakerEncoding
857            }
858        } else if method_int == 1 {
859            MeshEncodingMethod::MeshEdgebreakerEncoding
860        } else {
861            MeshEncodingMethod::MeshSequentialEncoding
862        };
863        self.method = if method == MeshEncodingMethod::MeshEdgebreakerEncoding {
864            1
865        } else {
866            0
867        };
868
869        // C++ behavior: use_single_connectivity_ when speed >= 6
870        // When false (speed < 6), use position attribute to deduplicate vertices
871        let speed = self.options.get_speed();
872        // Check if split_mesh_on_seams is explicitly set, otherwise use speed-based default
873        let split_on_seams_explicit = self.options.get_global_int("split_mesh_on_seams", -1);
874        let use_single_connectivity = if split_on_seams_explicit >= 0 {
875            split_on_seams_explicit != 0
876        } else {
877            speed >= 6
878        };
879
880        // Only build corner table if needed (not for sequential encoding)
881        if method == MeshEncodingMethod::MeshEdgebreakerEncoding {
882            let faces = if use_single_connectivity {
883                // CreateCornerTableFromAllAttributes: use point indices directly
884                let faces: Vec<[crate::geometry_indices::VertexIndex; 3]> = (0..mesh.num_faces())
885                    .map(|i| {
886                        let face = mesh.face(FaceIndex(i as u32));
887                        [
888                            crate::geometry_indices::VertexIndex(face[0].0),
889                            crate::geometry_indices::VertexIndex(face[1].0),
890                            crate::geometry_indices::VertexIndex(face[2].0),
891                        ]
892                    })
893                    .collect();
894                faces
895            } else {
896                // CreateCornerTableFromPositionAttribute: use position attribute to deduplicate
897                self.create_corner_table_from_position_attribute(mesh)
898            };
899
900            // Initialize corner table for the mesh
901            let mut corner_table = CornerTable::new(0);
902            corner_table.init(&faces);
903
904            // A mesh whose every face is degenerate has no connectivity to
905            // traverse: `point_ids` comes back empty, and everything downstream
906            // that assumes at least one encoded point panics rather than
907            // failing cleanly. C++ rejects the same input outright --
908            // `MeshEdgebreakerEncoderImpl::Init` checks
909            // `num_faces() == NumDegeneratedFaces()` before doing anything else.
910            if corner_table.num_faces() > 0
911                && corner_table.num_faces() == corner_table.num_degenerated_faces()
912            {
913                return Err(DracoError::general(
914                    "All triangles are degenerate.".to_string(),
915                ));
916            }
917
918            self.corner_table = Some(corner_table);
919            self.edgebreaker_attribute_connectivity.clear();
920            if !use_single_connectivity {
921                if let Some(ref ct) = self.corner_table {
922                    for i in 0..mesh.num_attributes() {
923                        let att = mesh.attribute(i);
924                        if att.attribute_type() != GeometryAttributeType::Position {
925                            self.edgebreaker_attribute_connectivity
926                                .push(EdgebreakerAttributeConnectivity::build(mesh, ct, i));
927                        }
928                    }
929                }
930            }
931        } else {
932            // Sequential encoding: no corner table needed.
933            self.edgebreaker_attribute_connectivity.clear();
934        }
935        self.use_single_connectivity = use_single_connectivity;
936
937        match method {
938            MeshEncodingMethod::MeshSequentialEncoding => {
939                self.encode_sequential_connectivity(out_buffer)
940            }
941            MeshEncodingMethod::MeshEdgebreakerEncoding => {
942                self.encode_edgebreaker_connectivity(out_buffer)
943            }
944        }
945    }
946
947    fn encode_edgebreaker_connectivity(&mut self, out_buffer: &mut EncoderBuffer) -> Status {
948        let mesh = self
949            .mesh
950            .as_ref()
951            .expect("mesh must be set before encoding");
952        let corner_table = self
953            .corner_table
954            .as_ref()
955            .expect("corner_table must be set before edgebreaker encoding");
956
957        let mut encoder = MeshEdgebreakerEncoder::new(mesh.num_faces(), mesh.num_points());
958        // Opt-in legacy predictive (type-1) traversal, for round-tripping the
959        // pre-0.10.0 connectivity. Requires a < 2.0 target version.
960        #[cfg(feature = "legacy_bitstream_encode")]
961        encoder.set_force_predictive(
962            self.options.get_global_int("force_predictive_traversal", 0) == 1,
963        );
964        let (point_ids, data_to_corner_map, vertex_to_data_map) = encoder.encode_connectivity(
965            mesh,
966            corner_table,
967            &self.edgebreaker_attribute_connectivity,
968            out_buffer,
969            self.options.get_speed() as usize,
970            self.use_single_connectivity,
971        )?;
972        #[cfg(feature = "debug_logs")]
973        {
974            debug_log!("DEBUG: encode_edgebreaker_connectivity: point_ids.len()={}, data_to_corner_map.len()={}, vertex_to_data_map.len()={}",
975                 point_ids.len(), data_to_corner_map.len(), vertex_to_data_map.len());
976        }
977        // At speed 0 the position walks the mesh by max prediction degree while
978        // every other attribute stays depth first, so the two orders part ways
979        // and the non-position groups need their own. At any other speed the
980        // position order already is the depth-first one.
981        //
982        // Whether there is a non-position group is `edgebreaker_attribute_connectivity`
983        // being non-empty, not `mesh.num_attributes() > 1`: a mesh can carry a
984        // single attribute that is not Position (no separate Position attribute
985        // registered at all, connectivity coming only from the face list), and
986        // then `num_attributes()` is 1 while that one attribute still needs its
987        // own traversal. Counting attributes undercounts exactly that mesh.
988        self.attribute_traversal = if self.options.get_speed() == 0
989            && !self.edgebreaker_attribute_connectivity.is_empty()
990        {
991            Some(encoder.generate_depth_first_traversal(mesh, corner_table))
992        } else {
993            None
994        };
995
996        self.point_ids = point_ids;
997
998        // Draco stores corner mapping in attribute (data) order.
999        self.data_to_corner_map = Some(data_to_corner_map);
1000        self.vertex_to_data_map = Some(vertex_to_data_map);
1001
1002        // Held for the corner order it carries: an attribute with interior
1003        // seams walks its own corner table seeded from that order, and this is
1004        // the last point at which it exists.
1005        self.edgebreaker_encoder = Some(encoder);
1006
1007        Ok(())
1008    }
1009
1010    /// Creates the faces array using the position attribute to deduplicate
1011    /// vertices, mimicking C++ CreateCornerTableFromPositionAttribute: each
1012    /// face carries the attribute value indices its points map to.
1013    fn create_corner_table_from_position_attribute(
1014        &self,
1015        mesh: &Mesh,
1016    ) -> Vec<[crate::geometry_indices::VertexIndex; 3]> {
1017        use crate::geometry_attribute::GeometryAttributeType;
1018
1019        let pos_att_id = mesh.named_attribute_id(GeometryAttributeType::Position);
1020        if pos_att_id < 0 {
1021            // No position attribute, fall back to identity mapping
1022            let faces: Vec<[crate::geometry_indices::VertexIndex; 3]> = (0..mesh.num_faces())
1023                .map(|i| {
1024                    let face = mesh.face(FaceIndex(i as u32));
1025                    [
1026                        crate::geometry_indices::VertexIndex(face[0].0),
1027                        crate::geometry_indices::VertexIndex(face[1].0),
1028                        crate::geometry_indices::VertexIndex(face[2].0),
1029                    ]
1030                })
1031                .collect();
1032            return faces;
1033        }
1034
1035        let pos_att = mesh.attribute(pos_att_id);
1036        let _buffer = pos_att.buffer();
1037        let num_components = pos_att.num_components() as usize;
1038        let _byte_stride = match pos_att.data_type() {
1039            crate::draco_types::DataType::Float32 => num_components * 4,
1040            crate::draco_types::DataType::Float64 => num_components * 8,
1041            crate::draco_types::DataType::Int8 | crate::draco_types::DataType::Uint8 => {
1042                num_components
1043            }
1044            crate::draco_types::DataType::Int16 | crate::draco_types::DataType::Uint16 => {
1045                num_components * 2
1046            }
1047            crate::draco_types::DataType::Int32 | crate::draco_types::DataType::Uint32 => {
1048                num_components * 4
1049            }
1050            crate::draco_types::DataType::Int64 | crate::draco_types::DataType::Uint64 => {
1051                num_components * 8
1052            }
1053            _ => num_components * 4, // Default to 4 bytes per component
1054        };
1055
1056        // Use attribute mapped indices directly to build point->vertex map. This mirrors
1057        // C++ CreateCornerTableFromAttribute which uses att->mapped_index(face[j]).
1058        let mut point_to_vertex: Vec<u32> = vec![0; mesh.num_points()];
1059        for i in 0..mesh.num_points() {
1060            let pt = PointIndex(i as u32);
1061            let val_idx = pos_att.mapped_index(pt);
1062            point_to_vertex[i] = val_idx.0;
1063        }
1064
1065        // Build faces using attribute mapped indices (exact same mapping as C++).
1066        let faces: Vec<[crate::geometry_indices::VertexIndex; 3]> = (0..mesh.num_faces())
1067            .map(|i| {
1068                let face = mesh.face(FaceIndex(i as u32));
1069                [
1070                    crate::geometry_indices::VertexIndex(point_to_vertex[face[0].0 as usize]),
1071                    crate::geometry_indices::VertexIndex(point_to_vertex[face[1].0 as usize]),
1072                    crate::geometry_indices::VertexIndex(point_to_vertex[face[2].0 as usize]),
1073                ]
1074            })
1075            .collect();
1076
1077        #[cfg(feature = "debug_logs")]
1078        {
1079            debug_log!(
1080                "Rust created faces (first 12): {:?}",
1081                faces
1082                    .iter()
1083                    .take(12)
1084                    .map(|f| [f[0].0, f[1].0, f[2].0])
1085                    .collect::<Vec<_>>()
1086            );
1087            debug_log!(
1088                "Rust point_to_vertex (first 25): {:?}",
1089                point_to_vertex.iter().take(25).cloned().collect::<Vec<_>>()
1090            );
1091        }
1092        faces
1093    }
1094
1095    fn encode_sequential_connectivity(&mut self, out_buffer: &mut EncoderBuffer) -> Status {
1096        let mesh = self
1097            .mesh
1098            .as_ref()
1099            .expect("mesh must be set before encoding");
1100
1101        // Encode the number of faces and points
1102        // Use the buffer's version (set in encode_header) for version checks
1103        let major = out_buffer.version_major();
1104        let minor = out_buffer.version_minor();
1105        // 2.2, not 2.0. `uses_varint_encoding` ignores its `minor` argument and
1106        // flips at the major, while the decoder (and upstream
1107        // `MeshSequentialDecoder`) reads these as varints only from 2.2. A
1108        // sequential mesh written at 2.0 or 2.1 was therefore unreadable. Those
1109        // versions are no longer claimed for sequential meshes, so this is
1110        // upstream parity rather than a live fix - but a predicate that ignores
1111        // half its input is a trap, and this is the second bug it caused.
1112        let counts_are_varint = crate::version::version_at_least(major, minor, (2, 2));
1113        if !counts_are_varint {
1114            out_buffer.encode_u32(mesh.num_faces() as u32);
1115            out_buffer.encode_u32(mesh.num_points() as u32);
1116        } else {
1117            out_buffer.encode_varint(mesh.num_faces() as u64);
1118            out_buffer.encode_varint(mesh.num_points() as u64);
1119        }
1120
1121        // Raw connectivity. The method byte goes out even with no faces or no
1122        // points, as upstream writes it, since its decoder reads it regardless.
1123        out_buffer.encode_u8(1);
1124        if mesh.num_points() < 256 {
1125            for face_id in 0..mesh.num_faces() {
1126                let face = mesh.face(FaceIndex(face_id as u32));
1127                for i in 0..3 {
1128                    out_buffer.encode_u8(face[i].0 as u8);
1129                }
1130            }
1131        } else if mesh.num_points() < 65536 {
1132            for face_id in 0..mesh.num_faces() {
1133                let face = mesh.face(FaceIndex(face_id as u32));
1134                for i in 0..3 {
1135                    out_buffer.encode_u16(face[i].0 as u16);
1136                }
1137            }
1138        } else if counts_are_varint && mesh.num_points() < (1 << 21) {
1139            // Varint indices when the points fit in 21 bits, as upstream
1140            // does - but only from 2.2, which is where the decoder starts
1141            // reading them that way. This branch had no version gate at
1142            // all, so every sequential mesh below 2.2 with 65536 or more
1143            // points was written unreadable; 1.3 is a claimed version, so
1144            // this one is a live fix, not just parity.
1145            for face_id in 0..mesh.num_faces() {
1146                let face = mesh.face(FaceIndex(face_id as u32));
1147                for i in 0..3 {
1148                    out_buffer.encode_varint(face[i].0 as u64);
1149                }
1150            }
1151        } else {
1152            // Default: use u32 for very large meshes
1153            for face_id in 0..mesh.num_faces() {
1154                let face = mesh.face(FaceIndex(face_id as u32));
1155                for i in 0..3 {
1156                    out_buffer.encode_u32(face[i].0);
1157                }
1158            }
1159        }
1160
1161        // Identity permutation for sequential encoding
1162        self.point_ids = (0..mesh.num_points())
1163            .map(|i| PointIndex(i as u32))
1164            .collect();
1165
1166        Ok(())
1167    }
1168
1169    /// Whether the position attribute is walked by max prediction degree
1170    /// rather than depth first.
1171    ///
1172    /// This is the predicate behind the `traversal_method` byte, and it has to
1173    /// be the same one `MeshEdgebreakerEncoder::position_uses_prediction_degree`
1174    /// walks by: the byte tells the decoder which order the position values are
1175    /// in, and a decoder that reorders them differently reads every value onto
1176    /// the wrong vertex. Upstream takes the prediction degree back when one
1177    /// connectivity is shared by more than one attribute -- see
1178    /// `mesh_edgebreaker_encoder_impl.cc:165-175` -- so the walk does too, and
1179    /// so must the byte.
1180    ///
1181    /// The third condition the walk applies, that the target bitstream can
1182    /// express the choice at all, needs no counterpart here: the byte itself
1183    /// arrived in 1.2, and every caller writes it only for 1.2 and above.
1184    fn position_traversal_is_prediction_degree(&self, mesh: &Mesh) -> bool {
1185        self.options.get_speed() == 0
1186            && !(self.use_single_connectivity && mesh.num_attributes() > 1)
1187    }
1188
1189    fn encode_attributes(&mut self, out_buffer: &mut EncoderBuffer) -> Status {
1190        // NOTE: Unlike the decoder, the encoder does NOT need to apply UpdatePointToAttributeIndexMapping
1191        // because the attribute still has identity mapping. The encoder uses the point_ids array
1192        // (from edgebreaker traversal) to determine the order in which to process points, and
1193        // mapped_index with identity mapping just returns the point index directly.
1194
1195        let method_int = self.options.get_global_int("encoding_method", -1);
1196        // Match C++ behavior: if encoding_method is not set (-1),
1197        // use Edgebreaker for all options except speed == 10
1198        let is_edgebreaker = if method_int == -1 {
1199            self.options.get_speed() != 10
1200        } else {
1201            method_int == 1
1202        };
1203
1204        if is_edgebreaker && !self.use_single_connectivity {
1205            return self.encode_edgebreaker_attributes_split(out_buffer);
1206        }
1207
1208        // Encode number of attribute decoders (u8).
1209        // For both sequential and edgebreaker with single-connectivity mode:
1210        // there's only ONE attribute encoder containing ALL attributes.
1211        // This matches C++ behavior when use_single_connectivity_ = true (speed >= 6).
1212        let num_attributes = self
1213            .point_cloud()
1214            .expect("point_cloud set")
1215            .num_attributes();
1216        let num_encoders = if num_attributes > 0 { 1 } else { 0 };
1217        // Use the buffer's version (set in encode_header) for version checks.
1218        let major = out_buffer.version_major();
1219        let minor = out_buffer.version_minor();
1220
1221        out_buffer.encode_u8(num_encoders as u8);
1222
1223        // Phase 1: attributes decoder identifiers.
1224        // For single-encoder mode: one encoder with att_data_id = -1 (uses position connectivity)
1225        if num_encoders > 0 && is_edgebreaker {
1226            // att_data_id (i8), encoder_type (u8), traversal_method (u8)
1227            // -1 means use position connectivity (single connectivity mode)
1228            out_buffer.encode_u8((-1i8) as u8); // att_data_id = -1
1229            out_buffer.encode_u8(0); // element_type = MESH_VERTEX_ATTRIBUTE
1230
1231            // Traversal method was added in bitstream 1.2. Older streams
1232            // default to DEPTH_FIRST on decode and must not carry the byte.
1233            if crate::version::bitstream_version(major, minor) >= 0x0102 {
1234                // This group carries att_data_id -1, so it is the position
1235                // group, and the byte is whatever the position walk turns out
1236                // to be.
1237                // Scoped so the `mesh` borrow cannot reach the portable
1238                // registrations in the value loop below.
1239                let traversal_method: u8 = {
1240                    let mesh = self
1241                        .mesh
1242                        .as_ref()
1243                        .expect("mesh must be set before encoding");
1244                    if self.position_traversal_is_prediction_degree(mesh) {
1245                        1
1246                    } else {
1247                        0
1248                    }
1249                };
1250                out_buffer.encode_u8(traversal_method);
1251            }
1252        }
1253        // For sequential, nothing is written in phase 1 (EncodeAttributesEncoderIdentifier does nothing)
1254
1255        let mut decoder_types: Vec<u8> = Vec::with_capacity(num_attributes as usize);
1256
1257        // Phase 2: Encode attribute encoder data
1258        // Both sequential and edgebreaker now use single-encoder mode:
1259        //   - Write num_attrs = total attributes
1260        //   - Write all attribute metadata
1261        //   - Write all decoder types
1262
1263        if num_encoders > 0 {
1264            // Single encoder with all attributes (single-connectivity mode for edgebreaker)
1265            // Write num_attrs = total number of attributes
1266            if !uses_varint_encoding(major, minor) {
1267                out_buffer.encode_u32(num_attributes as u32);
1268            } else {
1269                out_buffer.encode_varint(num_attributes as u64);
1270            }
1271
1272            // Write all attribute metadata first
1273            for i in 0..num_attributes {
1274                let att = self.point_cloud().expect("point_cloud set").attribute(i);
1275
1276                #[cfg(feature = "debug_logs")]
1277                {
1278                    debug_log!("DEBUG: Encoder encoding attribute {} metadata. Type: {:?}, Components: {}, Data: {:?}", i, att.attribute_type(), att.num_components(), att.data_type());
1279                }
1280                out_buffer.encode_u8(att.attribute_type() as u8);
1281                out_buffer.encode_u8(att.data_type() as u8);
1282                out_buffer.encode_u8(att.num_components());
1283                out_buffer.encode_u8(if att.normalized() { 1 } else { 0 });
1284
1285                if !uses_varint_unique_id(major, minor) {
1286                    out_buffer.encode_u16(att.unique_id() as u16);
1287                } else {
1288                    out_buffer.encode_varint(att.unique_id() as u64);
1289                }
1290            }
1291
1292            // Write all decoder types after all metadata (SequentialAttributeEncodersController pattern)
1293            for i in 0..num_attributes {
1294                let att = self.point_cloud().expect("point_cloud set").attribute(i);
1295                let quantization_bits = self.options.get_attribute_int(i, "quantization_bits", -1);
1296                let decoder_type = select_sequential_encoder(att, quantization_bits) as u8;
1297                out_buffer.encode_u8(decoder_type);
1298                decoder_types.push(decoder_type);
1299            }
1300        }
1301
1302        // Phase 3: Encode attribute values (all attributes first)
1303        // C++ order: all EncodePortableAttribute calls, then all EncodeDataNeededByPortableTransform calls
1304
1305        // Store transforms and encoders for later use in transform data encoding
1306        let mut quantization_transforms: Vec<Option<AttributeQuantizationTransform>> = Vec::new();
1307        let mut normal_encoders: Vec<Option<SequentialNormalAttributeEncoder>> = Vec::new();
1308        // Collected here rather than written straight to `self`, which is
1309        // borrowed as the `GeometryEncoder` the attribute encoders predict
1310        // against for as long as this loop runs.
1311        let mut predictions = Vec::new();
1312
1313        // First pass: encode all attribute VALUES
1314        for i in 0..num_attributes {
1315            let att = self.point_cloud().expect("point_cloud set").attribute(i);
1316            let decoder_type = decoder_types[i as usize];
1317            let quantization_bits = self.options.get_attribute_int(i, "quantization_bits", -1);
1318
1319            match decoder_type {
1320                3 => {
1321                    // Normal attribute with octahedral encoding
1322                    let mut encoder = SequentialNormalAttributeEncoder::new();
1323                    encoder
1324                        .init(
1325                            self.point_cloud().expect("point_cloud set"),
1326                            i,
1327                            &self.options,
1328                        )
1329                        .map_err(|e| {
1330                            DracoError::general(format!("Failed to init normal encoder: {e}"))
1331                        })?;
1332                    encoder.encode_values(
1333                        self.point_cloud().expect("point_cloud set"),
1334                        &self.point_ids,
1335                        out_buffer,
1336                        &self.options,
1337                        self,
1338                    )?;
1339                    if let Some((method, transform)) = encoder.selected_prediction() {
1340                        predictions.push((i, method, transform));
1341                    }
1342                    normal_encoders.push(Some(encoder));
1343                    quantization_transforms.push(None);
1344                }
1345                2 => {
1346                    // Quantized attribute (mapping already applied at start of encode_attributes)
1347                    let mut q_transform = AttributeQuantizationTransform::new();
1348                    q_transform
1349                        .compute_parameters(att, quantization_bits)
1350                        .map_err(|e| {
1351                            DracoError::general(format!(
1352                                "Failed to compute quantization parameters: {e}"
1353                            ))
1354                        })?;
1355                    let mut portable = PointAttribute::default();
1356                    q_transform
1357                        .transform_attribute(
1358                            att,
1359                            EntryToPointIdMap::from_point_indices(&self.point_ids),
1360                            &mut portable,
1361                        )
1362                        .map_err(|e| {
1363                            DracoError::general(format!("Failed to quantize attribute: {e}"))
1364                        })?;
1365
1366                    // A quantized position is the parent the portable schemes
1367                    // read, and what they read must be the quantized values
1368                    // with the map rebuilt into encoding order -- what the
1369                    // decoder reconstructs. Registration is what makes the
1370                    // attribute a parent: without it there is no copy for
1371                    // `get_portable_attribute` to answer, and the binding is
1372                    // left with the attribute itself.
1373                    if ParentAttributes::is_prediction_parent(
1374                        att,
1375                        self.point_cloud().expect("point_cloud set"),
1376                        &self.options,
1377                    ) {
1378                        rebuild_parent_point_map(
1379                            att,
1380                            &mut portable,
1381                            &self.point_ids,
1382                            self.point_cloud().expect("point_cloud set").num_points(),
1383                        )?;
1384                        self.parent_attributes.register(i, portable.clone());
1385                    }
1386
1387                    let mut att_encoder = SequentialIntegerAttributeEncoder::new();
1388                    att_encoder.init(i);
1389                    att_encoder.encode_values(
1390                        self.point_cloud().expect("point_cloud set"),
1391                        &self.point_ids,
1392                        out_buffer,
1393                        &self.options,
1394                        self,
1395                        Some(&portable),
1396                        true,
1397                    )?;
1398                    if let Some((method, transform)) = att_encoder.selected_prediction() {
1399                        predictions.push((i, method, transform));
1400                    }
1401
1402                    self.attribute_quantization.push((i, q_transform.clone()));
1403                    quantization_transforms.push(Some(q_transform));
1404                    normal_encoders.push(None);
1405                }
1406                1 => {
1407                    // Integer attribute
1408                    // An integral position still needs a portable form for the
1409                    // portable schemes' parent reads, for the reason
1410                    // `integral_portable_attribute` gives: the predictor must
1411                    // read what the decoder reconstructs.
1412                    if ParentAttributes::is_prediction_parent(
1413                        att,
1414                        self.point_cloud().expect("point_cloud set"),
1415                        &self.options,
1416                    ) {
1417                        let mut portable = integral_portable_attribute(att, &self.point_ids)?;
1418                        rebuild_parent_point_map(
1419                            att,
1420                            &mut portable,
1421                            &self.point_ids,
1422                            self.point_cloud().expect("point_cloud set").num_points(),
1423                        )?;
1424                        self.parent_attributes.register(i, portable);
1425                    }
1426
1427                    let mut att_encoder = SequentialIntegerAttributeEncoder::new();
1428                    att_encoder.init(i);
1429                    att_encoder.encode_values(
1430                        self.point_cloud().expect("point_cloud set"),
1431                        &self.point_ids,
1432                        out_buffer,
1433                        &self.options,
1434                        self,
1435                        None,
1436                        true,
1437                    )?;
1438                    if let Some((method, transform)) = att_encoder.selected_prediction() {
1439                        predictions.push((i, method, transform));
1440                    }
1441                    quantization_transforms.push(None);
1442                    normal_encoders.push(None);
1443                }
1444                0 => {
1445                    // Generic/float attribute
1446                    let mut att_encoder = SequentialAttributeEncoder::new();
1447                    att_encoder.init(i);
1448                    att_encoder.encode_values(
1449                        self.point_cloud().expect("point_cloud set"),
1450                        &self.point_ids,
1451                        out_buffer,
1452                    )?;
1453                    quantization_transforms.push(None);
1454                    normal_encoders.push(None);
1455                }
1456                _ => {
1457                    return Err(DracoError::general(format!(
1458                        "Unsupported encoder type {}",
1459                        decoder_type
1460                    )));
1461                }
1462            }
1463        }
1464
1465        // Second pass: encode all TRANSFORM DATA
1466        for i in 0..num_attributes {
1467            let decoder_type = decoder_types[i as usize];
1468
1469            match decoder_type {
1470                3 => {
1471                    // Normal attribute - encode octahedral transform data
1472                    let bitstream_version = crate::version::bitstream_version(major, minor);
1473                    if bitstream_version != 0 && bitstream_version < 0x0200 {
1474                        continue;
1475                    }
1476                    if let Some(ref encoder) = normal_encoders[i as usize] {
1477                        encoder
1478                            .encode_data_needed_by_portable_transform(out_buffer)
1479                            .map_err(|err| {
1480                                DracoError::general(format!(
1481                                    "Failed to encode normal transform data: {err}"
1482                                ))
1483                            })?;
1484                    }
1485                }
1486                2 => {
1487                    // Quantized attribute - encode quantization parameters,
1488                    // unless the target version already carried them inline
1489                    // ahead of the values.
1490                    if self.quantization_parameters_are_inline(i) {
1491                        continue;
1492                    }
1493                    if let Some(ref q_transform) = quantization_transforms[i as usize] {
1494                        q_transform.encode_parameters(out_buffer).map_err(|e| {
1495                            DracoError::general(format!(
1496                                "Failed to encode quantization parameters: {e}"
1497                            ))
1498                        })?;
1499                    }
1500                }
1501                1 | 0 => {
1502                    // No transform data for integer/generic attributes
1503                }
1504                _ => {}
1505            }
1506        }
1507
1508        self.attribute_predictions.extend(predictions);
1509        Ok(())
1510    }
1511
1512    fn encode_edgebreaker_attributes_split(&mut self, out_buffer: &mut EncoderBuffer) -> Status {
1513        let mesh = self
1514            .mesh
1515            .as_ref()
1516            .expect("mesh must be set before encoding");
1517        let mut groups: Vec<(i8, Vec<i32>)> = Vec::new();
1518        let mut position_attrs = Vec::new();
1519        for i in 0..mesh.num_attributes() {
1520            if mesh.attribute(i).attribute_type() == GeometryAttributeType::Position {
1521                position_attrs.push(i);
1522            }
1523        }
1524        if !position_attrs.is_empty() {
1525            groups.push((-1, position_attrs));
1526        }
1527        for (data_id, attr_conn) in self.edgebreaker_attribute_connectivity.iter().enumerate() {
1528            groups.push((data_id as i8, vec![attr_conn.attribute_id]));
1529        }
1530
1531        // The group count is one byte in the bitstream, so a mesh needing more
1532        // groups than that cannot be described. Truncating wrote a stream that
1533        // decodes as a different mesh: 261 groups became 5, and the decoder
1534        // read the sixth group's bytes as attribute data. Measured at the
1535        // boundary rather than assumed - 256 groups is the first count that
1536        // breaks, and everything below it round-trips, including the counts
1537        // where the per-group `i8` data id goes negative.
1538        if groups.len() > u8::MAX as usize {
1539            return Err(DracoError::general(format!(
1540                "Mesh needs {} attribute groups but the bitstream field holds {}",
1541                groups.len(),
1542                u8::MAX
1543            )));
1544        }
1545        out_buffer.encode_u8(groups.len() as u8);
1546
1547        let major = out_buffer.version_major();
1548        let minor = out_buffer.version_minor();
1549        let writes_traversal_method = crate::version::bitstream_version(major, minor) >= 0x0102;
1550        // Prediction degree is the position group's traversal alone. Every
1551        // other group is walked depth first, whatever the speed -- upstream
1552        // guards on the attribute being POSITION, and the groups here carry
1553        // att_data_id -1 for exactly that one. Declaring it for the rest
1554        // mislabels a stream whose values were written in depth-first order.
1555        let position_prediction_degree = self.position_traversal_is_prediction_degree(mesh);
1556        for (att_data_id, _) in &groups {
1557            out_buffer.encode_u8(*att_data_id as u8);
1558            let element_type = if *att_data_id >= 0
1559                && !self.edgebreaker_attribute_connectivity[*att_data_id as usize].no_interior_seams
1560            {
1561                1 // MESH_CORNER_ATTRIBUTE
1562            } else {
1563                0 // MESH_VERTEX_ATTRIBUTE
1564            };
1565            out_buffer.encode_u8(element_type);
1566            if writes_traversal_method {
1567                let is_position_group = *att_data_id < 0;
1568                let traversal_method: u8 = if position_prediction_degree && is_position_group {
1569                    1
1570                } else {
1571                    0
1572                };
1573                out_buffer.encode_u8(traversal_method);
1574            }
1575        }
1576
1577        let mut decoder_types_by_group: Vec<Vec<u8>> = Vec::with_capacity(groups.len());
1578
1579        for (_, attr_ids) in &groups {
1580            if !uses_varint_encoding(major, minor) {
1581                out_buffer.encode_u32(attr_ids.len() as u32);
1582            } else {
1583                out_buffer.encode_varint(attr_ids.len() as u64);
1584            }
1585
1586            for &att_id in attr_ids {
1587                let att = mesh.attribute(att_id);
1588                out_buffer.encode_u8(att.attribute_type() as u8);
1589                out_buffer.encode_u8(att.data_type() as u8);
1590                out_buffer.encode_u8(att.num_components());
1591                out_buffer.encode_u8(if att.normalized() { 1 } else { 0 });
1592                if !uses_varint_unique_id(major, minor) {
1593                    out_buffer.encode_u16(att.unique_id() as u16);
1594                } else {
1595                    out_buffer.encode_varint(att.unique_id() as u64);
1596                }
1597            }
1598
1599            let mut decoder_types = Vec::with_capacity(attr_ids.len());
1600            for &att_id in attr_ids {
1601                let decoder_type = self.decoder_type_for_attribute(att_id);
1602                out_buffer.encode_u8(decoder_type);
1603                decoder_types.push(decoder_type);
1604            }
1605            decoder_types_by_group.push(decoder_types);
1606        }
1607
1608        for (group_i, (att_data_id, attr_ids)) in groups.iter().enumerate() {
1609            let point_ids = if *att_data_id >= 0 {
1610                self.prepare_active_attribute_connectivity(*att_data_id as usize)?
1611            } else {
1612                self.active_corner_table = None;
1613                self.active_data_to_corner_map = None;
1614                self.active_vertex_to_data_map = None;
1615                self.point_ids.clone()
1616            };
1617
1618            self.encode_attribute_group_values(
1619                attr_ids,
1620                &decoder_types_by_group[group_i],
1621                &point_ids,
1622                out_buffer,
1623            )?;
1624            self.split_value_counts
1625                .extend(attr_ids.iter().map(|&att_id| (att_id, point_ids.len())));
1626            if let (true, Some(table), Some(corner_table)) = (
1627                self.mark_attribute_seam_corners,
1628                self.active_corner_table.as_ref(),
1629                self.corner_table.as_ref(),
1630            ) {
1631                self.attribute_seam_corners.mark(corner_table, table);
1632            }
1633        }
1634
1635        self.active_corner_table = None;
1636        self.active_data_to_corner_map = None;
1637        self.active_vertex_to_data_map = None;
1638        Ok(())
1639    }
1640
1641    fn decoder_type_for_attribute(&self, att_id: i32) -> u8 {
1642        let mesh = self
1643            .mesh
1644            .as_ref()
1645            .expect("mesh must be set before encoding");
1646        let att = mesh.attribute(att_id);
1647        let quantization_bits = self
1648            .options
1649            .get_attribute_int(att_id, "quantization_bits", -1);
1650        select_sequential_encoder(att, quantization_bits) as u8
1651    }
1652
1653    fn prepare_active_attribute_connectivity(
1654        &mut self,
1655        data_id: usize,
1656    ) -> Result<Vec<PointIndex>, DracoError> {
1657        let mesh = self
1658            .mesh
1659            .as_ref()
1660            .expect("mesh must be set before encoding");
1661        let base_ct = self
1662            .corner_table
1663            .as_ref()
1664            .ok_or_else(|| DracoError::general("corner_table must be set".to_string()))?;
1665        let attr_conn = self
1666            .edgebreaker_attribute_connectivity
1667            .get(data_id)
1668            .ok_or_else(|| DracoError::general("Invalid attribute connectivity id".to_string()))?;
1669
1670        if attr_conn.no_interior_seams {
1671            // Same corner table as the position, but not necessarily the same
1672            // walk over it: `attribute_traversal` is set when the position took
1673            // the max-prediction-degree order and this attribute must not.
1674            self.active_corner_table = None;
1675            if let Some((point_ids, data_to_corner_map, vertex_to_data_map)) =
1676                self.attribute_traversal.clone()
1677            {
1678                self.active_data_to_corner_map = Some(data_to_corner_map);
1679                self.active_vertex_to_data_map = Some(vertex_to_data_map);
1680                return Ok(point_ids);
1681            }
1682            self.active_data_to_corner_map = None;
1683            self.active_vertex_to_data_map = None;
1684            return Ok(self.point_ids.clone());
1685        }
1686
1687        // Same seam-cut-and-recompute the decoder runs on its own seam bits
1688        // (`mesh_decoder.rs::make_attribute_corner_table`) -- one function
1689        // instead of the two hand-rolled copies this used to be.
1690        let (attr_ct, _is_vertex_on_seam) =
1691            crate::mesh_attribute_corner_table::cut_seam_edges_and_recompute_vertices(
1692                base_ct,
1693                &attr_conn.seam_edges,
1694            )?;
1695
1696        // Walk the attribute's own table depth first, seeded by the edgebreaker
1697        // corner order, as upstream does with
1698        // `DepthFirstTraverser<MeshAttributeCornerTable>` and
1699        // `SetCornerOrder(processed_connectivity_corners_)`.
1700        //
1701        // Enumerating `vertex_corners` instead, as this used to, yields the
1702        // identity permutation of attribute-vertex indices -- `vertex_corners[v]`
1703        // has vertex `v` by construction -- which is not an encoding order at
1704        // all. The decoder walks the table it rebuilds from the seam bits, so
1705        // the values came back attached to the wrong points.
1706        let Some(encoder) = self.edgebreaker_encoder.as_ref() else {
1707            return Err(DracoError::general(
1708                "Attribute seams need the edgebreaker corner order".to_string(),
1709            ));
1710        };
1711        let (point_ids, data_to_corner_map, vertex_to_data_map) =
1712            encoder.generate_depth_first_traversal(mesh, &attr_ct);
1713
1714        self.active_corner_table = Some(attr_ct);
1715        self.active_data_to_corner_map = Some(data_to_corner_map);
1716        self.active_vertex_to_data_map = Some(vertex_to_data_map);
1717        Ok(point_ids)
1718    }
1719
1720    fn encode_attribute_group_values(
1721        &mut self,
1722        attr_ids: &[i32],
1723        decoder_types: &[u8],
1724        point_ids: &[PointIndex],
1725        out_buffer: &mut EncoderBuffer,
1726    ) -> Status {
1727        // Three passes over the group, one per step of C++
1728        // SequentialAttributeEncodersController: transform every attribute to its
1729        // portable form, encode them all, then encode the data their transforms
1730        // need. Each pass is marked below.
1731        //
1732        // Pass one, TransformAttributesToPortableFormat. It has to finish before
1733        // any attribute is encoded: a prediction scheme that reads a parent needs
1734        // the parent's portable values, and in a single pass the parent would not
1735        // exist yet for anything encoded ahead of it.
1736        let mut quantization_transforms: Vec<Option<AttributeQuantizationTransform>> = Vec::new();
1737        // The copy each type-2 attribute encodes from, kept for pass two. A
1738        // parent's copy is not here: it is registered on `self`, where later
1739        // groups' predictors reach it, and pass two reads it there.
1740        let mut own_portables: Vec<(i32, PointAttribute)> = Vec::new();
1741        {
1742            let mesh = self
1743                .mesh
1744                .as_ref()
1745                .expect("mesh must be set before encoding");
1746            // Parent registrations, collected rather than written straight to
1747            // `self`, which is borrowed as the mesh for as long as this loop
1748            // runs.
1749            let mut parent_registrations: Vec<(i32, PointAttribute)> = Vec::new();
1750            let mut quantized: Vec<(i32, AttributeQuantizationTransform)> = Vec::new();
1751            for (local_i, &att_id) in attr_ids.iter().enumerate() {
1752                let att = mesh.attribute(att_id);
1753                let is_parent_attribute =
1754                    ParentAttributes::is_prediction_parent(att, mesh, &self.options);
1755                if decoder_types[local_i] != 2 {
1756                    // An already-integral parent still needs a portable form,
1757                    // for the reason `integral_portable_attribute` gives: the
1758                    // predictor must read what the decoder will reconstruct,
1759                    // not the original the mesh was handed. Only a parent, as
1760                    // upstream only rebuilds the map under `is_parent_encoder`.
1761                    if is_parent_attribute && decoder_types[local_i] == 1 {
1762                        let mut portable = integral_portable_attribute(att, point_ids)?;
1763                        // The same rebuild the quantized arm below does, and for
1764                        // the same reason: the values are in encoding order and
1765                        // a predictor reads its parent as
1766                        // `mapped_index(point_id)`. Without it the map stays the
1767                        // identity, the encoder reads the entry sitting at the
1768                        // point's own index, and the decoder -- whose parent
1769                        // carries the rebuilt map -- reads a different one.
1770                        rebuild_parent_point_map(att, &mut portable, point_ids, mesh.num_points())?;
1771                        parent_registrations.push((att_id, portable));
1772                    }
1773                    quantization_transforms.push(None);
1774                    continue;
1775                }
1776                let quantization_bits =
1777                    self.options
1778                        .get_attribute_int(att_id, "quantization_bits", -1);
1779                let mut q_transform = AttributeQuantizationTransform::new();
1780                q_transform
1781                    .compute_parameters(att, quantization_bits)
1782                    .map_err(|e| {
1783                        DracoError::general(format!(
1784                            "Failed to compute quantization parameters: {e}"
1785                        ))
1786                    })?;
1787                let mut portable = PointAttribute::default();
1788                q_transform
1789                    .transform_attribute(
1790                        att,
1791                        EntryToPointIdMap::from_point_indices(point_ids),
1792                        &mut portable,
1793                    )
1794                    .map_err(|e| {
1795                        DracoError::general(format!("Failed to quantize attribute: {e}"))
1796                    })?;
1797
1798                // Only a parent needs the rebuilt map, which is the guard
1799                // upstream spells `is_parent_encoder()`. What declares a parent
1800                // is `position_is_a_prediction_parent` above -- not the speed
1801                // alone, which is what this used to say.
1802                if is_parent_attribute {
1803                    rebuild_parent_point_map(att, &mut portable, point_ids, mesh.num_points())?;
1804                    parent_registrations.push((att_id, portable));
1805                } else {
1806                    own_portables.push((att_id, portable));
1807                }
1808                quantized.push((att_id, q_transform.clone()));
1809                quantization_transforms.push(Some(q_transform));
1810            }
1811            // Accumulated across groups, not replaced: attributes are encoded one
1812            // group at a time and the position lives in its own, so replacing
1813            // here would take the position's portable values away from every
1814            // later group's predictors.
1815            self.attribute_quantization.extend(quantized);
1816            for (att_id, portable) in parent_registrations {
1817                self.parent_attributes.register(att_id, portable);
1818            }
1819        }
1820
1821        // Pass two, EncodePortableAttributes: the values themselves, in attribute
1822        // order.
1823        let mesh = self
1824            .mesh
1825            .as_ref()
1826            .expect("mesh must be set before encoding");
1827        let mut normal_encoders: Vec<Option<SequentialNormalAttributeEncoder>> = Vec::new();
1828        // See the sibling collection in `encode_attributes`: `self` is the
1829        // `GeometryEncoder` the attribute encoders borrow for the whole loop.
1830        let mut predictions = Vec::new();
1831
1832        for (local_i, &att_id) in attr_ids.iter().enumerate() {
1833            let att = mesh.attribute(att_id);
1834            let decoder_type = decoder_types[local_i];
1835            let _ = att;
1836
1837            match decoder_type {
1838                3 => {
1839                    let mut encoder = SequentialNormalAttributeEncoder::new();
1840                    encoder
1841                        .init(
1842                            self.point_cloud().expect("point_cloud set"),
1843                            att_id,
1844                            &self.options,
1845                        )
1846                        .map_err(|e| {
1847                            DracoError::general(format!("Failed to init normal encoder: {e}"))
1848                        })?;
1849                    encoder.encode_values(
1850                        self.point_cloud().expect("point_cloud set"),
1851                        point_ids,
1852                        out_buffer,
1853                        &self.options,
1854                        self,
1855                    )?;
1856                    if let Some((method, transform)) = encoder.selected_prediction() {
1857                        predictions.push((att_id, method, transform));
1858                    }
1859                    normal_encoders.push(Some(encoder));
1860                }
1861                2 => {
1862                    // The attribute's own copy: a parent's is registered on
1863                    // `self`, anything else type-2 sits in this group's own
1864                    // collection from pass one.
1865                    let portable = self
1866                        .parent_attributes
1867                        .get(att_id)
1868                        .or_else(|| {
1869                            own_portables
1870                                .iter()
1871                                .find(|(id, _)| *id == att_id)
1872                                .map(|(_, att)| att)
1873                        })
1874                        .ok_or_else(|| {
1875                            DracoError::general(format!("Missing portable attribute for {att_id}"))
1876                        })?;
1877
1878                    let mut att_encoder = SequentialIntegerAttributeEncoder::new();
1879                    att_encoder.init(att_id);
1880                    att_encoder.encode_values(
1881                        mesh as &PointCloud,
1882                        point_ids,
1883                        out_buffer,
1884                        &self.options,
1885                        self,
1886                        Some(portable),
1887                        true,
1888                    )?;
1889                    if let Some((method, transform)) = att_encoder.selected_prediction() {
1890                        predictions.push((att_id, method, transform));
1891                    }
1892                    normal_encoders.push(None);
1893                }
1894                1 => {
1895                    let mut att_encoder = SequentialIntegerAttributeEncoder::new();
1896                    att_encoder.init(att_id);
1897                    att_encoder.encode_values(
1898                        mesh as &PointCloud,
1899                        point_ids,
1900                        out_buffer,
1901                        &self.options,
1902                        self,
1903                        None,
1904                        true,
1905                    )?;
1906                    if let Some((method, transform)) = att_encoder.selected_prediction() {
1907                        predictions.push((att_id, method, transform));
1908                    }
1909                    normal_encoders.push(None);
1910                }
1911                0 => {
1912                    let mut att_encoder = SequentialAttributeEncoder::new();
1913                    att_encoder.init(att_id);
1914                    att_encoder.encode_values(mesh as &PointCloud, point_ids, out_buffer)?;
1915                    normal_encoders.push(None);
1916                }
1917                _ => {
1918                    return Err(DracoError::general(format!(
1919                        "Unsupported encoder type {}",
1920                        decoder_type
1921                    )));
1922                }
1923            }
1924        }
1925
1926        // Pass three, EncodeDataNeededByPortableTransforms: the parameters a
1927        // decoder needs to undo each transform -- quantization ranges, and the
1928        // octahedron's bit count. Separate from pass two because upstream emits
1929        // every attribute's values first and only then every attribute's
1930        // transform data, so the two cannot be interleaved.
1931        for (local_i, &decoder_type) in decoder_types.iter().enumerate() {
1932            match decoder_type {
1933                3 => {
1934                    let major = out_buffer.version_major();
1935                    let minor = out_buffer.version_minor();
1936                    let bitstream_version = crate::version::bitstream_version(major, minor);
1937                    if bitstream_version != 0 && bitstream_version < 0x0200 {
1938                        continue;
1939                    }
1940                    if let Some(ref encoder) = normal_encoders[local_i] {
1941                        encoder
1942                            .encode_data_needed_by_portable_transform(out_buffer)
1943                            .map_err(|err| {
1944                                DracoError::general(format!(
1945                                    "Failed to encode normal transform data: {err}"
1946                                ))
1947                            })?;
1948                    }
1949                }
1950                2 => {
1951                    if self.quantization_parameters_are_inline(attr_ids[local_i]) {
1952                        continue;
1953                    }
1954                    if let Some(ref q_transform) = quantization_transforms[local_i] {
1955                        q_transform.encode_parameters(out_buffer).map_err(|e| {
1956                            DracoError::general(format!(
1957                                "Failed to encode quantization parameters: {e}"
1958                            ))
1959                        })?;
1960                    }
1961                }
1962                1 | 0 => {}
1963                _ => {}
1964            }
1965        }
1966
1967        self.attribute_predictions.extend(predictions);
1968        Ok(())
1969    }
1970
1971    fn compute_number_of_encoded_faces(&mut self) {
1972        self.num_encoded_faces = self.encoded_num_faces();
1973    }
1974
1975    /// The faces a decoder reconstructs, after C++
1976    /// `ComputeNumberOfEncodedFaces`: EdgeBreaker leaves degenerate faces out of
1977    /// the stream, the sequential encoder writes every face.
1978    fn encoded_num_faces(&self) -> usize {
1979        match (self.method, self.corner_table.as_ref()) {
1980            (1, Some(corner_table)) => {
1981                corner_table.num_faces() - corner_table.num_degenerated_faces()
1982            }
1983            _ => self.mesh.as_ref().map_or(0, Mesh::num_faces),
1984        }
1985    }
1986
1987    fn build_encoded_mesh_info(&mut self) -> Result<EncodedMeshInfo, DracoError> {
1988        let num_attributes = self
1989            .mesh
1990            .as_ref()
1991            .expect("mesh must be set before encoding")
1992            .num_attributes();
1993        let mut attributes = Vec::with_capacity(num_attributes as usize);
1994
1995        for att_id in 0..num_attributes {
1996            // Every attribute outside the split encode, and the position
1997            // inside it, is walked in the position's order.
1998            let num_encoded_values = self
1999                .split_value_counts
2000                .iter()
2001                .find(|(id, _)| *id == att_id)
2002                .map_or(self.point_ids.len(), |&(_, count)| count);
2003
2004            let (position_min, position_max) =
2005                self.position_bounds_for_attribute(att_id, &self.point_ids)?;
2006            let mesh = self
2007                .mesh
2008                .as_ref()
2009                .expect("mesh must be set before encoding");
2010            let encoder_type = self.attribute_encoder_type(mesh, att_id);
2011            let quantization_bits = match encoder_type {
2012                SequentialAttributeEncoderType::Quantization
2013                | SequentialAttributeEncoderType::Normals => Some(self.options.get_attribute_int(
2014                    att_id,
2015                    "quantization_bits",
2016                    -1,
2017                )),
2018                // A `quantization_bits` option on an integer or generic
2019                // attribute never reaches a transform, so reporting it would
2020                // describe the request rather than the encode.
2021                SequentialAttributeEncoderType::Integer
2022                | SequentialAttributeEncoderType::Generic => None,
2023            };
2024            let prediction = self
2025                .attribute_predictions
2026                .iter()
2027                .find(|(id, _, _)| *id == att_id)
2028                .map(|&(_, method, transform)| (method, transform));
2029            let att = mesh.attribute(att_id);
2030            attributes.push(EncodedAttributeInfo {
2031                source_attribute_id: att_id,
2032                attribute_type: att.attribute_type(),
2033                data_type: att.data_type(),
2034                num_components: att.num_components(),
2035                normalized: att.normalized(),
2036                unique_id: att.unique_id(),
2037                num_encoded_values,
2038                encoder_type,
2039                quantization_bits,
2040                prediction,
2041                position_min,
2042                position_max,
2043            });
2044        }
2045
2046        let (source_num_points, num_faces) = self
2047            .mesh
2048            .as_ref()
2049            .map(|mesh| (mesh.num_points(), mesh.num_faces()))
2050            .expect("mesh must be set before encoding");
2051        let encoded_num_points = if self.method == 0 {
2052            source_num_points
2053        } else {
2054            self.encoded_num_points_for_mesh()?
2055        };
2056
2057        let (mut major, mut minor) = self.options.get_version();
2058        if major == 0 && minor == 0 {
2059            (major, minor) = DEFAULT_MESH_VERSION;
2060        }
2061        let traversal = (self.method == 1).then(|| {
2062            select_edgebreaker_traversal(
2063                self.options.get_speed() as usize,
2064                num_faces,
2065                self.options.get_global_int("force_predictive_traversal", 0) == 1,
2066            )
2067        });
2068        Ok(EncodedMeshInfo {
2069            encoding_method: self.method,
2070            bitstream_version: (major, minor),
2071            traversal,
2072            speed: self.options.get_speed(),
2073            single_connectivity: self.use_single_connectivity,
2074            num_encoded_faces: self.encoded_num_faces(),
2075            num_encoded_points: encoded_num_points,
2076            attributes,
2077        })
2078    }
2079
2080    /// The points a decoder reconstructs from an EdgeBreaker stream, as C++
2081    /// `MeshEdgebreakerEncoder::ComputeNumberOfEncodedPoints` counts them.
2082    ///
2083    /// Its point-id check is left out: a point change the decoder can see is
2084    /// also an attribute-vertex change, and identical unwelded points merge.
2085    fn encoded_num_points_for_mesh(&self) -> Result<usize, DracoError> {
2086        let mesh = self
2087            .mesh
2088            .as_ref()
2089            .expect("mesh must be set before encoding");
2090        let corner_table = self
2091            .corner_table
2092            .as_ref()
2093            .ok_or_else(|| DracoError::general("corner_table must be set".to_string()))?;
2094        let mut num_points = corner_table.num_vertices() - corner_table.num_isolated_vertices();
2095        if self.use_single_connectivity || mesh.num_attributes() <= 1 {
2096            return Ok(num_points);
2097        }
2098
2099        // Without interior seams an attribute shares the position's table.
2100        let seams = &self.attribute_seam_corners;
2101        if seams.is_empty() {
2102            return Ok(num_points);
2103        }
2104
2105        for vertex in 0..corner_table.num_vertices() {
2106            let vertex = crate::geometry_indices::VertexIndex(vertex as u32);
2107            let first_corner = corner_table.left_most_corner(vertex);
2108            if first_corner == INVALID_CORNER_INDEX {
2109                continue;
2110            }
2111            let mut last_corner = first_corner;
2112            let mut corner = corner_table.swing_right(first_corner);
2113            let mut num_attribute_seams = 0usize;
2114            while corner != INVALID_CORNER_INDEX {
2115                if seams.contains(last_corner) {
2116                    num_attribute_seams += 1;
2117                }
2118                if corner == first_corner {
2119                    break;
2120                }
2121                last_corner = corner;
2122                corner = corner_table.swing_right(corner);
2123            }
2124            // A full turn around an interior vertex crosses its first seam twice.
2125            if !corner_table.is_vertex_on_boundary(vertex) && num_attribute_seams > 0 {
2126                num_points += num_attribute_seams - 1;
2127            } else {
2128                num_points += num_attribute_seams;
2129            }
2130        }
2131        Ok(num_points)
2132    }
2133
2134    fn position_bounds_for_attribute(
2135        &self,
2136        att_id: i32,
2137        point_ids: &[PointIndex],
2138    ) -> Result<PositionBounds, DracoError> {
2139        let mesh = self
2140            .mesh
2141            .as_ref()
2142            .expect("mesh must be set before encoding");
2143        let att = mesh.attribute(att_id);
2144        if att.attribute_type() != GeometryAttributeType::Position {
2145            return Ok((None, None));
2146        }
2147        if att.num_components() != 3 || att.data_type() != DataType::Float32 {
2148            return Ok((None, None));
2149        }
2150
2151        if self.decoder_type_for_attribute(att_id) == 2 {
2152            let quantization_bits = self
2153                .options
2154                .get_attribute_int(att_id, "quantization_bits", -1);
2155            // The encode has already computed these for this attribute, and
2156            // computing them again means sweeping every value for its minimum
2157            // a second time. Recompute only if the attribute was quantized by
2158            // some path that did not record it, so this reports the same
2159            // bounds either way.
2160            let recorded = self
2161                .attribute_quantization
2162                .iter()
2163                .find(|(id, _)| *id == att_id)
2164                .map(|(_, transform)| transform.clone());
2165            let q_transform = match recorded {
2166                Some(transform) => transform,
2167                None => {
2168                    let mut transform = AttributeQuantizationTransform::new();
2169                    transform
2170                        .compute_parameters(att, quantization_bits)
2171                        .map_err(|e| {
2172                            DracoError::general(format!(
2173                                "Failed to compute position quantization parameters: {e}"
2174                            ))
2175                        })?;
2176                    transform
2177                }
2178            };
2179
2180            // These are the bounds of the attribute as the decoder will see it,
2181            // so each extreme goes through the same quantize/dequantize round
2182            // trip the encoded values do. The round trip is monotonic per
2183            // component, so the extremes of the round-tripped values are the
2184            // round-tripped extremes -- folding the original and transforming
2185            // six scalars gives the same answer as building the portable and
2186            // dequantized attributes to fold the result, without two full
2187            // passes over every point and the two attributes they allocate.
2188            // `quantization_round_trip_monotonic_test` pins that property.
2189            let (min, max) = Self::position_bounds_from_attribute(att, point_ids)?;
2190            let (Some(min), Some(max)) = (min, max) else {
2191                return Ok((None, None));
2192            };
2193            let round_trip = |bound: Vec<f64>| -> Result<Vec<f64>, DracoError> {
2194                bound
2195                    .into_iter()
2196                    .enumerate()
2197                    .map(|(component, value)| {
2198                        q_transform
2199                            .round_trip_component(component, value as f32)
2200                            .map(f64::from)
2201                            .map_err(|e| {
2202                                DracoError::general(format!(
2203                                    "Failed to quantize position bounds for encoded mesh info: {e}"
2204                                ))
2205                            })
2206                    })
2207                    .collect()
2208            };
2209            return Ok((Some(round_trip(min)?), Some(round_trip(max)?)));
2210        }
2211
2212        Self::position_bounds_from_attribute(att, point_ids)
2213    }
2214
2215    fn position_bounds_from_attribute(
2216        att: &PointAttribute,
2217        point_ids: &[PointIndex],
2218    ) -> Result<PositionBounds, DracoError> {
2219        let count = if point_ids.is_empty() {
2220            att.size()
2221        } else {
2222            point_ids.len()
2223        };
2224        if count == 0 {
2225            return Ok((None, None));
2226        }
2227
2228        let stride = usize::try_from(att.byte_stride()).map_err(|_| {
2229            DracoError::general("Position attribute has invalid byte stride".to_string())
2230        })?;
2231        let bytes = att.buffer().data();
2232        let mut min = [f32::INFINITY; 3];
2233        let mut max = [f32::NEG_INFINITY; 3];
2234
2235        for i in 0..count {
2236            let point = if point_ids.is_empty() {
2237                PointIndex(i as u32)
2238            } else {
2239                point_ids[i]
2240            };
2241            let value_index = att.mapped_index(point);
2242            if value_index == INVALID_ATTRIBUTE_VALUE_INDEX {
2243                return Err(DracoError::general(
2244                    "Position attribute point map contains an invalid entry".to_string(),
2245                ));
2246            }
2247
2248            // A point's three components are twelve contiguous bytes, so one
2249            // slice of a fixed size answers what three offset computations and
2250            // three bounds checks answered per point before.
2251            const POSITION_BYTES: usize = 3 * 4;
2252            let value_offset = (value_index.0 as usize)
2253                .checked_mul(stride)
2254                .ok_or_else(|| {
2255                    DracoError::general("Position attribute offset overflow".to_string())
2256                })?;
2257            let end = value_offset.checked_add(POSITION_BYTES).ok_or_else(|| {
2258                DracoError::general("Position attribute offset overflow".to_string())
2259            })?;
2260            let Some(point_bytes) = bytes.get(value_offset..end) else {
2261                return Err(DracoError::general(
2262                    "Position attribute buffer is shorter than metadata".to_string(),
2263                ));
2264            };
2265            for component in 0..3 {
2266                let at = component * 4;
2267                let value = f32::from_le_bytes([
2268                    point_bytes[at],
2269                    point_bytes[at + 1],
2270                    point_bytes[at + 2],
2271                    point_bytes[at + 3],
2272                ]);
2273                min[component] = min[component].min(value);
2274                max[component] = max[component].max(value);
2275            }
2276        }
2277
2278        Ok((
2279            Some(min.into_iter().map(f64::from).collect()),
2280            Some(max.into_iter().map(f64::from).collect()),
2281        ))
2282    }
2283}
2284
2285impl Default for MeshEncoder {
2286    fn default() -> Self {
2287        Self::new()
2288    }
2289}
2290
2291/// The corners after which some attribute changes vertex going around a
2292/// position vertex: bit `c` is set when an attribute's table gives
2293/// `swing_right(c)` a different vertex than `c`, `swing_right` taken in the
2294/// position's table.
2295///
2296/// This is all the point count needs from the attribute tables, so each one is
2297/// folded in while the encode still has it and freed as before, instead of
2298/// being held until the report: one bit per corner rather than a table's
2299/// twelve bytes.
2300#[derive(Debug, Default)]
2301struct AttributeSeamCorners {
2302    words: Vec<u64>,
2303}
2304
2305impl AttributeSeamCorners {
2306    fn mark(&mut self, corner_table: &CornerTable, attribute_table: &CornerTable) {
2307        let num_corners = corner_table.num_corners();
2308        self.words.resize(num_corners.div_ceil(64), 0);
2309        for c in 0..num_corners {
2310            let corner = crate::geometry_indices::CornerIndex(c as u32);
2311            let next = corner_table.swing_right(corner);
2312            if next != INVALID_CORNER_INDEX
2313                && attribute_table.vertex(next) != attribute_table.vertex(corner)
2314            {
2315                self.words[c / 64] |= 1 << (c % 64);
2316            }
2317        }
2318    }
2319
2320    fn is_empty(&self) -> bool {
2321        self.words.is_empty()
2322    }
2323
2324    fn contains(&self, corner: crate::geometry_indices::CornerIndex) -> bool {
2325        let c = corner.0 as usize;
2326        self.words
2327            .get(c / 64)
2328            .is_some_and(|word| word & (1 << (c % 64)) != 0)
2329    }
2330}