use axiolid_contracts::{GeomError, GeomResult};
use axiolid_core::{Plane3, PlaneFrame, Point2, Point3, Scalar, Tolerance, Transform3, Vec3};
use axiolid_mesh::TriMesh;
use axiolid_primitive::{ClipMargin, HalfSpace};
use crate::loft::{self, Frame, Station};
use crate::profile::Rings;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct BoundaryPrism {
pub origin: Point3,
pub x: Vec3,
pub y: Vec3,
pub axis: Vec3,
}
impl BoundaryPrism {
pub fn along_normal(
frame: PlaneFrame,
plane: Plane3,
tolerance: Tolerance,
) -> GeomResult<Self> {
let normal = plane_normal(plane)?;
let turn = frame.x_axis().cross(frame.y_axis()).dot(normal);
if turn.abs() <= unit_floor(tolerance) {
return Err(GeomError::InvalidInput(
"authored boundary frame stands perpendicular to the clip plane, so it \
has no handedness there"
.to_owned(),
));
}
Self::laid_on(
(frame.origin(), frame.x_axis(), frame.y_axis()),
turn < 0.0,
normal,
plane,
)
}
pub fn along_plane_normal(
placement: Transform3,
plane: Plane3,
tolerance: Tolerance,
) -> GeomResult<Self> {
let normal = plane_normal(plane)?;
let (x, y, handedness) = placement_axes(placement, tolerance)?;
Self::laid_on(
(placement.translation, x, y),
handedness < 0.0,
normal,
plane,
)
}
fn laid_on(
(origin, x, y): (Point3, Vec3, Vec3),
mirrored: bool,
normal: Vec3,
plane: Plane3,
) -> GeomResult<Self> {
let reference = (x - normal * x.dot(normal)).normalize_or_zero();
if reference == Vec3::ZERO {
return Err(GeomError::InvalidInput(
"authored boundary frame x axis is parallel to the clip plane normal, \
so it fixes no in-plane direction"
.to_owned(),
));
}
let anchor = origin - normal * (origin - plane.origin).dot(normal);
let base = Frame::from_reference(anchor, normal, reference)?;
let length = x.length();
let shear = y.dot(x) / length;
let rise = (y - x * (shear / length)).length();
let across = if mirrored { -base.y } else { base.y };
Ok(Self {
origin: base.origin,
x: base.x * length,
y: base.x * shear + across * rise,
axis: normal,
})
}
pub fn along_placement_z(placement: Transform3, tolerance: Tolerance) -> GeomResult<Self> {
let (x, y, _) = placement_axes(placement, tolerance)?;
Ok(Self {
origin: placement.translation,
x,
y,
axis: placement.matrix3.z_axis,
})
}
pub fn crossing(&self, plane: Plane3, tolerance: Tolerance) -> GeomResult<Scalar> {
let normal = plane_normal(plane)?;
let axis = self.axis.normalize_or_zero();
if !axis.is_finite() || axis == Vec3::ZERO {
return Err(GeomError::InvalidInput(
"bounded half-space prism axis must be a non-zero direction".to_owned(),
));
}
let crossing = axis.dot(normal);
if crossing.abs() <= unit_floor(tolerance) {
return Err(GeomError::InvalidInput(
"bounded half-space prism axis is parallel to the clip plane, so its prism \
never crosses it"
.to_owned(),
));
}
Ok(crossing)
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum SlabDepth {
Relative(ClipMargin),
Reach(Scalar),
}
pub fn bounded_half_space(
boundary: &Rings,
plane: Plane3,
agreement: bool,
margin: ClipMargin,
tolerance: Tolerance,
) -> GeomResult<TriMesh> {
bounded_half_space_framed(boundary, plane, None, agreement, margin, tolerance)
}
pub fn bounded_half_space_in_frame(
boundary: &Rings,
plane: Plane3,
frame: PlaneFrame,
agreement: bool,
margin: ClipMargin,
tolerance: Tolerance,
) -> GeomResult<TriMesh> {
bounded_half_space_framed(boundary, plane, Some(frame), agreement, margin, tolerance)
}
fn bounded_half_space_framed(
boundary: &Rings,
plane: Plane3,
authored: Option<PlaneFrame>,
agreement: bool,
margin: ClipMargin,
tolerance: Tolerance,
) -> GeomResult<TriMesh> {
let prism = match authored {
Some(frame) => BoundaryPrism::along_normal(frame, plane, tolerance)?,
None => {
let normal = plane_normal(plane)?;
let reference = if normal.x.abs() < 0.9 {
Vec3::X
} else {
Vec3::Y
};
let base = Frame::from_reference(plane.origin, normal, reference)?;
BoundaryPrism {
origin: base.origin,
x: base.x,
y: base.y,
axis: normal,
}
}
};
bounded_half_space_prism(
boundary,
plane,
prism,
agreement,
SlabDepth::Relative(margin),
tolerance,
)
}
pub fn bounded_half_space_prism(
boundary: &Rings,
plane: Plane3,
prism: BoundaryPrism,
agreement: bool,
depth: SlabDepth,
tolerance: Tolerance,
) -> GeomResult<TriMesh> {
let normal = plane_normal(plane)?;
if boundary.outer.len() < 3 {
return Err(GeomError::InvalidInput(format!(
"half-space boundary needs at least 3 vertices, got {}",
boundary.outer.len()
)));
}
let crossing = prism.crossing(plane, tolerance)?;
let lean = if agreement { crossing } else { -crossing };
let axis = prism.axis.normalize();
let toward = axis * lean.signum();
let carry = |v: Vec3| v - axis * (v.dot(normal) / crossing);
let origin = prism.origin - axis * ((prism.origin - plane.origin).dot(normal) / crossing);
let (x, y) = (carry(prism.x), carry(prism.y));
let face = x.cross(y);
if !(origin.is_finite() && face.is_finite())
|| face.length() <= unit_floor(tolerance) * x.length() * y.length()
{
return Err(GeomError::InvalidInput(
"bounded half-space boundary frame is degenerate on the clip plane: its axes \
are parallel or zero"
.to_owned(),
));
}
let reach = match depth {
SlabDepth::Relative(margin) => {
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());
}
extent * x.length().max(y.length()) * margin.factor()
}
SlabDepth::Reach(reach) => reach,
};
if !(reach.is_finite() && reach > 0.0) {
return Err(GeomError::InvalidInput(format!(
"bounded half-space slab depth must be positive and finite, got {reach}"
)));
}
let step = toward * (reach / lean.abs());
let rings = counter_clockwise(boundary);
let near = Frame { origin, x, y };
let far = Frame {
origin: origin + step,
x,
y,
};
let ordered = if face.dot(toward) > 0.0 {
[near, far]
} else {
[far, near]
};
let stations: Vec<Station> = ordered
.iter()
.map(|f| loft::place(&rings, |p| loft::at(f, p)))
.collect();
loft::loft(&rings, &stations, false)
}
fn placement_axes(placement: Transform3, tolerance: Tolerance) -> GeomResult<(Vec3, Vec3, Scalar)> {
let [x, y, z] = [
placement.matrix3.x_axis,
placement.matrix3.y_axis,
placement.matrix3.z_axis,
];
if !(placement.translation.is_finite() && x.is_finite() && y.is_finite() && z.is_finite()) {
return Err(GeomError::InvalidInput(
"bounded half-space placement is not finite".to_owned(),
));
}
let volume = x.cross(y).dot(z);
if volume.abs() <= unit_floor(tolerance) * x.length() * y.length() * z.length() {
return Err(GeomError::InvalidInput(
"bounded half-space placement is degenerate: its axes are parallel or zero".to_owned(),
));
}
Ok((x, y, volume))
}
fn plane_normal(plane: Plane3) -> GeomResult<Vec3> {
let normal = plane.normal.normalize_or_zero();
if normal == Vec3::ZERO || !normal.is_finite() {
return Err(GeomError::InvalidInput(
"half-space boundary plane needs a non-zero normal".to_owned(),
));
}
Ok(normal)
}
fn unit_floor(tolerance: Tolerance) -> Scalar {
tolerance.angular().max(Scalar::EPSILON * 8.0)
}
fn counter_clockwise(rings: &Rings) -> std::borrow::Cow<'_, Rings> {
let twice_area: Scalar = (0..rings.outer.len())
.map(|i| {
let (a, b) = (rings.outer[i], rings.outer[(i + 1) % rings.outer.len()]);
a.perp_dot(b)
})
.sum();
if twice_area >= 0.0 {
return std::borrow::Cow::Borrowed(rings);
}
let reversed = |ring: &Vec<Point2>| ring.iter().rev().copied().collect();
std::borrow::Cow::Owned(Rings {
outer: reversed(&rings.outer),
holes: rings.holes.iter().map(reversed).collect(),
})
}
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,
)
}