1#[derive(Clone, Copy, Debug)]
50pub struct QuantizationBits {
51 pub position: i32,
53 pub normal: i32,
55 pub color: i32,
57 pub texcoord: i32,
59 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#[derive(Error, Debug)]
93pub enum GltfWriteError {
94 #[error("IO error: {0}")]
96 Io(#[from] io::Error),
97
98 #[error("JSON serialize error: {0}")]
100 Json(#[from] serde_json::Error),
101
102 #[error("Draco encode error: {0}")]
104 DracoEncode(String),
105
106 #[error("Invalid mesh: {0}")]
108 InvalidMesh(String),
109
110 #[error("Unsupported feature: {0}")]
112 Unsupported(String),
113}
114
115pub type Result<T> = std::result::Result<T, GltfWriteError>;
117
118#[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 #[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
242const GLB_MAGIC: u32 = 0x46546C67; const GLB_VERSION: u32 = 2;
248const GLB_CHUNK_JSON: u32 = 0x4E4F534A; const GLB_CHUNK_BIN: u32 = 0x004E4942; pub 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 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
318pub 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 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 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 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 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 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 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 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), 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, 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 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 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 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 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 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 fs::write(bin_path, &self.binary_data)?;
637
638 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 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 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 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 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 pub fn write_to_vec(&self) -> Result<Vec<u8>> {
686 self.to_glb()
687 }
688
689 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
1028impl 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 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 self.write_glb(path)
1047 .map_err(|e| io::Error::other(e.to_string()))
1048 }
1049
1050 fn vertex_count(&self) -> usize {
1051 self.accessors.iter().map(|a| a.count).sum()
1053 }
1054
1055 fn face_count(&self) -> usize {
1056 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 self.add_scene(scene, None)
1080 .map(|_| ())
1081 .map_err(|e| io::Error::other(e.to_string()))
1082 }
1083}
1084
1085#[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 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 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 writer
1250 .add_draco_mesh(&mesh, Some("Triangle"), None)
1251 .unwrap();
1252
1253 let glb = writer.to_glb().unwrap();
1254
1255 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 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 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 writer
1440 .add_draco_mesh(&mesh, Some("Triangle"), None)
1441 .unwrap();
1442
1443 let json = writer.to_gltf_embedded().unwrap();
1445
1446 assert!(json.contains("data:application/octet-stream;base64,"));
1448 assert!(json.contains("KHR_draco_mesh_compression"));
1449
1450 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 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}