Skip to main content

ogeom_mesh/
shadow.rs

1//! The shadow a triangle mesh casts on a plane: the outline of everything
2//! it covers seen along the plane's normal, with its holes.
3//!
4//! The shadow is the union of the triangles projected onto the plane. A
5//! pocket or a dent in a face seen from above is covered by the face
6//! around it and leaves no mark; an indentation in the outline itself
7//! stays; a hole through the part, which no triangle covers, is a hole.
8//! A triangle seen edge on covers no area and adds nothing, so a sheet
9//! seen edge on casts no shadow.
10//!
11//! The vertices are projected and snapped to the grid of
12//! [`planar`](crate::planar) once each, and every triangle's orientation is
13//! read from those grid points exactly. Each triangle, turned to run
14//! counter-clockwise, adds its three edges to a chain, and the edges two
15//! triangles share in opposite directions cancel: those are the edges with
16//! a triangle either side of them in the projection, which bound nothing.
17//! What is left is the outline the mesh shows seen along the normal, its
18//! folds and its open edges, and only that goes on to the region tracing:
19//! the points covered by at least one triangle.
20//!
21//! The tracing runs in two rounds. The triangles are dealt to square tiles
22//! of some thousands each by their first corner, and each tile's union is
23//! traced on its own, in parallel: the folds a noisy scan has all over its
24//! faces are covered within their tile and go no further. The tiles'
25//! outlines are then traced together, the edges two tiles share
26//! cancelling as before.
27
28use ogeom_core::collections::FastMap;
29use ogeom_core::parallel::map_ordered;
30use ogeom_core::{OgeomResult, ogeom_bail};
31use ogeom_math::{Plane, Point2};
32use ogeom_topo::Triangulation;
33
34use crate::planar::{Grid, Loop2, Q, Seg, estimate, orient, slot, trace, whole};
35
36/// The shadow of a mesh on a plane, seen along its normal.
37///
38/// The loops are in the plane's own coordinates, along its frame's `x` and
39/// `y`, with the shadow on their left: outer boundaries counter-clockwise,
40/// holes clockwise. Each round of tracing moves the boundary by at most a
41/// cell of the snapping grid, `2^-38` of the projected mesh's extent
42/// rounded up to a power of two: within two cells of the exact union of the
43/// projected triangles, as the projection rounds them.
44///
45/// # Errors
46///
47/// [`OgeomError::Range`](ogeom_core::OgeomError::Range) if a triangle names
48/// a vertex the mesh does not have, and
49/// [`OgeomError::Numeric`](ogeom_core::OgeomError::Numeric) if a position or
50/// the plane's origin is not finite.
51pub fn shadow(mesh: &Triangulation, plane: &Plane) -> OgeomResult<Vec<Loop2>> {
52    if !plane.origin().is_finite() {
53        ogeom_bail!(Numeric, "the shadow plane's origin is not finite");
54    }
55    if let Some(k) = mesh.positions.iter().position(|p| !p.is_finite()) {
56        ogeom_bail!(Numeric, "mesh vertex {k} is not finite");
57    }
58    let count = mesh.positions.len();
59    if let Some(&k) = mesh
60        .triangles
61        .iter()
62        .flatten()
63        .find(|&&k| k as usize >= count)
64    {
65        ogeom_bail!(Range, "a triangle names vertex {k} of {count}");
66    }
67    let frame = plane.frame();
68    let flat: Vec<Point2> = mesh
69        .positions
70        .iter()
71        .map(|&p| frame.to_local(p).xy())
72        .collect();
73    let Some(grid) = Grid::around(flat.iter().copied()) else {
74        return Ok(Vec::new());
75    };
76
77    // One index per grid point, so vertices projecting together are one.
78    let mut index: FastMap<Q, u32> = FastMap::default();
79    let mut points: Vec<Q> = Vec::new();
80    let id: Vec<u32> = flat
81        .iter()
82        .map(|&p| {
83            let q = grid.snap(p);
84            *index.entry(q).or_insert_with(|| {
85                points.push(q);
86                slot((points.len() - 1) as i64)
87            })
88        })
89        .collect();
90
91    // Every edge of every triangle seen counter-clockwise, keyed by the
92    // tile the triangle falls in and its ends smaller first, with +1
93    // running that way and -1 the other.
94    let tiles = Tiles::over(&points, mesh.triangles.len());
95    let mut edges: Vec<(u32, u64, i32)> = Vec::with_capacity(mesh.triangles.len() * 3);
96    for t in &mesh.triangles {
97        let [a, b, c] = t.map(|k| id[k as usize]);
98        let turn = orient(points[a as usize], points[b as usize], points[c as usize]);
99        if turn == 0 {
100            continue;
101        }
102        let ring = if turn > 0 { [a, b, c] } else { [a, c, b] };
103        let tile = tiles.of(points[a as usize]);
104        for k in 0..3 {
105            let (u, v) = (ring[k], ring[(k + 1) % 3]);
106            let (lo, hi, w) = if u < v { (u, v, 1) } else { (v, u, -1) };
107            edges.push((tile, (u64::from(lo) << 32) | u64::from(hi), w));
108        }
109    }
110    edges.sort_unstable_by_key(|e| (e.0, e.1));
111    let mut ranges = Vec::new();
112    let mut k = 0;
113    while k < edges.len() {
114        let from = k;
115        while k < edges.len() && edges[k].0 == edges[from].0 {
116            k += 1;
117        }
118        ranges.push(from..k);
119    }
120    let covered = |w: [i64; 2]| w[0] > 0;
121    let per_tile = map_ordered(&ranges, |_, range| {
122        let mut segments = Vec::new();
123        let here = &edges[range.clone()];
124        let mut k = 0;
125        while k < here.len() {
126            let key = here[k].1;
127            let mut w = 0;
128            while k < here.len() && here[k].1 == key {
129                w += here[k].2;
130                k += 1;
131            }
132            if w != 0 {
133                segments.push(Seg {
134                    a: points[(key >> 32) as usize],
135                    b: points[(key & 0xffff_ffff) as usize],
136                    w: [w, 0],
137                });
138            }
139        }
140        if ranges.len() == 1 {
141            return Ok(segments);
142        }
143        Ok(rings(trace(segments, &covered)?))
144    });
145    let mut segments = Vec::new();
146    for tile in per_tile {
147        segments.extend(tile?);
148    }
149    Ok(trace(segments, &covered)?
150        .into_iter()
151        .map(|ring| Loop2 {
152            points: ring.into_iter().map(|q| grid.point(q)).collect(),
153        })
154        .collect())
155}
156
157/// Loops as a chain, each edge counted once.
158fn rings(loops: Vec<Vec<Q>>) -> Vec<Seg> {
159    let mut out = Vec::new();
160    for ring in loops {
161        let n = ring.len();
162        for k in 0..n {
163            out.push(Seg {
164                a: ring[k],
165                b: ring[(k + 1) % n],
166                w: [1, 0],
167            });
168        }
169    }
170    out
171}
172
173/// Square tiles over the grid, holding some thousands of triangles each.
174struct Tiles {
175    lo: Q,
176    size: i64,
177    columns: i64,
178}
179
180/// About how many triangles a tile holds.
181const PER_TILE: usize = 4096;
182
183impl Tiles {
184    fn over(points: &[Q], triangles: usize) -> Self {
185        let mut lo = [i64::MAX; 2];
186        let mut hi = [i64::MIN; 2];
187        for p in points {
188            lo = [lo[0].min(p[0]), lo[1].min(p[1])];
189            hi = [hi[0].max(p[0]), hi[1].max(p[1])];
190        }
191        let per_side = whole(estimate(triangles / PER_TILE).sqrt().ceil()).max(1);
192        let span = (hi[0] - lo[0]).max(hi[1] - lo[1]) + 1;
193        let size = (span + per_side - 1) / per_side;
194        Self {
195            lo,
196            size,
197            columns: (hi[0] - lo[0]) / size + 1,
198        }
199    }
200
201    fn of(&self, p: Q) -> u32 {
202        let (c, r) = (
203            (p[0] - self.lo[0]) / self.size,
204            (p[1] - self.lo[1]) / self.size,
205        );
206        slot(r * self.columns + c)
207    }
208}
209
210#[cfg(test)]
211mod tests {
212    #![allow(clippy::unwrap_used, reason = "test code")]
213    use super::*;
214    use ogeom_math::Point;
215
216    /// A unit cube seen from above casts a unit square, whatever the
217    /// winding of its triangles.
218    #[test]
219    fn a_cube_casts_its_square() {
220        let mut mesh = Triangulation::new();
221        for z in [0.0, 1.0] {
222            for (x, y) in [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0)] {
223                mesh.positions.push(Point::new(x, y, z));
224            }
225        }
226        mesh.triangles = vec![
227            [0, 2, 1],
228            [0, 3, 2],
229            [4, 5, 6],
230            [4, 6, 7],
231            [0, 1, 5],
232            [0, 5, 4],
233            [1, 2, 6],
234            [1, 6, 5],
235            [2, 3, 7],
236            [2, 7, 6],
237            [3, 0, 4],
238            [3, 4, 7],
239        ];
240        let loops = shadow(&mesh, &Plane::XY).unwrap();
241        assert_eq!(loops.len(), 1);
242        assert_eq!(loops[0].points.len(), 4);
243        assert!((loops[0].area() - 1.0).abs() < 1e-12);
244    }
245}