Compare commits

...

1 Commits

Author SHA1 Message Date
Brennen Herbruck 6678f4ee9c solidcore-tess: ask the pole classifier once per iso-line, not once per node
Whether a surface is collapsed along d at a point is decided on the iso-line
through it alone, so the answer depends only on the other parameter and the
reach. Each question rebuilds that iso-line's whole control row (a de Boor
evaluation per column of the net), and the planner asked it at every node,
although the nodes of a face share iso-lines (an edge on a patch side, grid
rows and columns, cut-grid lines). It was 60-75% of solidcore-tess's time on
spline bodies.

Each face's surface now carries a memo of the kernel's answers keyed by
(direction, other parameter, reach). Only answers are kept; a refusal is asked
again. Planes skip the memo.

Kernel classifier calls (export): boss-root fillet 5,822 -> 1,898; twisted
closed sweep 1,621 -> 562; coil through cylinder wall (4505) 93,770 -> 15,085;
coil cut with negative pitch (3832) 58,054 -> 3,502; coil split by a plane
(793) 42,458 -> 2,772. Triangle counts are unchanged on all 320 body-settings
measured.

Refs tangency-labs/solidcraft#13655

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 22:11:35 -05:00
7 changed files with 274 additions and 51 deletions
+20 -19
View File
@@ -49,6 +49,7 @@ use crate::{FacePlan, TessPlan};
use solidcore::eval::EvalSurface;
use solidcore_record::Vec3;
use solidcore_record::brep::{self, Curve, Loop, Shell};
pub(crate) use surface::FaceSurface;
use crate::{Result, TessError};
@@ -225,12 +226,12 @@ impl std::fmt::Display for Unclassified {
/// Whether `s` is collapsed along `d` at `q`, to within `reach` (a pole, or a patch side
/// collapsed to within a tolerance), as the kernel's classifier says; its refusal the error.
pub(crate) fn pole_at(
s: &EvalSurface,
s: &FaceSurface,
q: [f64; 2],
d: usize,
reach: f64,
) -> std::result::Result<bool, Unclassified> {
solidcore::eval::uv::degenerate_within(s, q, d, reach).map_err(|e| Unclassified {
s.collapsed(q, d, reach).map_err(|e| Unclassified {
at: q,
why: crate::eval_message(e),
})
@@ -238,7 +239,7 @@ pub(crate) fn pole_at(
/// The first direction (u, then v) `s` is collapsed along at `q` ([`pole_at`]), if any.
pub(crate) fn pole_dir(
s: &EvalSurface,
s: &FaceSurface,
q: [f64; 2],
reach: f64,
) -> std::result::Result<Option<usize>, Unclassified> {
@@ -445,10 +446,10 @@ fn plan_at(b: &brep::Brep, tol: f64, turn: Option<size::Turn>) -> Result<(TessPl
// one about a hundred, so the pool is used only where curved faces (or intersection
// curves) make up the work. A face refused at one step (its surface, an edge of it, its
// boundary) is passed over by the steps after, and left out of the plan, named.
let mut evals: Vec<Option<EvalSurface>> = Vec::new();
let mut evals: Vec<Option<FaceSurface>> = Vec::new();
for (fi, f) in faces.iter().enumerate() {
let s = match surface::eval_surface(f.parameters()) {
Ok(s) => Some(s),
Ok(s) => Some(FaceSurface::new(s)),
Err(e) => {
refused.face(
fi,
@@ -462,7 +463,7 @@ fn plan_at(b: &brep::Brep, tol: f64, turn: Option<size::Turn>) -> Result<(TessPl
let curved = evals
.iter()
.flatten()
.filter(|e| !matches!(e, EvalSurface::Plane { .. }))
.filter(|e| !matches!(***e, EvalSurface::Plane { .. }))
.count();
let sizes: Vec<Option<size::FaceSize>> =
crate::par::map("face sizes", &evals, curved >= PARALLEL_SIZES, |s| {
@@ -636,7 +637,7 @@ fn plan_at(b: &brep::Brep, tol: f64, turn: Option<size::Turn>) -> Result<(TessPl
let surface = match eval {
Some(eval) => {
let i = u32::try_from(plan.surfaces.len()).map_err(|_| too_many())?;
plan.surfaces.push(eval);
plan.surfaces.push(eval.into_eval());
Some(i)
}
None => None,
@@ -790,7 +791,7 @@ fn boundary(
b: &brep::Brep,
fi: usize,
face: &brep::Face,
s: &EvalSurface,
s: &FaceSurface,
nodes: &[edges::Nodes],
rules: &size::Rules,
) -> Result<Boundary> {
@@ -1133,7 +1134,7 @@ struct Boundary {
#[allow(clippy::too_many_arguments)]
fn untangle(
b: &brep::Brep,
evals: &[Option<EvalSurface>],
evals: &[Option<FaceSurface>],
edge_faces: &[Vec<usize>],
tol: f64,
nodes: &mut [edges::Nodes],
@@ -1155,7 +1156,7 @@ fn untangle(
let curved = evals
.iter()
.flatten()
.filter(|e| !matches!(e, EvalSurface::Plane { .. }))
.filter(|e| !matches!(***e, EvalSurface::Plane { .. }))
.count();
let found: Vec<Option<Vec<(u32, u32)>>> = crate::par::map(
"boundary crossings",
@@ -1254,7 +1255,7 @@ fn untangle(
/// emptied, so it crosses nothing.
fn rebuild(
fi: usize,
evals: &[Option<EvalSurface>],
evals: &[Option<FaceSurface>],
nodes: &[edges::Nodes],
boundaries: &mut [Boundary],
face_boundary: &(dyn Fn(usize, &[edges::Nodes]) -> Result<Boundary> + Sync),
@@ -1309,7 +1310,7 @@ const MAX_SNAP: u32 = 8;
/// (in `refused`), which are passed over from then on.
#[allow(clippy::too_many_arguments)]
fn snap_touches(
evals: &[Option<EvalSurface>],
evals: &[Option<FaceSurface>],
edge_faces: &[Vec<usize>],
tol: f64,
nodes: &mut [edges::Nodes],
@@ -1660,7 +1661,7 @@ fn area2(uv: &[[f64; 2]]) -> f64 {
/// 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,
s: &FaceSurface,
dom: &solidcore::eval::uv::Domain,
reach: f64,
l: &mut seam::Loop,
@@ -1751,7 +1752,7 @@ fn open_pole_to_pole(
/// 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,
s: &FaceSurface,
reach: f64,
per: f64,
uv: &[[f64; 2]],
@@ -1780,7 +1781,7 @@ fn turning_poles(
/// 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,
s: &FaceSurface,
dom: &solidcore::eval::uv::Domain,
reach: f64,
l: &mut seam::Loop,
@@ -1913,7 +1914,7 @@ fn holes_beside_outer(dom: &solidcore::eval::uv::Domain, loops: &mut [seam::Loop
fn plan_or_fill(
fi: u32,
kind: &str,
s: &EvalSurface,
s: &FaceSurface,
loops: &[seam::Loop],
rules: &size::Rules,
size: &size::FaceSize,
@@ -1976,7 +1977,7 @@ fn plan_or_fill(
fn plan_face(
fi: u32,
kind: &str,
s: &EvalSurface,
s: &FaceSurface,
loops: &[seam::Loop],
rules: &size::Rules,
size: &size::FaceSize,
@@ -2137,7 +2138,7 @@ fn plan_face(
let fill = cdt::Fill {
seeds: &seeds,
refine: Some((sag, MAX_REFINE)),
shape: !matches!(s, EvalSurface::Plane { .. }),
shape: !matches!(**s, EvalSurface::Plane { .. }),
lenient: lenient == Lenient::Yes,
};
cdt::triangulate(&uvs, &ranges, fill)
@@ -2146,7 +2147,7 @@ fn plan_face(
};
// Planar and curved faces alike: a structured grid where the boundary runs along the
// parameter box (a rectangle, a cylinder half), a Coons grid on four corners, else Delaunay.
let planar = matches!(s, EvalSurface::Plane { .. });
let planar = matches!(**s, EvalSurface::Plane { .. });
// The lenient fill is the constrained Delaunay alone: the caller names the face in the stats.
let (extra, triangles) = if lenient == Lenient::Yes {
delaunay()?
+84 -11
View File
@@ -3,6 +3,7 @@
//! needs, a node of a seam cut, or a point a size rule probes between the nodes it places).
//! Nothing is substituted for the point, and no point is skipped.
use super::FaceSurface;
use solidcore::eval::{EvalCurve, EvalSurface};
use solidcore_convert as kernel;
use solidcore_record::Vec3;
@@ -100,7 +101,16 @@ fn a_face_whose_surface_refuses_an_inner_point_is_refused_whole() {
let s = cylinder_without(0.5);
let loops = [quarter(&s, 8, 1.0)];
let rules = Rules::new(0.01, 40.0, None);
let r = plan_or_fill(0, "revolution", &s, &loops, &rules, &size(1.0), 0.0, None);
let r = plan_or_fill(
0,
"revolution",
&FaceSurface::new(s.clone()),
&loops,
&rules,
&size(1.0),
0.0,
None,
);
match r {
Err(e) => assert!(
e.to_string()
@@ -156,7 +166,16 @@ fn a_face_whose_surface_refuses_a_point_between_its_nodes_is_refused_whole() {
let s = cylinder_without(0.25);
let loops = [quarter(&s, 16, 1.0)];
let rules = Rules::new(0.01, 40.0, None);
let r = plan_or_fill(0, "revolution", &s, &loops, &rules, &size(50.0), 0.0, None);
let r = plan_or_fill(
0,
"revolution",
&FaceSurface::new(s.clone()),
&loops,
&rules,
&size(50.0),
0.0,
None,
);
match r {
Err(e) => assert!(
e.to_string()
@@ -224,7 +243,16 @@ fn a_size_sample_the_surface_refuses_refuses_only_a_face_it_lies_in() {
);
// A face beside the circle: the samples on it lie outside, and it meshes.
let low = [quarter(&s, 16, 0.25)];
match plan_or_fill(0, "revolution", &s, &low, &rules, &size, 0.0, None) {
match plan_or_fill(
0,
"revolution",
&FaceSurface::new(s.clone()),
&low,
&rules,
&size,
0.0,
None,
) {
Ok((fp, stats)) => assert!(
!fp.triangles.is_empty() && stats.lenient.is_empty(),
"{stats:?}"
@@ -241,7 +269,16 @@ fn a_size_sample_the_surface_refuses_refuses_only_a_face_it_lies_in() {
let Some(inside) = inside else {
panic!("a sample inside the quarter turn: {size:?}")
};
match plan_or_fill(0, "revolution", &s, &whole, &rules, &size, 0.0, None) {
match plan_or_fill(
0,
"revolution",
&FaceSurface::new(s.clone()),
&whole,
&rules,
&size,
0.0,
None,
) {
Err(e) => assert!(
e.to_string()
.contains(&format!("does not evaluate at {inside:?}")),
@@ -319,7 +356,16 @@ fn a_point_in_a_faces_notch_the_surface_does_not_evaluate_at_does_not_refuse_it(
[0.0, 1.0],
],
)];
match plan_or_fill(0, "spline", &s, &notched, &rules, &size, 0.0, None) {
match plan_or_fill(
0,
"spline",
&FaceSurface::new(s.clone()),
&notched,
&rules,
&size,
0.0,
None,
) {
Ok((fp, stats)) => assert!(
!fp.triangles.is_empty() && stats.lenient.is_empty(),
"{stats:?}"
@@ -330,7 +376,16 @@ fn a_point_in_a_faces_notch_the_surface_does_not_evaluate_at_does_not_refuse_it(
&s,
&[[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]],
)];
match plan_or_fill(0, "spline", &s, &square, &rules, &size, 0.0, None) {
match plan_or_fill(
0,
"spline",
&FaceSurface::new(s.clone()),
&square,
&rules,
&size,
0.0,
None,
) {
Err(e) => assert!(
e.to_string()
.contains("the surface does not evaluate at [0.5, 0.5]"),
@@ -382,7 +437,7 @@ fn the_flips_name_a_middle_the_surface_does_not_evaluate_at() {
match super::sag::flip(
&s,
0.01,
&mut super::sag::Poles::new(&s, 1e-9),
&mut super::sag::Poles::new(&FaceSurface::new(s.clone()), 1e-9),
&fixed,
&uv,
&mut tris,
@@ -482,7 +537,16 @@ fn a_division_point_too_many_periods_away_to_place_refuses_the_face_for_that() {
let s = cylinder_without(0.5);
let loops = [long_strip(&s)];
let rules = Rules::new(0.01, 40.0, None);
match plan_or_fill(0, "revolution", &s, &loops, &rules, &size(1.0), 0.0, None) {
match plan_or_fill(
0,
"revolution",
&FaceSurface::new(s.clone()),
&loops,
&rules,
&size(1.0),
0.0,
None,
) {
Err(e) => assert!(e.to_string().contains("periods, more than the 4096"), "{e}"),
Ok((fp, _)) => panic!("meshed with {} triangles", fp.triangles.len()),
}
@@ -514,7 +578,7 @@ fn a_size_sample_too_many_periods_away_to_place_refuses_the_face_for_that() {
unevaluated: vec![[0.5, 0.3]],
..size(1.0)
};
match plan_or_fill(0, "revolution", &s, &loops, &rules, &size, 0.0, None) {
match plan_or_fill(0, "revolution", &FaceSurface::new(s.clone()), &loops, &rules, &size, 0.0, None) {
Err(e) => assert!(
e.to_string().ends_with(
"periods, more than the 4096 a point the surface does not evaluate at ([0.5, 0.3]) is looked for in"
@@ -553,7 +617,7 @@ fn the_worst_sag_is_measured_where_the_surface_evaluates() {
let uv = [[0.0, 0.0], [0.75, -0.5], [0.75, 0.5]];
let got = super::sag::worst(
&s,
&mut super::sag::Poles::new(&s, 1e-9),
&mut super::sag::Poles::new(&FaceSurface::new(s.clone()), 1e-9),
&[],
&uv,
&[[0, 1, 2]],
@@ -597,7 +661,16 @@ fn a_face_whose_poles_cannot_be_told_is_refused_with_the_classifiers_reason() {
speed: [1.0, 1.0],
unevaluated: Vec::new(),
};
match plan_or_fill(3, "spline", &s, &loops, &rules, &size, 0.0, None) {
match plan_or_fill(
3,
"spline",
&FaceSurface::new(s.clone()),
&loops,
&rules,
&size,
0.0,
None,
) {
Err(e) => {
let why = e.to_string();
assert!(
+22 -8
View File
@@ -14,7 +14,7 @@ use crate::vec::VecExt as _;
use solidcore::eval::EvalSurface;
use solidcore_record::Vec3;
use super::{NoValue, Unclassified};
use super::{FaceSurface, NoValue, Unclassified};
/// 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
@@ -192,7 +192,7 @@ pub(crate) fn refine(
/// triangulation, as the kernel's classifier says (within `reach`): a property of the node's
/// parameters, so it is asked once per node however many triangles and rounds look at it.
pub(crate) struct Poles<'a> {
s: &'a EvalSurface,
s: &'a FaceSurface,
reach: f64,
/// Per node index; `None` until asked.
flags: Vec<Option<[bool; 2]>>,
@@ -202,7 +202,7 @@ pub(crate) struct Poles<'a> {
}
impl<'a> Poles<'a> {
pub(crate) fn new(s: &'a EvalSurface, reach: f64) -> Self {
pub(crate) fn new(s: &'a FaceSurface, reach: f64) -> Self {
Poles {
s,
reach,
@@ -648,7 +648,7 @@ mod tests {
refine(
&s,
0.01,
&mut Poles::new(&s, 1e-9),
&mut Poles::new(&FaceSurface::new(s.clone()), 1e-9),
&fixed,
&mut uv,
&mut tris,
@@ -677,7 +677,7 @@ mod tests {
refine(
&s,
0.01,
&mut Poles::new(&s, 1e-9),
&mut Poles::new(&FaceSurface::new(s.clone()), 1e-9),
&fixed,
&mut uv,
&mut tris,
@@ -686,7 +686,14 @@ mod tests {
Err(Stop::NoValue(NoValue([0.5, 0.5])))
);
assert_eq!(
flip(&s, 0.01, &mut Poles::new(&s, 1e-9), &fixed, &uv, &mut tris),
flip(
&s,
0.01,
&mut Poles::new(&FaceSurface::new(s.clone()), 1e-9),
&fixed,
&uv,
&mut tris
),
Err(Stop::NoValue(NoValue([0.5, 0.5])))
);
}
@@ -715,7 +722,13 @@ mod tests {
};
let uv = [[0.5, -0.1], [0.5, 0.1], [0.6, 0.0]];
let side = 3.0 * (1.0 - 0.1f64.cos());
let got = worst(&s, &mut Poles::new(&s, 0.0), &[], &uv, &[[0, 1, 2]]);
let got = worst(
&s,
&mut Poles::new(&FaceSurface::new(s.clone()), 0.0),
&[],
&uv,
&[[0, 1, 2]],
);
assert!(
got.as_ref()
.is_ok_and(|(w, unmeasured)| (w - side).abs() < 1e-9 * side && *unmeasured == 0),
@@ -725,7 +738,7 @@ mod tests {
assert_eq!(
worst(
&s,
&mut Poles::new(&s, 0.0),
&mut Poles::new(&FaceSurface::new(s.clone()), 0.0),
&[],
&uv,
&[[0, 1, 2], [0, 1, 3]]
@@ -774,6 +787,7 @@ mod tests {
tris.push([a, d, b]);
}
}
let s = FaceSurface::new(s);
let mut poles = Poles::new(&s, 1e-9);
let start = uv.len();
// Refinement is capped (it may or may not run out of room): the count holds either way.
+125
View File
@@ -7,6 +7,9 @@
//! nothing of its own: [`SurfaceEval`] makes the evaluator's form, and a label in place of its
//! form is the record's error.
use std::collections::HashMap;
use std::sync::Mutex;
use solidcore::eval::{EvalSurface, SurfaceEval};
use solidcore_convert as kernel;
use solidcore_record::brep;
@@ -33,11 +36,133 @@ pub(crate) fn eval_surface(surface: &brep::Surface) -> Result<EvalSurface> {
}
}
/// A face's surface with the kernel's pole classifier's answers kept per iso-line.
///
/// Whether a surface is collapsed along `d` at `q` is decided on the iso-line through `q` along
/// `d` alone ([`solidcore::eval::collapse`]): it depends on `q` only through the other
/// parameter (wrapped into the surface's period, [`EvalSurface::wrap`]) and on the reach. One
/// classification builds that iso-line's whole control row (a de Boor evaluation per column of
/// the net), so on a spline of many poles it costs far more than a point evaluation, while the
/// planner asks it at every node, and the nodes of a face share iso-lines: every
/// node of an edge on a patch side, a structured grid's rows and columns, a cut grid's lines.
/// Each answer is kept here under its iso-line and asked of the kernel once. A refusal is not
/// kept (its message names the point), so it is asked again and comes out the same.
pub(crate) struct FaceSurface {
eval: EvalSurface,
/// `(d, other parameter, reach)`, the two numbers by their bits, to the kernel's answer.
collapsed: Mutex<HashMap<(usize, u64, u64), bool>>,
/// Calls of the kernel's classifier made, for the tests.
#[cfg(test)]
asked: std::sync::atomic::AtomicUsize,
}
impl FaceSurface {
pub(crate) fn new(eval: EvalSurface) -> FaceSurface {
FaceSurface {
eval,
collapsed: Mutex::new(HashMap::new()),
#[cfg(test)]
asked: std::sync::atomic::AtomicUsize::new(0),
}
}
/// The surface, for the plan.
pub(crate) fn into_eval(self) -> EvalSurface {
self.eval
}
/// Whether the surface is collapsed along `d` at `q` to within `reach`, as the kernel's
/// classifier says ([`solidcore::eval::uv::degenerate_within`]), asked once per iso-line.
pub(crate) fn collapsed(
&self,
q: [f64; 2],
d: usize,
reach: f64,
) -> std::result::Result<bool, solidcore::eval::EvalError> {
let ask = || {
#[cfg(test)]
self.asked
.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
solidcore::eval::uv::degenerate_within(&self.eval, q, d, reach)
};
// A plane's answer is its axis alone, cheaper than the lookup; a parameter that is not
// finite, or a direction past v, is the kernel's refusal.
if matches!(self.eval, EvalSurface::Plane { .. })
|| d > 1
|| !(q[0].is_finite() && q[1].is_finite())
{
return ask();
}
let Some(&other) = self.eval.wrap(q).get(1 - d) else {
return ask();
};
let key = (d, other.to_bits(), reach.to_bits());
if let Some(&known) = self.lock().get(&key) {
return Ok(known);
}
let answer = ask()?;
self.lock().insert(key, answer);
Ok(answer)
}
fn lock(&self) -> std::sync::MutexGuard<'_, HashMap<(usize, u64, u64), bool>> {
// The map holds only finished answers, so one left by a panicking thread is sound.
self.collapsed
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
}
/// Calls of the kernel's classifier made so far.
#[cfg(test)]
pub(crate) fn asked(&self) -> usize {
self.asked.load(std::sync::atomic::Ordering::Relaxed)
}
}
impl std::ops::Deref for FaceSurface {
type Target = EvalSurface;
fn deref(&self) -> &EvalSurface {
&self.eval
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::planner::tests::fixture;
/// The memo gives the kernel's own answer at every node, and asks the kernel once per
/// iso-line: on a 9 x 9 grid of each curved face of the coil and of a sphere (whose poles
/// collapse a whole row), 18 questions per reach, not 162.
#[test]
fn the_pole_classifier_is_asked_once_per_iso_line() {
for name in ["smlib_coil_lower_half", "sphere_r4"] {
for face in &fixture(name).faces {
let s = FaceSurface::new(eval_surface(face.parameters()).unwrap());
if matches!(*s, EvalSurface::Plane { .. }) {
continue;
}
let dom = solidcore::eval::uv::domain(&s);
let [(u0, u1), (v0, v1)] = dom.range.map(|r| r.unwrap_or((0.0, 1.0)));
let at = |a: f64, b: f64, i: usize| a + (b - a) * i as f64 / 8.0;
for reach in [0.0, 1e-3] {
for i in 0..9 {
for j in 0..9 {
let q = [at(u0, u1, i), at(v0, v1, j)];
for d in 0..2 {
let want = solidcore::eval::uv::degenerate_within(&s, q, d, reach)
.unwrap();
assert_eq!(s.collapsed(q, d, reach).unwrap(), want, "{name} {q:?}");
}
}
}
}
assert_eq!(s.asked(), 2 * 2 * 9, "{name}");
}
}
}
/// Every face of the primitives evaluates on its own parameters (a plane, or the NURBS form
/// the record holds), and a cylinder, sphere or torus label in place of its form is refused
/// as the record's error: tess makes no form of its own.
+1 -3
View File
@@ -4,8 +4,6 @@
use std::cmp::Ordering;
use solidcore::eval::EvalSurface;
use super::seam;
/// Two chords cross only where each one's ends lie strictly either side of the other, by more
@@ -49,7 +47,7 @@ fn shifted(q: [f64; 2], d: usize, by: f64) -> [f64; 2] {
/// [`solidcore::eval::uv::degenerate_within`]); a node whose poles the kernel's classifier
/// cannot tell is the error.
pub(crate) fn crossing(
s: &EvalSurface,
s: &super::FaceSurface,
reach: f64,
loops: &[seam::Loop],
) -> Result<Vec<(usize, usize)>, super::Unclassified> {
+21 -9
View File
@@ -982,7 +982,7 @@ fn a_curved_face_crossing_itself_is_filled_on_its_surface() {
plan_face(
0,
"spline",
&s,
&FaceSurface::new(s.clone()),
loops,
&rules,
&size,
@@ -995,7 +995,7 @@ fn a_curved_face_crossing_itself_is_filled_on_its_surface() {
let Ok(fp) = plan_face(
0,
"spline",
&s,
&FaceSurface::new(s.clone()),
loops,
&rules,
&size,
@@ -1038,7 +1038,7 @@ fn a_face_filled_leniently_is_named_in_the_stats() {
let Ok((fp, stats)) = plan_or_fill(
7,
"spline",
&s,
&FaceSurface::new(s.clone()),
std::slice::from_ref(&l),
&rules,
&size,
@@ -1072,7 +1072,7 @@ fn a_face_untangling_cannot_resolve_is_filled_leniently_and_named() {
let Ok((fp, stats)) = plan_or_fill(
7,
"spline",
&s,
&FaceSurface::new(s.clone()),
std::slice::from_ref(&l),
&rules,
&size,
@@ -1348,7 +1348,7 @@ fn sag_refinement_across_an_inflection_ends_without_slivers() {
sag::refine(
&s,
tol,
&mut sag::Poles::new(&s, 1e-9),
&mut sag::Poles::new(&FaceSurface::new(s.clone()), 1e-9),
&fixed,
&mut uv,
&mut tris,
@@ -1409,7 +1409,15 @@ fn triangles_standing_across_a_curved_sheet_are_flipped_along_it() {
.sum()
};
let before = area(&tris);
sag::flip(&s, 0.1, &mut sag::Poles::new(&s, 1e-9), &[], &uv, &mut tris).unwrap();
sag::flip(
&s,
0.1,
&mut sag::Poles::new(&FaceSurface::new(s.clone()), 1e-9),
&[],
&uv,
&mut tris,
)
.unwrap();
let after = area(&tris);
let sides: Vec<(u32, u32)> = tris
.iter()
@@ -1868,7 +1876,7 @@ fn a_face_with_a_loop_of_one_node_is_refused_naming_the_loop() {
assert_eq!(nodes[e as usize].points.len(), 2);
let face = &b.faces[4];
let s = ok(surface::eval_surface(face.parameters()));
match boundary(&b, 4, face, &s, &nodes, &rules) {
match boundary(&b, 4, face, &FaceSurface::new(s.clone()), &nodes, &rules) {
Err(e) => assert!(
e.to_string()
.ends_with("face 4 (plane): loop 1 has 1 node (1 edge uses)"),
@@ -1937,10 +1945,14 @@ fn a_thin_loops_edge_that_cannot_be_split_is_returned_with_why() {
fn an_edge_untangling_cannot_split_refuses_its_faces() {
let b = fixture("box_10x6x4");
let rules = size::Rules::new(0.01, 12.0, None);
let evals: Vec<Option<EvalSurface>> = b
let evals: Vec<Option<FaceSurface>> = b
.faces
.iter()
.map(|f| surface::eval_surface(f.parameters()).ok())
.map(|f| {
surface::eval_surface(f.parameters())
.ok()
.map(FaceSurface::new)
})
.collect();
let mut edge_faces: Vec<Vec<usize>> = vec![Vec::new(); b.edges.len()];
for (fi, f) in b.faces.iter().enumerate() {
+1 -1
View File
@@ -135,7 +135,7 @@ pub(crate) fn limit_normal(
/// which refuses the face there.
pub(crate) fn plan_poles(
fi: usize,
s: &EvalSurface,
s: &crate::planner::FaceSurface,
f: &FacePlan,
reach: f64,
) -> (Vec<PoleNode>, Vec<AveragedPole>) {