Skip to main content

oxihuman_morph/
expression_composer.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3
4//! High-level expression composition from action units and morph sliders.
5
6use std::collections::HashMap;
7
8/// A single named layer that contributes morph weights to the final expression.
9#[allow(dead_code)]
10#[derive(Debug, Clone)]
11pub struct ExpressionComposerLayer {
12    pub name: String,
13    pub weight: f32,
14    /// Morph name → weight pairs for this layer.
15    pub morphs: HashMap<String, f32>,
16}
17
18/// The result of composing all layers.
19#[allow(dead_code)]
20#[derive(Debug, Clone)]
21pub struct ComposedExpression {
22    /// Final blended morph weights after all layers are mixed.
23    pub weights: HashMap<String, f32>,
24    /// Number of layers that contributed.
25    pub layer_count: usize,
26}
27
28/// Configuration for the expression composer.
29#[allow(dead_code)]
30#[derive(Debug, Clone)]
31pub struct ExpressionComposerConfig {
32    /// Whether to normalise output weights to the 0..1 range.
33    pub auto_normalize: bool,
34    /// Maximum number of layers allowed.
35    pub max_layers: usize,
36}
37
38/// Runtime state of the expression composer.
39#[allow(dead_code)]
40#[derive(Debug, Clone)]
41pub struct ExpressionComposer {
42    pub config: ExpressionComposerConfig,
43    pub layers: Vec<ExpressionComposerLayer>,
44}
45
46// ---------------------------------------------------------------------------
47// Functions
48// ---------------------------------------------------------------------------
49
50/// Return a sensible default [`ExpressionComposerConfig`].
51#[allow(dead_code)]
52pub fn default_composer_config() -> ExpressionComposerConfig {
53    ExpressionComposerConfig {
54        auto_normalize: false,
55        max_layers: 32,
56    }
57}
58
59/// Create an empty [`ComposedExpression`].
60#[allow(dead_code)]
61pub fn new_composed_expression() -> ComposedExpression {
62    ComposedExpression {
63        weights: HashMap::new(),
64        layer_count: 0,
65    }
66}
67
68/// Create a new [`ExpressionComposer`] with the given config.
69#[allow(dead_code)]
70pub fn new_expression_composer(config: ExpressionComposerConfig) -> ExpressionComposer {
71    ExpressionComposer {
72        config,
73        layers: Vec::new(),
74    }
75}
76
77/// Add a layer by name with the given morphs and weight 1.0.
78/// Returns `false` if the layer limit has been reached.
79#[allow(dead_code)]
80pub fn add_layer(
81    composer: &mut ExpressionComposer,
82    name: &str,
83    morphs: HashMap<String, f32>,
84) -> bool {
85    if composer.layers.len() >= composer.config.max_layers {
86        return false;
87    }
88    composer.layers.push(ExpressionComposerLayer {
89        name: name.to_string(),
90        weight: 1.0,
91        morphs,
92    });
93    true
94}
95
96/// Remove the first layer whose name matches. Returns `true` if found.
97#[allow(dead_code)]
98pub fn remove_layer(composer: &mut ExpressionComposer, name: &str) -> bool {
99    let before = composer.layers.len();
100    composer.layers.retain(|l| l.name != name);
101    composer.layers.len() < before
102}
103
104/// Blend all layers by their weights (weighted average per morph).
105#[allow(dead_code)]
106pub fn blend_layers(composer: &ExpressionComposer) -> ComposedExpression {
107    let mut weights: HashMap<String, f32> = HashMap::new();
108    let weight_sum: f32 = composer.layers.iter().map(|l| l.weight.abs()).sum();
109    for layer in &composer.layers {
110        let layer_w = if weight_sum > 0.0 {
111            layer.weight / weight_sum
112        } else {
113            0.0
114        };
115        for (morph, &mv) in &layer.morphs {
116            let entry = weights.entry(morph.clone()).or_insert(0.0);
117            *entry += mv * layer_w;
118        }
119    }
120    ComposedExpression {
121        layer_count: composer.layers.len(),
122        weights,
123    }
124}
125
126/// Evaluate the final expression, applying auto-normalisation if configured.
127#[allow(dead_code)]
128pub fn evaluate_expression(composer: &ExpressionComposer) -> ComposedExpression {
129    let mut expr = blend_layers(composer);
130    if composer.config.auto_normalize {
131        normalize_expression(&mut expr);
132    }
133    expr
134}
135
136/// Return the number of layers in the composer.
137#[allow(dead_code)]
138pub fn layer_count(composer: &ExpressionComposer) -> usize {
139    composer.layers.len()
140}
141
142/// Set the weight for the named layer (clamped 0..1). Returns `false` if not found.
143#[allow(dead_code)]
144pub fn set_layer_weight(composer: &mut ExpressionComposer, name: &str, weight: f32) -> bool {
145    for layer in &mut composer.layers {
146        if layer.name == name {
147            layer.weight = weight.clamp(0.0, 1.0);
148            return true;
149        }
150    }
151    false
152}
153
154/// Get the weight of the named layer, or `None` if not found.
155#[allow(dead_code)]
156pub fn get_layer_weight(composer: &ExpressionComposer, name: &str) -> Option<f32> {
157    composer
158        .layers
159        .iter()
160        .find(|l| l.name == name)
161        .map(|l| l.weight)
162}
163
164/// Serialise the composed expression weights to a simple JSON string.
165#[allow(dead_code)]
166pub fn expression_to_json(expr: &ComposedExpression) -> String {
167    let mut pairs: Vec<String> = expr
168        .weights
169        .iter()
170        .map(|(k, v)| format!("  \"{k}\": {v:.4}"))
171        .collect();
172    pairs.sort();
173    format!("{{\n{}\n}}", pairs.join(",\n"))
174}
175
176/// Reset the composer by removing all layers.
177#[allow(dead_code)]
178pub fn reset_expression(composer: &mut ExpressionComposer) {
179    composer.layers.clear();
180}
181
182/// Add a named preset layer from a slice of `(morph_name, weight)` pairs.
183#[allow(dead_code)]
184pub fn add_preset_layer(
185    composer: &mut ExpressionComposer,
186    name: &str,
187    presets: &[(&str, f32)],
188) -> bool {
189    let morphs: HashMap<String, f32> = presets.iter().map(|(k, v)| (k.to_string(), *v)).collect();
190    add_layer(composer, name, morphs)
191}
192
193/// Compute the total "energy" of a composed expression (sum of absolute weights).
194#[allow(dead_code)]
195pub fn expression_energy(expr: &ComposedExpression) -> f32 {
196    expr.weights.values().map(|v| v.abs()).sum()
197}
198
199/// Normalise all weights in a [`ComposedExpression`] so the max is 1.0.
200/// No-op if all weights are zero.
201#[allow(dead_code)]
202pub fn normalize_expression(expr: &mut ComposedExpression) {
203    let max = expr.weights.values().cloned().fold(0.0_f32, f32::max);
204    if max > 0.0 {
205        for v in expr.weights.values_mut() {
206            *v /= max;
207        }
208    }
209}
210
211// ---------------------------------------------------------------------------
212// Tests
213// ---------------------------------------------------------------------------
214
215#[cfg(test)]
216mod tests {
217    use super::*;
218
219    fn simple_morphs(v: f32) -> HashMap<String, f32> {
220        let mut m = HashMap::new();
221        m.insert("smile".to_string(), v);
222        m.insert("frown".to_string(), v * 0.5);
223        m
224    }
225
226    fn make_composer() -> ExpressionComposer {
227        new_expression_composer(default_composer_config())
228    }
229
230    #[test]
231    fn test_default_config() {
232        let cfg = default_composer_config();
233        assert!(!cfg.auto_normalize);
234        assert!(cfg.max_layers > 0);
235    }
236
237    #[test]
238    fn test_new_composed_expression_empty() {
239        let expr = new_composed_expression();
240        assert!(expr.weights.is_empty());
241        assert_eq!(expr.layer_count, 0);
242    }
243
244    #[test]
245    fn test_add_layer_increases_count() {
246        let mut c = make_composer();
247        add_layer(&mut c, "happy", simple_morphs(1.0));
248        assert_eq!(layer_count(&c), 1);
249    }
250
251    #[test]
252    fn test_remove_layer() {
253        let mut c = make_composer();
254        add_layer(&mut c, "happy", simple_morphs(1.0));
255        let removed = remove_layer(&mut c, "happy");
256        assert!(removed);
257        assert_eq!(layer_count(&c), 0);
258    }
259
260    #[test]
261    fn test_remove_layer_not_found() {
262        let mut c = make_composer();
263        let removed = remove_layer(&mut c, "missing");
264        assert!(!removed);
265    }
266
267    #[test]
268    fn test_blend_layers_single_layer() {
269        let mut c = make_composer();
270        add_layer(&mut c, "happy", simple_morphs(0.8));
271        let expr = blend_layers(&c);
272        assert!(!expr.weights.is_empty());
273        assert_eq!(expr.layer_count, 1);
274    }
275
276    #[test]
277    fn test_blend_layers_empty() {
278        let c = make_composer();
279        let expr = blend_layers(&c);
280        assert!(expr.weights.is_empty());
281    }
282
283    #[test]
284    fn test_evaluate_expression_returns_weights() {
285        let mut c = make_composer();
286        add_layer(&mut c, "sad", simple_morphs(0.5));
287        let expr = evaluate_expression(&c);
288        assert!(expr.weights.contains_key("smile"));
289    }
290
291    #[test]
292    fn test_set_layer_weight() {
293        let mut c = make_composer();
294        add_layer(&mut c, "angry", simple_morphs(1.0));
295        let ok = set_layer_weight(&mut c, "angry", 0.3);
296        assert!(ok);
297        assert!((get_layer_weight(&c, "angry").expect("should succeed") - 0.3).abs() < 1e-6);
298    }
299
300    #[test]
301    fn test_set_layer_weight_not_found() {
302        let mut c = make_composer();
303        let ok = set_layer_weight(&mut c, "ghost", 0.5);
304        assert!(!ok);
305    }
306
307    #[test]
308    fn test_get_layer_weight_none() {
309        let c = make_composer();
310        assert!(get_layer_weight(&c, "nonexistent").is_none());
311    }
312
313    #[test]
314    fn test_expression_to_json_contains_keys() {
315        let mut c = make_composer();
316        add_layer(&mut c, "test", simple_morphs(1.0));
317        let expr = evaluate_expression(&c);
318        let json = expression_to_json(&expr);
319        assert!(json.contains("smile"));
320    }
321
322    #[test]
323    fn test_reset_expression() {
324        let mut c = make_composer();
325        add_layer(&mut c, "layer1", simple_morphs(1.0));
326        reset_expression(&mut c);
327        assert_eq!(layer_count(&c), 0);
328    }
329
330    #[test]
331    fn test_add_preset_layer() {
332        let mut c = make_composer();
333        let ok = add_preset_layer(&mut c, "joy", &[("smile", 0.9), ("brow_raise", 0.4)]);
334        assert!(ok);
335        assert_eq!(layer_count(&c), 1);
336    }
337
338    #[test]
339    fn test_expression_energy() {
340        let mut c = make_composer();
341        add_layer(&mut c, "full", simple_morphs(1.0));
342        let expr = evaluate_expression(&c);
343        let energy = expression_energy(&expr);
344        assert!(energy > 0.0);
345    }
346
347    #[test]
348    fn test_normalize_expression() {
349        let mut expr = new_composed_expression();
350        expr.weights.insert("a".to_string(), 2.0);
351        expr.weights.insert("b".to_string(), 1.0);
352        normalize_expression(&mut expr);
353        assert!((expr.weights["a"] - 1.0).abs() < 1e-6);
354        assert!((expr.weights["b"] - 0.5).abs() < 1e-6);
355    }
356
357    #[test]
358    fn test_normalize_expression_zero_noop() {
359        let mut expr = new_composed_expression();
360        expr.weights.insert("x".to_string(), 0.0);
361        normalize_expression(&mut expr);
362        assert_eq!(expr.weights["x"], 0.0);
363    }
364
365    #[test]
366    fn test_layer_count_limit() {
367        let mut c = new_expression_composer(ExpressionComposerConfig {
368            auto_normalize: false,
369            max_layers: 2,
370        });
371        assert!(add_layer(&mut c, "l1", simple_morphs(1.0)));
372        assert!(add_layer(&mut c, "l2", simple_morphs(1.0)));
373        assert!(!add_layer(&mut c, "l3", simple_morphs(1.0)));
374        assert_eq!(layer_count(&c), 2);
375    }
376
377    #[test]
378    fn test_two_layer_blend() {
379        let mut c = make_composer();
380        let mut m1 = HashMap::new();
381        m1.insert("x".to_string(), 1.0_f32);
382        let mut m2 = HashMap::new();
383        m2.insert("x".to_string(), 0.0_f32);
384        add_layer(&mut c, "a", m1);
385        add_layer(&mut c, "b", m2);
386        let expr = blend_layers(&c);
387        // equal weights → average of 1.0 and 0.0 = 0.5
388        assert!((expr.weights["x"] - 0.5).abs() < 1e-6);
389    }
390}