Files
solidcraft-upstream/crates/kernel/src/tess/surface.rs
T
Irakli KourtisandClaude Opus 5.5 bfbf5f9432 Display tessellation plans (1/4): planner with shared edge nodes (#802)
Adds TessPlan and the kernel planner (shared edge nodes, structured and Coons grids, constrained Delaunay for trimmed faces) behind Body::display_plan. Nothing drawn changes yet.

Refs #303

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 14:27:44 -04:00

157 lines
7.2 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! Truck surfaces in the evaluator's form ([`EvalSurface`]), which the CPU and GPU evaluators
//! share. Surfaces the evaluators cannot express (a revolved intersection curve) or that are
//! too large for them give `None`; their faces are evaluated here instead.
use solidcraft_geom::Vec3;
use solidcraft_geom::bspline::MAX_SPLINE_DEGREE;
use solidcraft_geom::tess::{EvalCurve, EvalSurface, MAX_EVAL_KNOTS, MAX_EVAL_POINTS};
use truck_meshalgo::prelude::*;
use truck_modeling as mt;
use crate::body::from_p3;
/// `surface` in the evaluator's form, parameterised exactly as truck parameterises it.
pub(crate) fn eval_surface(surface: &mt::Surface) -> Option<EvalSurface> {
let s = match surface {
mt::Surface::Plane(p) => {
EvalSurface::Plane { origin: from_p3(p.subs(0.0, 0.0)), du: from_v3(p.uder(0.0, 0.0)), dv: from_v3(p.vder(0.0, 0.0)) }
}
mt::Surface::BSplineSurface(b) => {
let ctrl = b.control_points().iter().map(|row| row.iter().map(|p| [p.x, p.y, p.z, 1.0]).collect()).collect();
nurbs(b.degrees(), b.knot_vecs(), ctrl)?
}
mt::Surface::NurbsSurface(n) => {
let ctrl = n.control_points().iter().map(|row| row.iter().map(|h| [h.x, h.y, h.z, h.w]).collect()).collect();
nurbs((n.udegree(), n.vdegree()), n.knot_vecs(), ctrl)?
}
mt::Surface::RevolutedCurve(proc) => {
let rev = proc.entity();
let profile = eval_curve(rev.entity_curve())?;
let axis = from_v3(rev.axis()).normalized()?;
let m = proc.transform();
// cgmath is column-major: m[column][row].
let transform = [0, 1, 2].map(|r| [m[0][r], m[1][r], m[2][r], m[3][r]]);
if transform.iter().flatten().any(|x| !x.is_finite()) {
return None;
}
EvalSurface::Revolved { profile, origin: from_p3(rev.origin()), axis, transform, swap: !proc.orientation() }
}
};
Some(s)
}
fn from_v3(v: mt::Vector3) -> Vec3 {
Vec3::new(v.x, v.y, v.z)
}
fn knots(k: &mt::KnotVec) -> Option<Vec<f64>> {
let v: Vec<f64> = k.iter().copied().collect();
(v.len() <= MAX_EVAL_KNOTS && v.iter().all(|x| x.is_finite())).then_some(v)
}
/// Truck's `control_points[i][j]` (i along u) as the evaluator's u-fastest array.
fn nurbs((pu, pv): (usize, usize), kv: &(mt::KnotVec, mt::KnotVec), rows: Vec<Vec<[f64; 4]>>) -> Option<EvalSurface> {
let nu = rows.len();
let nv = rows.first()?.len();
if pu == 0 || pv == 0 || pu > MAX_SPLINE_DEGREE || pv > MAX_SPLINE_DEGREE || nu.checked_mul(nv)? > MAX_EVAL_POINTS {
return None;
}
let mut ctrl = Vec::with_capacity(nu * nv);
for j in 0..nv {
for row in &rows {
ctrl.push(<[[f64; 4]]>::get(row, j).copied()?);
}
}
if ctrl.iter().flatten().any(|x| !x.is_finite()) {
return None;
}
let knots = [knots(&kv.0)?, knots(&kv.1)?];
(knots[0].len() == nu + pu + 1 && knots[1].len() == nv + pv + 1).then_some(EvalSurface::Nurbs { degree: [pu, pv], knots, count: [nu, nv], ctrl })
}
/// A profile curve in the evaluator's form (a line extends for ever, as truck's does).
pub(crate) fn eval_curve(c: &mt::Curve) -> Option<EvalCurve> {
let (degree, kv, ctrl): (usize, Vec<f64>, Vec<[f64; 4]>) = match c {
mt::Curve::Line(l) => (1, vec![0.0, 0.0, 1.0, 1.0], vec![[l.0.x, l.0.y, l.0.z, 1.0], [l.1.x, l.1.y, l.1.z, 1.0]]),
mt::Curve::BSplineCurve(b) => (b.degree(), knots(b.knot_vec())?, b.control_points().iter().map(|p| [p.x, p.y, p.z, 1.0]).collect()),
mt::Curve::NurbsCurve(n) => (n.degree(), knots(n.knot_vec())?, n.control_points().iter().map(|h| [h.x, h.y, h.z, h.w]).collect()),
mt::Curve::IntersectionCurve(_) => return None,
};
let ok = (1..=MAX_SPLINE_DEGREE).contains(&degree)
&& ctrl.len() <= MAX_EVAL_POINTS
&& kv.len() == ctrl.len() + degree + 1
&& ctrl.iter().flatten().all(|x| x.is_finite());
ok.then_some(EvalCurve { degree, knots: kv, ctrl })
}
#[cfg(test)]
mod tests {
use super::*;
use crate::body::Solid;
/// Every face of `solid` evaluates as truck evaluates it, over its own parameter box.
fn assert_matches_truck(solid: &Solid) -> [usize; 4] {
let mut kinds = [0; 4];
for face in solid.face_iter() {
let surface = face.surface();
let kind = match surface {
mt::Surface::Plane(_) => 0,
mt::Surface::BSplineSurface(_) => 1,
mt::Surface::NurbsSurface(_) => 2,
mt::Surface::RevolutedCurve(_) => 3,
};
let Some(eval) = eval_surface(&surface) else { panic!("not evaluable: {surface:?}") };
// Sample the face's parameter box from its boundary vertices.
let pts: Vec<(f64, f64)> = face.vertex_iter().filter_map(|v| surface.search_parameter(v.point(), None, 100)).collect();
if pts.is_empty() {
continue;
}
let (u0, u1) = pts.iter().fold((f64::MAX, f64::MIN), |(a, b), p| (a.min(p.0), b.max(p.0)));
let (v0, v1) = pts.iter().fold((f64::MAX, f64::MIN), |(a, b), p| (a.min(p.1), b.max(p.1)));
for i in 0..=6 {
for j in 0..=6 {
let (u, v) = (u0 + (u1 - u0) * i as f64 / 6.0, v0 + (v1 - v0) * j as f64 / 6.0);
let (p, n) = eval.eval(u, v);
let want = from_p3(surface.subs(u, v));
assert!(p.dist(want) < 1e-9 * (1.0 + want.len()), "point at ({u}, {v}): {p:?} vs {want:?}");
let wn = from_v3(surface.normal(u, v));
if wn.len() > 0.5 {
assert!(n.dist(wn) < 1e-6, "normal at ({u}, {v}): {n:?} vs {wn:?} ({surface:?})");
}
kinds[kind] += 1;
}
}
}
kinds
}
fn solid(b: crate::Body) -> Solid {
(*b.solid).clone()
}
#[test]
fn primitives_evaluate_like_truck() {
let Ok(bx) = crate::box_solid(Vec3::new(-1.0, -2.0, 0.0), Vec3::new(3.0, 4.0, 5.0)) else { panic!("box") };
let Ok(cyl) = crate::cylinder(Vec3::new(1.0, 2.0, 3.0), Vec3::new(0.0, 1.0, 1.0), 2.5, 7.0) else { panic!("cylinder") };
let Ok(sph) = crate::sphere(Vec3::new(0.0, 0.0, 1.0), 4.0) else { panic!("sphere") };
let Ok(tor) = crate::torus(Vec3::ZERO, 6.0, 1.5) else { panic!("torus") };
let mut kinds = [0; 4];
for b in [bx, cyl, sph, tor] {
for (k, n) in kinds.iter_mut().zip(assert_matches_truck(&solid(b))) {
*k += n;
}
}
// Planes, rational patches (sphere) and revolved profiles (cylinder, torus) all covered.
assert!(kinds[0] > 0 && kinds[3] > 0 && kinds[1] + kinds[2] > 0, "{kinds:?}");
}
#[test]
fn transformed_and_mirrored_bodies_evaluate_like_truck() {
let Ok(cyl) = crate::cylinder(Vec3::ZERO, Vec3::Z, 2.0, 5.0) else { panic!("cylinder") };
// Column-major: x → 3 − x, z → z + 1.
let mirror = [[-1.0, 0.0, 0.0, 0.0], [0.0, 1.0, 0.0, 0.0], [0.0, 0.0, 1.0, 0.0], [3.0, 0.0, 1.0, 1.0]];
let Ok(m) = crate::transform_matrix(&cyl, mirror) else { panic!("transform") };
assert!(assert_matches_truck(&solid(m)).iter().sum::<usize>() > 0);
}
}