use axiolid_contracts::{GeomError, GeomResult, Operation};
use axiolid_core::{Point2, Scalar, Tolerance};
use axiolid_curve::{Circle2, Curve2};
use axiolid_overlay::{ArcRing, ArcVertex};
use axiolid_profile::{Contour, ProfileSegment};
pub fn contour_to_arc_ring(contour: &Contour, tolerance: Tolerance) -> GeomResult<ArcRing> {
if contour.segments.len() < 2 {
return Err(GeomError::InvalidInput(format!(
"a closed contour needs at least two segments, got {}",
contour.segments.len()
)));
}
let mut vertices = Vec::with_capacity(contour.segments.len());
let mut previous_end: Option<Point2> = None;
for segment in &contour.segments {
let (start, end, bulge) = lower_segment(segment)?;
if let Some(previous) = previous_end {
let gap = (start - previous).length();
if gap > tolerance.linear() {
return Err(GeomError::InvalidInput(format!(
"contour segments leave a gap of {gap} at {start:?}"
)));
}
}
vertices.push(ArcVertex {
point: start,
bulge,
});
previous_end = Some(end);
}
if let (Some(last), Some(first)) = (previous_end, vertices.first()) {
let gap = (first.point - last).length();
if gap > tolerance.linear() {
return Err(GeomError::InvalidInput(format!(
"contour does not close: {gap} from last segment end to start"
)));
}
}
Ok(ArcRing::new(vertices))
}
fn lower_segment(segment: &ProfileSegment) -> GeomResult<(Point2, Point2, Scalar)> {
let (from, to) = if segment.same_sense {
(segment.domain.start, segment.domain.end)
} else {
(segment.domain.end, segment.domain.start)
};
match &segment.curve {
Curve2::Line(line) => {
let start = line.origin + line.direction * from;
let end = line.origin + line.direction * to;
Ok((start, end, 0.0))
}
Curve2::Circle(circle) => lower_arc(circle, from, to),
other => Err(GeomError::UnsupportedInput {
backend: crate::BACKEND_ID,
operation: Operation::Sweep,
input: unsupported_curve_name(other),
}),
}
}
fn lower_arc(circle: &Circle2, from: Scalar, to: Scalar) -> GeomResult<(Point2, Point2, Scalar)> {
let evaluate = |t: Scalar| {
let (sin, cos) = t.sin_cos();
circle.frame.origin
+ circle.frame.x * (circle.radius * cos)
+ circle.frame.y * (circle.radius * sin)
};
let start = evaluate(from);
let end = evaluate(to);
let handedness = circle.frame.x.perp_dot(circle.frame.y);
if handedness == 0.0 {
return Err(GeomError::Degenerate(
"circular profile segment has a degenerate frame".to_owned(),
));
}
let sweep = (to - from) * handedness.signum();
if sweep == 0.0 {
return Err(GeomError::Degenerate(
"circular profile segment has an empty parameter range".to_owned(),
));
}
if sweep.abs() >= core::f64::consts::PI {
return Err(GeomError::UnsupportedInput {
backend: crate::BACKEND_ID,
operation: Operation::Sweep,
input: "circular profile segment sweeping half a turn or more",
});
}
Ok((start, end, (sweep / 4.0).tan()))
}
fn unsupported_curve_name(curve: &Curve2) -> &'static str {
match curve {
Curve2::Ellipse(_) => "elliptical contour segment",
Curve2::Polyline(_) => "polyline contour segment",
Curve2::BSpline(_) => "B-spline contour segment",
Curve2::Intrinsic(_) => "intrinsic contour segment",
_ => "contour segment of an unsupported curve kind",
}
}
pub fn arc_ring_signed_area(ring: &ArcRing) -> Scalar {
let count = ring.vertices.len();
let mut total = 0.0;
for index in 0..count {
let here = ring.vertices[index];
let next = ring.vertices[(index + 1) % count];
total += here.point.perp_dot(next.point);
if here.bulge != 0.0 {
let sweep = 4.0 * here.bulge.atan();
let chord = (next.point - here.point).length();
let half = (sweep.abs() / 2.0).sin();
if half > 0.0 {
let radius = chord / (2.0 * half);
total += radius * radius * (sweep - sweep.sin());
}
}
}
total / 2.0
}
pub fn reverse_arc_ring(ring: &ArcRing) -> ArcRing {
let count = ring.vertices.len();
let mut vertices = Vec::with_capacity(count);
for index in (0..count).rev() {
let previous = (index + count - 1) % count;
vertices.push(ArcVertex {
point: ring.vertices[index].point,
bulge: -ring.vertices[previous].bulge,
});
}
ArcRing { vertices }
}
pub fn orient_arc_ring(ring: &ArcRing, counter_clockwise: bool) -> GeomResult<ArcRing> {
let area = arc_ring_signed_area(ring);
if area == 0.0 {
return Err(GeomError::Degenerate(
"contour ring encloses no area".to_owned(),
));
}
if (area > 0.0) == counter_clockwise {
Ok(ring.clone())
} else {
Ok(reverse_arc_ring(ring))
}
}