1use std::fs::File;
32use std::io::{self, BufWriter, Cursor, Seek, SeekFrom, Write};
33use std::path::Path;
34
35use draco_core::geometry_attribute::GeometryAttributeType;
36use draco_core::geometry_indices::FaceIndex;
37use draco_core::mesh::Mesh;
38
39use crate::traits::{WriteToBytes, Writer};
40
41const FBX_MAGIC: &[u8; 21] = b"Kaydara FBX Binary \0";
43
44const FBX_VERSION: u32 = 7500;
46
47const NULL_RECORD_SIZE_64: usize = 25;
49
50const NULL_RECORD_SIZE_32: usize = 13;
52
53#[derive(Debug, Clone)]
71pub struct FbxWriter {
72 compress: bool,
74 compression_threshold: usize,
76 meshes: Vec<MeshData>,
78 next_id: i64,
80}
81
82#[derive(Debug, Clone)]
84struct MeshData {
85 vertices: Vec<f64>,
86 indices: Vec<i32>,
87 name: String,
88 geometry_id: i64,
89 model_id: i64,
90}
91
92impl Default for FbxWriter {
93 fn default() -> Self {
94 Self::new()
95 }
96}
97
98impl FbxWriter {
99 pub fn new() -> Self {
101 Self {
102 compress: false,
103 compression_threshold: 128,
104 meshes: Vec::new(),
105 next_id: 1000, }
107 }
108
109 pub fn with_compression(mut self, compress: bool) -> Self {
114 self.compress = compress;
115 self
116 }
117
118 pub fn with_compression_threshold(mut self, threshold: usize) -> Self {
123 self.compression_threshold = threshold;
124 self
125 }
126
127 fn allocate_id(&mut self) -> i64 {
129 let id = self.next_id;
130 self.next_id += 1;
131 id
132 }
133
134 pub fn write<P: AsRef<Path>>(&self, path: P) -> io::Result<()> {
137 let file = File::create(path)?;
138 let mut writer = BufWriter::new(file);
139 self.write_to(&mut writer)
140 }
141
142 pub fn write_to<W: Write + Seek>(&self, writer: &mut W) -> io::Result<()> {
144 let options = WriterOptions {
145 compress: self.compress,
146 compression_threshold: self.compression_threshold,
147 };
148
149 writer.write_all(FBX_MAGIC)?;
151 writer.write_all(&[0x1A, 0x00])?; writer.write_all(&FBX_VERSION.to_le_bytes())?;
153
154 let is_64 = FBX_VERSION >= 7500;
155
156 write_header_extension(writer, is_64)?;
158 write_global_settings(writer, is_64)?;
159 write_documents(writer, is_64)?;
160 write_definitions(writer, is_64, &self.meshes)?;
161 write_objects(writer, &self.meshes, is_64, &options)?;
162 write_connections(writer, &self.meshes, is_64)?;
163
164 write_null_record(writer, is_64)?;
166
167 write_footer(writer)?;
169
170 Ok(())
171 }
172
173 pub fn write_to_vec(&self) -> io::Result<Vec<u8>> {
175 let mut cursor = Cursor::new(Vec::new());
176 self.write_to(&mut cursor)?;
177 Ok(cursor.into_inner())
178 }
179
180 pub fn mesh_count(&self) -> usize {
182 self.meshes.len()
183 }
184
185 pub fn is_compression_enabled(&self) -> bool {
187 self.compress
188 }
189}
190
191struct WriterOptions {
193 compress: bool,
194 compression_threshold: usize,
195}
196
197impl Writer for FbxWriter {
202 fn new() -> Self {
203 Self::default()
204 }
205
206 fn add_mesh(&mut self, mesh: &Mesh, name: Option<&str>) -> io::Result<()> {
207 validate_supported_fbx_attributes(mesh)?;
208
209 let geometry_id = self.allocate_id();
210 let model_id = self.allocate_id();
211 let name = name.unwrap_or("Mesh").to_string();
212
213 let vertices = extract_vertices(mesh);
215
216 let indices = extract_polygon_indices(mesh);
218
219 self.meshes.push(MeshData {
220 vertices,
221 indices,
222 name,
223 geometry_id,
224 model_id,
225 });
226 Ok(())
227 }
228
229 fn write<P: AsRef<Path>>(&self, path: P) -> io::Result<()> {
230 self.write(path)
231 }
232
233 fn vertex_count(&self) -> usize {
234 self.meshes.iter().map(|m| m.vertices.len() / 3).sum()
235 }
236
237 fn face_count(&self) -> usize {
238 self.meshes.iter().map(|m| m.indices.len() / 3).sum()
239 }
240}
241
242impl WriteToBytes for FbxWriter {
243 fn write_to_vec(&self) -> io::Result<Vec<u8>> {
244 FbxWriter::write_to_vec(self)
245 }
246}
247
248pub fn write_fbx_mesh<P: AsRef<Path>>(path: P, mesh: &Mesh) -> io::Result<()> {
256 let mut writer = FbxWriter::new();
257 Writer::add_mesh(&mut writer, mesh, None)?;
258 writer.write(path)
259}
260
261#[cfg(feature = "compression")]
265pub fn write_fbx_mesh_compressed<P: AsRef<Path>>(path: P, mesh: &Mesh) -> io::Result<()> {
266 let mut writer = FbxWriter::new().with_compression(true);
267 Writer::add_mesh(&mut writer, mesh, None)?;
268 writer.write(path)
269}
270
271struct NodeWriter<'a, W: Write + Seek> {
277 writer: &'a mut W,
278 start_pos: u64,
279 properties_start: u64,
280 num_properties: u64,
281 is_64: bool,
282}
283
284impl<'a, W: Write + Seek> NodeWriter<'a, W> {
285 fn start(writer: &'a mut W, name: &str, is_64: bool) -> io::Result<Self> {
286 let start_pos = writer.stream_position()?;
287
288 let header_size = if is_64 { 24 } else { 12 }; writer.write_all(&vec![0u8; header_size])?;
291
292 writer.write_all(&[name.len() as u8])?;
294 writer.write_all(name.as_bytes())?;
295
296 let properties_start = writer.stream_position()?;
297
298 Ok(Self {
299 writer,
300 start_pos,
301 properties_start,
302 num_properties: 0,
303 is_64,
304 })
305 }
306
307 fn write_property_i16(&mut self, value: i16) -> io::Result<()> {
308 self.writer.write_all(b"Y")?;
309 self.writer.write_all(&value.to_le_bytes())?;
310 self.num_properties += 1;
311 Ok(())
312 }
313
314 fn write_property_i32(&mut self, value: i32) -> io::Result<()> {
315 self.writer.write_all(b"I")?;
316 self.writer.write_all(&value.to_le_bytes())?;
317 self.num_properties += 1;
318 Ok(())
319 }
320
321 fn write_property_i64(&mut self, value: i64) -> io::Result<()> {
322 self.writer.write_all(b"L")?;
323 self.writer.write_all(&value.to_le_bytes())?;
324 self.num_properties += 1;
325 Ok(())
326 }
327
328 fn write_property_f64(&mut self, value: f64) -> io::Result<()> {
329 self.writer.write_all(b"D")?;
330 self.writer.write_all(&value.to_le_bytes())?;
331 self.num_properties += 1;
332 Ok(())
333 }
334
335 fn write_property_string(&mut self, value: &str) -> io::Result<()> {
336 self.writer.write_all(b"S")?;
337 self.writer.write_all(&(value.len() as u32).to_le_bytes())?;
338 self.writer.write_all(value.as_bytes())?;
339 self.num_properties += 1;
340 Ok(())
341 }
342
343 fn write_property_f64_array(
344 &mut self,
345 values: &[f64],
346 options: &WriterOptions,
347 ) -> io::Result<()> {
348 self.write_array_property(b'd', values, options, |v| v.to_le_bytes().to_vec())
349 }
350
351 fn write_property_i32_array(
352 &mut self,
353 values: &[i32],
354 options: &WriterOptions,
355 ) -> io::Result<()> {
356 self.write_array_property(b'i', values, options, |v| v.to_le_bytes().to_vec())
357 }
358
359 fn write_array_property<T, F>(
360 &mut self,
361 type_code: u8,
362 values: &[T],
363 options: &WriterOptions,
364 to_bytes: F,
365 ) -> io::Result<()>
366 where
367 F: Fn(&T) -> Vec<u8>,
368 {
369 self.writer.write_all(&[type_code])?;
370 self.writer
371 .write_all(&(values.len() as u32).to_le_bytes())?;
372
373 let raw_data: Vec<u8> = values.iter().flat_map(&to_bytes).collect();
375 let raw_size = raw_data.len();
376
377 let should_compress = options.compress && raw_size >= options.compression_threshold;
379
380 #[cfg(feature = "compression")]
381 if should_compress {
382 use miniz_oxide::deflate::compress_to_vec_zlib;
383 let compressed = compress_to_vec_zlib(&raw_data, 6); if compressed.len() < raw_size {
387 self.writer.write_all(&1u32.to_le_bytes())?; self.writer
389 .write_all(&(compressed.len() as u32).to_le_bytes())?;
390 self.writer.write_all(&compressed)?;
391 self.num_properties += 1;
392 return Ok(());
393 }
394 }
395
396 #[cfg(not(feature = "compression"))]
398 let _ = should_compress; self.writer.write_all(&0u32.to_le_bytes())?; self.writer.write_all(&(raw_size as u32).to_le_bytes())?;
402 self.writer.write_all(&raw_data)?;
403 self.num_properties += 1;
404 Ok(())
405 }
406
407 fn finish(self) -> io::Result<()> {
408 write_null_record(self.writer, self.is_64)?;
410 self.finalize_header()
411 }
412
413 fn finish_with_children<F>(self, write_children: F) -> io::Result<()>
414 where
415 F: FnOnce(&mut W) -> io::Result<()>,
416 {
417 let properties_end = self.writer.stream_position()?;
418 let property_list_len = properties_end - self.properties_start;
419
420 write_children(self.writer)?;
422
423 write_null_record(self.writer, self.is_64)?;
425
426 let end_pos = self.writer.stream_position()?;
427
428 self.writer.seek(SeekFrom::Start(self.start_pos))?;
430 if self.is_64 {
431 self.writer.write_all(&end_pos.to_le_bytes())?;
432 self.writer.write_all(&self.num_properties.to_le_bytes())?;
433 self.writer.write_all(&property_list_len.to_le_bytes())?;
434 } else {
435 self.writer.write_all(&(end_pos as u32).to_le_bytes())?;
436 self.writer
437 .write_all(&(self.num_properties as u32).to_le_bytes())?;
438 self.writer
439 .write_all(&(property_list_len as u32).to_le_bytes())?;
440 }
441
442 self.writer.seek(SeekFrom::Start(end_pos))?;
444 Ok(())
445 }
446
447 fn finalize_header(self) -> io::Result<()> {
448 let end_pos = self.writer.stream_position()?;
449 let null_size = if self.is_64 {
450 NULL_RECORD_SIZE_64
451 } else {
452 NULL_RECORD_SIZE_32
453 };
454 let property_list_len = if self.num_properties > 0 {
455 end_pos - self.properties_start - null_size as u64
456 } else {
457 0u64
458 };
459
460 self.writer.seek(SeekFrom::Start(self.start_pos))?;
462 if self.is_64 {
463 self.writer.write_all(&end_pos.to_le_bytes())?;
464 self.writer.write_all(&self.num_properties.to_le_bytes())?;
465 self.writer.write_all(&property_list_len.to_le_bytes())?;
466 } else {
467 self.writer.write_all(&(end_pos as u32).to_le_bytes())?;
468 self.writer
469 .write_all(&(self.num_properties as u32).to_le_bytes())?;
470 self.writer
471 .write_all(&(property_list_len as u32).to_le_bytes())?;
472 }
473
474 self.writer.seek(SeekFrom::Start(end_pos))?;
476 Ok(())
477 }
478}
479
480fn write_null_record<W: Write>(writer: &mut W, is_64: bool) -> io::Result<()> {
481 let size = if is_64 {
482 NULL_RECORD_SIZE_64
483 } else {
484 NULL_RECORD_SIZE_32
485 };
486 writer.write_all(&vec![0u8; size])
487}
488
489fn write_header_extension<W: Write + Seek>(writer: &mut W, is_64: bool) -> io::Result<()> {
494 let node = NodeWriter::start(writer, "FBXHeaderExtension", is_64)?;
495 node.finish_with_children(|w| {
496 let mut ver = NodeWriter::start(w, "FBXHeaderVersion", is_64)?;
498 ver.write_property_i32(1003)?;
499 ver.finish()?;
500
501 let mut ver = NodeWriter::start(w, "FBXVersion", is_64)?;
503 ver.write_property_i32(FBX_VERSION as i32)?;
504 ver.finish()?;
505
506 let mut creator = NodeWriter::start(w, "Creator", is_64)?;
508 creator.write_property_string("draco-io-rs")?;
509 creator.finish()?;
510
511 Ok(())
512 })
513}
514
515fn write_global_settings<W: Write + Seek>(writer: &mut W, is_64: bool) -> io::Result<()> {
516 let node = NodeWriter::start(writer, "GlobalSettings", is_64)?;
517 node.finish_with_children(|w| {
518 let mut ver = NodeWriter::start(w, "Version", is_64)?;
520 ver.write_property_i32(1000)?;
521 ver.finish()?;
522
523 let props = NodeWriter::start(w, "Properties70", is_64)?;
525 props.finish_with_children(|pw| {
526 write_property_node(pw, is_64, "UpAxis", "int", "Integer", "", 1i32)?;
527 write_property_node(pw, is_64, "UpAxisSign", "int", "Integer", "", 1i32)?;
528 write_property_node(pw, is_64, "FrontAxis", "int", "Integer", "", 2i32)?;
529 write_property_node(pw, is_64, "FrontAxisSign", "int", "Integer", "", 1i32)?;
530 write_property_node(pw, is_64, "CoordAxis", "int", "Integer", "", 0i32)?;
531 write_property_node(pw, is_64, "CoordAxisSign", "int", "Integer", "", 1i32)?;
532 write_property_node_f64(pw, is_64, "UnitScaleFactor", "double", "Number", "", 1.0)?;
533 Ok(())
534 })
535 })
536}
537
538fn write_property_node<W: Write + Seek>(
539 writer: &mut W,
540 is_64: bool,
541 name: &str,
542 type1: &str,
543 type2: &str,
544 flags: &str,
545 value: i32,
546) -> io::Result<()> {
547 let mut p = NodeWriter::start(writer, "P", is_64)?;
548 p.write_property_string(name)?;
549 p.write_property_string(type1)?;
550 p.write_property_string(type2)?;
551 p.write_property_string(flags)?;
552 p.write_property_i32(value)?;
553 p.finish()
554}
555
556fn write_property_node_f64<W: Write + Seek>(
557 writer: &mut W,
558 is_64: bool,
559 name: &str,
560 type1: &str,
561 type2: &str,
562 flags: &str,
563 value: f64,
564) -> io::Result<()> {
565 let mut p = NodeWriter::start(writer, "P", is_64)?;
566 p.write_property_string(name)?;
567 p.write_property_string(type1)?;
568 p.write_property_string(type2)?;
569 p.write_property_string(flags)?;
570 p.write_property_f64(value)?;
571 p.finish()
572}
573
574fn write_documents<W: Write + Seek>(writer: &mut W, is_64: bool) -> io::Result<()> {
575 let node = NodeWriter::start(writer, "Documents", is_64)?;
576 node.finish_with_children(|w| {
577 let mut count = NodeWriter::start(w, "Count", is_64)?;
578 count.write_property_i32(1)?;
579 count.finish()?;
580
581 let mut doc = NodeWriter::start(w, "Document", is_64)?;
582 doc.write_property_i64(0)?; doc.write_property_string("")?;
584 doc.write_property_string("Scene")?;
585 doc.finish()
586 })
587}
588
589fn write_definitions<W: Write + Seek>(
590 writer: &mut W,
591 is_64: bool,
592 meshes: &[MeshData],
593) -> io::Result<()> {
594 let node = NodeWriter::start(writer, "Definitions", is_64)?;
595 node.finish_with_children(|w| {
596 let mut ver = NodeWriter::start(w, "Version", is_64)?;
598 ver.write_property_i32(100)?;
599 ver.finish()?;
600
601 let mut count = NodeWriter::start(w, "Count", is_64)?;
603 count.write_property_i32(2)?; count.finish()?;
605
606 write_object_type(w, is_64, "Geometry", meshes.len() as i32)?;
608
609 write_object_type(w, is_64, "Model", meshes.len() as i32)?;
611
612 Ok(())
613 })
614}
615
616fn write_object_type<W: Write + Seek>(
617 writer: &mut W,
618 is_64: bool,
619 type_name: &str,
620 count: i32,
621) -> io::Result<()> {
622 let mut ot = NodeWriter::start(writer, "ObjectType", is_64)?;
623 ot.write_property_string(type_name)?;
624 ot.finish_with_children(|w| {
625 let mut c = NodeWriter::start(w, "Count", is_64)?;
626 c.write_property_i32(count)?;
627 c.finish()
628 })
629}
630
631fn write_objects<W: Write + Seek>(
632 writer: &mut W,
633 meshes: &[MeshData],
634 is_64: bool,
635 options: &WriterOptions,
636) -> io::Result<()> {
637 let node = NodeWriter::start(writer, "Objects", is_64)?;
638 node.finish_with_children(|w| {
639 for mesh_data in meshes {
640 write_geometry(w, mesh_data, is_64, options)?;
641 write_model(w, mesh_data, is_64)?;
642 }
643 Ok(())
644 })
645}
646
647fn write_model<W: Write + Seek>(
648 writer: &mut W,
649 mesh_data: &MeshData,
650 is_64: bool,
651) -> io::Result<()> {
652 let mut node = NodeWriter::start(writer, "Model", is_64)?;
653 node.write_property_i64(mesh_data.model_id)?;
654 let name_class = format!("{}\x00\x01Model", mesh_data.name);
656 node.write_property_string(&name_class)?;
657 node.write_property_string("Mesh")?;
658
659 node.finish_with_children(|w| {
660 let mut ver = NodeWriter::start(w, "Version", is_64)?;
661 ver.write_property_i32(232)?;
662 ver.finish()?;
663
664 let props = NodeWriter::start(w, "Properties70", is_64)?;
666 props.finish()?;
667
668 let mut shading = NodeWriter::start(w, "Shading", is_64)?;
670 shading.write_property_i16(1)?;
671 shading.finish()?;
672
673 let mut culling = NodeWriter::start(w, "Culling", is_64)?;
675 culling.write_property_string("CullingOff")?;
676 culling.finish()?;
677
678 Ok(())
679 })
680}
681
682fn write_geometry<W: Write + Seek>(
683 writer: &mut W,
684 mesh_data: &MeshData,
685 is_64: bool,
686 options: &WriterOptions,
687) -> io::Result<()> {
688 let mut node = NodeWriter::start(writer, "Geometry", is_64)?;
689 node.write_property_i64(mesh_data.geometry_id)?;
690 let name_class = format!("{}\x00\x01Geometry", mesh_data.name);
692 node.write_property_string(&name_class)?;
693 node.write_property_string("Mesh")?;
694
695 node.finish_with_children(|w| {
696 let mut gver = NodeWriter::start(w, "GeometryVersion", is_64)?;
698 gver.write_property_i32(124)?;
699 gver.finish()?;
700
701 if !mesh_data.vertices.is_empty() {
703 let mut vert_node = NodeWriter::start(w, "Vertices", is_64)?;
704 vert_node.write_property_f64_array(&mesh_data.vertices, options)?;
705 vert_node.finish()?;
706 }
707
708 if !mesh_data.indices.is_empty() {
710 let mut poly_node = NodeWriter::start(w, "PolygonVertexIndex", is_64)?;
711 poly_node.write_property_i32_array(&mesh_data.indices, options)?;
712 poly_node.finish()?;
713 }
714
715 Ok(())
716 })
717}
718
719fn write_connections<W: Write + Seek>(
720 writer: &mut W,
721 meshes: &[MeshData],
722 is_64: bool,
723) -> io::Result<()> {
724 let node = NodeWriter::start(writer, "Connections", is_64)?;
725 node.finish_with_children(|w| {
726 for mesh_data in meshes {
727 let mut c1 = NodeWriter::start(w, "C", is_64)?;
729 c1.write_property_string("OO")?;
730 c1.write_property_i64(mesh_data.model_id)?;
731 c1.write_property_i64(0)?; c1.finish()?;
733
734 let mut c2 = NodeWriter::start(w, "C", is_64)?;
736 c2.write_property_string("OO")?;
737 c2.write_property_i64(mesh_data.geometry_id)?;
738 c2.write_property_i64(mesh_data.model_id)?;
739 c2.finish()?;
740 }
741 Ok(())
742 })
743}
744
745fn write_footer<W: Write + Seek>(writer: &mut W) -> io::Result<()> {
746 let padding = [0u8; 20];
748 writer.write_all(&padding)?;
749
750 let footer_version: [u8; 4] = [0xFA, 0xBC, 0xAB, 0x09];
752 writer.write_all(&footer_version)?;
753
754 let pos = writer.stream_position()?;
756 let padding_needed = (16 - (pos % 16)) % 16;
757 if padding_needed > 0 {
758 writer.write_all(&vec![0u8; padding_needed as usize])?;
759 }
760
761 Ok(())
762}
763
764fn validate_supported_fbx_attributes(mesh: &Mesh) -> io::Result<()> {
769 for i in 0..mesh.num_attributes() {
770 let attribute_type = mesh.attribute(i).attribute_type();
771 if attribute_type != GeometryAttributeType::Position {
772 return Err(io::Error::new(
773 io::ErrorKind::InvalidInput,
774 format!(
775 "FBX writer currently supports only Position attributes; {:?} is not written",
776 attribute_type
777 ),
778 ));
779 }
780 }
781 Ok(())
782}
783
784fn extract_vertices(mesh: &Mesh) -> Vec<f64> {
785 let pos_att_id = mesh.named_attribute_id(GeometryAttributeType::Position);
786 if pos_att_id < 0 {
787 return Vec::new();
788 }
789
790 let att = mesh.attribute(pos_att_id);
791 let byte_stride = att.byte_stride() as usize;
792 let buffer = att.buffer();
793 let mut vertices = Vec::with_capacity(mesh.num_points() * 3);
794
795 for i in 0..mesh.num_points() {
796 let mut bytes = [0u8; 12];
797 buffer.read(i * byte_stride, &mut bytes);
798 let x = f32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]) as f64;
799 let y = f32::from_le_bytes([bytes[4], bytes[5], bytes[6], bytes[7]]) as f64;
800 let z = f32::from_le_bytes([bytes[8], bytes[9], bytes[10], bytes[11]]) as f64;
801 vertices.push(x);
802 vertices.push(y);
803 vertices.push(z);
804 }
805 vertices
806}
807
808fn extract_polygon_indices(mesh: &Mesh) -> Vec<i32> {
809 let mut indices = Vec::with_capacity(mesh.num_faces() * 3);
810 for i in 0..mesh.num_faces() as u32 {
811 let face = mesh.face(FaceIndex(i));
812 indices.push(face[0].0 as i32);
813 indices.push(face[1].0 as i32);
814 indices.push(!(face[2].0 as i32));
816 }
817 indices
818}
819
820#[cfg(test)]
825mod tests {
826 use super::*;
827 use draco_core::draco_types::DataType;
828 use draco_core::geometry_attribute::PointAttribute;
829 use draco_core::geometry_indices::PointIndex;
830 use std::io::Cursor;
831 use tempfile::NamedTempFile;
832
833 fn create_triangle_mesh() -> Mesh {
834 let mut mesh = Mesh::new();
835 let mut pos_att = PointAttribute::new();
836
837 pos_att.init(
838 GeometryAttributeType::Position,
839 3,
840 DataType::Float32,
841 false,
842 3,
843 );
844 let buffer = pos_att.buffer_mut();
845 let positions: [[f32; 3]; 3] = [[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]];
846 for (i, pos) in positions.iter().enumerate() {
847 let bytes: Vec<u8> = pos.iter().flat_map(|v| v.to_le_bytes()).collect();
848 buffer.write(i * 12, &bytes);
849 }
850 mesh.add_attribute(pos_att);
851
852 mesh.set_num_faces(1);
853 mesh.set_face(FaceIndex(0), [PointIndex(0), PointIndex(1), PointIndex(2)]);
854
855 mesh
856 }
857
858 #[test]
859 fn test_fbx_writer_new() {
860 let writer = FbxWriter::new();
861 assert_eq!(writer.mesh_count(), 0);
862 assert!(!writer.is_compression_enabled());
863 }
864
865 #[test]
866 fn test_fbx_writer_with_options() {
867 let writer = FbxWriter::new()
868 .with_compression(true)
869 .with_compression_threshold(64);
870 assert!(writer.is_compression_enabled());
871 }
872
873 #[test]
874 fn test_fbx_writer_add_mesh() {
875 let mesh = create_triangle_mesh();
876 let mut writer = FbxWriter::new();
877 Writer::add_mesh(&mut writer, &mesh, Some("TestMesh")).unwrap();
878 assert_eq!(writer.mesh_count(), 1);
879 }
880
881 #[test]
882 fn test_fbx_writer_write() {
883 let mesh = create_triangle_mesh();
884 let mut writer = FbxWriter::new();
885 Writer::add_mesh(&mut writer, &mesh, Some("Triangle")).unwrap();
886
887 let mut buffer = Cursor::new(Vec::new());
888 writer.write_to(&mut buffer).unwrap();
889
890 let data = buffer.into_inner();
891
892 assert_eq!(&data[0..21], FBX_MAGIC);
894 let version = u32::from_le_bytes([data[23], data[24], data[25], data[26]]);
896 assert_eq!(version, FBX_VERSION);
897 }
898
899 #[test]
900 fn test_write_fbx_mesh_convenience() {
901 let mesh = create_triangle_mesh();
902 let file = NamedTempFile::new().unwrap();
903 write_fbx_mesh(file.path(), &mesh).unwrap();
904
905 let metadata = std::fs::metadata(file.path()).unwrap();
906 assert!(metadata.len() > 27);
907 }
908
909 #[test]
910 fn test_multiple_meshes() {
911 let mesh1 = create_triangle_mesh();
912 let mesh2 = create_triangle_mesh();
913
914 let mut writer = FbxWriter::new();
915 Writer::add_mesh(&mut writer, &mesh1, Some("Mesh1")).unwrap();
916 Writer::add_mesh(&mut writer, &mesh2, Some("Mesh2")).unwrap();
917
918 assert_eq!(writer.mesh_count(), 2);
919
920 let mut buffer = Cursor::new(Vec::new());
921 writer.write_to(&mut buffer).unwrap();
922
923 let data = buffer.into_inner();
924 assert!(!data.is_empty());
925 }
926
927 #[cfg(feature = "compression")]
928 #[test]
929 fn test_write_with_compression() {
930 let mesh = create_triangle_mesh();
931 let mut writer = FbxWriter::new()
932 .with_compression(true)
933 .with_compression_threshold(0);
934 Writer::add_mesh(&mut writer, &mesh, None).unwrap();
935
936 let mut buffer = Cursor::new(Vec::new());
937 writer.write_to(&mut buffer).unwrap();
938
939 let data = buffer.into_inner();
940 assert!(!data.is_empty());
941 }
942}