Skip to main content

oxihuman_export/
animation_layer_export.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3#![allow(dead_code)]
4
5//! Animation layer export: blended animation layer data serialisation.
6
7/// Blend mode for an animation layer.
8#[allow(dead_code)]
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10pub enum LayerBlendMode {
11    Override,
12    Additive,
13    Multiply,
14}
15
16/// A single animation layer.
17#[allow(dead_code)]
18#[derive(Debug, Clone)]
19pub struct AnimLayer {
20    pub name: String,
21    pub weight: f32,
22    pub blend_mode: LayerBlendMode,
23    pub enabled: bool,
24    pub track_count: usize,
25}
26
27/// Export bundle for animation layers.
28#[allow(dead_code)]
29#[derive(Debug, Clone)]
30pub struct AnimLayerExport {
31    pub layers: Vec<AnimLayer>,
32}
33
34/// Create a new animation layer export.
35#[allow(dead_code)]
36pub fn new_anim_layer_export() -> AnimLayerExport {
37    AnimLayerExport { layers: Vec::new() }
38}
39
40/// Add a layer.
41#[allow(dead_code)]
42pub fn add_anim_layer(exp: &mut AnimLayerExport, layer: AnimLayer) {
43    exp.layers.push(layer);
44}
45
46/// Layer count.
47#[allow(dead_code)]
48pub fn anim_layer_count(exp: &AnimLayerExport) -> usize {
49    exp.layers.len()
50}
51
52/// Total weight of enabled layers.
53#[allow(dead_code)]
54pub fn total_enabled_weight(exp: &AnimLayerExport) -> f32 {
55    exp.layers
56        .iter()
57        .filter(|l| l.enabled)
58        .map(|l| l.weight)
59        .sum()
60}
61
62/// Find layer by name.
63#[allow(dead_code)]
64pub fn find_layer_by_name<'a>(exp: &'a AnimLayerExport, name: &str) -> Option<&'a AnimLayer> {
65    exp.layers.iter().find(|l| l.name == name)
66}
67
68/// Enabled layer count.
69#[allow(dead_code)]
70pub fn enabled_layer_count(exp: &AnimLayerExport) -> usize {
71    exp.layers.iter().filter(|l| l.enabled).count()
72}
73
74/// Serialise to JSON.
75#[allow(dead_code)]
76pub fn anim_layer_to_json(exp: &AnimLayerExport) -> String {
77    format!(
78        "{{\"layer_count\":{},\"enabled\":{}}}",
79        anim_layer_count(exp),
80        enabled_layer_count(exp)
81    )
82}
83
84/// Blend mode name string.
85#[allow(dead_code)]
86pub fn blend_mode_name(mode: LayerBlendMode) -> &'static str {
87    match mode {
88        LayerBlendMode::Override => "override",
89        LayerBlendMode::Additive => "additive",
90        LayerBlendMode::Multiply => "multiply",
91    }
92}
93
94/// Validate: all weights in `[0,1]`.
95#[allow(dead_code)]
96pub fn validate_anim_layers(exp: &AnimLayerExport) -> bool {
97    exp.layers.iter().all(|l| (0.0..=1.0).contains(&l.weight))
98}
99
100#[cfg(test)]
101mod tests {
102    use super::*;
103
104    fn sample_layer(name: &str, w: f32) -> AnimLayer {
105        AnimLayer {
106            name: name.to_string(),
107            weight: w,
108            blend_mode: LayerBlendMode::Override,
109            enabled: true,
110            track_count: 3,
111        }
112    }
113
114    #[test]
115    fn new_export_empty() {
116        let exp = new_anim_layer_export();
117        assert_eq!(anim_layer_count(&exp), 0);
118    }
119
120    #[test]
121    fn add_layer_increments() {
122        let mut exp = new_anim_layer_export();
123        add_anim_layer(&mut exp, sample_layer("base", 1.0));
124        assert_eq!(anim_layer_count(&exp), 1);
125    }
126
127    #[test]
128    fn total_weight_sums() {
129        let mut exp = new_anim_layer_export();
130        add_anim_layer(&mut exp, sample_layer("a", 0.5));
131        add_anim_layer(&mut exp, sample_layer("b", 0.3));
132        assert!((total_enabled_weight(&exp) - 0.8).abs() < 1e-5);
133    }
134
135    #[test]
136    fn find_by_name_some() {
137        let mut exp = new_anim_layer_export();
138        add_anim_layer(&mut exp, sample_layer("walk", 1.0));
139        assert!(find_layer_by_name(&exp, "walk").is_some());
140    }
141
142    #[test]
143    fn find_by_name_none() {
144        let exp = new_anim_layer_export();
145        assert!(find_layer_by_name(&exp, "missing").is_none());
146    }
147
148    #[test]
149    fn enabled_count() {
150        let mut exp = new_anim_layer_export();
151        add_anim_layer(
152            &mut exp,
153            AnimLayer {
154                enabled: false,
155                ..sample_layer("x", 0.5)
156            },
157        );
158        add_anim_layer(&mut exp, sample_layer("y", 0.5));
159        assert_eq!(enabled_layer_count(&exp), 1);
160    }
161
162    #[test]
163    fn validate_valid() {
164        let mut exp = new_anim_layer_export();
165        add_anim_layer(&mut exp, sample_layer("a", 0.7));
166        assert!(validate_anim_layers(&exp));
167    }
168
169    #[test]
170    fn blend_mode_names() {
171        assert_eq!(blend_mode_name(LayerBlendMode::Additive), "additive");
172    }
173
174    #[test]
175    fn json_contains_layer_count() {
176        let mut exp = new_anim_layer_export();
177        add_anim_layer(&mut exp, sample_layer("base", 1.0));
178        let j = anim_layer_to_json(&exp);
179        assert!(j.contains("layer_count"));
180    }
181
182    #[test]
183    fn weight_in_range() {
184        let l = sample_layer("t", 0.5);
185        assert!((0.0..=1.0).contains(&l.weight));
186    }
187}