use axiolid_brep::{EdgeName, ExactBRep, FaceName, SweptFace};
use axiolid_contracts::{GeomError, GeomResult, Operation};
use axiolid_core::{Point2, Scalar, Tolerance, Vec2, Vec3};
use axiolid_profile::{Profile, RectangleProfile};
use crate::extrude_exact::{
extrude_polygon_rings, extrude_with_cylindrical_blend, extrude_with_cylindrical_blends,
};
use crate::BACKEND_ID;
fn unsupported(input: &'static str) -> GeomError {
GeomError::UnsupportedInput {
backend: BACKEND_ID,
operation: Operation::Sweep,
input,
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum EdgeSelector {
NearestCorner(Point2),
Named(EdgeName),
}
impl EdgeSelector {
fn corner_index(&self, corners: &[Point2]) -> GeomResult<usize> {
match self {
Self::NearestCorner(target) => corners
.iter()
.enumerate()
.min_by(|(_, a), (_, b)| {
(**a - *target)
.length_squared()
.total_cmp(&(**b - *target).length_squared())
})
.map(|(index, _)| index)
.ok_or_else(|| GeomError::Degenerate("profile has no corners".to_owned())),
Self::Named(name) => {
let count = corners.len();
(0..count)
.find(|index| {
let previous = (index + count - 1) % count;
let (Ok(ordinal), Ok(previous_ordinal)) =
(u32::try_from(*index), u32::try_from(previous))
else {
return false;
};
let candidate = EdgeName::between(
FaceName::swept(SweptFace::Side(previous_ordinal)),
FaceName::swept(SweptFace::Side(ordinal)),
);
candidate == *name
})
.ok_or_else(|| {
unsupported("edge name does not resolve to a corner of this profile")
})
}
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum FeatureSize {
ConstantDistance(Scalar),
ConstantRadius(Scalar),
}
pub fn chamfer_extruded_profile(
profile: &Profile,
direction: Vec3,
depth: Scalar,
edge: EdgeSelector,
size: FeatureSize,
tolerance: Tolerance,
) -> GeomResult<ExactBRep> {
let distance = match size {
FeatureSize::ConstantDistance(distance) => distance,
FeatureSize::ConstantRadius(_) => {
return Err(unsupported("constant-radius fillet on an extruded solid"))
}
};
if !distance.is_finite() || distance <= 0.0 {
return Err(GeomError::InvalidInput(format!(
"chamfer distance must be positive and finite, got {distance}"
)));
}
let Profile::Rectangle(rectangle) = profile else {
return Err(unsupported("chamfer on a non-rectangle profile"));
};
let RectangleProfile {
x,
y,
thickness,
outer_radius,
inner_radius,
} = *rectangle;
if thickness.is_some() {
return Err(unsupported("chamfer on a hollow profile"));
}
if outer_radius.is_some() || inner_radius.is_some() {
return Err(unsupported("chamfer on an already-rounded profile"));
}
if !x.is_finite() || !y.is_finite() || x <= 0.0 || y <= 0.0 {
return Err(GeomError::InvalidInput(format!(
"chamfer profile must have positive finite extents, got {x} x {y}"
)));
}
if !depth.is_finite() || depth <= 0.0 {
return Err(GeomError::InvalidInput(format!(
"chamfer extrusion depth must be positive and finite, got {depth}"
)));
}
if direction.normalize_or_zero().dot(Vec3::Z) < 1.0 - tolerance.linear() {
return Err(unsupported("chamfer on an oblique extrusion"));
}
let (half_x, half_y) = (x / 2.0, y / 2.0);
let corners = [
Point2::new(-half_x, -half_y),
Point2::new(half_x, -half_y),
Point2::new(half_x, half_y),
Point2::new(-half_x, half_y),
];
if distance * 2.0 >= x || distance * 2.0 >= y {
return Err(GeomError::Degenerate(format!(
"chamfer distance {distance} consumes an entire edge of the {x} x {y} profile"
)));
}
let index = edge.corner_index(&corners)?;
let previous = corners[(index + corners.len() - 1) % corners.len()];
let corner = corners[index];
let next = corners[(index + 1) % corners.len()];
let into_previous = (previous - corner).normalize();
let into_next = (next - corner).normalize();
let mut ring = Vec::with_capacity(corners.len() + 1);
for (position, point) in corners.iter().enumerate() {
if position == index {
ring.push(corner + into_previous * distance);
ring.push(corner + into_next * distance);
} else {
ring.push(*point);
}
}
extrude_polygon_rings(&[ring], Vec3::Z * depth)
}
pub fn fillet_extruded_profile(
profile: &Profile,
direction: Vec3,
depth: Scalar,
edge: EdgeSelector,
size: FeatureSize,
tolerance: Tolerance,
) -> GeomResult<ExactBRep> {
let radius = match size {
FeatureSize::ConstantRadius(radius) => radius,
FeatureSize::ConstantDistance(_) => {
return Err(unsupported(
"chamfer requested through the fillet entry point",
))
}
};
if !radius.is_finite() || radius <= 0.0 {
return Err(GeomError::InvalidInput(format!(
"fillet radius must be positive and finite, got {radius}"
)));
}
let geometry = rectangle_prism(profile, direction, depth, tolerance)?;
let (x, y) = geometry;
if radius * 2.0 >= x || radius * 2.0 >= y {
return Err(GeomError::Degenerate(format!(
"fillet radius {radius} reaches past a neighbouring corner of the {x} x {y} profile"
)));
}
build_filleted_prism(x, y, depth, radius, edge)
}
fn rectangle_prism(
profile: &Profile,
direction: Vec3,
depth: Scalar,
tolerance: Tolerance,
) -> GeomResult<(Scalar, Scalar)> {
let Profile::Rectangle(rectangle) = profile else {
return Err(unsupported("fillet on a non-rectangle profile"));
};
let RectangleProfile {
x,
y,
thickness,
outer_radius,
inner_radius,
} = *rectangle;
if thickness.is_some() {
return Err(unsupported("fillet on a hollow profile"));
}
if outer_radius.is_some() || inner_radius.is_some() {
return Err(unsupported("fillet on an already-rounded profile"));
}
if !x.is_finite() || !y.is_finite() || x <= 0.0 || y <= 0.0 {
return Err(GeomError::InvalidInput(format!(
"fillet profile must have positive finite extents, got {x} x {y}"
)));
}
if !depth.is_finite() || depth <= 0.0 {
return Err(GeomError::InvalidInput(format!(
"fillet extrusion depth must be positive and finite, got {depth}"
)));
}
if direction.normalize_or_zero().dot(Vec3::Z) < 1.0 - tolerance.linear() {
return Err(unsupported("fillet on an oblique extrusion"));
}
Ok((x, y))
}
pub struct BlendCorner {
pub centre: Point2,
pub start: Point2,
pub end: Point2,
pub sweep: Scalar,
}
fn blend_corner(corners: &[Point2], index: usize, radius: Scalar) -> Option<BlendCorner> {
let count = corners.len();
let previous = corners[(index + count - 1) % count];
let corner = corners[index];
let next = corners[(index + 1) % count];
let into_previous = (previous - corner).normalize_or_zero();
let into_next = (next - corner).normalize_or_zero();
if into_previous == Vec2::ZERO || into_next == Vec2::ZERO {
return None;
}
let cosine = into_previous.dot(into_next).clamp(-1.0, 1.0);
let theta = cosine.acos();
let half = theta / 2.0;
let (sin_half, tan_half) = (half.sin(), half.tan());
if sin_half.abs() <= f64::EPSILON || tan_half.abs() <= f64::EPSILON {
return None;
}
let setback = radius / tan_half;
let start = corner + into_previous * setback;
let end = corner + into_next * setback;
let bisector = (into_previous + into_next).normalize_or_zero();
if bisector == Vec2::ZERO {
return None;
}
let centre = corner + bisector * (radius / sin_half);
let start_angle = (start.y - centre.y).atan2(start.x - centre.x);
let end_angle = (end.y - centre.y).atan2(end.x - centre.x);
let mut sweep = end_angle - start_angle;
while sweep > core::f64::consts::PI {
sweep -= core::f64::consts::TAU;
}
while sweep < -core::f64::consts::PI {
sweep += core::f64::consts::TAU;
}
Some(BlendCorner {
centre,
start,
end,
sweep,
})
}
fn build_filleted_prism(
x: Scalar,
y: Scalar,
depth: Scalar,
radius: Scalar,
edge: EdgeSelector,
) -> GeomResult<ExactBRep> {
let (half_x, half_y) = (x / 2.0, y / 2.0);
let corners = [
Point2::new(-half_x, -half_y),
Point2::new(half_x, -half_y),
Point2::new(half_x, half_y),
Point2::new(-half_x, half_y),
];
let index = edge.corner_index(&corners)?;
let blend = blend_corner(&corners, index, radius)
.ok_or_else(|| unsupported("fillet at a degenerate corner"))?;
let mut ring = Vec::with_capacity(corners.len() + 1);
for (position, point) in corners.iter().enumerate() {
if position == index {
ring.push(blend.start);
ring.push(blend.end);
} else {
ring.push(*point);
}
}
extrude_with_cylindrical_blend(&ring, Vec3::Z * depth, index, &blend, radius)
}
pub fn fillet_polygon_corner(
ring: &[Point2],
corner: usize,
radius: Scalar,
depth: Scalar,
) -> GeomResult<ExactBRep> {
if ring.len() < 3 {
return Err(GeomError::InvalidInput(
"fillet needs a ring of at least three corners".to_owned(),
));
}
if corner >= ring.len() {
return Err(GeomError::InvalidInput(format!(
"fillet corner {corner} is outside a ring of {} corners",
ring.len()
)));
}
if !radius.is_finite() || radius <= 0.0 {
return Err(GeomError::InvalidInput(format!(
"fillet radius must be positive and finite, got {radius}"
)));
}
if !depth.is_finite() || depth <= 0.0 {
return Err(GeomError::InvalidInput(format!(
"fillet extrusion depth must be positive and finite, got {depth}"
)));
}
if !ring.iter().all(|p| p.x.is_finite() && p.y.is_finite()) {
return Err(GeomError::InvalidInput(
"fillet ring has a non-finite corner".to_owned(),
));
}
let count = ring.len();
let previous = ring[(corner + count - 1) % count];
let here = ring[corner];
let next = ring[(corner + 1) % count];
let ring_is_ccw = signed_area(ring) > 0.0;
let turn = (here - previous).perp_dot(next - here);
let convex = if ring_is_ccw { turn > 0.0 } else { turn < 0.0 };
if !convex {
return Err(unsupported("fillet on a reflex corner"));
}
let blend = blend_corner(ring, corner, radius)
.ok_or_else(|| unsupported("fillet at a degenerate corner"))?;
let setback = (blend.start - here).length();
let to_previous = (previous - here).length();
let to_next = (next - here).length();
if setback >= to_previous || setback >= to_next {
return Err(unsupported("fillet radius larger than an adjacent edge"));
}
let mut blended = Vec::with_capacity(count + 1);
for (index, point) in ring.iter().enumerate() {
if index == corner {
blended.push(blend.start);
blended.push(blend.end);
} else {
blended.push(*point);
}
}
let blend_index = corner;
extrude_with_cylindrical_blend(&blended, Vec3::Z * depth, blend_index, &blend, radius)
}
fn signed_area(ring: &[Point2]) -> Scalar {
let count = ring.len();
(0..count)
.map(|index| {
let a = ring[index];
let b = ring[(index + 1) % count];
a.x * b.y - b.x * a.y
})
.sum()
}
pub fn fillet_polygon_corners(
ring: &[Point2],
fillets: &[(usize, Scalar)],
depth: Scalar,
) -> GeomResult<ExactBRep> {
if fillets.is_empty() {
return Err(GeomError::InvalidInput(
"multi-corner fillet needs at least one corner".to_owned(),
));
}
if ring.len() < 3 {
return Err(GeomError::InvalidInput(
"fillet profile ring needs at least three points".to_owned(),
));
}
let count = ring.len();
let mut seen = vec![false; count];
let mut setbacks = vec![0.0; count];
let mut blends: Vec<Option<BlendCorner>> = (0..count).map(|_| None).collect();
for (corner, radius) in fillets.iter().copied() {
if corner >= count {
return Err(GeomError::InvalidInput(format!(
"fillet corner {corner} is outside a ring of {count} points"
)));
}
if seen[corner] {
return Err(GeomError::InvalidInput(format!(
"fillet corner {corner} given more than once"
)));
}
seen[corner] = true;
if !radius.is_finite() || radius <= 0.0 {
return Err(GeomError::InvalidInput(format!(
"fillet radius must be positive and finite, got {radius}"
)));
}
let blend = blend_corner(ring, corner, radius)
.ok_or_else(|| unsupported("fillet at a degenerate corner"))?;
setbacks[corner] = (blend.start - ring[corner]).length();
blends[corner] = Some(blend);
}
for corner in 0..count {
if !seen[corner] {
continue;
}
let previous = ring[(corner + count - 1) % count];
let here = ring[corner];
let next = ring[(corner + 1) % count];
let cross = (here - previous).perp_dot(next - here);
if cross <= 0.0 {
return Err(unsupported("fillet on a reflex corner"));
}
}
for start in 0..count {
let end = (start + 1) % count;
let length = (ring[end] - ring[start]).length();
if setbacks[start] + setbacks[end] >= length {
return Err(unsupported(
"fillet radii too large for the edge between two corners",
));
}
}
let mut blended = Vec::with_capacity(count + fillets.len());
let mut table: Vec<Option<(BlendCorner, Scalar)>> = Vec::with_capacity(count + fillets.len());
let radius_of: Vec<Scalar> = {
let mut r = vec![0.0; count];
for (corner, radius) in fillets.iter().copied() {
r[corner] = radius;
}
r
};
for corner in 0..count {
match blends[corner].take() {
Some(blend) => {
let end = blend.end;
blended.push(blend.start);
table.push(Some((blend, radius_of[corner])));
blended.push(end);
table.push(None);
}
None => {
blended.push(ring[corner]);
table.push(None);
}
}
}
extrude_with_cylindrical_blends(&blended, Vec3::Z * depth, &table)
}
pub fn chamfer_polygon_corners(
ring: &[Point2],
chamfers: &[(usize, Scalar)],
depth: Scalar,
) -> GeomResult<ExactBRep> {
if chamfers.is_empty() {
return Err(GeomError::InvalidInput(
"multi-corner chamfer needs at least one corner".to_owned(),
));
}
if ring.len() < 3 {
return Err(GeomError::InvalidInput(
"chamfer profile ring needs at least three points".to_owned(),
));
}
if !depth.is_finite() || depth <= 0.0 {
return Err(GeomError::InvalidInput(format!(
"chamfer extrusion depth must be positive and finite, got {depth}"
)));
}
let count = ring.len();
let mut seen = vec![false; count];
let mut setbacks = vec![0.0; count];
for (corner, distance) in chamfers.iter().copied() {
if corner >= count {
return Err(GeomError::InvalidInput(format!(
"chamfer corner {corner} is outside a ring of {count} points"
)));
}
if seen[corner] {
return Err(GeomError::InvalidInput(format!(
"chamfer corner {corner} given more than once"
)));
}
seen[corner] = true;
if !distance.is_finite() || distance <= 0.0 {
return Err(GeomError::InvalidInput(format!(
"chamfer distance must be positive and finite, got {distance}"
)));
}
setbacks[corner] = distance;
}
for corner in 0..count {
if !seen[corner] {
continue;
}
let previous = ring[(corner + count - 1) % count];
let here = ring[corner];
let next = ring[(corner + 1) % count];
if (here - previous).perp_dot(next - here) <= 0.0 {
return Err(unsupported("chamfer on a reflex corner"));
}
}
for start in 0..count {
let end = (start + 1) % count;
let length = (ring[end] - ring[start]).length();
if setbacks[start] + setbacks[end] >= length {
return Err(unsupported(
"chamfer distances too large for the edge between two corners",
));
}
}
let mut chamfered = Vec::with_capacity(count + chamfers.len());
for corner in 0..count {
let here = ring[corner];
if !seen[corner] {
chamfered.push(here);
continue;
}
let previous = ring[(corner + count - 1) % count];
let next = ring[(corner + 1) % count];
let into_previous = (previous - here).normalize_or_zero();
let into_next = (next - here).normalize_or_zero();
if into_previous == Vec2::ZERO || into_next == Vec2::ZERO {
return Err(unsupported("chamfer at a degenerate corner"));
}
chamfered.push(here + into_previous * setbacks[corner]);
chamfered.push(here + into_next * setbacks[corner]);
}
extrude_polygon_rings(&[chamfered], Vec3::Z * depth)
}