use std::f32::consts::PI;
use glam::Vec3;
use indicatrix_formats::asc::{AscSchedule, AscTier};
use crate::geometry::{cuts::StandardGemCuts, plane::GpuFacetPlane};
#[test]
fn index_to_azimuth_zero_reference_angle_is_bit_for_bit_identical_to_the_plain_formula() {
for &gear_teeth in &[16.0f32, 32.0, 64.0, 80.0, 96.0, 120.0] {
for idx in 0..gear_teeth as i32 {
let index = idx as f32;
let old = 2.0 * PI * index / gear_teeth;
let new = StandardGemCuts::index_to_azimuth(index, gear_teeth, 0.0);
assert_eq!(
old.to_bits(),
new.to_bits(),
"index {index}, gear {gear_teeth}: old={old}, new={new}"
);
}
}
}
#[test]
fn index_to_azimuth_reference_angle_is_additive_on_the_index() {
let gear_teeth = 96.0f32;
for &(index, reference_angle) in &[(0.0, 48.0), (5.0, 20.0), (84.0, -6.0)] {
let via_reference_angle =
StandardGemCuts::index_to_azimuth(index, gear_teeth, reference_angle);
let via_shifted_index =
StandardGemCuts::index_to_azimuth(index + reference_angle, gear_teeth, 0.0);
assert!(
(via_reference_angle - via_shifted_index).abs() < 1e-6,
"index {index}, reference angle {reference_angle}"
);
}
}
#[test]
fn from_asc_schedule_zero_reference_angle_reproduces_the_previous_mapping_bit_for_bit() {
let gear_teeth = 96.0f32;
let mast = 0.6f64;
let indices = vec![3.0, 17.0, 55.0, 90.0];
let schedule = AscSchedule {
gear_teeth: 96,
gear_reference_angle: 0.0,
symmetry_order: 1,
refractive_index: 1.0,
tiers: vec![AscTier {
angle_deg: -41.0,
mast,
indices: indices.clone(),
..Default::default()
}],
..Default::default()
};
let planes = StandardGemCuts::from_asc_schedule(&schedule);
assert_eq!(planes.len(), indices.len());
let theta = 41.0f32.to_radians();
let d = -(mast.abs() as f32);
for (&idx, plane) in indices.iter().zip(&planes) {
let phi = 2.0 * PI * (idx as f32) / gear_teeth; let expected = GpuFacetPlane::new(
Vec3::new(
theta.sin() * phi.cos(),
-theta.cos(),
theta.sin() * phi.sin(),
),
d,
);
assert_eq!(*plane, expected, "index {idx}");
}
}
#[test]
fn from_asc_schedule_asymmetric_indices_map_to_expected_azimuths_with_reference_angle() {
let gear_teeth = 96.0f32;
let reference_angle = 48.0f64; let mast = 0.6f64;
let indices = vec![5.0, 20.0, 47.0];
let schedule = AscSchedule {
gear_teeth: 96,
gear_reference_angle: reference_angle,
symmetry_order: 1,
refractive_index: 1.0,
tiers: vec![AscTier {
angle_deg: 40.0,
mast,
indices: indices.clone(),
..Default::default()
}],
..Default::default()
};
let planes = StandardGemCuts::from_asc_schedule(&schedule);
assert_eq!(planes.len(), indices.len());
let theta = 40.0f32.to_radians();
for (&idx, plane) in indices.iter().zip(&planes) {
let phi = 2.0 * PI * (idx as f32 + reference_angle as f32) / gear_teeth;
let expected = [
theta.sin() * phi.cos(),
theta.cos(),
theta.sin() * phi.sin(),
];
for (component, &value) in plane.normal.iter().zip(expected.iter()) {
assert!(
(component - value).abs() < 1e-5,
"index {idx}: got {component}, expected {value}"
);
}
}
}
#[test]
fn from_asc_schedule_symmetric_design_is_globally_rotated_by_reference_angle() {
let gear_teeth = 96.0f32;
let mast = 0.59f64;
let indices = vec![0.0, 12.0, 24.0, 36.0, 48.0, 60.0, 72.0, 84.0];
let reference_angle = 6.0f64;
let base = AscSchedule {
gear_teeth: 96,
gear_reference_angle: 0.0,
symmetry_order: 8,
refractive_index: 1.0,
tiers: vec![AscTier {
angle_deg: 34.5,
mast,
indices: indices.clone(),
..Default::default()
}],
..Default::default()
};
let mut rotated = base.clone();
rotated.gear_reference_angle = reference_angle;
let planes_base = StandardGemCuts::from_asc_schedule(&base);
let planes_rotated = StandardGemCuts::from_asc_schedule(&rotated);
assert_eq!(planes_base.len(), planes_rotated.len());
assert_eq!(planes_base.len(), indices.len());
let delta = 2.0 * PI * reference_angle as f32 / gear_teeth;
let (sin_delta, cos_delta) = (delta.sin(), delta.cos());
for base_plane in &planes_base {
let n = base_plane.normal;
let rotated_normal = [
n[0].mul_add(cos_delta, -(n[2] * sin_delta)),
n[1],
n[0].mul_add(sin_delta, n[2] * cos_delta),
];
let matches_one = planes_rotated.iter().any(|p| {
(p.normal[0] - rotated_normal[0]).abs() < 1e-5
&& (p.normal[1] - rotated_normal[1]).abs() < 1e-5
&& (p.normal[2] - rotated_normal[2]).abs() < 1e-5
&& (p.d - base_plane.d).abs() < 1e-5
});
assert!(
matches_one,
"base normal {n:?} rotated by {delta} rad must reappear in the \
reference-angle-shifted schedule's plane set"
);
}
}