1use std::f64::consts::PI;
16
17use crate::{
18 CircularArc2, Classification, Contour2, CurveError, CurvePolicy, CurveResult, CurveString2,
19 Point2, Region2, RegionContourProfile, RegionView2, Segment2,
20};
21
22#[derive(Clone, Copy, Debug, PartialEq)]
24pub struct FiniteProjectionOptions {
25 arc_chord_error: f64,
26}
27
28#[derive(Clone, Debug, PartialEq)]
30pub struct FinitePolyline2 {
31 points: Vec<[f64; 2]>,
32 arc_chord_error: f64,
33 closed: bool,
34}
35
36#[derive(Clone, Debug, PartialEq)]
41pub struct FiniteRegionProjection2 {
42 material_rings: Vec<FinitePolyline2>,
43 hole_rings: Vec<FinitePolyline2>,
44}
45
46#[derive(Clone, Debug, PartialEq)]
52pub struct FiniteRegionProfile2 {
53 material: FinitePolyline2,
54 holes: Vec<FinitePolyline2>,
55}
56
57impl FiniteProjectionOptions {
58 pub fn try_new(arc_chord_error: f64) -> CurveResult<Self> {
60 if arc_chord_error.is_finite() && arc_chord_error > 0.0 {
61 Ok(Self { arc_chord_error })
62 } else {
63 Err(CurveError::InvalidFiniteProjectionOptions)
64 }
65 }
66
67 pub const fn arc_chord_error(&self) -> f64 {
69 self.arc_chord_error
70 }
71}
72
73impl FinitePolyline2 {
74 fn new(points: Vec<[f64; 2]>, arc_chord_error: f64, closed: bool) -> Self {
75 Self {
76 points,
77 arc_chord_error,
78 closed,
79 }
80 }
81
82 pub fn points(&self) -> &[[f64; 2]] {
84 &self.points
85 }
86
87 pub fn into_points(self) -> Vec<[f64; 2]> {
89 self.points
90 }
91
92 pub const fn arc_chord_error(&self) -> f64 {
94 self.arc_chord_error
95 }
96
97 pub const fn is_closed(&self) -> bool {
99 self.closed
100 }
101
102 pub fn signed_ring_area(&self) -> f64 {
109 finite_ring_signed_area(&self.points)
110 }
111
112 pub fn try_signed_ring_area(&self) -> CurveResult<f64> {
114 try_finite_ring_signed_area(&self.points)
115 }
116
117 pub fn vertex_centroid(&self) -> Option<[f64; 2]> {
127 finite_polyline_vertex_centroid(&self.points)
128 }
129
130 pub fn try_vertex_centroid(&self) -> CurveResult<Option<[f64; 2]>> {
132 try_finite_polyline_vertex_centroid(&self.points)
133 }
134}
135
136impl FiniteRegionProjection2 {
137 fn new(material_rings: Vec<FinitePolyline2>, hole_rings: Vec<FinitePolyline2>) -> Self {
138 Self {
139 material_rings,
140 hole_rings,
141 }
142 }
143
144 pub fn material_rings(&self) -> &[FinitePolyline2] {
146 &self.material_rings
147 }
148
149 pub fn hole_rings(&self) -> &[FinitePolyline2] {
151 &self.hole_rings
152 }
153}
154
155impl FiniteRegionProfile2 {
156 fn new(material: FinitePolyline2, holes: Vec<FinitePolyline2>) -> Self {
157 Self { material, holes }
158 }
159
160 pub const fn material(&self) -> &FinitePolyline2 {
162 &self.material
163 }
164
165 pub fn holes(&self) -> &[FinitePolyline2] {
167 &self.holes
168 }
169
170 pub fn projected_filled_area(&self) -> f64 {
178 let material = self.material.signed_ring_area().abs();
179 let holes = self
180 .holes
181 .iter()
182 .map(|hole| hole.signed_ring_area().abs())
183 .sum::<f64>();
184 material - holes
185 }
186
187 pub fn try_projected_filled_area(&self) -> CurveResult<f64> {
189 let material = self.material.try_signed_ring_area()?.abs();
190 let holes = self.holes.iter().try_fold(0.0, |sum, hole| {
191 let next = sum + hole.try_signed_ring_area()?.abs();
192 next.is_finite()
193 .then_some(next)
194 .ok_or(CurveError::NonFiniteProjectionPoint)
195 })?;
196 let area = material - holes;
197 area.is_finite()
198 .then_some(area)
199 .ok_or(CurveError::NonFiniteProjectionPoint)
200 }
201}
202
203pub fn finite_ring_signed_area(ring: &[[f64; 2]]) -> f64 {
212 if ring.len() < 3 {
213 return 0.0;
214 }
215 let mut area = 0.0;
216 for edge in ring.windows(2) {
217 area += edge[0][0] * edge[1][1] - edge[1][0] * edge[0][1];
218 }
219 if let (Some(first), Some(last)) = (ring.first(), ring.last()) {
220 area += last[0] * first[1] - first[0] * last[1];
221 }
222 0.5 * area
223}
224
225pub fn try_finite_ring_signed_area(ring: &[[f64; 2]]) -> CurveResult<f64> {
230 let ring = normalize_finite_ring_vertices(ring)?;
231 if ring.len() < 3 {
232 return Ok(0.0);
233 }
234
235 let mut area = 0.0;
236 for edge in ring.windows(2) {
237 area = checked_shoelace_sum(area, edge[0], edge[1])?;
238 }
239 if let (Some(first), Some(last)) = (ring.first(), ring.last()) {
240 area = checked_shoelace_sum(area, *last, *first)?;
241 }
242 let area = 0.5 * area;
243 area.is_finite()
244 .then_some(area)
245 .ok_or(CurveError::NonFiniteProjectionPoint)
246}
247
248pub fn finite_polyline_vertex_centroid(points: &[[f64; 2]]) -> Option<[f64; 2]> {
254 let unique = points
255 .iter()
256 .copied()
257 .enumerate()
258 .filter_map(|(index, point)| {
259 (index + 1 != points.len() || Some(&point) != points.first()).then_some(point)
260 })
261 .collect::<Vec<_>>();
262 if unique.is_empty() {
263 return None;
264 }
265 let count = unique.len() as f64;
266 let (sum_x, sum_y) = unique
267 .iter()
268 .fold((0.0, 0.0), |(x, y), point| (x + point[0], y + point[1]));
269 Some([sum_x / count, sum_y / count])
270}
271
272pub fn try_finite_polyline_vertex_centroid(points: &[[f64; 2]]) -> CurveResult<Option<[f64; 2]>> {
277 let unique = finite_unique_polyline_vertices(points)?;
278 if unique.is_empty() {
279 return Ok(None);
280 }
281 let count = unique.len() as f64;
282 let (sum_x, sum_y) = unique.iter().try_fold((0.0, 0.0), |(x, y), point| {
283 let next_x = x + point[0];
284 let next_y = y + point[1];
285 (next_x.is_finite() && next_y.is_finite())
286 .then_some((next_x, next_y))
287 .ok_or(CurveError::NonFiniteProjectionPoint)
288 })?;
289 let centroid = [sum_x / count, sum_y / count];
290 centroid
291 .iter()
292 .all(|value| value.is_finite())
293 .then_some(Some(centroid))
294 .ok_or(CurveError::NonFiniteProjectionPoint)
295}
296
297pub(crate) fn normalize_finite_ring_vertices(ring: &[[f64; 2]]) -> CurveResult<Vec<[f64; 2]>> {
298 let mut normalized = Vec::with_capacity(ring.len());
299 for point in finite_unique_polyline_vertices(ring)? {
300 if normalized.last() == Some(&point) {
301 continue;
302 }
303 normalized.push(point);
304 }
305 if normalized.len() > 1 && normalized.first() == normalized.last() {
306 normalized.pop();
307 }
308 Ok(normalized)
309}
310
311fn finite_unique_polyline_vertices(points: &[[f64; 2]]) -> CurveResult<Vec<[f64; 2]>> {
312 let mut unique = Vec::with_capacity(points.len());
313 for (index, &[x, y]) in points.iter().enumerate() {
314 if !x.is_finite() || !y.is_finite() {
315 return Err(CurveError::NonFiniteProjectionPoint);
316 }
317 let point = [x, y];
318 if index + 1 != points.len() || Some(&point) != points.first() {
319 unique.push(point);
320 }
321 }
322 Ok(unique)
323}
324
325fn checked_shoelace_sum(sum: f64, start: [f64; 2], end: [f64; 2]) -> CurveResult<f64> {
326 let term = start[0] * end[1] - end[0] * start[1];
327 let next = sum + term;
328 next.is_finite()
329 .then_some(next)
330 .ok_or(CurveError::NonFiniteProjectionPoint)
331}
332
333impl CurveString2 {
334 pub fn project_to_finite_polyline(
340 &self,
341 options: &FiniteProjectionOptions,
342 ) -> CurveResult<FinitePolyline2> {
343 project_curve_string(self, options, false)
344 }
345}
346
347impl Contour2 {
348 pub fn project_to_finite_ring(
354 &self,
355 options: &FiniteProjectionOptions,
356 ) -> CurveResult<FinitePolyline2> {
357 project_curve_string(self.curve_string(), options, true)
358 }
359}
360
361impl Region2 {
362 pub fn project_to_finite_region(
368 &self,
369 options: &FiniteProjectionOptions,
370 ) -> CurveResult<FiniteRegionProjection2> {
371 self.as_view().project_to_finite_region(options)
372 }
373
374 pub fn project_to_finite_profiles(
384 &self,
385 options: &FiniteProjectionOptions,
386 policy: &CurvePolicy,
387 ) -> CurveResult<Classification<Vec<FiniteRegionProfile2>>> {
388 self.as_view().project_to_finite_profiles(options, policy)
389 }
390}
391
392impl<'a> RegionView2<'a> {
393 pub fn project_to_finite_region(
398 &self,
399 options: &FiniteProjectionOptions,
400 ) -> CurveResult<FiniteRegionProjection2> {
401 let material_rings = project_contour_slice(self.material_contours(), options)?;
402 let hole_rings = project_contour_slice(self.hole_contours(), options)?;
403 Ok(FiniteRegionProjection2::new(material_rings, hole_rings))
404 }
405
406 pub fn project_to_finite_profiles(
408 &self,
409 options: &FiniteProjectionOptions,
410 policy: &CurvePolicy,
411 ) -> CurveResult<Classification<Vec<FiniteRegionProfile2>>> {
412 match self.contour_profiles(policy) {
413 Classification::Decided(profiles) => profiles
414 .iter()
415 .map(|profile| project_region_profile(profile, options))
416 .collect::<CurveResult<Vec<_>>>()
417 .map(Classification::Decided),
418 Classification::Uncertain(reason) => Ok(Classification::Uncertain(reason)),
419 }
420 }
421}
422
423fn project_region_profile(
424 profile: &RegionContourProfile<'_>,
425 options: &FiniteProjectionOptions,
426) -> CurveResult<FiniteRegionProfile2> {
427 let material = profile.material.project_to_finite_ring(options)?;
428 let holes = profile
429 .holes
430 .iter()
431 .map(|hole| hole.project_to_finite_ring(options))
432 .collect::<CurveResult<Vec<_>>>()?;
433 Ok(FiniteRegionProfile2::new(material, holes))
434}
435
436fn project_contour_slice(
437 contours: &[&Contour2],
438 options: &FiniteProjectionOptions,
439) -> CurveResult<Vec<FinitePolyline2>> {
440 contours
441 .iter()
442 .map(|contour| contour.project_to_finite_ring(options))
443 .collect()
444}
445
446fn project_curve_string(
447 curve: &CurveString2,
448 options: &FiniteProjectionOptions,
449 close: bool,
450) -> CurveResult<FinitePolyline2> {
451 let first = curve.start().ok_or(CurveError::EmptyCurveString)?;
452 let mut points = Vec::with_capacity(curve.len() + 1);
453 push_if_new(&mut points, finite_point(first)?);
454
455 for segment in curve.segments() {
456 match segment {
457 Segment2::Line(line) => {
458 push_if_new(&mut points, finite_point(line.end())?);
459 }
460 Segment2::Arc(arc) => {
461 append_arc_samples(&mut points, arc, options.arc_chord_error)?;
462 }
463 }
464 }
465
466 if close {
467 close_ring(&mut points);
468 }
469
470 Ok(FinitePolyline2::new(points, options.arc_chord_error, close))
471}
472
473fn finite_point(point: &Point2) -> CurveResult<[f64; 2]> {
474 let x = point
475 .x()
476 .to_f64_lossy()
477 .filter(|value| value.is_finite())
478 .ok_or(CurveError::NonFiniteProjectionPoint)?;
479 let y = point
480 .y()
481 .to_f64_lossy()
482 .filter(|value| value.is_finite())
483 .ok_or(CurveError::NonFiniteProjectionPoint)?;
484 Ok([x, y])
485}
486
487fn append_arc_samples(
488 points: &mut Vec<[f64; 2]>,
489 arc: &CircularArc2,
490 chord_error: f64,
491) -> CurveResult<usize> {
492 let start = finite_point(arc.start())?;
493 let end = finite_point(arc.end())?;
494 let center = finite_point(arc.center())?;
495
496 let radius = ((start[0] - center[0]).powi(2) + (start[1] - center[1]).powi(2)).sqrt();
497 if !radius.is_finite() || radius <= f64::EPSILON {
498 return Err(CurveError::NonFiniteProjectionPoint);
499 }
500
501 let a0 = (start[1] - center[1]).atan2(start[0] - center[0]);
502 let a1 = (end[1] - center[1]).atan2(end[0] - center[0]);
503 let mut sweep = a1 - a0;
504 if arc.is_clockwise() {
505 if sweep > 0.0 {
506 sweep -= 2.0 * PI;
507 }
508 } else if sweep < 0.0 {
509 sweep += 2.0 * PI;
510 }
511
512 let max_angle = (1.0 - (chord_error / radius).min(1.0)).acos().max(1e-3) * 2.0;
513 let steps = ((sweep.abs() / max_angle).ceil() as usize).max(1);
514 let before = points.len();
515 for step in 1..=steps {
516 let t = step as f64 / steps as f64;
517 let angle = a0 + sweep * t;
518 let point = [
519 center[0] + radius * angle.cos(),
520 center[1] + radius * angle.sin(),
521 ];
522 if !point[0].is_finite() || !point[1].is_finite() {
523 return Err(CurveError::NonFiniteProjectionPoint);
524 }
525 push_if_new(points, point);
526 }
527 Ok(points.len() - before)
528}
529
530fn close_ring(points: &mut Vec<[f64; 2]>) {
531 if points.len() >= 2 && points.first() != points.last() {
532 points.push(points[0]);
533 }
534}
535
536fn push_if_new(points: &mut Vec<[f64; 2]>, point: [f64; 2]) {
537 if points.last().is_none_or(|last| *last != point) {
538 points.push(point);
539 }
540}