1use crate::model::Point2;
21use crate::numeric::Tolerance;
22use serde::{Deserialize, Serialize};
23use std::f64::consts::{FRAC_PI_2, TAU};
24
25pub mod arrange;
26pub mod extract;
27pub mod machining;
28pub mod ops;
29
30pub use ops::{boolean, chain_capsule, clip_line, offset_chain, offset_region, BoolOp, Side};
31
32#[derive(Clone, Debug, PartialEq)]
34pub enum PlanarError {
35 NonFinite { what: String },
37 Degenerate(String),
39 OpenBoundary { at: Point2 },
41 InvalidInput(String),
43 Inconsistent(String),
46}
47
48impl std::fmt::Display for PlanarError {
49 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
50 match self {
51 PlanarError::NonFinite { what } => write!(f, "planar: {what} is not finite"),
52 PlanarError::Degenerate(m) => write!(f, "planar: degenerate input: {m}"),
53 PlanarError::OpenBoundary { at } => {
54 write!(f, "planar: the result boundary does not close at ({}, {})", at.x, at.y)
55 }
56 PlanarError::InvalidInput(m) => write!(f, "planar: {m}"),
57 PlanarError::Inconsistent(m) => write!(f, "planar: inconsistent result: {m}"),
58 }
59 }
60}
61
62impl std::error::Error for PlanarError {}
63
64impl From<PlanarError> for crate::CamError {
65 fn from(e: PlanarError) -> Self {
66 crate::CamError::InvalidInput(e.to_string())
67 }
68}
69
70pub(crate) trait Vec2 {
75 fn add(self, o: Point2) -> Point2;
76 fn sub(self, o: Point2) -> Point2;
77 fn scale(self, s: f64) -> Point2;
78 fn dot(self, o: Point2) -> f64;
79 fn cross(self, o: Point2) -> f64;
80 fn length(self) -> f64;
81 fn distance(self, o: Point2) -> f64;
82 fn perp(self) -> Point2;
84 fn normalized(self) -> Option<Point2>;
85 fn angle(self) -> f64;
86}
87
88impl Vec2 for Point2 {
89 fn add(self, o: Point2) -> Point2 {
90 Point2::new(self.x + o.x, self.y + o.y)
91 }
92 fn sub(self, o: Point2) -> Point2 {
93 Point2::new(self.x - o.x, self.y - o.y)
94 }
95 fn scale(self, s: f64) -> Point2 {
96 Point2::new(self.x * s, self.y * s)
97 }
98 fn dot(self, o: Point2) -> f64 {
99 self.x * o.x + self.y * o.y
100 }
101 fn cross(self, o: Point2) -> f64 {
102 self.x * o.y - self.y * o.x
103 }
104 fn length(self) -> f64 {
105 self.x.hypot(self.y)
106 }
107 fn distance(self, o: Point2) -> f64 {
108 self.sub(o).length()
109 }
110 fn perp(self) -> Point2 {
111 Point2::new(-self.y, self.x)
112 }
113 fn normalized(self) -> Option<Point2> {
114 let l = self.length();
115 if l > 0.0 && l.is_finite() {
116 Some(self.scale(1.0 / l))
117 } else {
118 None
119 }
120 }
121 fn angle(self) -> f64 {
122 self.y.atan2(self.x)
123 }
124}
125
126pub(crate) fn dir(a: f64) -> Point2 {
127 Point2::new(a.cos(), a.sin())
128}
129
130#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
136#[serde(rename_all = "camelCase")]
137pub struct Arc {
138 pub center: Point2,
139 pub ccw: bool,
141}
142
143#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
146#[serde(rename_all = "camelCase")]
147pub struct Segment {
148 pub start: Point2,
149 pub end: Point2,
150 #[serde(default, skip_serializing_if = "Option::is_none")]
151 pub arc: Option<Arc>,
152}
153
154#[derive(Clone, Copy, Debug, PartialEq)]
156pub struct Hit {
157 pub point: Point2,
158 pub ta: f64,
160 pub tb: f64,
162}
163
164impl Segment {
165 pub fn line(start: Point2, end: Point2) -> Self {
166 Self { start, end, arc: None }
167 }
168
169 pub fn arc(start: Point2, end: Point2, center: Point2, ccw: bool) -> Self {
170 Self { start, end, arc: Some(Arc { center, ccw }) }
171 }
172
173 pub fn is_arc(&self) -> bool {
174 self.arc.is_some()
175 }
176
177 pub fn reversed(&self) -> Self {
178 Self { start: self.end, end: self.start, arc: self.arc.map(|a| Arc { center: a.center, ccw: !a.ccw }) }
179 }
180
181 pub fn radius(&self) -> f64 {
183 self.arc.map_or(0.0, |a| self.start.distance(a.center))
184 }
185
186 pub fn angles(&self) -> (f64, f64) {
189 match self.arc {
190 None => (0.0, 0.0),
191 Some(a) => {
192 let a0 = self.start.sub(a.center).angle();
193 let a1 = self.end.sub(a.center).angle();
194 let ccw = (a1 - a0).rem_euclid(TAU);
195 if a.ccw {
196 (a0, ccw)
197 } else {
198 (a0, ccw - TAU)
199 }
200 }
201 }
202 }
203
204 pub fn length(&self) -> f64 {
206 match self.arc {
207 None => self.start.distance(self.end),
208 Some(_) => self.radius() * self.angles().1.abs(),
209 }
210 }
211
212 pub fn point_at(&self, t: f64) -> Point2 {
213 let t = t.clamp(0.0, 1.0);
214 if t == 0.0 {
215 return self.start;
216 }
217 if t == 1.0 {
218 return self.end;
219 }
220 match self.arc {
221 None => self.start.add(self.end.sub(self.start).scale(t)),
222 Some(a) => {
223 let (a0, sweep) = self.angles();
224 a.center.add(dir(a0 + sweep * t).scale(self.radius()))
225 }
226 }
227 }
228
229 pub fn tangent_at(&self, t: f64) -> Point2 {
231 match self.arc {
232 None => self.end.sub(self.start).normalized().unwrap_or(Point2::new(1.0, 0.0)),
233 Some(a) => {
234 let (a0, sweep) = self.angles();
235 let radial = dir(a0 + sweep * t.clamp(0.0, 1.0));
236 if a.ccw {
237 radial.perp()
238 } else {
239 radial.perp().scale(-1.0)
240 }
241 }
242 }
243 }
244
245 pub fn midpoint(&self) -> Point2 {
246 self.point_at(0.5)
247 }
248
249 pub fn bounds(&self) -> (Point2, Point2) {
251 let mut lo = Point2::new(self.start.x.min(self.end.x), self.start.y.min(self.end.y));
252 let mut hi = Point2::new(self.start.x.max(self.end.x), self.start.y.max(self.end.y));
253 if let Some(a) = self.arc {
254 let r = self.radius();
255 let (a0, sweep) = self.angles();
256 let (lo_a, hi_a) = if sweep >= 0.0 { (a0, a0 + sweep) } else { (a0 + sweep, a0) };
257 let k0 = (lo_a / FRAC_PI_2).floor() as i64;
259 for k in k0..=k0 + 5 {
260 let ang = k as f64 * FRAC_PI_2;
261 if ang > lo_a && ang < hi_a {
262 let p = a.center.add(dir(ang).scale(r));
263 lo = Point2::new(lo.x.min(p.x), lo.y.min(p.y));
264 hi = Point2::new(hi.x.max(p.x), hi.y.max(p.y));
265 }
266 }
267 }
268 (lo, hi)
269 }
270
271 pub fn sub_segment(&self, start: Point2, end: Point2) -> Segment {
274 Segment { start, end, arc: self.arc }
275 }
276
277 pub fn nearest(&self, p: Point2) -> (Point2, f64) {
280 match self.arc {
281 None => {
282 let d = self.end.sub(self.start);
283 let l2 = d.dot(d);
284 let t = if l2 > 0.0 { (p.sub(self.start).dot(d) / l2).clamp(0.0, 1.0) } else { 0.0 };
285 (self.point_at(t), t)
286 }
287 Some(a) => {
288 let (a0, sweep) = self.angles();
289 let v = p.sub(a.center);
290 if v.length() == 0.0 {
291 return (self.start, 0.0);
292 }
293 let ang = v.angle();
294 let rel = if sweep >= 0.0 { (ang - a0).rem_euclid(TAU) } else { -((a0 - ang).rem_euclid(TAU)) };
296 let t = rel / sweep;
297 if t.is_finite() && (0.0..=1.0).contains(&t) {
298 (a.center.add(dir(ang).scale(self.radius())), t)
299 } else if p.distance(self.start) <= p.distance(self.end) {
300 (self.start, 0.0)
301 } else {
302 (self.end, 1.0)
303 }
304 }
305 }
306 }
307
308 pub fn distance(&self, p: Point2) -> f64 {
309 self.nearest(p).0.distance(p)
310 }
311
312 pub fn bulge_area(&self) -> f64 {
315 match self.arc {
316 None => 0.0,
317 Some(_) => {
318 let (_, s) = self.angles();
319 let r = self.radius();
320 0.5 * r * r * (s - s.sin())
321 }
322 }
323 }
324
325 pub fn discretize(&self, tol: f64) -> Vec<Point2> {
327 match self.arc {
328 None => vec![self.start, self.end],
329 Some(_) => {
330 let r = self.radius();
331 let (_, sweep) = self.angles();
332 let max_step = if tol >= r || tol <= 0.0 { FRAC_PI_2 } else { 2.0 * (1.0 - tol / r).acos() };
333 let n = ((sweep.abs() / max_step.max(1e-9)).ceil() as usize).clamp(1, 100_000);
334 let mut pts = Vec::with_capacity(n + 1);
335 pts.push(self.start);
336 for i in 1..n {
337 pts.push(self.point_at(i as f64 / n as f64));
338 }
339 pts.push(self.end);
340 pts
341 }
342 }
343 }
344
345 pub fn discretize_outward(&self, tol: f64) -> Vec<Point2> {
353 let Some(a) = self.arc else { return vec![self.start, self.end] };
354 let r = self.radius();
355 let (a0, sweep) = self.angles();
356 let max_step = if tol >= r || tol <= 0.0 { FRAC_PI_2 } else { 2.0 * (r / (r + tol)).acos() };
358 let n = ((sweep.abs() / max_step.max(1e-9)).ceil() as usize).clamp(1, 100_000);
359 let step = sweep / n as f64;
360 let outer = r / (0.5 * step).cos();
361 let mut pts = Vec::with_capacity(n + 2);
362 pts.push(self.start);
363 for i in 0..n {
364 pts.push(a.center.add(dir(a0 + step * (i as f64 + 0.5)).scale(outer)));
365 }
366 pts.push(self.end);
367 pts
368 }
369
370 pub fn validate(&self, tol: &Tolerance) -> Result<(), PlanarError> {
371 if !self.start.is_finite() || !self.end.is_finite() {
372 return Err(PlanarError::NonFinite { what: "segment endpoint".into() });
373 }
374 if self.start.distance(self.end) <= tol.point_eps {
375 return Err(PlanarError::Degenerate(format!("segment of zero length at ({}, {})", self.start.x, self.start.y)));
376 }
377 if let Some(a) = self.arc {
378 if !a.center.is_finite() {
379 return Err(PlanarError::NonFinite { what: "arc centre".into() });
380 }
381 let r0 = self.start.distance(a.center);
382 let r1 = self.end.distance(a.center);
383 if r0 <= tol.sample_eps {
384 return Err(PlanarError::Degenerate("arc radius is below the sample epsilon".into()));
385 }
386 if (r0 - r1).abs() > (2.0 * tol.point_eps).max(tol.distance_eps).max(1e-9 * r0) {
391 return Err(PlanarError::Degenerate(format!("arc endpoints have radii {r0} and {r1}")));
392 }
393 }
394 Ok(())
395 }
396
397 pub fn canonical(&self, tol: &Tolerance) -> Vec<Segment> {
400 let Some(a) = self.arc else { return vec![*self] };
401 let r = self.radius();
402 let (a0, sweep) = self.angles();
403 let (lo, hi) = if sweep >= 0.0 { (a0, a0 + sweep) } else { (a0 + sweep, a0) };
404 let mut cuts: Vec<f64> = Vec::new();
405 let k0 = (lo / FRAC_PI_2).floor() as i64;
406 for k in k0..=k0 + 5 {
407 let ang = k as f64 * FRAC_PI_2;
408 if ang - lo > tol.point_eps / r && hi - ang > tol.point_eps / r {
410 cuts.push(ang);
411 }
412 }
413 if cuts.is_empty() {
414 return vec![*self];
415 }
416 if sweep < 0.0 {
417 cuts.reverse();
418 }
419 let mut out = Vec::with_capacity(cuts.len() + 1);
420 let mut from = self.start;
421 for c in cuts {
422 let p = a.center.add(dir(c).scale(r));
423 out.push(Segment::arc(from, p, a.center, a.ccw));
424 from = p;
425 }
426 out.push(Segment::arc(from, self.end, a.center, a.ccw));
427 out
428 }
429
430 pub fn intersect(&self, other: &Segment, tol: &Tolerance) -> Vec<Hit> {
434 let eps = tol.distance_eps;
435 let mut hits = Vec::new();
436 let (alo, ahi) = self.bounds();
438 let (blo, bhi) = other.bounds();
439 if alo.x > bhi.x + eps || blo.x > ahi.x + eps || alo.y > bhi.y + eps || blo.y > ahi.y + eps {
440 return hits;
441 }
442 match (self.arc, other.arc) {
443 (None, None) => {
444 let d1 = self.end.sub(self.start);
445 let d2 = other.end.sub(other.start);
446 let denom = d1.cross(d2);
447 let w = other.start.sub(self.start);
448 let l1 = d1.length();
449 let l2 = d2.length();
450 if denom.abs() <= tol.angle_eps * l1 * l2 {
451 if w.cross(d1).abs() <= eps * l1 {
453 overlap_hits(self, other, &mut hits);
454 }
455 return hits;
456 }
457 let ta = w.cross(d2) / denom;
458 let tb = w.cross(d1) / denom;
459 let slack_a = eps / l1;
460 let slack_b = eps / l2;
461 if (-slack_a..=1.0 + slack_a).contains(&ta) && (-slack_b..=1.0 + slack_b).contains(&tb) {
462 let p = self.start.add(d1.scale(ta.clamp(0.0, 1.0)));
463 hits.push(Hit { point: p, ta: ta.clamp(0.0, 1.0), tb: tb.clamp(0.0, 1.0) });
464 }
465 }
466 (None, Some(_)) => {
467 for h in other.intersect(self, tol) {
468 hits.push(Hit { point: h.point, ta: h.tb, tb: h.ta });
469 }
470 }
471 (Some(a), None) => {
472 let r = self.radius();
477 self.incidences(other, tol, &mut hits);
478 let shadow = 2.0 * (2.0 * r * eps).sqrt();
479 let d = other.end.sub(other.start);
481 let f = other.start.sub(a.center);
482 let qa = d.dot(d);
483 let qb = 2.0 * f.dot(d);
484 let qc = f.dot(f) - r * r;
485 let disc = qb * qb - 4.0 * qa * qc;
486 let l = d.length();
487 let tangent_slack = 4.0 * qa * (2.0 * r * eps);
489 if disc < -tangent_slack {
490 return hits;
491 }
492 let roots: Vec<f64> = if disc <= tangent_slack {
493 vec![-qb / (2.0 * qa)]
494 } else {
495 let s = disc.sqrt();
496 vec![(-qb - s) / (2.0 * qa), (-qb + s) / (2.0 * qa)]
497 };
498 let known: Vec<Point2> = hits.iter().map(|h| h.point).collect();
499 for tb in roots {
500 let slack = eps / l;
501 if !(-slack..=1.0 + slack).contains(&tb) {
502 continue;
503 }
504 let p = other.start.add(d.scale(tb.clamp(0.0, 1.0)));
505 if known.iter().any(|k| k.distance(p) <= shadow) {
506 continue;
507 }
508 if let Some(ta) = self.arc_param(p, tol) {
509 hits.push(Hit { point: p, ta, tb: tb.clamp(0.0, 1.0) });
510 }
511 }
512 }
513 (Some(a), Some(b)) => {
514 let r0 = self.radius();
515 let r1 = other.radius();
516 let cd = b.center.sub(a.center);
517 let dist = cd.length();
518 if dist <= tol.point_eps && (r0 - r1).abs() <= eps {
519 overlap_hits(self, other, &mut hits);
520 return hits;
521 }
522 if dist <= tol.point_eps {
523 return hits; }
525 if dist > r0 + r1 + eps || dist < (r0 - r1).abs() - eps {
526 return hits;
527 }
528 self.incidences(other, tol, &mut hits);
529 let shadow = 2.0 * (2.0 * r0.max(r1) * eps).sqrt();
530 let known: Vec<Point2> = hits.iter().map(|h| h.point).collect();
531 let x = (dist * dist + r0 * r0 - r1 * r1) / (2.0 * dist);
532 let h2 = r0 * r0 - x * x;
533 let u = cd.scale(1.0 / dist);
534 let base = a.center.add(u.scale(x));
535 let gap = (r0 + r1 - dist).min(dist - (r0 - r1).abs());
545 let candidates: Vec<Point2> = if gap <= eps {
546 vec![base]
547 } else {
548 let h = h2.max(0.0).sqrt();
549 vec![base.sub(u.perp().scale(h)), base.add(u.perp().scale(h))]
550 };
551 for p in candidates {
552 if known.iter().any(|k| k.distance(p) <= shadow) {
553 continue;
554 }
555 if let (Some(ta), Some(tb)) = (self.arc_param(p, tol), other.arc_param(p, tol)) {
556 hits.push(Hit { point: p, ta, tb });
557 }
558 }
559 }
560 }
561 hits
562 }
563
564 fn incidences(&self, other: &Segment, tol: &Tolerance, hits: &mut Vec<Hit>) {
568 let eps = tol.distance_eps;
569 for (tb, p) in [(0.0, other.start), (1.0, other.end)] {
570 let (q, ta) = self.nearest(p);
571 if q.distance(p) <= eps && !hits.iter().any(|h| h.point.distance(p) <= tol.point_eps) {
572 hits.push(Hit { point: p, ta, tb });
573 }
574 }
575 for (ta, p) in [(0.0, self.start), (1.0, self.end)] {
576 let (q, tb) = other.nearest(p);
577 if q.distance(p) <= eps && !hits.iter().any(|h| h.point.distance(p) <= tol.point_eps) {
578 hits.push(Hit { point: p, ta, tb });
579 }
580 }
581 }
582
583 fn arc_param(&self, p: Point2, tol: &Tolerance) -> Option<f64> {
586 let a = self.arc?;
587 let (a0, sweep) = self.angles();
588 let r = self.radius();
589 let ang = p.sub(a.center).angle();
590 let rel = if sweep >= 0.0 { (ang - a0).rem_euclid(TAU) } else { -((a0 - ang).rem_euclid(TAU)) };
591 let slack = tol.distance_eps / r;
592 let t = rel / sweep;
593 if (0.0..=1.0).contains(&t) {
594 return Some(t);
595 }
596 if (rel.abs() - TAU).abs() <= slack || rel.abs() <= slack {
598 return Some(0.0);
599 }
600 if (rel.abs() - sweep.abs()).abs() <= slack {
601 return Some(1.0);
602 }
603 None
604 }
605}
606
607fn overlap_hits(a: &Segment, b: &Segment, hits: &mut Vec<Hit>) {
609 for (tb, p) in [(0.0, b.start), (1.0, b.end)] {
610 let (q, ta) = a.nearest(p);
611 if q.distance(p) <= 1e-9 * (1.0 + a.length()) {
612 hits.push(Hit { point: p, ta, tb });
613 }
614 }
615 for (ta, p) in [(0.0, a.start), (1.0, a.end)] {
616 let (q, tb) = b.nearest(p);
617 if q.distance(p) <= 1e-9 * (1.0 + b.length()) {
618 hits.push(Hit { point: p, ta, tb });
619 }
620 }
621}
622
623#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
629#[serde(rename_all = "camelCase")]
630pub struct Loop {
631 pub segments: Vec<Segment>,
632}
633
634impl Loop {
635 pub fn new(segments: Vec<Segment>) -> Self {
636 Self { segments }
637 }
638
639 pub fn polygon(points: &[Point2]) -> Self {
641 let n = points.len();
642 Self::new((0..n).map(|i| Segment::line(points[i], points[(i + 1) % n])).collect())
643 }
644
645 pub fn rectangle(min: Point2, max: Point2) -> Self {
647 Self::polygon(&[min, Point2::new(max.x, min.y), max, Point2::new(min.x, max.y)])
648 }
649
650 pub fn circle(center: Point2, radius: f64, ccw: bool) -> Self {
652 let p = |k: usize| center.add(dir(k as f64 * FRAC_PI_2).scale(radius));
653 let mut segs: Vec<Segment> = (0..4).map(|k| Segment::arc(p(k), p(k + 1), center, true)).collect();
654 if !ccw {
655 segs.reverse();
656 for s in &mut segs {
657 *s = s.reversed();
658 }
659 }
660 Self::new(segs)
661 }
662
663 pub fn stadium(a: Point2, b: Point2, radius: f64) -> Self {
666 let u = b.sub(a).normalized().unwrap_or(Point2::new(1.0, 0.0));
667 let n = u.perp().scale(radius);
668 let mut segs = vec![Segment::line(a.sub(n), b.sub(n))];
669 segs.extend(Segment::arc(b.sub(n), b.add(n), b, true).canonical(&Tolerance::default()));
670 segs.push(Segment::line(b.add(n), a.add(n)));
671 segs.extend(Segment::arc(a.add(n), a.sub(n), a, true).canonical(&Tolerance::default()));
672 Self::new(segs)
673 }
674
675 pub fn reversed(&self) -> Self {
676 Self::new(self.segments.iter().rev().map(Segment::reversed).collect())
677 }
678
679 pub fn signed_area(&self) -> f64 {
681 let mut a = 0.0;
682 for s in &self.segments {
683 a += 0.5 * s.start.cross(s.end) + s.bulge_area();
684 }
685 a
686 }
687
688 pub fn is_ccw(&self) -> bool {
689 self.signed_area() > 0.0
690 }
691
692 pub fn length(&self) -> f64 {
693 self.segments.iter().map(Segment::length).sum()
694 }
695
696 pub fn bounds(&self) -> (Point2, Point2) {
697 bounds_of(self.segments.iter())
698 }
699
700 pub fn discretize(&self, tol: f64) -> Vec<Point2> {
702 let mut out = Vec::new();
703 for s in &self.segments {
704 let pts = s.discretize(tol);
705 out.extend_from_slice(&pts[..pts.len() - 1]);
706 }
707 out
708 }
709
710 pub fn winding(&self, p: Point2, tol: &Tolerance) -> i32 {
712 let canon: Vec<Segment> = self.segments.iter().flat_map(|s| s.canonical(tol)).collect();
713 crossings(canon.iter(), p, true)
714 }
715
716 pub fn validate(&mut self, tol: &Tolerance) -> Result<(), PlanarError> {
720 if self.segments.len() < 2 {
721 return Err(PlanarError::Degenerate("a loop needs at least two segments".into()));
722 }
723 for s in &self.segments {
724 s.validate(tol)?;
725 }
726 let n = self.segments.len();
727 for i in 0..n {
728 let a = self.segments[i].end;
729 let b = self.segments[(i + 1) % n].start;
730 if a.distance(b) > tol.point_eps {
731 return Err(PlanarError::Degenerate(format!(
732 "loop is open between ({}, {}) and ({}, {})",
733 a.x, a.y, b.x, b.y
734 )));
735 }
736 self.segments[(i + 1) % n].start = a;
737 }
738 Ok(())
739 }
740
741 pub fn canonical(&self, tol: &Tolerance) -> Loop {
743 Loop::new(self.segments.iter().flat_map(|s| s.canonical(tol)).collect())
744 }
745}
746
747#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
749#[serde(rename_all = "camelCase")]
750pub struct Chain {
751 pub segments: Vec<Segment>,
752 pub closed: bool,
753}
754
755impl Chain {
756 pub fn open(segments: Vec<Segment>) -> Self {
757 Self { segments, closed: false }
758 }
759
760 pub fn from_loop(l: &Loop) -> Self {
761 Self { segments: l.segments.clone(), closed: true }
762 }
763
764 pub fn length(&self) -> f64 {
765 self.segments.iter().map(Segment::length).sum()
766 }
767
768 pub fn start(&self) -> Option<Point2> {
769 self.segments.first().map(|s| s.start)
770 }
771
772 pub fn end(&self) -> Option<Point2> {
773 self.segments.last().map(|s| s.end)
774 }
775
776 pub fn reversed(&self) -> Self {
777 Self { segments: self.segments.iter().rev().map(Segment::reversed).collect(), closed: self.closed }
778 }
779
780 pub fn discretize(&self, tol: f64) -> Vec<Point2> {
782 let mut out = Vec::new();
783 for (i, s) in self.segments.iter().enumerate() {
784 let pts = s.discretize(tol);
785 if i == 0 {
786 out.push(pts[0]);
787 }
788 out.extend_from_slice(&pts[1..]);
789 }
790 out
791 }
792
793 pub fn validate(&mut self, tol: &Tolerance) -> Result<(), PlanarError> {
794 if self.segments.is_empty() {
795 return Err(PlanarError::Degenerate("a chain needs at least one segment".into()));
796 }
797 for s in &self.segments {
798 s.validate(tol)?;
799 }
800 let n = self.segments.len();
801 let last = if self.closed { n } else { n - 1 };
802 for i in 0..last {
803 let a = self.segments[i].end;
804 let b = self.segments[(i + 1) % n].start;
805 if a.distance(b) > tol.point_eps {
806 return Err(PlanarError::Degenerate(format!("chain is broken between ({}, {}) and ({}, {})", a.x, a.y, b.x, b.y)));
807 }
808 self.segments[(i + 1) % n].start = a;
809 }
810 Ok(())
811 }
812}
813
814#[derive(Clone, Debug, Default, PartialEq)]
816pub struct Shell {
817 pub outer: Loop,
818 pub holes: Vec<Loop>,
819}
820
821impl Shell {
822 pub fn region(&self) -> Region {
823 let mut loops = vec![self.outer.clone()];
824 loops.extend(self.holes.iter().cloned());
825 Region { loops }
826 }
827}
828
829#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
831#[serde(rename_all = "camelCase")]
832pub struct Region {
833 pub loops: Vec<Loop>,
834}
835
836impl Region {
837 pub fn new(loops: Vec<Loop>, tol: &Tolerance) -> Result<Self, PlanarError> {
839 let mut loops = loops;
840 for l in &mut loops {
841 l.validate(tol)?;
842 }
843 Ok(Self { loops })
844 }
845
846 pub fn from_unoriented(loops: Vec<Loop>, tol: &Tolerance) -> Result<Self, PlanarError> {
849 let mut loops = loops;
850 for l in &mut loops {
851 l.validate(tol)?;
852 }
853 let canon: Vec<Loop> = loops.iter().map(|l| l.canonical(tol)).collect();
854 let mut out = Vec::with_capacity(loops.len());
855 for (i, l) in loops.iter().enumerate() {
856 let probe = l.segments[0].midpoint();
857 let depth = canon
858 .iter()
859 .enumerate()
860 .filter(|(j, _)| *j != i)
861 .filter(|(_, other)| crossings(other.segments.iter(), probe, true) != 0)
862 .count();
863 let want_ccw = depth % 2 == 0;
864 out.push(if l.is_ccw() == want_ccw { l.clone() } else { l.reversed() });
865 }
866 Ok(Self { loops: out })
867 }
868
869 pub fn empty() -> Self {
870 Self { loops: Vec::new() }
871 }
872
873 pub fn is_empty(&self) -> bool {
874 self.loops.is_empty()
875 }
876
877 pub fn area(&self) -> f64 {
878 self.loops.iter().map(Loop::signed_area).sum()
879 }
880
881 pub fn bounds(&self) -> (Point2, Point2) {
882 bounds_of(self.loops.iter().flat_map(|l| l.segments.iter()))
883 }
884
885 pub fn canonical_segments(&self, tol: &Tolerance) -> Vec<Segment> {
887 self.loops.iter().flat_map(|l| l.segments.iter().flat_map(|s| s.canonical(tol))).collect()
888 }
889
890 pub fn winding(&self, p: Point2, tol: &Tolerance) -> i32 {
892 let canon = self.canonical_segments(tol);
893 crossings(canon.iter(), p, true)
894 }
895
896 pub fn contains(&self, p: Point2, tol: &Tolerance) -> bool {
897 self.winding(p, tol) > 0
898 }
899
900 pub fn nearest_boundary(&self, p: Point2) -> Option<(Point2, f64)> {
902 let mut best: Option<(Point2, f64)> = None;
903 for s in self.loops.iter().flat_map(|l| l.segments.iter()) {
904 let (q, _) = s.nearest(p);
905 let d = q.distance(p);
906 if best.map_or(true, |(_, bd)| d < bd) {
907 best = Some((q, d));
908 }
909 }
910 best
911 }
912
913 pub fn signed_distance(&self, p: Point2, tol: &Tolerance) -> f64 {
915 let d = self.nearest_boundary(p).map_or(f64::INFINITY, |(_, d)| d);
916 if self.contains(p, tol) {
917 d
918 } else {
919 -d
920 }
921 }
922
923 pub fn shells(&self, tol: &Tolerance) -> Vec<Shell> {
927 let outers: Vec<usize> = (0..self.loops.len()).filter(|&i| self.loops[i].is_ccw()).collect();
928 let canon: Vec<Loop> = self.loops.iter().map(|l| l.canonical(tol)).collect();
929 let mut shells: Vec<Shell> = outers.iter().map(|&i| Shell { outer: self.loops[i].clone(), holes: Vec::new() }).collect();
930 for l in &self.loops {
931 if l.is_ccw() {
932 continue;
933 }
934 let probe = l.segments[0].midpoint();
935 let mut parent: Option<(usize, f64)> = None;
937 for (k, &o) in outers.iter().enumerate() {
938 if crossings(canon[o].segments.iter(), probe, true) != 0 {
939 let a = self.loops[o].signed_area();
940 if parent.map_or(true, |(_, pa)| a < pa) {
941 parent = Some((k, a));
942 }
943 }
944 }
945 if let Some((k, _)) = parent {
946 shells[k].holes.push(l.clone());
947 }
948 }
949 shells
950 }
951
952 pub fn offset(&self, distance: f64, tol: &Tolerance) -> Result<Region, PlanarError> {
954 offset_region(self, distance, tol)
955 }
956
957 pub fn union(&self, other: &Region, tol: &Tolerance) -> Result<Region, PlanarError> {
958 boolean(self, other, BoolOp::Union, tol)
959 }
960
961 pub fn difference(&self, other: &Region, tol: &Tolerance) -> Result<Region, PlanarError> {
962 boolean(self, other, BoolOp::Difference, tol)
963 }
964
965 pub fn intersection(&self, other: &Region, tol: &Tolerance) -> Result<Region, PlanarError> {
966 boolean(self, other, BoolOp::Intersection, tol)
967 }
968
969 pub fn to_polylines(&self, tol: f64) -> Vec<Vec<Point2>> {
972 self.loops.iter().map(|l| l.discretize(tol)).collect()
973 }
974}
975
976pub(crate) fn simplified(l: &Loop, tol: &Tolerance) -> Loop {
979 let same_support = |a: &Segment, b: &Segment| -> bool {
980 match (a.arc, b.arc) {
981 (None, None) => {
982 let (Some(u), Some(v)) = (a.end.sub(a.start).normalized(), b.end.sub(b.start).normalized()) else { return false };
983 u.cross(v).abs() <= tol.angle_eps && u.dot(v) > 0.0
984 }
985 (Some(x), Some(y)) => x.ccw == y.ccw && x.center.distance(y.center) <= tol.point_eps,
986 _ => false,
987 }
988 };
989 let mut out: Vec<Segment> = Vec::with_capacity(l.segments.len());
990 for s in &l.segments {
991 if let Some(last) = out.last_mut() {
992 if same_support(last, s) && s.end.distance(last.start) > tol.point_eps {
993 let merged = Segment { start: last.start, end: s.end, arc: last.arc };
994 if merged.arc.is_none() || (merged.length() >= last.length() + s.length() - tol.distance_eps && merged.length() <= std::f64::consts::PI * merged.radius() + tol.distance_eps) {
995 *last = merged;
996 continue;
997 }
998 }
999 }
1000 out.push(*s);
1001 }
1002 if out.len() > 2 {
1003 let n = out.len();
1004 if same_support(&out[n - 1], &out[0]) && out[n - 1].start.distance(out[0].end) > tol.point_eps {
1005 let last = out[n - 1];
1006 let merged = Segment { start: last.start, end: out[0].end, arc: last.arc };
1007 if merged.arc.is_none() || (merged.length() >= last.length() + out[0].length() - tol.distance_eps && merged.length() <= std::f64::consts::PI * merged.radius() + tol.distance_eps) {
1008 out.pop();
1009 out[0] = merged;
1010 }
1011 }
1012 }
1013 Loop::new(out)
1014}
1015
1016pub(crate) fn bounds_of<'a>(segments: impl Iterator<Item = &'a Segment>) -> (Point2, Point2) {
1017 let mut lo = Point2::new(f64::INFINITY, f64::INFINITY);
1018 let mut hi = Point2::new(f64::NEG_INFINITY, f64::NEG_INFINITY);
1019 for s in segments {
1020 let (a, b) = s.bounds();
1021 lo = Point2::new(lo.x.min(a.x), lo.y.min(a.y));
1022 hi = Point2::new(hi.x.max(b.x), hi.y.max(b.y));
1023 }
1024 (lo, hi)
1025}
1026
1027pub(crate) fn crossings<'a>(segments: impl Iterator<Item = &'a Segment>, p: Point2, upper: bool) -> i32 {
1035 let mut w = 0;
1036 for s in segments {
1037 let (ylo, yhi) = if s.start.y <= s.end.y { (s.start.y, s.end.y) } else { (s.end.y, s.start.y) };
1038 let inside = if upper { ylo <= p.y && p.y < yhi } else { ylo < p.y && p.y <= yhi };
1039 if !inside {
1040 continue;
1041 }
1042 let x = match s.arc {
1043 None => s.start.x + (p.y - s.start.y) * (s.end.x - s.start.x) / (s.end.y - s.start.y),
1044 Some(a) => {
1045 let r = s.radius();
1046 let dy = p.y - a.center.y;
1047 let h = (r * r - dy * dy).max(0.0).sqrt();
1048 if s.midpoint().x >= a.center.x {
1049 a.center.x + h
1050 } else {
1051 a.center.x - h
1052 }
1053 }
1054 };
1055 if x > p.x {
1056 w += if s.end.y > s.start.y { 1 } else { -1 };
1057 }
1058 }
1059 w
1060}
1061