Skip to main content

ogeom_mesh/
planar.rs

1//! Regions of the plane bounded by weighted chains of segments, found
2//! robustly: the engine under [`shadow`](crate::shadow()) and the outline
3//! fit's offsets.
4//!
5//! A set of directed segments with integer weights, closed as a chain (at
6//! every point the weights in equal the weights out), gives every point off
7//! the segments a winding count: the weights of the segments a ray from it
8//! crosses, from right to left minus from left to right. [`region`] returns
9//! the boundary of the points whose counts a rule accepts. Two independent
10//! counts are carried, so a rule can take a difference as well as a union.
11//!
12//! Every decision is exact. The endpoints are first snapped to a grid of
13//! `2^-38` of the chain's extent, a power of two, and from then on every
14//! coordinate is an integer and every test is integer arithmetic: the
15//! orientation of three points, where two segments cross, whether a segment
16//! meets a grid cell. The crossings are resolved by snap rounding: every
17//! cell holding an endpoint or a crossing is *hot*, and every segment is
18//! replaced by the path through the centres of the hot cells it meets, in
19//! the order it meets them. The paths cross nowhere but at shared centres,
20//! each moved by at most `sqrt(2)/2` of a cell from its segment, and a
21//! segment's path starts and ends where the segment does, so the chain
22//! stays closed and every count stays defined.
23//!
24//! The paths are then the constraints of a triangulation. A count changes
25//! across a constraint by its weight and across nothing else, so one walk
26//! from the unbounded face, whose counts are zero, gives every triangle's
27//! counts. The boundary is every triangle edge with an accepted triangle on
28//! its left and a rejected one on its right, linked into loops by turning
29//! through accepted triangles only, so two loops touching at a corner come
30//! out as two loops. Points where a loop runs straight on are dropped.
31
32use ogeom_core::{OgeomResult, ogeom_bail};
33use ogeom_math::Point2;
34use spade::handles::{FixedDirectedEdgeHandle, FixedVertexHandle};
35use spade::{ConstrainedDelaunayTriangulation, Triangulation as _};
36
37/// One closed loop of a region's boundary.
38///
39/// The region lies on the left: an outer boundary runs counter-clockwise
40/// and a hole's clockwise.
41#[derive(Debug, Clone, PartialEq, Default)]
42pub struct Loop2 {
43    /// The corners in order. The first is not repeated at the end.
44    pub points: Vec<Point2>,
45}
46
47impl Loop2 {
48    /// The signed area the loop encloses: positive for an outer boundary,
49    /// negative for a hole.
50    #[must_use]
51    pub fn area(&self) -> f64 {
52        let n = self.points.len();
53        let mut twice = 0.0;
54        for i in 0..n {
55            let (a, b) = (self.points[i], self.points[(i + 1) % n]);
56            twice += a.x.mul_add(b.y, -(a.y * b.x));
57        }
58        twice / 2.0
59    }
60
61    /// Whether the loop bounds a hole: whether it runs clockwise.
62    #[must_use]
63    pub fn is_hole(&self) -> bool {
64        self.area() < 0.0
65    }
66}
67
68/// A directed segment of a chain, with its weight in each of the two
69/// counts.
70#[derive(Debug, Clone, Copy, PartialEq)]
71pub struct Edge2 {
72    /// Where the segment starts.
73    pub from: Point2,
74    /// Where it ends.
75    pub to: Point2,
76    /// How much it adds to each count of the points on its left, against
77    /// the points on its right.
78    pub weight: [i32; 2],
79}
80
81/// The boundary of the points whose winding counts `inside` accepts.
82///
83/// The loops are within half a cell's diagonal of the exact boundary, a
84/// cell being the power of two at or above `2^-38` of the widest extent of
85/// the endpoints: at most `2^-37` of it.
86///
87/// The edges must form a closed chain in each count: at every endpoint the
88/// weights of the edges arriving sum to those of the edges leaving. A
89/// chain that is not closed has no counts, and its region is whatever the
90/// walk from the unbounded face meets first. `inside` must reject `[0, 0]`,
91/// the counts of every point far away.
92///
93/// # Errors
94///
95/// [`OgeomError::Numeric`](ogeom_core::OgeomError::Numeric) if an endpoint
96/// is not finite, and
97/// [`OgeomError::Construction`](ogeom_core::OgeomError::Construction) if
98/// `inside` accepts `[0, 0]`.
99pub fn region(edges: &[Edge2], inside: impl Fn([i64; 2]) -> bool) -> OgeomResult<Vec<Loop2>> {
100    if inside([0, 0]) {
101        ogeom_bail!(
102            Construction,
103            "a region accepting the unbounded face has no boundary"
104        );
105    }
106    if let Some(e) = edges
107        .iter()
108        .find(|e| !e.from.is_finite() || !e.to.is_finite())
109    {
110        ogeom_bail!(Numeric, "a chain edge from {:?} is not finite", e.from);
111    }
112    let Some(grid) = Grid::around(edges.iter().flat_map(|e| [e.from, e.to])) else {
113        return Ok(Vec::new());
114    };
115    let segments = edges
116        .iter()
117        .map(|e| Seg {
118            a: grid.snap(e.from),
119            b: grid.snap(e.to),
120            w: e.weight,
121        })
122        .collect();
123    Ok(trace(segments, &inside)?
124        .into_iter()
125        .map(|ring| Loop2 {
126            points: ring.into_iter().map(|q| grid.point(q)).collect(),
127        })
128        .collect())
129}
130
131/// A grid coordinate as a double, exactly: grid coordinates are below
132/// `2^41`.
133#[allow(
134    clippy::cast_precision_loss,
135    reason = "grid coordinates are below 2^41, exact in a double"
136)]
137pub(crate) const fn real(v: i64) -> f64 {
138    v as f64
139}
140
141/// A count as a double, to size buckets by.
142#[allow(clippy::cast_precision_loss, reason = "an estimate, rounding harmless")]
143pub(crate) const fn estimate(n: usize) -> f64 {
144    n as f64
145}
146
147/// A double holding an integer of grid size, as one.
148#[allow(
149    clippy::cast_possible_truncation,
150    reason = "callers pass values rounded to integers below 2^42"
151)]
152pub(crate) const fn whole(v: f64) -> i64 {
153    v as i64
154}
155
156/// A bucket or point index as the `u32` the lists keep. Bucket counts are
157/// capped at `4096^2` and points at the `2^32` the triangulation allows.
158#[allow(
159    clippy::cast_possible_truncation,
160    clippy::cast_sign_loss,
161    reason = "indices are below 2^32"
162)]
163pub(crate) const fn slot(v: i64) -> u32 {
164    v as u32
165}
166
167/// A point on the grid, in units of half a cell: cell centres are even.
168pub(crate) type Q = [i64; 2];
169
170/// The grid points snap to: square cells, a power of two wide, from an
171/// origin on a cell corner's multiple.
172#[derive(Debug, Clone, Copy)]
173pub(crate) struct Grid {
174    origin: [f64; 2],
175    cell: f64,
176}
177
178/// How many cells the widest extent spans at most.
179const SPAN_BITS: i32 = 38;
180
181impl Grid {
182    /// The grid for points spread over this box, or `None` for none.
183    pub(crate) fn around(points: impl Iterator<Item = Point2>) -> Option<Self> {
184        let mut lo = [f64::INFINITY; 2];
185        let mut hi = [f64::NEG_INFINITY; 2];
186        for p in points {
187            lo = [lo[0].min(p.x), lo[1].min(p.y)];
188            hi = [hi[0].max(p.x), hi[1].max(p.y)];
189        }
190        if lo[0] > hi[0] {
191            return None;
192        }
193        let extent = (hi[0] - lo[0]).max(hi[1] - lo[1]);
194        // A single point spans nothing; any cell size then serves.
195        let cell = if extent > 0.0 {
196            2f64.powi(i32::try_from(whole(extent.log2().ceil())).unwrap_or(0) - SPAN_BITS)
197        } else {
198            1.0
199        };
200        Some(Self {
201            origin: [(lo[0] / cell).floor() * cell, (lo[1] / cell).floor() * cell],
202            cell,
203        })
204    }
205
206    /// The centre of the cell a point falls in.
207    pub(crate) fn snap(&self, p: Point2) -> Q {
208        let k = |v: f64, o: f64| 2 * whole(((v - o) / self.cell).round());
209        [k(p.x, self.origin[0]), k(p.y, self.origin[1])]
210    }
211
212    /// A grid point back in the plane.
213    pub(crate) fn point(&self, q: Q) -> Point2 {
214        let c = self.cell / 2.0;
215        Point2::new(
216            real(q[0]).mul_add(c, self.origin[0]),
217            real(q[1]).mul_add(c, self.origin[1]),
218        )
219    }
220}
221
222/// A directed segment between grid points, and its weights.
223#[derive(Debug, Clone, Copy)]
224pub(crate) struct Seg {
225    pub(crate) a: Q,
226    pub(crate) b: Q,
227    pub(crate) w: [i32; 2],
228}
229
230/// Twice the signed area of `(a, b, c)`, exactly. Grid coordinates are
231/// below `2^40`, so the products are below `2^80`.
232pub(crate) fn orient(a: Q, b: Q, c: Q) -> i128 {
233    let d = |p: Q, q: Q| [i128::from(q[0] - p[0]), i128::from(q[1] - p[1])];
234    let (u, v) = (d(a, b), d(a, c));
235    u[0] * v[1] - u[1] * v[0]
236}
237
238/// Sum the weights of segments running between the same two points either
239/// way, and drop those that cancel or have no length.
240fn cancel(mut segments: Vec<Seg>) -> Vec<Seg> {
241    for s in &mut segments {
242        if s.b < s.a {
243            *s = Seg {
244                a: s.b,
245                b: s.a,
246                w: [-s.w[0], -s.w[1]],
247            };
248        }
249    }
250    segments.sort_unstable_by_key(|s| (s.a, s.b));
251    let mut out: Vec<Seg> = Vec::with_capacity(segments.len());
252    for s in segments {
253        match out.last_mut() {
254            Some(t) if t.a == s.a && t.b == s.b => {
255                t.w = [t.w[0] + s.w[0], t.w[1] + s.w[1]];
256            }
257            _ => out.push(s),
258        }
259    }
260    out.retain(|s| s.a != s.b && s.w != [0, 0]);
261    out
262}
263
264/// Uniform buckets over the grid, each listing what may lie in it.
265struct Buckets {
266    lo: Q,
267    size: i64,
268    columns: i64,
269    rows: i64,
270}
271
272impl Buckets {
273    fn over(segments: &[Seg]) -> Self {
274        let mut lo = [i64::MAX; 2];
275        let mut hi = [i64::MIN; 2];
276        for s in segments {
277            for p in [s.a, s.b] {
278                lo = [lo[0].min(p[0]), lo[1].min(p[1])];
279                hi = [hi[0].max(p[0]), hi[1].max(p[1])];
280            }
281        }
282        // A margin of the two units the segment lists are widened by.
283        lo = [lo[0] - 2, lo[1] - 2];
284        hi = [hi[0] + 2, hi[1] + 2];
285        let span = (hi[0] - lo[0]).max(hi[1] - lo[1]) + 1;
286        let per_side = whole(estimate(segments.len()).sqrt().ceil()).clamp(1, 4096);
287        let size = (span + per_side - 1) / per_side;
288        Self {
289            lo,
290            size,
291            columns: (hi[0] - lo[0]) / size + 1,
292            rows: (hi[1] - lo[1]) / size + 1,
293        }
294    }
295
296    fn column(&self, x: i64) -> i64 {
297        ((x - self.lo[0]) / self.size).clamp(0, self.columns - 1)
298    }
299
300    fn row(&self, y: i64) -> i64 {
301        ((y - self.lo[1]) / self.size).clamp(0, self.rows - 1)
302    }
303
304    fn of(&self, p: Q) -> u32 {
305        slot(self.row(p[1]) * self.columns + self.column(p[0]))
306    }
307
308    /// Every bucket holding a point within two units of the segment, and
309    /// perhaps a few more: within each row of buckets the segment's span
310    /// is found in floating point, then widened by two units, far more than
311    /// its rounding.
312    fn along(&self, s: &Seg, mut visit: impl FnMut(u32)) {
313        let (y_lo, y_hi) = (s.a[1].min(s.b[1]) - 2, s.a[1].max(s.b[1]) + 2);
314        let (x_lo, x_hi) = (s.a[0].min(s.b[0]) - 2, s.a[0].max(s.b[0]) + 2);
315        let dx = real(s.b[0] - s.a[0]);
316        let dy = real(s.b[1] - s.a[1]);
317        let x_at = |y: i64| (real(y - s.a[1]) * dx / dy) + real(s.a[0]);
318        for r in self.row(y_lo)..=self.row(y_hi) {
319            let (from, to) = if dy == 0.0 {
320                (x_lo, x_hi)
321            } else {
322                let top = self.lo[1] + r * self.size;
323                let y0 = top.max(y_lo);
324                let y1 = (top + self.size - 1).min(y_hi);
325                let (xa, xb) = (x_at(y0), x_at(y1));
326                (
327                    whole((xa.min(xb) - 2.0).floor()).max(x_lo),
328                    whole((xa.max(xb) + 2.0).ceil()).min(x_hi),
329                )
330            };
331            for c in self.column(from)..=self.column(to) {
332                visit(slot(r * self.columns + c));
333            }
334        }
335    }
336
337    /// Compressed lists of the items each bucket holds.
338    fn lists(&self, pairs: &mut [(u32, u32)]) -> (Vec<usize>, Vec<u32>) {
339        pairs.sort_unstable();
340        let count = slot(self.rows * self.columns) as usize;
341        let mut start = vec![0usize; count + 1];
342        for &(b, _) in pairs.iter() {
343            start[b as usize + 1] += 1;
344        }
345        for k in 0..count {
346            start[k + 1] += start[k];
347        }
348        (start, pairs.iter().map(|&(_, i)| i).collect())
349    }
350}
351
352/// The cell of the point where two segments cross at a point inside both,
353/// or `None` if they do not. Segments that touch or overlap meet at an
354/// endpoint, whose cell is hot already.
355fn crossing(s: &Seg, t: &Seg) -> Option<Q> {
356    let s1 = orient(s.a, s.b, t.a).signum();
357    let s2 = orient(s.a, s.b, t.b).signum();
358    if s1 * s2 >= 0 {
359        return None;
360    }
361    let along = orient(t.a, t.b, s.a);
362    let other = orient(t.a, t.b, s.b);
363    if along.signum() * other.signum() >= 0 {
364        return None;
365    }
366    // The crossing is s.a + (s.b - s.a) * along / (along - other). Every
367    // term is below 2^123; the cell's centre is the even integer nearest.
368    let (mut num, mut den) = (along, along - other);
369    if den < 0 {
370        (num, den) = (-num, -den);
371    }
372    let cell = |a: i64, b: i64| {
373        let x = i128::from(a) * den + i128::from(b - a) * num;
374        i64::try_from(2 * (x + den).div_euclid(2 * den)).unwrap_or(i64::MAX)
375    };
376    Some([cell(s.a[0], s.b[0]), cell(s.a[1], s.b[1])])
377}
378
379/// A bound on a segment's parameter, `num / den` with `den > 0`, and
380/// whether the bound itself is excluded.
381#[derive(Debug, Clone, Copy)]
382struct Bound {
383    num: i128,
384    den: i128,
385    open: bool,
386}
387
388impl Bound {
389    /// Compare the values, then a closed bound before an open one.
390    fn cmp(self, other: Self) -> core::cmp::Ordering {
391        self.value_cmp(other).then(self.open.cmp(&other.open))
392    }
393
394    /// Compare the values alone. Numerators are below `2^41` and
395    /// denominators below `2^40`.
396    fn value_cmp(self, other: Self) -> core::cmp::Ordering {
397        (self.num * other.den).cmp(&(other.num * self.den))
398    }
399}
400
401/// Where along `s` it first meets the half-open cell centred on `c`,
402/// `[c - 1, c + 1)` on each axis, or `None` if it does not.
403fn entry(s: &Seg, c: Q) -> Option<Bound> {
404    let mut lo = Bound {
405        num: 0,
406        den: 1,
407        open: false,
408    };
409    let mut hi = Bound {
410        num: 1,
411        den: 1,
412        open: false,
413    };
414    for (k, &centre) in c.iter().enumerate() {
415        let (p, d) = (i128::from(s.a[k]), i128::from(s.b[k] - s.a[k]));
416        let (left, right) = (i128::from(centre - 1), i128::from(centre + 1));
417        if d == 0 {
418            if p < left || p >= right {
419                return None;
420            }
421            continue;
422        }
423        // left <= p + t d < right.
424        let (from, to) = if d > 0 {
425            (
426                Bound {
427                    num: left - p,
428                    den: d,
429                    open: false,
430                },
431                Bound {
432                    num: right - p,
433                    den: d,
434                    open: true,
435                },
436            )
437        } else {
438            (
439                Bound {
440                    num: p - right,
441                    den: -d,
442                    open: true,
443                },
444                Bound {
445                    num: p - left,
446                    den: -d,
447                    open: false,
448                },
449            )
450        };
451        // The tighter of each pair: among equal values, the open one.
452        if from.cmp(lo).is_gt() {
453            lo = from;
454        }
455        let upper = to.value_cmp(hi);
456        if upper.is_lt() || (upper.is_eq() && to.open) {
457            hi = to;
458        }
459    }
460    let ordered = lo.value_cmp(hi);
461    let meets = ordered.is_lt() || (ordered.is_eq() && !lo.open && !hi.open);
462    meets.then_some(lo)
463}
464
465/// Snap-round the segments and return the boundary of the accepted region
466/// as loops of grid points.
467pub(crate) fn trace(
468    segments: Vec<Seg>,
469    inside: &dyn Fn([i64; 2]) -> bool,
470) -> OgeomResult<Vec<Vec<Q>>> {
471    let segments = cancel(segments);
472    if segments.is_empty() {
473        return Ok(Vec::new());
474    }
475    let buckets = Buckets::over(&segments);
476    let mut pairs = Vec::with_capacity(segments.len() * 2);
477    for (i, s) in segments.iter().enumerate() {
478        buckets.along(s, |b| pairs.push((b, slot(i as i64))));
479    }
480    let (start, held) = buckets.lists(&mut pairs);
481
482    // Hot cells: every endpoint's, and every crossing's, each crossing
483    // found once, in the bucket its cell's centre falls in.
484    let mut hot: Vec<Q> = segments.iter().flat_map(|s| [s.a, s.b]).collect();
485    for b in 0..start.len() - 1 {
486        let here = &held[start[b]..start[b + 1]];
487        for (k, &i) in here.iter().enumerate() {
488            let s = &segments[i as usize];
489            for &j in &here[k + 1..] {
490                let t = &segments[j as usize];
491                if let Some(c) = crossing(s, t)
492                    && buckets.of(c) as usize == b
493                {
494                    hot.push(c);
495                }
496            }
497        }
498    }
499    hot.sort_unstable();
500    hot.dedup();
501    let mut placed: Vec<(u32, u32)> = hot
502        .iter()
503        .enumerate()
504        .map(|(k, &c)| (buckets.of(c), slot(k as i64)))
505        .collect();
506    let (hot_start, hot_held) = buckets.lists(&mut placed);
507
508    // Each segment as the path through the hot cells it meets.
509    let mut rounded = Vec::with_capacity(segments.len());
510    let mut met: Vec<(Bound, Q)> = Vec::new();
511    for s in &segments {
512        met.clear();
513        buckets.along(s, |b| {
514            let b = b as usize;
515            for &k in &hot_held[hot_start[b]..hot_start[b + 1]] {
516                let c = hot[k as usize];
517                if let Some(at) = entry(s, c) {
518                    met.push((at, c));
519                }
520            }
521        });
522        met.sort_unstable_by(|x, y| x.0.cmp(y.0));
523        for pair in met.windows(2) {
524            rounded.push(Seg {
525                a: pair[0].1,
526                b: pair[1].1,
527                w: s.w,
528            });
529        }
530    }
531    regions_of(&cancel(rounded), inside)
532}
533
534/// Triangulate non-crossing segments, count each triangle, and link the
535/// edges between accepted and rejected triangles into loops.
536fn regions_of(segments: &[Seg], inside: &dyn Fn([i64; 2]) -> bool) -> OgeomResult<Vec<Vec<Q>>> {
537    if segments.is_empty() {
538        return Ok(Vec::new());
539    }
540    let mut points: Vec<Q> = segments.iter().flat_map(|s| [s.a, s.b]).collect();
541    points.sort_unstable();
542    points.dedup();
543    let index = |q: Q| points.binary_search(&q).unwrap_or(0);
544    let vertices: Vec<spade::Point2<f64>> = points
545        .iter()
546        .map(|q| spade::Point2::new(real(q[0]), real(q[1])))
547        .collect();
548    let Ok(mut cdt) =
549        ConstrainedDelaunayTriangulation::<spade::Point2<f64>>::bulk_load_cdt(vertices, Vec::new())
550    else {
551        ogeom_bail!(Numeric, "the region's points could not be triangulated");
552    };
553    let mut constraints: Vec<(FixedDirectedEdgeHandle, [i32; 2])> = Vec::new();
554    for s in segments {
555        let (from, to) = (index(s.a), index(s.b));
556        let made = cdt.try_add_constraint(
557            FixedVertexHandle::from_index(from),
558            FixedVertexHandle::from_index(to),
559        );
560        if made.is_empty() {
561            ogeom_bail!(
562                Construction,
563                "a snap-rounded segment crosses another; the region is not traced"
564            );
565        }
566        let along = [s.b[0] - s.a[0], s.b[1] - s.a[1]];
567        for h in made {
568            let e = cdt.directed_edge(h);
569            let [p, q] = e.positions();
570            let forward = (q.x - p.x).mul_add(real(along[0]), (q.y - p.y) * real(along[1])) > 0.0;
571            constraints.push((if forward { h } else { h.rev() }, s.w));
572        }
573    }
574    // Weights in each direction of every edge, by directed edge index.
575    let mut weight = vec![[0i64; 2]; cdt.num_directed_edges()];
576    for (h, w) in constraints {
577        for k in 0..2 {
578            weight[h.index()][k] += i64::from(w[k]);
579            weight[h.rev().index()][k] -= i64::from(w[k]);
580        }
581    }
582
583    // Counts by face, walked from the unbounded face. The face on an
584    // edge's left counts its weight more than the face on its right.
585    let mut count: Vec<Option<[i64; 2]>> = vec![None; cdt.num_all_faces()];
586    let outer = cdt.outer_face().fix().index();
587    count[outer] = Some([0, 0]);
588    let mut sides = vec![[None; 3]; cdt.num_all_faces()];
589    for face in cdt.inner_faces() {
590        sides[face.fix().index()] = face.adjacent_edges().map(|e| Some(e.fix()));
591    }
592    let mut stack = vec![outer];
593    let mut around = Vec::with_capacity(3);
594    while let Some(f) = stack.pop() {
595        let Some(here) = count[f] else { continue };
596        around.clear();
597        if f == outer {
598            for e in cdt.convex_hull() {
599                around.push(if e.face().is_outer() {
600                    e.fix()
601                } else {
602                    e.rev().fix()
603                });
604            }
605        } else {
606            around.extend(sides[f].iter().flatten());
607        }
608        for &e in &around {
609            let w = weight[e.index()];
610            let g = cdt.directed_edge(e).rev().face().fix().index();
611            if count[g].is_none() {
612                count[g] = Some([here[0] - w[0], here[1] - w[1]]);
613                stack.push(g);
614            }
615        }
616    }
617    let accepted: Vec<bool> = count
618        .iter()
619        .enumerate()
620        .map(|(f, c)| f != outer && c.is_some_and(inside))
621        .collect();
622
623    let boundary = |e: FixedDirectedEdgeHandle| {
624        let h = cdt.directed_edge(e);
625        accepted[h.face().fix().index()] && !accepted[h.rev().face().fix().index()]
626    };
627    let mut used = vec![false; cdt.num_directed_edges()];
628    let mut loops = Vec::new();
629    for e in cdt.directed_edges() {
630        let first = e.fix();
631        if used[first.index()] || !boundary(first) {
632            continue;
633        }
634        let mut ring = Vec::new();
635        let mut at = first;
636        loop {
637            used[at.index()] = true;
638            let h = cdt.directed_edge(at);
639            ring.push(points[h.from().fix().index()]);
640            // Turn about the end through accepted faces to the next edge
641            // with a rejected face on its right.
642            let mut next = h.next();
643            let mut turns = 0usize;
644            while !boundary(next.fix()) {
645                next = next.rev().next();
646                turns += 1;
647                if turns > cdt.num_directed_edges() {
648                    ogeom_bail!(Construction, "a region boundary does not link up");
649                }
650            }
651            at = next.fix();
652            if at == first {
653                break;
654            }
655            if used[at.index()] {
656                ogeom_bail!(Construction, "a region boundary links into itself");
657            }
658        }
659        loops.push(straightened(ring));
660    }
661    Ok(loops)
662}
663
664/// The loop without the points where it runs straight on.
665fn straightened(mut ring: Vec<Q>) -> Vec<Q> {
666    loop {
667        let n = ring.len();
668        if n < 3 {
669            return ring;
670        }
671        let keep: Vec<bool> = (0..n)
672            .map(|i| orient(ring[(i + n - 1) % n], ring[i], ring[(i + 1) % n]) != 0)
673            .collect();
674        if keep.iter().all(|&k| k) {
675            return ring;
676        }
677        let mut k = 0;
678        ring.retain(|_| {
679            k += 1;
680            keep[k - 1]
681        });
682    }
683}
684
685#[cfg(test)]
686mod tests {
687    #![allow(clippy::unwrap_used, reason = "test code")]
688    use super::*;
689
690    fn square(x: f64, y: f64, s: f64) -> Vec<Edge2> {
691        let p = [
692            Point2::new(x, y),
693            Point2::new(x + s, y),
694            Point2::new(x + s, y + s),
695            Point2::new(x, y + s),
696        ];
697        (0..4)
698            .map(|i| Edge2 {
699                from: p[i],
700                to: p[(i + 1) % 4],
701                weight: [1, 0],
702            })
703            .collect()
704    }
705
706    /// Two overlapping unit squares unite into one outline of eight
707    /// corners, two of them where the sides cross, of area `2 - 1/4`, and
708    /// meet in a square of area `1/4`.
709    #[test]
710    fn two_overlapping_squares_unite() {
711        let mut edges = square(0.0, 0.0, 1.0);
712        edges.extend(square(0.5, 0.5, 1.0));
713        let loops = region(&edges, |w| w[0] > 0).unwrap();
714        assert_eq!(loops.len(), 1);
715        assert_eq!(loops[0].points.len(), 8);
716        assert!((loops[0].area() - 1.75).abs() < 1e-9);
717        let both = region(&edges, |w| w[0] > 1).unwrap();
718        assert_eq!(both.len(), 1);
719        assert!((both[0].area() - 0.25).abs() < 1e-9);
720    }
721
722    /// A square with a smaller one taken away in the second count keeps
723    /// the smaller as a hole, clockwise.
724    #[test]
725    fn a_difference_keeps_its_hole() {
726        let mut edges = square(0.0, 0.0, 3.0);
727        edges.extend(square(1.0, 1.0, 1.0).into_iter().map(|e| Edge2 {
728            weight: [0, 1],
729            ..e
730        }));
731        let loops = region(&edges, |w| w[0] > 0 && w[1] == 0).unwrap();
732        assert_eq!(loops.len(), 2);
733        let mut areas: Vec<f64> = loops.iter().map(Loop2::area).collect();
734        areas.sort_by(f64::total_cmp);
735        assert!((areas[0] + 1.0).abs() < 1e-9 && (areas[1] - 9.0).abs() < 1e-9);
736    }
737
738    /// Squares touching at one corner stay two loops.
739    #[test]
740    fn squares_touching_at_a_corner_stay_apart() {
741        let mut edges = square(0.0, 0.0, 1.0);
742        edges.extend(square(1.0, 1.0, 1.0));
743        let loops = region(&edges, |w| w[0] > 0).unwrap();
744        assert_eq!(loops.len(), 2);
745        assert!(loops.iter().all(|l| l.points.len() == 4));
746    }
747
748    /// Many thin slivers crossing near one point, the case where rounded
749    /// crossings go wrong: the union of a fan of long thin triangles about
750    /// a common centre is traced without error, and its area is the sum
751    /// of the parts less what they share, which here is under the sum.
752    #[test]
753    fn a_fan_of_slivers_crossing_near_a_point() {
754        let mut edges = Vec::new();
755        for k in 0..64 {
756            let a = f64::from(k) * 0.049_087_385;
757            let (c, s) = (a.cos(), a.sin());
758            let p = [
759                Point2::new(-c + 1e-9 * s, -s - 1e-9 * c),
760                Point2::new(c + 1e-9 * s, s - 1e-9 * c),
761                Point2::new(c - 1e-3 * s, s + 1e-3 * c),
762            ];
763            for i in 0..3 {
764                edges.push(Edge2 {
765                    from: p[i],
766                    to: p[(i + 1) % 3],
767                    weight: [1, 0],
768                });
769            }
770        }
771        let loops = region(&edges, |w| w[0] > 0).unwrap();
772        let area: f64 = loops.iter().map(Loop2::area).sum();
773        assert!(area > 0.0 && area < 64.0 * 1e-3);
774    }
775}