1use miniz_oxide::deflate::compress_to_vec_zlib;
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 = compress_to_vec_zlib(&raw, 6);
145 let compressed_len = u32::try_from(compressed.len())
146 .map_err(|_| anyhow::anyhow!("FBX compressed array too large: {} bytes", compressed.len()))?;
147 buf.extend_from_slice(&1u32.to_le_bytes()); buf.extend_from_slice(&compressed_len.to_le_bytes()); buf.extend_from_slice(&compressed);
150 } else {
151 let data_len = u32::try_from(raw.len())
153 .map_err(|_| anyhow::anyhow!("FBX array byte length overflow"))?;
154 buf.extend_from_slice(&0u32.to_le_bytes()); buf.extend_from_slice(&data_len.to_le_bytes()); buf.extend_from_slice(&raw);
157 }
158 Ok(())
159}
160
161#[derive(Debug, Clone)]
165pub struct FbxNode {
166 pub name: String,
168 pub properties: Vec<FbxProperty>,
170 pub children: Vec<FbxNode>,
172}
173
174impl FbxNode {
175 pub fn new(name: impl Into<String>) -> Self {
177 Self {
178 name: name.into(),
179 properties: Vec::new(),
180 children: Vec::new(),
181 }
182 }
183
184 pub fn add_property(&mut self, prop: FbxProperty) {
186 self.properties.push(prop);
187 }
188
189 pub fn add_child(&mut self, child: FbxNode) -> &mut FbxNode {
191 self.children.push(child);
192 let idx = self.children.len() - 1;
193 &mut self.children[idx]
194 }
195}
196
197pub struct FbxBinaryWriter {
201 output: Vec<u8>,
202}
203
204impl Default for FbxBinaryWriter {
205 fn default() -> Self {
206 Self::new()
207 }
208}
209
210impl FbxBinaryWriter {
211 pub fn new() -> Self {
213 Self {
214 output: Vec::with_capacity(256 * 1024),
215 }
216 }
217
218 pub fn write_header(&mut self) -> anyhow::Result<()> {
220 self.output.write_all(FBX_MAGIC)?;
221 self.output.write_all(&FBX_VERSION.to_le_bytes())?;
222 Ok(())
223 }
224
225 pub fn write_node(&mut self, node: &FbxNode) -> anyhow::Result<()> {
229 write_node_recursive(&mut self.output, node)
230 }
231
232 pub fn write_mesh(
235 &mut self,
236 name: &str,
237 positions: &[[f64; 3]],
238 normals: &[[f64; 3]],
239 uvs: &[[f64; 2]],
240 triangles: &[[usize; 3]],
241 ) -> anyhow::Result<()> {
242 let geometry_id: i64 = 200_000_000;
243 let model_id: i64 = 200_000_001;
244
245 let mut objects = FbxNode::new("Objects");
247
248 let mut geom = FbxNode::new("Geometry");
250 geom.add_property(FbxProperty::I64(geometry_id));
251 geom.add_property(FbxProperty::String(format!("Geometry::{name}\x00\x01Geometry")));
252 geom.add_property(FbxProperty::String("Mesh".into()));
253
254 let flat_verts: Vec<f64> = positions.iter().flat_map(|p| p.iter().copied()).collect();
256 let mut verts_node = FbxNode::new("Vertices");
257 verts_node.add_property(FbxProperty::F64Array(flat_verts));
258 geom.add_child(verts_node);
259
260 let flat_idx: Vec<i32> = triangles
262 .iter()
263 .flat_map(|tri| {
264 let a = i32::try_from(tri[0]).unwrap_or(0);
265 let b = i32::try_from(tri[1]).unwrap_or(0);
266 let c = i32::try_from(tri[2]).unwrap_or(0);
267 [a, b, -(c + 1)]
269 })
270 .collect();
271 let mut idx_node = FbxNode::new("PolygonVertexIndex");
272 idx_node.add_property(FbxProperty::I32Array(flat_idx));
273 geom.add_child(idx_node);
274
275 if !normals.is_empty() {
277 let mut layer_normal = FbxNode::new("LayerElementNormal");
278 layer_normal.add_property(FbxProperty::I32(0));
279
280 let mut ver = FbxNode::new("Version");
281 ver.add_property(FbxProperty::I32(101));
282 layer_normal.add_child(ver);
283
284 let mut mapping = FbxNode::new("MappingInformationType");
285 mapping.add_property(FbxProperty::String("ByVertice".into()));
286 layer_normal.add_child(mapping);
287
288 let mut reference = FbxNode::new("ReferenceInformationType");
289 reference.add_property(FbxProperty::String("Direct".into()));
290 layer_normal.add_child(reference);
291
292 let flat_normals: Vec<f64> =
293 normals.iter().flat_map(|n| n.iter().copied()).collect();
294 let mut ndata = FbxNode::new("Normals");
295 ndata.add_property(FbxProperty::F64Array(flat_normals));
296 layer_normal.add_child(ndata);
297
298 geom.add_child(layer_normal);
299 }
300
301 if !uvs.is_empty() {
303 let mut layer_uv = FbxNode::new("LayerElementUV");
304 layer_uv.add_property(FbxProperty::I32(0));
305
306 let mut ver = FbxNode::new("Version");
307 ver.add_property(FbxProperty::I32(101));
308 layer_uv.add_child(ver);
309
310 let mut mapping = FbxNode::new("MappingInformationType");
311 mapping.add_property(FbxProperty::String("ByVertice".into()));
312 layer_uv.add_child(mapping);
313
314 let mut reference = FbxNode::new("ReferenceInformationType");
315 reference.add_property(FbxProperty::String("Direct".into()));
316 layer_uv.add_child(reference);
317
318 let flat_uv: Vec<f64> = uvs.iter().flat_map(|u| u.iter().copied()).collect();
319 let mut uv_data = FbxNode::new("UV");
320 uv_data.add_property(FbxProperty::F64Array(flat_uv));
321 layer_uv.add_child(uv_data);
322
323 geom.add_child(layer_uv);
324 }
325
326 objects.add_child(geom);
327
328 let mut model = FbxNode::new("Model");
330 model.add_property(FbxProperty::I64(model_id));
331 model.add_property(FbxProperty::String(format!("Model::{name}\x00\x01Model")));
332 model.add_property(FbxProperty::String("Mesh".into()));
333
334 let mut version_node = FbxNode::new("Version");
335 version_node.add_property(FbxProperty::I32(232));
336 model.add_child(version_node);
337
338 objects.add_child(model);
339
340 self.write_node(&objects)?;
341
342 let mut conns = FbxNode::new("Connections");
344
345 let mut c1 = FbxNode::new("C");
347 c1.add_property(FbxProperty::String("OO".into()));
348 c1.add_property(FbxProperty::I64(geometry_id));
349 c1.add_property(FbxProperty::I64(model_id));
350 conns.add_child(c1);
351
352 let mut c2 = FbxNode::new("C");
354 c2.add_property(FbxProperty::String("OO".into()));
355 c2.add_property(FbxProperty::I64(model_id));
356 c2.add_property(FbxProperty::I64(0));
357 conns.add_child(c2);
358
359 self.write_node(&conns)?;
360
361 Ok(())
362 }
363
364 pub fn write_skeleton(
367 &mut self,
368 bone_names: &[String],
369 bone_parents: &[Option<usize>],
370 bind_poses: &[[f64; 16]],
371 ) -> anyhow::Result<()> {
372 if bone_names.len() != bone_parents.len() || bone_names.len() != bind_poses.len() {
373 return Err(anyhow::anyhow!(
374 "Skeleton arrays have mismatched lengths: names={}, parents={}, poses={}",
375 bone_names.len(),
376 bone_parents.len(),
377 bind_poses.len(),
378 ));
379 }
380
381 let base_id: i64 = 300_000_000;
382
383 let mut objects = FbxNode::new("Objects");
384
385 for (i, bone_name) in bone_names.iter().enumerate() {
386 let attr_id = base_id + (i as i64) * 2;
387 let model_id = attr_id + 1;
388
389 let mut attr = FbxNode::new("NodeAttribute");
391 attr.add_property(FbxProperty::I64(attr_id));
392 attr.add_property(FbxProperty::String(format!(
393 "NodeAttribute::{bone_name}\x00\x01NodeAttribute"
394 )));
395 attr.add_property(FbxProperty::String("LimbNode".into()));
396
397 let mut tf = FbxNode::new("TypeFlags");
398 tf.add_property(FbxProperty::String("Skeleton".into()));
399 attr.add_child(tf);
400
401 objects.add_child(attr);
402
403 let mut model = FbxNode::new("Model");
405 model.add_property(FbxProperty::I64(model_id));
406 model.add_property(FbxProperty::String(format!(
407 "Model::{bone_name}\x00\x01Model"
408 )));
409 model.add_property(FbxProperty::String("LimbNode".into()));
410
411 let mut props70 = FbxNode::new("Properties70");
413
414 let pose = &bind_poses[i];
417 let tx = pose[12];
418 let ty = pose[13];
419 let tz = pose[14];
420
421 let mut p_trans = FbxNode::new("P");
422 p_trans.add_property(FbxProperty::String("Lcl Translation".into()));
423 p_trans.add_property(FbxProperty::String("Lcl Translation".into()));
424 p_trans.add_property(FbxProperty::String(String::new()));
425 p_trans.add_property(FbxProperty::String("A".into()));
426 p_trans.add_property(FbxProperty::F64(tx));
427 p_trans.add_property(FbxProperty::F64(ty));
428 p_trans.add_property(FbxProperty::F64(tz));
429 props70.add_child(p_trans);
430
431 model.add_child(props70);
432 objects.add_child(model);
433 }
434
435 self.write_node(&objects)?;
436
437 let mut conns = FbxNode::new("Connections");
439
440 for (i, parent_opt) in bone_parents.iter().enumerate() {
441 let attr_id = base_id + (i as i64) * 2;
442 let model_id = attr_id + 1;
443
444 let mut c_attr = FbxNode::new("C");
446 c_attr.add_property(FbxProperty::String("OO".into()));
447 c_attr.add_property(FbxProperty::I64(attr_id));
448 c_attr.add_property(FbxProperty::I64(model_id));
449 conns.add_child(c_attr);
450
451 let parent_model_id = match parent_opt {
453 Some(pi) => base_id + (*pi as i64) * 2 + 1,
454 None => 0,
455 };
456 let mut c_model = FbxNode::new("C");
457 c_model.add_property(FbxProperty::String("OO".into()));
458 c_model.add_property(FbxProperty::I64(model_id));
459 c_model.add_property(FbxProperty::I64(parent_model_id));
460 conns.add_child(c_model);
461 }
462
463 self.write_node(&conns)?;
464
465 Ok(())
466 }
467
468 pub fn finish(mut self) -> anyhow::Result<Vec<u8>> {
471 self.output.extend_from_slice(&[0u8; NULL_RECORD_LEN]);
473
474 let footer_padding_target = self.output.len().div_ceil(16) * 16;
477 while self.output.len() < footer_padding_target {
478 self.output.push(0);
479 }
480
481 self.output.extend_from_slice(&FBX_VERSION.to_le_bytes());
484 self.output.extend_from_slice(&[0u8; 120]);
486 self.output.extend_from_slice(&[
488 0xf8, 0x5a, 0x8c, 0x6a, 0xde, 0xf5, 0xd9, 0x7e, 0xec, 0xe9, 0x0c, 0xe3, 0x75, 0x8f,
489 0x29, 0x0b,
490 ]);
491
492 Ok(self.output)
493 }
494}
495
496pub fn export_mesh_fbx_binary(mesh: &MeshBuffers) -> anyhow::Result<Vec<u8>> {
505 let mut writer = FbxBinaryWriter::new();
506 writer.write_header()?;
507
508 let positions_f64: Vec<[f64; 3]> = mesh
510 .positions
511 .iter()
512 .map(|p| [p[0] as f64, p[1] as f64, p[2] as f64])
513 .collect();
514
515 let normals_f64: Vec<[f64; 3]> = mesh
516 .normals
517 .iter()
518 .map(|n| [n[0] as f64, n[1] as f64, n[2] as f64])
519 .collect();
520
521 let uvs_f64: Vec<[f64; 2]> = mesh
522 .uvs
523 .iter()
524 .map(|u| [u[0] as f64, u[1] as f64])
525 .collect();
526
527 let triangles: Vec<[usize; 3]> = mesh
529 .indices
530 .chunks(3)
531 .filter_map(|tri| {
532 if tri.len() == 3 {
533 Some([tri[0] as usize, tri[1] as usize, tri[2] as usize])
534 } else {
535 None
536 }
537 })
538 .collect();
539
540 writer.write_mesh("Mesh", &positions_f64, &normals_f64, &uvs_f64, &triangles)?;
541 writer.finish()
542}
543
544fn write_node_recursive(buf: &mut Vec<u8>, node: &FbxNode) -> anyhow::Result<()> {
560 let record_start = buf.len();
561
562 buf.extend_from_slice(&[0u8; 13]);
565
566 let name_bytes = node.name.as_bytes();
568 let name_len = u8::try_from(name_bytes.len())
569 .map_err(|_| anyhow::anyhow!("FBX node name too long: {}", node.name))?;
570 buf[record_start + 12] = name_len;
571 buf.extend_from_slice(name_bytes);
572
573 let props_start = buf.len();
575 for prop in &node.properties {
576 prop.write_to(buf)?;
577 }
578 let props_end = buf.len();
579 let property_list_len = u32::try_from(props_end - props_start)
580 .map_err(|_| anyhow::anyhow!("FBX property list too large"))?;
581 let num_properties = u32::try_from(node.properties.len())
582 .map_err(|_| anyhow::anyhow!("FBX too many properties"))?;
583
584 if !node.children.is_empty() {
586 for child in &node.children {
587 write_node_recursive(buf, child)?;
588 }
589 buf.extend_from_slice(&[0u8; NULL_RECORD_LEN]);
591 }
592
593 let end_offset = u32::try_from(buf.len())
594 .map_err(|_| anyhow::anyhow!("FBX file too large for 32-bit offsets"))?;
595
596 buf[record_start..record_start + 4].copy_from_slice(&end_offset.to_le_bytes());
598 buf[record_start + 4..record_start + 8].copy_from_slice(&num_properties.to_le_bytes());
599 buf[record_start + 8..record_start + 12].copy_from_slice(&property_list_len.to_le_bytes());
600
601 Ok(())
602}
603
604#[cfg(test)]
607mod tests {
608 use super::*;
609
610 #[test]
611 fn test_header() {
612 let mut w = FbxBinaryWriter::new();
613 w.write_header().expect("write_header failed");
614 let out = &w.output;
615 assert_eq!(&out[..23], FBX_MAGIC);
616 let ver = u32::from_le_bytes([out[23], out[24], out[25], out[26]]);
617 assert_eq!(ver, 7400);
618 }
619
620 #[test]
621 fn test_write_simple_node() {
622 let mut w = FbxBinaryWriter::new();
623 w.write_header().expect("header");
624 let mut node = FbxNode::new("TestNode");
625 node.add_property(FbxProperty::I32(42));
626 w.write_node(&node).expect("write_node");
627 let data = w.finish().expect("finish");
628 assert_eq!(&data[..23], FBX_MAGIC);
630 assert!(data.len() > 27 + NULL_RECORD_LEN);
632 }
633
634 #[test]
635 fn test_write_mesh() {
636 let mut w = FbxBinaryWriter::new();
637 w.write_header().expect("header");
638
639 let positions = vec![
640 [0.0, 0.0, 0.0],
641 [1.0, 0.0, 0.0],
642 [0.0, 1.0, 0.0],
643 ];
644 let normals = vec![
645 [0.0, 0.0, 1.0],
646 [0.0, 0.0, 1.0],
647 [0.0, 0.0, 1.0],
648 ];
649 let uvs = vec![[0.0, 0.0], [1.0, 0.0], [0.0, 1.0]];
650 let triangles = vec![[0, 1, 2]];
651
652 w.write_mesh("Triangle", &positions, &normals, &uvs, &triangles)
653 .expect("write_mesh");
654
655 let data = w.finish().expect("finish");
656 assert!(data.len() > 200);
657 assert_eq!(&data[..23], FBX_MAGIC);
659 }
660
661 #[test]
662 fn test_write_skeleton() {
663 let mut w = FbxBinaryWriter::new();
664 w.write_header().expect("header");
665
666 let names = vec!["Hips".to_string(), "Spine".to_string(), "Head".to_string()];
667 let parents = vec![None, Some(0), Some(1)];
668 #[rustfmt::skip]
669 let identity = [
670 1.0, 0.0, 0.0, 0.0,
671 0.0, 1.0, 0.0, 0.0,
672 0.0, 0.0, 1.0, 0.0,
673 0.0, 0.0, 0.0, 1.0,
674 ];
675 let poses = vec![identity; 3];
676
677 w.write_skeleton(&names, &parents, &poses)
678 .expect("write_skeleton");
679
680 let data = w.finish().expect("finish");
681 assert!(data.len() > 200);
682 }
683
684 #[test]
685 fn test_skeleton_mismatched_lengths() {
686 let mut w = FbxBinaryWriter::new();
687 w.write_header().expect("header");
688
689 let names = vec!["A".to_string()];
690 let parents = vec![None, Some(0)]; let poses = vec![[0.0; 16]];
692
693 let result = w.write_skeleton(&names, &parents, &poses);
694 assert!(result.is_err());
695 }
696
697 #[test]
698 fn test_property_type_codes() {
699 assert_eq!(FbxProperty::Bool(true).type_code(), b'C');
700 assert_eq!(FbxProperty::I16(0).type_code(), b'Y');
701 assert_eq!(FbxProperty::I32(0).type_code(), b'I');
702 assert_eq!(FbxProperty::I64(0).type_code(), b'L');
703 assert_eq!(FbxProperty::F32(0.0).type_code(), b'F');
704 assert_eq!(FbxProperty::F64(0.0).type_code(), b'D');
705 assert_eq!(FbxProperty::String(String::new()).type_code(), b'S');
706 assert_eq!(FbxProperty::Raw(vec![]).type_code(), b'R');
707 assert_eq!(FbxProperty::I32Array(vec![]).type_code(), b'i');
708 assert_eq!(FbxProperty::F64Array(vec![]).type_code(), b'd');
709 assert_eq!(FbxProperty::F32Array(vec![]).type_code(), b'f');
710 }
711
712 #[test]
713 fn test_empty_mesh() {
714 let mut w = FbxBinaryWriter::new();
715 w.write_header().expect("header");
716 w.write_mesh("Empty", &[], &[], &[], &[]).expect("write_mesh");
717 let data = w.finish().expect("finish");
718 assert!(data.len() > 27);
719 }
720
721 #[test]
722 fn test_node_children() {
723 let mut parent = FbxNode::new("Parent");
724 let child = FbxNode::new("Child");
725 parent.add_child(child);
726 assert_eq!(parent.children.len(), 1);
727 assert_eq!(parent.children[0].name, "Child");
728 }
729
730 #[test]
731 fn test_finish_contains_footer_magic() {
732 let mut w = FbxBinaryWriter::new();
733 w.write_header().expect("header");
734 let data = w.finish().expect("finish");
735 let footer = &data[data.len() - 16..];
737 assert_eq!(footer[0], 0xf8);
738 assert_eq!(footer[1], 0x5a);
739 }
740
741 #[test]
742 fn test_default_trait() {
743 let w = FbxBinaryWriter::default();
744 assert!(w.output.is_empty());
745 }
746
747 fn minimal_mesh() -> MeshBuffers {
750 MeshBuffers {
751 positions: vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]],
752 normals: vec![[0.0, 0.0, 1.0]; 3],
753 tangents: vec![[1.0, 0.0, 0.0, 1.0]; 3],
754 uvs: vec![[0.0, 0.0], [1.0, 0.0], [0.0, 1.0]],
755 indices: vec![0, 1, 2],
756 colors: None,
757 has_suit: false,
758 }
759 }
760
761 #[test]
763 fn test_fbx_magic_bytes() {
764 let mesh = minimal_mesh();
765 let data = export_mesh_fbx_binary(&mesh).expect("export_mesh_fbx_binary failed");
766 assert_eq!(
767 &data[..23],
768 b"Kaydara FBX Binary \x00\x1a\x00",
769 "FBX magic header mismatch"
770 );
771 }
772
773 #[test]
777 fn test_zlib_array_round_trip() {
778 let data: Vec<f32> = (0..1000).map(|i| i as f32).collect();
779 let mut buf: Vec<u8> = Vec::new();
780 write_array_with_compression(&mut buf, &data, 4, |b, v: &f32| {
782 b.extend_from_slice(&v.to_le_bytes());
783 })
784 .expect("write_array_with_compression failed");
785
786 let encoding = u32::from_le_bytes(
788 buf[4..8]
789 .try_into()
790 .expect("encoding slice must be 4 bytes"),
791 );
792 assert_eq!(encoding, 1, "expected zlib encoding (1) for large array");
793
794 let compressed_len = u32::from_le_bytes(
796 buf[8..12]
797 .try_into()
798 .expect("compressed_len slice must be 4 bytes"),
799 ) as usize;
800 assert!(
801 compressed_len < data.len() * 4,
802 "compressed payload ({compressed_len} B) should be smaller than raw ({} B)",
803 data.len() * 4
804 );
805 }
806
807 #[test]
810 fn test_mesh_export_smoke() {
811 let mesh = minimal_mesh();
812 let result = export_mesh_fbx_binary(&mesh);
813 assert!(result.is_ok(), "export_mesh_fbx_binary returned error");
814 let bytes = result.expect("already checked above");
815 assert!(
816 bytes.len() > 27,
817 "exported FBX should be larger than the 27-byte header, got {} bytes",
818 bytes.len()
819 );
820 }
821
822 #[test]
823 fn test_mesh_then_skeleton() {
824 let mut w = FbxBinaryWriter::new();
825 w.write_header().expect("header");
826
827 let positions = vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]];
828 let triangles = vec![[0, 1, 2]];
829 w.write_mesh("Body", &positions, &[], &[], &triangles)
830 .expect("mesh");
831
832 let names = vec!["Root".to_string()];
833 let parents = vec![None];
834 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]];
835 w.write_skeleton(&names, &parents, &poses)
836 .expect("skeleton");
837
838 let data = w.finish().expect("finish");
839 assert!(data.len() > 300);
840 }
841}