1use 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
36pub 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 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 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
157fn 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
173struct Tiles {
175 lo: Q,
176 size: i64,
177 columns: i64,
178}
179
180const 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 #[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}