Skip to main content

oxihuman_morph/
inbetween_shape.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3#![allow(dead_code)]
4
5//! In-between (combo) shape key stub.
6
7/// An in-between shape definition: activates at a specific driver weight.
8#[derive(Debug, Clone)]
9pub struct InbetweenShape {
10    pub name: String,
11    pub trigger_weight: f32,
12    pub deltas: Vec<[f32; 3]>,
13    pub current_weight: f32,
14}
15
16impl InbetweenShape {
17    pub fn new(name: &str, trigger_weight: f32, vertex_count: usize) -> Self {
18        InbetweenShape {
19            name: name.to_string(),
20            trigger_weight: trigger_weight.clamp(0.0, 1.0),
21            deltas: vec![[0.0; 3]; vertex_count],
22            current_weight: 0.0,
23        }
24    }
25}
26
27/// Create a new inbetween shape.
28pub fn new_inbetween_shape(name: &str, trigger_weight: f32, vertex_count: usize) -> InbetweenShape {
29    InbetweenShape::new(name, trigger_weight, vertex_count)
30}
31
32/// Evaluate the inbetween weight given the driver weight.
33/// Returns the blended activation weight.
34pub fn inbetween_evaluate(shape: &mut InbetweenShape, driver_weight: f32) -> f32 {
35    /* Activation is a tent function centered at trigger_weight with width 0.5 */
36    let dist = (driver_weight - shape.trigger_weight).abs();
37    let half_width = 0.25_f32;
38    if dist >= half_width {
39        shape.current_weight = 0.0;
40    } else {
41        shape.current_weight = 1.0 - dist / half_width;
42    }
43    shape.current_weight
44}
45
46/// Set the delta for a vertex.
47pub fn inbetween_set_delta(shape: &mut InbetweenShape, index: usize, delta: [f32; 3]) {
48    if index < shape.deltas.len() {
49        shape.deltas[index] = delta;
50    }
51}
52
53/// Reset the current weight.
54pub fn inbetween_reset(shape: &mut InbetweenShape) {
55    shape.current_weight = 0.0;
56}
57
58/// Return vertex count.
59pub fn inbetween_vertex_count(shape: &InbetweenShape) -> usize {
60    shape.deltas.len()
61}
62
63/// Return a JSON-like string.
64pub fn inbetween_to_json(shape: &InbetweenShape) -> String {
65    format!(
66        r#"{{"name":"{}","trigger":{:.4},"weight":{:.4},"vertices":{}}}"#,
67        shape.name,
68        shape.trigger_weight,
69        shape.current_weight,
70        shape.deltas.len()
71    )
72}
73
74#[cfg(test)]
75mod tests {
76    use super::*;
77
78    #[test]
79    fn test_new_inbetween_vertex_count() {
80        let s = new_inbetween_shape("smile_half", 0.5, 10);
81        assert_eq!(
82            inbetween_vertex_count(&s),
83            10, /* vertex count must match */
84        );
85    }
86
87    #[test]
88    fn test_trigger_weight_clamped() {
89        let s = new_inbetween_shape("test", 2.0, 5);
90        assert!((s.trigger_weight - 1.0).abs() < 1e-5, /* trigger clamped to 1 */);
91    }
92
93    #[test]
94    fn test_evaluate_at_trigger_is_one() {
95        let mut s = new_inbetween_shape("brow_half", 0.5, 5);
96        let w = inbetween_evaluate(&mut s, 0.5);
97        assert!((w - 1.0).abs() < 1e-5, /* at trigger point weight should be 1 */);
98    }
99
100    #[test]
101    fn test_evaluate_far_from_trigger_is_zero() {
102        let mut s = new_inbetween_shape("brow_half", 0.5, 5);
103        let w = inbetween_evaluate(&mut s, 0.0);
104        assert!((w).abs() < 1e-5 /* far from trigger should give 0 */,);
105    }
106
107    #[test]
108    fn test_reset_zeroes_weight() {
109        let mut s = new_inbetween_shape("lip_half", 0.3, 4);
110        inbetween_evaluate(&mut s, 0.3);
111        inbetween_reset(&mut s);
112        assert!((s.current_weight).abs() < 1e-6, /* reset should zero weight */);
113    }
114
115    #[test]
116    fn test_set_delta_updates() {
117        let mut s = new_inbetween_shape("test", 0.5, 5);
118        inbetween_set_delta(&mut s, 0, [1.0, 2.0, 3.0]);
119        assert!((s.deltas[0][0] - 1.0).abs() < 1e-5, /* delta x must match */);
120    }
121
122    #[test]
123    fn test_set_delta_out_of_bounds_ignored() {
124        let mut s = new_inbetween_shape("test", 0.5, 2);
125        inbetween_set_delta(&mut s, 99, [1.0, 0.0, 0.0]);
126        assert_eq!(
127            inbetween_vertex_count(&s),
128            2, /* vertex count unchanged */
129        );
130    }
131
132    #[test]
133    fn test_to_json_contains_name() {
134        let s = new_inbetween_shape("smile_quarter", 0.25, 3);
135        let j = inbetween_to_json(&s);
136        assert!(j.contains("smile_quarter"), /* JSON must contain shape name */);
137    }
138
139    #[test]
140    fn test_initial_weight_zero() {
141        let s = new_inbetween_shape("test", 0.5, 3);
142        assert!((s.current_weight).abs() < 1e-6, /* initial weight should be 0 */);
143    }
144
145    #[test]
146    fn test_evaluate_partial_activation() {
147        let mut s = new_inbetween_shape("mid", 0.5, 3);
148        let w = inbetween_evaluate(&mut s, 0.625);
149        assert!(w > 0.0 && w < 1.0, /* partial activation should be between 0 and 1 */);
150    }
151}