use axiolid_contracts::{GeomError, GeomResult};
use axiolid_core::{Point3, Scalar};
use axiolid_mesh::TriMesh;
use axiolid_surface::Surface;
use crate::surface::{evaluate, Patch};
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct TessellationBudget {
pub chord_tolerance: Scalar,
pub max_samples_per_direction: usize,
}
impl TessellationBudget {
pub fn new(chord_tolerance: Scalar, max_samples_per_direction: usize) -> GeomResult<Self> {
if !(chord_tolerance.is_finite() && chord_tolerance > 0.0) {
return Err(GeomError::InvalidInput(format!(
"chord tolerance must be positive and finite, got {chord_tolerance}"
)));
}
if max_samples_per_direction < 2 {
return Err(GeomError::InvalidInput(format!(
"need at least 2 samples per direction, got {max_samples_per_direction}"
)));
}
Ok(Self {
chord_tolerance,
max_samples_per_direction,
})
}
}
#[derive(Debug, Clone)]
pub struct TessellationOutcome {
pub mesh: TriMesh,
pub u_samples: usize,
pub v_samples: usize,
pub max_sagitta: Option<Scalar>,
pub budget_exhausted: bool,
}
pub fn tessellate_patch(
surface: &Surface,
patch: Patch,
budget: TessellationBudget,
) -> GeomResult<TessellationOutcome> {
let (nu, su) = resolve_samples(surface, patch, budget, Direction::U)?;
let (nv, sv) = resolve_samples(surface, patch, budget, Direction::V)?;
let exhausted =
nu >= budget.max_samples_per_direction || nv >= budget.max_samples_per_direction;
let mut positions = Vec::with_capacity(nu * nv);
for i in 0..nu {
let u = lerp(patch.u_start, patch.u_end, i, nu);
for j in 0..nv {
let v = lerp(patch.v_start, patch.v_end, j, nv);
positions.push(evaluate(surface, u, v)?);
}
}
let mut indices = Vec::with_capacity((nu - 1) * (nv - 1) * 6);
for i in 0..nu - 1 {
for j in 0..nv - 1 {
let a = (i * nv + j) as u32;
let b = (i * nv + j + 1) as u32;
let c = ((i + 1) * nv + j) as u32;
let d = ((i + 1) * nv + j + 1) as u32;
if shorter_diagonal_is_ad(&positions, a, b, c, d) {
indices.extend_from_slice(&[a, c, d]);
indices.extend_from_slice(&[a, d, b]);
} else {
indices.extend_from_slice(&[a, c, b]);
indices.extend_from_slice(&[b, c, d]);
}
}
}
let max_sagitta = match (su, sv) {
(Some(a), Some(b)) => Some(a.max(b)),
(Some(a), None) => Some(a),
(None, Some(b)) => Some(b),
(None, None) => None,
};
Ok(TessellationOutcome {
mesh: TriMesh::new(positions, indices),
u_samples: nu,
v_samples: nv,
max_sagitta,
budget_exhausted: exhausted,
})
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Direction {
U,
V,
}
fn lerp(a: Scalar, b: Scalar, i: usize, n: usize) -> Scalar {
if n <= 1 {
return a;
}
let t = i as Scalar / (n - 1) as Scalar;
a + (b - a) * t
}
fn shorter_diagonal_is_ad(positions: &[Point3], a: u32, b: u32, c: u32, d: u32) -> bool {
let p = |i: u32| positions[i as usize];
(p(a) - p(d)).length_squared() <= (p(b) - p(c)).length_squared()
}
fn resolve_samples(
surface: &Surface,
patch: Patch,
budget: TessellationBudget,
direction: Direction,
) -> GeomResult<(usize, Option<Scalar>)> {
let mut n = 2usize;
loop {
let worst = worst_sagitta(surface, patch, direction, n)?;
match worst {
Some(s) if s > budget.chord_tolerance => {}
_ => return Ok((n, worst)),
}
if n >= budget.max_samples_per_direction {
return Ok((n, worst));
}
n = (2 * (n - 1) + 1).min(budget.max_samples_per_direction);
}
}
fn worst_sagitta(
surface: &Surface,
patch: Patch,
direction: Direction,
n: usize,
) -> GeomResult<Option<Scalar>> {
const CROSS_PROBES: usize = 3;
let mut worst: Option<Scalar> = None;
for span in 0..n.saturating_sub(1) {
for k in 0..CROSS_PROBES {
let (a, b, mid, other) = span_probe(patch, direction, span, n, k, CROSS_PROBES);
let pa = eval_at(surface, direction, a, other)?;
let pb = eval_at(surface, direction, b, other)?;
let pm = eval_at(surface, direction, mid, other)?;
let deviation = point_to_segment(pm, pa, pb);
worst = Some(worst.map_or(deviation, |w: Scalar| w.max(deviation)));
}
}
Ok(worst)
}
fn span_probe(
patch: Patch,
direction: Direction,
span: usize,
n: usize,
k: usize,
probes: usize,
) -> (Scalar, Scalar, Scalar, Scalar) {
let (start, end, cross_start, cross_end) = match direction {
Direction::U => (patch.u_start, patch.u_end, patch.v_start, patch.v_end),
Direction::V => (patch.v_start, patch.v_end, patch.u_start, patch.u_end),
};
let a = lerp(start, end, span, n);
let b = lerp(start, end, span + 1, n);
let other = lerp(cross_start, cross_end, k, probes);
(a, b, 0.5 * (a + b), other)
}
fn eval_at(
surface: &Surface,
direction: Direction,
along: Scalar,
other: Scalar,
) -> GeomResult<Point3> {
match direction {
Direction::U => evaluate(surface, along, other),
Direction::V => evaluate(surface, other, along),
}
}
fn point_to_segment(m: Point3, a: Point3, b: Point3) -> Scalar {
let ab = b - a;
let len2 = ab.length_squared();
if len2 <= 0.0 {
return (m - a).length();
}
let t = ((m - a).dot(ab) / len2).clamp(0.0, 1.0);
(m - (a + ab * t)).length()
}