kernel: the mesher plans a schema 4 record, with no truck types

tess::plan takes a brep::Brep: faces, loops, shells' face sides and edges in the record's
order. Surfaces go to the evaluator's form from the record (planes, splines and revolutions
through SurfaceEval; a cylinder, cone, sphere or torus as the exact revolution of its line or
rational-arc generator about its axis). Edges are sampled on the record's own curve evaluator
(CurveEval), an intersection curve included, so tess/ssi.rs and the per-node parameters it
carried are gone; every boundary node's parameters come from projection.

No fallback is left in the planner's path: a curve that does not evaluate on its interval is
an error naming the edge (it was a two-point chord, or truck's evaluation); a surface the
evaluator cannot express an error naming the face and its kind. Errors name the face, its
surface kind and, for a node off the surface, the loop, node and point. The module doc states
the order: a cheaper layout where the face suits one, else constrained Delaunay in (u, v),
else an error.

MAX_ANGLE_BOUNDARY and MAX_RELAX are tess::size's own. Removed with truck: the winding test
and pole-to-pole seam test that chose between this planner and truck's mesher, and
boundaries_on_surfaces (truck blends). The tests in tess/ still build truck bodies and are
left for the record-based Body.

(not built: base in progress) tess/ has no errors under cargo check; the rest of the crate is
the no-truck branch's.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
This commit is contained in:
jyviko
2026-10-22 00:28:20 -04:00
parent 42997a62ed
commit 8b6bfc48ca
6 changed files with 308 additions and 981 deletions
+46 -87
View File
@@ -2,51 +2,37 @@
//! same boundary points (watertight by construction).
use solidcraft_geom::Vec3;
use truck_meshalgo::prelude::*;
use truck_modeling as mt;
use solidcraft_geom::tess::EvalSurface;
use solidcraft_geom::brep::eval::CurveEval;
use solidcraft_geom::brep::{self, Curve};
use super::size::Rules;
use crate::body::from_p3;
use crate::{KernelError, Result};
/// Most nodes on one edge.
pub(crate) const MAX_EDGE_NODES: usize = 20_000;
/// Deepest bisection of one edge segment.
const MAX_DEPTH: u32 = 24;
/// An edge's nodes, from its absolute front to its absolute back.
/// An edge's nodes, from its start vertex (interval min) to its end vertex (interval max).
pub(crate) struct Nodes {
pub points: Vec<Vec3>,
/// For an intersection edge: its two surfaces and, per node, both surfaces' parameters
/// (u1, v1, u2, v2) where Newton placed the node (`None` where truck's evaluation did).
pub on: Option<([EvalSurface; 2], Vec<Option<[f64; 4]>>)>,
}
impl Nodes {
fn plain(points: Vec<Vec3>) -> Nodes {
Nodes { points, on: None }
}
/// The parameters of each node on `surface`, where the edge knows them: `None` unless
/// `surface` is one of the edge's two surfaces.
pub(crate) fn params_on(&self, surface: &EvalSurface) -> Option<Vec<Option<[f64; 2]>>> {
let (surfaces, params) = self.on.as_ref()?;
let side = surfaces.iter().position(|s| s == surface)?;
Some(params.iter().map(|q| q.map(|q| [q[2 * side], q[2 * side + 1]])).collect())
}
}
/// Nodes of `edge` from its absolute front to its absolute back: every chord within the
/// tolerance of the curve (checked at the quarter points, so an S-bend in one segment is not
/// missed), at most `h` long, and within the rules' facet angle. The end nodes are the
/// vertices' own points, so edges meeting at a vertex meet exactly.
pub(crate) fn sample_edge(edge: &mt::Edge, rules: &Rules, h: f64) -> Nodes {
/// Nodes of edge `ei` of `b` from its start to its end: every chord within the tolerance of
/// the curve (checked at the quarter points, so an S-bend in one segment is not missed), at
/// most `h` long, and within the rules' facet angle. The end nodes are the vertices' own
/// points, so edges meeting at a vertex meet exactly. A curve that does not evaluate somewhere
/// on its interval is an error naming the edge.
pub(crate) fn sample_edge(b: &brep::Brep, ei: usize, rules: &Rules, h: f64) -> Result<Nodes> {
let fail = |why: String| KernelError::Failed(format!("tessellation: edge {ei}: {why}"));
let edge = b.edges.get(ei).ok_or_else(|| fail("no such edge".into()))?;
let vertex = |k: usize| {
let v = edge.vertices.get(k).and_then(|&v| b.vertices.get(v as usize)).ok_or_else(|| fail("no such vertex".into()))?;
Ok::<Vec3, KernelError>(Vec3::new(v.point[0], v.point[1], v.point[2]))
};
let (front, back) = (vertex(0)?, vertex(1)?);
let tol = rules.tol;
let front = from_p3(edge.absolute_front().point());
let back = from_p3(edge.absolute_back().point());
let curve = edge.curve();
if matches!(curve, mt::Curve::Line(_)) {
if matches!(edge.curve, Curve::Line { .. }) {
// Evenly, as few segments as keep each at most `h`. The ends are the vertices' own
// points: `front + (back − front) · 1` is not `back` to the last bit, and the face that
// runs the edge backwards starts its loop with that node, where the face running it
@@ -56,92 +42,65 @@ pub(crate) fn sample_edge(edge: &mt::Edge, rules: &Rules, h: f64) -> Nodes {
if let Some(p) = pts.last_mut() {
*p = back;
}
return Nodes::plain(pts);
return Ok(Nodes { points: pts });
}
let (t0, t1) = curve.range_tuple();
let [t0, t1] = edge.interval;
if !(t0.is_finite() && t1.is_finite() && t1 > t0) {
return Nodes::plain(vec![front, back]);
return Err(fail(format!("interval [{t0}, {t1}]")));
}
// Our evaluators: the curve's own form where it has one (no allocation per point); for an
// intersection curve, Newton on both surfaces from seeded parameters (see `ssi`), which
// also gives each node's parameters on both faces; truck's evaluation only where that does
// not converge.
let fast = super::surface::eval_curve(&curve);
let ssi = if fast.is_none() { super::ssi::Intersection::new(&curve) } else { None };
type Pt = (Vec3, Option<[f64; 4]>);
let at = |t: f64| -> Option<Pt> {
match (&fast, &ssi) {
(Some(c), _) => c.eval(t).map(|(p, _)| (p, None)),
(None, Some(ix)) => ix.eval(t).map(|(p, q)| (p, Some(q))).or_else(|| Some((from_p3(curve.subs(t)), None))),
(None, None) => Some((from_p3(curve.subs(t)), None)),
}
.filter(|(p, _)| p.is_finite())
};
let fallback = || Nodes::plain(vec![front, back]);
let curve = CurveEval::new(&edge.curve).map_err(|e| fail(e.to_string()))?;
let at =
|t: f64| -> Result<Vec3> { curve.point(t).filter(|p| p.is_finite()).ok_or_else(|| fail(format!("the curve does not evaluate at t = {t}"))) };
// A closed curve's chord between its ends is empty: start it from a few segments. An open
// one starts whole, so an arc the tolerance leaves flat stays one segment.
let initial: u32 = if edge.absolute_front().id() == edge.absolute_back().id() { 4 } else { 1 };
let initial: u32 = if edge.vertices[0] == edge.vertices[1] { 4 } else { 1 };
let t_at = |i: u32| t0 + (t1 - t0) * f64::from(i) / f64::from(initial);
let mut ends: Vec<Pt> = Vec::with_capacity(initial as usize + 1);
for i in 0..=initial {
let Some(p) = at(t_at(i)) else { return fallback() };
ends.push(p);
}
let ends: Vec<Vec3> = (0..=initial).map(|i| at(t_at(i))).collect::<Result<_>>()?;
// Depth-first bisection, segment by segment, keeping the output ordered. Each segment
// carries its end points and middle, so every parameter is evaluated once (an
// intersection curve's point is a Newton solve).
struct Seg {
a: f64,
b: f64,
pa: Pt,
pb: Pt,
mid: Option<Pt>,
pa: Vec3,
pb: Vec3,
mid: Option<Vec3>,
depth: u32,
}
let Some(&first) = ends.first() else { return fallback() };
let mut out: Vec<Pt> = vec![(front, first.1)];
let mut out: Vec<Vec3> = vec![front];
let mut stack: Vec<Seg> = Vec::with_capacity(64);
for i in (0..initial).rev() {
let (Some(pa), Some(pb)) = (<[Pt]>::get(&ends, i as usize).copied(), <[Pt]>::get(&ends, i as usize + 1).copied()) else {
return fallback();
};
for (i, w) in ends.windows(2).enumerate().rev() {
let (Some(&pa), Some(&pb)) = (w.first(), w.get(1)) else { continue };
let i = i as u32;
stack.push(Seg { a: t_at(i), b: t_at(i + 1), pa, pb, mid: None, depth: 0 });
}
while let Some(sg) = stack.pop() {
let (a, b, pa, pb) = (sg.a, sg.b, sg.pa, sg.pb);
let m = 0.5 * (a + b);
let pm = match sg.mid {
Some(p) => Some(p),
None => at(m),
Some(p) => p,
None => at(m)?,
};
let (q1, q3) = (at(a + 0.25 * (b - a)), at(a + 0.75 * (b - a)));
let (q1, q3) = (at(a + 0.25 * (b - a))?, at(a + 0.75 * (b - a))?);
let done = sg.depth >= MAX_DEPTH || out.len() + stack.len() >= MAX_EDGE_NODES;
let fine = || {
let sag = [q1, pm, q3].iter().flatten().map(|q| q.0.dist_to_segment(pa.0, pb.0)).fold(0.0, f64::max);
let short = !(pa.0.dist(pb.0) > h * (1.0 + 1e-9));
let sag = [q1, pm, q3].iter().map(|q| q.dist_to_segment(pa, pb)).fold(0.0, f64::max);
let short = !(pa.dist(pb) > h * (1.0 + 1e-9));
// The two half chords of an arc turning θ differ by θ/2.
let straight = match (rules.turn, pm) {
(Some(t), Some(q)) => !t.exceeds(2.0 * turn(q.0 - pa.0, pb.0 - q.0), sag),
_ => true,
};
let straight = rules.turn.is_none_or(|t| !t.exceeds(2.0 * turn(pm - pa, pb - pm), sag));
short && straight && sag <= tol
};
match pm {
Some(pm) if !done && !fine() => {
stack.push(Seg { a: m, b, pa: pm, pb, mid: q3, depth: sg.depth + 1 });
stack.push(Seg { a, b: m, pa, pb: pm, mid: q1, depth: sg.depth + 1 });
}
_ => out.push(pb),
if !done && !fine() {
stack.push(Seg { a: m, b, pa: pm, pb, mid: Some(q3), depth: sg.depth + 1 });
stack.push(Seg { a, b: m, pa, pb: pm, mid: Some(q1), depth: sg.depth + 1 });
} else {
out.push(pb);
}
}
if out.len() < 2 {
return fallback();
}
if let Some(p) = out.last_mut() {
p.0 = back;
*p = back;
}
let on = ssi.map(|ix| (ix.surfaces().clone(), out.iter().map(|p| p.1).collect()));
Nodes { points: out.into_iter().map(|p| p.0).collect(), on }
Ok(Nodes { points: out })
}
/// Angle (radians) between two directions; zero when either is empty.
+148 -382
View File
@@ -1,20 +1,19 @@
//! Display tessellation: each face's triangulation in its own parameters, with every edge
//! sampled once so neighbouring faces share their boundary points exactly, handed over as a
//! [`TessPlan`] for the CPU or GPU evaluator.
//! SolidCraft's mesher: each face of a schema 4 record ([`brep::Brep`]) triangulated in its own
//! parameters, with every edge sampled once so neighbouring faces share their boundary points
//! exactly, handed over as a [`TessPlan`] for the CPU or GPU evaluator.
//!
//! Per face, cheapest first:
//! - planar faces: a constrained Delaunay triangulation of the edge nodes alone (a plane needs
//! no interior points);
//! - faces whose boundary runs along their parameter box (cylinder halves, sphere and torus
//! patches, fillets): a structured grid zipped to the edge nodes;
//! - four-cornered faces whose sides are not parameter lines (mitred fillet strips, cross
//! hole walls): a Coons blend of the four sides, zipped the same way;
//! - anything else (trimmed faces, holes): constrained Delaunay with interior seeds and
//! Ruppert refinement.
//!
//! A face the planner cannot handle (its surface has no evaluable form, a boundary that does
//! not project or crosses itself) fails the whole plan, and the caller meshes the body the
//! old way.
//! Per face:
//! 1. a cheaper layout where the face suits one: planar faces whose boundary runs along their
//! parameter box, and curved ones, take a structured grid zipped to the edge nodes; a
//! four-cornered face whose sides are not parameter lines (mitred fillet strips, cross hole
//! walls) a Coons blend of its four sides; a trimmed face the patches of its cuts or the
//! iso-line grid cut along its motorcycle traces;
//! 2. otherwise, the general method for every face: constrained Delaunay in the face's (u, v),
//! with interior seeds and Ruppert refinement, every node then evaluated on the exact
//! surface (and refined where a triangle sags off it);
//! 3. a face even that refuses (its loops cross or do not close in (u, v), or wind round a
//! seam or pole) is an error naming the face and why. Its cause is upstream, in the face's
//! boundary or parameters, and is fixed there; there is no other mesher to fall back to.
pub(crate) mod cdt;
pub(crate) mod collar;
@@ -28,17 +27,15 @@ pub(crate) mod pieces;
pub(crate) mod sag;
pub(crate) mod seam;
pub(crate) mod size;
pub(crate) mod ssi;
pub(crate) mod surface;
pub(crate) mod uv;
use std::collections::HashMap;
use solidcraft_geom::Vec3;
use solidcraft_geom::brep::{self, Curve, Loop, Shell};
use solidcraft_geom::tess::{EvalSurface, FacePlan, TessPlan};
use truck_modeling as mt;
use crate::body::{Solid, edge_order};
use crate::{KernelError, Result};
/// How the faces of a plan were meshed.
@@ -81,63 +78,111 @@ fn unsupported(what: impl std::fmt::Display) -> KernelError {
KernelError::Failed(format!("not supported yet: tessellation: {what}"))
}
/// Plan the display tessellation of `solid` with chord tolerance `tol` and, when given, a
/// facet angle no facet may turn more than (where it sags above the angle's floor).
/// `boundary_tol`: how far the body's boundaries may lie off their surfaces (mm) as the file it
/// was read from declares, `None` for a body this kernel built (see
/// [`crate::body::Body::boundary_tol`]).
pub(crate) fn plan(solid: &Solid, tol: f64, turn: Option<size::Turn>, boundary_tol: Option<f64>) -> Result<(TessPlan, PlanStats)> {
let faces: Vec<&mt::Face> = solid.face_iter().collect();
let (edges, edge_faces) = edge_order(faces.iter().copied());
let rules = size::Rules::new(tol, size::extent(solid.vertex_iter().map(|v| crate::body::from_p3(v.point()))), turn, boundary_tol);
/// The surface kind of a face, for an error that names it.
fn kind(s: &brep::Surface) -> &'static str {
match s {
brep::Surface::Plane { .. } | brep::Surface::PlaneThrough { .. } => "plane",
brep::Surface::Cylinder { .. } => "cylinder",
brep::Surface::Cone { .. } => "cone",
brep::Surface::Sphere { .. } => "sphere",
brep::Surface::Torus { .. } => "torus",
brep::Surface::Spline { .. } => "spline",
brep::Surface::RationalSpline { .. } => "rational spline",
brep::Surface::PlacedRevolution { .. } => "revolution",
}
}
/// Whether each face's normal is its surface's natural normal, as the shell that uses it says
/// (a face no shell lists keeps its surface's).
fn forward_faces(b: &brep::Brep) -> Vec<bool> {
let mut forward = vec![true; b.faces.len()];
for shell in &b.shells {
if let Shell::Faces { faces, .. } = shell {
for f in faces {
if let Some(slot) = forward.get_mut(f.face as usize) {
*slot = f.forward;
}
}
}
}
forward
}
/// The edges of each face's loops, in loop order.
fn face_edges(f: &brep::Face) -> impl Iterator<Item = &brep::OrientedEdge> {
f.loops.iter().flat_map(|l| match l {
Loop::Edges { edges } => edges.as_slice(),
Loop::Vertex { .. } => &[],
})
}
/// Plan the tessellation of `b` with chord tolerance `tol` and, when given, a facet angle no
/// facet may turn more than (where it sags above the angle's floor). Faces and edges are the
/// record's, in its order.
pub(crate) fn plan(b: &brep::Brep, tol: f64, turn: Option<size::Turn>) -> Result<(TessPlan, PlanStats)> {
let faces = &b.faces;
let forward = forward_faces(b);
// The faces each edge bounds, each once, in the order of the edge's uses.
let edge_faces: Vec<Vec<usize>> = b
.edges
.iter()
.map(|e| {
let mut fs: Vec<usize> = Vec::with_capacity(e.uses.len());
for u in &e.uses {
if !fs.contains(&(u.face as usize)) {
fs.push(u.face as usize);
}
}
fs
})
.collect();
let rules = size::Rules::new(tol, size::extent(b.vertices.iter().map(|v| Vec3::new(v.point[0], v.point[1], v.point[2]))), turn);
// Each face's surface and target side length, then every edge once at the smaller size of
// its faces; faces then read their nodes. A planar face costs microseconds and a curved
// one about a hundred, so the pool is used only where curved faces (or intersection
// curves) make up the work.
let evals: Vec<Option<EvalSurface>> = faces.iter().map(|f| surface::eval_surface(&f.surface())).collect();
let curved = evals.iter().filter(|e| !matches!(e, Some(EvalSurface::Plane { .. }))).count();
let sizes =
crate::par::map("face sizes", &evals, curved >= PARALLEL_SIZES, |eval| Ok(eval.as_ref().map(|s| size::face_size(s, &uv::domain(s), &rules))));
let sized: Vec<(Option<EvalSurface>, Option<(f64, [f64; 2])>)> =
evals.into_iter().zip(sizes).map(|(e, s)| s.map(|s| (e, s))).collect::<Result<_>>()?;
let face_h = |f: usize| sized.get(f).and_then(|(_, s)| s.map(|(h, _)| h)).unwrap_or(rules.max_len);
let evals: Vec<EvalSurface> = faces
.iter()
.enumerate()
.map(|(fi, f)| surface::eval_surface(&f.surface, f.domain).map_err(|e| unsupported(format!("face {fi} ({}): {e}", kind(&f.surface)))))
.collect::<Result<_>>()?;
let curved = evals.iter().filter(|e| !matches!(e, EvalSurface::Plane { .. })).count();
let sizes: Vec<(f64, [f64; 2])> =
crate::par::map("face sizes", &evals, curved >= PARALLEL_SIZES, |s| Ok(size::face_size(s, &uv::domain(s), &rules)))
.into_iter()
.collect::<Result<_>>()?;
let face_h = |f: usize| sizes.get(f).map_or(rules.max_len, |(h, _)| *h);
let edge_h: Vec<f64> = edge_faces.iter().map(|fs| fs.iter().map(|&f| face_h(f)).fold(rules.max_len, f64::min)).collect();
let jobs: Vec<(&mt::Edge, f64)> = edges.iter().zip(edge_h).collect();
let solved = edges.iter().filter(|e| matches!(e.curve(), mt::Curve::IntersectionCurve(_))).count();
let sampled = crate::par::map("edge nodes", &jobs, solved >= PARALLEL_CURVES || edges.len() >= PARALLEL_EDGES, |(e, h)| {
Ok(edges::sample_edge(e, &rules, *h))
});
let mut nodes: HashMap<_, edges::Nodes> = HashMap::with_capacity(edges.len());
for (e, n) in edges.iter().zip(sampled) {
nodes.insert(e.id(), n?);
}
relax_crowded_faces(&faces, &jobs, &rules, &mut nodes);
divide_collapsed_faces(&faces, &jobs, &rules, &mut nodes);
let indexed: Vec<(usize, &mt::Face)> = faces.iter().copied().enumerate().collect();
// A face's boundary nodes, loop by loop (each edge's last node is the next edge's first),
// with their parameters on the face's surface where the edge knows them (an intersection
// edge on this face's surface).
type Boundary = (Vec<Vec<Vec3>>, Vec<Vec<Option<[f64; 2]>>>);
let boundary = |face: &mt::Face, eval: &EvalSurface, nodes: &HashMap<mt::EdgeID, edges::Nodes>| -> Result<Boundary> {
let jobs: Vec<(usize, f64)> = edge_h.into_iter().enumerate().collect();
let solved = b.edges.iter().filter(|e| matches!(e.curve, Curve::SurfaceIntersection { .. })).count();
let mut nodes: Vec<edges::Nodes> = crate::par::map("edge nodes", &jobs, solved >= PARALLEL_CURVES || jobs.len() >= PARALLEL_EDGES, |(e, h)| {
edges::sample_edge(b, *e, &rules, *h)
})
.into_iter()
.collect::<Result<_>>()?;
relax_crowded_faces(b, &jobs, &rules, &mut nodes)?;
let indexed: Vec<usize> = (0..faces.len()).collect();
let planned = crate::par::map("face tessellation", &indexed, curved >= PARALLEL_FACES, |&fi| {
let (Some(face), Some(eval), Some(size)) = (faces.get(fi), evals.get(fi), sizes.get(fi)) else {
return Err(unsupported(format!("face {fi}: missing")));
};
// Boundary nodes, loop by loop (each edge's last node is the next edge's first). A
// vertex loop (a pole) bounds no area and has no nodes of its own.
let mut loops3: Vec<Vec<Vec3>> = Vec::new();
let mut known: Vec<Vec<Option<[f64; 2]>>> = Vec::new();
for wire in face.absolute_boundaries() {
let (mut pts, mut uvs) = (Vec::new(), Vec::new());
for e in wire.edge_iter() {
let n = nodes.get(&e.id()).ok_or_else(|| unsupported("edge without nodes"))?;
let (mut ep, mut eq) = (n.points.clone(), n.params_on(eval).unwrap_or_else(|| vec![None; n.points.len()]));
if !e.orientation() {
for l in &face.loops {
let Loop::Edges { edges } = l else { continue };
let mut pts = Vec::new();
for oe in edges {
let n = nodes.get(oe.edge as usize).ok_or_else(|| unsupported(format!("face {fi}: edge {} without nodes", oe.edge)))?;
let mut ep = n.points.clone();
if !oe.forward {
ep.reverse();
eq.reverse();
}
ep.pop();
eq.pop();
pts.extend(ep);
uvs.extend(eq);
}
if pts.len() >= 2 {
loops3.push(pts);
known.push(uvs);
}
}
Ok((loops3, known))
@@ -157,95 +202,16 @@ pub(crate) fn plan(solid: &Solid, tol: f64, turn: Option<size::Turn>, boundary_t
let (eval, size) = surface_of(fi)?;
let (loops3, known) = boundary(face, &eval, nodes)?;
let mut stats = PlanStats::default();
let replan = |stats: &mut PlanStats| match plan_face(fi, &eval, &loops3, &known, rules, size, stats) {
Ok(fp) => Ok(fp),
// A boundary that doubles straight back on itself (a zero-width slit in the face,
// which some STEP writers leave where two surfaces were trimmed apart) encloses no
// area but crosses itself in (u, v). Dropping the slits leaves the same face.
Err(first) => {
let (slit3, slitq) = drop_slits(&loops3, &known, rules.tol * 1e-2);
if slit3 == loops3 {
return Err(first);
}
// Nothing left but slits: a sliver face (two edges between the same two vertices,
// as STEP files write one) encloses no area, and the faces on either side already
// meet along its nodes.
if slit3.iter().all(|l| l.len() < 3) {
return Ok(FacePlan { face: u32::try_from(fi).unwrap_or(u32::MAX), ..FacePlan::default() });
}
*stats = PlanStats::default();
plan_face(fi, &eval, &slit3, &slitq, rules, size, stats).map_err(|_| first)
}
};
let fp = replan(&mut stats)?;
Ok((oriented_face(face, fp), eval, stats))
};
let mut planned = crate::par::map("face tessellation", &indexed, curved >= PARALLEL_FACES, |(fi, face)| plan_one(*fi, face, &nodes, &rules));
// A face refused at this level of detail (a boundary that comes out degenerate, or that a
// triangulation cannot follow, between nodes too far apart) has its edges divided again,
// finer, and it and the faces beside those edges, which share their nodes, are planned
// again from them.
if planned.iter().any(Result::is_err) {
let index_of: HashMap<mt::EdgeID, usize> = edges.iter().enumerate().map(|(i, e)| (e.id(), i)).collect();
let face_edges: Vec<Vec<usize>> = faces
.iter()
.map(|f| f.absolute_boundaries().iter().flat_map(|w| w.edge_iter().filter_map(|e| index_of.get(&e.id()).copied())).collect())
.collect();
let mut finer: HashMap<usize, size::Rules> = HashMap::new();
for factor in REFINE_STEPS {
let refused: Vec<usize> = planned.iter().enumerate().filter(|(_, r)| r.is_err()).map(|(i, _)| i).collect();
if refused.is_empty() {
break;
}
let fine = rules.finer(factor);
let mut touched: Vec<usize> = refused.iter().filter_map(|f| face_edges.get(*f)).flatten().copied().collect();
touched.sort_unstable();
touched.dedup();
let resampled = crate::par::map("edge nodes", &touched, touched.len() >= PARALLEL_EDGES, |i| {
Ok(jobs.get(*i).map(|(e, h)| (e.id(), edges::sample_edge(e, &fine, *h / factor))))
});
for (id, n) in resampled.into_iter().flatten().flatten() {
nodes.insert(id, n);
}
finer.extend(refused.iter().map(|f| (*f, fine)));
// The faces sharing a divided edge read its new nodes.
let mut again: Vec<usize> = touched.iter().filter_map(|i| edge_faces.get(*i)).flatten().copied().collect();
again.sort_unstable();
again.dedup();
let again: Vec<(usize, &mt::Face)> = again.into_iter().filter_map(|f| faces.get(f).map(|face| (f, *face))).collect();
let results = crate::par::map("face tessellation", &again, again.len() >= PARALLEL_FACES, |(fi, face)| {
plan_one(*fi, face, &nodes, finer.get(fi).unwrap_or(&rules))
});
for ((f, _), r) in again.iter().zip(results) {
if let Some(slot) = planned.get_mut(*f) {
*slot = r;
}
}
}
}
// A face refused however finely its edges are divided is filled flat from its boundary
// nodes (see `fallback`), so the rest of the body keeps its plan.
for (fi, slot) in planned.iter_mut().enumerate() {
if slot.is_ok() {
continue;
}
let (Some(face), Ok((eval, _))) = (faces.get(fi), surface_of(fi)) else { continue };
let Ok((loops3, _)) = boundary(face, &eval, &nodes) else { continue };
if let Some(fp) = fallback::fill(fi, &eval, &loops3, off_surface(&rules), rules.tol) {
*slot = Ok((oriented_face(face, fp), eval, PlanStats { filled: 1, ..PlanStats::default() }));
}
}
let oriented = |e: &mt::Edge| -> Option<Vec<Vec3>> {
let mut pts = nodes.get(&e.id())?.points.clone();
if !e.orientation() {
pts.reverse();
}
Some(pts)
};
let fp = plan_face(fi, kind(&face.surface), eval, &loops3, &rules, *size, &mut stats)?;
let fwd = forward.get(fi).copied().unwrap_or(true);
let fp =
FacePlan { flip: !fwd, triangles: if fwd { fp.triangles } else { fp.triangles.into_iter().map(|[a, b, c]| [a, c, b]).collect() }, ..fp };
Ok((fp, stats))
});
let mut plan = TessPlan::default();
let mut stats = PlanStats::default();
for r in planned {
let (fp, eval, s) = r?;
for (r, eval) in planned.into_iter().zip(evals) {
let (fp, s) = r?;
let surface = u32::try_from(plan.surfaces.len()).map_err(|_| unsupported("too many faces"))?;
plan.surfaces.push(eval);
plan.faces.push(FacePlan { surface: Some(surface), ..fp });
@@ -254,239 +220,48 @@ pub(crate) fn plan(solid: &Solid, tol: f64, turn: Option<size::Turn>, boundary_t
stats.delaunay += s.delaunay;
stats.filled += s.filled;
}
plan.edges = edges.iter().filter_map(oriented).collect();
plan.edges = nodes.into_iter().map(|n| n.points).collect();
plan.edge_faces = edge_faces.into_iter().map(|fs| fs.into_iter().map(|f| u32::try_from(f).unwrap_or(u32::MAX)).collect()).collect();
Ok((plan, stats))
}
/// Whether every curved face's boundary lies on the face's surface, within `accept`: a point
/// off the surface's own parameter range is off it. The planner needs this of every face; a
/// blend that runs on past the end of a neighbouring round's surface does not have it.
pub(crate) fn boundaries_on_surfaces(solid: &Solid, accept: f64) -> bool {
use mt::{BoundedCurve, ParametricCurve};
for face in solid.face_iter() {
let Some(eval) = surface::eval_surface(&face.surface()) else { continue };
if matches!(eval, EvalSurface::Plane { .. }) {
continue;
}
let dom = uv::domain(&eval);
let mut prev: Option<[f64; 2]> = None;
for e in face.edge_iter() {
let c = e.oriented_curve();
let (lo, hi) = c.range_tuple();
for k in 0..=32 {
let p = crate::body::from_p3(c.subs(lo + (hi - lo) * f64::from(k) / 32.0));
match uv::project(&eval, &dom, p, prev, accept) {
Some(uv) => prev = Some(uv),
None => return false,
}
}
}
}
true
}
/// Bounded work for the facet angle, as the reference mesher bounds it: a face whose boundary
/// would get more than `MAX_ANGLE_BOUNDARY` segments has its curved edges sampled again at twice
/// the angle, as often as it takes, down to the tolerance alone (a plate with a thousand small
/// holes is not worth a hundred times the work to draw each hole round). An edge keeps one set
/// of nodes for both its faces, at the coarser of their levels, so every rim of a crowded face
/// gets the same share whatever the order of the faces.
fn relax_crowded_faces(faces: &[&mt::Face], jobs: &[(&mt::Edge, f64)], rules: &size::Rules, nodes: &mut HashMap<mt::EdgeID, edges::Nodes>) {
use truck_meshalgo::tessellation::Deflection;
let Some(turn) = rules.turn else { return };
let h_of: HashMap<mt::EdgeID, (&mt::Edge, f64)> = jobs.iter().map(|(e, h)| (e.id(), (*e, *h))).collect();
let boundaries: Vec<Vec<mt::EdgeID>> =
faces.iter().map(|f| f.absolute_boundaries().iter().flat_map(|w| w.edge_iter().map(|e| e.id())).collect()).collect();
let mut level: HashMap<mt::EdgeID, u8> = HashMap::new();
for l in 1..=Deflection::MAX_RELAX {
let segments = |id: &mt::EdgeID| nodes.get(id).map_or(0, |n| n.points.len().saturating_sub(1));
let mut crowded: Vec<mt::EdgeID> = boundaries
/// Bounded work for the facet angle: a face whose boundary would get more than
/// [`size::MAX_ANGLE_BOUNDARY`] segments has its curved edges sampled again at twice the angle,
/// as often as it takes, down to the tolerance alone (a plate with a thousand small holes is
/// not worth a hundred times the work to draw each hole round). An edge keeps one set of nodes
/// for both its faces, at the coarser of their levels, so every rim of a crowded face gets the
/// same share whatever the order of the faces.
fn relax_crowded_faces(b: &brep::Brep, jobs: &[(usize, f64)], rules: &size::Rules, nodes: &mut [edges::Nodes]) -> Result<()> {
let Some(turn) = rules.turn else { return Ok(()) };
let boundaries: Vec<Vec<usize>> = b.faces.iter().map(|f| face_edges(f).map(|oe| oe.edge as usize).collect()).collect();
let curved = |e: usize| b.edges.get(e).is_some_and(|e| !matches!(e.curve, Curve::Line { .. }));
let mut level: Vec<u8> = vec![0; nodes.len()];
for l in 1..=size::MAX_RELAX {
let segments = |e: &usize| nodes.get(*e).map_or(0, |n| n.points.len().saturating_sub(1));
let mut crowded: Vec<usize> = boundaries
.iter()
.filter(|ids| ids.iter().map(segments).sum::<usize>() > Deflection::MAX_ANGLE_BOUNDARY)
.filter(|es| es.iter().map(segments).sum::<usize>() > size::MAX_ANGLE_BOUNDARY)
.flatten()
.filter(|id| level.get(*id).copied().unwrap_or(0) < l)
.filter(|id| h_of.get(*id).is_some_and(|(e, _)| !matches!(e.curve(), mt::Curve::Line(_))))
.filter(|e| level.get(**e).copied().unwrap_or(0) < l && curved(**e))
.copied()
.collect();
crowded.sort_unstable_by_key(|id| format!("{id:?}"));
crowded.sort_unstable();
crowded.dedup();
if crowded.is_empty() {
break;
}
let relaxed = size::Rules { turn: turn.relaxed(l), ..*rules };
for id in crowded {
if let Some((e, h)) = h_of.get(&id) {
nodes.insert(id, edges::sample_edge(e, &relaxed, *h));
level.insert(id, l);
for e in crowded {
let Some(&(_, h)) = jobs.get(e) else { continue };
if let Some(slot) = nodes.get_mut(e) {
*slot = edges::sample_edge(b, e, &relaxed, h)?;
}
if let Some(slot) = level.get_mut(e) {
*slot = l;
}
}
}
}
/// Every loop with its zero-width slits removed: wherever the boundary steps out to a node and
/// straight back to where it came from, the pair is dropped. A slit encloses no area, so the
/// face is the same without it, and without it the boundary no longer runs along itself. The
/// two sides need only agree within `eps` (a hundredth of the chord tolerance): a STEP writer
/// may draw them as two curves a few nanometres apart, which no mesh could tell apart anyway.
fn drop_slits(loops3: &[Vec<Vec3>], known: &[Vec<Option<[f64; 2]>>], eps: f64) -> (Vec<Vec<Vec3>>, Vec<Vec<Option<[f64; 2]>>>) {
/// Slits dropped from one loop before giving up (the search is a scan per slit).
const MAX_SLITS: usize = 4096;
let mut out3: Vec<Vec<Vec3>> = Vec::with_capacity(loops3.len());
let mut outq: Vec<Vec<Option<[f64; 2]>>> = Vec::with_capacity(loops3.len());
for (li, pts) in loops3.iter().enumerate() {
let mut pts = pts.clone();
let mut qs = known.get(li).cloned().unwrap_or_else(|| vec![None; pts.len()]);
qs.resize(pts.len(), None);
for _ in 0..MAX_SLITS {
let n = pts.len();
if n < 3 {
break;
}
let same = |a: usize, b: usize| pts.get(a).zip(pts.get(b)).is_some_and(|(p, q)| p.dist(*q) <= eps);
let tip = (0..n).find(|&k| same((k + n - 1) % n, (k + 1) % n));
let Some(k) = tip else { break };
// Drop the tip and the node that repeats the one before it.
let mut drop = [k, (k + 1) % n];
drop.sort_unstable();
for &i in drop.iter().rev() {
if i < pts.len() {
pts.remove(i);
}
if i < qs.len() {
qs.remove(i);
}
}
}
if pts.len() >= 2 {
out3.push(pts);
outq.push(qs);
}
}
(out3, outq)
}
/// Boundary nodes a loop keeps only one of: nodes whose parameters land on the node before them.
///
/// A boundary that runs a little off its surface (a STEP file writes its curves and surfaces to
/// its own uncertainty, so an edge may lie micrometres off the face it bounds) projects onto the
/// nearest surface point. Around a convex corner of the surface every point of such an edge is
/// nearest that corner, so a whole run of nodes comes out with the corner's parameters. The finer
/// the chord tolerance, the more nodes the run holds, and a boundary that repeats a point in
/// (u, v) encloses no area there and cannot be triangulated (#1721).
///
/// One node stands for the run. The nodes dropped are nearer to it than `SAME_NODE` on the
/// surface, so the face is the same without them.
fn drop_collapsed(l: &mut seam::Loop, speed: [f64; 2]) {
let eps = |d: usize| {
let s = speed.get(d).copied().unwrap_or(1.0);
if s.is_finite() && s > 0.0 { SAME_NODE / s } else { 0.0 }
};
let (eu, ev) = (eps(0), eps(1));
let same = |a: &[f64; 2], b: &[f64; 2]| (a[0] - b[0]).abs() <= eu && (a[1] - b[1]).abs() <= ev;
let (mut uv, mut pts) = (Vec::with_capacity(l.uv.len()), Vec::with_capacity(l.pts.len()));
for (q, p) in l.uv.iter().zip(&l.pts) {
if uv.last().is_some_and(|last| same(last, q)) {
continue;
}
uv.push(*q);
pts.push(*p);
}
// The loop is cyclic: its last node may land on its first.
while uv.len() > 2 && uv.last().zip(uv.first()).is_some_and(|(a, b)| same(a, b)) {
uv.pop();
pts.pop();
}
l.uv = uv;
l.pts = pts;
}
/// Bring every loop of a face into the same period as its outer boundary. A loop is projected
/// on its own, and a periodic surface repeats, so a hole can come out a whole turn away from the
/// boundary it lies in — where it no longer cuts the face, and the face is drawn over its hole.
fn align_periods(loops: &mut [seam::Loop], dom: &uv::Domain) {
for d in 0..2 {
let Some(per) = dom.period.get(d).copied().flatten().filter(|p| *p > 0.0) else { continue };
let middle = |l: &seam::Loop| {
let (lo, hi) = l.uv.iter().filter_map(|q| q.get(d)).fold((f64::MAX, f64::MIN), |(a, b), x| (a.min(*x), b.max(*x)));
0.5 * (lo + hi)
};
let Some(outer) = loops.first().map(middle) else { continue };
if !outer.is_finite() {
continue;
}
for l in loops.iter_mut().skip(1) {
let shift = ((outer - middle(l)) / per).round() * per;
if shift != 0.0 && shift.is_finite() {
for q in l.uv.iter_mut() {
if let Some(x) = q.get_mut(d) {
*x += shift;
}
}
}
}
}
}
/// Nodes a face's boundary needs before it can hold a triangle.
const MIN_BOUNDARY_NODES: usize = 3;
/// A face whose whole boundary comes to fewer than three nodes has no triangle to stand on: it is
/// a sliver no wider than the tolerance (a chord with the shallow arc beside it, which the edge
/// sampler rightly leaves as one segment each). Its edges are sampled again, halved until the
/// boundary has nodes enough, so the sliver is drawn instead of failing the whole body's plan.
/// The nodes stay shared, so the faces beside it follow the same points.
fn divide_collapsed_faces(faces: &[&mt::Face], jobs: &[(&mt::Edge, f64)], rules: &size::Rules, nodes: &mut HashMap<mt::EdgeID, edges::Nodes>) {
/// Halvings before giving up (a boundary of two edges reaches four nodes in one).
const MAX_HALVINGS: usize = 8;
let h_of: HashMap<mt::EdgeID, (&mt::Edge, f64)> = jobs.iter().map(|(e, h)| (e.id(), (*e, *h))).collect();
for f in faces {
let ids: Vec<mt::EdgeID> = f.absolute_boundaries().iter().flat_map(|w| w.edge_iter().map(|e| e.id())).collect();
let count = |nodes: &HashMap<mt::EdgeID, edges::Nodes>| {
ids.iter().map(|id| nodes.get(id).map_or(0, |n| n.points.len().saturating_sub(1))).sum::<usize>()
};
if count(nodes) >= MIN_BOUNDARY_NODES {
continue;
}
for _ in 0..MAX_HALVINGS {
for id in &ids {
let Some((e, h)) = h_of.get(id) else { continue };
let longest =
nodes.get(id).map_or(0.0, |n| n.points.windows(2).filter_map(|w| Some(w.first()?.dist(*w.get(1)?))).fold(0.0, f64::max));
let finer = if longest > 0.0 { (longest * 0.5).min(*h) } else { *h };
nodes.insert(*id, edges::sample_edge(e, rules, finer));
}
if count(nodes) >= MIN_BOUNDARY_NODES {
break;
}
}
}
}
/// How far a boundary node may lie off its surface and still be projected onto it (see the note
/// in [`plan_face`]).
fn off_surface(rules: &size::Rules) -> f64 {
(rules.extent * ON_SURFACE).max(rules.boundary_tol.unwrap_or(0.0)).max(1e-6)
}
/// Whether a face of `solid` has a loop that runs along a seam edge between two distinct
/// vertices and straight back along it, the two uses one after the other (they meet at a pole):
/// a whole sphere as one face bounded by its meridian alone, as SMLib writes it, or a sphere
/// face whose meridian is split where a trim crosses it. Truck's mesher projects both uses onto
/// one parameter line and meshes nothing; the display plan opens the loop across the period
/// ([`open_pole_to_pole`], and [`open_at_pole`] for a seam from one pole to a rim).
pub(crate) fn has_pole_to_pole_seam(solid: &Solid) -> bool {
solid.face_iter().any(|f| {
f.boundaries().iter().any(|w| {
let edges: Vec<&mt::Edge> = w.edge_iter().collect();
let n = edges.len();
n >= 2
&& (0..n).any(|k| match (edges.get(k), edges.get((k + 1) % n)) {
(Some(a), Some(b)) => a.id() == b.id() && a.orientation() != b.orientation() && a.front() != a.back(),
_ => false,
})
})
})
Ok(())
}
/// Twice the signed area of a parameter polygon.
@@ -636,9 +411,9 @@ fn holes_beside_outer(dom: &uv::Domain, loops: &mut [seam::Loop]) {
/// `size`: the face's target side length (mm) and the surface's mean speed along u and v.
fn plan_face(
fi: usize,
kind: &str,
s: &EvalSurface,
loops3: &[Vec<Vec3>],
known: &[Vec<Option<[f64; 2]>>],
rules: &size::Rules,
size: (f64, [f64; 2]),
stats: &mut PlanStats,
@@ -646,26 +421,17 @@ fn plan_face(
let tol = rules.tol;
let (h, speed) = size;
let dom = uv::domain(s);
// How far a boundary node may lie off its surface and still be projected onto it: a property
// of the body, not of the mesh asked for, so a finer chord tolerance never refuses a boundary
// a coarser one accepted, and zooming in never drops a face (#1721). A hundredth of the body,
// or more where the file it was read from declares more; a file's declared accuracy never
// lowers it, since writers' edges stray farther from their surfaces than they declare.
let accept = off_surface(rules);
let fail = |why: &str| unsupported(format!("face {fi}: {why}"));
let accept = (tol * 10.0).max(1e-6);
let fail = |why: &str| unsupported(format!("face {fi} ({kind}): {why}"));
// Project every loop, unwrapping periodic parameters along it.
let mut projected: Vec<seam::Loop> = Vec::with_capacity(loops3.len());
for (li, pts) in loops3.iter().enumerate() {
let mut luv: Vec<[f64; 2]> = Vec::with_capacity(pts.len());
let mut prev: Option<[f64; 2]> = None;
for (k, p) in pts.iter().enumerate() {
// Known parameters (an intersection edge's), else a projection seeded by the
// previous node.
let given = known.get(li).and_then(|l| l.get(k)).copied().flatten();
let mut q = match given {
Some(q) => q,
None => uv::project(s, &dom, *p, prev, accept).ok_or_else(|| fail("boundary does not lie on the surface"))?,
};
// A projection seeded by the previous node.
let mut q = uv::project(s, &dom, *p, prev, accept)
.ok_or_else(|| fail(&format!("boundary node {k} of loop {li} at {p:?} does not lie on the surface")))?;
if let Some(pr) = prev {
for d in 0..2 {
q[d] = uv::unwrap(q[d], pr[d], dom.period[d]);
-115
View File
@@ -384,118 +384,3 @@ fn seam_nodes(s: &EvalSurface, swap: bool, x: f64, y0: f64, y1: f64, rules: &Rul
let inner = ys.get(1..ys.len() - 1)?;
Some(inner.iter().map(|&y| (y, s.eval(uv(y)[0], uv(y)[1]).0)).collect())
}
/// Whether a face of `solid` has a boundary loop turning more than once around the axis its
/// surface turns about (a helical trim): a surface of revolution's own axis, or the axis a
/// spline's iso-line winds around (the flank of a screw sweep, as SMLib writes a modelled
/// thread's groove, whose bore or shaft it splits into one face per turn). Truck's mesher cannot
/// mesh such a face (its triangles cut across the solid, or its triangulation fails), so the
/// reference mesh is made from the display plan instead.
pub(crate) fn turns_more_than_once(solid: &crate::body::Solid) -> bool {
use truck_meshalgo::prelude::*;
use truck_modeling as mt;
let tau = std::f64::consts::TAU;
for face in solid.face_iter() {
let Some((o, k)) = winding_axis(&face.surface()) else { continue };
let e1 = k.cross(if k.x.abs() < 0.9 { Vec3::X } else { Vec3::Y }).normalized().unwrap_or(Vec3::X);
let e2 = k.cross(e1);
let angle = |p: mt::Point3| {
let d = crate::body::from_p3(p) - o;
d.dot(e2).atan2(d.dot(e1))
};
for wire in face.boundaries() {
let (mut total, mut lo, mut hi) = (0.0f64, 0.0f64, 0.0f64);
let mut prev: Option<f64> = None;
for e in wire.edge_iter() {
let c = e.oriented_curve();
let (t0, t1) = c.range_tuple();
// Samples dense enough that no step turns half way round.
let mut n = 16usize;
let steps = loop {
let xs: Vec<f64> = (0..=n).map(|i| angle(c.subs(t0 + (t1 - t0) * i as f64 / n as f64))).collect();
let ok = xs.windows(2).all(|w| half_turn(w[1] - w[0]).abs() < 1.0);
if ok || n >= 4096 {
break xs;
}
n *= 4;
};
for x in steps {
if let Some(p) = prev {
total += half_turn(x - p);
lo = lo.min(total);
hi = hi.max(total);
}
prev = Some(x);
}
}
if hi - lo > tau * 1.01 {
return true;
}
}
}
false
}
/// An angle step taken the short way round, in (−π, π].
fn half_turn(x: f64) -> f64 {
let pi = std::f64::consts::PI;
(x + pi).rem_euclid(std::f64::consts::TAU) - pi
}
/// Samples per knot span of a spline's iso-line in [`winding_axis`], and their bounds.
const ISO_SPAN_SAMPLES: usize = 4;
const ISO_MIN_SAMPLES: usize = 64;
const ISO_MAX_SAMPLES: usize = 8192;
/// The axis (a point on it, its unit direction) a face's surface turns about, if it turns at
/// all: a surface of revolution's own axis; for a spline, the axis an iso-line through the
/// middle of the other parameter winds more than once around (a screw sweep's flank, a coil's
/// wall). That axis is the mean binormal of the iso-line (each pair of successive tangents
/// turns about it), through the mean of its samples; `None` for a spline neither iso-line of
/// which turns more than once about it.
fn winding_axis(surface: &truck_modeling::Surface) -> Option<(Vec3, Vec3)> {
use truck_meshalgo::prelude::*;
use truck_modeling as mt;
if let mt::Surface::RevolutedCurve(proc) = surface {
let rev = proc.entity();
let m = *proc.transform();
let o = crate::body::from_p3(m.transform_point(rev.origin()));
let a = m.transform_vector(rev.axis());
return Vec3::new(a.x, a.y, a.z).normalized().map(|k| (o, k));
}
if matches!(surface, mt::Surface::Plane(_)) {
return None;
}
let s = super::surface::eval_surface(surface)?;
let EvalSurface::Nurbs { knots, .. } = &s else { return None };
let dom = super::uv::domain(&s);
let tau = std::f64::consts::TAU;
(0..2).find_map(|d| {
let (a, b) = dom.range[d]?;
let (c0, c1) = dom.range[1 - d]?;
let across = 0.5 * (c0 + c1);
let n = knots.get(d).map_or(0, |k| k.len()).saturating_mul(ISO_SPAN_SAMPLES).clamp(ISO_MIN_SAMPLES, ISO_MAX_SAMPLES);
let pts: Vec<Vec3> = (0..=n)
.filter_map(|i| {
let t = a + (b - a) * i as f64 / n as f64;
s.derivs(if d == 0 { t } else { across }, if d == 0 { across } else { t }).map(|(p, _, _)| p)
})
.collect();
if pts.len() != n + 1 {
return None;
}
let tangents: Vec<Vec3> = pts.windows(2).filter_map(|w| (w[1] - w[0]).normalized()).collect();
let binormal = tangents.windows(2).fold(Vec3::ZERO, |acc, w| acc + w[0].cross(w[1]));
let k = binormal.normalized()?;
let o = pts.iter().fold(Vec3::ZERO, |acc, p| acc + *p) * (1.0 / pts.len() as f64);
let e1 = k.cross(if k.x.abs() < 0.9 { Vec3::X } else { Vec3::Y }).normalized()?;
let e2 = k.cross(e1);
let angles: Vec<f64> = pts.iter().map(|p| (*p - o).dot(e2).atan2((*p - o).dot(e1))).collect();
// Too coarse to follow the turns (a step of a radian or more): not taken as winding.
let steps: Vec<f64> = angles.windows(2).map(|w| half_turn(w[1] - w[0])).collect();
if steps.iter().any(|x| x.abs() >= 1.0) {
return None;
}
(steps.iter().sum::<f64>().abs() > tau * 1.01).then_some((o, k))
})
}
+11 -5
View File
@@ -26,6 +26,13 @@ const MIN_LEN_FRACTION: f64 = 1.0 / 4.0;
/// Curvature samples per parameter direction.
const SAMPLES: usize = 9;
/// Most segments a face's boundary may get from the facet angle: past it, the face's curved
/// edges are sampled again at twice the angle (a plate with a thousand small holes is not worth
/// a hundred times the work to draw each hole round).
pub(crate) const MAX_ANGLE_BOUNDARY: usize = 4096;
/// How often the angle is doubled for a crowded face before only the chord tolerance is left.
pub(crate) const MAX_RELAX: u8 = 4;
/// The most one facet may turn (radians): an edge segment's tangent or a cell's surface normal.
/// The angle divides only where a facet would sag more than `floor` (mm), so a radius far below
/// the tolerance keeps its chord facets.
@@ -41,12 +48,11 @@ impl Turn {
sag > self.floor && angle > self.angle
}
/// The angle doubled `level` times, as the reference mesher relaxes it for a crowded face;
/// from its last level, or past half a turn, no angle at all (the tolerance alone).
/// The angle doubled `level` times, for a crowded face (see [`MAX_ANGLE_BOUNDARY`]); from
/// level [`MAX_RELAX`], or past half a turn, no angle at all (the tolerance alone).
pub(crate) fn relaxed(&self, level: u8) -> Option<Turn> {
use truck_meshalgo::tessellation::Deflection;
let angle = self.angle * f64::from(1u32 << u32::from(level.min(Deflection::MAX_RELAX)));
(level == 0 || (level < Deflection::MAX_RELAX && angle < std::f64::consts::PI)).then_some(Turn { angle, ..*self })
let angle = self.angle * f64::from(1u32 << u32::from(level.min(MAX_RELAX)));
(level == 0 || (level < MAX_RELAX && angle < std::f64::consts::PI)).then_some(Turn { angle, ..*self })
}
}
-282
View File
@@ -1,282 +0,0 @@
//! Points on a surface–surface intersection edge, on our own surface evaluators.
//!
//! truck stores an intersection edge as a leader curve (a polyline or B-spline near the
//! intersection) and its two surfaces, and finds each point by projecting the leader point onto
//! each surface in turn from no starting guess: a nearest-point search per surface per point.
//! Here the intersection is solved exactly at a set of leader parameters, and a cubic B-spline
//! is fitted through those points together with both surfaces' parameters there (a smooth
//! leader with its curves on both surfaces). The B-spline runs over the chord length of the
//! solved points, never over the curve's own parameter: truck's parameter can change speed
//! abruptly (a polyline leader with short and long spans), and a cubic interpolated over it
//! rings there, stepping back along the curve (Lee, "Choosing nodes in parametric curve
//! interpolation", CAD 21, 1989). The curve's parameter reaches the chord length through the
//! piecewise-linear map between the seeds, which is monotone. Each point is then refined from
//! the B-spline by Newton's method on the four unknowns (u1, v1, u2, v2): S1(u1, v1) = S2(u2, v2), held on the
//! plane through the B-spline point normal to the B-spline (Barnhill & Kersey, "A marching
//! method for parametric surface/surface intersection", CAGD 7, 1990; Patrikalakis & Maekawa,
//! "Shape Interrogation for CAD/CAM", 2002, ch. 5). Every point is independent once seeded. Where the surfaces are close to
//! tangent along the edge (the system becomes singular) or Newton does not converge, the caller
//! uses truck's evaluation.
use solidcraft_geom::Vec3;
use solidcraft_geom::bspline::CurveN;
use solidcraft_geom::tess::EvalSurface;
use truck_meshalgo::prelude::*;
use truck_modeling as mt;
use super::uv::{self, Domain};
use crate::body::from_p3;
/// Leader parameters at which both surfaces' parameters are found up front.
const SEEDS: usize = 32;
/// Newton iterations per point (two or three suffice from a seed).
const MAX_ITER: usize = 8;
/// Below this sine of the angle between the surface normals the intersection is treated as
/// near-tangent: points there are left to truck.
const MIN_SIN: f64 = 0.05;
/// An intersection edge ready for point evaluation.
pub(crate) struct Intersection {
surfaces: [EvalSurface; 2],
domains: [Domain; 2],
/// The smooth leader over the chord length of the seeds: the intersection point (x, y, z)
/// and both surfaces' parameters (u1, v1, u2, v2), periodic directions unwrapped along it.
leader: CurveN<7>,
/// The curve's parameter at each seed and the chord length there, both increasing.
seeds: Vec<(f64, f64)>,
/// Convergence tolerance (mm).
eps: f64,
}
impl Intersection {
/// `None` unless `curve` is an intersection curve whose surfaces have evaluators and whose
/// leader projects onto both.
pub(crate) fn new(curve: &mt::Curve) -> Option<Self> {
let mt::Curve::IntersectionCurve(ic) = curve else { return None };
let surfaces = [super::surface::eval_surface(ic.surface0())?, super::surface::eval_surface(ic.surface1())?];
let domains = [uv::domain(&surfaces[0]), uv::domain(&surfaces[1])];
let (t0, t1) = curve.range_tuple();
if !(t0.is_finite() && t1.is_finite() && t1 > t0) {
return None;
}
let leader = |t: f64| match ic.leader() {
mt::Leader::Polyline(l) => (from_p3(l.subs(t)), from_v3(l.der(t))),
mt::Leader::BSpline(l) => (from_p3(l.subs(t)), from_v3(l.der(t))),
};
// The leader lies within the curve's tolerance of both surfaces.
let accept = (ic.tolerance() * 100.0).max(1e-6);
let scale = (0..=SEEDS).map(|k| leader(t0 + (t1 - t0) * k as f64 / SEEDS as f64).0.len()).fold(1.0, f64::max);
let mut it = Intersection {
surfaces,
domains,
leader: CurveN { degree: 1, knots: Vec::new(), ctrl: Vec::new() },
seeds: Vec::new(),
eps: scale * 1e-10,
};
let (mut params, mut points): (Vec<f64>, Vec<[f64; 7]>) = (Vec::with_capacity(SEEDS + 1), Vec::with_capacity(SEEDS + 1));
let mut prev: Option<[f64; 4]> = None;
for k in 0..=SEEDS {
let t = t0 + (t1 - t0) * k as f64 / SEEDS as f64;
let (p, d) = leader(t);
let mut q = [0.0; 4];
for (i, (s, dom)) in it.surfaces.iter().zip(&it.domains).enumerate() {
let hint = prev.map(|w| [w[2 * i], w[2 * i + 1]]);
let mut uv = uv::project(s, dom, p, hint, accept)?;
if let Some(h) = hint {
for d in 0..2 {
uv[d] = uv::unwrap(uv[d], h[d], dom.period[d]);
}
}
q[2 * i] = uv[0];
q[2 * i + 1] = uv[1];
}
// The fit takes only points exactly on the intersection: a leader point Newton
// cannot place (near-tangent, or the solution leaves a surface's domain where a
// polyline leader overshoots a vertex) is off the curve and would bend the fit back
// along it. The ends stay either way, so the fit spans the whole curve.
let solved = d.normalized().and_then(|dir| it.newton(p, dir, q));
prev = Some(solved.map_or(q, |(_, q)| q));
let (p, q) = match solved {
Some(x) => x,
None if k == 0 || k == SEEDS => (p, q),
None => continue,
};
// A seed that lands on the previous one adds no chord and stays out of the fit
// (whose parameters must increase).
let s = match it.seeds.last().zip(points.last()) {
Some((&(_, s), &[x, y, z, ..])) => {
let chord = p.dist(Vec3::new(x, y, z));
if !(chord > it.eps) {
continue;
}
s + chord
}
None => 0.0,
};
it.seeds.push((t, s));
params.push(s);
points.push([p.x, p.y, p.z, q[0], q[1], q[2], q[3]]);
}
let degree = points.len().checked_sub(1)?.min(3);
it.leader = CurveN::interpolate(&params, &points, degree).ok()?;
Some(it)
}
/// The point at curve parameter `t` and both surfaces' parameters there, or `None`
/// (near-tangent, or Newton failed).
pub(crate) fn eval(&self, t: f64) -> Option<(Vec3, [f64; 4])> {
let (x, dx) = self.leader.eval(self.chord_at(t)?)?;
let dir = Vec3::new(dx[0], dx[1], dx[2]).normalized()?;
self.newton(Vec3::new(x[0], x[1], x[2]), dir, [x[3], x[4], x[5], x[6]])
}
/// The chord length at curve parameter `t`: linear between the seeds, clamped to the ends.
fn chord_at(&self, t: f64) -> Option<f64> {
let (&(ta, sa), &(tb, sb)) = (self.seeds.first()?, self.seeds.last()?);
if !(t > ta) {
return Some(sa);
}
if !(t < tb) {
return Some(sb);
}
let k = self.seeds.partition_point(|&(tk, _)| tk <= t);
let (Some(&(t0, s0)), Some(&(t1, s1))) = (<[_]>::get(&self.seeds, k.checked_sub(1)?), <[_]>::get(&self.seeds, k)) else { return None };
Some(s0 + (s1 - s0) * (t - t0) / (t1 - t0))
}
/// The two surfaces, in the order of the parameters [`Intersection::eval`] gives.
pub(crate) fn surfaces(&self) -> &[EvalSurface; 2] {
&self.surfaces
}
/// Newton from the parameters `x` on S1 = S2, held on the plane through `l` normal to `dir`:
/// the point and its parameters, or `None` (near-tangent, singular, or not converged).
fn newton(&self, l: Vec3, dir: Vec3, mut x: [f64; 4]) -> Option<(Vec3, [f64; 4])> {
for _ in 0..MAX_ITER {
let (Some((p1, u1, v1)), Some((p2, u2, v2))) = (self.surfaces[0].derivs(x[0], x[1]), self.surfaces[1].derivs(x[2], x[3])) else {
return None;
};
let gap = p1 - p2;
let plane = dir.dot(p1 - l);
let n = u1.cross(v1).normalized()?.cross(u2.cross(v2).normalized()?);
if n.len() < MIN_SIN {
return None;
}
if gap.len() <= self.eps && plane.abs() <= self.eps {
return Some(((p1 + p2) * 0.5, x));
}
// J · dx = −F, F = (S1 − S2, dir · (S1 − L)).
let j = [[u1.x, v1.x, -u2.x, -v2.x], [u1.y, v1.y, -u2.y, -v2.y], [u1.z, v1.z, -u2.z, -v2.z], [dir.dot(u1), dir.dot(v1), 0.0, 0.0]];
let dx = solve4(j, [-gap.x, -gap.y, -gap.z, -plane])?;
for (xi, d) in x.iter_mut().zip(dx) {
*xi += d;
}
for i in 0..2 {
let [u, v] = self.domains[i].clamp([x[2 * i], x[2 * i + 1]]);
x[2 * i] = u;
x[2 * i + 1] = v;
}
}
None
}
}
fn from_v3(v: mt::Vector3) -> Vec3 {
Vec3::new(v.x, v.y, v.z)
}
/// `a · x = b` for a 4 × 4 system by Gaussian elimination with partial pivoting; `None` when
/// singular.
fn solve4(mut a: [[f64; 4]; 4], mut b: [f64; 4]) -> Option<[f64; 4]> {
let scale = a.iter().flatten().fold(0.0f64, |m, x| m.max(x.abs()));
if !(scale > 0.0) {
return None;
}
for c in 0..4 {
let p = (c..4).max_by(|&i, &j| a[i][c].abs().total_cmp(&a[j][c].abs()))?;
if !(a[p][c].abs() > 1e-12 * scale) {
return None;
}
a.swap(c, p);
b.swap(c, p);
let pivot = a[c];
for r in c + 1..4 {
let f = a[r][c] / pivot[c];
for (x, p) in a[r].iter_mut().zip(pivot).skip(c) {
*x -= f * p;
}
b[r] -= f * b[c];
}
}
let mut x = [0.0; 4];
for c in (0..4).rev() {
let s: f64 = (c + 1..4).map(|k| a[c][k] * x[k]).sum();
x[c] = (b[c] - s) / a[c][c];
}
x.iter().all(|v| v.is_finite()).then_some(x)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn solves_a_four_by_four_system() {
let a = [[2.0, 1.0, 0.0, 0.0], [1.0, 3.0, 1.0, 0.0], [0.0, 1.0, 4.0, 1.0], [0.0, 0.0, 1.0, 5.0]];
let x = [1.0, -2.0, 0.5, 3.0];
let b = [0, 1, 2, 3].map(|r| (0..4).map(|c| a[r][c] * x[c]).sum::<f64>());
let Some(got) = solve4(a, b) else { panic!("solves") };
for (g, w) in got.iter().zip(x) {
assert!((g - w).abs() < 1e-12);
}
assert!(solve4([[1.0; 4]; 4], [1.0; 4]).is_none(), "singular");
}
/// Points on the intersection edges of a boolean (a cross hole: two cylinders) lie on both
/// surfaces, on truck's curve (the parameterisation follows our smooth leader, so the same
/// parameter may sit at a slightly different place along it), and advance along it.
#[test]
fn intersection_points_lie_on_both_surfaces_and_on_truck_s_curve() {
let Ok(pin) = crate::cylinder(Vec3::new(5.0, 5.0, -1.0), Vec3::Z, 2.0, 6.0) else { panic!("pin") };
let Ok(cross) = crate::cylinder(Vec3::new(5.0, -1.0, 2.0), Vec3::Y, 1.0, 12.0) else { panic!("cross") };
let Ok(Some(body)) = crate::boolean(&pin, &cross, crate::BoolOp::Cut) else { panic!("cut") };
let mut checked = 0;
for e in body.solid.edge_iter() {
let curve = e.curve();
let Some(ix) = Intersection::new(&curve) else { continue };
let (t0, t1) = curve.range_tuple();
let dense: Vec<Vec3> = (0..=1000).map(|k| from_p3(curve.subs(t0 + (t1 - t0) * f64::from(k) / 1000.0))).collect();
let near = |p: Vec3| dense.windows(2).map(|w| p.dist_to_segment(w[0], w[1])).fold(f64::MAX, f64::min);
// Arc length along truck's curve to the point nearest `p`.
let along = |p: Vec3| {
let (mut run, mut best) = (0.0, (f64::MAX, 0.0));
for w in dense.windows(2) {
let (a, b) = (w[0], w[1]);
let len = a.dist(b);
let f = if len > 0.0 { ((p - a).dot(b - a) / (len * len)).clamp(0.0, 1.0) } else { 0.0 };
let d = p.dist(a + (b - a) * f);
if d < best.0 {
best = (d, run + f * len);
}
run += len;
}
best.1
};
let mut last: Option<Vec3> = None;
for k in 0..=50 {
let t = t0 + (t1 - t0) * f64::from(k) / 50.0;
let Some((p, _)) = ix.eval(t) else { continue };
for (s, dom) in ix.surfaces.iter().zip(&ix.domains) {
assert!(uv::project(s, dom, p, None, 1e-8).is_some(), "t {t}: {p:?} off a surface");
}
assert!(near(p) < 1e-4, "t {t}: {p:?} is {} from truck's curve", near(p));
if let Some(q) = last {
assert!(along(p) > along(q), "t {t}: the points advance along the curve");
}
last = Some(p);
checked += 1;
}
}
assert!(checked > 100, "{checked} points checked");
}
}
+103 -110
View File
@@ -1,119 +1,112 @@
//! 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.
//! Surface records ([`brep::Surface`]) in the evaluator's form ([`EvalSurface`]), which the
//! planner and the CPU and GPU evaluators share. Planes, splines and revolutions keep their
//! own parameters ([`SurfaceEval`] holds them in that form already). A cylinder, cone, sphere
//! or torus is the revolution it is, exactly: its generator (a line, or a circle as an exact
//! rational arc) turned about its axis, the angle as the revolution's parameter. The planner
//! finds a boundary's parameters by projecting its nodes, so the generator's own parameter
//! need not be the record's height or latitude.
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 solidcraft_geom::brep::eval::SurfaceEval;
use solidcraft_geom::brep::{self, Frame};
use solidcraft_geom::tess::{EvalCurve, EvalSurface};
use crate::body::from_p3;
use crate::{KernelError, Result};
/// `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() }
}
fn v3(p: [f64; 3]) -> Vec3 {
Vec3::new(p[0], p[1], p[2])
}
/// No transform: the revolution as it is.
const IDENTITY: [[f64; 4]; 3] = [[1.0, 0.0, 0.0, 0.0], [0.0, 1.0, 0.0, 0.0], [0.0, 0.0, 1.0, 0.0]];
/// `surface` in the evaluator's form. `domain` is the face's parameter box in the record's
/// parameters, `[[u0, u1], [v0, v1]]`: a cylinder's or cone's generator spans the face's
/// heights (an unbounded line has no parameter box to project into).
pub(crate) fn eval_surface(surface: &brep::Surface, domain: [[f64; 2]; 2]) -> Result<EvalSurface> {
use std::f64::consts::{FRAC_PI_2, TAU};
let invalid = |what: &str| KernelError::Invalid(format!("tessellation: {what}"));
let axes = |f: &Frame| {
let (o, x, y) = (v3(f.origin), v3(f.x_axis), v3(f.y_axis));
(o, x, x.cross(y))
};
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)?];
if !(knots[0].len() == nu + pu + 1 && knots[1].len() == nv + pv + 1) {
return None;
}
let closed = [closed_along(0, &ctrl, [nu, nv], [pu, pv], &knots), closed_along(1, &ctrl, [nu, nv], [pu, pv], &knots)];
Some(EvalSurface::Nurbs { degree: [pu, pv], knots, count: [nu, nv], ctrl, closed })
}
/// Does the surface join itself along direction `d` (a tube's way round)? Its knots there must
/// be clamped, so the ends of the range are the first and last rows of control points, and
/// those two rows must be the same points: the join is then the parameter line `d = end`, and
/// the direction is periodic.
fn closed_along(d: usize, ctrl: &[[f64; 4]], count: [usize; 2], degree: [usize; 2], knots: &[Vec<f64>; 2]) -> bool {
let (n, p) = (count[d], degree[d]);
if n < 2 {
return false;
}
let Some(k) = knots.get(d) else { return false };
let Some((&a, &b)) = k.first().zip(k.last()) else { return false };
// Clamped: degree + 1 knots equal at each end, and a range to speak of.
if !(b > a && k.iter().take(p + 1).all(|x| *x == a) && k.iter().rev().take(p + 1).all(|x| *x == b)) {
return false;
}
// Control point `(i, j)` with `i` along u, as a point.
let at =
|i: usize, j: usize| ctrl.get(j * count[0] + i).and_then(|h| (h[3].abs() > 1e-300).then(|| Vec3::new(h[0] / h[3], h[1] / h[3], h[2] / h[3])));
let along_d = |x: usize, t: usize| if d == 0 { at(x, t) } else { at(t, x) };
let (mut extent, mut gap) = (0.0f64, 0.0f64);
for t in 0..count[1 - d] {
let (Some(first), Some(last)) = (along_d(0, t), along_d(n - 1, t)) else { return false };
extent = extent.max(first.len()).max(last.len());
gap = gap.max(first.dist(last));
}
gap <= 1e-9 * extent.max(1.0)
}
/// 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 revolved = |profile: EvalCurve, origin: Vec3, axis: Vec3| -> Result<EvalSurface> {
let axis = axis.normalized().ok_or_else(|| invalid("an analytic surface's frame"))?;
Ok(EvalSurface::Revolved { profile, origin, axis, transform: IDENTITY, swap: false })
};
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 })
// The generator of a cylinder or cone over the face's heights, run from the top down so the
// revolution's (generator, angle) normal is the record's natural normal (outward).
let generator = |at: &dyn Fn(f64) -> Vec3| -> Result<EvalCurve> {
let [h0, h1] = domain[1];
if !(h0.is_finite() && h1.is_finite() && h1 > h0) {
return Err(invalid("a cylinder or cone face with no height"));
}
let (a, b) = (at(h1), at(h0));
// Parameter −v: the generator at t is the surface at height −t.
Ok(EvalCurve { degree: 1, knots: vec![-h1, -h1, -h0, -h0], ctrl: vec![[a.x, a.y, a.z, 1.0], [b.x, b.y, b.z, 1.0]] })
};
match surface {
brep::Surface::Cylinder { frame, radius } => {
let (o, x, z) = axes(frame);
revolved(generator(&|h| o + x * *radius + z * h)?, o, z)
}
brep::Surface::Cone { frame, radius_at_origin, half_angle } => {
let (o, x, z) = axes(frame);
let slope = half_angle.tan();
revolved(generator(&|h| o + x * (radius_at_origin + h * slope) + z * h)?, o, z)
}
brep::Surface::Sphere { frame, radius } => {
let (o, x, z) = axes(frame);
// The meridian from the north pole to the south pole.
revolved(arc(o, x, z, *radius, [-FRAC_PI_2, FRAC_PI_2]).reversed(), o, z)
}
brep::Surface::Torus { frame, major_radius, minor_radius } => {
let (o, x, z) = axes(frame);
revolved(arc(o + x * *major_radius, x, z, *minor_radius, [0.0, TAU]).reversed(), o, z)
}
_ => match SurfaceEval::new(surface) {
Ok(SurfaceEval::Eval(e, _)) => Ok(e),
Ok(_) => Err(invalid("an analytic surface out of place")),
Err(e) => Err(KernelError::Failed(format!("not supported yet: tessellation: {e}"))),
},
}
}
/// The circle `centre + r (cos t x + sin t y)` for t in `angle`, as an exact rational quadratic
/// spline over [0, 1], one span per quarter turn or less.
fn arc(centre: Vec3, x: Vec3, y: Vec3, r: f64, angle: [f64; 2]) -> EvalCurve {
let span = angle[1] - angle[0];
let count = (span / std::f64::consts::FRAC_PI_2).ceil().clamp(1.0, 4.0) as usize;
let step = span / count as f64;
let weight = (step / 2.0).cos();
// A point at angle `t`, its radius divided by `scale` (the middle control point of a span
// lies 1 / cos(step / 2) out), as homogeneous coordinates of weight `w`.
let at = |t: f64, scale: f64, w: f64| {
let p = centre + x * (r * t.cos() / scale) + y * (r * t.sin() / scale);
[p.x * w, p.y * w, p.z * w, w]
};
let mut ctrl = vec![at(angle[0], 1.0, 1.0)];
let mut knots = vec![0.0; 3];
for i in 0..count {
let start = angle[0] + i as f64 * step;
ctrl.push(at(start + step / 2.0, weight, weight));
ctrl.push(at(start + step, 1.0, 1.0));
knots.extend(std::iter::repeat_n((i + 1) as f64 / count as f64, if i + 1 == count { 3 } else { 2 }));
}
EvalCurve { degree: 2, knots, ctrl }
}
/// A curve on [0, 1] run the other way: t becomes 1 − t.
trait Reversed {
fn reversed(self) -> Self;
}
impl Reversed for EvalCurve {
fn reversed(mut self) -> Self {
self.ctrl.reverse();
self.knots = self.knots.iter().rev().map(|k| 1.0 - k).collect();
self
}
}
#[cfg(test)]