1#[allow(dead_code)]
7#[derive(Debug, Clone)]
8pub struct VolumeMorphConfig {
9 pub preservation_strength: f32,
10 pub iterations: u32,
11 pub smooth_correction: bool,
12}
13
14impl Default for VolumeMorphConfig {
15 fn default() -> Self {
16 Self {
17 preservation_strength: 0.5,
18 iterations: 3,
19 smooth_correction: true,
20 }
21 }
22}
23
24#[allow(dead_code)]
25#[derive(Debug, Clone)]
26pub struct VolumeMorphResult {
27 pub corrected_deltas: Vec<[f32; 3]>,
28 pub original_volume: f32,
29 pub morphed_volume: f32,
30 pub corrected_volume: f32,
31 pub volume_error_pct: f32,
32}
33
34#[allow(dead_code)]
36pub fn compute_mesh_volume(positions: &[[f32; 3]], indices: &[u32]) -> f32 {
37 let mut volume = 0.0f32;
38 let tri_count = indices.len() / 3;
39 for t in 0..tri_count {
40 let i0 = indices[t * 3] as usize;
41 let i1 = indices[t * 3 + 1] as usize;
42 let i2 = indices[t * 3 + 2] as usize;
43 if i0 >= positions.len() || i1 >= positions.len() || i2 >= positions.len() {
44 continue;
45 }
46 let v0 = positions[i0];
47 let v1 = positions[i1];
48 let v2 = positions[i2];
49 let cross_x = v1[1] * v2[2] - v1[2] * v2[1];
51 let cross_y = v1[2] * v2[0] - v1[0] * v2[2];
52 let cross_z = v1[0] * v2[1] - v1[1] * v2[0];
53 volume += v0[0] * cross_x + v0[1] * cross_y + v0[2] * cross_z;
54 }
55 volume / 6.0
56}
57
58#[allow(dead_code)]
59pub fn volume_preserving_delta(
60 base: &[[f32; 3]],
61 indices: &[u32],
62 deltas: &[[f32; 3]],
63 cfg: &VolumeMorphConfig,
64) -> VolumeMorphResult {
65 let n = base.len();
66
67 let morphed: Vec<[f32; 3]> = base
69 .iter()
70 .enumerate()
71 .map(|(i, b)| {
72 let d = if i < deltas.len() {
73 deltas[i]
74 } else {
75 [0.0; 3]
76 };
77 [b[0] + d[0], b[1] + d[1], b[2] + d[2]]
78 })
79 .collect();
80
81 let original_volume = compute_mesh_volume(base, indices);
82 let morphed_volume = compute_mesh_volume(&morphed, indices);
83
84 let ratio = mesh_volume_ratio(base, &morphed, indices);
86 let scale = 1.0 + (ratio - 1.0) * cfg.preservation_strength;
88
89 let mut corrected_deltas: Vec<[f32; 3]> = uniform_scale_correction(base, deltas, scale);
90
91 if cfg.smooth_correction {
92 corrected_deltas = laplacian_smooth_deltas(&corrected_deltas, indices, cfg.iterations);
93 }
94
95 let corrected_positions: Vec<[f32; 3]> = base
97 .iter()
98 .enumerate()
99 .map(|(i, b)| {
100 let d = if i < n { corrected_deltas[i] } else { [0.0; 3] };
101 [b[0] + d[0], b[1] + d[1], b[2] + d[2]]
102 })
103 .collect();
104 let corrected_volume = compute_mesh_volume(&corrected_positions, indices);
105 let volume_error_pct = volume_error_percent(original_volume, corrected_volume);
106
107 VolumeMorphResult {
108 corrected_deltas,
109 original_volume,
110 morphed_volume,
111 corrected_volume,
112 volume_error_pct,
113 }
114}
115
116#[allow(dead_code)]
117pub fn uniform_scale_correction(
118 _base: &[[f32; 3]],
119 deltas: &[[f32; 3]],
120 scale: f32,
121) -> Vec<[f32; 3]> {
122 deltas
123 .iter()
124 .map(|d| [d[0] * scale, d[1] * scale, d[2] * scale])
125 .collect()
126}
127
128#[allow(dead_code)]
129pub fn laplacian_smooth_deltas(
130 deltas: &[[f32; 3]],
131 indices: &[u32],
132 iterations: u32,
133) -> Vec<[f32; 3]> {
134 let n = deltas.len();
135 if n == 0 || indices.is_empty() {
136 return deltas.to_vec();
137 }
138
139 let mut neighbors: Vec<Vec<usize>> = vec![Vec::new(); n];
141 let tri_count = indices.len() / 3;
142 for t in 0..tri_count {
143 let i0 = indices[t * 3] as usize;
144 let i1 = indices[t * 3 + 1] as usize;
145 let i2 = indices[t * 3 + 2] as usize;
146 if i0 < n && i1 < n && i2 < n {
147 if !neighbors[i0].contains(&i1) {
148 neighbors[i0].push(i1);
149 }
150 if !neighbors[i0].contains(&i2) {
151 neighbors[i0].push(i2);
152 }
153 if !neighbors[i1].contains(&i0) {
154 neighbors[i1].push(i0);
155 }
156 if !neighbors[i1].contains(&i2) {
157 neighbors[i1].push(i2);
158 }
159 if !neighbors[i2].contains(&i0) {
160 neighbors[i2].push(i0);
161 }
162 if !neighbors[i2].contains(&i1) {
163 neighbors[i2].push(i1);
164 }
165 }
166 }
167
168 let mut current = deltas.to_vec();
169 for _ in 0..iterations {
170 let prev = current.clone();
171 for i in 0..n {
172 let nb = &neighbors[i];
173 if nb.is_empty() {
174 continue;
175 }
176 let mut avg = [0.0f32; 3];
177 for &j in nb {
178 avg[0] += prev[j][0];
179 avg[1] += prev[j][1];
180 avg[2] += prev[j][2];
181 }
182 let k = nb.len() as f32;
183 current[i] = [avg[0] / k, avg[1] / k, avg[2] / k];
184 }
185 }
186 current
187}
188
189#[allow(dead_code)]
190pub fn mesh_volume_ratio(
191 base_positions: &[[f32; 3]],
192 morphed_positions: &[[f32; 3]],
193 indices: &[u32],
194) -> f32 {
195 let v_base = compute_mesh_volume(base_positions, indices);
196 let v_morphed = compute_mesh_volume(morphed_positions, indices);
197 if v_morphed.abs() < 1e-10 {
198 return 1.0;
199 }
200 v_base / v_morphed
201}
202
203#[allow(dead_code)]
204pub fn volume_error_percent(original: f32, corrected: f32) -> f32 {
205 if original.abs() < 1e-10 {
206 return 0.0;
207 }
208 (corrected - original) / original * 100.0
209}
210
211#[cfg(test)]
212mod tests {
213 use super::*;
214
215 fn tetra_positions() -> Vec<[f32; 3]> {
218 vec![
219 [1.0, 1.0, 1.0],
220 [-1.0, -1.0, 1.0],
221 [-1.0, 1.0, -1.0],
222 [1.0, -1.0, -1.0],
223 ]
224 }
225
226 fn tetra_indices() -> Vec<u32> {
227 vec![0, 1, 2, 0, 2, 3, 0, 3, 1, 1, 3, 2]
229 }
230
231 #[test]
232 fn test_compute_mesh_volume_tetrahedron() {
233 let pos = tetra_positions();
234 let idx = tetra_indices();
235 let vol = compute_mesh_volume(&pos, &idx).abs();
236 assert!((vol - 8.0 / 3.0).abs() < 0.01, "tetra volume: {vol}");
238 }
239
240 #[test]
241 fn test_compute_mesh_volume_empty() {
242 let vol = compute_mesh_volume(&[], &[]);
243 assert_eq!(vol, 0.0);
244 }
245
246 #[test]
247 fn test_compute_mesh_volume_single_triangle() {
248 let pos = vec![[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]];
250 let idx = vec![0u32, 1, 2];
251 let vol = compute_mesh_volume(&pos, &idx).abs();
252 assert!((vol - 1.0 / 6.0).abs() < 1e-5, "single tri vol: {vol}");
257 }
258
259 #[test]
260 fn test_volume_error_percent_formula() {
261 let pct = volume_error_percent(100.0, 105.0);
262 assert!((pct - 5.0).abs() < 1e-5, "5% error: {pct}");
263 }
264
265 #[test]
266 fn test_volume_error_percent_zero_original() {
267 let pct = volume_error_percent(0.0, 5.0);
268 assert_eq!(pct, 0.0);
269 }
270
271 #[test]
272 fn test_uniform_scale_correction() {
273 let base = vec![[0.0, 0.0, 0.0]];
274 let deltas = vec![[1.0, 2.0, 3.0]];
275 let result = uniform_scale_correction(&base, &deltas, 2.0);
276 assert!((result[0][0] - 2.0).abs() < 1e-6);
277 assert!((result[0][1] - 4.0).abs() < 1e-6);
278 assert!((result[0][2] - 6.0).abs() < 1e-6);
279 }
280
281 #[test]
282 fn test_uniform_scale_correction_identity() {
283 let base = vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0]];
284 let deltas = vec![[2.0, 0.0, 0.0], [3.0, 0.0, 0.0]];
285 let result = uniform_scale_correction(&base, &deltas, 1.0);
286 assert!((result[0][0] - 2.0).abs() < 1e-6);
287 assert!((result[1][0] - 3.0).abs() < 1e-6);
288 }
289
290 #[test]
291 fn test_laplacian_smooth_reduces_magnitude() {
292 let deltas = vec![[10.0, 0.0, 0.0], [0.0, 0.0, 0.0], [0.0, 0.0, 0.0]];
294 let indices = vec![0u32, 1, 2];
295 let smoothed = laplacian_smooth_deltas(&deltas, &indices, 1);
296 assert!(
298 smoothed[0][0].abs() < deltas[0][0].abs(),
299 "spike should be reduced: {}",
300 smoothed[0][0]
301 );
302 }
303
304 #[test]
305 fn test_laplacian_smooth_zero_delta_noop() {
306 let deltas = vec![[0.0, 0.0, 0.0]; 4];
307 let indices = vec![0u32, 1, 2, 0, 2, 3];
308 let smoothed = laplacian_smooth_deltas(&deltas, &indices, 3);
309 for d in &smoothed {
310 for &v in d {
311 assert!(v.abs() < 1e-6);
312 }
313 }
314 }
315
316 #[test]
317 fn test_mesh_volume_ratio_same_mesh() {
318 let pos = tetra_positions();
319 let idx = tetra_indices();
320 let ratio = mesh_volume_ratio(&pos, &pos, &idx);
321 assert!((ratio - 1.0).abs() < 1e-5, "same mesh ratio=1: {ratio}");
322 }
323
324 #[test]
325 fn test_volume_preserving_delta_reduces_error() {
326 let pos = tetra_positions();
327 let idx = tetra_indices();
328 let inflate: Vec<[f32; 3]> = pos
330 .iter()
331 .map(|v| [v[0] * 0.2, v[1] * 0.2, v[2] * 0.2])
332 .collect();
333 let cfg = VolumeMorphConfig {
334 preservation_strength: 1.0,
335 iterations: 1,
336 smooth_correction: false,
337 };
338 let result = volume_preserving_delta(&pos, &idx, &inflate, &cfg);
339 let uncorrected_pct =
341 volume_error_percent(result.original_volume, result.morphed_volume).abs();
342 let corrected_pct = result.volume_error_pct.abs();
343 assert!(
344 corrected_pct < uncorrected_pct,
345 "corrected error {corrected_pct} should be < uncorrected {uncorrected_pct}"
346 );
347 }
348
349 #[test]
350 fn test_volume_preserving_delta_zero_deltas() {
351 let pos = tetra_positions();
352 let idx = tetra_indices();
353 let deltas = vec![[0.0, 0.0, 0.0]; pos.len()];
354 let cfg = VolumeMorphConfig::default();
355 let result = volume_preserving_delta(&pos, &idx, &deltas, &cfg);
356 assert!(
357 (result.original_volume - result.morphed_volume).abs() < 1e-5,
358 "zero delta: volumes should match"
359 );
360 }
361
362 #[test]
363 fn test_laplacian_smooth_empty() {
364 let result = laplacian_smooth_deltas(&[], &[], 3);
365 assert!(result.is_empty());
366 }
367}