Skip to main content

oxihuman_morph/
body_symmetry_v2.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan) / SPDX-License-Identifier: Apache-2.0
2#![allow(dead_code)]
3
4//! Enforce bilateral vertex symmetry by axis mirroring (v2).
5
6/// Symmetry axis.
7#[allow(dead_code)]
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9pub enum SymmetryAxis {
10    X,
11    Y,
12    Z,
13}
14
15/// Body symmetry v2 parameters.
16#[allow(dead_code)]
17#[derive(Debug, Clone, PartialEq)]
18pub struct BodySymmetryV2Params {
19    /// Symmetry blend weight: 0 = no correction, 1 = fully symmetric.
20    pub symmetry_weight: f32,
21    /// Axis of symmetry.
22    pub axis: SymmetryAxis,
23    /// Tolerance for matching mirror vertices.
24    pub position_tolerance: f32,
25    /// Which side is the master (true = positive side).
26    pub positive_side_master: bool,
27}
28
29impl Default for BodySymmetryV2Params {
30    fn default() -> Self {
31        Self {
32            symmetry_weight: 1.0,
33            axis: SymmetryAxis::X,
34            position_tolerance: 0.001,
35            positive_side_master: true,
36        }
37    }
38}
39
40/// Create default params.
41#[allow(dead_code)]
42pub fn default_body_symmetry_v2_params() -> BodySymmetryV2Params {
43    BodySymmetryV2Params::default()
44}
45
46/// Mirror a 3D position across the given axis.
47#[allow(dead_code)]
48pub fn mirror_position(pos: [f32; 3], axis: SymmetryAxis) -> [f32; 3] {
49    match axis {
50        SymmetryAxis::X => [-pos[0], pos[1], pos[2]],
51        SymmetryAxis::Y => [pos[0], -pos[1], pos[2]],
52        SymmetryAxis::Z => [pos[0], pos[1], -pos[2]],
53    }
54}
55
56/// Check if two positions are mirror-symmetric within tolerance.
57#[allow(dead_code)]
58pub fn are_mirror_pair(a: [f32; 3], b: [f32; 3], axis: SymmetryAxis, tol: f32) -> bool {
59    let mirrored = mirror_position(a, axis);
60    let dx = (mirrored[0] - b[0]).abs();
61    let dy = (mirrored[1] - b[1]).abs();
62    let dz = (mirrored[2] - b[2]).abs();
63    dx < tol && dy < tol && dz < tol
64}
65
66/// Compute the symmetrized position given master and slave.
67#[allow(dead_code)]
68pub fn symmetrize_position(
69    master: [f32; 3],
70    slave: [f32; 3],
71    axis: SymmetryAxis,
72    weight: f32,
73) -> [f32; 3] {
74    let w = weight.clamp(0.0, 1.0);
75    let target = mirror_position(master, axis);
76    [
77        slave[0] + (target[0] - slave[0]) * w,
78        slave[1] + (target[1] - slave[1]) * w,
79        slave[2] + (target[2] - slave[2]) * w,
80    ]
81}
82
83/// Apply symmetry correction to a buffer of vertex positions.
84///
85/// `pairs`: list of (master_idx, slave_idx) index pairs.
86#[allow(dead_code)]
87pub fn apply_symmetry(
88    positions: &mut [[f32; 3]],
89    pairs: &[(usize, usize)],
90    params: &BodySymmetryV2Params,
91) {
92    for &(master, slave) in pairs {
93        if master < positions.len() && slave < positions.len() {
94            let m_pos = positions[master];
95            let s_pos = positions[slave];
96            let axis = params.axis;
97            let w = params.symmetry_weight;
98            if params.positive_side_master {
99                positions[slave] = symmetrize_position(m_pos, s_pos, axis, w);
100            } else {
101                positions[master] = symmetrize_position(s_pos, m_pos, axis, w);
102            }
103        }
104    }
105}
106
107/// Set symmetry weight.
108#[allow(dead_code)]
109pub fn set_symmetry_weight(params: &mut BodySymmetryV2Params, value: f32) {
110    params.symmetry_weight = value.clamp(0.0, 1.0);
111}
112
113/// Reset to default.
114#[allow(dead_code)]
115pub fn reset_body_symmetry_v2(params: &mut BodySymmetryV2Params) {
116    *params = BodySymmetryV2Params::default();
117}
118
119/// Serialize to JSON.
120#[allow(dead_code)]
121pub fn body_symmetry_v2_to_json(params: &BodySymmetryV2Params) -> String {
122    let axis = match params.axis {
123        SymmetryAxis::X => "X",
124        SymmetryAxis::Y => "Y",
125        SymmetryAxis::Z => "Z",
126    };
127    format!(
128        r#"{{"symmetry_weight":{:.4},"axis":"{}","tolerance":{:.6},"positive_master":{}}}"#,
129        params.symmetry_weight, axis, params.position_tolerance, params.positive_side_master
130    )
131}
132
133#[cfg(test)]
134mod tests {
135    use super::*;
136
137    #[test]
138    fn test_default() {
139        let p = BodySymmetryV2Params::default();
140        assert!((p.symmetry_weight - 1.0).abs() < 1e-6);
141    }
142
143    #[test]
144    fn test_mirror_x() {
145        let m = mirror_position([1.0, 2.0, 3.0], SymmetryAxis::X);
146        assert!((m[0] + 1.0).abs() < 1e-6);
147        assert!((m[1] - 2.0).abs() < 1e-6);
148    }
149
150    #[test]
151    fn test_mirror_y() {
152        let m = mirror_position([1.0, 2.0, 3.0], SymmetryAxis::Y);
153        assert!((m[1] + 2.0).abs() < 1e-6);
154    }
155
156    #[test]
157    fn test_mirror_z() {
158        let m = mirror_position([1.0, 2.0, 3.0], SymmetryAxis::Z);
159        assert!((m[2] + 3.0).abs() < 1e-6);
160    }
161
162    #[test]
163    fn test_are_mirror_pair_true() {
164        let a = [1.0f32, 0.0, 0.0];
165        let b = [-1.0f32, 0.0, 0.0];
166        assert!(are_mirror_pair(a, b, SymmetryAxis::X, 0.001));
167    }
168
169    #[test]
170    fn test_are_mirror_pair_false() {
171        let a = [1.0f32, 0.0, 0.0];
172        let b = [1.0f32, 0.0, 0.0];
173        assert!(!are_mirror_pair(a, b, SymmetryAxis::X, 0.001));
174    }
175
176    #[test]
177    fn test_symmetrize_full() {
178        let master = [1.0f32, 0.5, 0.5];
179        let slave = [0.0f32, 0.5, 0.5];
180        let result = symmetrize_position(master, slave, SymmetryAxis::X, 1.0);
181        assert!((result[0] + 1.0).abs() < 1e-6);
182    }
183
184    #[test]
185    fn test_apply_symmetry() {
186        let mut positions = [[1.0f32, 0.0, 0.0], [0.5f32, 0.0, 0.0]];
187        let params = BodySymmetryV2Params::default();
188        let pairs = vec![(0usize, 1usize)];
189        apply_symmetry(&mut positions, &pairs, &params);
190        assert!((positions[1][0] + 1.0).abs() < 1e-6);
191    }
192
193    #[test]
194    fn test_set_weight_clamp() {
195        let mut p = BodySymmetryV2Params::default();
196        set_symmetry_weight(&mut p, 5.0);
197        assert!((p.symmetry_weight - 1.0).abs() < 1e-6);
198    }
199
200    #[test]
201    fn test_to_json() {
202        let j = body_symmetry_v2_to_json(&BodySymmetryV2Params::default());
203        assert!(j.contains("symmetry_weight"));
204        assert!(j.contains("axis"));
205    }
206}