Skip to main content

draco_io/
gltf_writer.rs

1//! glTF/GLB writer with Draco mesh compression support.
2//!
3//! This module provides support for writing glTF 2.0 files with the
4//! `KHR_draco_mesh_compression` extension. Multiple output formats are supported:
5//!
6//! - **GLB** - Binary container (single .glb file)
7//! - **glTF + .bin** - JSON + separate binary file
8//! - **glTF (embedded)** - Single JSON file with base64-encoded data URIs
9//!
10//! # Example - GLB
11//!
12//! ```no_run
13//! use draco_io::gltf_writer::GltfWriter;
14//!
15//! let mesh = draco_core::mesh::Mesh::new();
16//! let mut writer = GltfWriter::new();
17//! writer.add_draco_mesh(&mesh, Some("MyMesh"), None)?;  // Uses default quantization
18//! writer.write_glb("output.glb")?;
19//! # Ok::<(), draco_io::GltfWriteError>(())
20//! ```
21//!
22//! # Example - Pure Text glTF (Embedded)
23//!
24//! ```no_run
25//! # let writer = draco_io::gltf_writer::GltfWriter::new();
26//! writer.write_gltf_embedded("output.gltf")?;
27//! // Creates a single text file with base64-embedded binary data
28//! # Ok::<(), draco_io::GltfWriteError>(())
29//! ```
30//!
31//! # Example - Writing a Scene Graph
32//!
33//! ```no_run
34//! use draco_io::gltf_writer::GltfWriter;
35//! use draco_io::{MeshInstance, Scene, SceneNode};
36//! # let mesh = draco_core::mesh::Mesh::new();
37//!
38//! let mut root = SceneNode::new(Some("Root".to_string()));
39//! root.mesh_instances.push(MeshInstance {
40//!     name: Some("Mesh".to_string()),
41//!     mesh,
42//!     transform: None,
43//! });
44//! let scene = Scene { name: Some("Scene".to_string()), root_nodes: vec![root] };
45//!
46//! let mut writer = GltfWriter::new();
47//! writer.add_scene(&scene, None)?;
48//! writer.write_glb("scene.glb")?;
49//! # Ok::<(), draco_io::GltfWriteError>(())
50//! ```
51
52use std::collections::HashMap;
53use std::fs;
54use std::io::{self, Write};
55use std::path::Path;
56
57use draco_core::draco_types::DataType;
58use draco_core::encoder_buffer::EncoderBuffer;
59use draco_core::encoder_options::EncoderOptions;
60use draco_core::geometry_attribute::{GeometryAttributeType, PointAttribute};
61use draco_core::mesh::Mesh;
62use draco_core::mesh_encoder::{EncodedAttributeInfo, EncodedMeshInfo, MeshEncoder};
63use serde::Serialize;
64use thiserror::Error;
65
66use crate::gltf_compress::{EncodingMethod, GltfCompressionOptions};
67use crate::gltf_container::{serialize_gltf_document, GltfContainerFormat, OutputFormat};
68use crate::traits::{WriteToBytes, Writer};
69
70/// Errors that can occur when writing glTF files.
71#[derive(Error, Debug)]
72pub enum GltfWriteError {
73    /// Filesystem or stream I/O failed.
74    #[error("IO error: {0}")]
75    Io(#[from] io::Error),
76
77    /// glTF JSON serialization failed.
78    #[error("JSON serialize error: {0}")]
79    Json(#[from] serde_json::Error),
80
81    /// Draco encoding failed.
82    #[error("Draco encode error: {0}")]
83    DracoEncode(#[source] draco_core::DracoError),
84
85    /// The encoder violated its documented postcondition.
86    #[error("Draco encoder invariant failed: {0}")]
87    EncoderInvariant(String),
88
89    /// Mesh data cannot be represented as supported glTF Draco geometry.
90    #[error("Invalid mesh: {0}")]
91    InvalidMesh(String),
92
93    /// The mesh or scene uses a feature outside this writer's supported scope.
94    #[error("Unsupported feature: {0}")]
95    Unsupported(String),
96
97    /// Compression options are outside their supported range.
98    #[error("Invalid compression options: {0}")]
99    InvalidOptions(String),
100
101    /// A checked size computation or allocation failed.
102    #[error("Resource limit exceeded: {0}")]
103    ResourceLimit(String),
104
105    /// Generated JSON was unexpectedly not UTF-8.
106    #[error("UTF-8 conversion error: {0}")]
107    Utf8(#[from] std::string::FromUtf8Error),
108}
109
110/// Result type used by glTF writers.
111pub type Result<T> = std::result::Result<T, GltfWriteError>;
112
113// ============================================================================
114// glTF JSON Schema for Writing
115// ============================================================================
116
117#[derive(Debug, Serialize)]
118#[serde(rename_all = "camelCase")]
119struct GltfRoot {
120    asset: Asset,
121    #[serde(skip_serializing_if = "Vec::is_empty")]
122    accessors: Vec<AccessorOut>,
123    #[serde(skip_serializing_if = "Vec::is_empty")]
124    buffer_views: Vec<BufferViewOut>,
125    #[serde(skip_serializing_if = "Vec::is_empty")]
126    buffers: Vec<BufferOut>,
127    #[serde(skip_serializing_if = "Vec::is_empty")]
128    meshes: Vec<MeshOut>,
129    #[serde(skip_serializing_if = "Vec::is_empty")]
130    nodes: Vec<NodeOut>,
131    #[serde(skip_serializing_if = "Option::is_none")]
132    scene: Option<usize>,
133    #[serde(skip_serializing_if = "Vec::is_empty")]
134    scenes: Vec<SceneOut>,
135    #[serde(skip_serializing_if = "Vec::is_empty")]
136    extensions_used: Vec<String>,
137    #[serde(skip_serializing_if = "Vec::is_empty")]
138    extensions_required: Vec<String>,
139}
140
141#[derive(Debug, Serialize)]
142struct Asset {
143    version: String,
144    #[serde(skip_serializing_if = "Option::is_none")]
145    generator: Option<String>,
146}
147
148#[derive(Debug, Clone, Serialize)]
149#[serde(rename_all = "camelCase")]
150struct AccessorOut {
151    #[serde(skip_serializing_if = "Option::is_none")]
152    buffer_view: Option<usize>,
153    #[serde(skip_serializing_if = "Option::is_none")]
154    byte_offset: Option<usize>,
155    component_type: u32,
156    #[serde(skip_serializing_if = "Option::is_none")]
157    normalized: Option<bool>,
158    count: usize,
159    #[serde(rename = "type")]
160    accessor_type: String,
161    #[serde(skip_serializing_if = "Vec::is_empty")]
162    min: Vec<f64>,
163    #[serde(skip_serializing_if = "Vec::is_empty")]
164    max: Vec<f64>,
165}
166
167#[derive(Debug, Clone, Serialize)]
168#[serde(rename_all = "camelCase")]
169struct BufferViewOut {
170    buffer: usize,
171    #[serde(skip_serializing_if = "Option::is_none")]
172    byte_offset: Option<usize>,
173    byte_length: usize,
174}
175
176#[derive(Debug, Serialize)]
177#[serde(rename_all = "camelCase")]
178struct BufferOut {
179    byte_length: usize,
180    #[serde(skip_serializing_if = "Option::is_none")]
181    uri: Option<String>,
182}
183
184#[derive(Debug, Clone, Serialize)]
185#[serde(rename_all = "camelCase")]
186struct MeshOut {
187    #[serde(skip_serializing_if = "Option::is_none")]
188    name: Option<String>,
189    primitives: Vec<PrimitiveOut>,
190}
191
192#[derive(Debug, Clone, Serialize)]
193#[serde(rename_all = "camelCase")]
194struct PrimitiveOut {
195    attributes: HashMap<String, usize>,
196    #[serde(skip_serializing_if = "Option::is_none")]
197    indices: Option<usize>,
198    #[serde(skip_serializing_if = "Option::is_none")]
199    mode: Option<u32>,
200    #[serde(skip_serializing_if = "Option::is_none")]
201    extensions: Option<PrimitiveExtensionsOut>,
202}
203
204#[derive(Debug, Clone, Serialize)]
205struct PrimitiveExtensionsOut {
206    #[serde(rename = "KHR_draco_mesh_compression")]
207    khr_draco_mesh_compression: DracoExtensionOut,
208}
209
210#[derive(Debug, Clone, Serialize)]
211#[serde(rename_all = "camelCase")]
212struct DracoExtensionOut {
213    buffer_view: usize,
214    attributes: HashMap<String, usize>,
215}
216
217#[derive(Debug, Clone, Serialize)]
218#[serde(rename_all = "camelCase")]
219struct NodeOut {
220    #[serde(skip_serializing_if = "Option::is_none")]
221    mesh: Option<usize>,
222    #[serde(skip_serializing_if = "Option::is_none")]
223    name: Option<String>,
224    #[serde(skip_serializing_if = "Vec::is_empty")]
225    children: Vec<usize>,
226    /// 4x4 transformation matrix (column-major).
227    #[serde(skip_serializing_if = "Option::is_none")]
228    matrix: Option<[f32; 16]>,
229}
230
231#[derive(Debug, Clone, Serialize)]
232#[serde(rename_all = "camelCase")]
233struct SceneOut {
234    #[serde(skip_serializing_if = "Option::is_none")]
235    name: Option<String>,
236    #[serde(skip_serializing_if = "Vec::is_empty")]
237    nodes: Vec<usize>,
238}
239
240// ============================================================================
241// GLB Constants
242// ============================================================================
243
244// ============================================================================
245// GltfWriter
246// ============================================================================
247
248/// A writer for creating glTF/GLB files with Draco-compressed meshes.
249pub struct GltfWriter {
250    accessors: Vec<AccessorOut>,
251    buffer_views: Vec<BufferViewOut>,
252    meshes: Vec<MeshOut>,
253    nodes: Vec<NodeOut>,
254    scenes: Vec<SceneOut>,
255    default_scene: Option<usize>,
256    binary_data: Vec<u8>,
257    has_draco: bool,
258}
259
260impl Default for GltfWriter {
261    fn default() -> Self {
262        Self::new()
263    }
264}
265
266pub(crate) fn encode_draco_mesh_with_info(
267    mesh: &Mesh,
268    compression: &GltfCompressionOptions,
269) -> Result<(Vec<u8>, EncodedMeshInfo)> {
270    compression
271        .validate()
272        .map_err(|error| GltfWriteError::InvalidOptions(error.to_string()))?;
273    validate_mesh_for_gltf_draco(mesh)?;
274
275    if mesh.num_faces() == 0 {
276        return Err(GltfWriteError::InvalidMesh("Mesh has no faces".into()));
277    }
278
279    let mut encoder = MeshEncoder::new();
280    encoder.set_mesh(mesh.clone());
281
282    let mut options = EncoderOptions::new();
283    options.set_global_int("encoding_speed", compression.encoding_speed as i32);
284    options.set_global_int("decoding_speed", compression.decoding_speed as i32);
285    match compression.encoding_method {
286        EncodingMethod::Auto => {}
287        EncodingMethod::Sequential => options.set_encoding_method(0),
288        EncodingMethod::Edgebreaker => options.set_encoding_method(1),
289    }
290
291    // `None` deliberately leaves a floating-point attribute unquantized.
292    for i in 0..mesh.num_attributes() {
293        let att = mesh.attribute(i);
294        if att.data_type() == draco_core::draco_types::DataType::Float32 {
295            let bits = match att.attribute_type() {
296                GeometryAttributeType::Position => compression.quantization.position,
297                GeometryAttributeType::Normal => compression.quantization.normal,
298                GeometryAttributeType::Color => compression.quantization.color,
299                GeometryAttributeType::TexCoord => compression.quantization.texcoord,
300                GeometryAttributeType::Generic | GeometryAttributeType::Invalid => {
301                    compression.quantization.generic
302                }
303            };
304            if let Some(bits) = bits {
305                options.set_attribute_int(i, "quantization_bits", bits as i32);
306            }
307        }
308    }
309
310    let mut enc_buffer = EncoderBuffer::new();
311    encoder
312        .encode(&options, &mut enc_buffer)
313        .map_err(GltfWriteError::DracoEncode)?;
314    let encoded_info = encoder.encoded_mesh_info().cloned().ok_or_else(|| {
315        GltfWriteError::EncoderInvariant("encoder did not return mesh info".into())
316    })?;
317
318    let encoded = enc_buffer.data();
319    let mut bytes = Vec::new();
320    bytes
321        .try_reserve_exact(encoded.len())
322        .map_err(|_| GltfWriteError::ResourceLimit("Draco output allocation failed".into()))?;
323    bytes.extend_from_slice(encoded);
324    Ok((bytes, encoded_info))
325}
326
327/// Encode a mesh to a Draco bitstream using the same settings as `GltfWriter`.
328///
329/// This is useful for tools/tests that need the raw `.drc` bytes without
330/// wrapping them into a glTF/GLB container.
331pub fn encode_draco_mesh(
332    mesh: &Mesh,
333    options: impl Into<Option<GltfCompressionOptions>>,
334) -> Result<Vec<u8>> {
335    let options = options.into().unwrap_or_default();
336    encode_draco_mesh_with_info(mesh, &options).map(|(bytes, _)| bytes)
337}
338
339impl GltfWriter {
340    /// Create a new glTF writer.
341    pub fn new() -> Self {
342        Self {
343            accessors: Vec::new(),
344            buffer_views: Vec::new(),
345            meshes: Vec::new(),
346            nodes: Vec::new(),
347            scenes: Vec::new(),
348            default_scene: None,
349            binary_data: Vec::new(),
350            has_draco: false,
351        }
352    }
353
354    /// Add a full scene graph (nodes + hierarchy + transforms) to the output.
355    ///
356    /// Geometry is written using Draco compression (KHR_draco_mesh_compression).
357    /// Non-Draco writing (raw accessors) is not currently supported by this writer.
358    pub fn add_scene(
359        &mut self,
360        scene: &crate::scene::Scene,
361        options: impl Into<Option<GltfCompressionOptions>>,
362    ) -> Result<usize> {
363        let options = options.into().unwrap_or_default();
364
365        // Build nodes recursively and record root node indices.
366        let mut root_node_indices = Vec::new();
367        root_node_indices
368            .try_reserve_exact(scene.root_nodes.len())
369            .map_err(|_| {
370                GltfWriteError::ResourceLimit("scene root table allocation failed".into())
371            })?;
372        for root in &scene.root_nodes {
373            let node_idx = self.push_scene_node(root, &options)?;
374            root_node_indices.push(node_idx);
375        }
376
377        let scene_idx = self.scenes.len();
378        self.scenes.push(SceneOut {
379            name: scene.name.clone(),
380            nodes: root_node_indices,
381        });
382
383        if self.default_scene.is_none() {
384            self.default_scene = Some(scene_idx);
385        }
386
387        Ok(scene_idx)
388    }
389
390    fn transform_to_gltf_matrix(transform: &crate::scene::Transform) -> [f32; 16] {
391        // Input is row-major; glTF expects column-major.
392        let m = &transform.matrix;
393        [
394            m[0][0], m[1][0], m[2][0], m[3][0], m[0][1], m[1][1], m[2][1], m[3][1], m[0][2],
395            m[1][2], m[2][2], m[3][2], m[0][3], m[1][3], m[2][3], m[3][3],
396        ]
397    }
398
399    fn push_scene_node(
400        &mut self,
401        node: &crate::scene::SceneNode,
402        options: &GltfCompressionOptions,
403    ) -> Result<usize> {
404        // glTF nodes can reference at most one mesh; if multiple mesh instances
405        // exist, we create child nodes for each instance.
406
407        // First, create this node (without children for now).
408        let node_idx = self.nodes.len();
409        self.nodes.push(NodeOut {
410            mesh: None,
411            name: node.name.clone(),
412            children: Vec::new(),
413            matrix: node.transform.as_ref().map(Self::transform_to_gltf_matrix),
414        });
415
416        // Attach mesh instances.
417        if node.mesh_instances.len() == 1 && node.mesh_instances[0].transform.is_none() {
418            let mesh_instance = &node.mesh_instances[0];
419            let mesh_idx = self.encode_draco_mesh_internal(
420                &mesh_instance.mesh,
421                mesh_instance.name.as_deref(),
422                options,
423            )?;
424            self.nodes[node_idx].mesh = Some(mesh_idx);
425        } else if !node.mesh_instances.is_empty() {
426            for (i, mesh_instance) in node.mesh_instances.iter().enumerate() {
427                let mesh_idx = self.encode_draco_mesh_internal(
428                    &mesh_instance.mesh,
429                    mesh_instance.name.as_deref(),
430                    options,
431                )?;
432                let child_idx = self.nodes.len();
433                self.nodes.push(NodeOut {
434                    mesh: Some(mesh_idx),
435                    name: mesh_instance.name.clone().or_else(|| {
436                        node.name
437                            .as_ref()
438                            .map(|n| format!("{}_mesh_instance{}", n, i))
439                    }),
440                    children: Vec::new(),
441                    matrix: mesh_instance
442                        .transform
443                        .as_ref()
444                        .map(Self::transform_to_gltf_matrix),
445                });
446                self.nodes[node_idx].children.push(child_idx);
447            }
448        }
449
450        // Recurse into children.
451        for child in &node.children {
452            let child_idx = self.push_scene_node(child, options)?;
453            self.nodes[node_idx].children.push(child_idx);
454        }
455
456        Ok(node_idx)
457    }
458
459    fn encode_draco_mesh_internal(
460        &mut self,
461        mesh: &Mesh,
462        name: Option<&str>,
463        options: &GltfCompressionOptions,
464    ) -> Result<usize> {
465        let (draco_data, encoded_info) = encode_draco_mesh_with_info(mesh, options)?;
466        let draco_buffer_view_idx = self.append_buffer_view(&draco_data)?;
467        let primitive = self.build_draco_primitive(&encoded_info, draco_buffer_view_idx)?;
468
469        let mesh_idx = self.meshes.len();
470        self.meshes.push(MeshOut {
471            name: name.map(String::from),
472            primitives: vec![primitive],
473        });
474
475        self.has_draco = true;
476        Ok(mesh_idx)
477    }
478
479    fn append_buffer_view(&mut self, data: &[u8]) -> Result<usize> {
480        let padding = (4 - self.binary_data.len() % 4) % 4;
481        let additional = padding
482            .checked_add(data.len())
483            .ok_or_else(|| GltfWriteError::ResourceLimit("binary buffer size overflow".into()))?;
484        let aligned_len =
485            self.binary_data.len().checked_add(padding).ok_or_else(|| {
486                GltfWriteError::ResourceLimit("binary buffer size overflow".into())
487            })?;
488        self.binary_data
489            .try_reserve_exact(additional)
490            .map_err(|_| GltfWriteError::ResourceLimit("binary buffer allocation failed".into()))?;
491        self.binary_data.resize(aligned_len, 0);
492        let aligned_offset = self.binary_data.len();
493
494        self.binary_data.extend_from_slice(data);
495        let buffer_view_idx = self.buffer_views.len();
496        self.buffer_views.push(BufferViewOut {
497            buffer: 0,
498            byte_offset: Some(aligned_offset),
499            byte_length: data.len(),
500        });
501
502        Ok(buffer_view_idx)
503    }
504
505    fn build_draco_primitive(
506        &mut self,
507        encoded_info: &EncodedMeshInfo,
508        draco_buffer_view_idx: usize,
509    ) -> Result<PrimitiveOut> {
510        let (attributes, draco_attributes) = self.add_mesh_attribute_accessors(encoded_info)?;
511        let index_count = encoded_info
512            .num_encoded_faces
513            .checked_mul(3)
514            .ok_or_else(|| GltfWriteError::ResourceLimit("index count overflow".into()))?;
515        let indices_accessor_idx = self.add_indices_accessor(index_count);
516
517        Ok(PrimitiveOut {
518            attributes,
519            indices: Some(indices_accessor_idx),
520            mode: Some(4), // TRIANGLES
521            extensions: Some(PrimitiveExtensionsOut {
522                khr_draco_mesh_compression: DracoExtensionOut {
523                    buffer_view: draco_buffer_view_idx,
524                    attributes: draco_attributes,
525                },
526            }),
527        })
528    }
529
530    fn add_mesh_attribute_accessors(
531        &mut self,
532        encoded_info: &EncodedMeshInfo,
533    ) -> Result<(HashMap<String, usize>, HashMap<String, usize>)> {
534        let mut attributes = HashMap::new();
535        let mut draco_attributes: HashMap<String, usize> = HashMap::new();
536        let mut counters = GltfSemanticCounters::default();
537
538        for att in &encoded_info.attributes {
539            let (semantic, accessor_type) =
540                gltf_attribute_info(att.attribute_type, att.num_components, &mut counters)?;
541
542            let accessor_idx =
543                self.add_attribute_accessor(att, accessor_type, encoded_info.num_encoded_points)?;
544            attributes.insert(semantic.clone(), accessor_idx);
545            draco_attributes.insert(semantic, att.unique_id as usize);
546        }
547
548        Ok((attributes, draco_attributes))
549    }
550
551    fn add_attribute_accessor(
552        &mut self,
553        att: &EncodedAttributeInfo,
554        accessor_type: &str,
555        count: usize,
556    ) -> Result<usize> {
557        let accessor_idx = self.accessors.len();
558        let (min, max) = if att.attribute_type == GeometryAttributeType::Position {
559            (
560                att.position_min.clone().ok_or_else(|| {
561                    GltfWriteError::InvalidMesh("POSITION accessor is missing min bounds".into())
562                })?,
563                att.position_max.clone().ok_or_else(|| {
564                    GltfWriteError::InvalidMesh("POSITION accessor is missing max bounds".into())
565                })?,
566            )
567        } else {
568            (Vec::new(), Vec::new())
569        };
570        self.accessors.push(AccessorOut {
571            buffer_view: None,
572            byte_offset: None,
573            component_type: component_type_for_data_type(att.data_type)?,
574            normalized: att.normalized.then_some(true),
575            count,
576            accessor_type: accessor_type.to_string(),
577            min,
578            max,
579        });
580        Ok(accessor_idx)
581    }
582
583    fn add_indices_accessor(&mut self, count: usize) -> usize {
584        let accessor_idx = self.accessors.len();
585        self.accessors.push(AccessorOut {
586            buffer_view: None,
587            byte_offset: None,
588            component_type: 5125, // UNSIGNED_INT
589            normalized: None,
590            count,
591            accessor_type: "SCALAR".to_string(),
592            min: Vec::new(),
593            max: Vec::new(),
594        });
595        accessor_idx
596    }
597
598    /// Add a mesh with Draco compression.
599    ///
600    /// # Arguments
601    /// * `mesh` - The mesh to encode
602    /// * `name` - Optional name for the mesh
603    /// * `options` - Optional compression settings. Pass `None` for defaults.
604    ///
605    /// # Returns
606    /// The index of the added mesh.
607    ///
608    /// # Examples
609    /// ```no_run
610    /// use draco_io::{GltfCompressionOptions, GltfWriter, QuantizationOptions};
611    /// # let mesh = draco_core::mesh::Mesh::new();
612    ///
613    /// let mut writer = GltfWriter::new();
614    ///
615    /// // Using defaults (recommended for most cases)
616    /// writer.add_draco_mesh(&mesh, Some("MyMesh"), None)?;
617    ///
618    /// // Custom quantization
619    /// writer.add_draco_mesh(&mesh, Some("HighQuality"), GltfCompressionOptions {
620    ///     quantization: QuantizationOptions { position: Some(16), ..Default::default() },
621    ///     ..Default::default()
622    /// })?;
623    /// # Ok::<(), draco_io::GltfWriteError>(())
624    /// ```
625    pub fn add_draco_mesh(
626        &mut self,
627        mesh: &Mesh,
628        name: Option<&str>,
629        options: impl Into<Option<GltfCompressionOptions>>,
630    ) -> Result<usize> {
631        let options = options.into().unwrap_or_default();
632        let mesh_idx = self.encode_draco_mesh_internal(mesh, name, &options)?;
633
634        // Add a root node for this mesh.
635        let node_idx = self.nodes.len();
636        self.nodes.push(NodeOut {
637            mesh: Some(mesh_idx),
638            name: name.map(String::from),
639            children: Vec::new(),
640            matrix: None,
641        });
642
643        // Default behavior: if caller isn't explicitly constructing scenes,
644        // keep a single default scene that references every node.
645        if self.scenes.is_empty() {
646            self.default_scene = Some(0);
647            self.scenes.push(SceneOut {
648                name: None,
649                nodes: Vec::new(),
650            });
651        }
652        if let Some(0) = self.default_scene {
653            self.scenes[0].nodes.push(node_idx);
654        }
655
656        Ok(mesh_idx)
657    }
658
659    /// Write as GLB (binary glTF) file.
660    pub fn write_glb<P: AsRef<Path>>(&self, path: P) -> Result<()> {
661        let glb_data = self.to_glb()?;
662        fs::write(path, glb_data)?;
663        Ok(())
664    }
665
666    /// Write as separate glTF JSON and .bin files.
667    pub fn write_gltf<P: AsRef<Path>>(&self, json_path: P, bin_path: P) -> Result<()> {
668        let json_path = json_path.as_ref();
669        let bin_path = bin_path.as_ref();
670
671        // Write binary buffer
672        fs::write(bin_path, &self.binary_data)?;
673
674        // Get relative path for URI
675        let bin_uri = bin_path
676            .file_name()
677            .map(|s| s.to_string_lossy().to_string())
678            .unwrap_or_else(|| "buffer.bin".to_string());
679
680        // Build glTF JSON
681        let root = self.build_gltf_root(Some(&bin_uri));
682        let json = serde_json::to_string_pretty(&root)?;
683        fs::write(json_path, json)?;
684
685        Ok(())
686    }
687
688    /// Write as a single glTF JSON file with embedded base64 data URI.
689    ///
690    /// This creates a pure text file with no external dependencies.
691    /// The binary data is embedded directly in the JSON using base64 encoding.
692    ///
693    /// # Example
694    /// ```no_run
695    /// # let writer = draco_io::gltf_writer::GltfWriter::new();
696    /// writer.write_gltf_embedded("model.gltf")?;
697    /// # Ok::<(), draco_io::GltfWriteError>(())
698    /// ```
699    pub fn write_gltf_embedded<P: AsRef<Path>>(&self, path: P) -> Result<()> {
700        fs::write(path, self.to_gltf_embedded()?)?;
701        Ok(())
702    }
703
704    /// Convert to glTF JSON string with embedded base64 data.
705    pub fn to_gltf_embedded(&self) -> Result<String> {
706        let root = serde_json::to_value(self.build_gltf_root(None))?;
707        let bytes = serialize_gltf_document(
708            &root,
709            &self.binary_data,
710            GltfContainerFormat::Gltf,
711            OutputFormat::GltfEmbeddedBuffers,
712        )
713        .map_err(|error| GltfWriteError::InvalidMesh(error.to_string()))?;
714        Ok(String::from_utf8(bytes)?)
715    }
716
717    /// Convert to GLB bytes.
718    pub fn to_glb(&self) -> Result<Vec<u8>> {
719        let root = serde_json::to_value(self.build_gltf_root(None))?;
720        serialize_gltf_document(
721            &root,
722            &self.binary_data,
723            GltfContainerFormat::Glb,
724            OutputFormat::Glb,
725        )
726        .map_err(|error| GltfWriteError::InvalidMesh(error.to_string()))
727    }
728
729    /// Write the default GLB output into a byte vector.
730    pub fn write_to_vec(&self) -> Result<Vec<u8>> {
731        self.to_glb()
732    }
733
734    /// Write the default GLB output into a byte sink.
735    pub fn write_to<W: Write>(&self, writer: &mut W) -> Result<()> {
736        writer.write_all(&self.write_to_vec()?)?;
737        Ok(())
738    }
739
740    fn build_gltf_root(&self, bin_uri: Option<&str>) -> GltfRoot {
741        let mut extensions_used = Vec::new();
742        let mut extensions_required = Vec::new();
743
744        if self.has_draco {
745            extensions_used.push("KHR_draco_mesh_compression".to_string());
746            extensions_required.push("KHR_draco_mesh_compression".to_string());
747        }
748
749        let buffers = if self.binary_data.is_empty() {
750            Vec::new()
751        } else {
752            vec![BufferOut {
753                byte_length: self.binary_data.len(),
754                uri: bin_uri.map(String::from),
755            }]
756        };
757
758        let scene = if self.scenes.is_empty() {
759            if self.nodes.is_empty() {
760                None
761            } else {
762                Some(0)
763            }
764        } else {
765            self.default_scene
766        };
767
768        let scenes = if self.scenes.is_empty() {
769            if self.nodes.is_empty() {
770                Vec::new()
771            } else {
772                vec![SceneOut {
773                    name: None,
774                    nodes: (0..self.nodes.len()).collect(),
775                }]
776            }
777        } else {
778            self.scenes.clone()
779        };
780
781        GltfRoot {
782            asset: Asset {
783                version: "2.0".to_string(),
784                generator: Some("draco-io-rs".to_string()),
785            },
786            accessors: self.accessors.clone(),
787            buffer_views: self.buffer_views.clone(),
788            buffers,
789            meshes: self.meshes.clone(),
790            nodes: self.nodes.clone(),
791            scene,
792            scenes,
793            extensions_used,
794            extensions_required,
795        }
796    }
797}
798
799fn validate_mesh_for_gltf_draco(mesh: &Mesh) -> Result<()> {
800    let mut position_count = 0usize;
801    if mesh.num_faces() == 0 {
802        return Err(GltfWriteError::InvalidMesh("Mesh has no faces".into()));
803    }
804
805    for face_id in 0..mesh.num_faces() {
806        let face_id_u32 = u32::try_from(face_id)
807            .map_err(|_| GltfWriteError::InvalidMesh("mesh has more than u32::MAX faces".into()))?;
808        let face = mesh.face(draco_core::geometry_indices::FaceIndex(face_id_u32));
809        for point in face {
810            if point.0 as usize >= mesh.num_points() {
811                return Err(GltfWriteError::InvalidMesh(format!(
812                    "Face {} references point {} but mesh has {} points",
813                    face_id,
814                    point.0,
815                    mesh.num_points()
816                )));
817            }
818        }
819    }
820
821    for i in 0..mesh.num_attributes() {
822        let att = mesh.attribute(i);
823        validate_attribute_for_gltf(att)?;
824        if att.attribute_type() == GeometryAttributeType::Position {
825            position_count += 1;
826        }
827    }
828
829    if position_count == 0 {
830        return Err(GltfWriteError::InvalidMesh(
831            "glTF Draco mesh requires a POSITION attribute".into(),
832        ));
833    }
834    if position_count > 1 {
835        return Err(GltfWriteError::Unsupported(
836            "glTF supports only one POSITION attribute".into(),
837        ));
838    }
839
840    Ok(())
841}
842
843fn validate_attribute_for_gltf(att: &PointAttribute) -> Result<()> {
844    if att.size() == 0 {
845        return Err(GltfWriteError::InvalidMesh(format!(
846            "Attribute {:?} has no values",
847            att.attribute_type()
848        )));
849    }
850
851    match att.attribute_type() {
852        GeometryAttributeType::Position => {
853            if att.num_components() != 3 || att.data_type() != DataType::Float32 {
854                return Err(GltfWriteError::Unsupported(
855                    "POSITION must be VEC3 FLOAT".into(),
856                ));
857            }
858            if att.normalized() {
859                return Err(GltfWriteError::Unsupported(
860                    "POSITION must not be normalized".into(),
861                ));
862            }
863        }
864        GeometryAttributeType::Normal => {
865            if att.num_components() != 3 || att.data_type() != DataType::Float32 {
866                return Err(GltfWriteError::Unsupported(
867                    "NORMAL must be VEC3 FLOAT".into(),
868                ));
869            }
870            if att.normalized() {
871                return Err(GltfWriteError::Unsupported(
872                    "NORMAL must not be normalized".into(),
873                ));
874            }
875        }
876        GeometryAttributeType::Color => {
877            if !(att.num_components() == 3 || att.num_components() == 4) {
878                return Err(GltfWriteError::Unsupported(
879                    "COLOR attributes must be VEC3 or VEC4".into(),
880                ));
881            }
882            validate_normalized_integer_or_float(att, "COLOR")?;
883        }
884        GeometryAttributeType::TexCoord => {
885            if att.num_components() != 2 {
886                return Err(GltfWriteError::Unsupported(
887                    "TEXCOORD attributes must be VEC2".into(),
888                ));
889            }
890            validate_normalized_integer_or_float(att, "TEXCOORD")?;
891        }
892        GeometryAttributeType::Generic => {
893            if !(1..=4).contains(&att.num_components()) {
894                return Err(GltfWriteError::Unsupported(
895                    "Generic attributes must have 1..=4 components".into(),
896                ));
897            }
898            component_type_for_data_type(att.data_type())?;
899            if att.data_type() == DataType::Uint32 {
900                return Err(GltfWriteError::Unsupported(
901                    "UNSIGNED_INT is only valid for glTF indices, not vertex attributes".into(),
902                ));
903            }
904        }
905        GeometryAttributeType::Invalid => {
906            return Err(GltfWriteError::Unsupported(
907                "Invalid Draco attribute type cannot be written to glTF".into(),
908            ));
909        }
910    }
911
912    Ok(())
913}
914
915fn validate_normalized_integer_or_float(att: &PointAttribute, semantic: &str) -> Result<()> {
916    match att.data_type() {
917        DataType::Float32 => Ok(()),
918        DataType::Uint8 | DataType::Uint16 => {
919            if att.normalized() {
920                Ok(())
921            } else {
922                Err(GltfWriteError::Unsupported(format!(
923                    "{} integer attributes must be normalized",
924                    semantic
925                )))
926            }
927        }
928        other => Err(GltfWriteError::Unsupported(format!(
929            "{} does not support component data type {:?}",
930            semantic, other
931        ))),
932    }
933}
934
935fn component_type_for_data_type(dt: DataType) -> Result<u32> {
936    match dt {
937        DataType::Int8 => Ok(5120),
938        DataType::Uint8 => Ok(5121),
939        DataType::Int16 => Ok(5122),
940        DataType::Uint16 => Ok(5123),
941        DataType::Uint32 => Ok(5125),
942        DataType::Float32 => Ok(5126),
943        _ => Err(GltfWriteError::Unsupported(format!(
944            "Unsupported glTF component data type: {:?}",
945            dt
946        ))),
947    }
948}
949
950#[derive(Debug, Default)]
951struct GltfSemanticCounters {
952    color: usize,
953    texcoord: usize,
954    generic: usize,
955}
956
957fn gltf_attribute_info(
958    attribute_type: GeometryAttributeType,
959    num_components: u8,
960    counters: &mut GltfSemanticCounters,
961) -> Result<(String, &'static str)> {
962    match attribute_type {
963        GeometryAttributeType::Position => Ok(("POSITION".to_string(), "VEC3")),
964        GeometryAttributeType::Normal => Ok(("NORMAL".to_string(), "VEC3")),
965        GeometryAttributeType::Color => {
966            let semantic = format!("COLOR_{}", counters.color);
967            counters.color += 1;
968            Ok((semantic, gltf_type_for_num_components(num_components)?))
969        }
970        GeometryAttributeType::TexCoord => {
971            let semantic = format!("TEXCOORD_{}", counters.texcoord);
972            counters.texcoord += 1;
973            Ok((semantic, "VEC2"))
974        }
975        GeometryAttributeType::Generic => {
976            let semantic = format!("_GENERIC_{}", counters.generic);
977            counters.generic += 1;
978            Ok((semantic, gltf_type_for_num_components(num_components)?))
979        }
980        GeometryAttributeType::Invalid => Err(GltfWriteError::Unsupported(
981            "Invalid Draco attribute type cannot be written to glTF".into(),
982        )),
983    }
984}
985
986fn gltf_type_for_num_components(num_components: u8) -> Result<&'static str> {
987    match num_components {
988        1 => Ok("SCALAR"),
989        2 => Ok("VEC2"),
990        3 => Ok("VEC3"),
991        4 => Ok("VEC4"),
992        _ => Err(GltfWriteError::Unsupported(format!(
993            "Unsupported glTF accessor component count: {}",
994            num_components
995        ))),
996    }
997}
998
999// ============================================================================
1000// Trait Implementations
1001// ============================================================================
1002
1003impl Writer for GltfWriter {
1004    fn new() -> Self {
1005        GltfWriter::new()
1006    }
1007
1008    fn add_mesh(&mut self, mesh: &Mesh, name: Option<&str>) -> io::Result<()> {
1009        // Use default quantization
1010        self.add_draco_mesh(mesh, name, None)
1011            .map(|_| ())
1012            .map_err(|e| io::Error::other(e.to_string()))
1013    }
1014
1015    fn write<P: AsRef<Path>>(&self, path: P) -> io::Result<()> {
1016        // Default to GLB format for Writer trait
1017        self.write_glb(path)
1018            .map_err(|e| io::Error::other(e.to_string()))
1019    }
1020
1021    fn vertex_count(&self) -> usize {
1022        // Count vertices from all accessors
1023        self.accessors.iter().map(|a| a.count).sum()
1024    }
1025
1026    fn face_count(&self) -> usize {
1027        // Count faces from meshes
1028        self.meshes
1029            .iter()
1030            .flat_map(|m| &m.primitives)
1031            .filter_map(|p| p.indices)
1032            .map(|idx| self.accessors.get(idx).map(|a| a.count / 3).unwrap_or(0))
1033            .sum()
1034    }
1035}
1036
1037impl WriteToBytes for GltfWriter {
1038    fn write_to_vec(&self) -> io::Result<Vec<u8>> {
1039        GltfWriter::write_to_vec(self).map_err(|e| io::Error::other(e.to_string()))
1040    }
1041
1042    fn write_to<W: Write>(&self, writer: &mut W) -> io::Result<()> {
1043        GltfWriter::write_to(self, writer).map_err(|e| io::Error::other(e.to_string()))
1044    }
1045}
1046
1047impl crate::scene::SceneWriter for GltfWriter {
1048    fn add_scene(&mut self, scene: &crate::scene::Scene) -> io::Result<()> {
1049        // Use default quantization for the trait method.
1050        self.add_scene(scene, None)
1051            .map(|_| ())
1052            .map_err(|e| io::Error::other(e.to_string()))
1053    }
1054}
1055
1056// ============================================================================
1057// Tests
1058// ============================================================================
1059
1060#[cfg(test)]
1061mod tests {
1062    use super::*;
1063    use draco_core::draco_types::DataType;
1064    use draco_core::geometry_attribute::PointAttribute;
1065    use draco_core::geometry_indices::{AttributeValueIndex, FaceIndex, PointIndex};
1066
1067    fn create_test_triangle() -> Mesh {
1068        let mut mesh = Mesh::new();
1069        let mut pos_att = PointAttribute::new();
1070
1071        pos_att.init(
1072            GeometryAttributeType::Position,
1073            3,
1074            draco_core::draco_types::DataType::Float32,
1075            false,
1076            3,
1077        );
1078
1079        let positions: [f32; 9] = [0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.5, 1.0, 0.0];
1080
1081        let buffer = pos_att.buffer_mut();
1082        for i in 0..3 {
1083            let bytes = [
1084                positions[i * 3].to_le_bytes(),
1085                positions[i * 3 + 1].to_le_bytes(),
1086                positions[i * 3 + 2].to_le_bytes(),
1087            ]
1088            .concat();
1089            buffer.write(i * 12, &bytes);
1090        }
1091
1092        mesh.add_attribute(pos_att);
1093        mesh.set_num_faces(1);
1094        mesh.set_face(FaceIndex(0), [PointIndex(0), PointIndex(1), PointIndex(2)]);
1095
1096        mesh
1097    }
1098
1099    fn add_attribute(
1100        mesh: &mut Mesh,
1101        attribute_type: GeometryAttributeType,
1102        components: u8,
1103        data_type: DataType,
1104        normalized: bool,
1105        bytes: Vec<u8>,
1106    ) {
1107        let mut attribute = PointAttribute::new();
1108        attribute.init(
1109            attribute_type,
1110            components,
1111            data_type,
1112            normalized,
1113            bytes.len() / (components as usize * data_type.byte_length()),
1114        );
1115        attribute.buffer_mut().write(0, &bytes);
1116        mesh.add_attribute(attribute);
1117    }
1118
1119    fn repeated_zero_attribute_bytes(data_type: DataType, components: u8, count: usize) -> Vec<u8> {
1120        vec![0; data_type.byte_length() * components as usize * count]
1121    }
1122
1123    fn write_f32s(attribute: &mut PointAttribute, values: &[f32]) {
1124        for (i, value) in values.iter().enumerate() {
1125            attribute
1126                .buffer_mut()
1127                .write(i * DataType::Float32.byte_length(), &value.to_le_bytes());
1128        }
1129    }
1130
1131    fn create_test_uv_seam_mesh() -> Mesh {
1132        let mut mesh = Mesh::new();
1133        mesh.set_num_points(6);
1134
1135        let mut positions = PointAttribute::new();
1136        positions.init(
1137            GeometryAttributeType::Position,
1138            3,
1139            DataType::Float32,
1140            false,
1141            4,
1142        );
1143        write_f32s(
1144            &mut positions,
1145            &[0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 1.0, 1.0, 0.0],
1146        );
1147        positions.set_explicit_mapping(6);
1148        for (point, entry) in [0, 1, 2, 1, 3, 2].iter().copied().enumerate() {
1149            positions.set_point_map_entry(PointIndex(point as u32), AttributeValueIndex(entry));
1150        }
1151        mesh.add_attribute(positions);
1152
1153        add_attribute(
1154            &mut mesh,
1155            GeometryAttributeType::TexCoord,
1156            2,
1157            DataType::Float32,
1158            false,
1159            [
1160                0.0f32, 0.0, 1.0, 0.0, 0.0, 1.0, 0.2, 0.0, 1.0, 1.0, 0.2, 1.0,
1161            ]
1162            .into_iter()
1163            .flat_map(f32::to_le_bytes)
1164            .collect(),
1165        );
1166
1167        mesh.add_face([PointIndex(0), PointIndex(1), PointIndex(2)]);
1168        mesh.add_face([PointIndex(3), PointIndex(4), PointIndex(5)]);
1169        mesh
1170    }
1171
1172    #[cfg(feature = "gltf-reader")]
1173    fn make_translation_transform(x: f32, y: f32, z: f32) -> crate::scene::Transform {
1174        crate::scene::Transform {
1175            matrix: [
1176                [1.0, 0.0, 0.0, x],
1177                [0.0, 1.0, 0.0, y],
1178                [0.0, 0.0, 1.0, z],
1179                [0.0, 0.0, 0.0, 1.0],
1180            ],
1181        }
1182    }
1183
1184    #[test]
1185    fn test_create_glb() {
1186        let mesh = create_test_triangle();
1187        let mut writer = GltfWriter::new();
1188
1189        // Use custom quantization (still works with explicit values)
1190        let idx = writer
1191            .add_draco_mesh(
1192                &mesh,
1193                Some("Triangle"),
1194                GltfCompressionOptions {
1195                    quantization: crate::QuantizationOptions {
1196                        position: Some(10),
1197                        normal: Some(10),
1198                        color: Some(8),
1199                        texcoord: Some(8),
1200                        generic: Some(8),
1201                    },
1202                    ..Default::default()
1203                },
1204            )
1205            .unwrap();
1206        assert_eq!(idx, 0);
1207
1208        let glb = writer.to_glb().unwrap();
1209
1210        // Check GLB header
1211        assert_eq!(&glb[0..4], b"glTF");
1212        assert!(glb.len() > 12);
1213    }
1214
1215    #[cfg(feature = "gltf-reader")]
1216    #[test]
1217    fn test_roundtrip() {
1218        use crate::gltf_reader::GltfReader;
1219
1220        let mesh = create_test_triangle();
1221        let mut writer = GltfWriter::new();
1222        // Use default quantization with None
1223        writer
1224            .add_draco_mesh(&mesh, Some("Triangle"), None)
1225            .unwrap();
1226
1227        let glb = writer.to_glb().unwrap();
1228
1229        // Read back
1230        let reader = GltfReader::from_glb(&glb).unwrap();
1231        assert!(reader.has_draco_extension());
1232        assert_eq!(reader.num_meshes(), 1);
1233
1234        let primitives = reader.draco_primitives();
1235        assert_eq!(primitives.len(), 1);
1236
1237        let decoded = reader.decode_draco_mesh(&primitives[0]).unwrap();
1238        assert_eq!(decoded.num_faces(), 1);
1239        assert_eq!(decoded.num_points(), 3);
1240    }
1241
1242    #[test]
1243    fn test_gltf_writer_uses_default_mesh_encoding_method_selection() {
1244        let encoded = encode_draco_mesh(&create_test_triangle(), None).unwrap();
1245
1246        assert!(encoded.len() > 8, "encoded Draco buffer is too small");
1247        assert_eq!(
1248            encoded[8], 1,
1249            "default mesh encoding method should match C++ ExpertEncoder selection"
1250        );
1251    }
1252
1253    #[test]
1254    fn compression_method_and_ranges_are_honored() {
1255        let mesh = create_test_triangle();
1256        let sequential = GltfCompressionOptions {
1257            encoding_method: EncodingMethod::Sequential,
1258            encoding_speed: 0,
1259            decoding_speed: 10,
1260            ..Default::default()
1261        };
1262        let encoded = encode_draco_mesh(&mesh, sequential).unwrap();
1263        assert_eq!(encoded[8], 0, "sequential method must reach the encoder");
1264
1265        let edgebreaker = GltfCompressionOptions {
1266            encoding_method: EncodingMethod::Edgebreaker,
1267            ..Default::default()
1268        };
1269        let encoded = encode_draco_mesh(&mesh, edgebreaker).unwrap();
1270        assert_eq!(encoded[8], 1, "EdgeBreaker method must reach the encoder");
1271
1272        let invalid = GltfCompressionOptions {
1273            encoding_speed: 11,
1274            ..Default::default()
1275        };
1276        assert!(matches!(
1277            encode_draco_mesh(&mesh, invalid),
1278            Err(GltfWriteError::InvalidOptions(_))
1279        ));
1280    }
1281
1282    #[cfg(feature = "gltf-reader")]
1283    #[test]
1284    fn test_scene_graph_roundtrip() {
1285        use crate::gltf_reader::GltfReader;
1286        use crate::scene::{MeshInstance, Scene, SceneNode, SceneReader};
1287
1288        let mesh = create_test_triangle();
1289
1290        // Build a small hierarchy: Root -> Child
1291        let mut root = SceneNode::new(Some("Root".to_string()));
1292        root.transform = Some(make_translation_transform(1.0, 2.0, 3.0));
1293
1294        let mut child = SceneNode::new(Some("Child".to_string()));
1295        child.transform = Some(make_translation_transform(4.0, 5.0, 6.0));
1296        child.mesh_instances.push(MeshInstance {
1297            name: Some("Triangle".to_string()),
1298            mesh: mesh.clone(),
1299            transform: None,
1300        });
1301        root.children.push(child);
1302
1303        let scene = Scene {
1304            name: Some("TestScene".to_string()),
1305            root_nodes: vec![root],
1306        };
1307
1308        let mut writer = GltfWriter::new();
1309        writer.add_scene(&scene, None).unwrap();
1310
1311        let glb = writer.to_glb().unwrap();
1312        let mut reader = GltfReader::from_glb(&glb).unwrap();
1313
1314        let out_scene = reader.read_scene().unwrap();
1315        assert_eq!(out_scene.name, Some("TestScene".to_string()));
1316        assert_eq!(out_scene.root_nodes.len(), 1);
1317        assert_eq!(out_scene.root_nodes[0].name, Some("Root".to_string()));
1318        assert_eq!(out_scene.root_nodes[0].children.len(), 1);
1319        assert_eq!(
1320            out_scene.root_nodes[0].children[0].name,
1321            Some("Child".to_string())
1322        );
1323        assert_eq!(out_scene.root_nodes[0].children[0].mesh_instances.len(), 1);
1324
1325        // Verify transforms survived matrix column/row conversion.
1326        let root_m = out_scene.root_nodes[0].transform.as_ref().unwrap().matrix;
1327        assert_eq!(root_m[0][3], 1.0);
1328        assert_eq!(root_m[1][3], 2.0);
1329        assert_eq!(root_m[2][3], 3.0);
1330
1331        let child_m = out_scene.root_nodes[0].children[0]
1332            .transform
1333            .as_ref()
1334            .unwrap()
1335            .matrix;
1336        assert_eq!(child_m[0][3], 4.0);
1337        assert_eq!(child_m[1][3], 5.0);
1338        assert_eq!(child_m[2][3], 6.0);
1339    }
1340
1341    #[cfg(feature = "gltf-reader")]
1342    #[test]
1343    fn test_flat_scene_mesh_instances_roundtrip() {
1344        use crate::gltf_reader::GltfReader;
1345        use crate::scene::{MeshInstance, Scene, SceneReader};
1346
1347        let mesh = create_test_triangle();
1348        let scene = Scene::from_mesh_instances(
1349            Some("FlatScene".to_string()),
1350            vec![
1351                MeshInstance {
1352                    name: Some("FlatA".to_string()),
1353                    mesh: mesh.clone(),
1354                    transform: Some(make_translation_transform(1.0, 2.0, 3.0)),
1355                },
1356                MeshInstance {
1357                    name: Some("FlatB".to_string()),
1358                    mesh,
1359                    transform: Some(make_translation_transform(4.0, 5.0, 6.0)),
1360                },
1361            ],
1362        );
1363
1364        let mut writer = GltfWriter::new();
1365        writer.add_scene(&scene, None).unwrap();
1366
1367        let glb = writer.to_glb().unwrap();
1368        let mut reader = GltfReader::from_glb(&glb).unwrap();
1369        let out_scene = reader.read_scene().unwrap();
1370
1371        assert_eq!(out_scene.name, Some("FlatScene".to_string()));
1372        assert_eq!(out_scene.root_nodes.len(), 1);
1373        assert_eq!(out_scene.root_nodes[0].name, Some("FlatScene".to_string()));
1374        assert_eq!(out_scene.root_nodes[0].children.len(), 2);
1375        assert_eq!(
1376            out_scene.root_nodes[0].children[0].mesh_instances[0].name,
1377            Some("FlatA".to_string())
1378        );
1379        assert_eq!(
1380            out_scene.root_nodes[0].children[1].mesh_instances[0].name,
1381            Some("FlatB".to_string())
1382        );
1383
1384        let first_m = out_scene.root_nodes[0].children[0]
1385            .transform
1386            .as_ref()
1387            .unwrap()
1388            .matrix;
1389        assert_eq!(first_m[0][3], 1.0);
1390        assert_eq!(first_m[1][3], 2.0);
1391        assert_eq!(first_m[2][3], 3.0);
1392
1393        let second_m = out_scene.root_nodes[0].children[1]
1394            .transform
1395            .as_ref()
1396            .unwrap()
1397            .matrix;
1398        assert_eq!(second_m[0][3], 4.0);
1399        assert_eq!(second_m[1][3], 5.0);
1400        assert_eq!(second_m[2][3], 6.0);
1401    }
1402
1403    #[cfg(feature = "gltf-reader")]
1404    #[test]
1405    fn test_scene_writer_trait_exports_flat_scene_mesh_instances() {
1406        use crate::gltf_reader::GltfReader;
1407        use crate::scene::{MeshInstance, Scene, SceneReader, SceneWriter};
1408
1409        let scene = Scene::from_mesh_instances(
1410            Some("TraitScene".to_string()),
1411            vec![MeshInstance {
1412                name: Some("TraitMeshInstance".to_string()),
1413                mesh: create_test_triangle(),
1414                transform: None,
1415            }],
1416        );
1417
1418        let mut writer = GltfWriter::new();
1419        SceneWriter::add_scene(&mut writer, &scene).unwrap();
1420
1421        let glb = writer.to_glb().unwrap();
1422        let mut reader = GltfReader::from_glb(&glb).unwrap();
1423        let out_scene = reader.read_scene().unwrap();
1424
1425        assert_eq!(out_scene.name, Some("TraitScene".to_string()));
1426        assert_eq!(out_scene.root_nodes.len(), 1);
1427        assert_eq!(out_scene.root_nodes[0].mesh_instances.len(), 1);
1428        assert_eq!(
1429            out_scene.root_nodes[0].mesh_instances[0].name,
1430            Some("TraitMeshInstance".to_string())
1431        );
1432    }
1433
1434    #[cfg(feature = "gltf-reader")]
1435    #[test]
1436    fn test_embedded_gltf() {
1437        use crate::gltf_reader::GltfReader;
1438
1439        let mesh = create_test_triangle();
1440        let mut writer = GltfWriter::new();
1441        // Use default quantization with None
1442        writer
1443            .add_draco_mesh(&mesh, Some("Triangle"), None)
1444            .unwrap();
1445
1446        // Generate embedded glTF JSON
1447        let json = writer.to_gltf_embedded().unwrap();
1448
1449        // Verify it contains data URI
1450        assert!(json.contains("data:application/octet-stream;base64,"));
1451        assert!(json.contains("KHR_draco_mesh_compression"));
1452
1453        // Read back
1454        let reader = GltfReader::from_gltf(json.as_bytes(), None).unwrap();
1455        assert!(reader.has_draco_extension());
1456        assert_eq!(reader.num_meshes(), 1);
1457
1458        let primitives = reader.draco_primitives();
1459        assert_eq!(primitives.len(), 1);
1460
1461        let decoded = reader.decode_draco_mesh(&primitives[0]).unwrap();
1462        assert_eq!(decoded.num_faces(), 1);
1463        assert_eq!(decoded.num_points(), 3);
1464    }
1465
1466    #[test]
1467    fn test_base64_encoding() {
1468        // Test base64 encoding
1469        let data = b"Hello";
1470        let encoded = crate::encode_data_uri("application/octet-stream", data).unwrap();
1471        assert!(encoded.starts_with("data:application/octet-stream;base64,"));
1472        assert!(encoded.contains("SGVsbG8="));
1473
1474        let data = b"Hello World";
1475        let encoded = crate::encode_data_uri("application/octet-stream", data).unwrap();
1476        assert!(encoded.contains("SGVsbG8gV29ybGQ="));
1477    }
1478
1479    #[test]
1480    fn test_writer_emits_position_bounds_and_normalized_metadata() {
1481        let mut mesh = create_test_triangle();
1482        add_attribute(
1483            &mut mesh,
1484            GeometryAttributeType::Color,
1485            4,
1486            DataType::Uint8,
1487            true,
1488            vec![255, 0, 0, 255, 0, 255, 0, 255, 0, 0, 255, 255],
1489        );
1490        add_attribute(
1491            &mut mesh,
1492            GeometryAttributeType::TexCoord,
1493            2,
1494            DataType::Uint16,
1495            true,
1496            [0u16, 0, 65535, 0, 0, 65535]
1497                .into_iter()
1498                .flat_map(u16::to_le_bytes)
1499                .collect(),
1500        );
1501        mesh.attribute_mut(0).set_unique_id(10);
1502        mesh.attribute_mut(1).set_unique_id(20);
1503        mesh.attribute_mut(2).set_unique_id(30);
1504
1505        let mut writer = GltfWriter::new();
1506        writer
1507            .add_draco_mesh(&mesh, Some("Triangle"), None)
1508            .unwrap();
1509        let json_text = writer.to_gltf_embedded().unwrap();
1510        let json: serde_json::Value =
1511            serde_json::from_str(&json_text).expect("writer JSON should parse");
1512
1513        let primitive = &json["meshes"][0]["primitives"][0];
1514        let position_accessor = primitive["attributes"]["POSITION"].as_u64().unwrap() as usize;
1515        let color_accessor = primitive["attributes"]["COLOR_0"].as_u64().unwrap() as usize;
1516        let texcoord_accessor = primitive["attributes"]["TEXCOORD_0"].as_u64().unwrap() as usize;
1517        let draco_attributes = &primitive["extensions"]["KHR_draco_mesh_compression"]["attributes"];
1518
1519        assert_eq!(json["accessors"][position_accessor]["count"], 3);
1520        assert!(json["accessors"][position_accessor]
1521            .get("bufferView")
1522            .is_none());
1523        assert!(json["accessors"][position_accessor]
1524            .get("byteOffset")
1525            .is_none());
1526        assert!(json["asset"]["generator"].is_string());
1527        assert_eq!(json["accessors"][color_accessor]["count"], 3);
1528        assert_eq!(json["accessors"][texcoord_accessor]["count"], 3);
1529        assert_eq!(draco_attributes["POSITION"], 10);
1530        assert_eq!(draco_attributes["COLOR_0"], 20);
1531        assert_eq!(draco_attributes["TEXCOORD_0"], 30);
1532        assert_eq!(
1533            json["accessors"][position_accessor]["min"]
1534                .as_array()
1535                .unwrap()
1536                .len(),
1537            3
1538        );
1539        assert_eq!(
1540            json["accessors"][position_accessor]["max"]
1541                .as_array()
1542                .unwrap()
1543                .len(),
1544            3
1545        );
1546        assert_eq!(json["accessors"][color_accessor]["normalized"], true);
1547        assert_eq!(json["accessors"][texcoord_accessor]["normalized"], true);
1548
1549        #[cfg(feature = "gltf-reader")]
1550        {
1551            let reader = crate::GltfReader::from_gltf(json_text.as_bytes(), None).unwrap();
1552            let info = reader.draco_primitives().remove(0);
1553            let decoded = reader.decode_draco_mesh(&info).unwrap();
1554            assert!(decoded.attribute_by_unique_id(10).is_some());
1555            assert!(decoded.attribute_by_unique_id(20).is_some());
1556            assert!(decoded.attribute_by_unique_id(30).is_some());
1557
1558            let mut invalid_json = json.clone();
1559            invalid_json["meshes"][0]["primitives"][0]["extensions"]
1560                ["KHR_draco_mesh_compression"]["attributes"]["POSITION"] =
1561                serde_json::Value::from(99);
1562            let invalid = serde_json::to_vec(&invalid_json).unwrap();
1563            let reader = crate::GltfReader::from_gltf(&invalid, None).unwrap();
1564            let info = reader.draco_primitives().remove(0);
1565            assert!(reader.decode_draco_mesh(&info).is_err());
1566        }
1567    }
1568
1569    #[test]
1570    fn test_writer_uses_encoded_point_count_for_split_connectivity_accessors() {
1571        let mesh = create_test_uv_seam_mesh();
1572        let mut writer = GltfWriter::new();
1573        writer.add_draco_mesh(&mesh, Some("Seam"), None).unwrap();
1574        let json: serde_json::Value = serde_json::from_str(&writer.to_gltf_embedded().unwrap())
1575            .expect("writer JSON should parse");
1576
1577        let primitive = &json["meshes"][0]["primitives"][0];
1578        let position_accessor = primitive["attributes"]["POSITION"].as_u64().unwrap() as usize;
1579        let texcoord_accessor = primitive["attributes"]["TEXCOORD_0"].as_u64().unwrap() as usize;
1580
1581        assert_eq!(json["accessors"][position_accessor]["count"], 6);
1582        assert_eq!(json["accessors"][texcoord_accessor]["count"], 6);
1583    }
1584
1585    #[test]
1586    fn test_writer_rejects_missing_position() {
1587        let mut mesh = Mesh::new();
1588        mesh.set_num_points(3);
1589        mesh.set_num_faces(1);
1590        mesh.set_face(FaceIndex(0), [PointIndex(0), PointIndex(1), PointIndex(2)]);
1591        add_attribute(
1592            &mut mesh,
1593            GeometryAttributeType::Normal,
1594            3,
1595            DataType::Float32,
1596            false,
1597            repeated_zero_attribute_bytes(DataType::Float32, 3, 3),
1598        );
1599
1600        let err = GltfWriter::new()
1601            .add_draco_mesh(&mesh, Some("invalid"), None)
1602            .unwrap_err();
1603        assert!(matches!(err, GltfWriteError::InvalidMesh(_)));
1604    }
1605
1606    #[test]
1607    fn test_writer_rejects_unsupported_attribute_data_types() {
1608        for data_type in [DataType::Float64, DataType::Int64, DataType::Uint32] {
1609            let mut mesh = create_test_triangle();
1610            add_attribute(
1611                &mut mesh,
1612                GeometryAttributeType::Generic,
1613                1,
1614                data_type,
1615                false,
1616                repeated_zero_attribute_bytes(data_type, 1, 3),
1617            );
1618
1619            let err = GltfWriter::new()
1620                .add_draco_mesh(&mesh, Some("invalid"), None)
1621                .unwrap_err();
1622            assert!(matches!(err, GltfWriteError::Unsupported(_)));
1623        }
1624    }
1625
1626    #[test]
1627    fn test_writer_rejects_attributes_it_would_previously_skip() {
1628        let mut mesh = create_test_triangle();
1629        add_attribute(
1630            &mut mesh,
1631            GeometryAttributeType::Color,
1632            2,
1633            DataType::Uint8,
1634            true,
1635            vec![255, 0, 0, 255, 0, 0],
1636        );
1637
1638        let err = GltfWriter::new()
1639            .add_draco_mesh(&mesh, Some("invalid"), None)
1640            .unwrap_err();
1641        assert!(matches!(err, GltfWriteError::Unsupported(_)));
1642    }
1643
1644    #[test]
1645    fn test_empty_glb_omits_bin_chunk() {
1646        let glb = GltfWriter::new().to_glb().unwrap();
1647        assert_eq!(&glb[0..4], b"glTF");
1648        assert_eq!(read_u32_le_for_test(&glb[12..16]) as usize + 20, glb.len());
1649        assert!(!glb.windows(4).any(|window| window == b"BIN\0"));
1650    }
1651
1652    fn read_u32_le_for_test(data: &[u8]) -> u32 {
1653        u32::from_le_bytes([data[0], data[1], data[2], data[3]])
1654    }
1655}