use std::f64::consts::PI;
use crate::{
CircularArc2, Classification, Contour2, CurveError, CurvePolicy, CurveResult, CurveString2,
Point2, Region2, RegionContourProfile, RegionView2, Segment2,
};
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct FiniteProjectionOptions {
arc_chord_error: f64,
}
#[derive(Clone, Debug, PartialEq)]
pub struct FinitePolyline2 {
points: Vec<[f64; 2]>,
arc_chord_error: f64,
closed: bool,
}
#[derive(Clone, Debug, PartialEq)]
pub struct FiniteRegionProjection2 {
material_rings: Vec<FinitePolyline2>,
hole_rings: Vec<FinitePolyline2>,
}
#[derive(Clone, Debug, PartialEq)]
pub struct FiniteRegionProfile2 {
material: FinitePolyline2,
holes: Vec<FinitePolyline2>,
}
impl FiniteProjectionOptions {
pub fn try_new(arc_chord_error: f64) -> CurveResult<Self> {
if arc_chord_error.is_finite() && arc_chord_error > 0.0 {
Ok(Self { arc_chord_error })
} else {
Err(CurveError::InvalidFiniteProjectionOptions)
}
}
pub const fn arc_chord_error(&self) -> f64 {
self.arc_chord_error
}
}
impl FinitePolyline2 {
fn new(points: Vec<[f64; 2]>, arc_chord_error: f64, closed: bool) -> Self {
Self {
points,
arc_chord_error,
closed,
}
}
pub fn points(&self) -> &[[f64; 2]] {
&self.points
}
pub fn into_points(self) -> Vec<[f64; 2]> {
self.points
}
pub const fn arc_chord_error(&self) -> f64 {
self.arc_chord_error
}
pub const fn is_closed(&self) -> bool {
self.closed
}
pub fn signed_ring_area(&self) -> f64 {
finite_ring_signed_area(&self.points)
}
pub fn try_signed_ring_area(&self) -> CurveResult<f64> {
try_finite_ring_signed_area(&self.points)
}
pub fn vertex_centroid(&self) -> Option<[f64; 2]> {
finite_polyline_vertex_centroid(&self.points)
}
pub fn try_vertex_centroid(&self) -> CurveResult<Option<[f64; 2]>> {
try_finite_polyline_vertex_centroid(&self.points)
}
}
impl FiniteRegionProjection2 {
fn new(material_rings: Vec<FinitePolyline2>, hole_rings: Vec<FinitePolyline2>) -> Self {
Self {
material_rings,
hole_rings,
}
}
pub fn material_rings(&self) -> &[FinitePolyline2] {
&self.material_rings
}
pub fn hole_rings(&self) -> &[FinitePolyline2] {
&self.hole_rings
}
}
impl FiniteRegionProfile2 {
fn new(material: FinitePolyline2, holes: Vec<FinitePolyline2>) -> Self {
Self { material, holes }
}
pub const fn material(&self) -> &FinitePolyline2 {
&self.material
}
pub fn holes(&self) -> &[FinitePolyline2] {
&self.holes
}
pub fn projected_filled_area(&self) -> f64 {
let material = self.material.signed_ring_area().abs();
let holes = self
.holes
.iter()
.map(|hole| hole.signed_ring_area().abs())
.sum::<f64>();
material - holes
}
pub fn try_projected_filled_area(&self) -> CurveResult<f64> {
let material = self.material.try_signed_ring_area()?.abs();
let holes = self.holes.iter().try_fold(0.0, |sum, hole| {
let next = sum + hole.try_signed_ring_area()?.abs();
next.is_finite()
.then_some(next)
.ok_or(CurveError::NonFiniteProjectionPoint)
})?;
let area = material - holes;
area.is_finite()
.then_some(area)
.ok_or(CurveError::NonFiniteProjectionPoint)
}
}
pub fn finite_ring_signed_area(ring: &[[f64; 2]]) -> f64 {
if ring.len() < 3 {
return 0.0;
}
let mut area = 0.0;
for edge in ring.windows(2) {
area += edge[0][0] * edge[1][1] - edge[1][0] * edge[0][1];
}
if let (Some(first), Some(last)) = (ring.first(), ring.last()) {
area += last[0] * first[1] - first[0] * last[1];
}
0.5 * area
}
pub fn try_finite_ring_signed_area(ring: &[[f64; 2]]) -> CurveResult<f64> {
let ring = normalize_finite_ring_vertices(ring)?;
if ring.len() < 3 {
return Ok(0.0);
}
let mut area = 0.0;
for edge in ring.windows(2) {
area = checked_shoelace_sum(area, edge[0], edge[1])?;
}
if let (Some(first), Some(last)) = (ring.first(), ring.last()) {
area = checked_shoelace_sum(area, *last, *first)?;
}
let area = 0.5 * area;
area.is_finite()
.then_some(area)
.ok_or(CurveError::NonFiniteProjectionPoint)
}
pub fn finite_polyline_vertex_centroid(points: &[[f64; 2]]) -> Option<[f64; 2]> {
let unique = points
.iter()
.copied()
.enumerate()
.filter_map(|(index, point)| {
(index + 1 != points.len() || Some(&point) != points.first()).then_some(point)
})
.collect::<Vec<_>>();
if unique.is_empty() {
return None;
}
let count = unique.len() as f64;
let (sum_x, sum_y) = unique
.iter()
.fold((0.0, 0.0), |(x, y), point| (x + point[0], y + point[1]));
Some([sum_x / count, sum_y / count])
}
pub fn try_finite_polyline_vertex_centroid(points: &[[f64; 2]]) -> CurveResult<Option<[f64; 2]>> {
let unique = finite_unique_polyline_vertices(points)?;
if unique.is_empty() {
return Ok(None);
}
let count = unique.len() as f64;
let (sum_x, sum_y) = unique.iter().try_fold((0.0, 0.0), |(x, y), point| {
let next_x = x + point[0];
let next_y = y + point[1];
(next_x.is_finite() && next_y.is_finite())
.then_some((next_x, next_y))
.ok_or(CurveError::NonFiniteProjectionPoint)
})?;
let centroid = [sum_x / count, sum_y / count];
centroid
.iter()
.all(|value| value.is_finite())
.then_some(Some(centroid))
.ok_or(CurveError::NonFiniteProjectionPoint)
}
pub(crate) fn normalize_finite_ring_vertices(ring: &[[f64; 2]]) -> CurveResult<Vec<[f64; 2]>> {
let mut normalized = Vec::with_capacity(ring.len());
for point in finite_unique_polyline_vertices(ring)? {
if normalized.last() == Some(&point) {
continue;
}
normalized.push(point);
}
if normalized.len() > 1 && normalized.first() == normalized.last() {
normalized.pop();
}
Ok(normalized)
}
fn finite_unique_polyline_vertices(points: &[[f64; 2]]) -> CurveResult<Vec<[f64; 2]>> {
let mut unique = Vec::with_capacity(points.len());
for (index, &[x, y]) in points.iter().enumerate() {
if !x.is_finite() || !y.is_finite() {
return Err(CurveError::NonFiniteProjectionPoint);
}
let point = [x, y];
if index + 1 != points.len() || Some(&point) != points.first() {
unique.push(point);
}
}
Ok(unique)
}
fn checked_shoelace_sum(sum: f64, start: [f64; 2], end: [f64; 2]) -> CurveResult<f64> {
let term = start[0] * end[1] - end[0] * start[1];
let next = sum + term;
next.is_finite()
.then_some(next)
.ok_or(CurveError::NonFiniteProjectionPoint)
}
impl CurveString2 {
pub fn project_to_finite_polyline(
&self,
options: &FiniteProjectionOptions,
) -> CurveResult<FinitePolyline2> {
project_curve_string(self, options, false)
}
}
impl Contour2 {
pub fn project_to_finite_ring(
&self,
options: &FiniteProjectionOptions,
) -> CurveResult<FinitePolyline2> {
project_curve_string(self.curve_string(), options, true)
}
}
impl Region2 {
pub fn project_to_finite_region(
&self,
options: &FiniteProjectionOptions,
) -> CurveResult<FiniteRegionProjection2> {
self.as_view().project_to_finite_region(options)
}
pub fn project_to_finite_profiles(
&self,
options: &FiniteProjectionOptions,
policy: &CurvePolicy,
) -> CurveResult<Classification<Vec<FiniteRegionProfile2>>> {
self.as_view().project_to_finite_profiles(options, policy)
}
}
impl<'a> RegionView2<'a> {
pub fn project_to_finite_region(
&self,
options: &FiniteProjectionOptions,
) -> CurveResult<FiniteRegionProjection2> {
let material_rings = project_contour_slice(self.material_contours(), options)?;
let hole_rings = project_contour_slice(self.hole_contours(), options)?;
Ok(FiniteRegionProjection2::new(material_rings, hole_rings))
}
pub fn project_to_finite_profiles(
&self,
options: &FiniteProjectionOptions,
policy: &CurvePolicy,
) -> CurveResult<Classification<Vec<FiniteRegionProfile2>>> {
match self.contour_profiles(policy) {
Classification::Decided(profiles) => profiles
.iter()
.map(|profile| project_region_profile(profile, options))
.collect::<CurveResult<Vec<_>>>()
.map(Classification::Decided),
Classification::Uncertain(reason) => Ok(Classification::Uncertain(reason)),
}
}
}
fn project_region_profile(
profile: &RegionContourProfile<'_>,
options: &FiniteProjectionOptions,
) -> CurveResult<FiniteRegionProfile2> {
let material = profile.material.project_to_finite_ring(options)?;
let holes = profile
.holes
.iter()
.map(|hole| hole.project_to_finite_ring(options))
.collect::<CurveResult<Vec<_>>>()?;
Ok(FiniteRegionProfile2::new(material, holes))
}
fn project_contour_slice(
contours: &[&Contour2],
options: &FiniteProjectionOptions,
) -> CurveResult<Vec<FinitePolyline2>> {
contours
.iter()
.map(|contour| contour.project_to_finite_ring(options))
.collect()
}
fn project_curve_string(
curve: &CurveString2,
options: &FiniteProjectionOptions,
close: bool,
) -> CurveResult<FinitePolyline2> {
let first = curve.start().ok_or(CurveError::EmptyCurveString)?;
let mut points = Vec::with_capacity(curve.len() + 1);
push_if_new(&mut points, finite_point(first)?);
for segment in curve.segments() {
match segment {
Segment2::Line(line) => {
push_if_new(&mut points, finite_point(line.end())?);
}
Segment2::Arc(arc) => {
append_arc_samples(&mut points, arc, options.arc_chord_error)?;
}
}
}
if close {
close_ring(&mut points);
}
Ok(FinitePolyline2::new(points, options.arc_chord_error, close))
}
fn finite_point(point: &Point2) -> CurveResult<[f64; 2]> {
let x = point
.x()
.to_f64_lossy()
.filter(|value| value.is_finite())
.ok_or(CurveError::NonFiniteProjectionPoint)?;
let y = point
.y()
.to_f64_lossy()
.filter(|value| value.is_finite())
.ok_or(CurveError::NonFiniteProjectionPoint)?;
Ok([x, y])
}
fn append_arc_samples(
points: &mut Vec<[f64; 2]>,
arc: &CircularArc2,
chord_error: f64,
) -> CurveResult<usize> {
let start = finite_point(arc.start())?;
let end = finite_point(arc.end())?;
let center = finite_point(arc.center())?;
let radius = ((start[0] - center[0]).powi(2) + (start[1] - center[1]).powi(2)).sqrt();
if !radius.is_finite() || radius <= f64::EPSILON {
return Err(CurveError::NonFiniteProjectionPoint);
}
let a0 = (start[1] - center[1]).atan2(start[0] - center[0]);
let a1 = (end[1] - center[1]).atan2(end[0] - center[0]);
let mut sweep = a1 - a0;
if arc.is_clockwise() {
if sweep > 0.0 {
sweep -= 2.0 * PI;
}
} else if sweep < 0.0 {
sweep += 2.0 * PI;
}
let max_angle = (1.0 - (chord_error / radius).min(1.0)).acos().max(1e-3) * 2.0;
let steps = ((sweep.abs() / max_angle).ceil() as usize).max(1);
let before = points.len();
for step in 1..=steps {
let t = step as f64 / steps as f64;
let angle = a0 + sweep * t;
let point = [
center[0] + radius * angle.cos(),
center[1] + radius * angle.sin(),
];
if !point[0].is_finite() || !point[1].is_finite() {
return Err(CurveError::NonFiniteProjectionPoint);
}
push_if_new(points, point);
}
Ok(points.len() - before)
}
fn close_ring(points: &mut Vec<[f64; 2]>) {
if points.len() >= 2 && points.first() != points.last() {
points.push(points[0]);
}
}
fn push_if_new(points: &mut Vec<[f64; 2]>, point: [f64; 2]) {
if points.last().is_none_or(|last| *last != point) {
points.push(point);
}
}