Skip to main content

axiolid_triangulate/
domain.rs

1// SPDX-License-Identifier: MPL-2.0
2
3//! Triangulating a domain: the area inside an outline, minus its holes.
4//!
5//! [`triangulate`](crate::triangulate) covers the convex hull of its input.
6//! A caller with a floor outline, a courtyard hole and an island in it
7//! wants the triangles of that region only. [`triangulate_domain`] takes the
8//! same points and constraints plus the rings that bound the region, and
9//! keeps exactly the triangles inside.
10//!
11//! # The rule: winding with ring roles
12//!
13//! Each ring is tagged [`RingKind::Outer`] or [`RingKind::Hole`] and may be
14//! given either way round. It is normalised (outer counter-clockwise, hole
15//! clockwise), and then every piece of its boundary adds one to the winding
16//! of the region on its left. The domain is the set of triangles with
17//! winding exactly 1: inside one outer ring (or inside an island of a hole,
18//! which is an outer ring again) and in no hole.
19//!
20//! The winding is computed by a flood fill over the triangles' shared
21//! edges, seeded outside the hull at winding 0. Crossing a ring piece from
22//! its left to its right changes the winding by minus the piece's net
23//! contribution. Only ring pieces carry winding; breaklines never do, so a
24//! breakline cannot change the domain. The fill is path independent: the
25//! winding difference between two triangles is the signed number of ring
26//! boundaries between them, whatever path joins them, so a revisit can only
27//! disagree on corrupted input; it is asserted in debug builds and not
28//! re-checked in release builds. A first visit with a winding outside
29//! `{0, 1}` is refused as [`TriangulationError::MisnestedRing`], and so is
30//! an empty result as [`TriangulationError::EmptyDomain`].
31//!
32//! Rejected alternatives:
33//!
34//! - Even-odd parity over unlabelled rings cannot tell a hole outside the
35//!   outline from an island, and a hole listed twice cancels silently.
36//! - Parity plus a per-edge role check still cancels a duplicate hole, and
37//!   refuses valid input where three rings share an edge.
38//!
39//! # Rings
40//!
41//! A ring lists indices into the points in order and is closed implicitly;
42//! repeating the first index at the end is refused as a repeated vertex.
43//! Rings may share vertices and edges with each other, but a ring may not
44//! visit one vertex twice. Its edges become constraints, so a ring edge
45//! through input vertices is split at them like any constraint (#360), and
46//! a breakline that crosses a ring edge between vertices is refused as
47//! [`TriangulationError::CrossingConstraints`]. Any two crossing
48//! breaklines are refused too, even outside the domain.
49//!
50//! The orientation of a ring is read exactly at its lexicographically
51//! lowest vertex, which is always a convex corner. The fold check (both
52//! neighbours of a vertex on one side of it on a common line) guarantees
53//! that turn is non-zero.
54//!
55//! # Points outside the domain
56//!
57//! A point outside the domain, or inside a hole, is dropped from the
58//! triangles and reported by [`Triangulation::outside_points`]; it is not an
59//! error, because a caller's point cloud rarely respects the outline.
60//! [`Triangulation::points`] still holds every point, so indices stay
61//! stable. Inside the domain the triangulation is constrained Delaunay:
62//! constraints block visibility, and so the dropped points do not change it.
63//!
64//! A constraint entirely outside the domain is removed from
65//! [`Triangulation::constraints`], so every listed constraint is an edge of
66//! an output triangle.
67
68use axiolid_core::Point2;
69
70use crate::build::build;
71use crate::mesh::{Triangulation, TriangulationError, NO_HALFEDGE};
72use crate::recover::Splits;
73use crate::{collinear, turns_left, Constraint};
74
75/// What a ring bounds.
76#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
77pub enum RingKind {
78    /// The interior of the ring is part of the domain. An island inside a
79    /// hole is an outer ring again.
80    Outer,
81    /// The interior of the ring is excluded from the domain.
82    Hole,
83}
84
85/// A closed boundary of the domain, as indices into the input points.
86///
87/// The ring is closed implicitly (the last vertex joins the first) and may
88/// be listed in either orientation.
89#[derive(Debug, Clone, PartialEq, Eq, Hash)]
90pub struct Ring {
91    /// The vertices in order, without repeating the first at the end.
92    pub vertices: Vec<u32>,
93    /// Whether the ring bounds the domain or a hole in it.
94    pub kind: RingKind,
95}
96
97impl Ring {
98    /// An outer ring through `vertices`.
99    #[must_use]
100    pub fn outer(vertices: impl Into<Vec<u32>>) -> Self {
101        Self {
102            vertices: vertices.into(),
103            kind: RingKind::Outer,
104        }
105    }
106
107    /// A hole bounded by `vertices`.
108    #[must_use]
109    pub fn hole(vertices: impl Into<Vec<u32>>) -> Self {
110        Self {
111            vertices: vertices.into(),
112            kind: RingKind::Hole,
113        }
114    }
115}
116
117/// Build a constrained Delaunay triangulation of the domain bounded by
118/// `rings`.
119///
120/// The result keeps exactly the triangles inside an outer ring and outside
121/// every hole (see the module documentation for the rule). The ring edges
122/// are constraints, as are `constraints`; the points that fall outside the
123/// domain are listed by [`Triangulation::outside_points`].
124///
125/// # Errors
126///
127/// Everything [`triangulate`](crate::triangulate) refuses, and
128/// [`TriangulationError::RingOutOfRange`],
129/// [`TriangulationError::TooFewRingVertices`],
130/// [`TriangulationError::RepeatedRingVertex`],
131/// [`TriangulationError::FoldedRing`], [`TriangulationError::MisnestedRing`]
132/// and [`TriangulationError::EmptyDomain`] (also for an empty ring list).
133/// Checks run in this order, ring by ring and vertex by vertex.
134pub fn triangulate_domain(
135    points: &[Point2],
136    constraints: &[Constraint],
137    rings: &[Ring],
138) -> Result<Triangulation, TriangulationError> {
139    let built = build(points, constraints, Some(rings))?;
140    let mut tri = built.tri;
141    classify(&mut tri, &built.kept, &built.splits, rings)?;
142    tri.domain = Some(rings.to_vec());
143    Ok(tri)
144}
145
146/// Re-triangulate `points` the way `domain` says: the whole hull when it is
147/// `None`, the domain's triangles otherwise. Refinement uses it so that a
148/// Steiner point does not re-expand the mesh to the hull.
149pub(crate) fn rebuild(
150    points: &[Point2],
151    constraints: &[Constraint],
152    domain: Option<&[Ring]>,
153) -> Result<Triangulation, TriangulationError> {
154    match domain {
155        None => crate::triangulate(points, constraints),
156        Some(rings) => triangulate_domain(points, constraints, rings),
157    }
158}
159
160/// Range and size of every ring, then the empty ring list.
161pub(crate) fn check_ring_sizes(rings: &[Ring], count: u32) -> Result<(), TriangulationError> {
162    for (r, ring) in rings.iter().enumerate() {
163        let r = r as u32;
164        if let Some(&index) = ring.vertices.iter().find(|&&v| v >= count) {
165            return Err(TriangulationError::RingOutOfRange { ring: r, index });
166        }
167        if ring.vertices.len() < 3 {
168            return Err(TriangulationError::TooFewRingVertices { ring: r });
169        }
170    }
171    if rings.is_empty() {
172        return Err(TriangulationError::EmptyDomain);
173    }
174    Ok(())
175}
176
177/// The ring's vertices as kept vertices.
178fn kept_vertices(ring: &Ring, kept: &[u32]) -> Vec<u32> {
179    ring.vertices.iter().map(|&v| kept[v as usize]).collect()
180}
181
182/// Lexicographic order of two positions; `-0.0` equals `0.0`.
183fn lex(p: Point2, q: Point2) -> core::cmp::Ordering {
184    (p.x, p.y)
185        .partial_cmp(&(q.x, q.y))
186        .unwrap_or(core::cmp::Ordering::Equal)
187}
188
189/// Check every ring's shape and return its edges as constraints between
190/// kept vertices, ring by ring.
191///
192/// A repeated vertex (after merging coincident points) is refused first,
193/// then a fold, per ring. The constraints are appended after the caller's.
194pub(crate) fn check_rings(
195    points: &[Point2],
196    kept: &[u32],
197    rings: &[Ring],
198) -> Result<Vec<Constraint>, TriangulationError> {
199    let at = |k: u32| points[k as usize];
200    let mut stamp = vec![u32::MAX; points.len()];
201    let mut edges = Vec::new();
202    for (r, ring) in rings.iter().enumerate() {
203        let r = r as u32;
204        let seq = kept_vertices(ring, kept);
205        for (i, &k) in seq.iter().enumerate() {
206            if stamp[k as usize] == r {
207                return Err(TriangulationError::RepeatedRingVertex {
208                    ring: r,
209                    index: ring.vertices[i],
210                });
211            }
212            stamp[k as usize] = r;
213        }
214        let m = seq.len();
215        for i in 0..m {
216            let (prev, v, next) = (seq[(i + m - 1) % m], seq[i], seq[(i + 1) % m]);
217            let folds = collinear(at(prev), at(v), at(next))
218                && lex(at(prev), at(v)) == lex(at(next), at(v));
219            if folds {
220                return Err(TriangulationError::FoldedRing {
221                    ring: r,
222                    index: ring.vertices[i],
223                });
224            }
225            edges.push(Constraint::new(v, next));
226        }
227    }
228    Ok(edges)
229}
230
231/// Whether the ring `seq` runs counter-clockwise, read at its lowest vertex.
232///
233/// The lowest vertex by position is a convex corner of any simple ring, and
234/// `check_rings` has already refused a fold there, so the turn is non-zero.
235fn is_ccw(points: &[Point2], seq: &[u32]) -> bool {
236    let m = seq.len();
237    let at = |i: usize| points[seq[i % m] as usize];
238    let lowest = (0..m)
239        .min_by(|&i, &j| lex(at(i), at(j)))
240        .unwrap_or_default();
241    turns_left(at(lowest + m - 1), at(lowest), at(lowest + 1))
242}
243
244/// The net winding contribution of ring pieces, by piece `(min, max)`.
245struct Pieces(Vec<((u32, u32), i32, u32)>);
246
247impl Pieces {
248    /// Net contribution along `u -> v` (crossing the piece from its left to
249    /// its right lowers the winding by it) and the lowest ring carrying it.
250    fn along(&self, u: u32, v: u32) -> (i32, u32) {
251        let key = (u.min(v), u.max(v));
252        match self.0.binary_search_by_key(&key, |e| e.0) {
253            Ok(i) if u < v => (self.0[i].1, self.0[i].2),
254            Ok(i) => (-self.0[i].1, self.0[i].2),
255            Err(_) => (0, 0),
256        }
257    }
258}
259
260/// The ring pieces: each ring edge as the chain it was split into, with the
261/// sign that normalises its ring.
262fn ring_pieces(points: &[Point2], kept: &[u32], splits: &Splits, rings: &[Ring]) -> Pieces {
263    let mut raw: Vec<((u32, u32), i32, u32)> = Vec::new();
264    for (r, ring) in rings.iter().enumerate() {
265        let seq = kept_vertices(ring, kept);
266        let outer = ring.kind == RingKind::Outer;
267        let sign = if is_ccw(points, &seq) == outer { 1 } else { -1 };
268        let m = seq.len();
269        for i in 0..m {
270            let (u, v) = (seq[i], seq[(i + 1) % m]);
271            let key = Constraint::new(u, v);
272            let mut chain: Vec<u32> = splits
273                .chain(key)
274                .map_or_else(|| vec![key.a, key.b], <[u32]>::to_vec);
275            if u != key.a {
276                chain.reverse();
277            }
278            for w in chain.windows(2) {
279                let (p, q) = (w[0], w[1]);
280                if p < q {
281                    raw.push(((p, q), sign, r as u32));
282                } else {
283                    raw.push(((q, p), -sign, r as u32));
284                }
285            }
286        }
287    }
288    raw.sort_unstable();
289    let mut net: Vec<((u32, u32), i32, u32)> = Vec::new();
290    for (key, delta, ring) in raw {
291        match net.last_mut() {
292            Some(last) if last.0 == key => {
293                last.1 += delta;
294                last.2 = last.2.min(ring);
295            }
296            _ => net.push((key, delta, ring)),
297        }
298    }
299    Pieces(net)
300}
301
302/// Keep the triangles with winding 1 and rebuild what depends on them.
303fn classify(
304    tri: &mut Triangulation,
305    kept: &[u32],
306    splits: &Splits,
307    rings: &[Ring],
308) -> Result<(), TriangulationError> {
309    let pieces = ring_pieces(&tri.points, kept, splits, rings);
310    let winding = flood(tri, &pieces)?;
311
312    let mut triangles = Vec::new();
313    for (t, corners) in tri.triangles.as_chunks::<3>().0.iter().enumerate() {
314        if winding[t] == 1 {
315            triangles.extend_from_slice(corners);
316        }
317    }
318    if triangles.is_empty() {
319        return Err(TriangulationError::EmptyDomain);
320    }
321    tri.triangles = triangles;
322    tri.rebuild_halfedges();
323
324    let mut edges: Vec<(u32, u32)> = tri
325        .triangles
326        .as_chunks::<3>()
327        .0
328        .iter()
329        .flat_map(|c| (0..3).map(move |i| (c[i].min(c[(i + 1) % 3]), c[i].max(c[(i + 1) % 3]))))
330        .collect();
331    edges.sort_unstable();
332    edges.dedup();
333    tri.constraints
334        .retain(|c| edges.binary_search(&(c.a, c.b)).is_ok());
335
336    let mut used = vec![false; tri.points.len()];
337    for &v in &tri.triangles {
338        used[v as usize] = true;
339    }
340    let mut merged = vec![false; tri.points.len()];
341    for m in &tri.merged {
342        merged[m.input as usize] = true;
343    }
344    tri.outside = (0..tri.points.len() as u32)
345        .filter(|&v| !used[v as usize] && !merged[v as usize])
346        .collect();
347    Ok(())
348}
349
350/// The winding of every triangle, by a first-in first-out flood fill.
351fn flood(tri: &Triangulation, pieces: &Pieces) -> Result<Vec<i32>, TriangulationError> {
352    use std::collections::VecDeque;
353
354    let edge = |t: usize, i: usize| (tri.triangles[3 * t + i], tri.triangles[3 * t + (i + 1) % 3]);
355    let seed = (0..tri.triangle_count())
356        .find_map(|t| {
357            (0..3)
358                .find(|&i| tri.halfedges[3 * t + i] == NO_HALFEDGE)
359                .map(|i| (t, i))
360        })
361        .ok_or(TriangulationError::EmptyDomain)?;
362
363    // Entering the hull across its edge `u -> v` from outside (its right)
364    // raises the winding by that edge's net contribution.
365    let (u, v) = edge(seed.0, seed.1);
366    let (net, ring) = pieces.along(u, v);
367    check_winding(net, ring)?;
368    let mut winding = vec![i32::MIN; tri.triangle_count()];
369    winding[seed.0] = net;
370    let mut queue = VecDeque::from([seed.0]);
371    while let Some(t) = queue.pop_front() {
372        for i in 0..3 {
373            let twin = tri.halfedges[3 * t + i];
374            let (u, v) = edge(t, i);
375            let (net, ring) = pieces.along(u, v);
376            if twin == NO_HALFEDGE {
377                debug_assert_eq!(winding[t], net, "hull edge disagrees with the flood");
378                continue;
379            }
380            let next = twin as usize / 3;
381            let w = winding[t] - net;
382            if winding[next] == i32::MIN {
383                check_winding(w, ring)?;
384                winding[next] = w;
385                queue.push_back(next);
386            } else {
387                debug_assert_eq!(winding[next], w, "the winding is path dependent");
388            }
389        }
390    }
391    Ok(winding)
392}
393
394/// Refuse a region whose winding is neither outside (0) nor inside (1).
395fn check_winding(winding: i32, ring: u32) -> Result<(), TriangulationError> {
396    if (0..=1).contains(&winding) {
397        Ok(())
398    } else {
399        Err(TriangulationError::MisnestedRing { ring, winding })
400    }
401}
402
403#[cfg(test)]
404mod tests {
405    use super::*;
406    use crate::TriangulationError as E;
407
408    fn p(x: f64, y: f64) -> Point2 {
409        Point2::new(x, y)
410    }
411
412    fn square() -> Vec<Point2> {
413        vec![p(0.0, 0.0), p(4.0, 0.0), p(4.0, 4.0), p(0.0, 4.0)]
414    }
415
416    fn refused(points: &[Point2], rings: &[Ring]) -> E {
417        triangulate_domain(points, &[], rings).unwrap_err()
418    }
419
420    #[test]
421    fn a_ring_out_of_range_is_refused() {
422        assert_eq!(
423            refused(&square(), &[Ring::outer([0, 1, 9])]),
424            E::RingOutOfRange { ring: 0, index: 9 }
425        );
426    }
427
428    #[test]
429    fn a_short_ring_is_refused() {
430        assert_eq!(
431            refused(&square(), &[Ring::outer([0, 1])]),
432            E::TooFewRingVertices { ring: 0 }
433        );
434    }
435
436    #[test]
437    fn closing_a_ring_explicitly_is_a_repeat() {
438        assert_eq!(
439            refused(&square(), &[Ring::outer([0, 1, 2, 3, 0])]),
440            E::RepeatedRingVertex { ring: 0, index: 0 }
441        );
442    }
443
444    #[test]
445    fn a_coincident_index_is_a_repeat() {
446        let mut pts = square();
447        pts.push(p(0.0, 0.0));
448        assert_eq!(
449            refused(&pts, &[Ring::outer([0, 1, 2, 3, 4])]),
450            E::RepeatedRingVertex { ring: 0, index: 4 }
451        );
452    }
453
454    #[test]
455    fn three_collinear_vertices_fold() {
456        let pts = vec![p(0.0, 0.0), p(1.0, 0.0), p(2.0, 0.0), p(0.0, 3.0)];
457        assert_eq!(
458            refused(&pts, &[Ring::outer([0, 1, 2])]),
459            E::FoldedRing { ring: 0, index: 0 }
460        );
461    }
462
463    #[test]
464    fn a_spike_folds() {
465        // A spike out to 4 and back along the same line through 5 folds
466        // at 4.
467        let pts = vec![
468            p(0.0, 0.0),
469            p(4.0, 0.0),
470            p(4.0, 4.0),
471            p(0.0, 4.0),
472            p(6.0, 0.0),
473            p(5.0, 0.0),
474        ];
475        assert_eq!(
476            refused(&pts, &[Ring::outer([0, 1, 4, 5, 2, 3])]),
477            E::FoldedRing { ring: 0, index: 4 }
478        );
479    }
480
481    #[test]
482    fn a_straight_vertex_is_accepted() {
483        let mut pts = square();
484        pts.push(p(2.0, 0.0));
485        let tri = triangulate_domain(&pts, &[], &[Ring::outer([0, 4, 1, 2, 3])]).unwrap();
486        assert_eq!(tri.triangle_count(), 3);
487        assert!(tri.outside_points().is_empty());
488    }
489
490    #[test]
491    fn orientation_is_read_at_a_reflex_first_vertex() {
492        // A reflex first vertex: the orientation is read at the lowest
493        // vertex (0, 0), a convex corner, whichever way the ring is listed.
494        let pts = vec![
495            p(2.0, 1.0),
496            p(4.0, 0.0),
497            p(4.0, 4.0),
498            p(0.0, 4.0),
499            p(0.0, 0.0),
500        ];
501        assert!(is_ccw(&pts, &[0, 1, 2, 3, 4]));
502        assert!(!is_ccw(&pts, &[4, 3, 2, 1, 0]));
503        let a = triangulate_domain(&pts, &[], &[Ring::outer([0, 1, 2, 3, 4])]).unwrap();
504        let b = triangulate_domain(&pts, &[], &[Ring::outer([4, 3, 2, 1, 0])]).unwrap();
505        assert_eq!(a.triangle_count(), 3);
506        assert_eq!(b.triangle_count(), 3);
507    }
508
509    #[test]
510    fn an_empty_ring_list_is_an_empty_domain() {
511        assert_eq!(refused(&square(), &[]), E::EmptyDomain);
512    }
513}