use std::fmt::Write as FmtWrite;
#[allow(dead_code)]
#[derive(Debug, Clone)]
pub struct FbxExportConfig {
pub version: String,
pub up_axis: String,
pub unit_scale: f32,
pub embed_textures: bool,
}
#[allow(dead_code)]
#[derive(Debug, Clone, PartialEq)]
pub enum FbxNodeKind {
Mesh,
Material,
Bone,
Null,
}
#[allow(dead_code)]
#[derive(Debug, Clone)]
pub struct FbxNode {
pub id: u32,
pub name: String,
pub kind: FbxNodeKind,
pub parent_id: Option<u32>,
pub vertex_count: u32,
pub face_count: u32,
pub color: [f32; 3],
}
#[allow(dead_code)]
#[derive(Debug, Clone)]
pub struct FbxDocument {
pub config: FbxExportConfig,
pub scene_name: String,
pub nodes: Vec<FbxNode>,
next_id: u32,
}
#[allow(dead_code)]
pub fn default_fbx_export_config() -> FbxExportConfig {
FbxExportConfig {
version: "7.4.0".to_string(),
up_axis: "Y".to_string(),
unit_scale: 1.0,
embed_textures: false,
}
}
#[allow(dead_code)]
pub fn new_fbx_document(cfg: &FbxExportConfig) -> FbxDocument {
FbxDocument {
config: cfg.clone(),
scene_name: "Scene".to_string(),
nodes: Vec::new(),
next_id: 1,
}
}
#[allow(dead_code)]
pub fn fbx_add_mesh_node(
doc: &mut FbxDocument,
name: &str,
vertex_count: u32,
face_count: u32,
) -> u32 {
let id = doc.next_id;
doc.next_id += 1;
doc.nodes.push(FbxNode {
id,
name: name.to_string(),
kind: FbxNodeKind::Mesh,
parent_id: None,
vertex_count,
face_count,
color: [1.0, 1.0, 1.0],
});
id
}
#[allow(dead_code)]
pub fn fbx_add_material_node(
doc: &mut FbxDocument,
name: &str,
color: [f32; 3],
) -> u32 {
let id = doc.next_id;
doc.next_id += 1;
doc.nodes.push(FbxNode {
id,
name: name.to_string(),
kind: FbxNodeKind::Material,
parent_id: None,
vertex_count: 0,
face_count: 0,
color,
});
id
}
#[allow(dead_code)]
pub fn fbx_add_bone_node(
doc: &mut FbxDocument,
name: &str,
parent_id: Option<u32>,
) -> u32 {
let id = doc.next_id;
doc.next_id += 1;
doc.nodes.push(FbxNode {
id,
name: name.to_string(),
kind: FbxNodeKind::Bone,
parent_id,
vertex_count: 0,
face_count: 0,
color: [0.8, 0.8, 0.8],
});
id
}
#[allow(dead_code)]
pub fn fbx_to_string(doc: &FbxDocument) -> String {
let mut out = String::new();
let _ = writeln!(
out,
"; FBX {ver} project file\n; Creator: OxiHuman FBX Exporter\n; Scene: {scene}",
ver = doc.config.version,
scene = doc.scene_name,
);
out.push_str("\nObjects: {\n");
for node in &doc.nodes {
let kind_str = match node.kind {
FbxNodeKind::Mesh => "Mesh",
FbxNodeKind::Material => "Material",
FbxNodeKind::Bone => "LimbNode",
FbxNodeKind::Null => "Null",
};
let _ = writeln!(
out,
" NodeAttribute: {id}, \"{name}\", \"{kind}\" {{",
id = node.id,
name = node.name,
kind = kind_str,
);
if node.kind == FbxNodeKind::Mesh {
let _ = writeln!(out, " VertexCount: {}", node.vertex_count);
let _ = writeln!(out, " FaceCount: {}", node.face_count);
}
if node.kind == FbxNodeKind::Material {
let _ = writeln!(
out,
" DiffuseColor: {:.3},{:.3},{:.3}",
node.color[0], node.color[1], node.color[2]
);
}
if let Some(pid) = node.parent_id {
let _ = writeln!(out, " Parent: {pid}");
}
out.push_str(" }\n");
}
out.push_str("}\n");
out
}
#[allow(dead_code)]
pub fn fbx_write_to_file(doc: &FbxDocument, path: &str) -> Result<(), String> {
let content = fbx_to_string(doc);
std::fs::write(path, content).map_err(|e| e.to_string())
}
#[allow(dead_code)]
pub fn fbx_node_count(doc: &FbxDocument) -> usize {
doc.nodes.len()
}
#[allow(dead_code)]
pub fn fbx_set_scene_name(doc: &mut FbxDocument, name: &str) {
doc.scene_name = name.to_string();
}
#[allow(dead_code)]
pub fn fbx_document_clear(doc: &mut FbxDocument) {
doc.nodes.clear();
doc.next_id = 1;
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_default_config_version() {
let cfg = default_fbx_export_config();
assert_eq!(cfg.version, "7.4.0");
assert_eq!(cfg.up_axis, "Y");
}
#[test]
fn test_new_document_empty() {
let cfg = default_fbx_export_config();
let doc = new_fbx_document(&cfg);
assert_eq!(fbx_node_count(&doc), 0);
assert_eq!(doc.scene_name, "Scene");
}
#[test]
fn test_add_mesh_node() {
let cfg = default_fbx_export_config();
let mut doc = new_fbx_document(&cfg);
let id = fbx_add_mesh_node(&mut doc, "Body", 1000, 500);
assert_eq!(id, 1);
assert_eq!(fbx_node_count(&doc), 1);
assert_eq!(doc.nodes[0].kind, FbxNodeKind::Mesh);
assert_eq!(doc.nodes[0].vertex_count, 1000);
}
#[test]
fn test_add_material_node() {
let cfg = default_fbx_export_config();
let mut doc = new_fbx_document(&cfg);
let id = fbx_add_material_node(&mut doc, "Skin", [0.9, 0.7, 0.6]);
assert_eq!(id, 1);
assert_eq!(doc.nodes[0].kind, FbxNodeKind::Material);
}
#[test]
fn test_add_bone_node_hierarchy() {
let cfg = default_fbx_export_config();
let mut doc = new_fbx_document(&cfg);
let root = fbx_add_bone_node(&mut doc, "Hips", None);
let child = fbx_add_bone_node(&mut doc, "Spine", Some(root));
assert_eq!(root, 1);
assert_eq!(child, 2);
assert_eq!(doc.nodes[1].parent_id, Some(1));
}
#[test]
fn test_fbx_to_string_contains_objects() {
let cfg = default_fbx_export_config();
let mut doc = new_fbx_document(&cfg);
fbx_add_mesh_node(&mut doc, "Mesh1", 10, 4);
let s = fbx_to_string(&doc);
assert!(s.contains("Objects:"));
assert!(s.contains("Mesh1"));
}
#[test]
fn test_set_scene_name() {
let cfg = default_fbx_export_config();
let mut doc = new_fbx_document(&cfg);
fbx_set_scene_name(&mut doc, "CharacterScene");
assert_eq!(doc.scene_name, "CharacterScene");
let s = fbx_to_string(&doc);
assert!(s.contains("CharacterScene"));
}
#[test]
fn test_document_clear() {
let cfg = default_fbx_export_config();
let mut doc = new_fbx_document(&cfg);
fbx_add_mesh_node(&mut doc, "A", 0, 0);
fbx_document_clear(&mut doc);
assert_eq!(fbx_node_count(&doc), 0);
let id = fbx_add_bone_node(&mut doc, "Root", None);
assert_eq!(id, 1);
}
#[test]
fn test_node_ids_are_sequential() {
let cfg = default_fbx_export_config();
let mut doc = new_fbx_document(&cfg);
let id1 = fbx_add_mesh_node(&mut doc, "M1", 0, 0);
let id2 = fbx_add_material_node(&mut doc, "Mat1", [1.0, 1.0, 1.0]);
let id3 = fbx_add_bone_node(&mut doc, "Bone1", None);
assert_eq!((id1, id2, id3), (1, 2, 3));
}
}