use axiolid_core::{Point3, Tolerance};
use axiolid_exact::{certify, Arith, Dyadic, SignExpr};
use axiolid_guarantees::Sign;
use axiolid_mesh::{audit_mesh, TriangleMeshView};
pub const MAX_SIGHT_CELLS: usize = 50_000;
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub enum Sight {
Visible {
through: Point3,
triangle: usize,
},
Hidden {
occluders: Vec<usize>,
},
Undecided,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum SightError {
NonFinite,
EmptyTarget,
}
pub fn line_of_sight<T, B>(eye: Point3, target: &T, blockers: &[&B]) -> Result<Sight, SightError>
where
T: TriangleMeshView + ?Sized,
B: TriangleMeshView + ?Sized,
{
line_of_sight_within(eye, target, blockers, MAX_SIGHT_CELLS)
}
pub fn line_of_sight_within<T, B>(
eye: Point3,
target: &T,
blockers: &[&B],
budget: usize,
) -> Result<Sight, SightError>
where
T: TriangleMeshView + ?Sized,
B: TriangleMeshView + ?Sized,
{
if !eye.is_finite() {
return Err(SightError::NonFinite);
}
let targets = triangles(target)?;
if targets.is_empty() {
return Err(SightError::EmptyTarget);
}
let mut walls: Vec<[Point3; 3]> = Vec::new();
let mut pieces: Vec<Piece> = Vec::new();
for (index, mesh) in blockers.iter().enumerate() {
let own = triangles(*mesh)?;
pieces.extend(pieces_of(eye, index, &own, *mesh));
walls.extend(own);
}
if let Some(sight) = witness(eye, &targets, &walls) {
return Ok(sight);
}
Ok(hidden(eye, &targets, &pieces, budget)
.map_or(Sight::Undecided, |occluders| Sight::Hidden { occluders }))
}
fn triangles<M: TriangleMeshView + ?Sized>(mesh: &M) -> Result<Vec<[Point3; 3]>, SightError> {
let mut out = Vec::with_capacity(mesh.triangle_count());
for t in 0..mesh.triangle_count() {
let corners = mesh.triangle(t).map(|i| mesh.position(i as usize));
if !corners.iter().all(|p| p.is_finite()) {
return Err(SightError::NonFinite);
}
let [a, b, c] = corners;
if !collinear(a, b, c) {
out.push(corners);
}
}
Ok(out)
}
fn collinear(a: Point3, b: Point3, c: Point3) -> bool {
let d = |p: Point3| [exact(p.x), exact(p.y), exact(p.z)];
let (a, b, c) = (d(a), d(b), d(c));
let u = sub(&b, &a);
let v = sub(&c, &a);
let n = cross(&u, &v);
n.iter().all(|x| x.sign() == Some(Sign::Zero))
}
fn witness(eye: Point3, targets: &[[Point3; 3]], walls: &[[Point3; 3]]) -> Option<Sight> {
let mut weights: Vec<[f64; 3]> = vec![[1.0 / 3.0; 3]];
let n = 6;
for i in 1..n {
for j in 1..n - i {
let (u, v) = (i as f64 / n as f64, j as f64 / n as f64);
weights.push([u, v, 1.0 - u - v]);
}
}
for (index, t) in targets.iter().enumerate() {
for w in &weights {
let through = t[0] * w[0] + t[1] * w[1] + t[2] * w[2];
if through == eye {
continue;
}
if let Some(near) = strict_hit(eye, through, t) {
if walls.iter().all(|wall| !blocks(eye, through, wall, &near)) {
return Some(Sight::Visible {
through,
triangle: index,
});
}
}
}
}
None
}
#[derive(Debug, Clone)]
struct Param {
num: Dyadic,
den: Dyadic,
}
impl Param {
fn of(eye: Point3, p: Point3, t: &[Point3; 3]) -> Option<Self> {
let oe = orient(&t.map(dpoint), &dpoint(eye));
let op = orient(&t.map(dpoint), &dpoint(p));
let den = oe.sub(&op);
if den.sign() == Some(Sign::Zero) {
return None;
}
Some(Self { num: oe, den })
}
fn sign(&self) -> Sign {
times(
self.num.sign().unwrap_or(Sign::Zero),
self.den.sign().unwrap_or(Sign::Zero),
)
}
fn compare(&self, other: &Self) -> Sign {
let gap = self.num.mul(&other.den).sub(&other.num.mul(&self.den));
let d = times(
self.den.sign().unwrap_or(Sign::Zero),
other.den.sign().unwrap_or(Sign::Zero),
);
times(gap.sign().unwrap_or(Sign::Zero), d)
}
}
fn strict_hit(eye: Point3, p: Point3, t: &[Point3; 3]) -> Option<Param> {
let s = inside_signs(eye, p, t);
let all_same = s[0] != Sign::Zero && s[0] == s[1] && s[1] == s[2];
if !all_same {
return None;
}
let param = Param::of(eye, p, t)?;
(param.sign() == Sign::Positive).then_some(param)
}
fn blocks(eye: Point3, p: Point3, wall: &[Point3; 3], near: &Param) -> bool {
let s = inside_signs(eye, p, wall);
let has_pos = s.contains(&Sign::Positive);
let has_neg = s.contains(&Sign::Negative);
if has_pos && has_neg {
return false;
}
let Some(param) = Param::of(eye, p, wall) else {
let eye_side = orient(&wall.map(dpoint), &dpoint(eye)).sign();
return eye_side == Some(Sign::Zero);
};
param.sign() != Sign::Negative && param.compare(near) != Sign::Positive
}
fn inside_signs(eye: Point3, p: Point3, t: &[Point3; 3]) -> [Sign; 3] {
[0, 1, 2].map(|i| {
sign(&Orient3 {
p: [dpoint(eye), dpoint(p), dpoint(t[i]), dpoint(t[(i + 1) % 3])],
})
})
}
type DPoint = [Dyadic; 3];
struct Piece {
blocker: usize,
cone: Vec<[DPoint; 3]>,
faces: Vec<[DPoint; 3]>,
}
fn pieces_of<M: TriangleMeshView + ?Sized>(
eye: Point3,
blocker: usize,
own: &[[Point3; 3]],
mesh: &M,
) -> Vec<Piece> {
let e = dpoint(eye);
let mut out = Vec::new();
let mut polygon = |ring: Vec<Point3>| {
let ring: Vec<DPoint> = ring.into_iter().map(dpoint).collect();
let side = orient(&[ring[0].clone(), ring[1].clone(), ring[2].clone()], &e);
let Some(s) = side.sign().filter(|s| *s != Sign::Zero) else {
return;
};
let face = if s == Sign::Positive {
[ring[0].clone(), ring[1].clone(), ring[2].clone()]
} else {
[ring[0].clone(), ring[2].clone(), ring[1].clone()]
};
let n = ring.len();
let planes: Vec<[DPoint; 3]> = (0..n)
.map(|i| [e.clone(), ring[i].clone(), ring[(i + 1) % n].clone()])
.collect();
let inward = orient(&planes[0], &ring[2 % n]).sign() == Some(Sign::Positive);
let cone = planes
.into_iter()
.map(|[a, b, c]| if inward { [a, b, c] } else { [a, c, b] })
.collect();
out.push(Piece {
blocker,
cone,
faces: vec![face],
});
};
for t in own {
polygon(t.to_vec());
}
for (i, a) in own.iter().enumerate() {
for b in &own[i + 1..] {
if let Some(quad) = convex_quad(a, b) {
polygon(quad);
}
}
}
if let Some(solid) = convex_solid(eye, blocker, own, mesh) {
out.push(solid);
}
out
}
fn convex_quad(a: &[Point3; 3], b: &[Point3; 3]) -> Option<Vec<Point3>> {
let shared: Vec<usize> = (0..3).filter(|&i| b.contains(&a[i])).collect();
if shared.len() != 2 {
return None;
}
let apex_a = (0..3).find(|i| !shared.contains(i))?;
let apex_b = *b.iter().find(|p| !a.contains(p))?;
let (u, v) = (a[(apex_a + 1) % 3], a[(apex_a + 2) % 3]);
let ring = vec![a[apex_a], u, apex_b, v];
let d: Vec<DPoint> = ring.iter().copied().map(dpoint).collect();
if orient(&[d[0].clone(), d[1].clone(), d[2].clone()], &d[3]).sign() != Some(Sign::Zero) {
return None;
}
let normal = cross(&sub(&d[1], &d[0]), &sub(&d[3], &d[0]));
let turns: Vec<Option<Sign>> = (0..4)
.map(|i| {
let (p, q, r) = (&d[i], &d[(i + 1) % 4], &d[(i + 2) % 4]);
dot(&cross(&sub(q, p), &sub(r, q)), &normal).sign()
})
.collect();
let first = turns[0]?;
(first != Sign::Zero && turns.iter().all(|t| *t == Some(first))).then_some(ring)
}
fn convex_solid<M: TriangleMeshView + ?Sized>(
eye: Point3,
blocker: usize,
own: &[[Point3; 3]],
mesh: &M,
) -> Option<Piece> {
if !audit_mesh(mesh, Tolerance::ZERO).is_closed_two_manifold() {
return None;
}
let mut vertices: Vec<Point3> = own.iter().flatten().copied().collect();
vertices.sort_by(|a, b| {
a.x.total_cmp(&b.x)
.then(a.y.total_cmp(&b.y))
.then(a.z.total_cmp(&b.z))
});
vertices.dedup();
let dv: Vec<DPoint> = vertices.iter().copied().map(dpoint).collect();
let e = dpoint(eye);
let mut faces = Vec::new();
let mut outside = false;
for t in own {
let plane = t.map(dpoint);
let mut side = Sign::Zero;
for v in &dv {
match orient(&plane, v).sign()? {
Sign::Zero => {}
s if side == Sign::Zero => side = s,
s if s != side => return None,
_ => {}
}
}
let eye_side = orient(&plane, &e).sign()?;
if eye_side != side {
outside = true;
}
if eye_side != Sign::Zero {
faces.push(if eye_side == Sign::Positive {
plane
} else {
[plane[0].clone(), plane[2].clone(), plane[1].clone()]
});
}
}
if !outside {
return None;
}
let mut cone = Vec::new();
for i in 0..dv.len() {
for j in i + 1..dv.len() {
let plane = [e.clone(), dv[i].clone(), dv[j].clone()];
let mut side = Sign::Zero;
let mut ok = true;
for v in &dv {
match orient(&plane, v).sign() {
Some(Sign::Zero) => {}
Some(s) if side == Sign::Zero => side = s,
Some(s) if s != side => {
ok = false;
break;
}
Some(_) => {}
None => return None,
}
}
if ok && side != Sign::Zero {
cone.push(if side == Sign::Positive {
plane
} else {
[plane[0].clone(), plane[2].clone(), plane[1].clone()]
});
}
}
}
Some(Piece {
blocker,
cone,
faces,
})
}
fn hidden(
eye: Point3,
targets: &[[Point3; 3]],
pieces: &[Piece],
budget: usize,
) -> Option<Vec<usize>> {
let _ = eye;
let mut used: Vec<usize> = Vec::new();
let mut stack: Vec<[DPoint; 3]> = targets.iter().map(|t| t.map(dpoint)).collect();
let mut cells = 0usize;
while let Some(cell) = stack.pop() {
cells += 1;
if cells > budget {
return None;
}
if let Some(piece) = pieces.iter().find(|p| covers(p, &cell)) {
if !used.contains(&piece.blocker) {
used.push(piece.blocker);
}
continue;
}
let half = exact(0.5);
let mid =
|a: &DPoint, b: &DPoint| -> DPoint { [0, 1, 2].map(|k| a[k].add(&b[k]).mul(&half)) };
let [a, b, c] = cell;
let (ab, bc, ca) = (mid(&a, &b), mid(&b, &c), mid(&c, &a));
stack.push([a, ab.clone(), ca.clone()]);
stack.push([ab.clone(), b, bc.clone()]);
stack.push([ca.clone(), bc.clone(), c]);
stack.push([ab, bc, ca]);
}
used.sort_unstable();
Some(used)
}
fn covers(piece: &Piece, cell: &[DPoint; 3]) -> bool {
let in_cone = piece.cone.iter().all(|plane| {
cell.iter().all(|g| {
sign(&Orient3 {
p: [
plane[0].clone(),
plane[1].clone(),
plane[2].clone(),
g.clone(),
],
}) == Sign::Positive
})
});
if !in_cone {
return false;
}
piece.faces.iter().any(|face| {
cell.iter().all(|g| {
sign(&Orient3 {
p: [face[0].clone(), face[1].clone(), face[2].clone(), g.clone()],
}) == Sign::Negative
})
})
}
fn exact(x: f64) -> Dyadic {
Dyadic::from_f64(x)
}
fn dpoint(p: Point3) -> DPoint {
[exact(p.x), exact(p.y), exact(p.z)]
}
fn sub<T: Arith>(a: &[T; 3], b: &[T; 3]) -> [T; 3] {
[a[0].sub(&b[0]), a[1].sub(&b[1]), a[2].sub(&b[2])]
}
fn cross<T: Arith>(u: &[T; 3], v: &[T; 3]) -> [T; 3] {
[
u[1].mul(&v[2]).sub(&u[2].mul(&v[1])),
u[2].mul(&v[0]).sub(&u[0].mul(&v[2])),
u[0].mul(&v[1]).sub(&u[1].mul(&v[0])),
]
}
fn dot<T: Arith>(u: &[T; 3], v: &[T; 3]) -> T {
u[0].mul(&v[0]).add(&u[1].mul(&v[1])).add(&u[2].mul(&v[2]))
}
fn orient(plane: &[DPoint; 3], d: &DPoint) -> Dyadic {
let n = cross(&sub(&plane[1], &plane[0]), &sub(&plane[2], &plane[0]));
dot(&n, &sub(d, &plane[0]))
}
struct Orient3 {
p: [DPoint; 4],
}
impl SignExpr for Orient3 {
fn sign_in<T: Arith>(&self) -> Option<Sign> {
let q = |i: usize| self.p[i].clone().map(|x| T::from_dyadic(&x));
let (a, b, c, d) = (q(0), q(1), q(2), q(3));
let n = cross(&sub(&b, &a), &sub(&c, &a));
dot(&n, &sub(&d, &a)).sign()
}
}
fn sign<E: SignExpr>(e: &E) -> Sign {
certify(e).unwrap_or(Sign::Zero)
}
fn times(a: Sign, b: Sign) -> Sign {
match (a, b) {
(Sign::Zero, _) | (_, Sign::Zero) => Sign::Zero,
(x, y) if x == y => Sign::Positive,
_ => Sign::Negative,
}
}
#[cfg(test)]
mod tests {
use super::*;
use axiolid_mesh::TriMesh;
fn p(x: f64, y: f64, z: f64) -> Point3 {
Point3::new(x, y, z)
}
fn extrusion(outline: &[(f64, f64)], z: (f64, f64)) -> TriMesh {
let n = outline.len() as u32;
let mut positions: Vec<Point3> = outline.iter().map(|&(x, y)| p(x, y, z.0)).collect();
positions.extend(outline.iter().map(|&(x, y)| p(x, y, z.1)));
let mut indices = Vec::new();
for i in 1..n - 1 {
indices.extend([0, i + 1, i]);
indices.extend([n, n + i, n + i + 1]);
}
for i in 0..n {
let j = (i + 1) % n;
indices.extend([i, j, j + n, i, j + n, i + n]);
}
TriMesh::new(positions, indices)
}
#[test]
fn only_convex_closed_blockers_are_solids() {
let eye = p(-10.0, 0.5, 0.0);
let cube = extrusion(
&[(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0)],
(-1.0, 1.0),
);
let own = triangles(&cube).unwrap();
assert!(convex_solid(eye, 0, &own, &cube).is_some());
let l = extrusion(
&[(1.0, 1.0), (0.0, 2.0), (0.0, 0.0), (2.0, 0.0), (2.0, 1.0)],
(-1.0, 1.0),
);
let own = triangles(&l).unwrap();
assert!(audit_mesh(&l, Tolerance::ZERO).is_closed_two_manifold());
assert!(convex_solid(eye, 0, &own, &l).is_none());
let own = triangles(&cube).unwrap();
assert!(convex_solid(p(0.5, 0.5, 0.0), 0, &own, &cube).is_none());
}
}