use glam::{DVec3, Vec3};
use indicatrix_formats::asc::AscSchedule;
use super::{CutError, StandardGemCuts};
use crate::geometry::{
brep::GemPolyhedron,
plane::{GpuFacetPlane, tier_is_crown_side},
};
impl StandardGemCuts {
#[must_use]
pub fn from_asc_schedule(schedule: &AscSchedule) -> Vec<GpuFacetPlane> {
let (planes, _) = Self::raw_asc_planes(schedule, schedule.gear_reference_angle);
dedup_planes(planes)
}
#[must_use]
pub fn asc_plane_provenance(schedule: &AscSchedule) -> Vec<Vec<usize>> {
let (planes, ends) = Self::raw_asc_planes(schedule, schedule.gear_reference_angle);
let representatives = dedup_representatives(&planes);
let mut output_indices = vec![0; planes.len()];
let mut count = 0;
for (index, &representative) in representatives.iter().enumerate() {
if index == representative {
output_indices[index] = count;
count += 1;
} else {
output_indices[index] = output_indices[representative];
}
}
let mut start = 0;
ends.into_iter()
.map(|end| {
let tier = output_indices[start..end].to_vec();
start = end;
tier
})
.collect()
}
#[must_use]
pub fn asc_tier_plane_boundaries(
schedule: &AscSchedule,
gear_reference_angle: f64,
) -> Vec<usize> {
let (planes, raw_ends) = Self::raw_asc_planes(schedule, gear_reference_angle);
let keep = dedup_keep_mask(&planes);
let mut kept_before = Vec::with_capacity(keep.len() + 1);
kept_before.push(0usize);
for &kept in &keep {
kept_before.push(kept_before[kept_before.len() - 1] + usize::from(kept));
}
raw_ends.into_iter().map(|end| kept_before[end]).collect()
}
#[expect(
clippy::cast_possible_truncation,
reason = "the schedule's f64 angles, masts and indices become the f32 GPU plane data; all are small normal-range values"
)]
fn raw_asc_planes(
schedule: &AscSchedule,
gear_reference_angle: f64,
) -> (Vec<GpuFacetPlane>, Vec<usize>) {
let gear_teeth = (schedule.gear_teeth_abs().max(1)) as f32;
let gear_reference_angle = gear_reference_angle as f32;
let mut planes = Vec::with_capacity(schedule.facet_plane_count());
let mut ends = Vec::with_capacity(schedule.tiers.len());
for tier in &schedule.tiers {
let is_crown = tier_is_crown_side(tier.angle_deg);
let theta = (tier.angle_deg.abs() as f32).to_radians();
let sin_theta = theta.sin();
let cos_theta = theta.cos();
let d = -(tier.mast.abs() as f32);
if tier.indices.is_empty() {
let n = if is_crown {
Vec3::new(0.0, cos_theta, sin_theta)
} else {
Vec3::new(0.0, -cos_theta, sin_theta)
};
planes.push(GpuFacetPlane::new(n, d));
ends.push(planes.len());
continue;
}
for &idx in &tier.indices {
let phi = Self::index_to_azimuth(idx as f32, gear_teeth, gear_reference_angle);
let (sin_phi, cos_phi) = (phi.sin(), phi.cos());
let n = if is_crown {
Vec3::new(sin_theta * cos_phi, cos_theta, sin_theta * sin_phi)
} else {
Vec3::new(sin_theta * cos_phi, -cos_theta, sin_theta * sin_phi)
};
planes.push(GpuFacetPlane::new(n, d));
}
ends.push(planes.len());
}
(planes, ends)
}
pub fn reconstruct_validated_brep_from_asc(
schedule: &AscSchedule,
) -> Result<GemPolyhedron, CutError> {
let planes = Self::from_asc_schedule(schedule);
let plane_count = planes.len();
let hull = GemPolyhedron::from_planes(planes)?;
let untouched = hull.untouched_planes();
if untouched.is_empty() {
Ok(hull)
} else {
Err(CutError::OverConstrained {
untouched_count: untouched.len(),
plane_count,
untouched,
})
}
}
}
pub(super) fn dedup_planes(planes: Vec<GpuFacetPlane>) -> Vec<GpuFacetPlane> {
let keep = dedup_keep_mask(&planes);
planes
.into_iter()
.zip(keep)
.filter_map(|(plane, kept)| kept.then_some(plane))
.collect()
}
fn dedup_keep_mask(planes: &[GpuFacetPlane]) -> Vec<bool> {
dedup_representatives(planes)
.into_iter()
.enumerate()
.map(|(index, representative)| index == representative)
.collect()
}
fn dedup_representatives(planes: &[GpuFacetPlane]) -> Vec<usize> {
const OFFSET_REL_EPSILON: f32 = 1.0 / 2048.0;
let mut kept: Vec<usize> = Vec::with_capacity(planes.len());
let mut representatives = Vec::with_capacity(planes.len());
for (index, plane) in planes.iter().enumerate() {
let pn = plane_normal_f64(plane);
let representative = kept.iter().copied().find(|&i| {
let k = &planes[i];
let offset_scale = k.d.abs().max(plane.d.abs()).max(1.0);
normals_coincide(plane_normal_f64(k), pn)
&& (k.d - plane.d).abs() <= OFFSET_REL_EPSILON * offset_scale
});
let representative = representative.unwrap_or_else(|| {
kept.push(index);
index
});
representatives.push(representative);
}
representatives
}
fn plane_normal_f64(plane: &GpuFacetPlane) -> DVec3 {
DVec3::new(
f64::from(plane.normal[0]),
f64::from(plane.normal[1]),
f64::from(plane.normal[2]),
)
}
#[must_use]
pub fn normals_coincide(a: DVec3, b: DVec3) -> bool {
const NORMAL_COINCIDE_EPSILON: f64 = 2e-7;
let (a_hat, b_hat) = (a.normalize_or_zero(), b.normalize_or_zero());
1.0 - a_hat.dot(b_hat) <= NORMAL_COINCIDE_EPSILON
}