Skip to main content

oxihuman_morph/
blend_shape_graph.rs

1//! DAG-based blend shape evaluation graph.
2//! Supports Add, Multiply, Override, and Screen blend operations.
3
4#[allow(dead_code)]
5#[derive(Clone)]
6pub enum BlendOp {
7    Add,
8    Multiply,
9    Override,
10    Screen,
11}
12
13#[allow(dead_code)]
14#[derive(Clone)]
15pub struct BlendNode {
16    pub id: u32,
17    pub name: String,
18    pub weight: f32,
19    pub op: BlendOp,
20    pub inputs: Vec<u32>,
21    pub target_name: Option<String>,
22}
23
24#[allow(dead_code)]
25pub struct BlendGraph {
26    pub nodes: Vec<BlendNode>,
27    pub roots: Vec<u32>,
28    pub next_id: u32,
29}
30
31#[allow(dead_code)]
32pub struct EvalResult {
33    pub target_weights: Vec<(String, f32)>,
34}
35
36#[allow(dead_code)]
37pub fn new_blend_graph() -> BlendGraph {
38    BlendGraph {
39        nodes: Vec::new(),
40        roots: Vec::new(),
41        next_id: 0,
42    }
43}
44
45#[allow(dead_code)]
46pub fn add_blend_node(graph: &mut BlendGraph, name: &str, op: BlendOp, weight: f32) -> u32 {
47    let id = graph.next_id;
48    graph.next_id += 1;
49    graph.nodes.push(BlendNode {
50        id,
51        name: name.to_string(),
52        weight,
53        op,
54        inputs: Vec::new(),
55        target_name: None,
56    });
57    id
58}
59
60#[allow(dead_code)]
61pub fn add_leaf_node(graph: &mut BlendGraph, target: &str, weight: f32) -> u32 {
62    let id = graph.next_id;
63    graph.next_id += 1;
64    graph.nodes.push(BlendNode {
65        id,
66        name: target.to_string(),
67        weight,
68        op: BlendOp::Add,
69        inputs: Vec::new(),
70        target_name: Some(target.to_string()),
71    });
72    id
73}
74
75#[allow(dead_code)]
76pub fn connect_nodes(graph: &mut BlendGraph, parent: u32, child: u32) {
77    if let Some(node) = graph.nodes.iter_mut().find(|n| n.id == parent) {
78        if !node.inputs.contains(&child) {
79            node.inputs.push(child);
80        }
81    }
82}
83
84#[allow(dead_code)]
85pub fn add_root(graph: &mut BlendGraph, node_id: u32) {
86    if !graph.roots.contains(&node_id) {
87        graph.roots.push(node_id);
88    }
89}
90
91/// Recursively evaluate a node and accumulate target weights.
92fn eval_node(
93    graph: &BlendGraph,
94    node_id: u32,
95    parent_weight: f32,
96    results: &mut Vec<(String, f32)>,
97    visited: &mut Vec<u32>,
98) {
99    if visited.contains(&node_id) {
100        return;
101    }
102    visited.push(node_id);
103
104    let node = match graph.nodes.iter().find(|n| n.id == node_id) {
105        Some(n) => n.clone(),
106        None => return,
107    };
108
109    let effective_weight = match node.op {
110        BlendOp::Add => parent_weight + node.weight,
111        BlendOp::Multiply => parent_weight * node.weight,
112        BlendOp::Override => node.weight,
113        BlendOp::Screen => 1.0 - (1.0 - parent_weight) * (1.0 - node.weight),
114    };
115
116    if let Some(ref target) = node.target_name {
117        // leaf node
118        if let Some(entry) = results.iter_mut().find(|(name, _)| name == target) {
119            entry.1 += effective_weight;
120        } else {
121            results.push((target.clone(), effective_weight));
122        }
123    }
124
125    for child_id in &node.inputs {
126        eval_node(graph, *child_id, effective_weight, results, visited);
127    }
128}
129
130/// DFS from all roots, accumulate weights per target.
131#[allow(dead_code)]
132pub fn evaluate_graph(graph: &BlendGraph) -> EvalResult {
133    let mut results: Vec<(String, f32)> = Vec::new();
134    let mut visited = Vec::new();
135
136    for &root_id in &graph.roots {
137        eval_node(graph, root_id, 1.0, &mut results, &mut visited);
138    }
139
140    EvalResult {
141        target_weights: results,
142    }
143}
144
145#[allow(dead_code)]
146pub fn set_node_weight(graph: &mut BlendGraph, id: u32, weight: f32) {
147    if let Some(node) = graph.nodes.iter_mut().find(|n| n.id == id) {
148        node.weight = weight;
149    }
150}
151
152#[allow(dead_code)]
153pub fn get_node(graph: &BlendGraph, id: u32) -> Option<&BlendNode> {
154    graph.nodes.iter().find(|n| n.id == id)
155}
156
157#[allow(dead_code)]
158pub fn node_count(graph: &BlendGraph) -> usize {
159    graph.nodes.len()
160}
161
162#[allow(dead_code)]
163pub fn leaf_nodes(graph: &BlendGraph) -> Vec<&BlendNode> {
164    graph
165        .nodes
166        .iter()
167        .filter(|n| n.target_name.is_some())
168        .collect()
169}
170
171/// Topological sort using DFS post-order (Kahn's-style via recursion).
172fn topo_visit(graph: &BlendGraph, node_id: u32, visited: &mut Vec<u32>, order: &mut Vec<u32>) {
173    if visited.contains(&node_id) {
174        return;
175    }
176    visited.push(node_id);
177    if let Some(node) = graph.nodes.iter().find(|n| n.id == node_id) {
178        for &child in &node.inputs {
179            topo_visit(graph, child, visited, order);
180        }
181    }
182    order.push(node_id);
183}
184
185/// Returns node IDs in evaluation order (roots first, leaves last via reverse post-order).
186#[allow(dead_code)]
187pub fn topological_sort_graph(graph: &BlendGraph) -> Vec<u32> {
188    let mut visited = Vec::new();
189    let mut order = Vec::new();
190
191    for &root in &graph.roots {
192        topo_visit(graph, root, &mut visited, &mut order);
193    }
194    // also handle disconnected nodes
195    for node in &graph.nodes {
196        if !visited.contains(&node.id) {
197            topo_visit(graph, node.id, &mut visited, &mut order);
198        }
199    }
200
201    order.reverse();
202    order
203}
204
205/// Remove leaf nodes (no children) with weight approximately zero.
206#[allow(dead_code)]
207pub fn prune_zero_weight(graph: &mut BlendGraph) {
208    // Keep a node if it has children OR its weight is non-zero.
209    graph
210        .nodes
211        .retain(|n| !n.inputs.is_empty() || n.weight.abs() >= 1e-6);
212}
213
214#[allow(dead_code)]
215pub fn blend_graph_to_json(graph: &BlendGraph) -> String {
216    let mut parts = Vec::new();
217    for node in &graph.nodes {
218        let op_str = match node.op {
219            BlendOp::Add => "Add",
220            BlendOp::Multiply => "Multiply",
221            BlendOp::Override => "Override",
222            BlendOp::Screen => "Screen",
223        };
224        let target_str = node
225            .target_name
226            .as_deref()
227            .map(|s| format!("\"{}\"", s))
228            .unwrap_or_else(|| "null".to_string());
229        let inputs_str = node
230            .inputs
231            .iter()
232            .map(|id| id.to_string())
233            .collect::<Vec<_>>()
234            .join(",");
235        parts.push(format!(
236            "{{\"id\":{},\"name\":\"{}\",\"weight\":{},\"op\":\"{}\",\"target\":{},\"inputs\":[{}]}}",
237            node.id, node.name, node.weight, op_str, target_str, inputs_str
238        ));
239    }
240    let roots_str = graph
241        .roots
242        .iter()
243        .map(|id| id.to_string())
244        .collect::<Vec<_>>()
245        .join(",");
246    format!(
247        "{{\"nodes\":[{}],\"roots\":[{}]}}",
248        parts.join(","),
249        roots_str
250    )
251}
252
253#[cfg(test)]
254mod tests {
255    use super::*;
256
257    #[test]
258    fn test_new_blend_graph() {
259        let g = new_blend_graph();
260        assert!(g.nodes.is_empty());
261        assert!(g.roots.is_empty());
262        assert_eq!(g.next_id, 0);
263    }
264
265    #[test]
266    fn test_add_blend_node() {
267        let mut g = new_blend_graph();
268        let id = add_blend_node(&mut g, "parent", BlendOp::Add, 0.8);
269        assert_eq!(id, 0);
270        assert_eq!(g.nodes.len(), 1);
271        assert_eq!(g.nodes[0].name, "parent");
272    }
273
274    #[test]
275    fn test_add_leaf_node() {
276        let mut g = new_blend_graph();
277        let id = add_leaf_node(&mut g, "smile", 0.5);
278        assert_eq!(id, 0);
279        assert!(g.nodes[0].target_name.is_some());
280        assert_eq!(g.nodes[0].target_name.as_deref(), Some("smile"));
281    }
282
283    #[test]
284    fn test_connect_nodes() {
285        let mut g = new_blend_graph();
286        let p = add_blend_node(&mut g, "parent", BlendOp::Add, 1.0);
287        let c = add_leaf_node(&mut g, "child_target", 0.5);
288        connect_nodes(&mut g, p, c);
289        assert!(g.nodes[0].inputs.contains(&c));
290    }
291
292    #[test]
293    fn test_add_root() {
294        let mut g = new_blend_graph();
295        let id = add_blend_node(&mut g, "root", BlendOp::Add, 1.0);
296        add_root(&mut g, id);
297        assert!(g.roots.contains(&id));
298        // duplicate should not be added
299        add_root(&mut g, id);
300        assert_eq!(g.roots.len(), 1);
301    }
302
303    #[test]
304    fn test_evaluate_single_leaf() {
305        let mut g = new_blend_graph();
306        let leaf = add_leaf_node(&mut g, "brow_raise", 0.7);
307        add_root(&mut g, leaf);
308        let result = evaluate_graph(&g);
309        assert_eq!(result.target_weights.len(), 1);
310        let (name, w) = &result.target_weights[0];
311        assert_eq!(name, "brow_raise");
312        // Override op is Add here, so parent_weight(1.0) + node.weight(0.7) for Add
313        assert!(*w > 0.0);
314    }
315
316    #[test]
317    fn test_evaluate_tree_add() {
318        let mut g = new_blend_graph();
319        let parent = add_blend_node(&mut g, "group", BlendOp::Multiply, 0.5);
320        let leaf = add_leaf_node(&mut g, "jaw_open", 1.0);
321        connect_nodes(&mut g, parent, leaf);
322        add_root(&mut g, parent);
323        let result = evaluate_graph(&g);
324        assert!(!result.target_weights.is_empty());
325    }
326
327    #[test]
328    fn test_set_node_weight() {
329        let mut g = new_blend_graph();
330        let id = add_blend_node(&mut g, "n", BlendOp::Add, 0.0);
331        set_node_weight(&mut g, id, 0.9);
332        assert!((get_node(&g, id).expect("should succeed").weight - 0.9).abs() < 1e-6);
333    }
334
335    #[test]
336    fn test_get_node() {
337        let mut g = new_blend_graph();
338        let id = add_leaf_node(&mut g, "target", 0.3);
339        let node = get_node(&g, id);
340        assert!(node.is_some());
341        assert_eq!(node.expect("should succeed").id, id);
342    }
343
344    #[test]
345    fn test_node_count() {
346        let mut g = new_blend_graph();
347        assert_eq!(node_count(&g), 0);
348        add_blend_node(&mut g, "a", BlendOp::Add, 1.0);
349        add_leaf_node(&mut g, "b", 0.5);
350        assert_eq!(node_count(&g), 2);
351    }
352
353    #[test]
354    fn test_leaf_nodes() {
355        let mut g = new_blend_graph();
356        add_blend_node(&mut g, "group", BlendOp::Add, 1.0);
357        add_leaf_node(&mut g, "target_a", 0.5);
358        add_leaf_node(&mut g, "target_b", 0.3);
359        let leaves = leaf_nodes(&g);
360        assert_eq!(leaves.len(), 2);
361    }
362
363    #[test]
364    fn test_topological_sort() {
365        let mut g = new_blend_graph();
366        let p = add_blend_node(&mut g, "root", BlendOp::Add, 1.0);
367        let c1 = add_leaf_node(&mut g, "t1", 0.5);
368        let c2 = add_leaf_node(&mut g, "t2", 0.5);
369        connect_nodes(&mut g, p, c1);
370        connect_nodes(&mut g, p, c2);
371        add_root(&mut g, p);
372        let order = topological_sort_graph(&g);
373        assert_eq!(order.len(), 3);
374        // root should appear before children
375        let root_pos = order
376            .iter()
377            .position(|&id| id == p)
378            .expect("should succeed");
379        let c1_pos = order
380            .iter()
381            .position(|&id| id == c1)
382            .expect("should succeed");
383        assert!(root_pos < c1_pos);
384    }
385
386    #[test]
387    fn test_prune_zero_weight() {
388        let mut g = new_blend_graph();
389        add_leaf_node(&mut g, "zero_target", 0.0);
390        let _keep = add_leaf_node(&mut g, "keep_target", 0.5);
391        prune_zero_weight(&mut g);
392        // zero-weight leaf with no children should be removed
393        assert_eq!(g.nodes.len(), 1);
394        assert_eq!(g.nodes[0].name, "keep_target");
395    }
396
397    #[test]
398    fn test_blend_graph_to_json() {
399        let mut g = new_blend_graph();
400        let leaf = add_leaf_node(&mut g, "smile", 0.8);
401        add_root(&mut g, leaf);
402        let json = blend_graph_to_json(&g);
403        assert!(json.contains("smile"));
404        assert!(json.contains("nodes"));
405        assert!(json.contains("roots"));
406    }
407}