Skip to main content

oxihuman_morph/
body_symmetry.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3
4//! Symmetry enforcement and controlled asymmetry injection.
5
6// ---------------------------------------------------------------------------
7// Structs
8// ---------------------------------------------------------------------------
9
10/// Which world axis is the symmetry axis.
11#[allow(dead_code)]
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub struct SymmetryAxis {
14    pub axis: u8,
15}
16
17impl SymmetryAxis {
18    pub const X: Self = Self { axis: 0 };
19    pub const Y: Self = Self { axis: 1 };
20    pub const Z: Self = Self { axis: 2 };
21}
22
23/// Configuration for symmetry enforcement.
24#[allow(dead_code)]
25#[derive(Debug, Clone)]
26pub struct SymmetryConfig {
27    pub axis: SymmetryAxis,
28    /// Position tolerance for pair matching.
29    pub tolerance: f32,
30    /// 0 = fully asymmetric, 1 = fully symmetric.
31    pub blend: f32,
32}
33
34impl Default for SymmetryConfig {
35    fn default() -> Self {
36        Self {
37            axis: SymmetryAxis::X,
38            tolerance: 0.001,
39            blend: 1.0,
40        }
41    }
42}
43
44/// Configuration for asymmetry noise injection.
45#[allow(dead_code)]
46#[derive(Debug, Clone)]
47pub struct AsymmetryConfig {
48    /// Overall magnitude 0..1.
49    pub strength: f32,
50    /// Spatial frequency of asymmetry noise.
51    pub frequency: f32,
52    pub seed: u64,
53}
54
55impl Default for AsymmetryConfig {
56    fn default() -> Self {
57        Self {
58            strength: 0.05,
59            frequency: 1.0,
60            seed: 42,
61        }
62    }
63}
64
65/// Symmetry analysis report.
66#[allow(dead_code)]
67#[derive(Debug, Clone)]
68pub struct SymmetryReport {
69    pub paired_vertices: usize,
70    pub unpaired_vertices: usize,
71    /// Mean distance between paired positions and their mirror.
72    pub symmetry_error: f32,
73    /// True when symmetry_error < tolerance.
74    pub is_symmetric: bool,
75}
76
77// ---------------------------------------------------------------------------
78// Free functions
79// ---------------------------------------------------------------------------
80
81/// Mirror a position through the given axis (flip the component).
82#[allow(dead_code)]
83pub fn mirror_position(p: [f32; 3], axis: &SymmetryAxis) -> [f32; 3] {
84    let mut q = p;
85    q[axis.axis as usize] = -q[axis.axis as usize];
86    q
87}
88
89/// LCG-based deterministic noise for asymmetry.
90/// Returns a float in roughly –1..1.
91#[allow(dead_code)]
92pub fn asymmetry_noise(x: f32, y: f32, z: f32, seed: u64) -> f32 {
93    // Encode the three floats into integers and mix with LCG.
94    let ix = (x * 1000.0) as i64 as u64;
95    let iy = (y * 1000.0) as i64 as u64;
96    let iz = (z * 1000.0) as i64 as u64;
97
98    let mut state: u64 = seed
99        .wrapping_add(ix.wrapping_mul(2_654_435_761))
100        .wrapping_add(iy.wrapping_mul(2_246_822_519))
101        .wrapping_add(iz.wrapping_mul(3_266_489_917));
102
103    // Three LCG steps for better mixing.
104    state = state
105        .wrapping_mul(6_364_136_223_846_793_005)
106        .wrapping_add(1_442_695_040_888_963_407);
107    state = state
108        .wrapping_mul(6_364_136_223_846_793_005)
109        .wrapping_add(1_442_695_040_888_963_407);
110    state = state
111        .wrapping_mul(6_364_136_223_846_793_005)
112        .wrapping_add(1_442_695_040_888_963_407);
113
114    // Map to –1..1.
115    let u = (state >> 11) as f32 / (1u64 << 53) as f32; // 0..1
116    u * 2.0 - 1.0
117}
118
119/// For each vertex with x > 0 find the best match at (−x, y, z) within tolerance.
120///
121/// Returns `(pos_x_index, neg_x_index)` pairs (no duplicates).
122#[allow(dead_code)]
123pub fn find_symmetry_pairs_x(positions: &[[f32; 3]], tolerance: f32) -> Vec<(usize, usize)> {
124    let mut pairs = Vec::new();
125    let mut used_neg: Vec<bool> = vec![false; positions.len()];
126
127    for (i, &pi) in positions.iter().enumerate() {
128        if pi[0] <= 0.0 {
129            continue;
130        }
131        // target: (−x, y, z)
132        let tx = -pi[0];
133        let ty = pi[1];
134        let tz = pi[2];
135
136        let mut best_j = None;
137        let mut best_d = tolerance;
138
139        for (j, &pj) in positions.iter().enumerate() {
140            if used_neg[j] {
141                continue;
142            }
143            let dx = pj[0] - tx;
144            let dy = pj[1] - ty;
145            let dz = pj[2] - tz;
146            let d = (dx * dx + dy * dy + dz * dz).sqrt();
147            if d <= best_d {
148                best_d = d;
149                best_j = Some(j);
150            }
151        }
152
153        if let Some(j) = best_j {
154            used_neg[j] = true;
155            pairs.push((i, j));
156        }
157    }
158
159    pairs
160}
161
162/// Enforce symmetry for each pair: average both sides (blended by `cfg.blend`).
163#[allow(dead_code)]
164pub fn enforce_symmetry(
165    positions: &mut [[f32; 3]],
166    pairs: &[(usize, usize)],
167    cfg: &SymmetryConfig,
168) {
169    let ax = cfg.axis.axis as usize;
170    for &(a, b) in pairs {
171        if a >= positions.len() || b >= positions.len() {
172            continue;
173        }
174
175        // Symmetric target: average the non-axis components, mirror axis.
176        let pa = positions[a];
177        let pb = positions[b];
178
179        // The non-axis coords should be averaged; the axis coords should be equal/opposite.
180        let avg_non_ax_y = if ax != 1 { (pa[1] + pb[1]) * 0.5 } else { 0.0 };
181        let avg_non_ax_z = if ax != 2 { (pa[2] + pb[2]) * 0.5 } else { 0.0 };
182
183        // For X-axis symmetry: a is +x side, b is −x side.
184        // We want: positions[a][0] = +|avg_x|, positions[b][0] = -|avg_x|.
185        let sym_ax = (pa[ax].abs() + pb[ax].abs()) * 0.5;
186
187        let mut new_a = pa;
188        let mut new_b = pb;
189
190        // Blend toward symmetric target.
191        let b_factor = cfg.blend;
192
193        if ax == 0 {
194            new_a[0] = pa[0] * (1.0 - b_factor) + sym_ax * b_factor;
195            new_b[0] = pb[0] * (1.0 - b_factor) + (-sym_ax) * b_factor;
196            new_a[1] = pa[1] * (1.0 - b_factor) + avg_non_ax_y * b_factor;
197            new_b[1] = pb[1] * (1.0 - b_factor) + avg_non_ax_y * b_factor;
198            new_a[2] = pa[2] * (1.0 - b_factor) + avg_non_ax_z * b_factor;
199            new_b[2] = pb[2] * (1.0 - b_factor) + avg_non_ax_z * b_factor;
200        } else {
201            // Generic: mirror along the given axis.
202            let avg_ax = (pa[ax] + pb[ax]) * 0.5;
203            new_a[ax] = pa[ax] * (1.0 - b_factor) + avg_ax * b_factor;
204            new_b[ax] = pb[ax] * (1.0 - b_factor) + avg_ax * b_factor;
205        }
206
207        positions[a] = new_a;
208        positions[b] = new_b;
209    }
210}
211
212/// Compute a symmetry report for `positions` given known pairs.
213#[allow(dead_code)]
214pub fn symmetry_report(
215    positions: &[[f32; 3]],
216    pairs: &[(usize, usize)],
217    axis: &SymmetryAxis,
218) -> SymmetryReport {
219    let tolerance = 0.001;
220    let paired_vertices = pairs.len() * 2;
221    let unpaired_vertices = positions.len().saturating_sub(paired_vertices);
222
223    let symmetry_error = if pairs.is_empty() {
224        0.0
225    } else {
226        let total: f32 = pairs
227            .iter()
228            .filter_map(|&(a, b)| {
229                if a < positions.len() && b < positions.len() {
230                    let mirrored = mirror_position(positions[b], axis);
231                    let dx = positions[a][0] - mirrored[0];
232                    let dy = positions[a][1] - mirrored[1];
233                    let dz = positions[a][2] - mirrored[2];
234                    Some((dx * dx + dy * dy + dz * dz).sqrt())
235                } else {
236                    None
237                }
238            })
239            .sum();
240        total / pairs.len() as f32
241    };
242
243    SymmetryReport {
244        paired_vertices,
245        unpaired_vertices,
246        symmetry_error,
247        is_symmetric: symmetry_error < tolerance,
248    }
249}
250
251/// Inject subtle LCG-noise asymmetry into vertex positions.
252#[allow(dead_code)]
253pub fn inject_asymmetry(positions: &mut [[f32; 3]], cfg: &AsymmetryConfig) {
254    if cfg.strength <= 0.0 {
255        return;
256    }
257    for p in positions.iter_mut() {
258        let nx = asymmetry_noise(
259            p[0] * cfg.frequency,
260            p[1] * cfg.frequency,
261            p[2] * cfg.frequency,
262            cfg.seed,
263        );
264        let ny = asymmetry_noise(
265            p[1] * cfg.frequency,
266            p[2] * cfg.frequency,
267            p[0] * cfg.frequency,
268            cfg.seed.wrapping_add(1),
269        );
270        let nz = asymmetry_noise(
271            p[2] * cfg.frequency,
272            p[0] * cfg.frequency,
273            p[1] * cfg.frequency,
274            cfg.seed.wrapping_add(2),
275        );
276        p[0] += nx * cfg.strength;
277        p[1] += ny * cfg.strength;
278        p[2] += nz * cfg.strength;
279    }
280}
281
282/// Make morph deltas symmetric: `delta[a]` = `delta[b]` mirrored (average both).
283#[allow(dead_code)]
284pub fn symmetrize_morph_deltas(
285    deltas: &mut [[f32; 3]],
286    pairs: &[(usize, usize)],
287    axis: &SymmetryAxis,
288) {
289    for &(a, b) in pairs {
290        if a >= deltas.len() || b >= deltas.len() {
291            continue;
292        }
293        let da = deltas[a];
294        let db = deltas[b];
295
296        // Mirror b's delta, then average.
297        let db_mirrored = mirror_position(db, axis);
298
299        let sym_a = [
300            (da[0] + db_mirrored[0]) * 0.5,
301            (da[1] + db_mirrored[1]) * 0.5,
302            (da[2] + db_mirrored[2]) * 0.5,
303        ];
304        let sym_b = mirror_position(sym_a, axis);
305
306        deltas[a] = sym_a;
307        deltas[b] = sym_b;
308    }
309}
310
311// ---------------------------------------------------------------------------
312// Tests
313// ---------------------------------------------------------------------------
314
315#[cfg(test)]
316mod tests {
317    use super::*;
318
319    #[test]
320    fn test_mirror_position_x_axis() {
321        let p = [1.0f32, 2.0, 3.0];
322        let m = mirror_position(p, &SymmetryAxis::X);
323        assert!((m[0] - (-1.0)).abs() < 1e-6);
324        assert!((m[1] - 2.0).abs() < 1e-6);
325        assert!((m[2] - 3.0).abs() < 1e-6);
326    }
327
328    #[test]
329    fn test_mirror_position_y_axis() {
330        let p = [1.0f32, 2.0, 3.0];
331        let m = mirror_position(p, &SymmetryAxis::Y);
332        assert!((m[0] - 1.0).abs() < 1e-6);
333        assert!((m[1] - (-2.0)).abs() < 1e-6);
334        assert!((m[2] - 3.0).abs() < 1e-6);
335    }
336
337    #[test]
338    fn test_mirror_position_z_axis() {
339        let p = [1.0f32, 2.0, 3.0];
340        let m = mirror_position(p, &SymmetryAxis::Z);
341        assert!((m[0] - 1.0).abs() < 1e-6);
342        assert!((m[1] - 2.0).abs() < 1e-6);
343        assert!((m[2] - (-3.0)).abs() < 1e-6);
344    }
345
346    #[test]
347    fn test_symmetry_report_no_pairs() {
348        let positions = vec![[0.0f32; 3]; 4];
349        let report = symmetry_report(&positions, &[], &SymmetryAxis::X);
350        assert_eq!(report.paired_vertices, 0);
351        assert_eq!(report.symmetry_error, 0.0);
352        assert!(report.is_symmetric);
353    }
354
355    #[test]
356    fn test_find_symmetry_pairs_x_symmetric_mesh() {
357        // 4 vertices: two on each side of X=0
358        let positions: Vec<[f32; 3]> = vec![
359            [1.0, 0.0, 0.0],
360            [2.0, 0.0, 0.0],
361            [-1.0, 0.0, 0.0],
362            [-2.0, 0.0, 0.0],
363        ];
364        let pairs = find_symmetry_pairs_x(&positions, 0.01);
365        assert_eq!(pairs.len(), 2);
366    }
367
368    #[test]
369    fn test_find_symmetry_pairs_x_asymmetric_mesh() {
370        // All vertices on same side – no pairs.
371        let positions: Vec<[f32; 3]> = vec![[1.0, 0.0, 0.0], [2.0, 0.0, 0.0], [3.0, 0.0, 0.0]];
372        let pairs = find_symmetry_pairs_x(&positions, 0.01);
373        assert_eq!(pairs.len(), 0);
374    }
375
376    #[test]
377    fn test_enforce_symmetry_makes_positions_symmetric() {
378        let mut positions: Vec<[f32; 3]> = vec![
379            [1.0, 0.0, 0.0],  // index 0: +x side
380            [-1.1, 0.0, 0.0], // index 1: −x side (slightly off)
381        ];
382        let pairs = vec![(0usize, 1usize)];
383        let cfg = SymmetryConfig::default(); // blend=1.0
384        enforce_symmetry(&mut positions, &pairs, &cfg);
385        // After full symmetry: pos[0][0] should be positive, pos[1][0] negative, same abs.
386        assert!(positions[0][0] > 0.0);
387        assert!(positions[1][0] < 0.0);
388        assert!((positions[0][0] + positions[1][0]).abs() < 1e-4);
389    }
390
391    #[test]
392    fn test_inject_asymmetry_changes_positions() {
393        let original = vec![[0.0f32, 0.0, 0.0]; 4];
394        let mut positions = original.clone();
395        let cfg = AsymmetryConfig {
396            strength: 0.1,
397            frequency: 1.0,
398            seed: 1234,
399        };
400        inject_asymmetry(&mut positions, &cfg);
401        let changed = positions.iter().any(|&p| p != [0.0, 0.0, 0.0]);
402        assert!(changed);
403    }
404
405    #[test]
406    fn test_inject_asymmetry_zero_strength_no_change() {
407        let original = vec![[1.0f32, 2.0, 3.0]; 4];
408        let mut positions = original.clone();
409        let cfg = AsymmetryConfig {
410            strength: 0.0,
411            frequency: 1.0,
412            seed: 99,
413        };
414        inject_asymmetry(&mut positions, &cfg);
415        assert_eq!(positions, original);
416    }
417
418    #[test]
419    fn test_symmetrize_morph_deltas_makes_deltas_mirror() {
420        let mut deltas: Vec<[f32; 3]> = vec![[1.0, 0.0, 0.0], [0.5, 0.0, 0.0]];
421        let pairs = vec![(0usize, 1usize)];
422        symmetrize_morph_deltas(&mut deltas, &pairs, &SymmetryAxis::X);
423        // Algorithm: mirror db=[0.5,0,0] along X → [-0.5,0,0]
424        // sym_a = avg(da, db_mirrored) = avg([1,0,0],[-0.5,0,0]) = [0.25, 0, 0]
425        // sym_b = mirror(sym_a) = [-0.25, 0, 0]
426        assert!(
427            (deltas[0][0] - 0.25).abs() < 1e-5,
428            "deltas[0]={:?}",
429            deltas[0]
430        );
431        assert!(
432            (deltas[1][0] - (-0.25)).abs() < 1e-5,
433            "deltas[1]={:?}",
434            deltas[1]
435        );
436        // The result IS mirrored: deltas[1] = -deltas[0] in X component
437        assert!((deltas[0][0] + deltas[1][0]).abs() < 1e-5);
438    }
439
440    #[test]
441    fn test_asymmetry_noise_deterministic() {
442        let n1 = asymmetry_noise(1.0, 2.0, 3.0, 42);
443        let n2 = asymmetry_noise(1.0, 2.0, 3.0, 42);
444        assert_eq!(n1, n2);
445    }
446
447    #[test]
448    fn test_asymmetry_noise_different_seeds_differ() {
449        let n1 = asymmetry_noise(1.0, 2.0, 3.0, 42);
450        let n2 = asymmetry_noise(1.0, 2.0, 3.0, 43);
451        assert_ne!(n1, n2);
452    }
453
454    #[test]
455    fn test_asymmetry_noise_in_range() {
456        let n = asymmetry_noise(0.5, 1.5, -0.3, 7);
457        assert!((-1.0..=1.0).contains(&n), "n={n}");
458    }
459
460    #[test]
461    fn test_symmetry_report_with_pairs() {
462        // Perfectly symmetric: +1 / -1
463        let positions: Vec<[f32; 3]> = vec![[1.0, 0.0, 0.0], [-1.0, 0.0, 0.0]];
464        let pairs = vec![(0usize, 1usize)];
465        let report = symmetry_report(&positions, &pairs, &SymmetryAxis::X);
466        assert_eq!(report.paired_vertices, 2);
467        assert!(
468            report.symmetry_error < 0.001,
469            "err={}",
470            report.symmetry_error
471        );
472        assert!(report.is_symmetric);
473    }
474}