1use axiolid_core::{Point3, Tolerance};
36use axiolid_exact::{certify, Arith, Dyadic, SignExpr};
37use axiolid_guarantees::Sign;
38use axiolid_mesh::{audit_mesh, TriangleMeshView};
39
40pub const MAX_SIGHT_CELLS: usize = 50_000;
42
43#[derive(Debug, Clone, PartialEq)]
45#[non_exhaustive]
46pub enum Sight {
47 Visible {
49 through: Point3,
51 triangle: usize,
54 },
55 Hidden {
57 occluders: Vec<usize>,
59 },
60 Undecided,
62}
63
64#[derive(Debug, Clone, Copy, PartialEq, Eq)]
66#[non_exhaustive]
67pub enum SightError {
68 NonFinite,
70 EmptyTarget,
72}
73
74pub fn line_of_sight<T, B>(eye: Point3, target: &T, blockers: &[&B]) -> Result<Sight, SightError>
80where
81 T: TriangleMeshView + ?Sized,
82 B: TriangleMeshView + ?Sized,
83{
84 line_of_sight_within(eye, target, blockers, MAX_SIGHT_CELLS)
85}
86
87pub fn line_of_sight_within<T, B>(
93 eye: Point3,
94 target: &T,
95 blockers: &[&B],
96 budget: usize,
97) -> Result<Sight, SightError>
98where
99 T: TriangleMeshView + ?Sized,
100 B: TriangleMeshView + ?Sized,
101{
102 if !eye.is_finite() {
103 return Err(SightError::NonFinite);
104 }
105 let targets = triangles(target)?;
106 if targets.is_empty() {
107 return Err(SightError::EmptyTarget);
108 }
109 let mut walls: Vec<[Point3; 3]> = Vec::new();
110 let mut pieces: Vec<Piece> = Vec::new();
111 for (index, mesh) in blockers.iter().enumerate() {
112 let own = triangles(*mesh)?;
113 pieces.extend(pieces_of(eye, index, &own, *mesh));
114 walls.extend(own);
115 }
116 if let Some(sight) = witness(eye, &targets, &walls) {
117 return Ok(sight);
118 }
119 Ok(hidden(eye, &targets, &pieces, budget)
120 .map_or(Sight::Undecided, |occluders| Sight::Hidden { occluders }))
121}
122
123fn triangles<M: TriangleMeshView + ?Sized>(mesh: &M) -> Result<Vec<[Point3; 3]>, SightError> {
124 let mut out = Vec::with_capacity(mesh.triangle_count());
125 for t in 0..mesh.triangle_count() {
126 let corners = mesh.triangle(t).map(|i| mesh.position(i as usize));
127 if !corners.iter().all(|p| p.is_finite()) {
128 return Err(SightError::NonFinite);
129 }
130 let [a, b, c] = corners;
131 if !collinear(a, b, c) {
132 out.push(corners);
133 }
134 }
135 Ok(out)
136}
137
138fn collinear(a: Point3, b: Point3, c: Point3) -> bool {
140 let d = |p: Point3| [exact(p.x), exact(p.y), exact(p.z)];
141 let (a, b, c) = (d(a), d(b), d(c));
142 let u = sub(&b, &a);
143 let v = sub(&c, &a);
144 let n = cross(&u, &v);
145 n.iter().all(|x| x.sign() == Some(Sign::Zero))
146}
147
148fn witness(eye: Point3, targets: &[[Point3; 3]], walls: &[[Point3; 3]]) -> Option<Sight> {
155 let mut weights: Vec<[f64; 3]> = vec![[1.0 / 3.0; 3]];
157 let n = 6;
158 for i in 1..n {
159 for j in 1..n - i {
160 let (u, v) = (i as f64 / n as f64, j as f64 / n as f64);
161 weights.push([u, v, 1.0 - u - v]);
162 }
163 }
164 for (index, t) in targets.iter().enumerate() {
165 for w in &weights {
166 let through = t[0] * w[0] + t[1] * w[1] + t[2] * w[2];
167 if through == eye {
168 continue;
169 }
170 if let Some(near) = strict_hit(eye, through, t) {
171 if walls.iter().all(|wall| !blocks(eye, through, wall, &near)) {
172 return Some(Sight::Visible {
173 through,
174 triangle: index,
175 });
176 }
177 }
178 }
179 }
180 None
181}
182
183#[derive(Debug, Clone)]
187struct Param {
188 num: Dyadic,
189 den: Dyadic,
190}
191
192impl Param {
193 fn of(eye: Point3, p: Point3, t: &[Point3; 3]) -> Option<Self> {
194 let oe = orient(&t.map(dpoint), &dpoint(eye));
195 let op = orient(&t.map(dpoint), &dpoint(p));
196 let den = oe.sub(&op);
197 if den.sign() == Some(Sign::Zero) {
198 return None;
199 }
200 Some(Self { num: oe, den })
201 }
202
203 fn sign(&self) -> Sign {
204 times(
205 self.num.sign().unwrap_or(Sign::Zero),
206 self.den.sign().unwrap_or(Sign::Zero),
207 )
208 }
209
210 fn compare(&self, other: &Self) -> Sign {
212 let gap = self.num.mul(&other.den).sub(&other.num.mul(&self.den));
213 let d = times(
214 self.den.sign().unwrap_or(Sign::Zero),
215 other.den.sign().unwrap_or(Sign::Zero),
216 );
217 times(gap.sign().unwrap_or(Sign::Zero), d)
218 }
219}
220
221fn strict_hit(eye: Point3, p: Point3, t: &[Point3; 3]) -> Option<Param> {
224 let s = inside_signs(eye, p, t);
225 let all_same = s[0] != Sign::Zero && s[0] == s[1] && s[1] == s[2];
226 if !all_same {
227 return None;
228 }
229 let param = Param::of(eye, p, t)?;
230 (param.sign() == Sign::Positive).then_some(param)
231}
232
233fn blocks(eye: Point3, p: Point3, wall: &[Point3; 3], near: &Param) -> bool {
236 let s = inside_signs(eye, p, wall);
237 let has_pos = s.contains(&Sign::Positive);
238 let has_neg = s.contains(&Sign::Negative);
239 if has_pos && has_neg {
240 return false;
241 }
242 let Some(param) = Param::of(eye, p, wall) else {
243 let eye_side = orient(&wall.map(dpoint), &dpoint(eye)).sign();
246 return eye_side == Some(Sign::Zero);
247 };
248 param.sign() != Sign::Negative && param.compare(near) != Sign::Positive
249}
250
251fn inside_signs(eye: Point3, p: Point3, t: &[Point3; 3]) -> [Sign; 3] {
254 [0, 1, 2].map(|i| {
255 sign(&Orient3 {
256 p: [dpoint(eye), dpoint(p), dpoint(t[i]), dpoint(t[(i + 1) % 3])],
257 })
258 })
259}
260
261type DPoint = [Dyadic; 3];
266
267struct Piece {
271 blocker: usize,
272 cone: Vec<[DPoint; 3]>,
275 faces: Vec<[DPoint; 3]>,
278}
279
280fn pieces_of<M: TriangleMeshView + ?Sized>(
281 eye: Point3,
282 blocker: usize,
283 own: &[[Point3; 3]],
284 mesh: &M,
285) -> Vec<Piece> {
286 let e = dpoint(eye);
287 let mut out = Vec::new();
288 let mut polygon = |ring: Vec<Point3>| {
289 let ring: Vec<DPoint> = ring.into_iter().map(dpoint).collect();
290 let side = orient(&[ring[0].clone(), ring[1].clone(), ring[2].clone()], &e);
292 let Some(s) = side.sign().filter(|s| *s != Sign::Zero) else {
293 return;
294 };
295 let face = if s == Sign::Positive {
296 [ring[0].clone(), ring[1].clone(), ring[2].clone()]
297 } else {
298 [ring[0].clone(), ring[2].clone(), ring[1].clone()]
299 };
300 let n = ring.len();
303 let planes: Vec<[DPoint; 3]> = (0..n)
304 .map(|i| [e.clone(), ring[i].clone(), ring[(i + 1) % n].clone()])
305 .collect();
306 let inward = orient(&planes[0], &ring[2 % n]).sign() == Some(Sign::Positive);
307 let cone = planes
308 .into_iter()
309 .map(|[a, b, c]| if inward { [a, b, c] } else { [a, c, b] })
310 .collect();
311 out.push(Piece {
312 blocker,
313 cone,
314 faces: vec![face],
315 });
316 };
317 for t in own {
318 polygon(t.to_vec());
319 }
320 for (i, a) in own.iter().enumerate() {
322 for b in &own[i + 1..] {
323 if let Some(quad) = convex_quad(a, b) {
324 polygon(quad);
325 }
326 }
327 }
328 if let Some(solid) = convex_solid(eye, blocker, own, mesh) {
329 out.push(solid);
330 }
331 out
332}
333
334fn convex_quad(a: &[Point3; 3], b: &[Point3; 3]) -> Option<Vec<Point3>> {
337 let shared: Vec<usize> = (0..3).filter(|&i| b.contains(&a[i])).collect();
338 if shared.len() != 2 {
339 return None;
340 }
341 let apex_a = (0..3).find(|i| !shared.contains(i))?;
342 let apex_b = *b.iter().find(|p| !a.contains(p))?;
343 let (u, v) = (a[(apex_a + 1) % 3], a[(apex_a + 2) % 3]);
345 let ring = vec![a[apex_a], u, apex_b, v];
346 let d: Vec<DPoint> = ring.iter().copied().map(dpoint).collect();
347 if orient(&[d[0].clone(), d[1].clone(), d[2].clone()], &d[3]).sign() != Some(Sign::Zero) {
348 return None;
349 }
350 let normal = cross(&sub(&d[1], &d[0]), &sub(&d[3], &d[0]));
353 let turns: Vec<Option<Sign>> = (0..4)
354 .map(|i| {
355 let (p, q, r) = (&d[i], &d[(i + 1) % 4], &d[(i + 2) % 4]);
356 dot(&cross(&sub(q, p), &sub(r, q)), &normal).sign()
357 })
358 .collect();
359 let first = turns[0]?;
360 (first != Sign::Zero && turns.iter().all(|t| *t == Some(first))).then_some(ring)
361}
362
363fn convex_solid<M: TriangleMeshView + ?Sized>(
367 eye: Point3,
368 blocker: usize,
369 own: &[[Point3; 3]],
370 mesh: &M,
371) -> Option<Piece> {
372 if !audit_mesh(mesh, Tolerance::ZERO).is_closed_two_manifold() {
373 return None;
374 }
375 let mut vertices: Vec<Point3> = own.iter().flatten().copied().collect();
376 vertices.sort_by(|a, b| {
377 a.x.total_cmp(&b.x)
378 .then(a.y.total_cmp(&b.y))
379 .then(a.z.total_cmp(&b.z))
380 });
381 vertices.dedup();
382 let dv: Vec<DPoint> = vertices.iter().copied().map(dpoint).collect();
383 let e = dpoint(eye);
384 let mut faces = Vec::new();
386 let mut outside = false;
387 for t in own {
388 let plane = t.map(dpoint);
389 let mut side = Sign::Zero;
390 for v in &dv {
391 match orient(&plane, v).sign()? {
392 Sign::Zero => {}
393 s if side == Sign::Zero => side = s,
394 s if s != side => return None,
395 _ => {}
396 }
397 }
398 let eye_side = orient(&plane, &e).sign()?;
399 if eye_side != side {
400 outside = true;
401 }
402 if eye_side != Sign::Zero {
404 faces.push(if eye_side == Sign::Positive {
405 plane
406 } else {
407 [plane[0].clone(), plane[2].clone(), plane[1].clone()]
408 });
409 }
410 }
411 if !outside {
412 return None;
413 }
414 let mut cone = Vec::new();
415 for i in 0..dv.len() {
416 for j in i + 1..dv.len() {
417 let plane = [e.clone(), dv[i].clone(), dv[j].clone()];
418 let mut side = Sign::Zero;
419 let mut ok = true;
420 for v in &dv {
421 match orient(&plane, v).sign() {
422 Some(Sign::Zero) => {}
423 Some(s) if side == Sign::Zero => side = s,
424 Some(s) if s != side => {
425 ok = false;
426 break;
427 }
428 Some(_) => {}
429 None => return None,
430 }
431 }
432 if ok && side != Sign::Zero {
433 cone.push(if side == Sign::Positive {
434 plane
435 } else {
436 [plane[0].clone(), plane[2].clone(), plane[1].clone()]
437 });
438 }
439 }
440 }
441 Some(Piece {
442 blocker,
443 cone,
444 faces,
445 })
446}
447
448fn hidden(
451 eye: Point3,
452 targets: &[[Point3; 3]],
453 pieces: &[Piece],
454 budget: usize,
455) -> Option<Vec<usize>> {
456 let _ = eye;
457 let mut used: Vec<usize> = Vec::new();
458 let mut stack: Vec<[DPoint; 3]> = targets.iter().map(|t| t.map(dpoint)).collect();
459 let mut cells = 0usize;
460 while let Some(cell) = stack.pop() {
461 cells += 1;
462 if cells > budget {
463 return None;
464 }
465 if let Some(piece) = pieces.iter().find(|p| covers(p, &cell)) {
466 if !used.contains(&piece.blocker) {
467 used.push(piece.blocker);
468 }
469 continue;
470 }
471 let half = exact(0.5);
472 let mid =
473 |a: &DPoint, b: &DPoint| -> DPoint { [0, 1, 2].map(|k| a[k].add(&b[k]).mul(&half)) };
474 let [a, b, c] = cell;
475 let (ab, bc, ca) = (mid(&a, &b), mid(&b, &c), mid(&c, &a));
476 stack.push([a, ab.clone(), ca.clone()]);
477 stack.push([ab.clone(), b, bc.clone()]);
478 stack.push([ca.clone(), bc.clone(), c]);
479 stack.push([ab, bc, ca]);
480 }
481 used.sort_unstable();
482 Some(used)
483}
484
485fn covers(piece: &Piece, cell: &[DPoint; 3]) -> bool {
487 let in_cone = piece.cone.iter().all(|plane| {
488 cell.iter().all(|g| {
489 sign(&Orient3 {
490 p: [
491 plane[0].clone(),
492 plane[1].clone(),
493 plane[2].clone(),
494 g.clone(),
495 ],
496 }) == Sign::Positive
497 })
498 });
499 if !in_cone {
500 return false;
501 }
502 piece.faces.iter().any(|face| {
503 cell.iter().all(|g| {
504 sign(&Orient3 {
505 p: [face[0].clone(), face[1].clone(), face[2].clone(), g.clone()],
506 }) == Sign::Negative
507 })
508 })
509}
510
511fn exact(x: f64) -> Dyadic {
516 Dyadic::from_f64(x)
517}
518
519fn dpoint(p: Point3) -> DPoint {
520 [exact(p.x), exact(p.y), exact(p.z)]
521}
522
523fn sub<T: Arith>(a: &[T; 3], b: &[T; 3]) -> [T; 3] {
524 [a[0].sub(&b[0]), a[1].sub(&b[1]), a[2].sub(&b[2])]
525}
526
527fn cross<T: Arith>(u: &[T; 3], v: &[T; 3]) -> [T; 3] {
528 [
529 u[1].mul(&v[2]).sub(&u[2].mul(&v[1])),
530 u[2].mul(&v[0]).sub(&u[0].mul(&v[2])),
531 u[0].mul(&v[1]).sub(&u[1].mul(&v[0])),
532 ]
533}
534
535fn dot<T: Arith>(u: &[T; 3], v: &[T; 3]) -> T {
536 u[0].mul(&v[0]).add(&u[1].mul(&v[1])).add(&u[2].mul(&v[2]))
537}
538
539fn orient(plane: &[DPoint; 3], d: &DPoint) -> Dyadic {
542 let n = cross(&sub(&plane[1], &plane[0]), &sub(&plane[2], &plane[0]));
543 dot(&n, &sub(d, &plane[0]))
544}
545
546struct Orient3 {
549 p: [DPoint; 4],
550}
551
552impl SignExpr for Orient3 {
553 fn sign_in<T: Arith>(&self) -> Option<Sign> {
554 let q = |i: usize| self.p[i].clone().map(|x| T::from_dyadic(&x));
555 let (a, b, c, d) = (q(0), q(1), q(2), q(3));
556 let n = cross(&sub(&b, &a), &sub(&c, &a));
557 dot(&n, &sub(&d, &a)).sign()
558 }
559}
560
561fn sign<E: SignExpr>(e: &E) -> Sign {
563 certify(e).unwrap_or(Sign::Zero)
564}
565
566fn times(a: Sign, b: Sign) -> Sign {
567 match (a, b) {
568 (Sign::Zero, _) | (_, Sign::Zero) => Sign::Zero,
569 (x, y) if x == y => Sign::Positive,
570 _ => Sign::Negative,
571 }
572}
573
574#[cfg(test)]
575mod tests {
576 use super::*;
577 use axiolid_mesh::TriMesh;
578
579 fn p(x: f64, y: f64, z: f64) -> Point3 {
580 Point3::new(x, y, z)
581 }
582
583 fn extrusion(outline: &[(f64, f64)], z: (f64, f64)) -> TriMesh {
586 let n = outline.len() as u32;
587 let mut positions: Vec<Point3> = outline.iter().map(|&(x, y)| p(x, y, z.0)).collect();
588 positions.extend(outline.iter().map(|&(x, y)| p(x, y, z.1)));
589 let mut indices = Vec::new();
590 for i in 1..n - 1 {
591 indices.extend([0, i + 1, i]);
592 indices.extend([n, n + i, n + i + 1]);
593 }
594 for i in 0..n {
595 let j = (i + 1) % n;
596 indices.extend([i, j, j + n, i, j + n, i + n]);
597 }
598 TriMesh::new(positions, indices)
599 }
600
601 #[test]
602 fn only_convex_closed_blockers_are_solids() {
603 let eye = p(-10.0, 0.5, 0.0);
604 let cube = extrusion(
605 &[(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0)],
606 (-1.0, 1.0),
607 );
608 let own = triangles(&cube).unwrap();
609 assert!(convex_solid(eye, 0, &own, &cube).is_some());
610 let l = extrusion(
612 &[(1.0, 1.0), (0.0, 2.0), (0.0, 0.0), (2.0, 0.0), (2.0, 1.0)],
613 (-1.0, 1.0),
614 );
615 let own = triangles(&l).unwrap();
616 assert!(audit_mesh(&l, Tolerance::ZERO).is_closed_two_manifold());
617 assert!(convex_solid(eye, 0, &own, &l).is_none());
618 let own = triangles(&cube).unwrap();
620 assert!(convex_solid(p(0.5, 0.5, 0.0), 0, &own, &cube).is_none());
621 }
622}