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