use axiolid_core::{Point2, Tolerance};
use i_overlay::mesh::outline::offset::OutlineOffset;
use i_overlay::mesh::stroke::offset::StrokeOffset;
use i_overlay::mesh::style::{LineCap, LineJoin, OutlineStyle, StrokeStyle};
use crate::{canonical, validate_ring, OverlayError, Polygon, Ring};
#[derive(Debug, Clone, Copy, PartialEq)]
#[non_exhaustive]
pub enum JoinStyle {
Bevel,
Miter { angle_limit: f64 },
Round { max_segment_ratio: f64 },
}
#[derive(Debug, Clone, Copy, PartialEq)]
#[non_exhaustive]
pub enum CapStyle {
Butt,
Square,
Round { max_segment_ratio: f64 },
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub struct OffsetEvidence {
pub input_rings: usize,
pub output_polygons: usize,
pub output_holes: usize,
pub collapsed: bool,
}
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub struct OffsetResult {
pub polygons: Vec<Polygon>,
pub evidence: OffsetEvidence,
}
impl JoinStyle {
fn to_backend(self) -> Result<LineJoin<f64>, OverlayError> {
match self {
Self::Bevel => Ok(LineJoin::Bevel),
Self::Miter { angle_limit } => {
if !angle_limit.is_finite() || angle_limit <= 0.0 {
return Err(OverlayError::InvalidOffsetStyle);
}
Ok(LineJoin::Miter(angle_limit))
}
Self::Round { max_segment_ratio } => {
if !max_segment_ratio.is_finite() || max_segment_ratio <= 0.0 {
return Err(OverlayError::InvalidOffsetStyle);
}
Ok(LineJoin::Round(max_segment_ratio))
}
}
}
}
impl CapStyle {
fn to_backend(self) -> Result<LineCap<[f64; 2], f64>, OverlayError> {
match self {
Self::Butt => Ok(LineCap::Butt),
Self::Square => Ok(LineCap::Square),
Self::Round { max_segment_ratio } => {
if !max_segment_ratio.is_finite() || max_segment_ratio <= 0.0 {
return Err(OverlayError::InvalidOffsetStyle);
}
Ok(LineCap::Round(max_segment_ratio))
}
}
}
}
fn to_kernel(shapes: Vec<Vec<Vec<[f64; 2]>>>) -> Vec<Polygon> {
let mut polygons: Vec<Polygon> = shapes
.into_iter()
.filter_map(|shape| {
let mut rings = shape.into_iter();
let outer = rings.next()?;
let outer = canonical(
Ring {
points: outer.into_iter().map(|p| Point2::new(p[0], p[1])).collect(),
},
true,
);
if outer.points.len() < 3 {
return None;
}
let holes = rings
.filter_map(|ring| {
let ring = canonical(
Ring {
points: ring.into_iter().map(|p| Point2::new(p[0], p[1])).collect(),
},
false,
);
(ring.points.len() >= 3).then_some(ring)
})
.collect();
Some(Polygon { outer, holes })
})
.collect();
polygons.sort_by(|a, b| {
a.outer.points[0]
.x
.total_cmp(&b.outer.points[0].x)
.then(a.outer.points[0].y.total_cmp(&b.outer.points[0].y))
});
polygons
}
fn backend_shape(polygons: &[Polygon]) -> Vec<Vec<Vec<[f64; 2]>>> {
polygons
.iter()
.map(|polygon| {
core::iter::once(&polygon.outer)
.chain(polygon.holes.iter())
.map(|ring| ring.points.iter().map(|p| [p.x, p.y]).collect())
.collect()
})
.collect()
}
pub fn offset_polygons(
polygons: &[Polygon],
distance: f64,
join: JoinStyle,
tolerance: Tolerance,
) -> Result<OffsetResult, OverlayError> {
if !distance.is_finite() {
return Err(OverlayError::InvalidOffsetDistance);
}
for polygon in polygons {
validate_ring(&polygon.outer, tolerance)?;
for hole in &polygon.holes {
validate_ring(hole, tolerance)?;
}
}
let input_rings = polygons.iter().map(|p| 1 + p.holes.len()).sum();
let result = if distance == 0.0 {
to_kernel(backend_shape(polygons))
} else {
let style = OutlineStyle::new(distance).line_join(join.to_backend()?);
crate::settle::settle(
to_kernel(backend_shape(polygons).outline(&style)),
tolerance,
)
};
let evidence = OffsetEvidence {
input_rings,
output_polygons: result.len(),
output_holes: result.iter().map(|p| p.holes.len()).sum(),
collapsed: !polygons.is_empty() && result.is_empty(),
};
Ok(OffsetResult {
polygons: result,
evidence,
})
}
pub fn stroke_polyline(
points: &[Point2],
width: f64,
join: JoinStyle,
cap: CapStyle,
closed: bool,
) -> Result<OffsetResult, OverlayError> {
if points.len() < 2 {
return Err(OverlayError::RingTooShort);
}
if !points.iter().all(|point| point.is_finite()) {
return Err(OverlayError::NonFinitePoint);
}
if !width.is_finite() || width <= 0.0 {
return Err(OverlayError::InvalidOffsetDistance);
}
let path: Vec<[f64; 2]> = points.iter().map(|p| [p.x, p.y]).collect();
let style = StrokeStyle::new(width)
.line_join(join.to_backend()?)
.start_cap(cap.to_backend()?)
.end_cap(cap.to_backend()?);
let result = to_kernel(path.stroke(style, closed));
let evidence = OffsetEvidence {
input_rings: 1,
output_polygons: result.len(),
output_holes: result.iter().map(|p| p.holes.len()).sum(),
collapsed: result.is_empty(),
};
Ok(OffsetResult {
polygons: result,
evidence,
})
}
#[must_use]
pub fn ring_area(ring: &Ring) -> f64 {
ring.points
.iter()
.zip(ring.points.iter().cycle().skip(1))
.take(ring.points.len())
.map(|(a, b)| a.x * b.y - b.x * a.y)
.sum::<f64>()
.abs()
* 0.5
}
#[must_use]
pub fn polygon_area(polygon: &Polygon) -> f64 {
let holes: f64 = polygon.holes.iter().map(ring_area).sum();
(ring_area(&polygon.outer) - holes).max(0.0)
}
#[must_use]
pub fn total_area(polygons: &[Polygon]) -> f64 {
polygons.iter().map(polygon_area).sum()
}