Skip to main content

oxihuman_export/
blend_tree_node_export.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3#![allow(dead_code)]
4
5//! Blend tree node export: serialise animator blend trees.
6
7/// Node type in a blend tree.
8#[allow(dead_code)]
9#[derive(Debug, Clone, PartialEq, Eq)]
10pub enum BlendNodeType {
11    Clip,
12    Blend1D,
13    Blend2D,
14    StateMachine,
15}
16
17/// A blend tree node.
18#[allow(dead_code)]
19#[derive(Debug, Clone)]
20pub struct BlendTreeNode {
21    pub id: u32,
22    pub name: String,
23    pub node_type: BlendNodeType,
24    pub children: Vec<u32>,
25    pub weight: f32,
26}
27
28/// Blend tree export container.
29#[allow(dead_code)]
30#[derive(Debug, Clone)]
31pub struct BlendTreeExport {
32    pub nodes: Vec<BlendTreeNode>,
33    pub root_id: Option<u32>,
34}
35
36/// Create an empty blend tree export.
37#[allow(dead_code)]
38pub fn new_blend_tree_export() -> BlendTreeExport {
39    BlendTreeExport {
40        nodes: Vec::new(),
41        root_id: None,
42    }
43}
44
45/// Add a node.
46#[allow(dead_code)]
47pub fn add_blend_node(exp: &mut BlendTreeExport, node: BlendTreeNode) {
48    exp.nodes.push(node);
49}
50
51/// Set root.
52#[allow(dead_code)]
53pub fn set_blend_root(exp: &mut BlendTreeExport, id: u32) {
54    exp.root_id = Some(id);
55}
56
57/// Node count.
58#[allow(dead_code)]
59pub fn blend_node_count(exp: &BlendTreeExport) -> usize {
60    exp.nodes.len()
61}
62
63/// Find node by id.
64#[allow(dead_code)]
65pub fn find_blend_node(exp: &BlendTreeExport, id: u32) -> Option<&BlendTreeNode> {
66    exp.nodes.iter().find(|n| n.id == id)
67}
68
69/// Total child connections.
70#[allow(dead_code)]
71pub fn total_connections(exp: &BlendTreeExport) -> usize {
72    exp.nodes.iter().map(|n| n.children.len()).sum()
73}
74
75/// Nodes of a given type.
76#[allow(dead_code)]
77pub fn nodes_of_type<'a>(exp: &'a BlendTreeExport, t: &BlendNodeType) -> Vec<&'a BlendTreeNode> {
78    exp.nodes.iter().filter(|n| &n.node_type == t).collect()
79}
80
81/// Serialise to JSON.
82#[allow(dead_code)]
83pub fn blend_tree_to_json(exp: &BlendTreeExport) -> String {
84    format!(
85        "{{\"node_count\":{},\"root_id\":{}}}",
86        blend_node_count(exp),
87        exp.root_id.map_or("null".to_string(), |id| id.to_string())
88    )
89}
90
91/// Validate: all child ids exist.
92#[allow(dead_code)]
93pub fn validate_blend_tree(exp: &BlendTreeExport) -> bool {
94    let ids: std::collections::HashSet<u32> = exp.nodes.iter().map(|n| n.id).collect();
95    exp.nodes
96        .iter()
97        .all(|n| n.children.iter().all(|&c| ids.contains(&c)))
98}
99
100#[cfg(test)]
101mod tests {
102    use super::*;
103
104    fn clip_node(id: u32, name: &str) -> BlendTreeNode {
105        BlendTreeNode {
106            id,
107            name: name.to_string(),
108            node_type: BlendNodeType::Clip,
109            children: Vec::new(),
110            weight: 1.0,
111        }
112    }
113
114    #[test]
115    fn new_export_empty() {
116        let exp = new_blend_tree_export();
117        assert_eq!(blend_node_count(&exp), 0);
118    }
119
120    #[test]
121    fn add_node_increments() {
122        let mut exp = new_blend_tree_export();
123        add_blend_node(&mut exp, clip_node(0, "idle"));
124        assert_eq!(blend_node_count(&exp), 1);
125    }
126
127    #[test]
128    fn find_existing() {
129        let mut exp = new_blend_tree_export();
130        add_blend_node(&mut exp, clip_node(5, "run"));
131        assert!(find_blend_node(&exp, 5).is_some());
132    }
133
134    #[test]
135    fn find_missing_none() {
136        let exp = new_blend_tree_export();
137        assert!(find_blend_node(&exp, 99).is_none());
138    }
139
140    #[test]
141    fn set_root() {
142        let mut exp = new_blend_tree_export();
143        add_blend_node(&mut exp, clip_node(0, "root"));
144        set_blend_root(&mut exp, 0);
145        assert!(exp.root_id.is_some_and(|id| id == 0));
146    }
147
148    #[test]
149    fn total_connections_none() {
150        let mut exp = new_blend_tree_export();
151        add_blend_node(&mut exp, clip_node(0, "a"));
152        assert_eq!(total_connections(&exp), 0);
153    }
154
155    #[test]
156    fn nodes_of_type_filter() {
157        let mut exp = new_blend_tree_export();
158        add_blend_node(&mut exp, clip_node(0, "a"));
159        add_blend_node(
160            &mut exp,
161            BlendTreeNode {
162                id: 1,
163                name: "b1d".to_string(),
164                node_type: BlendNodeType::Blend1D,
165                children: vec![0],
166                weight: 0.5,
167            },
168        );
169        assert_eq!(nodes_of_type(&exp, &BlendNodeType::Clip).len(), 1);
170    }
171
172    #[test]
173    fn validate_no_dangling_children() {
174        let mut exp = new_blend_tree_export();
175        add_blend_node(&mut exp, clip_node(0, "a"));
176        add_blend_node(&mut exp, clip_node(1, "b"));
177        assert!(validate_blend_tree(&exp));
178    }
179
180    #[test]
181    fn json_contains_node_count() {
182        let exp = new_blend_tree_export();
183        let j = blend_tree_to_json(&exp);
184        assert!(j.contains("node_count"));
185    }
186
187    #[test]
188    fn weight_in_range() {
189        let n = clip_node(0, "t");
190        assert!((0.0..=1.0).contains(&n.weight));
191    }
192}