axiolid_route/
skeleton.rs

1//! The skeleton of a region with holes: its corridors as a graph, with
2//! path ends, junctions and the clearance at each (#139).
3//!
4//! # Approximate where it must be, certified where it can be
5//!
6//! The medial axis of a polygon with holes has parabolic arcs and
7//! algebraic vertices. What a circulation check needs from it is its shape
8//! as a graph -- where paths end, where they meet -- and how much room
9//! there is along it. So the shape is approximated and the room is proven:
10//!
11//! - The boundary is sampled no coarser than `spacing` and triangulated
12//!   with its edges as constraints. Each triangle inside the region gives
13//!   a node at its circumcentre -- the centre of an empty circle touching
14//!   three boundary samples, a Voronoi vertex of the samples -- joined to
15//!   the nodes of the triangles it shares an unconstrained edge with. As
16//!   the spacing shrinks these converge to the medial axis; how close they
17//!   are is not proven, and nothing below depends on it. Nodes the
18//!   rounding or a constrained triangle puts outside the region are
19//!   dropped.
20//! - Spurs -- branches into convex corners, which every corner has -- are
21//!   pruned as in the lambda-medial axis: a node stays when the feet on its
22//!   nearest walls lie at least `prune` times its clearance apart. Across
23//!   a corridor they are two clearances apart; into a right-angled corner
24//!   only the square root of two. So 1.5 keeps corridors, junctions and
25//!   dead ends and drops the spurs into right-angled and sharper corners;
26//!   leaf branches then left shorter than the clearance where they join
27//!   go too. What remains ends where corridors and rooms end. The spacing
28//!   should be under an eighth of the narrowest width.
29//! - Every node is decided to lie in the region exactly, and its
30//!   [`SkeletonNode::clearance`] -- the distance to the nearest wall -- is
31//!   an interval proven to contain the true value (outward-rounded
32//!   point-to-segment distances). A path end also names the wall ahead of
33//!   it: the first wall the path, continued straight on, runs into,
34//!   decided by exact crossing tests.
35
36use axiolid_contracts::Sign;
37use axiolid_core::Point2;
38use axiolid_overlay::Polygon;
39use axiolid_triangulate::{triangulate, Constraint};
40use std::collections::HashMap;
41
42use crate::{
43    contains, crosses, dedup_points, ring_edges, side, validate_region, within, RouteError,
44};
45
46/// What a skeleton node is.
47#[derive(Debug, Clone, Copy, PartialEq, Eq)]
48#[non_exhaustive]
49pub enum NodeKind {
50    /// Where a path ends: one neighbour.
51    End,
52    /// Along a path: two neighbours.
53    Path,
54    /// Where paths meet: three or more.
55    Junction,
56    /// No neighbours: a region too small to hold a path.
57    Isolated,
58}
59
60/// A wall edge: which polygon, which ring (0 the outer, `k` the `k`-th
61/// hole) and which edge of it (from point `edge` to the next).
62#[derive(Debug, Clone, Copy, PartialEq, Eq)]
63pub struct Wall {
64    /// Polygon index in the region.
65    pub polygon: usize,
66    /// Ring: 0 for the outer ring, `k` for hole `k - 1`.
67    pub ring: usize,
68    /// Edge index within the ring.
69    pub edge: usize,
70}
71
72/// One node of the skeleton.
73#[derive(Debug, Clone, Copy, PartialEq)]
74#[non_exhaustive]
75pub struct SkeletonNode {
76    /// Where it is; inside the region, decided exactly.
77    pub point: Point2,
78    /// Its kind, by its number of neighbours.
79    pub kind: NodeKind,
80    /// Proven bounds `(lower, upper)` on the distance to the nearest wall.
81    pub clearance: (f64, f64),
82    /// For an end: the wall the path runs into if continued straight on.
83    pub ahead: Option<Wall>,
84}
85
86/// The skeleton: nodes and the edges joining them.
87#[derive(Debug, Clone, PartialEq)]
88#[non_exhaustive]
89pub struct Skeleton {
90    /// The nodes.
91    pub nodes: Vec<SkeletonNode>,
92    /// Edges as node index pairs, lower first, sorted.
93    pub edges: Vec<(usize, usize)>,
94    /// The boundary sample spacing used.
95    pub spacing: f64,
96}
97
98impl Skeleton {
99    /// Indices of the path ends.
100    #[must_use]
101    pub fn ends(&self) -> Vec<usize> {
102        self.kinds(NodeKind::End)
103    }
104
105    /// Indices of the junctions.
106    #[must_use]
107    pub fn junctions(&self) -> Vec<usize> {
108        self.kinds(NodeKind::Junction)
109    }
110
111    fn kinds(&self, kind: NodeKind) -> Vec<usize> {
112        (0..self.nodes.len())
113            .filter(|&i| self.nodes[i].kind == kind)
114            .collect()
115    }
116}
117
118/// Why no skeleton was built.
119#[derive(Debug, Clone, Copy, PartialEq)]
120#[non_exhaustive]
121pub enum SkeletonError {
122    /// The region is malformed, or a predicate was undecidable.
123    Route(RouteError),
124    /// The spacing or prune factor is not positive and finite.
125    InvalidParameter,
126    /// The boundary could not be triangulated: rings cross.
127    Triangulation,
128}
129
130impl From<RouteError> for SkeletonError {
131    fn from(error: RouteError) -> Self {
132        Self::Route(error)
133    }
134}
135
136/// The skeleton of `region`, its boundary sampled at most `spacing` apart,
137/// keeping the nodes whose nearest walls spread at least `prune` times
138/// their clearance apart (1.5 drops the spurs into right-angled corners; 0
139/// keeps every node).
140///
141/// # Errors
142///
143/// [`SkeletonError`] for a malformed region or parameters.
144pub fn skeleton(region: &[Polygon], spacing: f64, prune: f64) -> Result<Skeleton, SkeletonError> {
145    if !(spacing.is_finite() && spacing > 0.0 && prune.is_finite() && prune >= 0.0) {
146        return Err(SkeletonError::InvalidParameter);
147    }
148    validate_region(region, &[])?;
149    // Walls, and the boundary sampled along them.
150    let mut walls: Vec<(Wall, Point2, Point2)> = Vec::new();
151    for (pi, polygon) in region.iter().enumerate() {
152        for (ri, ring) in std::iter::once(&polygon.outer)
153            .chain(&polygon.holes)
154            .enumerate()
155        {
156            for (ei, (a, b)) in ring_edges(ring).into_iter().enumerate() {
157                if a != b {
158                    let wall = Wall {
159                        polygon: pi,
160                        ring: ri,
161                        edge: ei,
162                    };
163                    walls.push((wall, a, b));
164                }
165            }
166        }
167    }
168    let mut points: Vec<Point2> = Vec::new();
169    let mut pieces: Vec<(Point2, Point2)> = Vec::new();
170    for &(_, a, b) in &walls {
171        let count = ((b - a).length() / spacing).ceil().max(1.0) as usize;
172        let mut prev = a;
173        for k in 1..=count {
174            let q = if k == count {
175                b
176            } else {
177                a + (b - a) * (k as f64 / count as f64)
178            };
179            pieces.push((prev, q));
180            points.push(prev);
181            prev = q;
182        }
183    }
184    dedup_points(&mut points);
185    let mut index: HashMap<(u64, u64), u32> = HashMap::new();
186    for (i, p) in points.iter().enumerate() {
187        index.insert((p.x.to_bits(), p.y.to_bits()), i as u32);
188    }
189    let id = |p: Point2| index[&(p.x.to_bits(), p.y.to_bits())];
190    let mut constraints: Vec<Constraint> = pieces
191        .iter()
192        .map(|&(p, q)| Constraint::new(id(p), id(q)))
193        .collect();
194    constraints.sort_unstable();
195    constraints.dedup();
196    let tri = triangulate(&points, &constraints).map_err(|_| SkeletonError::Triangulation)?;
197    let at = tri.points();
198    let fixed: std::collections::BTreeSet<Constraint> = tri.constraints().iter().copied().collect();
199    // Triangles inside the region.
200    let mut inside: Vec<[u32; 3]> = Vec::new();
201    for t in tri.triangles().chunks_exact(3) {
202        let (a, b, c) = (at[t[0] as usize], at[t[1] as usize], at[t[2] as usize]);
203        let centroid = Point2::new((a.x + b.x + c.x) / 3.0, (a.y + b.y + c.y) / 3.0);
204        if contains(region, centroid)? {
205            inside.push([t[0], t[1], t[2]]);
206        }
207    }
208    // Voronoi dual: a node per inside triangle at its circumcentre (the
209    // centre of the empty circle through three boundary samples), joined
210    // to the triangles it shares an unconstrained edge with.
211    let mut nodes: Vec<Point2> = Vec::with_capacity(inside.len());
212    let mut by_edge: HashMap<(u32, u32), usize> = HashMap::new();
213    let mut links: Vec<(usize, usize)> = Vec::new();
214    for (k, t) in inside.iter().enumerate() {
215        let (a, b, c) = (at[t[0] as usize], at[t[1] as usize], at[t[2] as usize]);
216        nodes.push(circumcentre(a, b, c));
217        for e in 0..3 {
218            let (u, v) = (t[e], t[(e + 1) % 3]);
219            if fixed.contains(&Constraint::new(u, v)) {
220                continue;
221            }
222            match by_edge.entry((u.min(v), u.max(v))) {
223                std::collections::hash_map::Entry::Occupied(o) => links.push((*o.get(), k)),
224                std::collections::hash_map::Entry::Vacant(slot) => {
225                    slot.insert(k);
226                }
227            }
228        }
229    }
230    let segments: Vec<(Point2, Point2)> = walls.iter().map(|&(_, a, b)| (a, b)).collect();
231    let clearance: Vec<(f64, f64)> = nodes.iter().map(|&p| clearance_of(p, &segments)).collect();
232    // Adjacency, then prune spurs.
233    let mut adj: Vec<Vec<usize>> = vec![Vec::new(); nodes.len()];
234    for &(a, b) in &links {
235        if a != b && !adj[a].contains(&b) {
236            adj[a].push(b);
237            adj[b].push(a);
238        }
239    }
240    // Keep the nodes the walls around them spread wide enough about: the
241    // feet on the walls nearest a node (within its clearance, and a slack
242    // for the node being off the true axis) at least `prune` times its
243    // clearance apart.
244    let alive: Vec<bool> = (0..nodes.len())
245        .map(|i| {
246            let c = clearance[i].1;
247            // A chordal node is off the true axis by a fraction of its
248            // clearance (for a spacing under an eighth of the narrowest
249            // width); a quarter allows for it.
250            spread(nodes[i], c, 0.25 * c, &segments) >= prune * c
251        })
252        .collect();
253    for (i, list) in adj.iter_mut().enumerate() {
254        if !alive[i] {
255            list.clear();
256        } else {
257            list.retain(|&j| alive[j]);
258        }
259    }
260    // What the filter leaves of a spur is short: drop leaf branches
261    // shorter than the clearance where they join a junction.
262    let mut alive = alive;
263    loop {
264        let mut changed = false;
265        for end in 0..nodes.len() {
266            if !alive[end] || adj[end].len() != 1 {
267                continue;
268            }
269            let mut path = vec![end];
270            let mut length = 0.0;
271            let (mut prev, mut here) = (end, adj[end][0]);
272            while adj[here].len() == 2 {
273                length += (nodes[here] - nodes[prev]).length();
274                path.push(here);
275                let next = if adj[here][0] == prev {
276                    adj[here][1]
277                } else {
278                    adj[here][0]
279                };
280                prev = here;
281                here = next;
282            }
283            length += (nodes[here] - nodes[prev]).length();
284            if adj[here].len() >= 3 && length < clearance[here].1 {
285                for &n in &path {
286                    alive[n] = false;
287                    for o in std::mem::take(&mut adj[n]) {
288                        adj[o].retain(|&x| x != n);
289                    }
290                }
291                changed = true;
292            }
293        }
294        if !changed {
295            break;
296        }
297    }
298    // Stray single nodes go, unless nothing else is left.
299    if alive.iter().zip(&adj).any(|(&a, l)| a && !l.is_empty()) {
300        for i in 0..nodes.len() {
301            if adj[i].is_empty() {
302                alive[i] = false;
303            }
304        }
305    }
306    // Renumber the survivors.
307    let mut renumber = vec![usize::MAX; nodes.len()];
308    let mut out_nodes = Vec::new();
309    for i in 0..nodes.len() {
310        if alive[i] && inside_exactly(region, nodes[i])? {
311            renumber[i] = out_nodes.len();
312            out_nodes.push(i);
313        }
314    }
315    let mut edges: Vec<(usize, usize)> = Vec::new();
316    for (i, list) in adj.iter().enumerate() {
317        for &j in list {
318            let (a, b) = (renumber[i], renumber[j]);
319            if i < j && a != usize::MAX && b != usize::MAX {
320                edges.push((a.min(b), a.max(b)));
321            }
322        }
323    }
324    edges.sort_unstable();
325    edges.dedup();
326    let mut degree = vec![0usize; out_nodes.len()];
327    for &(a, b) in &edges {
328        degree[a] += 1;
329        degree[b] += 1;
330    }
331    let mut result = Vec::with_capacity(out_nodes.len());
332    for (k, &i) in out_nodes.iter().enumerate() {
333        let kind = match degree[k] {
334            0 => NodeKind::Isolated,
335            1 => NodeKind::End,
336            2 => NodeKind::Path,
337            _ => NodeKind::Junction,
338        };
339        let ahead = if kind == NodeKind::End {
340            // The path's direction into the end: back along it until the
341            // nodes are half a clearance apart (cocircular samples put
342            // neighbouring nodes on one point).
343            let here = nodes[i];
344            let (mut prev, mut at_node) = (usize::MAX, k);
345            let mut from = None;
346            for _ in 0..out_nodes.len() {
347                let next = edges.iter().find_map(|&(a, b)| {
348                    let o = if a == at_node {
349                        b
350                    } else if b == at_node {
351                        a
352                    } else {
353                        return None;
354                    };
355                    (o != prev).then_some(o)
356                });
357                let Some(next) = next else { break };
358                let p = nodes[out_nodes[next]];
359                if (p - here).length() >= 0.5 * clearance[i].0 {
360                    from = Some(p);
361                    break;
362                }
363                prev = at_node;
364                at_node = next;
365            }
366            match from {
367                Some(from) => wall_ahead(from, here, &walls)?,
368                None => None,
369            }
370        } else {
371            None
372        };
373        result.push(SkeletonNode {
374            point: nodes[i],
375            kind,
376            clearance: clearance[i],
377            ahead,
378        });
379    }
380    Ok(Skeleton {
381        nodes: result,
382        edges,
383        spacing,
384    })
385}
386
387/// The centre of the circle through three points (rounded).
388fn circumcentre(a: Point2, b: Point2, c: Point2) -> Point2 {
389    let (u, v) = (b - a, c - a);
390    let d = 2.0 * u.perp_dot(v);
391    let (uu, vv) = (u.dot(u), v.dot(v));
392    a + Point2::new(v.y * uu - u.y * vv, u.x * vv - v.x * uu) / d
393}
394
395/// In some polygon of the region, closed, decided exactly.
396fn inside_exactly(region: &[Polygon], p: Point2) -> Result<bool, RouteError> {
397    for polygon in region {
398        if crate::map::in_polygon(polygon, p)? {
399            return Ok(true);
400        }
401    }
402    Ok(false)
403}
404
405/// The first wall the ray from `from` through `to`, beyond `to`, crosses,
406/// by exact tests against a far point along it.
407fn wall_ahead(
408    from: Point2,
409    to: Point2,
410    walls: &[(Wall, Point2, Point2)],
411) -> Result<Option<Wall>, RouteError> {
412    let d = to - from;
413    if d.length() == 0.0 {
414        return Ok(None);
415    }
416    let reach = walls
417        .iter()
418        .map(|&(_, a, b)| (a - to).length().max((b - to).length()))
419        .fold(0.0, f64::max);
420    let far = to + d * (4.0 * reach / d.length() + 1.0);
421    let mut best: Option<(f64, Wall)> = None;
422    for &(wall, a, b) in walls {
423        // Meets the segment from `to` to `far`, crossing or touching.
424        let hit = crosses(to, far, a, b)?
425            || (side(to, far, a)? == Sign::Zero && within(to, far, a))
426            || (side(to, far, b)? == Sign::Zero && within(to, far, b));
427        if !hit {
428            continue;
429        }
430        // Distance along the ray to the wall's line (rounded; only the
431        // order of nearby walls could be affected, and then either is a
432        // wall the path runs into).
433        let e = b - a;
434        let den = d.perp_dot(e);
435        let t = if den == 0.0 {
436            (a - to).length()
437        } else {
438            (a - to).perp_dot(e) / den
439        };
440        if best.is_none_or(|(bt, _)| t < bt) {
441            best = Some((t, wall));
442        }
443    }
444    Ok(best.map(|(_, w)| w))
445}
446
447/// How far apart the feet of `p` on its nearest walls lie: the walls
448/// within `clearance + slack` of it, each at its nearest point.
449fn spread(p: Point2, clearance: f64, slack: f64, segments: &[(Point2, Point2)]) -> f64 {
450    let feet: Vec<Point2> = segments
451        .iter()
452        .filter_map(|&(a, b)| {
453            let e = b - a;
454            let t = ((p - a).dot(e) / e.dot(e)).clamp(0.0, 1.0);
455            let foot = a + e * t;
456            ((p - foot).length() <= clearance + slack).then_some(foot)
457        })
458        .collect();
459    let mut widest = 0.0f64;
460    for (i, a) in feet.iter().enumerate() {
461        for b in &feet[i + 1..] {
462            widest = widest.max((*a - *b).length());
463        }
464    }
465    widest
466}
467
468/// Proven bounds on the distance from `p` to the nearest of `segments`.
469fn clearance_of(p: Point2, segments: &[(Point2, Point2)]) -> (f64, f64) {
470    let mut low = f64::INFINITY;
471    let mut high = f64::INFINITY;
472    for &(a, b) in segments {
473        let (lo, hi) = segment_distance(p, a, b);
474        low = low.min(lo);
475        high = high.min(hi);
476    }
477    (low, high)
478}
479
480/// Bounds on the distance from `p` to the segment `a b`: the squared
481/// distance in outward-rounded intervals, square-rooted outward.
482fn segment_distance(p: Point2, a: Point2, b: Point2) -> (f64, f64) {
483    let iv = |x: f64| Iv { lo: x, hi: x };
484    let (ex, ey) = (iv(b.x).sub(iv(a.x)), iv(b.y).sub(iv(a.y)));
485    let (wx, wy) = (iv(p.x).sub(iv(a.x)), iv(p.y).sub(iv(a.y)));
486    let dot = ex.mul(wx).add(ey.mul(wy));
487    let len2 = ex.mul(ex).add(ey.mul(ey));
488    let w2 = wx.mul(wx).add(wy.mul(wy));
489    let (vx, vy) = (iv(p.x).sub(iv(b.x)), iv(p.y).sub(iv(b.y)));
490    let v2 = vx.mul(vx).add(vy.mul(vy));
491    // Which part of the segment is nearest, when decided; otherwise the
492    // hull of the candidates.
493    let perp = || {
494        let c = ex.mul(wy).sub(ey.mul(wx));
495        c.mul(c).div(len2)
496    };
497    let d2 = if dot.hi <= 0.0 {
498        w2
499    } else if dot.lo >= len2.hi {
500        v2
501    } else if dot.lo > 0.0 && dot.hi < len2.lo {
502        perp()
503    } else {
504        let q = perp();
505        Iv {
506            lo: q.lo.min(w2.lo).min(v2.lo),
507            hi: q.hi.max(w2.hi).max(v2.hi),
508        }
509    };
510    let lo = d2.lo.max(0.0).sqrt().next_down().max(0.0);
511    let hi = d2.hi.max(0.0).sqrt().next_up();
512    (lo, hi)
513}
514
515/// An outward-rounded interval.
516#[derive(Debug, Clone, Copy)]
517struct Iv {
518    lo: f64,
519    hi: f64,
520}
521
522impl Iv {
523    fn outward(lo: f64, hi: f64) -> Self {
524        Self {
525            lo: lo.next_down(),
526            hi: hi.next_up(),
527        }
528    }
529
530    fn add(self, o: Self) -> Self {
531        Self::outward(self.lo + o.lo, self.hi + o.hi)
532    }
533
534    fn sub(self, o: Self) -> Self {
535        Self::outward(self.lo - o.hi, self.hi - o.lo)
536    }
537
538    fn mul(self, o: Self) -> Self {
539        let p = [
540            self.lo * o.lo,
541            self.lo * o.hi,
542            self.hi * o.lo,
543            self.hi * o.hi,
544        ];
545        Self::outward(
546            p.iter().copied().fold(f64::INFINITY, f64::min),
547            p.iter().copied().fold(f64::NEG_INFINITY, f64::max),
548        )
549    }
550
551    /// Quotient by a positive interval.
552    fn div(self, o: Self) -> Self {
553        if o.lo <= 0.0 {
554            return Self {
555                lo: 0.0,
556                hi: f64::INFINITY,
557            };
558        }
559        let q = [
560            self.lo / o.lo,
561            self.lo / o.hi,
562            self.hi / o.lo,
563            self.hi / o.hi,
564        ];
565        Self::outward(
566            q.iter().copied().fold(f64::INFINITY, f64::min),
567            q.iter().copied().fold(f64::NEG_INFINITY, f64::max),
568        )
569    }
570}