1#[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}