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