Skip to main content

oxihuman_export/
bone_bind_pose_export.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3#![allow(dead_code)]
4
5//! Bone bind-pose export: rest-pose matrices and inverse bind matrices.
6
7/// A 4x4 column-major matrix.
8pub type Mat4 = [f32; 16];
9
10/// Bind pose data for a single bone.
11#[allow(dead_code)]
12#[derive(Debug, Clone)]
13pub struct BoneBindPose {
14    pub name: String,
15    pub local_matrix: Mat4,
16    pub world_matrix: Mat4,
17    pub inverse_bind: Mat4,
18}
19
20/// Bind pose export bundle.
21#[allow(dead_code)]
22#[derive(Debug, Clone)]
23pub struct BindPoseExport {
24    pub bones: Vec<BoneBindPose>,
25}
26
27fn identity_mat4() -> Mat4 {
28    [
29        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,
30    ]
31}
32
33/// Create a new bind pose export.
34#[allow(dead_code)]
35pub fn new_bind_pose_export() -> BindPoseExport {
36    BindPoseExport { bones: Vec::new() }
37}
38
39/// Add a bone with identity matrices.
40#[allow(dead_code)]
41pub fn add_bind_pose_bone(exp: &mut BindPoseExport, name: &str) {
42    exp.bones.push(BoneBindPose {
43        name: name.to_string(),
44        local_matrix: identity_mat4(),
45        world_matrix: identity_mat4(),
46        inverse_bind: identity_mat4(),
47    });
48}
49
50/// Set the local matrix of a bone.
51#[allow(dead_code)]
52pub fn set_local_matrix(exp: &mut BindPoseExport, idx: usize, mat: Mat4) {
53    if idx < exp.bones.len() {
54        exp.bones[idx].local_matrix = mat;
55    }
56}
57
58/// Bone count.
59#[allow(dead_code)]
60pub fn bind_bone_count(exp: &BindPoseExport) -> usize {
61    exp.bones.len()
62}
63
64/// Find bone by name.
65#[allow(dead_code)]
66pub fn find_bind_bone(exp: &BindPoseExport, name: &str) -> Option<usize> {
67    exp.bones.iter().position(|b| b.name == name)
68}
69
70/// Check all inverse bind matrices are non-identity (have been set).
71#[allow(dead_code)]
72pub fn all_inverse_binds_set(exp: &BindPoseExport) -> bool {
73    exp.bones
74        .iter()
75        .all(|b| b.inverse_bind != identity_mat4() || b.world_matrix == identity_mat4())
76}
77
78/// Serialise to JSON.
79#[allow(dead_code)]
80pub fn bind_pose_to_json(exp: &BindPoseExport) -> String {
81    format!("{{\"bone_count\":{}}}", bind_bone_count(exp))
82}
83
84/// Serialise to flat f32 array (world matrices).
85#[allow(dead_code)]
86pub fn bind_pose_to_flat(exp: &BindPoseExport) -> Vec<f32> {
87    exp.bones.iter().flat_map(|b| b.world_matrix).collect()
88}
89
90/// Validate: no empty names.
91#[allow(dead_code)]
92pub fn validate_bind_pose(exp: &BindPoseExport) -> bool {
93    exp.bones.iter().all(|b| !b.name.is_empty())
94}
95
96#[cfg(test)]
97mod tests {
98    use super::*;
99
100    #[test]
101    fn new_export_empty() {
102        let exp = new_bind_pose_export();
103        assert_eq!(bind_bone_count(&exp), 0);
104    }
105
106    #[test]
107    fn add_bone_increments() {
108        let mut exp = new_bind_pose_export();
109        add_bind_pose_bone(&mut exp, "root");
110        assert_eq!(bind_bone_count(&exp), 1);
111    }
112
113    #[test]
114    fn find_existing() {
115        let mut exp = new_bind_pose_export();
116        add_bind_pose_bone(&mut exp, "spine");
117        assert!(find_bind_bone(&exp, "spine").is_some_and(|i| i == 0));
118    }
119
120    #[test]
121    fn find_missing_none() {
122        let exp = new_bind_pose_export();
123        assert!(find_bind_bone(&exp, "missing").is_none());
124    }
125
126    #[test]
127    fn set_local_matrix_updates() {
128        let mut exp = new_bind_pose_export();
129        add_bind_pose_bone(&mut exp, "a");
130        let mut mat = identity_mat4();
131        mat[12] = 5.0;
132        set_local_matrix(&mut exp, 0, mat);
133        assert!((exp.bones[0].local_matrix[12] - 5.0).abs() < 1e-5);
134    }
135
136    #[test]
137    fn flat_output_length() {
138        let mut exp = new_bind_pose_export();
139        add_bind_pose_bone(&mut exp, "a");
140        add_bind_pose_bone(&mut exp, "b");
141        assert_eq!(bind_pose_to_flat(&exp).len(), 32);
142    }
143
144    #[test]
145    fn json_contains_bone_count() {
146        let exp = new_bind_pose_export();
147        let j = bind_pose_to_json(&exp);
148        assert!(j.contains("bone_count"));
149    }
150
151    #[test]
152    fn validate_valid() {
153        let mut exp = new_bind_pose_export();
154        add_bind_pose_bone(&mut exp, "hip");
155        assert!(validate_bind_pose(&exp));
156    }
157
158    #[test]
159    fn identity_mat4_diagonal_one() {
160        let m = identity_mat4();
161        assert!((m[0] - 1.0).abs() < 1e-6);
162        assert!((m[5] - 1.0).abs() < 1e-6);
163    }
164
165    #[test]
166    fn contains_range() {
167        let v = 0.5_f32;
168        assert!((0.0..=1.0).contains(&v));
169    }
170}