use axiolid_contracts::{GeomError, GeomResult, Operation};
use axiolid_core::{Frame2, Interval, Point2, Scalar, Tolerance, Transform2, Vec2};
use axiolid_curve::{Circle2, Curve2, Line2};
use axiolid_overlay::{union_soup, Ring};
use axiolid_profile::{
CircleProfile, Contour, ContourProfile, Profile, ProfileSegment, RectangleProfile,
};
use crate::BACKEND_ID;
fn unsupported(input: &'static str) -> GeomError {
GeomError::UnsupportedInput {
backend: BACKEND_ID,
operation: Operation::Sweep,
input,
}
}
fn conformal_scale(transform: &Transform2, tolerance: Tolerance) -> Option<Scalar> {
let x = transform.matrix2.x_axis;
let y = transform.matrix2.y_axis;
let xx = x.dot(x);
let yy = y.dot(y);
let xy = x.dot(y);
if xx <= 0.0 || yy <= 0.0 {
return None;
}
let slack = tolerance.linear() * xx.max(yy).max(1.0);
if xy.abs() > slack || (xx - yy).abs() > slack {
return None;
}
Some(xx.sqrt())
}
fn apply(transform: &Transform2, point: Point2) -> Point2 {
transform.transform_point2(point)
}
pub fn lower_derived(
basis: &Profile,
transform: &Transform2,
tolerance: Tolerance,
) -> GeomResult<Profile> {
match basis {
Profile::Derived {
basis: inner,
transform: inner_transform,
} => lower_derived(inner, &(*transform * *inner_transform), tolerance),
Profile::Rectangle(rectangle) => lower_rectangle(rectangle, transform, tolerance),
Profile::Circle(circle) => lower_circle(circle, transform, tolerance),
Profile::Contour(contour) => Ok(Profile::Contour(ContourProfile {
outer: lower_contour(&contour.outer, transform, tolerance)?,
holes: contour
.holes
.iter()
.map(|hole| lower_contour(hole, transform, tolerance))
.collect::<GeomResult<Vec<_>>>()?,
})),
Profile::Composite(members) => Ok(Profile::Composite(
members
.iter()
.map(|member| lower_derived(member, transform, tolerance))
.collect::<GeomResult<Vec<_>>>()?,
)),
_ => Err(unsupported("derived profile over an unsupported basis")),
}
}
fn lower_rectangle(
rectangle: &RectangleProfile,
transform: &Transform2,
tolerance: Tolerance,
) -> GeomResult<Profile> {
if rectangle.thickness.is_some() {
return Err(unsupported("derived profile over a hollow rectangle"));
}
if rectangle.outer_radius.is_some() || rectangle.inner_radius.is_some() {
return Err(unsupported("derived profile over a rounded rectangle"));
}
if !rectangle.x.is_finite()
|| !rectangle.y.is_finite()
|| rectangle.x <= 0.0
|| rectangle.y <= 0.0
{
return Err(GeomError::InvalidInput(format!(
"derived rectangle extents must be positive and finite, got {} x {}",
rectangle.x, rectangle.y
)));
}
let (half_x, half_y) = (rectangle.x / 2.0, rectangle.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 mut moved: Vec<Point2> = corners.iter().map(|p| apply(transform, *p)).collect();
orient_counter_clockwise(&mut moved);
let _ = tolerance;
Ok(Profile::Contour(ContourProfile {
outer: polygon_contour(&moved),
holes: Vec::new(),
}))
}
fn lower_circle(
circle: &CircleProfile,
transform: &Transform2,
tolerance: Tolerance,
) -> GeomResult<Profile> {
let Some(scale) = conformal_scale(transform, tolerance) else {
return Err(unsupported(
"derived circle under a non-conformal transform is an ellipse",
));
};
if transform.translation != Vec2::ZERO {
return Err(unsupported("derived circle translated off the origin"));
}
Ok(Profile::Circle(CircleProfile {
radius: circle.radius * scale,
thickness: circle.thickness.map(|value| value * scale),
}))
}
fn lower_contour(
contour: &Contour,
transform: &Transform2,
tolerance: Tolerance,
) -> GeomResult<Contour> {
let mirrors = transform.matrix2.determinant() < 0.0;
let mut segments = Vec::with_capacity(contour.segments.len());
for segment in &contour.segments {
segments.push(lower_segment(segment, transform, tolerance, mirrors)?);
}
if mirrors {
segments.reverse();
}
Ok(Contour::new(segments))
}
fn lower_segment(
segment: &ProfileSegment,
transform: &Transform2,
tolerance: Tolerance,
mirrors: bool,
) -> GeomResult<ProfileSegment> {
let same_sense = segment.same_sense != mirrors;
match &segment.curve {
Curve2::Line(line) => Ok(ProfileSegment {
curve: Curve2::Line(Line2 {
origin: apply(transform, line.origin),
direction: transform.matrix2 * line.direction,
}),
domain: segment.domain,
same_sense,
}),
Curve2::Circle(circle) => {
let Some(scale) = conformal_scale(transform, tolerance) else {
return Err(unsupported(
"derived contour arc under a non-conformal transform is elliptical",
));
};
Ok(ProfileSegment {
curve: Curve2::Circle(Circle2 {
frame: Frame2 {
origin: apply(transform, circle.frame.origin),
x: transform.matrix2 * circle.frame.x,
y: transform.matrix2 * circle.frame.y,
},
radius: circle.radius * scale,
}),
domain: segment.domain,
same_sense,
})
}
_ => Err(unsupported(
"derived contour over a segment kind that cannot be transformed exactly",
)),
}
}
fn polygon_contour(points: &[Point2]) -> Contour {
let count = points.len();
let segments = (0..count)
.map(|index| {
let from = points[index];
let to = points[(index + 1) % count];
ProfileSegment {
curve: Curve2::Line(Line2 {
origin: from,
direction: to - from,
}),
domain: Interval::UNIT,
same_sense: true,
}
})
.collect();
Contour::new(segments)
}
fn signed_area(points: &[Point2]) -> Scalar {
let count = points.len();
(0..count)
.map(|index| {
let a = points[index];
let b = points[(index + 1) % count];
a.perp_dot(b)
})
.sum::<Scalar>()
/ 2.0
}
fn orient_counter_clockwise(points: &mut [Point2]) {
if signed_area(points) < 0.0 {
points.reverse();
}
}
pub fn lower_composite(
members: &[Profile],
tolerance: Tolerance,
) -> GeomResult<(Vec<Point2>, Vec<Vec<Point2>>)> {
if members.is_empty() {
return Err(GeomError::InvalidInput(
"a composite profile needs at least one member".to_owned(),
));
}
let mut rings = Vec::with_capacity(members.len());
for member in members {
rings.push(Ring {
points: member_ring(member, tolerance)?,
});
}
let polygons = union_soup(&rings, tolerance).map_err(|error| {
GeomError::InvalidInput(format!("composite member union failed: {error:?}"))
})?;
match polygons.len() {
0 => Err(GeomError::Degenerate(
"composite profile members union to nothing".to_owned(),
)),
1 => {
let polygon = &polygons[0];
Ok((
polygon.outer.points.clone(),
polygon.holes.iter().map(|h| h.points.clone()).collect(),
))
}
_ => Err(unsupported("composite profile whose members are disjoint")),
}
}
fn member_ring(member: &Profile, tolerance: Tolerance) -> GeomResult<Vec<Point2>> {
let lowered;
let resolved = match member {
Profile::Derived { basis, transform } => {
lowered = lower_derived(basis, transform, tolerance)?;
&lowered
}
other => other,
};
match resolved {
Profile::Rectangle(rectangle) => {
if rectangle.thickness.is_some()
|| rectangle.outer_radius.is_some()
|| rectangle.inner_radius.is_some()
{
return Err(unsupported(
"composite member with a hollow or rounded rectangle",
));
}
let (half_x, half_y) = (rectangle.x / 2.0, rectangle.y / 2.0);
Ok(vec![
Point2::new(-half_x, -half_y),
Point2::new(half_x, -half_y),
Point2::new(half_x, half_y),
Point2::new(-half_x, half_y),
])
}
Profile::Contour(contour) => {
if !contour.holes.is_empty() {
return Err(unsupported("composite member carrying its own holes"));
}
let mut points: Vec<Point2> = Vec::with_capacity(contour.outer.segments.len());
for segment in &contour.outer.segments {
match &segment.curve {
Curve2::Line(line) => {
let t = if segment.same_sense {
segment.domain.start
} else {
segment.domain.end
};
points.push(line.origin + line.direction * t);
}
_ => return Err(unsupported("composite member with a curved segment")),
}
}
orient_counter_clockwise(&mut points);
Ok(points)
}
_ => Err(unsupported(
"composite member of an unsupported profile kind",
)),
}
}