Skip to main content

oxihuman_export/
fbx.rs

1//! FBX format export stub (ASCII FBX 7.4 compatible header + geometry).
2
3#[allow(dead_code)]
4pub struct FbxNode {
5    pub name: String,
6    pub id: u64,
7    pub parent_id: Option<u64>,
8    pub transform: [[f32; 4]; 4],
9}
10
11#[allow(dead_code)]
12pub struct FbxMesh {
13    pub node_id: u64,
14    pub positions: Vec<[f32; 3]>,
15    pub normals: Vec<[f32; 3]>,
16    pub uvs: Vec<[f32; 2]>,
17    pub indices: Vec<u32>,
18}
19
20#[allow(dead_code)]
21pub struct FbxScene {
22    pub name: String,
23    pub nodes: Vec<FbxNode>,
24    pub meshes: Vec<FbxMesh>,
25    pub up_axis: u8,
26    pub units: f32,
27}
28
29#[allow(dead_code)]
30pub struct FbxExport {
31    pub content: String,
32    pub version: u32,
33}
34
35#[allow(dead_code)]
36pub fn new_fbx_scene(name: &str) -> FbxScene {
37    FbxScene {
38        name: name.to_string(),
39        nodes: Vec::new(),
40        meshes: Vec::new(),
41        up_axis: 1,
42        units: 1.0,
43    }
44}
45
46#[allow(dead_code)]
47pub fn add_fbx_node(scene: &mut FbxScene, name: &str, parent: Option<u64>) -> u64 {
48    let id = scene.nodes.len() as u64 + 1;
49    scene.nodes.push(FbxNode {
50        name: name.to_string(),
51        id,
52        parent_id: parent,
53        transform: fbx_identity_matrix(),
54    });
55    id
56}
57
58#[allow(dead_code)]
59pub fn add_fbx_mesh(scene: &mut FbxScene, mesh: FbxMesh) {
60    scene.meshes.push(mesh);
61}
62
63#[allow(dead_code)]
64pub fn fbx_identity_matrix() -> [[f32; 4]; 4] {
65    [
66        [1.0, 0.0, 0.0, 0.0],
67        [0.0, 1.0, 0.0, 0.0],
68        [0.0, 0.0, 1.0, 0.0],
69        [0.0, 0.0, 0.0, 1.0],
70    ]
71}
72
73#[allow(dead_code)]
74pub fn fbx_header(version: u32) -> String {
75    let major = version / 1000;
76    let minor = (version % 1000) / 100;
77    let patch = version % 100;
78    format!(
79        "; FBX {major}.{minor}.{patch} project file\n\
80         ; Copyright (C) 1997-2023 Autodesk Inc. and/or its licensors.\n\
81         ; All Rights Reserved.\n\
82         \n\
83         FBXHeaderExtension: {{\n\
84         \tFBXHeaderVersion: 1003\n\
85         \tFBXVersion: {version}\n\
86         }}\n"
87    )
88}
89
90#[allow(dead_code)]
91pub fn fbx_node_to_string(node: &FbxNode) -> String {
92    let parent_str = match node.parent_id {
93        Some(pid) => format!("\tParentId: {pid}\n"),
94        None => String::new(),
95    };
96    let t = &node.transform;
97    format!(
98        "Model: {id}, \"{name}\", \"Mesh\" {{\n\
99         {parent_str}\
100         \tTransform: {r0:?}, {r1:?}, {r2:?}, {r3:?}\n\
101         }}\n",
102        id = node.id,
103        name = node.name,
104        r0 = t[0],
105        r1 = t[1],
106        r2 = t[2],
107        r3 = t[3],
108    )
109}
110
111#[allow(dead_code)]
112pub fn fbx_mesh_to_string(mesh: &FbxMesh) -> String {
113    let vert_count = mesh.positions.len();
114    let mut s = format!(
115        "Geometry: {id}, \"Geometry::\", \"Mesh\" {{\n\
116         \tVertices: *{vert_count} {{\n",
117        id = mesh.node_id,
118    );
119    s.push_str("\t\ta: ");
120    let coords: Vec<String> = mesh
121        .positions
122        .iter()
123        .map(|p| format!("{},{},{}", p[0], p[1], p[2]))
124        .collect();
125    s.push_str(&coords.join(","));
126    s.push('\n');
127    s.push_str("\t}\n");
128
129    let idx_count = mesh.indices.len();
130    s.push_str(&format!("\tPolygonVertexIndex: *{idx_count} {{\n\t\ta: "));
131    let idx_strs: Vec<String> = mesh
132        .indices
133        .chunks(3)
134        .flat_map(|tri| {
135            if tri.len() == 3 {
136                vec![
137                    tri[0].to_string(),
138                    tri[1].to_string(),
139                    format!("-{}", tri[2] + 1),
140                ]
141            } else {
142                tri.iter().map(|v| v.to_string()).collect()
143            }
144        })
145        .collect();
146    s.push_str(&idx_strs.join(","));
147    s.push_str("\n\t}\n}\n");
148    s
149}
150
151#[allow(dead_code)]
152pub fn fbx_connections(scene: &FbxScene) -> String {
153    let mut s = "Connections: {\n".to_string();
154    for node in &scene.nodes {
155        let parent = node.parent_id.unwrap_or(0);
156        s.push_str(&format!("\tC: \"OO\", {}, {}\n", node.id, parent));
157    }
158    for mesh in &scene.meshes {
159        s.push_str(&format!(
160            "\tC: \"OO\", Geo_{}, {}\n",
161            mesh.node_id, mesh.node_id
162        ));
163    }
164    s.push_str("}\n");
165    s
166}
167
168#[allow(dead_code)]
169#[deprecated(
170    since = "0.1.1",
171    note = "ASCII FBX export is superseded by the binary writer; \
172            use fbx_binary::export_mesh_fbx_binary instead"
173)]
174pub fn export_fbx_ascii(scene: &FbxScene) -> FbxExport {
175    let version = 7400u32;
176    let mut content = fbx_header(version);
177
178    content.push_str("\nObjects: {\n");
179    for node in &scene.nodes {
180        content.push_str(&fbx_node_to_string(node));
181    }
182    for mesh in &scene.meshes {
183        content.push_str(&fbx_mesh_to_string(mesh));
184    }
185    content.push_str("}\n\n");
186    content.push_str(&fbx_connections(scene));
187
188    FbxExport { content, version }
189}
190
191#[allow(dead_code)]
192pub fn node_count_fbx(scene: &FbxScene) -> usize {
193    scene.nodes.len()
194}
195
196#[allow(dead_code)]
197pub fn mesh_count_fbx(scene: &FbxScene) -> usize {
198    scene.meshes.len()
199}
200
201#[allow(dead_code)]
202pub fn validate_fbx_scene(scene: &FbxScene) -> Vec<String> {
203    let mut issues = Vec::new();
204    if scene.name.is_empty() {
205        issues.push("Scene name is empty".to_string());
206    }
207    let node_ids: Vec<u64> = scene.nodes.iter().map(|n| n.id).collect();
208    for node in &scene.nodes {
209        if let Some(pid) = node.parent_id {
210            if !node_ids.contains(&pid) {
211                issues.push(format!(
212                    "Node '{}' references non-existent parent id {pid}",
213                    node.name
214                ));
215            }
216        }
217    }
218    for mesh in &scene.meshes {
219        if mesh.positions.is_empty() {
220            issues.push(format!("Mesh for node {} has no positions", mesh.node_id));
221        }
222        if mesh.indices.len() % 3 != 0 {
223            issues.push(format!(
224                "Mesh for node {} has non-triangulated index count {}",
225                mesh.node_id,
226                mesh.indices.len()
227            ));
228        }
229    }
230    if scene.units <= 0.0 {
231        issues.push("Scene units must be positive".to_string());
232    }
233    issues
234}
235
236#[allow(dead_code)]
237pub fn fbx_export_size_estimate(export: &FbxExport) -> usize {
238    export.content.len()
239}
240
241#[cfg(test)]
242#[allow(deprecated)]
243mod tests {
244    use super::*;
245
246    #[test]
247    fn test_new_fbx_scene() {
248        let scene = new_fbx_scene("TestScene");
249        assert_eq!(scene.name, "TestScene");
250        assert!(scene.nodes.is_empty());
251        assert!(scene.meshes.is_empty());
252        assert_eq!(scene.up_axis, 1);
253        assert!((scene.units - 1.0).abs() < 1e-6);
254    }
255
256    #[test]
257    fn test_add_fbx_node_no_parent() {
258        let mut scene = new_fbx_scene("S");
259        let id = add_fbx_node(&mut scene, "Root", None);
260        assert_eq!(id, 1);
261        assert_eq!(scene.nodes.len(), 1);
262        assert_eq!(scene.nodes[0].name, "Root");
263        assert!(scene.nodes[0].parent_id.is_none());
264    }
265
266    #[test]
267    fn test_add_fbx_node_with_parent() {
268        let mut scene = new_fbx_scene("S");
269        let root = add_fbx_node(&mut scene, "Root", None);
270        let child = add_fbx_node(&mut scene, "Child", Some(root));
271        assert_eq!(child, 2);
272        assert_eq!(scene.nodes[1].parent_id, Some(root));
273    }
274
275    #[test]
276    fn test_add_fbx_mesh() {
277        let mut scene = new_fbx_scene("S");
278        let mesh = FbxMesh {
279            node_id: 1,
280            positions: vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]],
281            normals: vec![[0.0, 0.0, 1.0]; 3],
282            uvs: vec![[0.0, 0.0]; 3],
283            indices: vec![0, 1, 2],
284        };
285        add_fbx_mesh(&mut scene, mesh);
286        assert_eq!(scene.meshes.len(), 1);
287    }
288
289    #[test]
290    fn test_fbx_header_contains_fbx() {
291        let header = fbx_header(7400);
292        assert!(header.contains("FBX"));
293        assert!(header.contains("7400"));
294    }
295
296    #[test]
297    fn test_fbx_identity_matrix() {
298        let m = fbx_identity_matrix();
299        for (i, row) in m.iter().enumerate() {
300            for (j, val) in row.iter().enumerate() {
301                let expected = if i == j { 1.0f32 } else { 0.0f32 };
302                assert!((val - expected).abs() < 1e-6, "m[{i}][{j}] = {val}");
303            }
304        }
305    }
306
307    #[test]
308    fn test_export_fbx_ascii_non_empty() {
309        let mut scene = new_fbx_scene("Test");
310        add_fbx_node(&mut scene, "Root", None);
311        let export = export_fbx_ascii(&scene);
312        assert!(!export.content.is_empty());
313        assert_eq!(export.version, 7400);
314    }
315
316    #[test]
317    fn test_node_count_fbx() {
318        let mut scene = new_fbx_scene("S");
319        assert_eq!(node_count_fbx(&scene), 0);
320        add_fbx_node(&mut scene, "A", None);
321        add_fbx_node(&mut scene, "B", None);
322        assert_eq!(node_count_fbx(&scene), 2);
323    }
324
325    #[test]
326    fn test_mesh_count_fbx() {
327        let mut scene = new_fbx_scene("S");
328        assert_eq!(mesh_count_fbx(&scene), 0);
329        let mesh = FbxMesh {
330            node_id: 1,
331            positions: vec![[0.0; 3]],
332            normals: vec![],
333            uvs: vec![],
334            indices: vec![],
335        };
336        add_fbx_mesh(&mut scene, mesh);
337        assert_eq!(mesh_count_fbx(&scene), 1);
338    }
339
340    #[test]
341    fn test_validate_fbx_scene_passes() {
342        let mut scene = new_fbx_scene("Valid");
343        add_fbx_node(&mut scene, "Root", None);
344        let issues = validate_fbx_scene(&scene);
345        assert!(issues.is_empty(), "Expected no issues, got: {issues:?}");
346    }
347
348    #[test]
349    fn test_validate_fbx_scene_empty_name() {
350        let scene = new_fbx_scene("");
351        let issues = validate_fbx_scene(&scene);
352        assert!(!issues.is_empty());
353    }
354
355    #[test]
356    fn test_fbx_mesh_to_string_contains_vertex_count() {
357        let mesh = FbxMesh {
358            node_id: 42,
359            positions: vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]],
360            normals: vec![],
361            uvs: vec![],
362            indices: vec![0, 1, 2],
363        };
364        let s = fbx_mesh_to_string(&mesh);
365        assert!(s.contains("*3"), "Expected vertex count 3 in: {s}");
366    }
367
368    #[test]
369    fn test_fbx_connections_non_empty() {
370        let mut scene = new_fbx_scene("S");
371        add_fbx_node(&mut scene, "Root", None);
372        let conn = fbx_connections(&scene);
373        assert!(conn.contains("Connections:"));
374        assert!(conn.contains("OO"));
375    }
376
377    #[test]
378    fn test_fbx_export_size_estimate() {
379        let mut scene = new_fbx_scene("S");
380        add_fbx_node(&mut scene, "Root", None);
381        let export = export_fbx_ascii(&scene);
382        let size = fbx_export_size_estimate(&export);
383        assert_eq!(size, export.content.len());
384        assert!(size > 0);
385    }
386
387    #[test]
388    fn test_fbx_node_to_string() {
389        let node = FbxNode {
390            name: "TestNode".to_string(),
391            id: 100,
392            parent_id: None,
393            transform: fbx_identity_matrix(),
394        };
395        let s = fbx_node_to_string(&node);
396        assert!(s.contains("TestNode"));
397        assert!(s.contains("100"));
398    }
399
400    #[test]
401    fn test_validate_fbx_bad_parent() {
402        let mut scene = new_fbx_scene("S");
403        scene.nodes.push(FbxNode {
404            name: "Orphan".to_string(),
405            id: 1,
406            parent_id: Some(999),
407            transform: fbx_identity_matrix(),
408        });
409        let issues = validate_fbx_scene(&scene);
410        assert!(!issues.is_empty());
411    }
412}