use geo::LineIntersection;
use itertools::Itertools;
use nalgebra::Point2;
use crate::{
curve::NurbsCurve2D,
misc::FloatingPoint,
offset::{
helper::{round_corner, sharp_corner_intersection, smooth_corner, to_line_helper},
CurveOffsetCornerType, CurveOffsetOption, Offset,
},
region::CompoundCurve2D,
};
impl<'a, T> Offset<'a, T> for CompoundCurve2D<T>
where
T: FloatingPoint,
{
type Output = anyhow::Result<Vec<CompoundCurve2D<T>>>;
type Option = CurveOffsetOption<T>;
fn offset(&'a self, option: Self::Option) -> Self::Output {
let offset = self
.spans()
.iter()
.map(|span| span.offset(option.clone()))
.collect::<anyhow::Result<Vec<_>>>()?;
if matches!(option.corner_type(), CurveOffsetCornerType::None) {
return Ok(offset.into_iter().flatten().collect());
}
let is_closed = self.is_closed(None);
let corners = offset
.windows(2)
.map(|window| {
let w0 = &window[0];
let w1 = &window[1];
find_corner(w0, w1, &option)
})
.collect::<anyhow::Result<Vec<_>>>()?;
let last_corner = if is_closed {
let last = offset.last();
let head = offset.first();
match (last, head) {
(Some(last), Some(head)) => find_corner(last, head, &option)?,
_ => None,
}
} else {
None
};
let corners = corners.into_iter().chain(vec![last_corner]).collect_vec();
let n = corners.len();
let curves = offset.into_iter().enumerate().map(|(i, o)| {
let prev_corner = &corners[(i + n - 1) % n];
let next_corner = &corners[i % n];
match (prev_corner, next_corner) {
(Some(Corner::Intersection(prev)), Some(next)) => {
match next {
Corner::Intersection(next) => {
let trimmed = trim_polyline(
o[0].spans().first().unwrap(),
Some(*prev),
Some(*next),
);
vec![trimmed]
}
Corner::Curve(corner) => {
let trimmed =
trim_polyline(o[0].spans().first().unwrap(), Some(*prev), None);
vec![trimmed, corner.clone()]
}
}
}
(_, Some(next)) => match next {
Corner::Intersection(next) => {
let trimmed =
trim_polyline(o[0].spans().first().unwrap(), None, Some(*next));
vec![trimmed]
}
Corner::Curve(corner) => o
.into_iter()
.flat_map(|c| c.into_nurbs_spans())
.chain(vec![corner.clone()])
.collect_vec(),
},
(_, None) => o
.into_iter()
.flat_map(|c| c.into_nurbs_spans())
.collect_vec(),
}
});
Ok(vec![CompoundCurve2D::new_unchecked_aligned(
curves.flatten().collect_vec(),
)])
}
}
#[derive(Debug)]
enum Corner<T: FloatingPoint> {
Intersection(Point2<T>),
Curve(NurbsCurve2D<T>),
}
fn trim_polyline<T: FloatingPoint>(
polyline: &NurbsCurve2D<T>,
start: Option<Point2<T>>,
end: Option<Point2<T>>,
) -> NurbsCurve2D<T> {
let dehomogenized_control_points = polyline.dehomogenized_control_points();
let start = match start {
Some(p) => p,
None => dehomogenized_control_points[0],
};
let end = match end {
Some(p) => p,
None => dehomogenized_control_points[dehomogenized_control_points.len() - 1],
};
let pts = vec![start]
.into_iter()
.chain(
dehomogenized_control_points
.iter()
.skip(1)
.take(dehomogenized_control_points.len() - 2)
.cloned(),
)
.chain(vec![end])
.collect_vec();
NurbsCurve2D::polyline(&pts, false)
}
fn find_corner<T: FloatingPoint>(
s0: &[CompoundCurve2D<T>],
s1: &[CompoundCurve2D<T>],
option: &CurveOffsetOption<T>,
) -> anyhow::Result<Option<Corner<T>>> {
match (s0.len(), s1.len()) {
(1, 1) => {
let last = s0[0].spans().last();
let head = s1[0].spans().first();
match (last, head) {
(Some(last), Some(head)) => corner(last, head, option),
_ => Ok(None),
}
}
_ => Ok(None),
}
}
fn corner<T: FloatingPoint>(
c0: &NurbsCurve2D<T>,
c1: &NurbsCurve2D<T>,
option: &CurveOffsetOption<T>,
) -> anyhow::Result<Option<Corner<T>>> {
let last_degree = c0.degree();
let head_degree = c1.degree();
if last_degree == 1 && last_degree == head_degree {
let pt0 = c0.dehomogenized_control_points();
let n0 = pt0.len();
let pt1 = c1.dehomogenized_control_points();
let s0 = pt0[n0 - 2..].iter().collect_vec();
let s1 = pt1[..2].iter().collect_vec();
let l0 = to_line_helper(s0[0], s0[1]);
let l1 = to_line_helper(s1[0], s1[1]);
let it = geo::algorithm::line_intersection::line_intersection(l0, l1);
match it {
Some(LineIntersection::SinglePoint {
intersection,
is_proper: _,
}) => {
let pt = Point2::new(
T::from_f64(intersection.x).unwrap(),
T::from_f64(intersection.y).unwrap(),
);
Ok(Some(Corner::Intersection(pt)))
}
_ => {
match option.corner_type() {
CurveOffsetCornerType::None => unreachable!(),
CurveOffsetCornerType::Sharp => {
let delta = option.distance().abs() * T::from_f64(2.0).unwrap();
let it = sharp_corner_intersection([s0[0], s0[1], s1[0], s1[1]], delta)?;
let corner = NurbsCurve2D::polyline(&[*s0[1], it, *s1[0]], false);
Ok(Some(Corner::Curve(corner)))
}
CurveOffsetCornerType::Round => {
let arc = round_corner(s0[0], s0[1], s1[0], *option.distance())?;
Ok(Some(Corner::Curve(arc)))
}
CurveOffsetCornerType::Smooth => {
let bezier = smooth_corner(s0[0], s0[1], s1[0], s1[1], *option.distance())?;
Ok(Some(Corner::Curve(bezier)))
}
CurveOffsetCornerType::Chamfer => {
let last = pt0.last().ok_or(anyhow::anyhow!("No last point"))?;
let head = pt1.first().ok_or(anyhow::anyhow!("No head point"))?;
Ok(Some(Corner::Curve(NurbsCurve2D::polyline(
&[*last, *head],
false,
))))
}
}
}
}
} else {
Ok(None)
}
}
#[cfg(test)]
mod tests {}