1#[allow(dead_code)]
4pub struct ExportBone {
5 pub id: u32,
6 pub name: String,
7 pub parent_id: Option<u32>,
8 pub head: [f32; 3],
9 pub tail: [f32; 3],
10 pub rotation: [f32; 4],
11 pub length: f32,
12}
13
14#[allow(dead_code)]
15pub struct SkeletonExport {
16 pub name: String,
17 pub bones: Vec<ExportBone>,
18 pub frame_rate: f32,
19 pub frames: Vec<Vec<([f32; 3], [f32; 4])>>,
20}
21
22#[allow(dead_code)]
23pub fn new_skeleton_export(name: &str) -> SkeletonExport {
24 SkeletonExport {
25 name: name.to_string(),
26 bones: Vec::new(),
27 frame_rate: 30.0,
28 frames: Vec::new(),
29 }
30}
31
32#[allow(dead_code)]
33pub fn add_export_bone(skel: &mut SkeletonExport, bone: ExportBone) {
34 skel.bones.push(bone);
35}
36
37#[allow(dead_code)]
38pub fn add_skeleton_frame(skel: &mut SkeletonExport, poses: Vec<([f32; 3], [f32; 4])>) {
39 skel.frames.push(poses);
40}
41
42#[allow(dead_code)]
43pub fn skeleton_to_json(skel: &SkeletonExport) -> String {
44 let bone_strs: Vec<String> = skel
45 .bones
46 .iter()
47 .map(|b| {
48 let parent = match b.parent_id {
49 Some(p) => format!("{p}"),
50 None => "null".to_string(),
51 };
52 format!(
53 r#"{{"id":{},"name":"{}","parent_id":{},"head":[{},{},{}],"tail":[{},{},{}],"rotation":[{},{},{},{}],"length":{}}}"#,
54 b.id,
55 b.name,
56 parent,
57 b.head[0], b.head[1], b.head[2],
58 b.tail[0], b.tail[1], b.tail[2],
59 b.rotation[0], b.rotation[1], b.rotation[2], b.rotation[3],
60 b.length
61 )
62 })
63 .collect();
64
65 format!(
66 r#"{{"name":"{}","frame_rate":{},"bones":[{}]}}"#,
67 skel.name,
68 skel.frame_rate,
69 bone_strs.join(",")
70 )
71}
72
73#[allow(dead_code)]
74pub fn skeleton_to_bvh_stub(skel: &SkeletonExport) -> String {
75 let mut out = String::new();
76 out.push_str("HIERARCHY\n");
77 out.push_str("ROOT\n");
78 out.push_str("{\n");
79 for bone in &skel.bones {
80 out.push_str(&format!(
81 " JOINT {}\n {{\n OFFSET {} {} {}\n }}\n",
82 bone.name, bone.head[0], bone.head[1], bone.head[2]
83 ));
84 }
85 out.push_str("}\n");
86 out.push_str("MOTION\n");
87 out.push_str(&format!("Frames: {}\n", skel.frames.len()));
88 out.push_str(&format!("Frame Time: {}\n", 1.0 / skel.frame_rate.max(1.0)));
89 for frame in &skel.frames {
90 let values: Vec<String> = frame
91 .iter()
92 .flat_map(|(pos, rot)| {
93 vec![
94 format!("{}", pos[0]),
95 format!("{}", pos[1]),
96 format!("{}", pos[2]),
97 format!("{}", rot[0]),
98 format!("{}", rot[1]),
99 format!("{}", rot[2]),
100 format!("{}", rot[3]),
101 ]
102 })
103 .collect();
104 out.push_str(&values.join(" "));
105 out.push('\n');
106 }
107 out
108}
109
110#[allow(dead_code)]
111pub fn bone_count_export(skel: &SkeletonExport) -> usize {
112 skel.bones.len()
113}
114
115#[allow(dead_code)]
116pub fn frame_count(skel: &SkeletonExport) -> usize {
117 skel.frames.len()
118}
119
120#[allow(dead_code)]
121#[allow(clippy::needless_lifetimes)]
122pub fn get_export_bone<'a>(skel: &'a SkeletonExport, name: &str) -> Option<&'a ExportBone> {
123 skel.bones.iter().find(|b| b.name == name)
124}
125
126#[allow(dead_code)]
127pub fn root_bones(skel: &SkeletonExport) -> Vec<&ExportBone> {
128 skel.bones
129 .iter()
130 .filter(|b| b.parent_id.is_none())
131 .collect()
132}
133
134#[allow(dead_code)]
135pub fn child_bones(skel: &SkeletonExport, parent_id: u32) -> Vec<&ExportBone> {
136 skel.bones
137 .iter()
138 .filter(|b| b.parent_id == Some(parent_id))
139 .collect()
140}
141
142#[allow(dead_code)]
143pub fn bone_world_matrix(bone: &ExportBone) -> [[f32; 4]; 4] {
144 let [qx, qy, qz, qw] = bone.rotation;
145 let r00 = 1.0 - 2.0 * (qy * qy + qz * qz);
147 let r01 = 2.0 * (qx * qy - qz * qw);
148 let r02 = 2.0 * (qx * qz + qy * qw);
149 let r10 = 2.0 * (qx * qy + qz * qw);
150 let r11 = 1.0 - 2.0 * (qx * qx + qz * qz);
151 let r12 = 2.0 * (qy * qz - qx * qw);
152 let r20 = 2.0 * (qx * qz - qy * qw);
153 let r21 = 2.0 * (qy * qz + qx * qw);
154 let r22 = 1.0 - 2.0 * (qx * qx + qy * qy);
155 let [tx, ty, tz] = bone.head;
156 [
157 [r00, r01, r02, 0.0],
158 [r10, r11, r12, 0.0],
159 [r20, r21, r22, 0.0],
160 [tx, ty, tz, 1.0],
161 ]
162}
163
164#[allow(dead_code)]
165pub fn skeleton_duration(skel: &SkeletonExport) -> f32 {
166 frame_count(skel) as f32 / skel.frame_rate.max(f32::EPSILON)
167}
168
169#[allow(dead_code)]
170pub fn bind_pose_snapshot(skel: &SkeletonExport) -> Vec<([f32; 3], [f32; 4])> {
171 skel.bones.iter().map(|b| (b.head, b.rotation)).collect()
172}
173
174#[cfg(test)]
175mod tests {
176 use super::*;
177
178 fn make_bone(id: u32, name: &str, parent: Option<u32>) -> ExportBone {
179 ExportBone {
180 id,
181 name: name.to_string(),
182 parent_id: parent,
183 head: [0.0, 0.0, 0.0],
184 tail: [0.0, 1.0, 0.0],
185 rotation: [0.0, 0.0, 0.0, 1.0],
186 length: 1.0,
187 }
188 }
189
190 #[test]
191 fn test_new_skeleton_export() {
192 let skel = new_skeleton_export("human");
193 assert_eq!(skel.name, "human");
194 assert!(skel.bones.is_empty());
195 assert!(skel.frames.is_empty());
196 }
197
198 #[test]
199 fn test_add_export_bone() {
200 let mut skel = new_skeleton_export("s");
201 add_export_bone(&mut skel, make_bone(0, "root", None));
202 assert_eq!(bone_count_export(&skel), 1);
203 }
204
205 #[test]
206 fn test_add_skeleton_frame() {
207 let mut skel = new_skeleton_export("s");
208 add_export_bone(&mut skel, make_bone(0, "root", None));
209 let poses = vec![([0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 1.0])];
210 add_skeleton_frame(&mut skel, poses);
211 assert_eq!(frame_count(&skel), 1);
212 }
213
214 #[test]
215 fn test_bone_count_export() {
216 let mut skel = new_skeleton_export("s");
217 assert_eq!(bone_count_export(&skel), 0);
218 add_export_bone(&mut skel, make_bone(0, "root", None));
219 add_export_bone(&mut skel, make_bone(1, "spine", Some(0)));
220 assert_eq!(bone_count_export(&skel), 2);
221 }
222
223 #[test]
224 fn test_frame_count() {
225 let mut skel = new_skeleton_export("s");
226 assert_eq!(frame_count(&skel), 0);
227 add_skeleton_frame(&mut skel, vec![]);
228 add_skeleton_frame(&mut skel, vec![]);
229 assert_eq!(frame_count(&skel), 2);
230 }
231
232 #[test]
233 fn test_get_export_bone_by_name() {
234 let mut skel = new_skeleton_export("s");
235 add_export_bone(&mut skel, make_bone(0, "hips", None));
236 add_export_bone(&mut skel, make_bone(1, "spine", Some(0)));
237 let bone = get_export_bone(&skel, "spine");
238 assert!(bone.is_some());
239 assert_eq!(bone.expect("should succeed").id, 1);
240 }
241
242 #[test]
243 fn test_get_export_bone_not_found() {
244 let skel = new_skeleton_export("s");
245 assert!(get_export_bone(&skel, "missing").is_none());
246 }
247
248 #[test]
249 fn test_root_bones() {
250 let mut skel = new_skeleton_export("s");
251 add_export_bone(&mut skel, make_bone(0, "root", None));
252 add_export_bone(&mut skel, make_bone(1, "child", Some(0)));
253 add_export_bone(&mut skel, make_bone(2, "root2", None));
254 let roots = root_bones(&skel);
255 assert_eq!(roots.len(), 2);
256 }
257
258 #[test]
259 fn test_child_bones() {
260 let mut skel = new_skeleton_export("s");
261 add_export_bone(&mut skel, make_bone(0, "root", None));
262 add_export_bone(&mut skel, make_bone(1, "child1", Some(0)));
263 add_export_bone(&mut skel, make_bone(2, "child2", Some(0)));
264 add_export_bone(&mut skel, make_bone(3, "grandchild", Some(1)));
265 let children = child_bones(&skel, 0);
266 assert_eq!(children.len(), 2);
267 }
268
269 #[test]
270 fn test_skeleton_to_json_nonempty() {
271 let mut skel = new_skeleton_export("test_rig");
272 add_export_bone(&mut skel, make_bone(0, "root", None));
273 let json = skeleton_to_json(&skel);
274 assert!(!json.is_empty());
275 assert!(json.contains("test_rig"));
276 assert!(json.contains("root"));
277 }
278
279 #[test]
280 fn test_bvh_stub_contains_hierarchy() {
281 let skel = new_skeleton_export("s");
282 let bvh = skeleton_to_bvh_stub(&skel);
283 assert!(bvh.contains("HIERARCHY"));
284 assert!(bvh.contains("MOTION"));
285 }
286
287 #[test]
288 fn test_skeleton_duration() {
289 let mut skel = new_skeleton_export("s");
290 skel.frame_rate = 24.0;
291 add_skeleton_frame(&mut skel, vec![]);
292 add_skeleton_frame(&mut skel, vec![]);
293 add_skeleton_frame(&mut skel, vec![]);
294 let dur = skeleton_duration(&skel);
295 assert!((dur - 3.0 / 24.0).abs() < 1e-5);
296 }
297
298 #[test]
299 fn test_bind_pose_snapshot() {
300 let mut skel = new_skeleton_export("s");
301 add_export_bone(&mut skel, make_bone(0, "root", None));
302 add_export_bone(&mut skel, make_bone(1, "spine", Some(0)));
303 let snapshot = bind_pose_snapshot(&skel);
304 assert_eq!(snapshot.len(), 2);
305 }
306
307 #[test]
308 fn test_bone_world_matrix_identity() {
309 let bone = ExportBone {
310 id: 0,
311 name: "b".to_string(),
312 parent_id: None,
313 head: [0.0, 0.0, 0.0],
314 tail: [0.0, 1.0, 0.0],
315 rotation: [0.0, 0.0, 0.0, 1.0],
316 length: 1.0,
317 };
318 let m = bone_world_matrix(&bone);
319 assert!((m[0][0] - 1.0).abs() < 1e-5);
320 assert!((m[1][1] - 1.0).abs() < 1e-5);
321 assert!((m[2][2] - 1.0).abs() < 1e-5);
322 }
323
324 #[test]
325 fn test_skeleton_duration_zero_frames() {
326 let skel = new_skeleton_export("s");
327 let dur = skeleton_duration(&skel);
328 assert!((dur - 0.0).abs() < 1e-5);
329 }
330}