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