use super::super::geom::{mesh_point, point_inside_mesh_agreed, project_aabb_in_frame};
use super::super::{OpeningFrame, NORMALIZE_EPSILON};
use crate::{Mesh, Point3, Vector3};
const CAP_PARALLEL_COS: f64 = 0.985;
const BAND_FRACTION: f64 = 1.0e-3;
pub(super) const RING_BAND_FRACTION: f64 = 0.25;
pub(super) struct CutterFrame {
frame: OpeningFrame,
lo: Point3<f64>,
hi: Point3<f64>,
}
impl CutterFrame {
pub fn new(mesh: &Mesh, d: Vector3<f64>) -> Option<Self> {
let frame = OpeningFrame::from_depth(d)?;
let (lo, hi) = project_aabb_in_frame(
mesh,
&[frame.cross_a, frame.cross_b, frame.depth],
Vector3::zeros(),
)?;
Some(Self { frame, lo, hi })
}
pub fn depth(&self) -> Vector3<f64> {
self.frame.depth
}
pub fn shrink(&self) -> f64 {
self.cap_band().min(self.span() * RING_BAND_FRACTION)
}
pub fn caps(&self) -> (f64, f64) {
(self.lo.z, self.hi.z)
}
pub fn span(&self) -> f64 {
self.hi.z - self.lo.z
}
fn cap_band(&self) -> f64 {
self.span().max(1.0) * BAND_FRACTION
}
fn point_at(&self, u: f64, v: f64, s: f64) -> Point3<f64> {
Point3::from(self.frame.cross_a * u + self.frame.cross_b * v + self.frame.depth * s)
}
fn probe_uv(&self, host: &Mesh) -> Option<(f64, f64)> {
let (hlo, hhi) = project_aabb_in_frame(
host,
&[self.frame.cross_a, self.frame.cross_b, self.frame.depth],
Vector3::zeros(),
)?;
let clamp = |c: f64, lo: f64, hi: f64| c.max(lo).min(hi);
Some((
clamp((hlo.x + hhi.x) * 0.5, self.lo.x, self.hi.x),
clamp((hlo.y + hhi.y) * 0.5, self.lo.y, self.hi.y),
))
}
fn probe_offset(&self) -> f64 {
self.cap_band() * 4.0
}
}
pub(super) enum Cap {
Free,
Exit(f64),
Jamb,
}
pub(super) struct ExitCaps {
pub min: Cap,
pub max: Cap,
}
impl ExitCaps {
pub fn push_back(&self, omn: f64, pad: f64, shrink: f64) -> f64 {
match self.min {
Cap::Free => 0.0,
Cap::Exit(h) => (omn - h).max(0.0) + pad,
Cap::Jamb => -shrink,
}
}
pub fn push_fwd(&self, omx: f64, pad: f64, shrink: f64) -> f64 {
match self.max {
Cap::Free => 0.0,
Cap::Exit(h) => (h - omx).max(0.0) + pad,
Cap::Jamb => -shrink,
}
}
pub fn any_moves(&self) -> bool {
!matches!((&self.min, &self.max), (Cap::Free, Cap::Free))
}
pub fn min_moves(&self) -> bool {
!matches!(self.min, Cap::Free)
}
pub fn max_moves(&self) -> bool {
!matches!(self.max, Cap::Free)
}
}
pub(super) fn detect(host: &Mesh, f: &CutterFrame, pad: f64) -> ExitCaps {
let (omn, omx) = f.caps();
let (d, band) = (f.depth(), f.cap_band());
let (mut min_has_surface, mut max_has_surface) = (false, false);
let (mut host_at_min, mut host_at_max) = (omn, omx);
for t in host.indices.chunks_exact(3) {
let (Some(a), Some(b), Some(c)) = (
mesh_point(host, t[0]),
mesh_point(host, t[1]),
mesh_point(host, t[2]),
) else {
continue;
};
let n = (b - a).cross(&(c - a));
let nl = n.norm();
if nl < NORMALIZE_EPSILON {
continue;
}
if (n.dot(&d) / nl).abs() < CAP_PARALLEL_COS {
continue;
}
let s = a.coords.dot(&d);
if (s - omn).abs() <= band {
min_has_surface = true;
host_at_min = host_at_min.min(s);
}
if (s - omx).abs() <= band {
max_has_surface = true;
host_at_max = host_at_max.max(s);
}
}
let qualified = min_has_surface || max_has_surface;
let far_field = qualified
&& host
.positions
.iter()
.any(|&v| (v as f64).abs() >= crate::LARGE_COORD_THRESHOLD_METERS);
let probe = (qualified && !far_field)
.then(|| f.probe_uv(host))
.flatten();
let occupied_along = |from: f64, dir: f64| -> bool {
let Some((cu, cv)) = probe else {
return false;
};
[f.probe_offset(), pad * 0.5, pad].into_iter().any(|t| {
point_inside_mesh_agreed(host, f.point_at(cu, cv, from + dir * t)).unwrap_or(false)
})
};
let classify = |has_surface: bool, from: f64, dir: f64, host_at: f64| {
if !has_surface {
return Cap::Free;
}
if occupied_along(from, dir) {
Cap::Jamb
} else {
Cap::Exit(host_at)
}
};
ExitCaps {
min: classify(min_has_surface, omn, -1.0, host_at_min),
max: classify(max_has_surface, omx, 1.0, host_at_max),
}
}