use axiolid_core::{Point3, Scalar, Vec3};
use crate::{
FieldConfig, FieldEvidence, LayeredCell, LayeredField, LayeredFieldError, SurfaceFacing,
SurfaceHit,
};
pub use axiolid_core::Triangle3;
#[derive(Debug, Clone, Copy, Default)]
pub struct CpuCoverageProvider;
impl CpuCoverageProvider {
pub const fn new() -> Self {
Self
}
pub fn sample(
&self,
config: &FieldConfig,
triangles: &[Triangle3],
) -> Result<LayeredField, LayeredFieldError> {
sample_triangles_cpu(config, triangles)
}
}
pub fn sample_triangles_cpu(
config: &FieldConfig,
triangles: &[Triangle3],
) -> Result<LayeredField, LayeredFieldError> {
if triangles
.iter()
.any(|t| !t.a.is_finite() || !t.b.is_finite() || !t.c.is_finite())
{
return Err(LayeredFieldError::NonFiniteGeometry);
}
let tolerance = config.tolerance();
let linear = tolerance.linear();
let direction = config.frame().z;
let span = config.bounds().normal_span();
let (w_low, w_high) = (span.start, span.end);
let budget = config.budget();
let mut prepared = Vec::with_capacity(triangles.len());
let mut evidence = FieldEvidence::default();
for triangle in triangles {
let normal = triangle.normal();
if !normal.is_finite() || normal.length() <= linear * linear {
evidence.degenerate_triangles += 1;
continue;
}
let denominator = normal.dot(direction);
if denominator.abs() <= linear.max(Scalar::EPSILON) {
evidence.parallel_triangles_skipped += 1;
continue;
}
prepared.push(Prepared {
triangle: *triangle,
normal,
denominator,
});
}
let empty = LayeredField::with_config(config)?;
let (width, height) = config.dimensions();
let mut cells = empty.cells().to_vec();
let mut stored = 0usize;
for y in 0..height {
for x in 0..width {
let origin = config.cell_center(x, y);
let mut hits: Vec<SurfaceHit> = Vec::new();
for item in &prepared {
let offset = item.triangle.a - origin;
let w = item.normal.dot(offset) / item.denominator;
if !w.is_finite() {
continue;
}
if w < w_low - linear || w > w_high + linear {
evidence.out_of_bounds_hits += 1;
continue;
}
let point = origin + direction * w;
match classify(&item.triangle, item.normal, point, linear) {
Containment::Outside => continue,
Containment::Boundary => evidence.boundary_contacts += 1,
Containment::Interior => {}
}
hits.push(SurfaceHit::new(w, facing_of(item.denominator)));
}
evidence.cells_sampled += 1;
hits.sort_by(|left, right| {
left.w()
.total_cmp(&right.w())
.then_with(|| left.facing().cmp(&right.facing()))
});
let before = hits.len();
hits.dedup_by(|right, left| {
left.facing() == right.facing() && (right.w() - left.w()).abs() <= linear
});
evidence.coincident_hits_merged += before - hits.len();
evidence.surface_hits += hits.len();
if hits.is_empty() {
evidence.empty_cells += 1;
} else if hits.len() > 1 {
evidence.multi_layer_cells += 1;
}
stored = stored
.checked_add(hits.len())
.ok_or(LayeredFieldError::SampleBudgetExceeded)?;
if stored > budget.max_intervals {
return Err(LayeredFieldError::SampleBudgetExceeded);
}
let index = empty
.linear_index(x, y)
.ok_or(LayeredFieldError::NodeOutsideField)?;
cells[index] = LayeredCell::with_layers(hits, Vec::new())?;
}
}
LayeredField::from_cells(width, height, cells, evidence)
}
struct Prepared {
triangle: Triangle3,
normal: Vec3,
denominator: Scalar,
}
enum Containment {
Interior,
Boundary,
Outside,
}
fn facing_of(denominator: Scalar) -> SurfaceFacing {
if denominator < 0.0 {
SurfaceFacing::AgainstNormal
} else {
SurfaceFacing::WithNormal
}
}
fn classify(triangle: &Triangle3, normal: Vec3, point: Point3, linear: Scalar) -> Containment {
let length = normal.length();
let scale = if length > 0.0 { length } else { 1.0 };
let edges = [
(triangle.b - triangle.a)
.cross(point - triangle.a)
.dot(normal)
/ scale,
(triangle.c - triangle.b)
.cross(point - triangle.b)
.dot(normal)
/ scale,
(triangle.a - triangle.c)
.cross(point - triangle.c)
.dot(normal)
/ scale,
];
let limit = linear.max(Scalar::EPSILON) * scale.max(1.0);
if edges.iter().any(|value| *value < -limit) {
return Containment::Outside;
}
if edges.iter().any(|value| value.abs() <= limit) {
return Containment::Boundary;
}
Containment::Interior
}