use axiolid_contracts::{GeomError, GeomResult, Operation};
use axiolid_core::{Interval, Point2, Scalar, Tolerance, Vec2};
use axiolid_curve::{Circle2, Curve2, Line2};
use axiolid_profile::{CenterLineProfile, Contour, ContourProfile, ProfileSegment};
use crate::BACKEND_ID;
fn unsupported(input: &'static str) -> GeomError {
GeomError::UnsupportedInput {
backend: BACKEND_ID,
operation: Operation::Sweep,
input,
}
}
#[derive(Debug, Clone, Copy)]
enum Piece {
Line {
from: Point2,
to: Point2,
},
Arc {
centre: Point2,
x: Vec2,
y: Vec2,
radius: Scalar,
start: Scalar,
end: Scalar,
sweep: Scalar,
},
}
impl Piece {
fn start_point(&self) -> Point2 {
match self {
Piece::Line { from, .. } => *from,
Piece::Arc {
centre,
x,
y,
radius,
start,
..
} => conic(*centre, *x, *y, *radius, *start),
}
}
fn end_point(&self) -> Point2 {
match self {
Piece::Line { to, .. } => *to,
Piece::Arc {
centre,
x,
y,
radius,
end,
..
} => conic(*centre, *x, *y, *radius, *end),
}
}
fn start_tangent(&self) -> Vec2 {
match self {
Piece::Line { from, to } => (*to - *from).normalize_or_zero(),
Piece::Arc {
x, y, start, end, ..
} => arc_tangent(*x, *y, *start, *end > *start),
}
}
fn end_tangent(&self) -> Vec2 {
match self {
Piece::Line { from, to } => (*to - *from).normalize_or_zero(),
Piece::Arc {
x, y, start, end, ..
} => arc_tangent(*x, *y, *end, *end > *start),
}
}
}
fn conic(centre: Point2, x: Vec2, y: Vec2, radius: Scalar, t: Scalar) -> Point2 {
let (s, c) = t.sin_cos();
centre + x * (radius * c) + y * (radius * s)
}
fn arc_tangent(x: Vec2, y: Vec2, t: Scalar, increasing: bool) -> Vec2 {
let (s, c) = t.sin_cos();
let d = x * -s + y * c;
let d = if increasing { d } else { -d };
d.normalize_or_zero()
}
fn read_piece(segment: &ProfileSegment) -> GeomResult<Piece> {
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) => Ok(Piece::Line {
from: line.origin + line.direction * from,
to: line.origin + line.direction * to,
}),
Curve2::Circle(circle) => {
let handedness = circle.frame.x.perp_dot(circle.frame.y);
if handedness == 0.0 {
return Err(GeomError::Degenerate(
"centre-line arc has a degenerate frame".to_owned(),
));
}
Ok(Piece::Arc {
centre: circle.frame.origin,
x: circle.frame.x,
y: circle.frame.y,
radius: circle.radius,
start: from,
end: to,
sweep: (to - from) * handedness.signum(),
})
}
Curve2::Ellipse(_) => Err(unsupported(
"centre-line elliptical segment has no elliptical offset",
)),
_ => Err(unsupported(
"centre-line segment of a kind with no exact offset",
)),
}
}
fn offset_piece(piece: &Piece, distance: Scalar) -> GeomResult<ProfileSegment> {
match piece {
Piece::Line { from, to } => {
let along = (*to - *from).normalize_or_zero();
if along == Vec2::ZERO {
return Err(GeomError::Degenerate(
"centre line has a zero-length segment".to_owned(),
));
}
let normal = Vec2::new(-along.y, along.x);
let start = *from + normal * distance;
let end = *to + normal * distance;
Ok(ProfileSegment {
curve: Curve2::Line(Line2 {
origin: start,
direction: end - start,
}),
domain: Interval::UNIT,
same_sense: true,
})
}
Piece::Arc {
centre,
x,
y,
radius,
start,
end,
sweep,
} => {
let offset_radius = radius - distance * sweep.signum();
if offset_radius <= 0.0 {
return Err(GeomError::Degenerate(format!(
"centre-line half-width {} collapses an arc of radius {radius}",
distance.abs()
)));
}
Ok(ProfileSegment {
curve: Curve2::Circle(Circle2 {
frame: axiolid_core::Frame2 {
origin: *centre,
x: *x,
y: *y,
},
radius: offset_radius,
}),
domain: Interval::new(*start, *end),
same_sense: true,
})
}
}
}
pub fn center_line_contour(
profile: &CenterLineProfile,
tolerance: Tolerance,
) -> GeomResult<ContourProfile> {
if profile.half_width <= 0.0 || profile.half_width.is_nan() {
return Err(GeomError::Degenerate(format!(
"centre line half-width must be positive, got {}",
profile.half_width
)));
}
if profile.path.segments.is_empty() {
return Err(GeomError::Degenerate(
"centre line path has no segments".to_owned(),
));
}
let pieces = profile
.path
.segments
.iter()
.map(read_piece)
.collect::<GeomResult<Vec<_>>>()?;
let eps = tolerance.linear();
for pair in pieces.windows(2) {
let gap = (pair[1].start_point() - pair[0].end_point()).length();
if gap > eps {
return Err(GeomError::InvalidInput(format!(
"centre line path is disconnected by {gap}"
)));
}
}
let first = pieces.first().expect("at least one segment");
let last = pieces.last().expect("at least one segment");
if (last.end_point() - first.start_point()).length() <= eps {
return Err(unsupported(
"closed centre-line path denotes an annulus, not a single ring",
));
}
for pair in pieces.windows(2) {
let incoming = pair[0].end_tangent();
let outgoing = pair[1].start_tangent();
if incoming.dot(outgoing) <= -1.0 + 1e-12 {
return Err(GeomError::Degenerate(
"centre line reverses on itself".to_owned(),
));
}
}
let half = profile.half_width;
let mut segments = Vec::with_capacity(2 * pieces.len() + 2);
for piece in &pieces {
segments.push(offset_piece(piece, -half)?);
}
let end_centre = last.end_point();
let end_normal = left_normal(last.end_tangent());
segments.push(straight(
end_centre - end_normal * half,
end_centre + end_normal * half,
));
for piece in pieces.iter().rev() {
let mut offset = offset_piece(piece, half)?;
offset.same_sense = !offset.same_sense;
segments.push(offset);
}
let start_centre = first.start_point();
let start_normal = left_normal(first.start_tangent());
segments.push(straight(
start_centre + start_normal * half,
start_centre - start_normal * half,
));
Ok(ContourProfile {
outer: Contour::new(segments),
holes: Vec::new(),
})
}
fn left_normal(tangent: Vec2) -> Vec2 {
Vec2::new(-tangent.y, tangent.x)
}
fn straight(from: Point2, to: Point2) -> ProfileSegment {
ProfileSegment {
curve: Curve2::Line(Line2 {
origin: from,
direction: to - from,
}),
domain: Interval::UNIT,
same_sense: true,
}
}