1#![allow(dead_code)]
4
5#[derive(Debug, Clone, Copy, PartialEq)]
9pub enum WrinklePattern {
10 Linear,
11 Radial,
12 Noise,
13}
14
15#[derive(Debug, Clone)]
17pub struct WrinkleRegion {
18 pub pattern: WrinklePattern,
19 pub center: [f32; 3],
20 pub radius: f32,
21 pub amplitude: f32,
22 pub frequency: f32,
23 pub driver_weight: f32,
24}
25
26#[derive(Debug, Clone)]
28pub struct ProceduralWrinkle {
29 pub regions: Vec<WrinkleRegion>,
30 pub vertex_count: usize,
31 pub enabled: bool,
32 pub global_scale: f32,
33}
34
35impl ProceduralWrinkle {
36 pub fn new(vertex_count: usize) -> Self {
37 ProceduralWrinkle {
38 regions: Vec::new(),
39 vertex_count,
40 enabled: true,
41 global_scale: 1.0,
42 }
43 }
44}
45
46pub fn new_procedural_wrinkle(vertex_count: usize) -> ProceduralWrinkle {
48 ProceduralWrinkle::new(vertex_count)
49}
50
51pub fn pw_add_region(pw: &mut ProceduralWrinkle, region: WrinkleRegion) {
53 pw.regions.push(region);
54}
55
56pub fn pw_evaluate(pw: &ProceduralWrinkle) -> Vec<[f32; 3]> {
62 vec![[0.0_f32; 3]; pw.vertex_count]
63}
64
65pub fn pw_evaluate_with_positions(pw: &ProceduralWrinkle, positions: &[[f32; 3]]) -> Vec<[f32; 3]> {
86 let count = pw.vertex_count.min(positions.len());
87 let mut offsets = vec![[0.0_f32; 3]; pw.vertex_count];
88
89 if !pw.enabled || pw.global_scale <= 0.0 {
90 return offsets;
91 }
92
93 for (v_idx, pos) in positions.iter().enumerate().take(count) {
94 let mut acc = [0.0_f32; 3];
95
96 for region in &pw.regions {
97 let dx = pos[0] - region.center[0];
99 let dy = pos[1] - region.center[1];
100 let dz = pos[2] - region.center[2];
101 let dist = (dx * dx + dy * dy + dz * dz).sqrt();
102
103 if dist > region.radius {
104 continue; }
106
107 let t = 1.0_f32 - dist / region.radius;
109
110 let ripple = t * region.amplitude * (dist * region.frequency).cos();
112
113 let displacement: [f32; 3] = match region.pattern {
114 WrinklePattern::Linear => {
115 [ripple, 0.0_f32, 0.0_f32]
117 }
118 WrinklePattern::Radial => {
119 if dist < 1e-8 {
121 [ripple, 0.0_f32, 0.0_f32]
122 } else {
123 let inv = 1.0_f32 / dist;
124 [ripple * dx * inv, ripple * dy * inv, ripple * dz * inv]
125 }
126 }
127 WrinklePattern::Noise => {
128 let noise = t * region.amplitude * (dist * region.frequency).sin();
130 [0.0_f32, noise, 0.0_f32]
131 }
132 };
133
134 let scale = region.driver_weight * pw.global_scale;
136 acc[0] += displacement[0] * scale;
137 acc[1] += displacement[1] * scale;
138 acc[2] += displacement[2] * scale;
139 }
140
141 offsets[v_idx] = acc;
142 }
143
144 offsets
145}
146
147pub fn pw_set_global_scale(pw: &mut ProceduralWrinkle, scale: f32) {
149 pw.global_scale = scale.max(0.0);
150}
151
152pub fn pw_set_enabled(pw: &mut ProceduralWrinkle, enabled: bool) {
154 pw.enabled = enabled;
155}
156
157pub fn pw_region_count(pw: &ProceduralWrinkle) -> usize {
159 pw.regions.len()
160}
161
162pub fn pw_to_json(pw: &ProceduralWrinkle) -> String {
164 format!(
165 r#"{{"vertex_count":{},"region_count":{},"global_scale":{},"enabled":{}}}"#,
166 pw.vertex_count,
167 pw.regions.len(),
168 pw.global_scale,
169 pw.enabled
170 )
171}
172
173#[cfg(test)]
174mod tests {
175 use super::*;
176
177 #[test]
178 fn test_new_vertex_count() {
179 let pw = new_procedural_wrinkle(200);
180 assert_eq!(pw.vertex_count, 200 ,);
181 }
182
183 #[test]
184 fn test_no_regions_initially() {
185 let pw = new_procedural_wrinkle(100);
186 assert_eq!(pw_region_count(&pw), 0 ,);
187 }
188
189 #[test]
190 fn test_add_region() {
191 let mut pw = new_procedural_wrinkle(100);
192 pw_add_region(
193 &mut pw,
194 WrinkleRegion {
195 pattern: WrinklePattern::Linear,
196 center: [0.0; 3],
197 radius: 1.0,
198 amplitude: 0.5,
199 frequency: 2.0,
200 driver_weight: 1.0,
201 },
202 );
203 assert_eq!(pw_region_count(&pw), 1 ,);
204 }
205
206 #[test]
207 fn test_evaluate_length() {
208 let pw = new_procedural_wrinkle(50);
209 let out = pw_evaluate(&pw);
210 assert_eq!(
211 out.len(),
212 50, );
214 }
215
216 #[test]
217 fn test_evaluate_zeroed() {
218 let pw = new_procedural_wrinkle(4);
219 let out = pw_evaluate(&pw);
220 assert!((out[0][0]).abs() < 1e-6 ,);
221 }
222
223 #[test]
224 fn test_set_global_scale() {
225 let mut pw = new_procedural_wrinkle(10);
226 pw_set_global_scale(&mut pw, 2.5);
227 assert!((pw.global_scale - 2.5).abs() < 1e-5, );
228 }
229
230 #[test]
231 fn test_global_scale_clamped_negative() {
232 let mut pw = new_procedural_wrinkle(10);
233 pw_set_global_scale(&mut pw, -1.0);
234 assert!((pw.global_scale).abs() < 1e-6, );
235 }
236
237 #[test]
238 fn test_set_enabled() {
239 let mut pw = new_procedural_wrinkle(10);
240 pw_set_enabled(&mut pw, false);
241 assert!(!pw.enabled ,);
242 }
243
244 #[test]
245 fn test_to_json_contains_vertex_count() {
246 let pw = new_procedural_wrinkle(30);
247 let j = pw_to_json(&pw);
248 assert!(j.contains("\"vertex_count\""), );
249 }
250
251 #[test]
252 fn test_enabled_default() {
253 let pw = new_procedural_wrinkle(1);
254 assert!(pw.enabled ,);
255 }
256
257 #[test]
258 fn pw_evaluate_region_affects_nearby_vertex() {
259 let mut pw = new_procedural_wrinkle(1);
262 pw_add_region(
263 &mut pw,
264 WrinkleRegion {
265 pattern: WrinklePattern::Radial,
266 center: [0.0, 0.0, 0.0],
267 radius: 1.0,
268 amplitude: 1.0,
269 frequency: 1.0,
270 driver_weight: 1.0,
271 },
272 );
273 let positions: Vec<[f32; 3]> = vec![[0.5, 0.0, 0.0]];
274 let offsets = pw_evaluate_with_positions(&pw, &positions);
275 let mag = offsets[0].iter().map(|&v| v * v).sum::<f32>().sqrt();
276 assert!(
277 mag > 1e-6,
278 "vertex inside region must receive a non-zero offset, got magnitude {mag}"
279 );
280 }
281
282 #[test]
283 fn pw_evaluate_vertex_outside_region_zero() {
284 let mut pw = new_procedural_wrinkle(1);
287 pw_add_region(
288 &mut pw,
289 WrinkleRegion {
290 pattern: WrinklePattern::Radial,
291 center: [0.0, 0.0, 0.0],
292 radius: 1.0,
293 amplitude: 1.0,
294 frequency: 1.0,
295 driver_weight: 1.0,
296 },
297 );
298 let positions: Vec<[f32; 3]> = vec![[2.0, 0.0, 0.0]];
299 let offsets = pw_evaluate_with_positions(&pw, &positions);
300 let mag = offsets[0].iter().map(|&v| v * v).sum::<f32>().sqrt();
301 assert!(
302 mag < 1e-6,
303 "vertex outside region must have zero offset, got magnitude {mag}"
304 );
305 }
306}