1use oxiarc_deflate::zlib_compress;
21use oxihuman_mesh::MeshBuffers;
22use std::io::Write;
23
24const FBX_MAGIC: &[u8] = b"Kaydara FBX Binary \x00\x1a\x00";
26
27const COMPRESSION_THRESHOLD: usize = 512;
29
30const FBX_VERSION: u32 = 7400;
32
33const NULL_RECORD_LEN: usize = 13;
35
36#[derive(Debug, Clone)]
40pub enum FbxProperty {
41 Bool(bool),
43 I16(i16),
45 I32(i32),
47 I64(i64),
49 F32(f32),
51 F64(f64),
53 String(String),
55 Raw(Vec<u8>),
57 I32Array(Vec<i32>),
59 F64Array(Vec<f64>),
61 F32Array(Vec<f32>),
63}
64
65impl FbxProperty {
66 fn type_code(&self) -> u8 {
68 match self {
69 Self::Bool(_) => b'C',
70 Self::I16(_) => b'Y',
71 Self::I32(_) => b'I',
72 Self::I64(_) => b'L',
73 Self::F32(_) => b'F',
74 Self::F64(_) => b'D',
75 Self::String(_) => b'S',
76 Self::Raw(_) => b'R',
77 Self::I32Array(_) => b'i',
78 Self::F64Array(_) => b'd',
79 Self::F32Array(_) => b'f',
80 }
81 }
82
83 fn write_to(&self, buf: &mut Vec<u8>) -> anyhow::Result<()> {
85 buf.push(self.type_code());
86 match self {
87 Self::Bool(v) => buf.push(if *v { 1 } else { 0 }),
88 Self::I16(v) => buf.extend_from_slice(&v.to_le_bytes()),
89 Self::I32(v) => buf.extend_from_slice(&v.to_le_bytes()),
90 Self::I64(v) => buf.extend_from_slice(&v.to_le_bytes()),
91 Self::F32(v) => buf.extend_from_slice(&v.to_le_bytes()),
92 Self::F64(v) => buf.extend_from_slice(&v.to_le_bytes()),
93 Self::String(s) => {
94 let bytes = s.as_bytes();
95 let len = u32::try_from(bytes.len())
96 .map_err(|_| anyhow::anyhow!("FBX string too long: {} bytes", bytes.len()))?;
97 buf.extend_from_slice(&len.to_le_bytes());
98 buf.extend_from_slice(bytes);
99 }
100 Self::Raw(data) => {
101 let len = u32::try_from(data.len())
102 .map_err(|_| anyhow::anyhow!("FBX raw blob too long: {} bytes", data.len()))?;
103 buf.extend_from_slice(&len.to_le_bytes());
104 buf.extend_from_slice(data);
105 }
106 Self::I32Array(arr) => write_array_with_compression(buf, arr, 4, |b, v| {
107 b.extend_from_slice(&v.to_le_bytes());
108 })?,
109 Self::F64Array(arr) => write_array_with_compression(buf, arr, 8, |b, v| {
110 b.extend_from_slice(&v.to_le_bytes());
111 })?,
112 Self::F32Array(arr) => write_array_with_compression(buf, arr, 4, |b, v| {
113 b.extend_from_slice(&v.to_le_bytes());
114 })?,
115 }
116 Ok(())
117 }
118}
119
120fn write_array_with_compression<T>(
126 buf: &mut Vec<u8>,
127 arr: &[T],
128 elem_size: u32,
129 mut write_elem: impl FnMut(&mut Vec<u8>, &T),
130) -> anyhow::Result<()> {
131 let count = u32::try_from(arr.len())
132 .map_err(|_| anyhow::anyhow!("FBX array too long: {} elements", arr.len()))?;
133
134 let mut raw: Vec<u8> = Vec::with_capacity(arr.len() * elem_size as usize);
136 for v in arr {
137 write_elem(&mut raw, v);
138 }
139
140 buf.extend_from_slice(&count.to_le_bytes()); if arr.len() > COMPRESSION_THRESHOLD {
143 let compressed = zlib_compress(&raw, 6)
145 .map_err(|e| anyhow::anyhow!("FBX zlib compression failed: {}", e))?;
146 let compressed_len = u32::try_from(compressed.len())
147 .map_err(|_| anyhow::anyhow!("FBX compressed array too large: {} bytes", compressed.len()))?;
148 buf.extend_from_slice(&1u32.to_le_bytes()); buf.extend_from_slice(&compressed_len.to_le_bytes()); buf.extend_from_slice(&compressed);
151 } else {
152 let data_len = u32::try_from(raw.len())
154 .map_err(|_| anyhow::anyhow!("FBX array byte length overflow"))?;
155 buf.extend_from_slice(&0u32.to_le_bytes()); buf.extend_from_slice(&data_len.to_le_bytes()); buf.extend_from_slice(&raw);
158 }
159 Ok(())
160}
161
162#[derive(Debug, Clone)]
166pub struct FbxNode {
167 pub name: String,
169 pub properties: Vec<FbxProperty>,
171 pub children: Vec<FbxNode>,
173}
174
175impl FbxNode {
176 pub fn new(name: impl Into<String>) -> Self {
178 Self {
179 name: name.into(),
180 properties: Vec::new(),
181 children: Vec::new(),
182 }
183 }
184
185 pub fn add_property(&mut self, prop: FbxProperty) {
187 self.properties.push(prop);
188 }
189
190 pub fn add_child(&mut self, child: FbxNode) -> &mut FbxNode {
192 self.children.push(child);
193 let idx = self.children.len() - 1;
194 &mut self.children[idx]
195 }
196}
197
198pub struct FbxBinaryWriter {
202 output: Vec<u8>,
203}
204
205impl Default for FbxBinaryWriter {
206 fn default() -> Self {
207 Self::new()
208 }
209}
210
211impl FbxBinaryWriter {
212 pub fn new() -> Self {
214 Self {
215 output: Vec::with_capacity(256 * 1024),
216 }
217 }
218
219 pub fn write_header(&mut self) -> anyhow::Result<()> {
221 self.output.write_all(FBX_MAGIC)?;
222 self.output.write_all(&FBX_VERSION.to_le_bytes())?;
223 Ok(())
224 }
225
226 pub fn write_node(&mut self, node: &FbxNode) -> anyhow::Result<()> {
230 write_node_recursive(&mut self.output, node)
231 }
232
233 pub fn write_mesh(
236 &mut self,
237 name: &str,
238 positions: &[[f64; 3]],
239 normals: &[[f64; 3]],
240 uvs: &[[f64; 2]],
241 triangles: &[[usize; 3]],
242 ) -> anyhow::Result<()> {
243 let geometry_id: i64 = 200_000_000;
244 let model_id: i64 = 200_000_001;
245
246 let mut objects = FbxNode::new("Objects");
248
249 let mut geom = FbxNode::new("Geometry");
251 geom.add_property(FbxProperty::I64(geometry_id));
252 geom.add_property(FbxProperty::String(format!("Geometry::{name}\x00\x01Geometry")));
253 geom.add_property(FbxProperty::String("Mesh".into()));
254
255 let flat_verts: Vec<f64> = positions.iter().flat_map(|p| p.iter().copied()).collect();
257 let mut verts_node = FbxNode::new("Vertices");
258 verts_node.add_property(FbxProperty::F64Array(flat_verts));
259 geom.add_child(verts_node);
260
261 let flat_idx: Vec<i32> = triangles
263 .iter()
264 .flat_map(|tri| {
265 let a = i32::try_from(tri[0]).unwrap_or(0);
266 let b = i32::try_from(tri[1]).unwrap_or(0);
267 let c = i32::try_from(tri[2]).unwrap_or(0);
268 [a, b, -(c + 1)]
270 })
271 .collect();
272 let mut idx_node = FbxNode::new("PolygonVertexIndex");
273 idx_node.add_property(FbxProperty::I32Array(flat_idx));
274 geom.add_child(idx_node);
275
276 if !normals.is_empty() {
278 let mut layer_normal = FbxNode::new("LayerElementNormal");
279 layer_normal.add_property(FbxProperty::I32(0));
280
281 let mut ver = FbxNode::new("Version");
282 ver.add_property(FbxProperty::I32(101));
283 layer_normal.add_child(ver);
284
285 let mut mapping = FbxNode::new("MappingInformationType");
286 mapping.add_property(FbxProperty::String("ByVertice".into()));
287 layer_normal.add_child(mapping);
288
289 let mut reference = FbxNode::new("ReferenceInformationType");
290 reference.add_property(FbxProperty::String("Direct".into()));
291 layer_normal.add_child(reference);
292
293 let flat_normals: Vec<f64> =
294 normals.iter().flat_map(|n| n.iter().copied()).collect();
295 let mut ndata = FbxNode::new("Normals");
296 ndata.add_property(FbxProperty::F64Array(flat_normals));
297 layer_normal.add_child(ndata);
298
299 geom.add_child(layer_normal);
300 }
301
302 if !uvs.is_empty() {
304 let mut layer_uv = FbxNode::new("LayerElementUV");
305 layer_uv.add_property(FbxProperty::I32(0));
306
307 let mut ver = FbxNode::new("Version");
308 ver.add_property(FbxProperty::I32(101));
309 layer_uv.add_child(ver);
310
311 let mut mapping = FbxNode::new("MappingInformationType");
312 mapping.add_property(FbxProperty::String("ByVertice".into()));
313 layer_uv.add_child(mapping);
314
315 let mut reference = FbxNode::new("ReferenceInformationType");
316 reference.add_property(FbxProperty::String("Direct".into()));
317 layer_uv.add_child(reference);
318
319 let flat_uv: Vec<f64> = uvs.iter().flat_map(|u| u.iter().copied()).collect();
320 let mut uv_data = FbxNode::new("UV");
321 uv_data.add_property(FbxProperty::F64Array(flat_uv));
322 layer_uv.add_child(uv_data);
323
324 geom.add_child(layer_uv);
325 }
326
327 objects.add_child(geom);
328
329 let mut model = FbxNode::new("Model");
331 model.add_property(FbxProperty::I64(model_id));
332 model.add_property(FbxProperty::String(format!("Model::{name}\x00\x01Model")));
333 model.add_property(FbxProperty::String("Mesh".into()));
334
335 let mut version_node = FbxNode::new("Version");
336 version_node.add_property(FbxProperty::I32(232));
337 model.add_child(version_node);
338
339 objects.add_child(model);
340
341 self.write_node(&objects)?;
342
343 let mut conns = FbxNode::new("Connections");
345
346 let mut c1 = FbxNode::new("C");
348 c1.add_property(FbxProperty::String("OO".into()));
349 c1.add_property(FbxProperty::I64(geometry_id));
350 c1.add_property(FbxProperty::I64(model_id));
351 conns.add_child(c1);
352
353 let mut c2 = FbxNode::new("C");
355 c2.add_property(FbxProperty::String("OO".into()));
356 c2.add_property(FbxProperty::I64(model_id));
357 c2.add_property(FbxProperty::I64(0));
358 conns.add_child(c2);
359
360 self.write_node(&conns)?;
361
362 Ok(())
363 }
364
365 pub fn write_skeleton(
368 &mut self,
369 bone_names: &[String],
370 bone_parents: &[Option<usize>],
371 bind_poses: &[[f64; 16]],
372 ) -> anyhow::Result<()> {
373 if bone_names.len() != bone_parents.len() || bone_names.len() != bind_poses.len() {
374 return Err(anyhow::anyhow!(
375 "Skeleton arrays have mismatched lengths: names={}, parents={}, poses={}",
376 bone_names.len(),
377 bone_parents.len(),
378 bind_poses.len(),
379 ));
380 }
381
382 let base_id: i64 = 300_000_000;
383
384 let mut objects = FbxNode::new("Objects");
385
386 for (i, bone_name) in bone_names.iter().enumerate() {
387 let attr_id = base_id + (i as i64) * 2;
388 let model_id = attr_id + 1;
389
390 let mut attr = FbxNode::new("NodeAttribute");
392 attr.add_property(FbxProperty::I64(attr_id));
393 attr.add_property(FbxProperty::String(format!(
394 "NodeAttribute::{bone_name}\x00\x01NodeAttribute"
395 )));
396 attr.add_property(FbxProperty::String("LimbNode".into()));
397
398 let mut tf = FbxNode::new("TypeFlags");
399 tf.add_property(FbxProperty::String("Skeleton".into()));
400 attr.add_child(tf);
401
402 objects.add_child(attr);
403
404 let mut model = FbxNode::new("Model");
406 model.add_property(FbxProperty::I64(model_id));
407 model.add_property(FbxProperty::String(format!(
408 "Model::{bone_name}\x00\x01Model"
409 )));
410 model.add_property(FbxProperty::String("LimbNode".into()));
411
412 let mut props70 = FbxNode::new("Properties70");
414
415 let pose = &bind_poses[i];
418 let tx = pose[12];
419 let ty = pose[13];
420 let tz = pose[14];
421
422 let mut p_trans = FbxNode::new("P");
423 p_trans.add_property(FbxProperty::String("Lcl Translation".into()));
424 p_trans.add_property(FbxProperty::String("Lcl Translation".into()));
425 p_trans.add_property(FbxProperty::String(String::new()));
426 p_trans.add_property(FbxProperty::String("A".into()));
427 p_trans.add_property(FbxProperty::F64(tx));
428 p_trans.add_property(FbxProperty::F64(ty));
429 p_trans.add_property(FbxProperty::F64(tz));
430 props70.add_child(p_trans);
431
432 model.add_child(props70);
433 objects.add_child(model);
434 }
435
436 self.write_node(&objects)?;
437
438 let mut conns = FbxNode::new("Connections");
440
441 for (i, parent_opt) in bone_parents.iter().enumerate() {
442 let attr_id = base_id + (i as i64) * 2;
443 let model_id = attr_id + 1;
444
445 let mut c_attr = FbxNode::new("C");
447 c_attr.add_property(FbxProperty::String("OO".into()));
448 c_attr.add_property(FbxProperty::I64(attr_id));
449 c_attr.add_property(FbxProperty::I64(model_id));
450 conns.add_child(c_attr);
451
452 let parent_model_id = match parent_opt {
454 Some(pi) => base_id + (*pi as i64) * 2 + 1,
455 None => 0,
456 };
457 let mut c_model = FbxNode::new("C");
458 c_model.add_property(FbxProperty::String("OO".into()));
459 c_model.add_property(FbxProperty::I64(model_id));
460 c_model.add_property(FbxProperty::I64(parent_model_id));
461 conns.add_child(c_model);
462 }
463
464 self.write_node(&conns)?;
465
466 Ok(())
467 }
468
469 pub fn finish(mut self) -> anyhow::Result<Vec<u8>> {
472 self.output.extend_from_slice(&[0u8; NULL_RECORD_LEN]);
474
475 let footer_padding_target = self.output.len().div_ceil(16) * 16;
478 while self.output.len() < footer_padding_target {
479 self.output.push(0);
480 }
481
482 self.output.extend_from_slice(&FBX_VERSION.to_le_bytes());
485 self.output.extend_from_slice(&[0u8; 120]);
487 self.output.extend_from_slice(&[
489 0xf8, 0x5a, 0x8c, 0x6a, 0xde, 0xf5, 0xd9, 0x7e, 0xec, 0xe9, 0x0c, 0xe3, 0x75, 0x8f,
490 0x29, 0x0b,
491 ]);
492
493 Ok(self.output)
494 }
495}
496
497pub fn export_mesh_fbx_binary(mesh: &MeshBuffers) -> anyhow::Result<Vec<u8>> {
506 let mut writer = FbxBinaryWriter::new();
507 writer.write_header()?;
508
509 let positions_f64: Vec<[f64; 3]> = mesh
511 .positions
512 .iter()
513 .map(|p| [p[0] as f64, p[1] as f64, p[2] as f64])
514 .collect();
515
516 let normals_f64: Vec<[f64; 3]> = mesh
517 .normals
518 .iter()
519 .map(|n| [n[0] as f64, n[1] as f64, n[2] as f64])
520 .collect();
521
522 let uvs_f64: Vec<[f64; 2]> = mesh
523 .uvs
524 .iter()
525 .map(|u| [u[0] as f64, u[1] as f64])
526 .collect();
527
528 let triangles: Vec<[usize; 3]> = mesh
530 .indices
531 .chunks(3)
532 .filter_map(|tri| {
533 if tri.len() == 3 {
534 Some([tri[0] as usize, tri[1] as usize, tri[2] as usize])
535 } else {
536 None
537 }
538 })
539 .collect();
540
541 writer.write_mesh("Mesh", &positions_f64, &normals_f64, &uvs_f64, &triangles)?;
542 writer.finish()
543}
544
545fn write_node_recursive(buf: &mut Vec<u8>, node: &FbxNode) -> anyhow::Result<()> {
561 let record_start = buf.len();
562
563 buf.extend_from_slice(&[0u8; 13]);
566
567 let name_bytes = node.name.as_bytes();
569 let name_len = u8::try_from(name_bytes.len())
570 .map_err(|_| anyhow::anyhow!("FBX node name too long: {}", node.name))?;
571 buf[record_start + 12] = name_len;
572 buf.extend_from_slice(name_bytes);
573
574 let props_start = buf.len();
576 for prop in &node.properties {
577 prop.write_to(buf)?;
578 }
579 let props_end = buf.len();
580 let property_list_len = u32::try_from(props_end - props_start)
581 .map_err(|_| anyhow::anyhow!("FBX property list too large"))?;
582 let num_properties = u32::try_from(node.properties.len())
583 .map_err(|_| anyhow::anyhow!("FBX too many properties"))?;
584
585 if !node.children.is_empty() {
587 for child in &node.children {
588 write_node_recursive(buf, child)?;
589 }
590 buf.extend_from_slice(&[0u8; NULL_RECORD_LEN]);
592 }
593
594 let end_offset = u32::try_from(buf.len())
595 .map_err(|_| anyhow::anyhow!("FBX file too large for 32-bit offsets"))?;
596
597 buf[record_start..record_start + 4].copy_from_slice(&end_offset.to_le_bytes());
599 buf[record_start + 4..record_start + 8].copy_from_slice(&num_properties.to_le_bytes());
600 buf[record_start + 8..record_start + 12].copy_from_slice(&property_list_len.to_le_bytes());
601
602 Ok(())
603}
604
605#[cfg(test)]
608mod tests {
609 use super::*;
610
611 #[test]
612 fn test_header() {
613 let mut w = FbxBinaryWriter::new();
614 w.write_header().expect("write_header failed");
615 let out = &w.output;
616 assert_eq!(&out[..23], FBX_MAGIC);
617 let ver = u32::from_le_bytes([out[23], out[24], out[25], out[26]]);
618 assert_eq!(ver, 7400);
619 }
620
621 #[test]
622 fn test_write_simple_node() {
623 let mut w = FbxBinaryWriter::new();
624 w.write_header().expect("header");
625 let mut node = FbxNode::new("TestNode");
626 node.add_property(FbxProperty::I32(42));
627 w.write_node(&node).expect("write_node");
628 let data = w.finish().expect("finish");
629 assert_eq!(&data[..23], FBX_MAGIC);
631 assert!(data.len() > 27 + NULL_RECORD_LEN);
633 }
634
635 #[test]
636 fn test_write_mesh() {
637 let mut w = FbxBinaryWriter::new();
638 w.write_header().expect("header");
639
640 let positions = vec![
641 [0.0, 0.0, 0.0],
642 [1.0, 0.0, 0.0],
643 [0.0, 1.0, 0.0],
644 ];
645 let normals = vec![
646 [0.0, 0.0, 1.0],
647 [0.0, 0.0, 1.0],
648 [0.0, 0.0, 1.0],
649 ];
650 let uvs = vec![[0.0, 0.0], [1.0, 0.0], [0.0, 1.0]];
651 let triangles = vec![[0, 1, 2]];
652
653 w.write_mesh("Triangle", &positions, &normals, &uvs, &triangles)
654 .expect("write_mesh");
655
656 let data = w.finish().expect("finish");
657 assert!(data.len() > 200);
658 assert_eq!(&data[..23], FBX_MAGIC);
660 }
661
662 #[test]
663 fn test_write_skeleton() {
664 let mut w = FbxBinaryWriter::new();
665 w.write_header().expect("header");
666
667 let names = vec!["Hips".to_string(), "Spine".to_string(), "Head".to_string()];
668 let parents = vec![None, Some(0), Some(1)];
669 #[rustfmt::skip]
670 let identity = [
671 1.0, 0.0, 0.0, 0.0,
672 0.0, 1.0, 0.0, 0.0,
673 0.0, 0.0, 1.0, 0.0,
674 0.0, 0.0, 0.0, 1.0,
675 ];
676 let poses = vec![identity; 3];
677
678 w.write_skeleton(&names, &parents, &poses)
679 .expect("write_skeleton");
680
681 let data = w.finish().expect("finish");
682 assert!(data.len() > 200);
683 }
684
685 #[test]
686 fn test_skeleton_mismatched_lengths() {
687 let mut w = FbxBinaryWriter::new();
688 w.write_header().expect("header");
689
690 let names = vec!["A".to_string()];
691 let parents = vec![None, Some(0)]; let poses = vec![[0.0; 16]];
693
694 let result = w.write_skeleton(&names, &parents, &poses);
695 assert!(result.is_err());
696 }
697
698 #[test]
699 fn test_property_type_codes() {
700 assert_eq!(FbxProperty::Bool(true).type_code(), b'C');
701 assert_eq!(FbxProperty::I16(0).type_code(), b'Y');
702 assert_eq!(FbxProperty::I32(0).type_code(), b'I');
703 assert_eq!(FbxProperty::I64(0).type_code(), b'L');
704 assert_eq!(FbxProperty::F32(0.0).type_code(), b'F');
705 assert_eq!(FbxProperty::F64(0.0).type_code(), b'D');
706 assert_eq!(FbxProperty::String(String::new()).type_code(), b'S');
707 assert_eq!(FbxProperty::Raw(vec![]).type_code(), b'R');
708 assert_eq!(FbxProperty::I32Array(vec![]).type_code(), b'i');
709 assert_eq!(FbxProperty::F64Array(vec![]).type_code(), b'd');
710 assert_eq!(FbxProperty::F32Array(vec![]).type_code(), b'f');
711 }
712
713 #[test]
714 fn test_empty_mesh() {
715 let mut w = FbxBinaryWriter::new();
716 w.write_header().expect("header");
717 w.write_mesh("Empty", &[], &[], &[], &[]).expect("write_mesh");
718 let data = w.finish().expect("finish");
719 assert!(data.len() > 27);
720 }
721
722 #[test]
723 fn test_node_children() {
724 let mut parent = FbxNode::new("Parent");
725 let child = FbxNode::new("Child");
726 parent.add_child(child);
727 assert_eq!(parent.children.len(), 1);
728 assert_eq!(parent.children[0].name, "Child");
729 }
730
731 #[test]
732 fn test_finish_contains_footer_magic() {
733 let mut w = FbxBinaryWriter::new();
734 w.write_header().expect("header");
735 let data = w.finish().expect("finish");
736 let footer = &data[data.len() - 16..];
738 assert_eq!(footer[0], 0xf8);
739 assert_eq!(footer[1], 0x5a);
740 }
741
742 #[test]
743 fn test_default_trait() {
744 let w = FbxBinaryWriter::default();
745 assert!(w.output.is_empty());
746 }
747
748 fn minimal_mesh() -> MeshBuffers {
751 MeshBuffers {
752 positions: vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]],
753 normals: vec![[0.0, 0.0, 1.0]; 3],
754 tangents: vec![[1.0, 0.0, 0.0, 1.0]; 3],
755 uvs: vec![[0.0, 0.0], [1.0, 0.0], [0.0, 1.0]],
756 indices: vec![0, 1, 2],
757 colors: None,
758 has_suit: false,
759 }
760 }
761
762 #[test]
764 fn test_fbx_magic_bytes() {
765 let mesh = minimal_mesh();
766 let data = export_mesh_fbx_binary(&mesh).expect("export_mesh_fbx_binary failed");
767 assert_eq!(
768 &data[..23],
769 b"Kaydara FBX Binary \x00\x1a\x00",
770 "FBX magic header mismatch"
771 );
772 }
773
774 #[test]
778 fn test_zlib_array_round_trip() {
779 let data: Vec<f32> = (0..1000).map(|i| i as f32).collect();
780 let mut buf: Vec<u8> = Vec::new();
781 write_array_with_compression(&mut buf, &data, 4, |b, v: &f32| {
783 b.extend_from_slice(&v.to_le_bytes());
784 })
785 .expect("write_array_with_compression failed");
786
787 let encoding = u32::from_le_bytes(
789 buf[4..8]
790 .try_into()
791 .expect("encoding slice must be 4 bytes"),
792 );
793 assert_eq!(encoding, 1, "expected zlib encoding (1) for large array");
794
795 let compressed_len = u32::from_le_bytes(
797 buf[8..12]
798 .try_into()
799 .expect("compressed_len slice must be 4 bytes"),
800 ) as usize;
801 assert!(
802 compressed_len < data.len() * 4,
803 "compressed payload ({compressed_len} B) should be smaller than raw ({} B)",
804 data.len() * 4
805 );
806 }
807
808 #[test]
811 fn test_mesh_export_smoke() {
812 let mesh = minimal_mesh();
813 let result = export_mesh_fbx_binary(&mesh);
814 assert!(result.is_ok(), "export_mesh_fbx_binary returned error");
815 let bytes = result.expect("already checked above");
816 assert!(
817 bytes.len() > 27,
818 "exported FBX should be larger than the 27-byte header, got {} bytes",
819 bytes.len()
820 );
821 }
822
823 #[test]
824 fn test_mesh_then_skeleton() {
825 let mut w = FbxBinaryWriter::new();
826 w.write_header().expect("header");
827
828 let positions = vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]];
829 let triangles = vec![[0, 1, 2]];
830 w.write_mesh("Body", &positions, &[], &[], &triangles)
831 .expect("mesh");
832
833 let names = vec!["Root".to_string()];
834 let parents = vec![None];
835 let poses = vec![[1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0]];
836 w.write_skeleton(&names, &parents, &poses)
837 .expect("skeleton");
838
839 let data = w.finish().expect("finish");
840 assert!(data.len() > 300);
841 }
842}