mirror of
https://github.com/bherbruck/solidcraft.git
synced 2026-11-04 19:15:57 +00:00
Tessellation: a loop corner on a side collapsed within its vertex's tolerance is a pole; a face no method plans is left out, named
A face's boundary node on a patch side collapsed to within the tolerance of the vertex there (capped at the chord tolerance), not to a point, is a pole: its parameter along the collapsed direction is free, as the record check and the STEP loop check take it (`uv::collapsed`, shared with `SurfaceEval::pole_within`). A loop through such a corner no longer winds round the surface as projected, and plans strictly (Poppy Humanoid's connector, face 43: a quarter of a sphere cap on a closed rational spline). A face the strict plan and the lenient fill both refuse no longer fails the whole body's mesh: it is left out with no triangles, named in PlanStats::unmeshed (logged by the display plan, counted by Body::unmeshed_faces), and the rest of the body is meshed; a body none of whose faces plan is still the first face's error. Body::volume_at gives no volume for a mesh with a face left out. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -294,20 +294,8 @@ impl SurfaceEval {
|
||||
/// period (or its whole range) moves the point no more than `reach` (a boundary point that
|
||||
/// lies at a pole to within a tolerance, where its periodic parameter is free).
|
||||
pub fn pole_within(&self, uv: [f64; 2], reach: f64) -> [bool; 2] {
|
||||
let mut out = self.pole(uv);
|
||||
let Some((_, su, sv)) = self.derivs(uv[0], uv[1]) else { return out };
|
||||
let dom = self.domain();
|
||||
for (d, speed) in [su.len(), sv.len()].into_iter().enumerate() {
|
||||
let span = match (dom.period.get(d).copied().flatten(), dom.range.get(d).copied().flatten()) {
|
||||
(Some(period), _) => 0.5 * period,
|
||||
(None, Some((a, b))) => (b - a).abs(),
|
||||
(None, None) => continue,
|
||||
};
|
||||
if let Some(flag) = out.get_mut(d) {
|
||||
*flag |= speed * span <= reach;
|
||||
}
|
||||
}
|
||||
out
|
||||
let Some((_, su, sv)) = self.derivs(uv[0], uv[1]) else { return [false; 2] };
|
||||
uv::collapsed([su.len(), sv.len()], &self.domain(), reach)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -320,6 +320,32 @@ pub fn degenerate(s: &EvalSurface, uv: [f64; 2], d: usize) -> bool {
|
||||
this.len() <= 1e-9 * other.len().max(1e-12)
|
||||
}
|
||||
|
||||
/// [`degenerate`], or within `reach` mm of a pole (see [`collapsed`]): a boundary point on a
|
||||
/// patch side collapsed to within a tolerance, not to a point, where its parameter along `d` is
|
||||
/// as free as at a pole.
|
||||
pub fn degenerate_within(s: &EvalSurface, dom: &Domain, uv: [f64; 2], d: usize, reach: f64) -> bool {
|
||||
let Some((_, su, sv)) = s.derivs(uv[0], uv[1]) else { return false };
|
||||
let speed = [su.len(), sv.len()];
|
||||
collapsed(speed, dom, reach).get(d).copied().unwrap_or(false)
|
||||
}
|
||||
|
||||
/// Along each direction, whether a point whose parameter speeds (mm per unit) are `speed` is a
|
||||
/// pole, or within `reach` mm of one: moving along the direction there not at all (to 1e-9 of
|
||||
/// the other's speed), or across half its period (or its whole range) by no more than `reach`.
|
||||
pub fn collapsed(speed: [f64; 2], dom: &Domain, reach: f64) -> [bool; 2] {
|
||||
let [a, b] = speed;
|
||||
let mut out = [a <= 1e-9 * b.max(1e-12), b <= 1e-9 * a.max(1e-12)];
|
||||
for (d, flag) in out.iter_mut().enumerate() {
|
||||
let span = match (dom.period[d], dom.range[d]) {
|
||||
(Some(period), _) => 0.5 * period,
|
||||
(None, Some((lo, hi))) => (hi - lo).abs(),
|
||||
(None, None) => continue,
|
||||
};
|
||||
*flag |= speed[d] * span <= reach;
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -878,7 +878,8 @@ impl Body {
|
||||
}
|
||||
|
||||
/// The body's volume from a mesh at `tol`, measured once per (record, tolerance): clones of
|
||||
/// a body share the measurement. `None` when the body cannot be meshed.
|
||||
/// a body share the measurement. `None` when the body cannot be meshed, or a face of it was
|
||||
/// left out of the mesh (an open mesh has no volume).
|
||||
pub fn volume_at(&self, tol: f64) -> Option<f64> {
|
||||
let key = if tol.is_finite() && tol > 0.0 { tol } else { 0.05 };
|
||||
let key = key.to_bits();
|
||||
@@ -889,7 +890,11 @@ impl Body {
|
||||
{
|
||||
return Some(volume);
|
||||
}
|
||||
let volume = self.mesh(&MeshTolerance::chord(f64::from_bits(key))).ok()?.measure().volume;
|
||||
let mesh = self.mesh(&MeshTolerance::chord(f64::from_bits(key))).ok()?;
|
||||
if self.faces_without_triangles(&mesh) > 0 {
|
||||
return None;
|
||||
}
|
||||
let volume = mesh.measure().volume;
|
||||
if let Some(cell) = &self.volume
|
||||
&& let Ok(mut slot) = cell.lock()
|
||||
{
|
||||
@@ -913,7 +918,8 @@ impl Body {
|
||||
/// joins, protected creases). Triangles carry their face index and the edges their polylines,
|
||||
/// in the record's order; neighbouring faces share every boundary node, so the mesh is
|
||||
/// watertight wherever the B-rep is. A mesh body gives its own triangles. A face the planner
|
||||
/// cannot mesh is an error that names it and why.
|
||||
/// cannot mesh is left out (logged with why; [`Body::unmeshed_faces`] counts it) and the rest
|
||||
/// meshed; a body none of whose faces mesh is an error that names the first and why.
|
||||
pub fn mesh(&self, t: &MeshTolerance) -> Result<Mesh> {
|
||||
crate::profile::timed(crate::profile::Kind::Tessellate, "mesh", || self.mesh_untimed(t))
|
||||
}
|
||||
@@ -956,6 +962,10 @@ impl Body {
|
||||
for f in &stats.lenient {
|
||||
log::warn!("display plan: face {} filled leniently in (u, v), a defect in its pcurves or seams: {}", f.face, f.reason);
|
||||
}
|
||||
// A face no method plans is left out (no triangles; `unmeshed_faces` counts it) and named.
|
||||
for f in &stats.unmeshed {
|
||||
log::warn!("display plan: face {} left out of the mesh: {}", f.face, f.reason);
|
||||
}
|
||||
Ok(plan)
|
||||
}
|
||||
|
||||
@@ -1047,6 +1057,14 @@ impl Body {
|
||||
return 0;
|
||||
}
|
||||
let Ok(m) = self.mesh(&MeshTolerance::chord(coarse_tolerance(self.size()))) else { return self.face_count() };
|
||||
self.faces_without_triangles(&m)
|
||||
}
|
||||
|
||||
/// Faces of the body with no triangles in its mesh `m` (left out by the mesher).
|
||||
fn faces_without_triangles(&self, m: &Mesh) -> usize {
|
||||
if self.is_mesh() {
|
||||
return 0;
|
||||
}
|
||||
let mut has = vec![false; self.face_count()];
|
||||
for f in &m.tri_face {
|
||||
if let Some(h) = has.get_mut(*f as usize) {
|
||||
|
||||
@@ -18,9 +18,10 @@
|
||||
//! named in the plan's stats ([`PlanStats::lenient`], with why it was refused) so it is
|
||||
//! reported and fixed there; it is filled leniently in (u, v) (the sides crossing one already
|
||||
//! kept are left out, see [`Lenient`]) so the display has no hole. A face that refuses even
|
||||
//! that (its loops do not close in (u, v), or wind round a seam or pole), or whose pcurves do
|
||||
//! not lie on its edges, is an error naming the face (and the edge) and why; there is no
|
||||
//! other mesher to fall back to.
|
||||
//! that (its loops do not close in (u, v), or wind round a seam or pole) is left out of the
|
||||
//! plan with no triangles and named in [`PlanStats::unmeshed`], and the rest of the body is
|
||||
//! meshed; there is no other mesher to fall back to. A face whose pcurves do not lie on its edges is an error naming the face and
|
||||
//! the edge, as is a body none of whose faces can be planned.
|
||||
|
||||
pub(crate) mod cdt;
|
||||
pub(crate) mod collar;
|
||||
@@ -64,12 +65,15 @@ pub struct PlanStats {
|
||||
pub delaunay: usize,
|
||||
/// Faces every method refused, filled leniently in (u, v) (see [`Lenient`]) so the display
|
||||
/// has no hole: each still a defect upstream, in its pcurves or seams.
|
||||
pub lenient: Vec<LenientFace>,
|
||||
pub lenient: Vec<RefusedFace>,
|
||||
/// Faces even the lenient fill refused: left out of the plan (no triangles), so the rest of
|
||||
/// the body is still meshed, each with why it could not be planned.
|
||||
pub unmeshed: Vec<RefusedFace>,
|
||||
}
|
||||
|
||||
/// A face filled leniently: its index in the record and why the strict plan refused it.
|
||||
/// A face a method refused: its index in the record and why.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct LenientFace {
|
||||
pub struct RefusedFace {
|
||||
pub face: usize,
|
||||
pub reason: String,
|
||||
}
|
||||
@@ -198,7 +202,7 @@ pub(crate) fn plan(b: &brep::Brep, tol: f64, turn: Option<size::Turn>) -> Result
|
||||
return Err(unsupported(format!("face {fi}: missing")));
|
||||
};
|
||||
let refused = tangled.iter().find(|(f, _)| *f == fi).map(|(_, why)| why.as_str());
|
||||
let (fp, stats) = plan_or_fill(fi, kind(face.parameters()), eval, &fb.loops, &rules, *size, refused)?;
|
||||
let (fp, stats) = plan_or_fill(fi, kind(face.parameters()), eval, &fb.loops, &rules, *size, fb.reach, refused)?;
|
||||
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 };
|
||||
@@ -206,16 +210,31 @@ pub(crate) fn plan(b: &brep::Brep, tol: f64, turn: Option<size::Turn>) -> Result
|
||||
});
|
||||
let mut plan = TessPlan::default();
|
||||
let mut stats = PlanStats::default();
|
||||
for (r, eval) in planned.into_iter().zip(evals) {
|
||||
let (fp, s) = r?;
|
||||
// A face no method plans is left out, named in the stats, and the rest of the body is
|
||||
// meshed; a body none of whose faces plan is the first face's error.
|
||||
let mut first_error: Option<KernelError> = None;
|
||||
for (fi, (r, eval)) in planned.into_iter().zip(evals).enumerate() {
|
||||
let surface = u32::try_from(plan.surfaces.len()).map_err(|_| unsupported("too many faces"))?;
|
||||
plan.surfaces.push(eval);
|
||||
let (fp, s) = match r {
|
||||
Ok(done) => done,
|
||||
Err(e) => {
|
||||
stats.unmeshed.push(RefusedFace { face: fi, reason: e.to_string() });
|
||||
first_error.get_or_insert(e);
|
||||
(FacePlan { face: u32::try_from(fi).unwrap_or(u32::MAX), ..FacePlan::default() }, PlanStats::default())
|
||||
}
|
||||
};
|
||||
plan.faces.push(FacePlan { surface: Some(surface), ..fp });
|
||||
stats.planar += s.planar;
|
||||
stats.structured += s.structured;
|
||||
stats.delaunay += s.delaunay;
|
||||
stats.lenient.extend(s.lenient);
|
||||
}
|
||||
if let Some(e) = first_error
|
||||
&& stats.unmeshed.len() == faces.len()
|
||||
{
|
||||
return Err(e);
|
||||
}
|
||||
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))
|
||||
@@ -226,7 +245,8 @@ pub(crate) fn plan(b: &brep::Brep, tol: f64, turn: Option<size::Turn>) -> Result
|
||||
/// at the node's curve parameter. A seam edge's two uses on one face have their own pcurves, so
|
||||
/// its two sides are told apart. A pcurve runs on continuously; where one edge meets the next, a
|
||||
/// pcurve that starts a whole period along from where the previous one ended is moved back by
|
||||
/// it, except in a direction the meeting point is a pole of: there the parameter is free, and
|
||||
/// it, except in a direction the meeting point is a pole of (or lies on a patch side collapsed
|
||||
/// to within its vertex's tolerance, see [`Boundary::reach`]): there the parameter is free, and
|
||||
/// the two pcurves' own values are where the face's boundary runs along the pole from and to
|
||||
/// (a meridian revolved half a turn meets the other at the pole half a period away; unwrapped,
|
||||
/// its loop would wind round the surface). Each loop then starts at a node off every pole whose
|
||||
@@ -251,7 +271,21 @@ fn boundary(
|
||||
) -> Result<Boundary> {
|
||||
let dom = uv::domain(s);
|
||||
let fail = |e: u32, why: String| unsupported(format!("face {fi} ({}): edge {e}: {why}", kind(&face.surface)));
|
||||
let mut out = Boundary { loops: Vec::with_capacity(face.loops.len()), segments: Vec::with_capacity(face.loops.len()) };
|
||||
// How near a pole a node is one: the tolerance of a vertex of the face that lies on a patch
|
||||
// side collapsed to within it, not to a point, as the record check takes a loop's pole; at
|
||||
// most the chord tolerance, finer than which the mesh resolves nothing (a vertex tolerance
|
||||
// as large as the surface's features would make a pole of every node near one).
|
||||
let mut reach: f64 = 0.0;
|
||||
for oe in face_edges(face) {
|
||||
let (Some(edge), Some([p0, p1])) = (b.edges.get(oe.edge as usize), oe.pcurve.interval()) else { continue };
|
||||
let tol = edge.vertices.get(usize::from(!oe.forward)).and_then(|v| b.vertices.get(*v as usize)).map_or(0.0, |v| v.tolerance.min(rules.tol));
|
||||
let Some(q) = oe.pcurve.eval(if oe.forward { p0 } else { p1 }).map(|q| map.to_eval(q)) else { continue };
|
||||
if tol.is_finite() && tol > reach && (0..2).any(|d| uv::degenerate_within(s, &dom, q, d, tol)) {
|
||||
reach = tol;
|
||||
}
|
||||
}
|
||||
let pole_along = |q: [f64; 2], d: usize| uv::degenerate_within(s, &dom, q, d, reach);
|
||||
let mut out = Boundary { loops: Vec::with_capacity(face.loops.len()), segments: Vec::with_capacity(face.loops.len()), reach };
|
||||
// Each vertex's parameters, from the first edge use of the face to start there.
|
||||
let mut at_vertex: std::collections::HashMap<u32, [f64; 2]> = std::collections::HashMap::new();
|
||||
for l in &face.loops {
|
||||
@@ -290,7 +324,7 @@ fn boundary(
|
||||
if let (Some(e), Some(&start)) = (end, eq.first()) {
|
||||
for d in 0..2 {
|
||||
let Some(per) = dom.period[d] else { continue };
|
||||
if !(uv::degenerate(s, e, d) || uv::degenerate(s, start, d)) {
|
||||
if !(pole_along(e, d) || pole_along(start, d)) {
|
||||
shift[d] = per * ((e[d] - start[d]) / per).round();
|
||||
}
|
||||
}
|
||||
@@ -321,7 +355,7 @@ fn boundary(
|
||||
uvs.extend(eq);
|
||||
}
|
||||
let n = uvs.len();
|
||||
let pole = |q: &[f64; 2]| (0..2).any(|d| uv::degenerate(s, *q, d));
|
||||
let pole = |q: &[f64; 2]| (0..2).any(|d| pole_along(*q, d));
|
||||
if let Some(k) = (0..n).find(|&k| uvs.get(k).is_some_and(|q| !pole(q)) && uvs.get((k + n - 1) % n).is_some_and(|q| !pole(q))) {
|
||||
uvs.rotate_left(k);
|
||||
pts.rotate_left(k);
|
||||
@@ -336,10 +370,13 @@ fn boundary(
|
||||
}
|
||||
|
||||
/// A face's boundary loops, and for each loop node the edge segment that starts there: the
|
||||
/// edge, and the segment's place along it from its start vertex.
|
||||
/// edge, and the segment's place along it from its start vertex. `reach`: how near a pole a
|
||||
/// point of the face is taken as one (see [`uv::degenerate_within`]): the tolerance of a vertex
|
||||
/// of it on a patch side collapsed to within that tolerance, else 0.
|
||||
struct Boundary {
|
||||
loops: Vec<seam::Loop>,
|
||||
segments: Vec<Vec<(u32, u32)>>,
|
||||
reach: f64,
|
||||
}
|
||||
|
||||
/// Edges are sampled to the chord tolerance each on its own, so where two curves of one face
|
||||
@@ -360,7 +397,7 @@ fn untangle(
|
||||
) -> Result<Vec<(usize, String)>> {
|
||||
let crossing = |fi: usize, fb: &Boundary| -> Vec<(u32, u32)> {
|
||||
let Some(s) = evals.get(fi) else { return Vec::new() };
|
||||
tangle::crossing(s, &fb.loops).into_iter().filter_map(|(l, k)| fb.segments.get(l)?.get(k).copied()).collect()
|
||||
tangle::crossing(s, fb.reach, &fb.loops).into_iter().filter_map(|(l, k)| fb.segments.get(l)?.get(k).copied()).collect()
|
||||
};
|
||||
let indexed: Vec<usize> = (0..boundaries.len()).collect();
|
||||
let curved = evals.iter().filter(|e| !matches!(e, EvalSurface::Plane { .. })).count();
|
||||
@@ -499,9 +536,9 @@ fn area2(uv: &[[f64; 2]]) -> f64 {
|
||||
/// unchanged across the pole. Across a pole it may jump by any amount, so the run from the
|
||||
/// second pole back to the first is moved one period along, whichever way keeps the loop
|
||||
/// within one period with a positive area: the seam's two uses then lie a period apart.
|
||||
fn open_pole_to_pole(s: &EvalSurface, dom: &uv::Domain, l: &mut seam::Loop) {
|
||||
fn open_pole_to_pole(s: &EvalSurface, dom: &uv::Domain, reach: f64, l: &mut seam::Loop) {
|
||||
let n = l.uv.len();
|
||||
let poles: Vec<usize> = (0..n).filter(|&k| l.uv.get(k).is_some_and(|q| (0..2).any(|d| uv::degenerate(s, *q, d)))).collect();
|
||||
let poles: Vec<usize> = (0..n).filter(|&k| l.uv.get(k).is_some_and(|q| (0..2).any(|d| uv::degenerate_within(s, dom, *q, d, reach)))).collect();
|
||||
let [p0, p1] = poles.as_slice() else { return };
|
||||
for d in 0..2 {
|
||||
let Some(per) = dom.period[d] else { continue };
|
||||
@@ -547,11 +584,11 @@ fn open_pole_to_pole(s: &EvalSurface, dom: &uv::Domain, l: &mut seam::Loop) {
|
||||
|
||||
/// The poles of a loop where it turns straight back along a seam: the nodes either side have
|
||||
/// the same parameters.
|
||||
fn turning_poles(s: &EvalSurface, per: f64, uv: &[[f64; 2]]) -> Vec<usize> {
|
||||
fn turning_poles(s: &EvalSurface, dom: &uv::Domain, reach: f64, per: f64, uv: &[[f64; 2]]) -> Vec<usize> {
|
||||
let n = uv.len();
|
||||
let near = |a: Option<&[f64; 2]>, b: Option<&[f64; 2]>| a.zip(b).is_some_and(|(a, b)| (a[0] - b[0]).abs().max((a[1] - b[1]).abs()) <= 1e-9 * per);
|
||||
(0..n)
|
||||
.filter(|&k| uv.get(k).is_some_and(|q| (0..2).any(|d| uv::degenerate(s, *q, d))))
|
||||
.filter(|&k| uv.get(k).is_some_and(|q| (0..2).any(|d| uv::degenerate_within(s, dom, *q, d, reach))))
|
||||
.filter(|&k| near(uv.get((k + n - 1) % n), uv.get((k + 1) % n)))
|
||||
.collect()
|
||||
}
|
||||
@@ -564,7 +601,7 @@ fn turning_poles(s: &EvalSurface, per: f64, uv: &[[f64; 2]]) -> Vec<usize> {
|
||||
/// to the pole, and starts at the node before it: the seam's uses then bound the face's parameter box on either side, and share
|
||||
/// their nodes as every other edge does. Kept as it was unless the opened loop spans at most a
|
||||
/// period with a positive area.
|
||||
fn open_at_pole(s: &EvalSurface, dom: &uv::Domain, l: &mut seam::Loop) {
|
||||
fn open_at_pole(s: &EvalSurface, dom: &uv::Domain, reach: f64, l: &mut seam::Loop) {
|
||||
let n = l.uv.len();
|
||||
if n < 4 || l.pts.len() != n {
|
||||
return;
|
||||
@@ -574,7 +611,7 @@ fn open_at_pole(s: &EvalSurface, dom: &uv::Domain, l: &mut seam::Loop) {
|
||||
if (seam::drift(l, d, per).abs() - per).abs() > per * 1e-6 {
|
||||
continue;
|
||||
}
|
||||
let [pole] = turning_poles(s, per, &l.uv)[..] else { continue };
|
||||
let [pole] = turning_poles(s, dom, reach, per, &l.uv)[..] else { continue };
|
||||
// From the node after the pole round to the pole, unwrapped along the way.
|
||||
let order: Vec<usize> = (1..=n).map(|i| (pole + i) % n).collect();
|
||||
let mut uv: Vec<[f64; 2]> = Vec::with_capacity(n);
|
||||
@@ -633,9 +670,10 @@ fn holes_beside_outer(dom: &uv::Domain, loops: &mut [seam::Loop]) {
|
||||
/// One face, planned by [`plan_face`]; a face it refuses because its loops cross in (u, v) is
|
||||
/// planned once more leniently (see [`Lenient`]) and named in the stats with the first refusal,
|
||||
/// so the defect upstream is still reported. When the lenient fill refuses too, the first
|
||||
/// refusal is the error. `refused`: a face whose boundary still crosses itself after
|
||||
/// [`untangle`] (two edges meeting tangentially at a cusp, whose chords cross however often
|
||||
/// they are halved), filled leniently straight away with that reason.
|
||||
/// refusal is the error (and [`plan`] leaves the face out, named). `refused`: a face whose
|
||||
/// boundary still crosses itself after [`untangle`] (two edges meeting tangentially at a cusp,
|
||||
/// whose chords cross however often they are halved), filled leniently straight away with that
|
||||
/// reason.
|
||||
fn plan_or_fill(
|
||||
fi: usize,
|
||||
kind: &str,
|
||||
@@ -643,20 +681,21 @@ fn plan_or_fill(
|
||||
loops: &[seam::Loop],
|
||||
rules: &size::Rules,
|
||||
size: (f64, [f64; 2]),
|
||||
reach: f64,
|
||||
refused: Option<&str>,
|
||||
) -> Result<(FacePlan, PlanStats)> {
|
||||
let mut stats = PlanStats::default();
|
||||
let strict = match refused {
|
||||
Some(why) => Err(unsupported(why.to_string())),
|
||||
None => plan_face(fi, kind, s, loops, rules, size, &mut stats, Lenient::No),
|
||||
None => plan_face(fi, kind, s, loops, rules, size, reach, &mut stats, Lenient::No),
|
||||
};
|
||||
match strict {
|
||||
Ok(fp) => Ok((fp, stats)),
|
||||
Err(first) => {
|
||||
let mut stats = PlanStats::default();
|
||||
match plan_face(fi, kind, s, loops, rules, size, &mut stats, Lenient::Yes) {
|
||||
match plan_face(fi, kind, s, loops, rules, size, reach, &mut stats, Lenient::Yes) {
|
||||
Ok(fp) => {
|
||||
stats.lenient.push(LenientFace { face: fi, reason: first.to_string() });
|
||||
stats.lenient.push(RefusedFace { face: fi, reason: first.to_string() });
|
||||
Ok((fp, stats))
|
||||
}
|
||||
Err(_) => Err(first),
|
||||
@@ -667,7 +706,8 @@ fn plan_or_fill(
|
||||
|
||||
/// One face: boundary loops in 3D (shared edge nodes) to parameters and triangles.
|
||||
/// `size`: the face's target side length (mm) and the surface's mean speed along u and v.
|
||||
/// `lenient`: the last resort for a face no method could plan (see [`Lenient`]).
|
||||
/// `reach`: how near a pole a point is taken as one ([`Boundary::reach`]). `lenient`: the last resort
|
||||
/// for a face no method could plan (see [`Lenient`]).
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn plan_face(
|
||||
fi: usize,
|
||||
@@ -676,6 +716,7 @@ fn plan_face(
|
||||
loops: &[seam::Loop],
|
||||
rules: &size::Rules,
|
||||
size: (f64, [f64; 2]),
|
||||
reach: f64,
|
||||
stats: &mut PlanStats,
|
||||
lenient: Lenient,
|
||||
) -> Result<FacePlan> {
|
||||
@@ -689,7 +730,7 @@ fn plan_face(
|
||||
// A loop that runs up a seam from a pole and straight back down it (a half sphere bounded
|
||||
// by its rim and its seam used both ways) winds once as projected: it is opened at the pole.
|
||||
for l in projected.iter_mut() {
|
||||
open_at_pole(s, &dom, l);
|
||||
open_at_pole(s, &dom, reach, l);
|
||||
}
|
||||
let wound: Vec<usize> = (0..2).filter(|&d| dom.period[d].is_some_and(|per| seam::winds(&projected, d, per))).collect();
|
||||
// A face cut along a seam is a long winding strip whose sides are the cut lines and the
|
||||
@@ -708,7 +749,7 @@ fn plan_face(
|
||||
// kernel writes it) projects onto one parameter line: its second run is a period along.
|
||||
if !seamed {
|
||||
for l in projected.iter_mut() {
|
||||
open_pole_to_pole(s, &dom, l);
|
||||
open_pole_to_pole(s, &dom, reach, l);
|
||||
}
|
||||
holes_beside_outer(&dom, &mut projected);
|
||||
}
|
||||
@@ -723,7 +764,7 @@ fn plan_face(
|
||||
// At a pole the parameter along the degenerate direction is free: one copy with
|
||||
// the previous node's value, one with the next node's.
|
||||
let (prev, next) = (luv.get((k + n - 1) % n).copied(), luv.get((k + 1) % n).copied());
|
||||
let pole = (0..2).find(|&d| uv::degenerate(s, *q, d));
|
||||
let pole = (0..2).find(|&d| uv::degenerate_within(s, &dom, *q, d, reach));
|
||||
match (pole, prev, next) {
|
||||
(Some(d), Some(a), Some(b)) => {
|
||||
let mut qa = *q;
|
||||
@@ -875,7 +916,7 @@ fn plan_face(
|
||||
all.extend(extra);
|
||||
// Whatever the layout, no triangle leaves the surface by more than the tolerance.
|
||||
if !planar {
|
||||
sag::refine(s, tol, &fixed, &mut all, &mut triangles, MAX_REFINE);
|
||||
sag::refine(s, tol, reach, &fixed, &mut all, &mut triangles, MAX_REFINE);
|
||||
}
|
||||
// Both evaluators take a closed spline's parameters inside its active interval.
|
||||
for q in &mut all {
|
||||
|
||||
@@ -14,12 +14,15 @@ use solidcraft_geom::tess::EvalSurface;
|
||||
|
||||
/// Split the triangles `tris` of a face (indices into `uv`; the first `fixed.len()` points sit
|
||||
/// at `fixed`, the rest on the surface at their parameters) until none of them lies farther
|
||||
/// than `tol` from the surface `s`, adding at most `max_points` points to `uv`.
|
||||
pub(crate) fn refine(s: &EvalSurface, tol: f64, fixed: &[Vec3], uv: &mut Vec<[f64; 2]>, tris: &mut Vec<[u32; 3]>, max_points: usize) {
|
||||
/// than `tol` from the surface `s`, adding at most `max_points` points to `uv`. `reach`: how near
|
||||
/// a pole a point is taken as one (see [`super::uv::degenerate_within`]).
|
||||
pub(crate) fn refine(s: &EvalSurface, tol: f64, reach: f64, fixed: &[Vec3], uv: &mut Vec<[f64; 2]>, tris: &mut Vec<[u32; 3]>, max_points: usize) {
|
||||
if !(tol.is_finite() && tol > 0.0) || tris.is_empty() {
|
||||
return;
|
||||
}
|
||||
let at = |q: [f64; 2]| s.eval(q[0], q[1]).0;
|
||||
let dom = super::uv::domain(s);
|
||||
let mean = |qs: &[[f64; 2]]| mean(s, &dom, reach, qs);
|
||||
// How far the flat point `c` lies from the surface, measured along the normal at the surface
|
||||
// point of the same parameters: those parameters need not be the nearest (a straight line in
|
||||
// the parameters is no geodesic near a pole), and only the departure across the surface
|
||||
@@ -64,7 +67,7 @@ pub(crate) fn refine(s: &EvalSurface, tol: f64, fixed: &[Vec3], uv: &mut Vec<[f6
|
||||
if len <= shortest {
|
||||
continue;
|
||||
}
|
||||
let sag = *sag_of.entry(e).or_insert_with(|| off(mean(s, &[ua, ub]), (xa + xb) * 0.5));
|
||||
let sag = *sag_of.entry(e).or_insert_with(|| off(mean(&[ua, ub]), (xa + xb) * 0.5));
|
||||
inner.push((sag, len, e));
|
||||
}
|
||||
// The surface's departure from a flat triangle is about quadratic over it: at the
|
||||
@@ -72,7 +75,7 @@ pub(crate) fn refine(s: &EvalSurface, tol: f64, fixed: &[Vec3], uv: &mut Vec<[f6
|
||||
// inside the face divides by four. A triangle whose middle is off splits its side inside
|
||||
// the face that sags most, while that side still contributes; what is left comes from
|
||||
// its boundary sides, which the edges' own sampling keeps within the tolerance.
|
||||
let centroid = mean(s, &[qa, qb, qc]);
|
||||
let centroid = mean(&[qa, qb, qc]);
|
||||
let worst = inner.iter().max_by(|x, y| x.0.total_cmp(&y.0)).copied();
|
||||
let split = match worst {
|
||||
Some((sag, _, e)) if sag > tol => e,
|
||||
@@ -80,7 +83,7 @@ pub(crate) fn refine(s: &EvalSurface, tol: f64, fixed: &[Vec3], uv: &mut Vec<[f6
|
||||
_ => continue,
|
||||
};
|
||||
let (Some(ua), Some(ub)) = (q(split.0), q(split.1)) else { continue };
|
||||
let m = mean(s, &[ua, ub]);
|
||||
let m = mean(&[ua, ub]);
|
||||
let mi = uv.len() as u32;
|
||||
uv.push(m);
|
||||
pos.push(at(m));
|
||||
@@ -92,10 +95,10 @@ pub(crate) fn refine(s: &EvalSurface, tol: f64, fixed: &[Vec3], uv: &mut Vec<[f6
|
||||
/// The mean of parameter points, as the surface point between them: at a pole the parameter
|
||||
/// along the degenerate direction is free (the pole copy carries a neighbour's), so there the
|
||||
/// mean takes it from the other points alone.
|
||||
fn mean(s: &EvalSurface, qs: &[[f64; 2]]) -> [f64; 2] {
|
||||
fn mean(s: &EvalSurface, dom: &super::uv::Domain, reach: f64, qs: &[[f64; 2]]) -> [f64; 2] {
|
||||
let mut out = [0.0; 2];
|
||||
for (d, slot) in out.iter_mut().enumerate() {
|
||||
let free: Vec<f64> = qs.iter().filter(|q| !super::uv::degenerate(s, **q, d)).map(|q| q[d]).collect();
|
||||
let free: Vec<f64> = qs.iter().filter(|q| !super::uv::degenerate_within(s, dom, **q, d, reach)).map(|q| q[d]).collect();
|
||||
let pick: Vec<f64> = if free.is_empty() { qs.iter().map(|q| q[d]).collect() } else { free };
|
||||
*slot = pick.iter().sum::<f64>() / pick.len().max(1) as f64;
|
||||
}
|
||||
|
||||
@@ -33,10 +33,10 @@ impl Chord {
|
||||
/// another, each once and in order. On a closed surface the boundary crosses itself where any
|
||||
/// copies of two chords, whole periods apart, cross; chords ending on a pole, where the
|
||||
/// parameter along the pole is free, are left out. Adjacent chords of a loop share a node and
|
||||
/// never cross.
|
||||
pub(crate) fn crossing(s: &EvalSurface, loops: &[seam::Loop]) -> Vec<(usize, usize)> {
|
||||
/// never cross. `reach`: how near a pole a node is taken as one (see [`uv::degenerate_within`]).
|
||||
pub(crate) fn crossing(s: &EvalSurface, reach: f64, loops: &[seam::Loop]) -> Vec<(usize, usize)> {
|
||||
let dom = uv::domain(s);
|
||||
let pole = |q: [f64; 2]| (0..2).any(|d| uv::degenerate(s, q, d));
|
||||
let pole = |q: [f64; 2]| (0..2).any(|d| uv::degenerate_within(s, &dom, q, d, reach));
|
||||
let mut chords: Vec<Chord> = Vec::new();
|
||||
for (li, l) in loops.iter().enumerate() {
|
||||
let n = l.uv.len();
|
||||
|
||||
@@ -99,7 +99,7 @@ fn a_box_is_a_regular_grid_on_every_face() {
|
||||
fn primitives_are_watertight_with_the_right_volume() {
|
||||
for tol in [0.05, 0.005] {
|
||||
let s = check("box", &ok(crate::box_solid(Vec3::ZERO, Vec3::new(10.0, 6.0, 4.0))), tol, 240.0);
|
||||
assert_eq!(s, PlanStats { planar: 6, structured: 0, delaunay: 0, lenient: Vec::new() });
|
||||
assert_eq!(s, PlanStats { planar: 6, ..PlanStats::default() });
|
||||
let s = check("cylinder", &ok(crate::cylinder(Vec3::new(1.0, 2.0, 3.0), Vec3::new(0.0, 1.0, 1.0), 2.5, 7.0)), tol, PI * 2.5 * 2.5 * 7.0);
|
||||
assert_eq!(s.delaunay, 0, "cylinder walls are structured");
|
||||
let s = check("torus", &ok(crate::torus(Vec3::ZERO, 6.0, 1.5)), tol, 2.0 * PI * PI * 6.0 * 1.5 * 1.5);
|
||||
@@ -725,8 +725,8 @@ fn a_curved_face_crossing_itself_is_filled_on_its_surface() {
|
||||
let size = size::face_size(&s, &uv::domain(&s), &rules);
|
||||
let loops = std::slice::from_ref(&l);
|
||||
let mut stats = PlanStats::default();
|
||||
assert!(plan_face(0, "spline", &s, loops, &rules, size, &mut stats, Lenient::No).is_err());
|
||||
let Ok(fp) = plan_face(0, "spline", &s, loops, &rules, size, &mut stats, Lenient::Yes) else { panic!("a lenient fill") };
|
||||
assert!(plan_face(0, "spline", &s, loops, &rules, size, 0.0, &mut stats, Lenient::No).is_err());
|
||||
let Ok(fp) = plan_face(0, "spline", &s, loops, &rules, size, 0.0, &mut stats, Lenient::Yes) else { panic!("a lenient fill") };
|
||||
assert!(!fp.triangles.is_empty());
|
||||
assert!(fp.uv.len() > fp.fixed.len(), "{} points, {} on the boundary", fp.uv.len(), fp.fixed.len());
|
||||
// Each point inside the face on the surface where its parameters put it.
|
||||
@@ -743,7 +743,7 @@ fn a_curved_face_crossing_itself_is_filled_on_its_surface() {
|
||||
fn a_face_filled_leniently_is_named_in_the_stats() {
|
||||
let (s, l, rules) = a_face_crossing_itself();
|
||||
let size = size::face_size(&s, &uv::domain(&s), &rules);
|
||||
let Ok((fp, stats)) = plan_or_fill(7, "spline", &s, std::slice::from_ref(&l), &rules, size, None) else { panic!("filled leniently") };
|
||||
let Ok((fp, stats)) = plan_or_fill(7, "spline", &s, std::slice::from_ref(&l), &rules, size, 0.0, None) else { panic!("filled leniently") };
|
||||
assert!(!fp.triangles.is_empty());
|
||||
assert_eq!((stats.planar, stats.structured, stats.delaunay), (0, 0, 0));
|
||||
let [lenient] = stats.lenient.as_slice() else { panic!("one lenient face: {:?}", stats.lenient) };
|
||||
@@ -760,7 +760,7 @@ fn a_face_untangling_cannot_resolve_is_filled_leniently_and_named() {
|
||||
let (s, l, rules) = a_face_crossing_itself();
|
||||
let size = size::face_size(&s, &uv::domain(&s), &rules);
|
||||
let why = "face 7 (spline): boundary crosses itself: edges [1, 2] run closer together than their chords can follow";
|
||||
let Ok((fp, stats)) = plan_or_fill(7, "spline", &s, std::slice::from_ref(&l), &rules, size, Some(why)) else { panic!("filled leniently") };
|
||||
let Ok((fp, stats)) = plan_or_fill(7, "spline", &s, std::slice::from_ref(&l), &rules, size, 0.0, Some(why)) else { panic!("filled leniently") };
|
||||
assert!(!fp.triangles.is_empty());
|
||||
let [lenient] = stats.lenient.as_slice() else { panic!("one lenient face: {:?}", stats.lenient) };
|
||||
assert_eq!(lenient.face, 7);
|
||||
@@ -807,3 +807,59 @@ fn a_face_bounded_by_two_short_arcs_keeps_an_area_to_mesh() {
|
||||
let lens = made.faces.first().and_then(|&f| plan.faces.get(f)).expect("the lens's plan");
|
||||
assert!(!lens.triangles.is_empty(), "the lens has triangles");
|
||||
}
|
||||
|
||||
/// Half of a square pyramid's sides as one spline face: degree 1, closed round the square
|
||||
/// (u in [0, 4], a corner at each whole u) and from the base (v = 0) up to a top side collapsed
|
||||
/// to a square 1e-7 mm across, not to a point, as the patch's side in Poppy Humanoid's connector
|
||||
/// a quarter of a sphere cap's corner sits on. Its loop runs down the side at u = 0, along the
|
||||
/// base to u = 2 and up again, then across the top back to the corner it started at, half a
|
||||
/// period along. Taken for no pole, that step is taken as the nearest copy, and the loop winds
|
||||
/// once round the surface with no partner to cut it along.
|
||||
fn pyramid_past_a_near_pole(pole_tolerance: f64) -> solidcraft_geom::brep::Brep {
|
||||
use solidcraft_geom::brep::{self as b, Curve, Curve2, Surface};
|
||||
let top = 1e-7;
|
||||
let corner = |u: usize, r: f64, z: f64| {
|
||||
let [x, y] = [[1.0, 0.0], [0.0, 1.0], [-1.0, 0.0], [0.0, -1.0]][u % 4];
|
||||
[r * x, r * y, z]
|
||||
};
|
||||
let points: Vec<Vec<[f64; 3]>> = (0..5).map(|u| vec![corner(u, 1.0, 0.0), corner(u, top, 1.0)]).collect();
|
||||
let surface = Surface::Spline { u_knots: vec![0.0, 0.0, 1.0, 2.0, 3.0, 4.0, 4.0], v_knots: vec![0.0, 0.0, 1.0, 1.0], points };
|
||||
let edge = |curve: Curve, interval: [f64; 2], vertices: [u32; 2]| b::Edge { nurbs: None, curve, interval, vertices, tolerance: 1e-6, uses: Vec::new() };
|
||||
let edges = vec![
|
||||
edge(Curve::Line { start: corner(0, 1.0, 0.0), end: corner(0, top, 1.0) }, [0.0, 1.0], [1, 0]),
|
||||
edge(Curve::Polyline { points: vec![corner(0, 1.0, 0.0), corner(1, 1.0, 0.0), corner(2, 1.0, 0.0)] }, [0.0, 2.0], [1, 2]),
|
||||
edge(Curve::Line { start: corner(2, 1.0, 0.0), end: corner(2, top, 1.0) }, [0.0, 1.0], [2, 0]),
|
||||
];
|
||||
let line = |t: [f64; 2], a: [f64; 2], z: [f64; 2]| Curve2::Spline { knots: vec![t[0], t[0], t[1], t[1]], points: vec![a, z] };
|
||||
let uses = vec![
|
||||
b::OrientedEdge { edge: 0, forward: false, pcurve: line([0.0, 1.0], [0.0, 0.0], [0.0, 1.0]) },
|
||||
b::OrientedEdge { edge: 1, forward: true, pcurve: line([0.0, 2.0], [0.0, 0.0], [2.0, 0.0]) },
|
||||
b::OrientedEdge { edge: 2, forward: true, pcurve: line([0.0, 1.0], [2.0, 0.0], [2.0, 1.0]) },
|
||||
];
|
||||
let face = b::Face { nurbs: None, surface, domain: [[0.0, 2.0], [0.0, 1.0]], loops: vec![b::Loop::Edges { edges: uses }] };
|
||||
let vertex = |point: [f64; 3], tolerance: f64| b::Vertex { point, tolerance };
|
||||
let loose = b::Brep {
|
||||
version: b::VERSION,
|
||||
tolerance: 1e-7,
|
||||
regions: vec![b::Region { shells: vec![0], void: false }],
|
||||
shells: vec![b::Shell::Faces { faces: vec![b::OrientedFace { face: 0, forward: true }], wires: Vec::new() }],
|
||||
faces: vec![face],
|
||||
edges,
|
||||
vertices: vec![vertex([0.0, 0.0, 1.0], pole_tolerance), vertex(corner(0, 1.0, 0.0), 1e-7), vertex(corner(2, 1.0, 0.0), 1e-7)],
|
||||
};
|
||||
ok(loose.canonical().map_err(|e| crate::KernelError::Invalid(format!("{e:?}")))).brep
|
||||
}
|
||||
|
||||
/// The corner of [`pyramid_past_a_near_pole`] within its vertex's tolerance of the collapsed top
|
||||
/// side is a pole, and the face plans strictly over both its sides; with a tolerance short of
|
||||
/// the top's size it is no pole, and the loop winds.
|
||||
#[test]
|
||||
fn a_loop_corner_within_its_tolerance_of_a_pole_is_a_pole() {
|
||||
let (made, stats) = ok(plan(&pyramid_past_a_near_pole(1e-6), 0.01, None));
|
||||
assert!(stats.lenient.is_empty() && stats.unmeshed.is_empty(), "{stats:?}");
|
||||
// Two of the pyramid's sides, each a triangle of area √3 / 2.
|
||||
let area = made.evaluate().measure().area;
|
||||
assert!((area - 3f64.sqrt()).abs() < 0.01 * 3f64.sqrt(), "area {area}");
|
||||
let Err(e) = plan(&pyramid_past_a_near_pole(1e-8), 0.01, None) else { panic!("no pole without the tolerance") };
|
||||
assert!(e.to_string().contains("winds around the surface"), "{e}");
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
//! face's parameter box.
|
||||
|
||||
use solidcraft_geom::tess::EvalSurface;
|
||||
pub(crate) use solidcraft_geom::tess::uv::{Domain, degenerate, domain, unwrap};
|
||||
pub(crate) use solidcraft_geom::tess::uv::{Domain, degenerate_within, domain, unwrap};
|
||||
|
||||
use super::size::Rules;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user