axiolid_inspect/
sight.rs

1//! Certified line of sight from an eye point to a mesh past blockers
2//! (#185).
3//!
4//! # Visible: one ray, checked exactly
5//!
6//! A ray from the eye through a point `p` is a witness when it crosses the
7//! interior of a target triangle strictly before it meets any blocker
8//! triangle -- touching a blocker's edge counts as meeting it. Where a ray
9//! meets a plane is a ratio of two exact `orient3d` values, so which comes
10//! first is an exact comparison. Candidate rays aim at points spread over
11//! each target triangle; the first that checks out is returned.
12//!
13//! # Hidden: every ray, by cones
14//!
15//! Every target triangle is cut into sub-triangles whose corners are exact
16//! dyadic points on it (midpoints of dyadic points are dyadic, so they stay
17//! on the triangle exactly). A sub-triangle is hidden when one blocker
18//! piece covers it: its three corners' rays pass strictly inside the
19//! piece's cone from the eye -- so every ray through the sub-triangle does,
20//! the cone being convex -- and a plane of the piece has the eye strictly
21//! on one side and the sub-triangle strictly on the other, so every such
22//! ray meets the piece before the target. A piece is a blocker triangle,
23//! two coplanar triangles forming a convex quadrilateral (a wall), or a
24//! whole closed convex blocker (a column). When every sub-triangle of
25//! every target triangle is covered, the target is hidden, and the
26//! blockers used are named.
27//!
28//! # Undecided
29//!
30//! Neither argument may be available: a target just grazed, or covered only
31//! by several pieces together, so that some sub-cone straddles a seam
32//! between them at every depth. Then the answer is undecided, never a
33//! guess.
34
35use axiolid_core::{Point3, Tolerance};
36use axiolid_exact::{certify, Arith, Dyadic, SignExpr};
37use axiolid_guarantees::Sign;
38use axiolid_mesh::{audit_mesh, TriangleMeshView};
39
40/// Sub-triangles the hidden argument examines at most.
41pub const MAX_SIGHT_CELLS: usize = 50_000;
42
43/// What can be proven about the view from an eye to a target.
44#[derive(Debug, Clone, PartialEq)]
45#[non_exhaustive]
46pub enum Sight {
47    /// Some part of the target is in view.
48    Visible {
49        /// The ray from the eye through this point is the witness.
50        through: Point3,
51        /// The target triangle it crosses, strictly inside, before any
52        /// blocker.
53        triangle: usize,
54    },
55    /// No part of the target is in view.
56    Hidden {
57        /// Indices of the blockers the argument used, ascending.
58        occluders: Vec<usize>,
59    },
60    /// Neither could be proven within the budget.
61    Undecided,
62}
63
64/// Why the question was not asked.
65#[derive(Debug, Clone, Copy, PartialEq, Eq)]
66#[non_exhaustive]
67pub enum SightError {
68    /// The eye or a vertex is not finite.
69    NonFinite,
70    /// The target has no triangles with area.
71    EmptyTarget,
72}
73
74/// Whether any part of `target` is in view from `eye`, past `blockers`.
75///
76/// # Errors
77///
78/// [`SightError`] for non-finite input or an empty target.
79pub 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
87/// [`line_of_sight`] with a caller-chosen budget of sub-triangles.
88///
89/// # Errors
90///
91/// As [`line_of_sight`].
92pub 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
138/// Whether three points are collinear, exactly.
139fn 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
148// ---------------------------------------------------------------------
149// Visible
150// ---------------------------------------------------------------------
151
152/// A ray through a point spread over some target triangle that meets it
153/// before every blocker.
154fn witness(eye: Point3, targets: &[[Point3; 3]], walls: &[[Point3; 3]]) -> Option<Sight> {
155    // Barycentric weights: the centroid first, then a grid inside.
156    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/// Where the ray from `eye` through `p` meets a triangle's plane: the
184/// parameter `t = O(eye) / (O(eye) - O(p))` along `eye + t (p - eye)`,
185/// kept as numerator and denominator, exact.
186#[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    /// The sign of `self - other`.
211    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
221/// The ray's parameter at a triangle it crosses strictly inside, ahead of
222/// the eye.
223fn 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
233/// Whether a blocker triangle meets the ray (edges included) no farther
234/// than `near`.
235fn 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        // Parallel to the wall's plane: off it, the ray never meets the
244        // wall; in it, count the wall as blocking, conservatively.
245        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
251/// `orient3d(eye, p, t_i, t_{i+1})` for each edge: all one strict sign
252/// when the line through the eye and `p` passes strictly inside.
253fn 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
261// ---------------------------------------------------------------------
262// Hidden
263// ---------------------------------------------------------------------
264
265type DPoint = [Dyadic; 3];
266
267/// A blocker piece: a convex polygon or a convex solid, with the planes
268/// bounding its cone from the eye and the planes that may separate it
269/// from the target.
270struct Piece {
271    blocker: usize,
272    /// Planes through the eye, as point triples, whose positive side is
273    /// the cone's inside.
274    cone: Vec<[DPoint; 3]>,
275    /// Candidate separating planes, as point triples whose positive side
276    /// holds the eye.
277    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        // The eye must be off the polygon's plane.
291        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        // Seen from the eye the ring turns one way; orient the cone planes
301        // so the inside is positive.
302        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    // Coplanar edge neighbours forming a convex quadrilateral: a wall.
321    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
334/// Two triangles sharing an edge, coplanar, whose union is a convex
335/// quadrilateral: its ring.
336fn 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    // Ring: a's apex, then round a to the shared edge, b's apex between.
344    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    // Convex: each corner turns the same way within the plane, judged by
351    // the cross products against the plane's normal.
352    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
363/// A closed convex blocker the eye is strictly outside of, as one piece:
364/// its cone is bounded by the planes through the eye and two of its
365/// vertices with every vertex on one side.
366fn 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    // Convex, with each face's inner side: every vertex on one side of it.
385    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        // Oriented so the eye's side is positive, for separation.
403        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
448/// Cover every target triangle by pieces, subdividing; the blockers used,
449/// or `None` when some part could not be covered within the budget.
450fn 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
485/// Whether the piece hides every point of the cell from the eye.
486fn 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
511// ---------------------------------------------------------------------
512// Exact helpers
513// ---------------------------------------------------------------------
514
515fn 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
539/// `orient3d(a, b, c, d)` exactly: positive when `d` lies on the side of
540/// the plane through `a, b, c` that `(b - a) x (c - a)` points to.
541fn 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
546/// [`orient`] as a filtered predicate: intervals first, dyadics if
547/// undecided.
548struct 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
561/// The inputs are finite, so the exact tier always decides.
562fn 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    /// An extrusion of a counter-clockwise outline in z, capped by a fan
584    /// from the first vertex (which must see every other).
585    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        // An L, fanned from its reflex corner.
611        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        // The eye inside a convex solid: not a solid piece.
619        let own = triangles(&cube).unwrap();
620        assert!(convex_solid(p(0.5, 0.5, 0.0), 0, &own, &cube).is_none());
621    }
622}