use axiolid_contracts::{GeomError, GeomResult};
use axiolid_core::{Plane3, Point2, Scalar, Tolerance, Vec3};
use axiolid_mesh::TriMesh;
use axiolid_primitive::{ClipMargin, HalfSpace};
use crate::loft::{self, Frame, Station};
use crate::profile::Rings;
pub fn bounded_half_space(
boundary: &Rings,
plane: Plane3,
agreement: bool,
margin: ClipMargin,
tolerance: Tolerance,
) -> GeomResult<TriMesh> {
let normal = plane.normal.normalize_or_zero();
if normal == Vec3::ZERO {
return Err(GeomError::InvalidInput(
"half-space boundary plane needs a non-zero normal".to_owned(),
));
}
if boundary.outer.len() < 3 {
return Err(GeomError::InvalidInput(format!(
"half-space boundary needs at least 3 vertices, got {}",
boundary.outer.len()
)));
}
let mut extent: Scalar = 0.0;
for p in boundary.outer.iter().chain(boundary.holes.iter().flatten()) {
extent = extent.max(p.x.abs()).max(p.y.abs());
}
let depth = extent * margin.factor();
let reference = if normal.x.abs() < 0.9 {
Vec3::X
} else {
Vec3::Y
};
let base = Frame::from_reference(plane.origin, normal, reference)?;
let step = if agreement { normal } else { -normal };
let (x, y) = if agreement {
(base.x, base.y)
} else {
(base.x, -base.y)
};
let near = Frame {
origin: base.origin,
x,
y,
};
let far = Frame {
origin: base.origin + step * depth,
x,
y,
};
let stations: Vec<Station> = [near, far]
.iter()
.map(|f| loft::place(boundary, |p| loft::at(f, p)))
.collect();
let _ = tolerance;
loft::loft(boundary, &stations, false)
}
pub fn for_subject(
subject: &TriMesh,
half_space: HalfSpace,
tolerance: Tolerance,
) -> GeomResult<TriMesh> {
let normal = half_space.boundary.normal.normalize_or_zero();
if normal == Vec3::ZERO || subject.positions.is_empty() {
return Err(GeomError::InvalidInput(
"half-space boolean needs finite subject bounds".into(),
));
}
let reference = if normal.x.abs() < 0.9 {
Vec3::X
} else {
Vec3::Y
};
let frame = Frame::from_reference(half_space.boundary.origin, normal, reference)?;
let mut min = Point2::splat(Scalar::INFINITY);
let mut max = Point2::splat(Scalar::NEG_INFINITY);
let side = if half_space.agreement { 1.0 } else { -1.0 };
let mut selected_depth: Scalar = 0.0;
for &point in &subject.positions {
if !point.is_finite() {
return Err(GeomError::InvalidInput(
"half-space subject contains non-finite points".into(),
));
}
let delta = point - half_space.boundary.origin;
let uv = Point2::new(delta.dot(frame.x), delta.dot(frame.y));
min = min.min(uv);
max = max.max(uv);
selected_depth = selected_depth.max(delta.dot(normal) * side);
}
let span = (max - min).max_element().max(tolerance.linear());
let pad = span * 0.1 + tolerance.linear();
min -= Point2::splat(pad);
max += Point2::splat(pad);
let boundary = Rings {
outer: vec![
min,
Point2::new(max.x, min.y),
max,
Point2::new(min.x, max.y),
],
holes: Vec::new(),
};
let extent = min.abs().max(max.abs()).max_element();
let depth = selected_depth + pad;
let factor = depth / extent;
let margin = ClipMargin::new(factor).ok_or_else(|| {
GeomError::InvalidInput("half-space subject has no finite clipping extent".into())
})?;
bounded_half_space(
&boundary,
half_space.boundary,
half_space.agreement,
margin,
tolerance,
)
}