Skip to main content

jagua_rs/geometry/
allowed_orientations.rs

1use std::f32::consts::{PI, TAU};
2
3use anyhow::{Result, ensure};
4
5use crate::geometry::geo_enums::RotationRange;
6use crate::geometry::{DTransformation, normalize_rotation};
7
8/// Permitted rotations without reflection and after canonical x-axis reflection.
9///
10/// Choose no reflection or one permitted local reflection axis, then an allowed rotation.
11/// Reflecting across an axis at angle `a`, followed by rotation `r`, is represented by
12/// [`DTransformation`] with `reflected = true` and rotation `r + 2*a`.
13/// All angles in this type are radians. Translation is unrestricted.
14#[derive(Clone, Debug, PartialEq)]
15pub struct AllowedOrientations {
16    rotations: RotationRange,
17    rotations_after_reflection: Option<RotationRange>,
18}
19
20impl AllowedOrientations {
21    /// Maximum entries per angle list and per reflected Cartesian expansion,
22    /// before deduplication of the resulting angles. Shared by JSON rotation modes.
23    pub const MAX_ANGLES: usize = 65_536;
24
25    /// Validates finite angles and normalizes rotations modulo 2π and axes modulo π.
26    /// Empty discrete rotations mean fixed zero; empty axes disable reflection.
27    /// Duplicate canonical angles are removed by exact equality after normalization.
28    pub fn new(rotations: RotationRange, mut reflection_axes: Vec<f32>) -> Result<Self> {
29        ensure!(
30            reflection_axes.len() <= Self::MAX_ANGLES,
31            "reflection axis list exceeds the count limit"
32        );
33        let rotations = match rotations {
34            RotationRange::Discrete(angles) => discrete_rotations(angles)?,
35            RotationRange::None => RotationRange::None,
36            RotationRange::Continuous => RotationRange::Continuous,
37        };
38        ensure!(
39            reflection_axes.iter().all(|a| a.is_finite()),
40            "reflection axes must be finite"
41        );
42        for axis in &mut reflection_axes {
43            *axis = axis.rem_euclid(PI) % PI;
44        }
45        reflection_axes.sort_by(f32::total_cmp);
46        reflection_axes.dedup();
47        let rotations_after_reflection = if reflection_axes.is_empty() {
48            None
49        } else {
50            Some(match &rotations {
51                RotationRange::Continuous => RotationRange::Continuous,
52                RotationRange::None => {
53                    discrete_rotations(reflection_axes.iter().map(|a| 2.0 * a).collect())?
54                }
55                RotationRange::Discrete(angles) => {
56                    ensure!(
57                        angles.len() <= Self::MAX_ANGLES / reflection_axes.len(),
58                        "reflected orientation expansion exceeds the count limit"
59                    );
60                    discrete_rotations(
61                        reflection_axes
62                            .iter()
63                            .flat_map(|a| angles.iter().map(move |r| r + 2.0 * a))
64                            .collect(),
65                    )?
66                }
67            })
68        };
69        Ok(Self {
70            rotations,
71            rotations_after_reflection,
72        })
73    }
74
75    /// Permitted rotations without reflection, in radians.
76    /// [`RotationRange::None`] means fixed zero.
77    #[must_use]
78    pub fn rotations(&self) -> &RotationRange {
79        &self.rotations
80    }
81
82    /// Permitted rotations after canonical x-axis reflection, in radians.
83    /// These are transformation angles, not reflection axes.
84    /// `None` disables reflection; [`RotationRange::None`] means fixed zero after reflection.
85    #[must_use]
86    pub fn rotations_after_reflection(&self) -> Option<&RotationRange> {
87        self.rotations_after_reflection.as_ref()
88    }
89
90    /// Checks orientation only, allowing 4 f32 epsilons of a full turn for matrix
91    /// decomposition and angle wrapping. This tolerance is not used to deduplicate inputs.
92    #[must_use]
93    pub fn allows(&self, transformation: &DTransformation) -> bool {
94        let rotation = transformation.rotation();
95        if !rotation.is_finite() {
96            return false;
97        }
98        let rotation = normalize_rotation(rotation);
99        let matches = |r: f32| {
100            let distance = (rotation - r).abs();
101            distance.min(TAU - distance) <= 4.0 * f32::EPSILON * TAU
102        };
103        let rotations = if transformation.reflected {
104            self.rotations_after_reflection()
105        } else {
106            Some(self.rotations())
107        };
108        match rotations {
109            None => false,
110            Some(RotationRange::Continuous) => true,
111            Some(RotationRange::None) => matches(0.0),
112            Some(RotationRange::Discrete(angles)) => angles.iter().copied().any(matches),
113        }
114    }
115}
116
117fn discrete_rotations(mut angles: Vec<f32>) -> Result<RotationRange> {
118    ensure!(
119        angles.len() <= AllowedOrientations::MAX_ANGLES,
120        "rotation angle list exceeds the count limit"
121    );
122    ensure!(
123        angles.iter().all(|a| a.is_finite()),
124        "rotations must be finite"
125    );
126    for angle in &mut angles {
127        *angle = normalize_rotation(*angle);
128    }
129    angles.sort_by(f32::total_cmp);
130    angles.dedup();
131    Ok(if angles.is_empty() || angles == [0.0] {
132        RotationRange::None
133    } else {
134        RotationRange::Discrete(angles)
135    })
136}