Skip to main content

oxihuman_export/
bone_custom_prop_export.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3#![allow(dead_code)]
4
5/// A custom property value.
6#[allow(dead_code)]
7#[derive(Clone)]
8pub enum BonePropValue {
9    Float(f32),
10    Int(i32),
11    Bool(bool),
12    Text(String),
13}
14
15/// A custom property entry on a bone.
16#[allow(dead_code)]
17pub struct BoneCustomProp {
18    pub bone_name: String,
19    pub key: String,
20    pub value: BonePropValue,
21}
22
23/// Export bundle for bone custom properties.
24#[allow(dead_code)]
25#[derive(Default)]
26pub struct BoneCustomPropExport {
27    pub props: Vec<BoneCustomProp>,
28}
29
30/// Create a new bone custom prop export.
31#[allow(dead_code)]
32pub fn new_bone_custom_prop_export() -> BoneCustomPropExport {
33    BoneCustomPropExport::default()
34}
35
36/// Add a float property.
37#[allow(dead_code)]
38pub fn add_float_prop(export: &mut BoneCustomPropExport, bone: &str, key: &str, val: f32) {
39    export.props.push(BoneCustomProp {
40        bone_name: bone.to_string(),
41        key: key.to_string(),
42        value: BonePropValue::Float(val),
43    });
44}
45
46/// Add a bool property.
47#[allow(dead_code)]
48pub fn add_bool_prop(export: &mut BoneCustomPropExport, bone: &str, key: &str, val: bool) {
49    export.props.push(BoneCustomProp {
50        bone_name: bone.to_string(),
51        key: key.to_string(),
52        value: BonePropValue::Bool(val),
53    });
54}
55
56/// Add a text property.
57#[allow(dead_code)]
58pub fn add_text_prop(export: &mut BoneCustomPropExport, bone: &str, key: &str, val: &str) {
59    export.props.push(BoneCustomProp {
60        bone_name: bone.to_string(),
61        key: key.to_string(),
62        value: BonePropValue::Text(val.to_string()),
63    });
64}
65
66/// Count total properties.
67#[allow(dead_code)]
68pub fn prop_count(export: &BoneCustomPropExport) -> usize {
69    export.props.len()
70}
71
72/// Find a property by bone name and key.
73#[allow(dead_code)]
74pub fn find_prop<'a>(
75    export: &'a BoneCustomPropExport,
76    bone: &str,
77    key: &str,
78) -> Option<&'a BoneCustomProp> {
79    export
80        .props
81        .iter()
82        .find(|p| p.bone_name == bone && p.key == key)
83}
84
85/// Props for a given bone.
86#[allow(dead_code)]
87pub fn props_for_bone<'a>(export: &'a BoneCustomPropExport, bone: &str) -> Vec<&'a BoneCustomProp> {
88    export
89        .props
90        .iter()
91        .filter(|p| p.bone_name == bone)
92        .collect()
93}
94
95/// Unique bone names with properties.
96#[allow(dead_code)]
97pub fn bone_names_with_props(export: &BoneCustomPropExport) -> Vec<String> {
98    let mut names: std::collections::HashSet<String> = std::collections::HashSet::new();
99    for p in &export.props {
100        names.insert(p.bone_name.clone());
101    }
102    let mut v: Vec<_> = names.into_iter().collect();
103    v.sort();
104    v
105}
106
107/// Serialize to JSON.
108#[allow(dead_code)]
109pub fn bone_custom_prop_to_json(export: &BoneCustomPropExport) -> String {
110    format!(r#"{{"custom_props":{}}}"#, export.props.len())
111}
112
113#[cfg(test)]
114mod tests {
115    use super::*;
116
117    #[test]
118    fn add_float_and_count() {
119        let mut e = new_bone_custom_prop_export();
120        add_float_prop(&mut e, "spine", "stiffness", 0.5);
121        assert_eq!(prop_count(&e), 1);
122    }
123
124    #[test]
125    fn find_prop_found() {
126        let mut e = new_bone_custom_prop_export();
127        add_float_prop(&mut e, "arm", "twist", 0.3);
128        assert!(find_prop(&e, "arm", "twist").is_some());
129    }
130
131    #[test]
132    fn find_prop_missing() {
133        let e = new_bone_custom_prop_export();
134        assert!(find_prop(&e, "arm", "twist").is_none());
135    }
136
137    #[test]
138    fn props_for_bone_filtered() {
139        let mut e = new_bone_custom_prop_export();
140        add_float_prop(&mut e, "arm", "a", 1.0);
141        add_float_prop(&mut e, "leg", "b", 2.0);
142        assert_eq!(props_for_bone(&e, "arm").len(), 1);
143    }
144
145    #[test]
146    fn bone_names_unique_sorted() {
147        let mut e = new_bone_custom_prop_export();
148        add_float_prop(&mut e, "b", "x", 1.0);
149        add_float_prop(&mut e, "a", "y", 1.0);
150        let names = bone_names_with_props(&e);
151        assert_eq!(names[0], "a");
152    }
153
154    #[test]
155    fn add_bool_prop_ok() {
156        let mut e = new_bone_custom_prop_export();
157        add_bool_prop(&mut e, "head", "ik_enabled", true);
158        assert_eq!(prop_count(&e), 1);
159    }
160
161    #[test]
162    fn add_text_prop_ok() {
163        let mut e = new_bone_custom_prop_export();
164        add_text_prop(&mut e, "root", "group", "physics");
165        assert!(find_prop(&e, "root", "group").is_some());
166    }
167
168    #[test]
169    fn json_has_count() {
170        let mut e = new_bone_custom_prop_export();
171        add_float_prop(&mut e, "x", "y", 1.0);
172        let j = bone_custom_prop_to_json(&e);
173        assert!(j.contains("\"custom_props\":1"));
174    }
175
176    #[test]
177    fn empty_export() {
178        let e = new_bone_custom_prop_export();
179        assert_eq!(prop_count(&e), 0);
180    }
181
182    #[test]
183    fn multiple_props_same_bone() {
184        let mut e = new_bone_custom_prop_export();
185        add_float_prop(&mut e, "arm", "a", 1.0);
186        add_float_prop(&mut e, "arm", "b", 2.0);
187        assert_eq!(props_for_bone(&e, "arm").len(), 2);
188    }
189}