Skip to main content

oxihuman_morph/
proximity_wrap.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3#![allow(dead_code)]
4
5//! Proximity-based surface wrap deformer stub.
6
7/// Configuration for the proximity wrap deformer.
8#[derive(Debug, Clone)]
9pub struct ProximityWrapConfig {
10    /// Maximum influence distance.
11    pub max_distance: f32,
12    /// Falloff exponent.
13    pub falloff: f32,
14}
15
16impl Default for ProximityWrapConfig {
17    fn default() -> Self {
18        ProximityWrapConfig {
19            max_distance: 0.5,
20            falloff: 2.0,
21        }
22    }
23}
24
25/// A proximity wrap binding between a driver mesh and a target mesh.
26#[derive(Debug, Clone)]
27pub struct ProximityWrap {
28    pub config: ProximityWrapConfig,
29    /// Influence weights per vertex.
30    pub weights: Vec<f32>,
31}
32
33impl ProximityWrap {
34    pub fn new(vertex_count: usize) -> Self {
35        ProximityWrap {
36            config: ProximityWrapConfig::default(),
37            weights: vec![0.0; vertex_count],
38        }
39    }
40}
41
42/// Create a new proximity wrap for the given vertex count.
43pub fn new_proximity_wrap(vertex_count: usize) -> ProximityWrap {
44    ProximityWrap::new(vertex_count)
45}
46
47/// Compute influence weight based on distance.
48pub fn proximity_influence(distance: f32, config: &ProximityWrapConfig) -> f32 {
49    if distance >= config.max_distance {
50        return 0.0;
51    }
52    let t = 1.0 - distance / config.max_distance;
53    t.powf(config.falloff)
54}
55
56/// Bake weights for all vertices given their distances to the driver surface.
57#[allow(clippy::needless_range_loop)]
58pub fn bake_proximity_weights(wrap: &mut ProximityWrap, distances: &[f32]) {
59    let n = wrap.weights.len().min(distances.len());
60    for i in 0..n {
61        wrap.weights[i] = proximity_influence(distances[i], &wrap.config);
62    }
63}
64
65/// Return the number of vertices.
66pub fn proximity_vertex_count(wrap: &ProximityWrap) -> usize {
67    wrap.weights.len()
68}
69
70/// Return a JSON-like string for the wrap state.
71pub fn proximity_wrap_to_json(wrap: &ProximityWrap) -> String {
72    format!(
73        r#"{{"max_distance":{:.4},"falloff":{:.4},"vertices":{}}}"#,
74        wrap.config.max_distance,
75        wrap.config.falloff,
76        wrap.weights.len()
77    )
78}
79
80/// Return the average weight across all vertices.
81pub fn proximity_average_weight(wrap: &ProximityWrap) -> f32 {
82    if wrap.weights.is_empty() {
83        return 0.0;
84    }
85    let sum: f32 = wrap.weights.iter().sum();
86    sum / wrap.weights.len() as f32
87}
88
89#[cfg(test)]
90mod tests {
91    use super::*;
92
93    #[test]
94    fn test_new_wrap_correct_vertex_count() {
95        let w = new_proximity_wrap(10);
96        assert_eq!(
97            proximity_vertex_count(&w),
98            10, /* vertex count must match */
99        );
100    }
101
102    #[test]
103    fn test_influence_at_zero_distance_is_one() {
104        let cfg = ProximityWrapConfig::default();
105        let inf = proximity_influence(0.0, &cfg);
106        assert!((inf - 1.0).abs() < 1e-5, /* influence at zero distance should be 1 */);
107    }
108
109    #[test]
110    fn test_influence_at_max_distance_is_zero() {
111        let cfg = ProximityWrapConfig::default();
112        let inf = proximity_influence(cfg.max_distance, &cfg);
113        assert!((inf).abs() < 1e-5, /* influence at max distance should be 0 */);
114    }
115
116    #[test]
117    fn test_influence_beyond_max_is_zero() {
118        let cfg = ProximityWrapConfig::default();
119        let inf = proximity_influence(cfg.max_distance + 1.0, &cfg);
120        assert!((inf).abs() < 1e-5, /* influence beyond max distance must be 0 */);
121    }
122
123    #[test]
124    fn test_bake_weights_fills_correctly() {
125        let mut w = new_proximity_wrap(3);
126        bake_proximity_weights(&mut w, &[0.0, 0.25, 1.0]);
127        assert!(w.weights[0] > 0.0, /* near vertex should have positive weight */);
128        assert!((w.weights[2]).abs() < 1e-5, /* far vertex should have zero weight */);
129    }
130
131    #[test]
132    fn test_average_weight_empty() {
133        let w = new_proximity_wrap(0);
134        assert!((proximity_average_weight(&w)).abs() < 1e-6, /* empty gives 0 average */);
135    }
136
137    #[test]
138    fn test_average_weight_all_zero() {
139        let w = new_proximity_wrap(5);
140        assert!((proximity_average_weight(&w)).abs() < 1e-6, /* all-zero average is 0 */);
141    }
142
143    #[test]
144    fn test_to_json_contains_max_distance() {
145        let w = new_proximity_wrap(4);
146        let j = proximity_wrap_to_json(&w);
147        assert!(j.contains("max_distance"), /* JSON should contain max_distance key */);
148    }
149
150    #[test]
151    fn test_falloff_default_is_two() {
152        let w = new_proximity_wrap(1);
153        assert!((w.config.falloff - 2.0).abs() < 1e-5, /* default falloff is 2.0 */);
154    }
155
156    #[test]
157    fn test_weights_initialized_zero() {
158        let w = new_proximity_wrap(8);
159        for &wt in &w.weights {
160            assert!((wt).abs() < 1e-6 /* initial weights must be zero */,);
161        }
162    }
163}