1#![forbid(unsafe_code)]
2mod arc;
36mod arc_check;
37mod arc_clearance;
38mod arc_overlay;
39mod arrangement;
40mod budget;
41mod circle;
42mod contact;
43mod convex_parts;
44mod cover;
45mod degenerate;
46mod exact_arc;
47mod exact_overlay;
48mod fill;
49mod free_faces;
50mod minkowski;
51mod nesting;
52mod offset;
53mod rectangle;
54mod region;
55mod segment_sweep;
56mod settle;
57mod visibility;
58
59pub use arc::{
60 arc_edge_radius, arc_ring_area, reverse_arc_ring, validate_arc_ring, ArcRing, ArcVertex,
61};
62pub use arc_check::{
63 check_arc_polygon, check_arc_polygon_with, ArcPolygonCheck, ArcPolygonDefect, ArcRingContact,
64 ArcRingRef,
65};
66pub use arc_clearance::ArcCheckOptions;
67pub use arc_overlay::{arc_overlay, ArcOverlayEvidence, ArcOverlayResult, ArcPolygon};
68pub use arrangement::{
69 ArcArrangement, ArrangementEdge, ArrangementRegion, EdgeSource, EdgeUse as ArrangementEdgeUse,
70};
71pub use budget::OverlayRefusal;
72pub use circle::{
73 minimum_enclosing_circle, CircleError, CircleEvidence, EnclosingCircle, MinimumCircle,
74};
75pub use degenerate::{
76 DegenerateRings, DropReason, DroppedRing, OverlayOperand, OverlayOptions, ReportedOverlay,
77 ReportedUnion, RingRole,
78};
79pub use free_faces::{
80 free_faces, free_faces_within, BarrierDefect, Bridge, FaceLocation, FreeFace, FreeFaceEvidence,
81 FreeFaces, FreeFacesError, FreeRing, FreeSpaceInput, PieceSource,
82};
83pub use minkowski::{BoundSide, MinkowskiError, MorphologyBound, MorphologyMode};
84pub use offset::{
85 offset_polygons, polygon_area, ring_area, stroke_polyline, total_area, CapStyle, JoinStyle,
86 OffsetEvidence, OffsetResult,
87};
88pub use rectangle::{
89 minimum_area_rectangle, MinimumRectangle, OrientedRectangle, RectangleError, RectangleEvidence,
90};
91pub use region::{Region, RegionEvidence};
92pub use segment_sweep::{
93 segment_intersections, ExactPoint2, Incidence, IntersectionPoint, SegmentIntersections,
94 SegmentLocation, SegmentOverlap, SegmentSweepError, SweepEvidence,
95};
96pub use visibility::VisibilityError;
97
98use std::borrow::Cow;
99
100use axiolid_contracts::ExecutionOptions;
101use axiolid_core::{Frame2, Point2, Polygon2, Tolerance};
102use axiolid_exact::{certify, Arith, SignExpr};
103use axiolid_guarantees::Sign;
104use budget::Charge;
105use contact::{on_segment, segments_intersect, side};
106
107#[derive(Debug, Clone, Copy, PartialEq, Eq)]
108pub enum FillRule {
109 EvenOdd,
110 NonZero,
111 Positive,
112 Negative,
113}
114#[derive(Debug, Clone, Copy, PartialEq, Eq)]
115pub enum OverlayOperation {
116 Intersection,
117 Union,
118 Difference,
119 Xor,
120}
121#[derive(Debug, Clone, PartialEq)]
122pub struct Ring {
123 pub points: Vec<Point2>,
124}
125
126impl From<Polygon2> for Ring {
134 fn from(polygon: Polygon2) -> Self {
135 Self {
136 points: polygon.vertices,
137 }
138 }
139}
140
141impl From<Ring> for Polygon2 {
142 fn from(ring: Ring) -> Self {
143 Self::new(ring.points)
144 }
145}
146
147impl From<&Ring> for Polygon2 {
148 fn from(ring: &Ring) -> Self {
149 Self::new(ring.points.clone())
150 }
151}
152
153#[derive(Debug, Clone, PartialEq)]
154pub struct Polygon {
155 pub outer: Ring,
156 pub holes: Vec<Ring>,
157}
158
159impl Polygon {
160 pub fn outline(&self) -> Polygon2 {
171 Polygon2::from(&self.outer)
172 }
173
174 pub fn has_holes(&self) -> bool {
178 !self.holes.is_empty()
179 }
180}
181#[derive(Debug, Clone, PartialEq)]
182pub struct OverlayInput {
183 pub frame: Frame2,
184 pub polygons: Vec<Polygon>,
185}
186#[derive(Debug, Clone, PartialEq, Eq)]
187#[non_exhaustive]
188pub enum OverlayError {
189 InvalidFrame,
190 NonFinitePoint,
191 RingTooShort,
192 RepeatedVertex,
193 ZeroArea,
194 SelfIntersection,
196 HoleOutsideOuter,
199 InvalidOffsetDistance,
202 InvalidOffsetStyle,
207 ZeroRadiusArc,
213 HolesOverlap,
217 PolygonsOverlap,
220}
221
222impl core::fmt::Display for OverlayError {
223 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
224 f.write_str(match self {
225 Self::InvalidFrame => "a frame is not orthonormal, or the operands' frames differ",
226 Self::NonFinitePoint => "a point has a non-finite coordinate",
227 Self::RingTooShort => "a ring or polyline has too few vertices",
228 Self::RepeatedVertex => "a ring repeats a vertex within tolerance",
229 Self::ZeroArea => "a ring encloses no area above tolerance",
230 Self::SelfIntersection => "non-adjacent boundary segments meet or cross",
231 Self::HoleOutsideOuter => "a hole lies outside its outer ring or crosses it",
232 Self::InvalidOffsetDistance => {
233 "an offset distance or stroke width is not finite, or a width is not positive"
234 }
235 Self::InvalidOffsetStyle => "a join or cap parameter is not a finite positive value",
236 Self::ZeroRadiusArc => "an arc edge implies a radius not above tolerance",
237 Self::HolesOverlap => "two holes of a polygon overlap",
238 Self::PolygonsOverlap => "two polygons of a region overlap",
239 })
240 }
241}
242
243impl std::error::Error for OverlayError {}
244
245#[derive(Debug, Clone, Copy, PartialEq, Eq)]
246pub struct OverlayEvidence {
247 pub subject_rings: usize,
248 pub clip_rings: usize,
249 pub output_polygons: usize,
250 pub output_holes: usize,
252}
253#[derive(Debug, Clone, PartialEq)]
254pub struct OverlayResult {
255 pub polygons: Vec<Polygon>,
256 pub evidence: OverlayEvidence,
257}
258fn signed(r: &Ring) -> f64 {
266 let Some(&o) = r.points.first() else {
267 return 0.0;
268 };
269 r.points
270 .windows(2)
271 .skip(1)
272 .map(|w| (w[0] - o).perp_dot(w[1] - o))
273 .sum::<f64>()
274 * 0.5
275}
276
277struct TwiceArea<'a> {
285 points: &'a [Point2],
286 offset: f64,
287}
288
289impl SignExpr for TwiceArea<'_> {
290 fn sign_in<T: Arith>(&self) -> Option<Sign> {
291 let f = T::from_f64;
292 let o = self.points.first()?;
293 let local = |p: &Point2| (f(p.x).sub(&f(o.x)), f(p.y).sub(&f(o.y)));
294 let mut sum = f(self.offset).neg();
295 for w in self.points.windows(2).skip(1) {
296 let ((ax, ay), (bx, by)) = (local(&w[0]), local(&w[1]));
297 sum = sum.add(&ax.mul(&by).sub(&ay.mul(&bx)));
298 }
299 sum.sign()
300 }
301}
302
303fn area_sign(points: &[Point2], offset: f64) -> Sign {
304 certify(&TwiceArea { points, offset }).unwrap_or(Sign::Zero)
305}
306
307pub(crate) fn orientation(points: &[Point2]) -> Sign {
310 area_sign(points, 0.0)
311}
312
313pub(crate) fn zero_area(points: &[Point2], linear: f64) -> bool {
316 let bound = 2.0 * linear * linear;
317 area_sign(points, bound) != Sign::Positive && area_sign(points, -bound) != Sign::Negative
318}
319fn self_intersects(r: &Ring, t: Tolerance) -> bool {
320 first_touching_pair(&r.points, t.linear()).is_some()
321}
322
323const ALL_PAIRS_UP_TO: usize = 32;
326
327pub(crate) fn first_touching_pair(points: &[Point2], linear: f64) -> Option<(usize, usize)> {
339 let n = points.len();
340 let edge = |i: usize| (points[i], points[(i + 1) % n]);
341 let meets = |i: usize, j: usize| {
342 if j == i + 1 || (i == 0 && j + 1 == n) {
343 return false;
344 }
345 let ((a, b), (c, d)) = (edge(i), edge(j));
346 segments_intersect(a, b, c, d, linear)
347 };
348 if n <= ALL_PAIRS_UP_TO {
349 return (0..n).find_map(|i| (i + 1..n).find(|&j| meets(i, j)).map(|j| (i, j)));
350 }
351 let boxes: Vec<exact_arc::Bounds> = (0..n)
352 .map(|i| {
353 let (a, b) = edge(i);
354 exact_arc::Bounds::around(&[a, b], linear)
355 })
356 .collect();
357 let tree = exact_arc::boxes::BoxTree::new(boxes.clone());
358 let (mut candidates, mut stack) = (Vec::new(), Vec::new());
359 for (i, own) in boxes.iter().enumerate() {
360 tree.query_into(|b| b.overlaps(own), &mut candidates, &mut stack);
361 if let Some(&j) = candidates.iter().find(|&&j| j > i && meets(i, j)) {
362 return Some((i, j));
363 }
364 }
365 None
366}
367
368pub(crate) fn validate_ring(r: &Ring, t: Tolerance) -> Result<(), OverlayError> {
379 #[cfg(test)]
380 tests::VALIDATED.with(|count| count.set(count.get() + 1));
381 ring_checks(r, t)
382}
383
384pub(crate) fn ring_checks(r: &Ring, t: Tolerance) -> Result<(), OverlayError> {
387 if r.points.len() < 3 {
388 return Err(OverlayError::RingTooShort);
389 };
390 if !r.points.iter().all(|p| p.is_finite()) {
391 return Err(OverlayError::NonFinitePoint);
392 };
393 if r.points
394 .iter()
395 .zip(r.points.iter().cycle().skip(1))
396 .take(r.points.len())
397 .any(|(a, b)| (*a - *b).length() <= t.linear())
398 {
399 return Err(OverlayError::RepeatedVertex);
400 };
401 if self_intersects(r, t) {
402 return Err(OverlayError::SelfIntersection);
403 }
404 if zero_area(&r.points, t.linear()) && !exact_overlay::on_one_line(&r.points) {
409 return Err(OverlayError::ZeroArea);
410 };
411 Ok(())
412}
413fn validate(input: &OverlayInput, t: Tolerance) -> Result<(), OverlayError> {
414 validate_frame(input, t)?;
415 for p in &input.polygons {
416 validate_ring(&p.outer, t)?;
417 for h in &p.holes {
418 validate_ring(h, t)?;
419 if hole_outside(&p.outer, h, t) {
420 return Err(OverlayError::HoleOutsideOuter);
421 }
422 }
423 }
424 Ok(())
425}
426fn admit_known<'a>(
429 input: &'a OverlayInput,
430 operand: OverlayOperand,
431 tolerance: Tolerance,
432 options: OverlayOptions,
433 known: bool,
434 dropped: &mut Vec<DroppedRing>,
435) -> Result<Cow<'a, [Polygon]>, OverlayError> {
436 if known && options.degenerate_rings == DegenerateRings::Refuse {
437 validate_known(input, tolerance)?;
438 return Ok(Cow::Borrowed(&input.polygons));
439 }
440 degenerate::admit(input, operand, tolerance, options, dropped)
441}
442
443fn validate_known(input: &OverlayInput, t: Tolerance) -> Result<(), OverlayError> {
446 validate_frame(input, t)?;
447 for p in &input.polygons {
448 if p.holes.iter().any(|h| hole_outside(&p.outer, h, t)) {
449 return Err(OverlayError::HoleOutsideOuter);
450 }
451 }
452 Ok(())
453}
454fn validate_frame(input: &OverlayInput, t: Tolerance) -> Result<(), OverlayError> {
455 let f = input.frame;
456 if !f.origin.is_finite()
457 || !f.x.is_finite()
458 || !f.y.is_finite()
459 || (f.x.length() - 1.).abs() > t.linear()
460 || (f.y.length() - 1.).abs() > t.linear()
461 || f.x.dot(f.y).abs() > t.linear()
462 || f.x.perp_dot(f.y) <= 0.
463 {
464 return Err(OverlayError::InvalidFrame);
465 }
466 Ok(())
467}
468fn hole_outside(outer: &Ring, hole: &Ring, t: Tolerance) -> bool {
474 let n = outer.points.len();
475 let on_boundary = |q: Point2| {
476 (0..n).any(|i| {
477 let (a, b) = (outer.points[i], outer.points[(i + 1) % n]);
478 on_segment(a, b, q, t.linear())
479 })
480 };
481 hole.points
482 .iter()
483 .any(|&q| !on_boundary(q) && !contains(outer, q))
484}
485fn contains(r: &Ring, p: Point2) -> bool {
486 let mut inside = false;
487 for (a, b) in r
488 .points
489 .iter()
490 .zip(r.points.iter().cycle().skip(1))
491 .take(r.points.len())
492 {
493 if (a.y > p.y) != (b.y > p.y) {
498 let towards = if b.y > a.y {
499 Sign::Positive
500 } else {
501 Sign::Negative
502 };
503 if side(*a, *b, p) == towards {
504 inside = !inside
505 }
506 }
507 }
508 inside
509}
510pub(crate) fn canonical(mut r: Ring, want_positive: bool) -> Ring {
511 if (orientation(&r.points) == Sign::Positive) != want_positive {
512 r.points.reverse()
513 };
514 let k = r
515 .points
516 .iter()
517 .enumerate()
518 .min_by(|a, b| {
519 a.1.x
520 .total_cmp(&b.1.x)
521 .then(a.1.y.total_cmp(&b.1.y))
522 .then(a.0.cmp(&b.0))
523 })
524 .map(|x| x.0)
525 .unwrap_or(0);
526 r.points.rotate_left(k);
527 r
528}
529pub fn overlay(
537 subject: &OverlayInput,
538 clip: &OverlayInput,
539 operation: OverlayOperation,
540 fill: FillRule,
541 tolerance: Tolerance,
542) -> Result<OverlayResult, OverlayError> {
543 overlay_with(
544 subject,
545 clip,
546 operation,
547 fill,
548 tolerance,
549 OverlayOptions::new(),
550 )
551 .map(|reported| reported.result)
552}
553
554pub fn overlay_with(
567 subject: &OverlayInput,
568 clip: &OverlayInput,
569 operation: OverlayOperation,
570 fill: FillRule,
571 tolerance: Tolerance,
572 options: OverlayOptions,
573) -> Result<ReportedOverlay, OverlayError> {
574 overlay_charged(
575 subject,
576 clip,
577 operation,
578 fill,
579 tolerance,
580 options,
581 &mut Charge::unlimited(),
582 )
583 .map_err(budget::unbudgeted)
584}
585
586pub fn overlay_within(
598 subject: &OverlayInput,
599 clip: &OverlayInput,
600 operation: OverlayOperation,
601 fill: FillRule,
602 options: OverlayOptions,
603 execution: &ExecutionOptions,
604) -> Result<ReportedOverlay, OverlayRefusal> {
605 overlay_charged(
606 subject,
607 clip,
608 operation,
609 fill,
610 execution.tolerance(),
611 options,
612 &mut Charge::within(execution.memory_budget_bytes()),
613 )
614}
615
616fn overlay_charged(
618 subject: &OverlayInput,
619 clip: &OverlayInput,
620 operation: OverlayOperation,
621 fill: FillRule,
622 tolerance: Tolerance,
623 options: OverlayOptions,
624 charge: &mut Charge,
625) -> Result<ReportedOverlay, OverlayRefusal> {
626 let known = [false, false];
627 overlay_known(
628 subject, clip, operation, fill, tolerance, options, charge, known,
629 )
630 .map(|(reported, _)| reported)
631}
632
633#[allow(clippy::too_many_arguments)]
642pub(crate) fn overlay_known(
643 subject: &OverlayInput,
644 clip: &OverlayInput,
645 operation: OverlayOperation,
646 fill: FillRule,
647 tolerance: Tolerance,
648 options: OverlayOptions,
649 charge: &mut Charge,
650 known: [bool; 2],
651) -> Result<(ReportedOverlay, Option<settle::Validated>), OverlayRefusal> {
652 let vertices = |input: &OverlayInput| -> usize {
653 input
654 .polygons
655 .iter()
656 .flat_map(|p| std::iter::once(&p.outer).chain(&p.holes))
657 .map(|ring| ring.points.len())
658 .sum()
659 };
660 charge.add(vertices(subject) + vertices(clip), budget::PER_VERTEX)?;
661 let mut dropped = Vec::new();
662 let subject_polygons = admit_known(
663 subject,
664 OverlayOperand::Subject,
665 tolerance,
666 options,
667 known[0],
668 &mut dropped,
669 )?;
670 let clip_polygons = admit_known(
671 clip,
672 OverlayOperand::Clip,
673 tolerance,
674 options,
675 known[1],
676 &mut dropped,
677 )?;
678 if subject.frame != clip.frame {
679 return Err(OverlayError::InvalidFrame.into());
680 };
681 let rings = exact_overlay::boolean(
682 &subject_polygons,
683 &clip_polygons,
684 operation,
685 fill,
686 tolerance.linear(),
687 charge,
688 )?;
689 let (polygons, validated) = settle::settle_validated(canonical_polygons(rings), tolerance);
690 let evidence = OverlayEvidence {
691 subject_rings: subject_polygons.iter().map(|p| 1 + p.holes.len()).sum(),
692 clip_rings: clip_polygons.iter().map(|p| 1 + p.holes.len()).sum(),
693 output_polygons: polygons.len(),
694 output_holes: polygons.iter().map(|polygon| polygon.holes.len()).sum(),
695 };
696 Ok((
697 ReportedOverlay {
698 result: OverlayResult { polygons, evidence },
699 dropped,
700 },
701 validated,
702 ))
703}
704
705pub fn union_soup(rings: &[Ring], tolerance: Tolerance) -> Result<Vec<Polygon>, OverlayError> {
713 for ring in rings {
714 validate_ring(ring, tolerance)?;
715 }
716 union_valid(rings, tolerance, &mut Charge::unlimited()).map_err(budget::unbudgeted)
717}
718
719pub fn union_soup_with(
730 rings: &[Ring],
731 tolerance: Tolerance,
732 options: OverlayOptions,
733) -> Result<ReportedUnion, OverlayError> {
734 union_soup_charged(rings, tolerance, options, &mut Charge::unlimited())
735 .map_err(budget::unbudgeted)
736}
737
738pub fn union_soup_within(
744 rings: &[Ring],
745 options: OverlayOptions,
746 execution: &ExecutionOptions,
747) -> Result<ReportedUnion, OverlayRefusal> {
748 union_soup_charged(
749 rings,
750 execution.tolerance(),
751 options,
752 &mut Charge::within(execution.memory_budget_bytes()),
753 )
754}
755
756fn union_soup_charged(
758 rings: &[Ring],
759 tolerance: Tolerance,
760 options: OverlayOptions,
761 charge: &mut Charge,
762) -> Result<ReportedUnion, OverlayRefusal> {
763 charge.add(
764 rings.iter().map(|ring| ring.points.len()).sum(),
765 budget::PER_VERTEX,
766 )?;
767 let mut dropped = Vec::new();
768 let mut kept = Vec::with_capacity(rings.len());
769 for (index, ring) in rings.iter().enumerate() {
770 if options.degenerate_rings == DegenerateRings::Refuse {
771 validate_ring(ring, tolerance)?;
772 kept.push(ring.clone());
773 continue;
774 }
775 match degenerate::checked(ring, tolerance)? {
776 None => kept.push(ring.clone()),
777 Some(reason) => {
778 let place = degenerate::Place {
779 operand: OverlayOperand::Subject,
780 polygon: index,
781 ring: RingRole::Outer,
782 };
783 dropped.push(degenerate::report(ring, place, reason));
784 }
785 }
786 }
787 Ok(ReportedUnion {
788 polygons: union_valid(&kept, tolerance, charge)?,
789 dropped,
790 })
791}
792
793fn union_valid(
795 rings: &[Ring],
796 tolerance: Tolerance,
797 charge: &mut Charge,
798) -> Result<Vec<Polygon>, OverlayRefusal> {
799 if rings.is_empty() {
800 return Ok(Vec::new());
801 }
802 let subject: Vec<Polygon> = rings
806 .iter()
807 .map(|ring| Polygon {
808 outer: ring.clone(),
809 holes: Vec::new(),
810 })
811 .collect();
812 let rings = exact_overlay::boolean(
813 &subject,
814 &[],
815 OverlayOperation::Union,
816 FillRule::NonZero,
817 tolerance.linear(),
818 charge,
819 )?;
820 Ok(settle::settle(canonical_polygons(rings), tolerance))
823}
824
825fn canonical_polygons(rings: Vec<(Ring, Vec<Ring>)>) -> Vec<Polygon> {
830 let mut polygons: Vec<Polygon> = rings
831 .into_iter()
832 .map(|(outer, holes)| Polygon {
833 outer: canonical(outer, true),
834 holes: holes.into_iter().map(|h| canonical(h, false)).collect(),
835 })
836 .collect();
837 polygons.sort_by(|a, b| {
838 a.outer.points[0]
839 .x
840 .total_cmp(&b.outer.points[0].x)
841 .then(a.outer.points[0].y.total_cmp(&b.outer.points[0].y))
842 });
843 polygons
844}
845
846#[cfg(test)]
847mod tests {
848 use super::*;
851 use std::cell::Cell;
852
853 thread_local! {
854 pub(super) static VALIDATED: Cell<usize> = const { Cell::new(0) };
856 }
857
858 fn validated() -> usize {
859 VALIDATED.with(Cell::get)
860 }
861
862 fn all_pairs(points: &[Point2], linear: f64) -> Option<(usize, usize)> {
864 let n = points.len();
865 for i in 0..n {
866 for j in i + 1..n {
867 if j == i + 1 || (i == 0 && j + 1 == n) {
868 continue;
869 }
870 let (a, b) = (points[i], points[(i + 1) % n]);
871 let (c, d) = (points[j], points[(j + 1) % n]);
872 if segments_intersect(a, b, c, d, linear) {
873 return Some((i, j));
874 }
875 }
876 }
877 None
878 }
879
880 struct Lcg(u64);
882
883 impl Lcg {
884 fn next(&mut self) -> f64 {
885 self.0 = self
886 .0
887 .wrapping_mul(6_364_136_223_846_793_005)
888 .wrapping_add(1_442_695_040_888_963_407);
889 (self.0 >> 11) as f64 / (1u64 << 53) as f64
890 }
891 }
892
893 fn star(rng: &mut Lcg, n: usize, faults: usize) -> Vec<Point2> {
898 let mut points: Vec<Point2> = (0..n)
899 .map(|i| {
900 let angle = std::f64::consts::TAU * (i as f64 + 0.5 * rng.next()) / n as f64;
901 let radius = 1.0 + 0.2 * rng.next();
902 Point2::new(radius * angle.cos(), radius * angle.sin())
903 })
904 .collect();
905 for _ in 0..faults {
906 let k = (rng.next() * n as f64) as usize % n;
907 points[k] = match (rng.next() * 3.0) as usize {
908 0 => Point2::new(-0.9 * points[k].x, -0.9 * points[k].y),
909 1 => points[(k + 2) % n],
910 _ => {
911 let j = (k + n / 2) % n;
912 let (a, b) = (points[j], points[(j + 1) % n]);
913 Point2::new(0.5 * (a.x + b.x) + 1e-10, 0.5 * (a.y + b.y))
914 }
915 };
916 }
917 points
918 }
919
920 #[test]
924 fn the_first_touching_pair_is_the_all_pairs_one() {
925 let mut rng = Lcg(341);
926 let (mut found, mut clean) = (0, 0);
927 for case in 0..400 {
928 let n = [5, 33, 40, 64, 200][case % 5];
929 let faults = case / 5 % 4;
930 let points = star(&mut rng, n, faults);
931 for linear in [1e-9, 1e-3, 0.05] {
932 let pair = first_touching_pair(&points, linear);
933 assert_eq!(pair, all_pairs(&points, linear), "case {case} at {linear}");
934 match pair {
935 Some(_) => found += 1,
936 None => clean += 1,
937 }
938 }
939 }
940 assert!(found > 200 && clean > 200, "{found} found, {clean} clean");
941 }
942
943 #[test]
946 fn a_long_ring_touching_itself_far_along_is_found() {
947 let mut points = Vec::new();
948 for k in 0..2_000 {
949 let x = k as f64;
950 points.push(Point2::new(x, 0.0));
951 points.push(Point2::new(x + 0.5, 1.0));
952 }
953 points.push(Point2::new(2_000.0, 0.0));
954 points.push(Point2::new(2_000.0, -1.0));
955 points.push(Point2::new(0.0, -1.0));
956 let t = Tolerance::new(1e-9, 1e-9).expect("a tolerance");
957 let ring = Ring {
958 points: points.clone(),
959 };
960 assert_eq!(validate_ring(&ring, t), Ok(()));
961 assert_eq!(first_touching_pair(&points, t.linear()), None);
962 let n = points.len();
963 points[n - 1] = Point2::new(0.25, 0.5 - 1e-10);
964 let pair = first_touching_pair(&points, t.linear());
965 assert!(pair.is_some());
966 assert_eq!(pair, all_pairs(&points, t.linear()));
967 assert_eq!(
968 validate_ring(&Ring { points }, t),
969 Err(OverlayError::SelfIntersection)
970 );
971 }
972
973 fn square(x: f64, y: f64, side: f64) -> Polygon {
974 Polygon {
975 outer: Ring {
976 points: vec![
977 Point2::new(x, y),
978 Point2::new(x + side, y),
979 Point2::new(x + side, y + side),
980 Point2::new(x, y + side),
981 ],
982 },
983 holes: Vec::new(),
984 }
985 }
986
987 #[test]
992 fn regions_are_validated_once() {
993 let t = Tolerance::new(1e-9, 1e-9).expect("a tolerance");
994 let a = Region::new(vec![square(0.0, 0.0, 2.0)], t).expect("valid");
995 let b = Region::new(vec![square(1.0, 1.0, 2.0)], t).expect("valid");
996 let before = validated();
997 let union = a.union(&b, t).expect("the union");
998 let cut = union.difference(&a, t).expect("the difference");
999 let again = cut.intersection(&b, t).expect("the intersection");
1000 assert_eq!(validated(), before, "no ring validated again");
1001 assert!(!again.is_empty());
1002 let input = |region: &Region| OverlayInput {
1003 frame: Frame2 {
1004 origin: Point2::ZERO,
1005 x: Point2::X,
1006 y: Point2::Y,
1007 },
1008 polygons: region.polygons().to_vec(),
1009 };
1010 let raw = overlay(
1011 &input(&a),
1012 &input(&b),
1013 OverlayOperation::Union,
1014 FillRule::NonZero,
1015 t,
1016 )
1017 .expect("the union");
1018 assert_eq!(raw.polygons, union.polygons());
1019 assert!(validated() > before, "overlay validates every ring");
1020
1021 let mut short = square(5.0, 5.0, 1.0);
1023 short.outer.points.insert(1, Point2::new(5.5, 5.0));
1024 short.outer.points.insert(2, Point2::new(5.5 + 1e-6, 5.0));
1025 let fine = Region::new(vec![short], t).expect("valid at 1e-9");
1026 let coarse = Tolerance::new(1e-3, 1e-3).expect("a tolerance");
1027 assert_eq!(fine.union(&a, coarse), Err(OverlayError::RepeatedVertex));
1028 let moved = a
1030 .translate(axiolid_core::Vec2::new(0.5, 0.5))
1031 .expect("moved");
1032 let before = validated();
1033 moved.union(&b, t).expect("the union");
1034 assert!(validated() > before, "a translate is validated again");
1035
1036 let eroded = union.erode_inner(0.1, t).expect("the erosion");
1038 let before = validated();
1039 eroded.difference(&b, t).expect("the difference");
1040 assert_eq!(validated(), before, "an erosion is not validated again");
1041
1042 let mut stray = square(10.0, 10.0, 1.0);
1045 let mut hole = square(20.0, 20.0, 0.5).outer;
1046 hole.points.reverse();
1047 stray.holes.push(hole);
1048 assert_eq!(
1049 Region::new(vec![stray.clone()], t),
1050 Err(OverlayError::HoleOutsideOuter)
1051 );
1052 let stray = Region::from_valid_polygons(vec![stray.clone()], false).with_validated(Some(
1053 crate::settle::Validated::check(&[stray], t).expect("each ring valid"),
1054 ));
1055 assert_eq!(stray.union(&a, t), Err(OverlayError::HoleOutsideOuter));
1056 }
1057
1058 #[test]
1059 fn overlay_error_displays_and_boxes() {
1060 let all = [
1061 OverlayError::InvalidFrame,
1062 OverlayError::NonFinitePoint,
1063 OverlayError::RingTooShort,
1064 OverlayError::RepeatedVertex,
1065 OverlayError::ZeroArea,
1066 OverlayError::SelfIntersection,
1067 OverlayError::HoleOutsideOuter,
1068 OverlayError::HolesOverlap,
1069 OverlayError::PolygonsOverlap,
1070 OverlayError::InvalidOffsetDistance,
1071 OverlayError::InvalidOffsetStyle,
1072 OverlayError::ZeroRadiusArc,
1073 ];
1074 let mut seen = std::collections::BTreeSet::new();
1075 for error in all {
1076 let text = error.to_string();
1077 assert!(!text.is_empty());
1078 assert!(seen.insert(text), "{error:?} has a distinct message");
1079 let boxed: Box<dyn std::error::Error> = Box::new(error);
1080 assert!(!boxed.to_string().is_empty());
1081 }
1082 }
1083
1084 #[test]
1087 fn overlay_refusal_wraps_and_exposes_its_error() {
1088 use crate::OverlayRefusal;
1089 use std::error::Error;
1090 let refusal = OverlayRefusal::from(OverlayError::ZeroArea);
1091 assert_eq!(
1092 refusal.to_string(),
1093 format!("invalid overlay operand: {}", OverlayError::ZeroArea)
1094 );
1095 assert_eq!(
1096 refusal.source().map(ToString::to_string),
1097 Some(OverlayError::ZeroArea.to_string())
1098 );
1099 let budget = OverlayRefusal::BudgetExceeded {
1100 needed: 2,
1101 budget: 1,
1102 };
1103 assert!(budget.source().is_none());
1104 }
1105}