Compare commits

..

2 Commits

Author SHA1 Message Date
bherbruck 6d01515911 implicit, torus: evaluates_as's bound derived term by term; the recognised torus's sweep frame from its form (#3185, #3199) (#13743)
Moved from tangency-labs/solidcore#3224.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-09 10:19:58 -05:00
bherbruck 8ffb97d541 convert: flat parity pins the bodies that differ, not the corpus size (#13999)
The corpus grows as cases are added, so the merged kernel -> main sync saw
1,337 bodies instead of 1,332 (all five new ones identical) and failed.
Every differing body is already pinned by name; the totals now check that
the rest are identical or within rounding.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-09 00:27:13 -05:00
27 changed files with 5209 additions and 2330 deletions
+12 -14
View File
@@ -7,8 +7,8 @@
//! kind, by what it became; a number within [`ROUNDING`] of its size (relative, no floor) is
//! counted as rounding, with its count and the largest.
//!
//! On the whole corpus (1,332 bodies, at #3187): 1,256 come back bit for bit, 36 within
//! rounding (the largest 4.9e-16 relative), and 40 differ, each in exactly the classes
//! On the whole corpus (1,333 bodies, at #3199 with kernel case 11991 (#3231) in): 1,259 come
//! back bit for bit, 36 within rounding (the largest 4.9e-16 relative), and 38 differ, each in exactly the classes
//! [`pinned`] lists for it.
#![allow(
clippy::unwrap_used,
@@ -390,8 +390,8 @@ fn pinned() -> BTreeMap<String, BTreeSet<String>> {
],
),
// a label its form is not proved on goes as the form alone (#2540): cylinders whose
// frames are about 1e-14 rad off their forms (two of them at the rounding of a derived
// bound, #3185's thread)
// frames are off their forms past the rounding `evaluates_as` derives (margins 1.23 to
// 2.03, tests/issue_3185_label_margins.rs)
(
label("cylinder -> rational_spline", &["radius"]),
&[
@@ -401,18 +401,14 @@ fn pinned() -> BTreeMap<String, BTreeSet<String>> {
"fillets/2903-original-rim-round.json:b0",
],
),
// tori whose frame and fitted sweep are about 1e-14 rad off their forms, rightly
// refused (#3185)
// a torus label whose frame is off its form's own first sweep row past the rounding
// `evaluates_as` derives (margin 1.69, tests/issue_3185_label_margins.rs)
(
label(
"torus -> rational_spline",
&["major_radius", "minor_radius"],
),
&[
"sweeps/3897-sweep-ring-along-a-short-arc-joined-to-a-collar.json:b6",
"sweeps/4280-circular-pattern-of-sweep-cut-groove-on-hub.json:b46",
"sweeps/4280-circular-pattern-of-sweep-cut-groove-on-hub.json:b85",
],
&["sweeps/4280-circular-pattern-of-sweep-cut-groove-on-hub.json:b46"],
),
// an extruded circle the flat hop left a spline, proved a cylinder
(
@@ -582,10 +578,12 @@ fn parity(stride: usize) {
missing.is_empty(),
"pinned bodies that no longer differ: {missing:#?}"
);
// The corpus grows as cases are added, so the totals aren't pinned: every body that
// differs is pinned above, and the rest must be identical or within rounding.
assert_eq!(
(t.bodies, t.identical, t.rounding_only),
(1332, 1256, 36),
"bodies, identical, within rounding"
(t.bodies - t.identical - t.rounding_only, t.rounding_only),
(pinned.len(), 36),
"bodies that differ, bodies within rounding"
);
// the largest rounding difference, relative (#3137: 4.885e-16)
assert!(t.rounding_largest <= 4.9e-16, "{:e}", t.rounding_largest);
@@ -0,0 +1,258 @@
//! (#3185) An analytic label is kept on its NURBS form when the kernel's analytic surface,
//! carrying the label's numbers, evaluates as the form within the rounding
//! `AnalyticSupport::build` derives term by term (`surface::implicit`'s `evaluates_as`):
//! [`AnalyticSupport::parametrisation_margin`] is the largest ratio of distance to bound over
//! the form's Greville points, at most 1 where the label stands.
//!
//! - The corpus labels #3137's parity run refused under the old constant `gamma(16 (p + q +
//! 2)) size`, each pinned to its margin as measured under the derived bound: the three tori
//! of b6 and b85 (0.02 to 0.50) stand; b46's torus (1.69) and the four cylinders (b94 1.23,
//! b43 1.40, the 2903 rims 1.52 and 2.03) do not: their frames are off their forms past
//! the rounding at their size. Since #3199 the kernel's recognised sweep frame is the form's
//! own (its end meridians), so a label is measured against the form, not against a fit
//! that shared the label's error.
//! - A torus the kernel makes at the groove's sizes stands on its own form, and the same label
//! turned about its axis by four times what the bound allows is refused.
#![allow(
clippy::unwrap_used,
clippy::expect_used,
clippy::panic,
clippy::indexing_slicing,
clippy::print_stderr,
clippy::disallowed_methods
)] // test (the turn's cos and sin)
use std::fmt::Write as _;
use std::io::Read as _;
use std::path::{Path, PathBuf};
use serde_json::Value;
use solidcore::blend::fillet_section::station::as_b_spline;
use solidcore::geom::axis2_placement::Axis2Placement;
use solidcore::geom::extent2d::Extent2d;
use solidcore::geom::vector3d::{Point3, Vec3};
use solidcore::kernel::{Kernel, KernelOptions, UpstreamFixes};
use solidcore::surface::from_parameters::from_parameters;
use solidcore::surface::implicit::AnalyticSupport;
use solidcore::surface::surface::{Surface, SurfaceKind};
use solidcore::surface::torus::{Torus, TorusOptions};
use solidcore_record::brep::Brep as Record;
fn corpus() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR")).join("../solidcore/tests/kernel_cases")
}
/// The corpus's kernel, its fixes on (as the parity run and SolidCraft run it).
fn kernel() -> Kernel {
let v: Value =
serde_json::from_str(&std::fs::read_to_string(corpus().join("fixes.json")).unwrap())
.unwrap();
let mut fixes = UpstreamFixes::default();
for flag in v.as_array().unwrap() {
*fixes.flag_mut(flag.as_str().unwrap()).unwrap() = true;
}
let mut k = Kernel::new(&KernelOptions::default());
k.fixes = fixes;
k
}
fn body(case: &str, id: &str) -> Record {
let gz = corpus().join(format!("{case}.bodies.json.gz"));
let mut text = String::new();
flate2::read::GzDecoder::new(std::fs::File::open(&gz).unwrap())
.read_to_string(&mut text)
.unwrap();
let records: Value = serde_json::from_str(&text).unwrap();
serde_json::from_value(records[id].clone()).unwrap()
}
fn placement(f: &solidcore::eval::Frame) -> Axis2Placement {
let v = |a: [f64; 3]| Vec3::new(a[0], a[1], a[2]);
Axis2Placement::from_stored(
Point3::new(f.origin[0], f.origin[1], f.origin[2]),
v(f.x_axis),
v(f.y_axis),
)
}
/// A corpus face's label as the kernel's support, and its NURBS form.
fn label_and_form(
k: &Kernel,
case: &str,
id: &str,
face: usize,
) -> (
AnalyticSupport,
solidcore::nurbs::b_spline_surface::BSplineSurface,
) {
let r = body(case, id);
let f = &r.faces[face];
let nurbs = solidcore_convert::surface(f.nurbs.as_ref().unwrap()).unwrap();
let (s, _) = from_parameters(k, &nurbs, f.domain).unwrap();
let SurfaceKind::BSpline(form) = s.kind() else {
panic!("{case} {id} {face}: the form is not a B-spline")
};
let support = match solidcore_convert::surface(&f.surface).unwrap() {
solidcore::eval::Surface::Torus {
frame,
major_radius,
minor_radius,
..
} => AnalyticSupport::Torus {
placement: placement(&frame),
major_radius,
minor_radius,
},
solidcore::eval::Surface::Cylinder { frame, radius } => AnalyticSupport::Cylinder {
placement: placement(&frame),
radius,
},
other => panic!("{case} {id} {face}: {other:?}"),
};
(support, form.copy())
}
/// (case, body, face, the margin measured at #3185, whether the label stands)
const CASES: &[(&str, &str, usize, f64, bool)] = &[
(
"sweeps/3897-sweep-ring-along-a-short-arc-joined-to-a-collar",
"b6",
1,
0.0201,
true,
),
(
"sweeps/4280-circular-pattern-of-sweep-cut-groove-on-hub",
"b46",
1,
1.6943,
false,
),
(
"sweeps/4280-circular-pattern-of-sweep-cut-groove-on-hub",
"b85",
15,
0.4995,
true,
),
(
"sweeps/4280-circular-pattern-of-sweep-cut-groove-on-hub",
"b85",
21,
0.4245,
true,
),
(
"booleans/3456-deep-cut-through-drilled-taper-leaves-base",
"b94",
3,
1.2339,
false,
),
(
"crashes/2243-step-export-flanged-enclosure",
"b43",
1,
1.4008,
false,
),
("fillets/2903-original-rim-chamfer", "b0", 1, 1.5243, false),
("fillets/2903-original-rim-chamfer", "b0", 3, 2.0275, false),
("fillets/2903-original-rim-round", "b0", 1, 1.5243, false),
("fillets/2903-original-rim-round", "b0", 3, 2.0275, false),
];
/// Each corpus label's margin, pinned to 1 % of what #3185 measured, on its side of 1, and
/// `build` agreeing with it.
#[test]
fn the_corpus_labels_stand_or_fall_by_the_derived_bound() {
let k = kernel();
let mut report = String::new();
let mut wrong = Vec::new();
for &(case, id, face, pinned, stands) in CASES {
let (support, form) = label_and_form(&k, case, id, face);
let m = support.parametrisation_margin(&k, &form).unwrap().unwrap();
let _ = writeln!(report, "{case} {id} face {face}: margin {m:.4}");
let built = support.build(&k, form).is_ok();
if (m <= 1.0) != stands || built != stands || (m - pinned).abs() > 0.01 * pinned {
wrong.push(format!(
"{case} {id} face {face}: margin {m} (pinned {pinned}, stands {stands}), build {built}"
));
}
}
eprintln!("{report}");
assert!(wrong.is_empty(), "{wrong:#?}");
}
/// A torus at the groove's sizes (#3185's b46: centre (19.375, -33.558, 37.5), R 33.657, r 2,
/// a 43.6 degree sweep of the whole tube), made by the kernel: its own label stands on its
/// form; turned about its axis by four times the turn the bound allows (the margin is linear
/// in a small turn: a turn of `t` reads `m(t)`, so `4 t / m(t)` reads 4), it is refused.
#[test]
fn a_torus_label_turned_past_its_bound_is_refused() {
let k = kernel();
let o = Point3::new(19.374_999_999_999_975, -33.558_484_396_647_01, 37.5);
let x = Vec3::new(
-0.427_099_277_807_218_7,
0.739_757_649_038_078_7,
-0.519_946_946_895_745_4,
);
let y = Vec3::new(
0.259_973_473_447_872_36,
-0.450_287_264_631_874_2,
-0.854_198_555_614_438_5,
);
let domain = Extent2d::from_bounds(0.0, -180.0, 43.602_818_972_703_645, 180.0);
let made = Torus::new(
&k,
o,
x,
y,
domain,
33.657_280_044_590_65,
2.0,
TorusOptions::default(),
)
.unwrap();
let s = Surface::from(made);
let form = as_b_spline(&k, &s).unwrap().unwrap().into_owned();
let own = AnalyticSupport::of(&s).unwrap();
let m0 = own.parametrisation_margin(&k, &form).unwrap().unwrap();
assert!(m0 <= 1.0, "own label: {m0}");
assert!(own.build(&k, form.copy()).is_ok());
let turned = |t: f64| {
let AnalyticSupport::Torus {
placement,
major_radius,
minor_radius,
} = own
else {
panic!()
};
let (c, sn) = (t.cos(), t.sin());
let (px, py) = (placement.x_axis(), placement.y_axis());
AnalyticSupport::Torus {
placement: Axis2Placement::from_stored(
placement.origin(),
px * c + py * sn,
py * c - px * sn,
),
major_radius,
minor_radius,
}
};
let probe = 1e-11;
let m1 = turned(probe)
.parametrisation_margin(&k, &form)
.unwrap()
.unwrap();
assert!(m1 > 1.0, "a turn of {probe} rad reads {m1}");
let four = 4.0 * probe / m1;
let m4 = turned(four)
.parametrisation_margin(&k, &form)
.unwrap()
.unwrap();
eprintln!("own {m0:.4}; turn {probe:e}: {m1:.3}; turn {four:e}: {m4:.3}");
assert!(m4 > 1.0, "a turn of {four:e} rad reads {m4}");
assert!(turned(four).build(&k, form).is_err());
}
-611
View File
@@ -1,611 +0,0 @@
//! tess's own display evaluator (docs/layers.md §3: private, display-only, never reachable from
//! ops or the apps): SolidCraft's plan evaluator, which the planner samples its faces on. The
//! plan itself carries the kernel's evaluator form ([`solidcore::eval::EvalSurface`]); this one
//! is the same surface, value for value ([`crate::planner::surface::fast_surface`]), evaluated
//! without the kernel's certificates, so the planner's many point and derivative evaluations
//! cost what they cost in SolidCraft's tess.
//!
//! References: Piegl & Tiller, *The NURBS Book* (2nd ed.), A2.1 (span), A2.3 (basis
//! functions and derivatives), §4.5 (derivatives of a rational surface).
// SolidCraft's planner and display evaluator, moved here as they are (RFC 0003, item 1.1), so
// they stay comparable line for line with `crates/kernel/src/tess` and `crates/geom/src/tess`
// until those are deleted (item 1.3). Their slice indexing is within bounds the code checks
// before it, and a panic anywhere in planning is caught at [`crate::plan`] and returned as
// [`crate::TessError::Internal`]; checked access and the rest of solidcore's style come with
// item 1.2, which adds solidcore-tess's own hardening to this base.
#![allow(
clippy::indexing_slicing,
clippy::neg_cmp_op_on_partial_ord,
clippy::type_complexity,
clippy::single_match_else,
clippy::items_after_statements,
clippy::redundant_closure_for_method_calls,
clippy::doc_markdown,
clippy::needless_for_each,
clippy::nonminimal_bool,
clippy::semicolon_if_nothing_returned,
clippy::explicit_iter_loop,
clippy::if_not_else,
clippy::needless_pass_by_value,
clippy::cast_lossless
)]
use crate::vec::VecExt as _;
use solidcore_record::Vec3;
pub(crate) mod uv;
/// The highest spline degree the evaluator takes.
pub(crate) const MAX_SPLINE_DEGREE: usize = solidcore_record::brep::MAX_SPLINE_DEGREE;
/// Basis slots for the highest evaluable degree.
pub(crate) const W: usize = MAX_SPLINE_DEGREE + 1;
/// A rational B-spline curve: homogeneous control points `[x·w, y·w, z·w, w]`. Outside its
/// domain it continues as the polynomial of the end span (a line extends for ever).
#[derive(Clone, Debug, PartialEq)]
pub struct EvalCurve {
pub degree: usize,
pub knots: Vec<f64>,
pub ctrl: Vec<[f64; 4]>,
}
/// A surface in a form both evaluators understand.
#[derive(Clone, Debug, PartialEq)]
pub enum EvalSurface {
/// `origin + u·du + v·dv`.
Plane { origin: Vec3, du: Vec3, dv: Vec3 },
/// A rational tensor B-spline; control point `(i, j)` is `ctrl[j·count[0] + i]`. `closed`:
/// whether it closes on itself along each direction ([`uv::closed`]), worked out once by
/// [`EvalSurface::nurbs`] (every evaluation outside the knot interval asks, and the test
/// walks the whole control net).
Nurbs {
degree: [usize; 2],
knots: [Vec<f64>; 2],
count: [usize; 2],
ctrl: Vec<[f64; 4]>,
closed: [bool; 2],
},
/// `profile(u)` turned by the angle `v` (right-handed) about the axis through `origin`
/// along the unit `axis`, then mapped by the affine `transform` (row-major 3×4); with
/// `swap` the parameters are exchanged first (the surface is evaluated at `(v, u)`).
Revolved {
profile: EvalCurve,
origin: Vec3,
axis: Vec3,
transform: [[f64; 4]; 3],
swap: bool,
},
}
impl EvalCurve {
/// Point and first derivative at `t`; `None` if the curve is malformed or its weight
/// vanishes there.
pub fn eval(&self, t: f64) -> Option<(Vec3, Vec3)> {
let p = self.degree;
let n = self.ctrl.len();
if p == 0
|| p > MAX_SPLINE_DEGREE
|| n <= p
|| self.knots.len() != n + p + 1
|| !t.is_finite()
{
return None;
}
self.eval_on(span_of(&self.knots, p, n, t), t)
}
/// Point and first derivative at `t` of the polynomial on knot span `span`.
fn eval_on(&self, span: usize, t: f64) -> Option<(Vec3, Vec3)> {
let p = self.degree;
let ders = basis_ders(&self.knots, span, p, t)?;
let (mut a, mut da) = ([0.0; 4], [0.0; 4]);
for (k, (n0, n1)) in ders[0].iter().zip(&ders[1]).take(p + 1).enumerate() {
let c = self.ctrl.get(span + k - p)?;
for d in 0..4 {
a[d] += n0 * c[d];
da[d] += n1 * c[d];
}
}
rational(a, da)
}
}
impl EvalSurface {
/// The rational tensor B-spline [`EvalSurface::Nurbs`], with whether it closes on itself.
pub fn nurbs(
degree: [usize; 2],
knots: [Vec<f64>; 2],
count: [usize; 2],
ctrl: Vec<[f64; 4]>,
) -> EvalSurface {
let closed = uv::closed(degree, &knots, count, &ctrl);
EvalSurface::Nurbs {
degree,
knots,
count,
ctrl,
closed,
}
}
/// Point and unit normal (`S_u × S_v`, oriented as the surface) at `(u, v)`. A degenerate
/// point (a cone's apex, a sphere's pole) takes its normal from a step inside; anything
/// that still fails gives the origin and +Z rather than a NaN.
pub fn eval(&self, u: f64, v: f64) -> (Vec3, Vec3) {
let Some((p, du, dv)) = self.derivs(u, v) else {
return (Vec3::ZERO, Vec3::Z);
};
let n = self.oriented(du.cross(dv));
if let Some(n) = n
.normalized()
.filter(|_| n.len2() > 1e-20 * (du.len2() * dv.len2()).max(1e-300))
{
return (p, n);
}
// Step into the parameter domain around the degenerate point: never past the end of an
// interval, where the extrapolated surface folds back over the pole and its normal
// points the other way.
let h = 1e-6 * (1.0 + u.abs().max(v.abs()));
let inside = |d: usize, t: f64, s: f64| {
self.interval(d)
.is_none_or(|(lo, hi)| (lo..=hi).contains(&(t + s * h)))
};
for (su, sv) in [(1.0, 1.0), (-1.0, 1.0), (1.0, -1.0), (-1.0, -1.0)] {
if inside(0, u, su)
&& inside(1, v, sv)
&& let Some((_, a, b)) = self.derivs(u + su * h, v + sv * h)
&& let Some(n) = self.oriented(a.cross(b)).normalized()
{
return (p, n);
}
}
(p, Vec3::Z)
}
/// The active parameter interval along `d` (0: u, 1: v) of a spline, or of a revolution's
/// profile; `None` where the parameter is unbounded (a plane, a revolution's angle).
fn interval(&self, d: usize) -> Option<(f64, f64)> {
match self {
EvalSurface::Plane { .. } => None,
EvalSurface::Nurbs {
degree,
knots,
count,
..
} => Some((
*knots.get(d)?.get(*degree.get(d)?)?,
*knots.get(d)?.get(*count.get(d)?)?,
)),
EvalSurface::Revolved { profile, swap, .. } => (d == usize::from(*swap))
.then(|| {
Some((
*profile.knots.get(profile.degree)?,
*profile.knots.get(profile.ctrl.len())?,
))
})
.flatten(),
}
}
/// Mirror transforms reverse the cross product of the mapped derivatives.
fn oriented(&self, n: Vec3) -> Vec3 {
match self {
EvalSurface::Revolved { transform: m, .. } if det3(m) < 0.0 => -n,
_ => n,
}
}
/// `uv` with each parameter along which a spline closes on itself (or a revolution's
/// profile does) moved by whole periods into the active knot interval (unchanged inside it,
/// and for any other surface): the parameters both evaluators take for a point of a closed
/// spline found a period away.
pub fn wrap(&self, uv: [f64; 2]) -> [f64; 2] {
if let EvalSurface::Revolved { profile, swap, .. } = self {
let d = usize::from(*swap);
let range = self.interval(d);
let mut out = uv;
if let (Some(r), Some(x)) = (range, out.get_mut(d))
&& !(r.0..=r.1).contains(x)
&& uv::closed_curve(profile)
{
*x = uv::wrap_into(*x, r);
}
return out;
}
let EvalSurface::Nurbs { closed, .. } = self else {
return uv;
};
let mut out = uv;
for d in 0..2 {
if let (true, Some(r)) = (closed[d], self.interval(d))
&& !(r.0..=r.1).contains(&uv[d])
{
out[d] = uv::wrap_into(uv[d], r);
}
}
out
}
/// Point and first partial derivatives (a closed spline's parameters wrapped into its
/// active interval first, [`EvalSurface::wrap`]).
pub fn derivs(&self, u: f64, v: f64) -> Option<(Vec3, Vec3, Vec3)> {
if !(u.is_finite() && v.is_finite()) {
return None;
}
match self {
EvalSurface::Plane { origin, du, dv } => Some((*origin + *du * u + *dv * v, *du, *dv)),
EvalSurface::Nurbs {
degree,
knots,
count,
ctrl,
..
} => {
let [u, v] = self.wrap([u, v]);
nurbs_derivs(*degree, knots, *count, ctrl, u, v)
}
EvalSurface::Revolved {
profile,
origin,
axis,
transform,
swap,
} => {
let [u, v] = self.wrap([u, v]);
let (s, a) = if *swap { (v, u) } else { (u, v) };
let (c, dc) = profile.eval(s)?;
let (sin, cos) = a.sin_cos();
let rot =
|x: Vec3| x * cos + axis.cross(x) * sin + *axis * (axis.dot(x) * (1.0 - cos));
let r = c - *origin;
let p = *origin + rot(r);
let ds = rot(dc);
// d/dθ of the rotated point: axis × (rotated radius).
let da = axis.cross(rot(r));
let (p, ds, da) = (
affine(transform, p),
linear(transform, ds),
linear(transform, da),
);
Some(if *swap { (p, da, ds) } else { (p, ds, da) })
}
}
}
}
impl EvalSurface {}
fn affine(m: &[[f64; 4]; 3], p: Vec3) -> Vec3 {
linear(m, p) + Vec3::new(m[0][3], m[1][3], m[2][3])
}
fn linear(m: &[[f64; 4]; 3], v: Vec3) -> Vec3 {
let row = |r: &[f64; 4]| r[0] * v.x + r[1] * v.y + r[2] * v.z;
Vec3::new(row(&m[0]), row(&m[1]), row(&m[2]))
}
fn det3(m: &[[f64; 4]; 3]) -> f64 {
m[0][0] * (m[1][1] * m[2][2] - m[1][2] * m[2][1])
- m[0][1] * (m[1][0] * m[2][2] - m[1][2] * m[2][0])
+ m[0][2] * (m[1][0] * m[2][1] - m[1][1] * m[2][0])
}
/// The point and derivative of a rational function from its homogeneous value and derivative.
fn rational(a: [f64; 4], da: [f64; 4]) -> Option<(Vec3, Vec3)> {
let w = a[3];
if !(w.abs() > 1e-300) {
return None;
}
let p = Vec3::new(a[0] / w, a[1] / w, a[2] / w);
let d = (Vec3::new(da[0], da[1], da[2]) - p * da[3]) * (1.0 / w);
(p.is_finite() && d.is_finite()).then_some((p, d))
}
fn nurbs_derivs(
degree: [usize; 2],
knots: &[Vec<f64>; 2],
count: [usize; 2],
ctrl: &[[f64; 4]],
u: f64,
v: f64,
) -> Option<(Vec3, Vec3, Vec3)> {
let [pu, pv] = degree;
let [nu, nv] = count;
let ok = |p: usize, n: usize, k: &[f64]| {
(1..=MAX_SPLINE_DEGREE).contains(&p) && n > p && k.len() == n + p + 1
};
if !ok(pu, nu, &knots[0]) || !ok(pv, nv, &knots[1]) || nu.checked_mul(nv)? != ctrl.len() {
return None;
}
let (su, sv) = (span_of(&knots[0], pu, nu, u), span_of(&knots[1], pv, nv, v));
let (mut bu, mut bv) = ([[0.0f64; W]; 2], [[0.0f64; W]; 2]);
basis_ders_into(&knots[0], su, pu, u, &mut bu)?;
basis_ders_into(&knots[1], sv, pv, v, &mut bv)?;
let (mut s, mut s_u, mut s_v) = ([0.0; 4], [0.0; 4], [0.0; 4]);
for (b, (nv0, nv1)) in bv[0].iter().zip(&bv[1]).take(pv + 1).enumerate() {
for (a, (nu0, nu1)) in bu[0].iter().zip(&bu[1]).take(pu + 1).enumerate() {
let c = ctrl.get((sv + b - pv) * nu + su + a - pu)?;
for d in 0..4 {
s[d] += nu0 * nv0 * c[d];
s_u[d] += nu1 * nv0 * c[d];
s_v[d] += nu0 * nv1 * c[d];
}
}
}
let (p, du) = rational(s, s_u)?;
let (_, dv) = rational(s, s_v)?;
Some((p, du, dv))
}
/// The knot span used at `t`: the last non-empty span starting at or before `t`, clamped to
/// the valid spans `p ≤ k < n` so parameters just outside the domain extend the end spans.
pub(crate) fn span_of(knots: &[f64], p: usize, n: usize, t: f64) -> usize {
let mut k = p;
for i in p..n {
if let (Some(&a), Some(&b)) = (knots.get(i), knots.get(i + 1))
&& a < b
{
if a <= t {
k = i;
} else {
break;
}
}
}
k
}
/// The non-zero basis functions of degree `p` on span `span` (NURBS Book A2.2): element `j` is
/// `N_{span−p+j}`.
fn basis_funs(knots: &[f64], span: usize, p: usize, t: f64, n: &mut [f64; W]) -> Option<()> {
if p >= W {
return None;
}
let kn = |i: usize| knots.get(i).copied();
n.fill(0.0);
n[0] = 1.0;
for j in 1..=p {
let mut saved = 0.0;
for (r, nr) in n.iter_mut().enumerate().take(j) {
// right[r + 1] and left[j − r] of A2.2.
let right = kn(span + r + 1)? - t;
let left = t - kn((span + 1 + r).checked_sub(j)?)?;
let den = right + left;
let temp = if den != 0.0 { *nr / den } else { 0.0 };
*nr = saved + right * temp;
saved = left * temp;
}
n[j] = saved;
}
Some(())
}
/// Non-zero basis functions of degree `p` on span `span` and their first derivatives
/// (NURBS Book eq. 2.7, from the degree p − 1 functions on the same span). `ders[0][j]` is
/// `N_{span−p+j}`, `ders[1][j]` its derivative. One-dimensional tables, as the GPU twin keeps
/// them, so a high degree costs its own work only.
pub(crate) fn basis_ders(knots: &[f64], span: usize, p: usize, t: f64) -> Option<[[f64; W]; 2]> {
let mut ders = [[0.0f64; W]; 2];
basis_ders_into(knots, span, p, t, &mut ders)?;
Some(ders)
}
/// [`basis_ders`] into a table the caller keeps: surface evaluation runs this for every point,
/// and moving the tables by value took about a quarter of the time of meshing a curved face.
fn basis_ders_into(
knots: &[f64],
span: usize,
p: usize,
t: f64,
ders: &mut [[f64; W]; 2],
) -> Option<()> {
if p == 0 || p >= W {
return None;
}
let kn = |i: usize| knots.get(i).copied();
let [values, slopes] = ders;
// The degree p − 1 functions first, kept in the derivative row: walked from the top down, so
// each slot still holds them when it and the slot above it are read.
basis_funs(knots, span, p - 1, t, slopes)?;
basis_funs(knots, span, p, t, values)?;
// N'_{i,p} = p·(N_{i,p−1}/(u_{i+p}−u_i) − N_{i+1,p−1}/(u_{i+p+1}−u_{i+1})), i = span − p + r.
for r in (0..=p).rev() {
let mut d = 0.0;
if r >= 1 {
let den = kn(span + r)? - kn((span + r).checked_sub(p)?)?;
if den != 0.0 {
d += slopes[r - 1] / den;
}
}
if r < p {
let den = kn(span + r + 1)? - kn((span + r + 1).checked_sub(p)?)?;
if den != 0.0 {
d -= slopes[r] / den;
}
}
slopes[r] = d * p as f64;
}
Some(())
}
#[cfg(test)]
mod tests {
use super::*;
fn close(a: Vec3, b: Vec3, tol: f64) -> bool {
a.dist(b) <= tol
}
fn line(a: Vec3, b: Vec3) -> EvalCurve {
EvalCurve {
degree: 1,
knots: vec![0.0, 0.0, 1.0, 1.0],
ctrl: vec![[a.x, a.y, a.z, 1.0], [b.x, b.y, b.z, 1.0]],
}
}
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],
];
#[test]
fn plane_point_and_normal() {
let s = EvalSurface::Plane {
origin: Vec3::new(1.0, 2.0, 3.0),
du: Vec3::X,
dv: Vec3::Y,
};
let (p, n) = s.eval(2.0, 3.0);
assert!(close(p, Vec3::new(3.0, 5.0, 3.0), 1e-12));
assert!(close(n, Vec3::Z, 1e-12));
}
#[test]
fn revolved_line_is_a_cylinder() {
// A vertical line at radius 2 turned about Z.
let s = EvalSurface::Revolved {
profile: line(Vec3::new(2.0, 0.0, 0.0), Vec3::new(2.0, 0.0, 5.0)),
origin: Vec3::ZERO,
axis: Vec3::Z,
transform: IDENTITY,
swap: false,
};
for &(u, v) in &[(0.0, 0.0), (0.5, 1.0), (1.0, 4.0), (0.25, 6.0)] {
let (p, n) = s.eval(u, v);
assert!(
close(p, Vec3::new(2.0 * v.cos(), 2.0 * v.sin(), 5.0 * u), 1e-12),
"{p:?}"
);
// S_u = +Z, S_v = tangential: the normal points inward (−radial) for this profile.
assert!(close(n, Vec3::new(-v.cos(), -v.sin(), 0.0), 1e-12), "{n:?}");
}
}
#[test]
fn swapped_parameters_reverse_the_normal() {
let mk = |swap| EvalSurface::Revolved {
profile: line(Vec3::new(2.0, 0.0, 0.0), Vec3::new(2.0, 0.0, 5.0)),
origin: Vec3::ZERO,
axis: Vec3::Z,
transform: IDENTITY,
swap,
};
let (p0, n0) = mk(false).eval(0.3, 1.1);
let (p1, n1) = mk(true).eval(1.1, 0.3);
assert!(close(p0, p1, 1e-12));
assert!(close(n0, -n1, 1e-12));
}
#[test]
fn mirror_transform_keeps_the_normal_outward() {
let mirror = [
[-1.0, 0.0, 0.0, 0.0],
[0.0, 1.0, 0.0, 0.0],
[0.0, 0.0, 1.0, 0.0],
];
let s = EvalSurface::Revolved {
profile: line(Vec3::new(2.0, 0.0, 0.0), Vec3::new(2.0, 0.0, 5.0)),
origin: Vec3::ZERO,
axis: Vec3::Z,
transform: mirror,
swap: false,
};
let (p, n) = s.eval(0.5, 0.0);
assert!(close(p, Vec3::new(-2.0, 0.0, 2.5), 1e-12));
// The unmirrored normal at θ = 0 is −X; mirrored it is +X, still pointing to the axis.
assert!(close(n, Vec3::X, 1e-12), "{n:?}");
}
#[test]
fn rational_quarter_circle_profile_is_exact() {
// A quarter circle of radius 3 in the XZ plane, turned about Z: a sphere zone.
let w = std::f64::consts::FRAC_1_SQRT_2;
let profile = EvalCurve {
degree: 2,
knots: vec![0.0, 0.0, 0.0, 1.0, 1.0, 1.0],
ctrl: vec![
[3.0, 0.0, 0.0, 1.0],
[3.0 * w, 0.0, 3.0 * w, w],
[0.0, 0.0, 3.0, 1.0],
],
};
let s = EvalSurface::Revolved {
profile,
origin: Vec3::ZERO,
axis: Vec3::Z,
transform: IDENTITY,
swap: false,
};
for i in 0..=10 {
for j in 0..=6 {
let (u, v) = (i as f64 / 10.0, j as f64);
let (p, n) = s.eval(u, v);
assert!((p.len() - 3.0).abs() < 1e-12, "{p:?}");
// S_u × S_v points into this sphere. At the pole (u = 1) the normal comes from
// a step inside: within ~1e-6 rad, and the same way (a step past the pole would
// turn it over).
assert!(
n.cross(p).len() < 3e-5 && n.dot(p) < 0.0,
"normal {n:?} not inward at {p:?}"
);
}
}
}
#[test]
fn bilinear_nurbs_patch_and_derivatives() {
// Degree (1, 1) over [0, 2] × [0, 1] with corners lifted: a hyperbolic paraboloid.
let ctrl = vec![
[0.0, 0.0, 0.0, 1.0],
[2.0, 0.0, 0.0, 1.0],
[0.0, 1.0, 0.0, 1.0],
[2.0, 1.0, 1.0, 1.0],
];
let s = EvalSurface::nurbs(
[1, 1],
[vec![0.0, 0.0, 2.0, 2.0], vec![0.0, 0.0, 1.0, 1.0]],
[2, 2],
ctrl,
);
let Some((p, du, dv)) = s.derivs(1.0, 0.5) else {
panic!("evaluates")
};
assert!(close(p, Vec3::new(1.0, 0.5, 0.25), 1e-12), "{p:?}");
assert!(close(du, Vec3::new(1.0, 0.0, 0.25), 1e-12), "{du:?}");
assert!(close(dv, Vec3::new(0.0, 1.0, 0.5), 1e-12), "{dv:?}");
}
#[test]
fn hostile_surfaces_do_not_panic() {
let bad = [
EvalSurface::nurbs([9, 1], [vec![0.0; 3], vec![0.0; 3]], [1, 1], vec![]),
EvalSurface::nurbs(
[1, 1],
[vec![0.0, 0.0, 1.0, 1.0], vec![0.0, 0.0, 1.0, 1.0]],
[2, 2],
vec![[0.0; 4]; 4],
),
EvalSurface::Revolved {
profile: EvalCurve {
degree: 0,
knots: vec![],
ctrl: vec![],
},
origin: Vec3::ZERO,
axis: Vec3::Z,
transform: IDENTITY,
swap: false,
},
];
for s in &bad {
for &(u, v) in &[(0.5, 0.5), (f64::NAN, 0.0), (1e300, -1e300)] {
let (p, n) = s.eval(u, v);
assert!(p.is_finite() && n.is_finite());
}
}
}
}
-328
View File
@@ -1,328 +0,0 @@
//! Parameters of points on an [`EvalSurface`], on the evaluator's own surface form: a
//! surface's parameter box and periods, the parameters of a point on it (closed form on a plane,
//! damped Gauss–Newton elsewhere), unwrapping across a period, and its poles.
//!
//! A spline that closes on itself along a direction (its first and last control rows coincide,
//! weights included, and its knots are clamped there: a NURBS cylinder, a fillet ring round a
//! hole's rim) is periodic along it, one active knot interval per period: the record stays as it
//! is, and parameters outside the interval are evaluated a whole number of periods back inside
//! it ([`EvalSurface::derivs`]). So is a revolution along a profile closed on itself (a torus's
//! whole circle).
use super::EvalSurface;
use crate::vec::VecExt as _;
use solidcore_record::Vec3;
/// The parameter box of a surface and its periods.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Domain {
/// Bounded ranges (`None`: unbounded, as a plane's).
pub range: [Option<(f64, f64)>; 2],
pub period: [Option<f64>; 2],
}
/// The parameter box and periods of `s`.
pub fn domain(s: &EvalSurface) -> Domain {
let tau = std::f64::consts::TAU;
match s {
EvalSurface::Plane { .. } => Domain {
range: [None, None],
period: [None, None],
},
EvalSurface::Nurbs {
degree,
knots,
count,
closed: shut,
..
} => {
let r = |d: usize| Some((*knots[d].get(degree[d])?, *knots[d].get(count[d])?));
let range = [r(0), r(1)];
let period = [0, 1].map(|d| {
range[d]
.filter(|_| shut[d])
.map(|(a, b)| b - a)
.filter(|p| *p > 0.0)
});
Domain { range, period }
}
EvalSurface::Revolved { profile, swap, .. } => {
let r = (|| {
Some((
*profile.knots.get(profile.degree)?,
*profile.knots.get(profile.ctrl.len())?,
))
})();
// A profile closed on itself (a whole circle: a torus as another kernel writes it)
// is periodic along the profile too.
let along = r
.filter(|_| closed_curve(profile))
.map(|(a, b)| b - a)
.filter(|p| *p > 0.0);
let (a, b) = ((r, along), (Some((0.0, tau)), Some(tau)));
let (u, v) = if *swap { (b, a) } else { (a, b) };
Domain {
range: [u.0, v.0],
period: [u.1, v.1],
}
}
}
}
/// Relative closeness of a spline's first and last control rows that makes it closed: control
/// points within this fraction of the control net's extent, weights within this fraction of
/// their size. Rows written as one (a producer's exact copy) pass; a surface whose ends merely
/// come near each other does not.
const CLOSED: f64 = 1e-9;
/// Whether a spline curve closes on itself: its knots clamped at both ends and its first and
/// last control points the same, weights included ([`closed`] for a curve).
pub fn closed_curve(c: &super::EvalCurve) -> bool {
let Some(extent) = net_extent(&c.ctrl) else {
return false;
};
c.ctrl.len() >= 2
&& clamped(c.degree, c.ctrl.len(), &c.knots)
&& same_point(c.ctrl.first(), c.ctrl.last(), extent)
}
/// Whether the tensor spline closes on itself along each direction: its knots clamped at both
/// ends of that direction (so its boundary rows are its edges) and its first and last control
/// rows the same points with the same weights. Control point `(i, j)` is `ctrl[j·count[0] + i]`.
pub fn closed(
degree: [usize; 2],
knots: &[Vec<f64>; 2],
count: [usize; 2],
ctrl: &[[f64; 4]],
) -> [bool; 2] {
let [nu, nv] = count;
if nu.checked_mul(nv) != Some(ctrl.len()) || nu < 2 || nv < 2 {
return [false; 2];
}
let Some(extent) = net_extent(ctrl) else {
return [false; 2];
};
let same = |a, b| same_point(ctrl.get(a), ctrl.get(b), extent);
let u = clamped(degree[0], nu, &knots[0]) && (0..nv).all(|j| same(j * nu, j * nu + nu - 1));
let v = clamped(degree[1], nv, &knots[1]) && (0..nu).all(|i| same(i, (nv - 1) * nu + i));
[u, v]
}
fn cartesian(c: &[f64; 4]) -> Option<Vec3> {
(c[3] != 0.0).then(|| Vec3::new(c[0] / c[3], c[1] / c[3], c[2] / c[3]))
}
/// The diagonal of a control net's box; `None` when a weight vanishes or the net is a point.
fn net_extent(ctrl: &[[f64; 4]]) -> Option<f64> {
let (mut lo, mut hi) = (
Vec3::new(f64::INFINITY, f64::INFINITY, f64::INFINITY),
Vec3::new(f64::NEG_INFINITY, f64::NEG_INFINITY, f64::NEG_INFINITY),
);
for c in ctrl {
let p = cartesian(c)?;
lo = Vec3::new(lo.x.min(p.x), lo.y.min(p.y), lo.z.min(p.z));
hi = Vec3::new(hi.x.max(p.x), hi.y.max(p.y), hi.z.max(p.z));
}
let extent = hi.dist(lo);
(extent.is_finite() && extent > 0.0).then_some(extent)
}
/// Two control points the same within [`CLOSED`] of the net's extent, weights included.
fn same_point(a: Option<&[f64; 4]>, b: Option<&[f64; 4]>, extent: f64) -> bool {
match (a, b) {
(Some(a), Some(b)) => {
matches!((cartesian(a), cartesian(b)), (Some(p), Some(q)) if p.dist(q) <= CLOSED * extent)
&& (a[3] - b[3]).abs() <= CLOSED * a[3].abs().max(b[3].abs())
}
_ => false,
}
}
/// Knots of degree `p` over `n` control points, clamped at both ends: the spline starts at its
/// first control point and ends at its last, its interval `[k[p], k[n]]` ending on knots of
/// multiplicity `p` within it. The outermost knots (`k[0]`, `k[n + p]`) take no part in the
/// spline over its interval, so they need not repeat: a periodic quadratic circle written with
/// knot multiplicities `1, 2, …, 2, 1` (as OpenCascade writes one) is clamped.
fn clamped(p: usize, n: usize, k: &[f64]) -> bool {
let ends = |r: std::ops::RangeInclusive<usize>| {
k.get(r)
.is_some_and(|s| s.windows(2).all(|w| w.first() == w.get(1)))
};
p >= 1 && k.len() == n + p + 1 && ends(1..=p) && ends(n..=n + p - 1)
}
/// How far past an end of the interval, relative to its size, a parameter is still that end:
/// rounding, not a period away.
const WRAP_SLACK: f64 = 1e-12;
/// `x` moved by whole periods `b − a` into `[a, b]` when it lies outside. A parameter a rounding
/// past an end (a pcurve ending on the seam one ulp beyond it) is that end, not moved a period
/// to the other one: a plan's node there would put its triangles across the whole period.
pub(crate) fn wrap_into(x: f64, (a, b): (f64, f64)) -> f64 {
let period = b - a;
if (a..=b).contains(&x) || !(period > 0.0) {
return x;
}
let slack = WRAP_SLACK * period.max(a.abs()).max(b.abs());
if x > b && x - b <= slack {
return b;
}
if x < a && a - x <= slack {
return a;
}
let w = x - ((x - a) / period).floor() * period;
w.clamp(a, b)
}
/// `x` moved by whole periods to lie within half a period of `near`.
pub fn unwrap(x: f64, near: f64, period: Option<f64>) -> f64 {
match period {
Some(p) if p > 0.0 => x - ((x - near) / p).round() * p,
_ => x,
}
}
/// [`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::*;
use crate::fast::EvalCurve;
fn cylinder() -> EvalSurface {
EvalSurface::Revolved {
profile: EvalCurve {
degree: 1,
knots: vec![0.0, 0.0, 1.0, 1.0],
ctrl: vec![[3.0, 0.0, 0.0, 1.0], [3.0, 0.0, 4.0, 1.0]],
},
origin: Vec3::ZERO,
axis: Vec3::Z,
transform: [
[1.0, 0.0, 0.0, 0.0],
[0.0, 1.0, 0.0, 0.0],
[0.0, 0.0, 1.0, 0.0],
],
swap: false,
}
}
/// A revolution of a whole circle (a torus, the profile a rational quadratic closed on
/// itself) is periodic along the profile too, over its knot interval; a cylinder's line
/// profile is not.
#[test]
fn a_revolution_of_a_closed_profile_is_periodic_along_it() {
let w = std::f64::consts::FRAC_1_SQRT_2;
// The circle of radius 1 about (3, 0, 0) in the xz plane, four quarter arcs.
let pts = [
[4.0, 0.0],
[4.0, 1.0],
[3.0, 1.0],
[2.0, 1.0],
[2.0, 0.0],
[2.0, -1.0],
[3.0, -1.0],
[4.0, -1.0],
[4.0, 0.0],
];
let ctrl: Vec<[f64; 4]> = pts
.iter()
.enumerate()
.map(|(i, [x, z])| {
let wt = if i % 2 == 1 { w } else { 1.0 };
[x * wt, 0.0, z * wt, wt]
})
.collect();
let knots = vec![
0.0, 0.0, 0.0, 0.25, 0.25, 0.5, 0.5, 0.75, 0.75, 1.0, 1.0, 1.0,
];
for swap in [false, true] {
let s = EvalSurface::Revolved {
profile: EvalCurve {
degree: 2,
knots: knots.clone(),
ctrl: ctrl.clone(),
},
origin: Vec3::ZERO,
axis: Vec3::Z,
transform: [
[1.0, 0.0, 0.0, 0.0],
[0.0, 1.0, 0.0, 0.0],
[0.0, 0.0, 1.0, 0.0],
],
swap,
};
let (along, around) = if swap { (1, 0) } else { (0, 1) };
let dom = domain(&s);
assert_eq!(
(dom.period[along], dom.period[around]),
(Some(1.0), Some(std::f64::consts::TAU)),
"swap {swap}"
);
let at = |t: f64, a: f64| if swap { [a, t] } else { [t, a] };
let wrapped = s.wrap(at(1.3, 0.4));
assert!(
(wrapped[along] - 0.3).abs() < 1e-12 && wrapped[around] == 0.4,
"swap {swap}: {wrapped:?}"
);
assert_eq!(
s.wrap(at(0.3, 7.0)),
at(0.3, 7.0),
"the angle is left alone"
);
let (Some(p), Some(q)) = (
s.derivs(at(1.3, 0.4)[0], at(1.3, 0.4)[1]),
s.derivs(at(0.3, 0.4)[0], at(0.3, 0.4)[1]),
) else {
panic!("evaluates")
};
assert!(p.0.dist(q.0) < 1e-12, "swap {swap}: {p:?} {q:?}");
}
assert_eq!(
domain(&cylinder()).period,
[None, Some(std::f64::consts::TAU)]
);
}
#[test]
fn unwraps_across_the_period() {
let tau = std::f64::consts::TAU;
assert!((unwrap(0.1, tau - 0.1, Some(tau)) - (tau + 0.1)).abs() < 1e-12);
assert_eq!(unwrap(0.1, 6.0, None), 0.1);
}
}
-1
View File
@@ -19,7 +19,6 @@
//! Library code never panics, unwraps or indexes on input-derived data: a face the planner
//! cannot mesh is named in its [`PlanStats`], and the rest of the body is meshed.
mod fast;
mod mesh;
mod par;
mod plan;
+119 -58
View File
@@ -5,12 +5,14 @@
//! the facet angle, then Ruppert refinement for shape. Constraint edges are never split, so the
//! boundary stays exactly the shared edge nodes.
use crate::vec::VecExt as _;
use std::cmp::Ordering;
use std::collections::HashMap;
use crate::vec::VecExt as _;
use solidcore_record::Vec3;
use super::size::Turn;
use super::{NoValue, UvMesh};
use spade::handles::FixedVertexHandle;
use spade::{
AngleLimit, ConstrainedDelaunayTriangulation, Point2, PositionInTriangulation,
@@ -33,14 +35,15 @@ const SAG_ROUNDS: usize = 16;
/// The surface, for refining where the triangles leave it.
pub(crate) struct Sag<'a> {
/// Point and unit normal (when not degenerate) at (u, v).
pub eval: &'a dyn Fn(f64, f64) -> Option<(Vec3, Option<Vec3>)>,
/// Point and unit normal (when not degenerate) at (u, v); the error where the surface does
/// not evaluate.
pub eval: &'a dyn Fn(f64, f64) -> Result<(Vec3, Option<Vec3>), NoValue>,
pub tol: f64,
/// Longest triangle side (mm).
pub h: f64,
/// The facet angle, if any: corner normals turning more get a point in the middle.
pub turn: Option<Turn>,
/// Scale of u and v in the triangulation (the surface's mean |S_u|, |S_v|).
/// Scale of u and v in the triangulation (the surface's mean `|S_u|`, `|S_v|`).
pub scale: [f64; 2],
}
@@ -62,16 +65,32 @@ pub(crate) struct Fill<'a> {
/// Triangulate the region bounded by `loops` (ranges of `boundary`, each closed). Boundary
/// vertices keep their indices; added points follow. Triangles are counter-clockwise in
/// (u, v). `None` when a loop cannot be kept as constraints (it crosses itself or another).
/// (u, v). `Ok(None)` when a loop cannot be kept as constraints (it crosses itself or another);
/// the error where the sag refinement needs a point the surface does not evaluate at.
pub(crate) fn triangulate(
boundary: &[[f64; 2]],
loops: &[std::ops::Range<usize>],
fill: Fill,
) -> Option<(Vec<[f64; 2]>, Vec<[u32; 3]>)> {
) -> Result<Option<UvMesh>, NoValue> {
super::layout(|stopped| triangulate_in(boundary, loops, fill, stopped))
}
/// [`triangulate`]'s body: the sag refinement's error goes to `stopped`.
fn triangulate_in(
boundary: &[[f64; 2]],
loops: &[std::ops::Range<usize>],
Fill {
seeds,
refine,
shape,
lenient,
}: Fill,
stopped: &mut Option<NoValue>,
) -> Option<UvMesh> {
if boundary.iter().flatten().any(|x| !x.is_finite()) {
return None;
}
let scale = match &fill.refine {
let scale = match &refine {
Some((sag, _)) if sag.scale.iter().all(|k| k.is_finite() && *k > 0.0) => sag.scale,
_ => [1.0, 1.0],
};
@@ -82,6 +101,7 @@ pub(crate) fn triangulate(
let from = |p: Point2<f64>| [p.x / scale[0], p.y / scale[1]];
let mut cdt = Cdt::new();
let mut index: HashMap<FixedVertexHandle, u32> = HashMap::new();
// Proven: capacity of allocated `boundary`'s length, its elements larger than a handle
let mut handles: Vec<FixedVertexHandle> = Vec::with_capacity(boundary.len());
for (i, p) in boundary.iter().enumerate() {
let h = cdt.insert(to(p)).ok()?;
@@ -96,6 +116,7 @@ pub(crate) fn triangulate(
for k in 0..n {
sides.push((
*handles.get(r.start + k)?,
// Proven: n = r.len() > 0 inside `for k in 0..n`
*handles.get(r.start + (k + 1) % n)?,
));
}
@@ -106,13 +127,14 @@ pub(crate) fn triangulate(
for k in 0..n {
let (a, b) = (
*handles.get(r.start + k)?,
// Proven: n = r.len() > 0 inside `for k in 0..n`
*handles.get(r.start + (k + 1) % n)?,
);
if a == b || cdt.exists_constraint(a, b) {
continue;
}
if !cdt.can_add_constraint(a, b) {
if fill.lenient {
if lenient {
continue;
}
return None;
@@ -120,15 +142,15 @@ pub(crate) fn triangulate(
cdt.add_constraint(a, b);
}
}
for s in fill.seeds {
for s in seeds {
if s.iter().all(|x| x.is_finite()) {
let _ = cdt.insert(to(s));
}
}
if let Some((sag, budget)) = &fill.refine {
if let Some((sag, budget)) = &refine {
let constraints = cdt.num_constraints();
let added = refine_sag(&mut cdt, sag, *budget, &from, &slits);
if fill.shape {
let added = super::stop(stopped, refine_sag(&mut cdt, sag, *budget, &from, &slits))?;
if shape {
let params = RefinementParameters::<f64>::new()
.keep_constraint_edges()
.exclude_outer_faces(true)
@@ -140,13 +162,16 @@ pub(crate) fn triangulate(
// Shaping flips sides and adds points of its own; the triangles it leaves may
// sag again (a long strip round a cylinder), so the sag is checked once more.
if shaped > 0 {
refine_sag(
&mut cdt,
sag,
budget.saturating_sub(added + shaped),
&from,
&slits,
);
super::stop(
stopped,
refine_sag(
&mut cdt,
sag,
budget.saturating_sub(added + shaped),
&from,
&slits,
),
)?;
}
}
// A split constraint would put a point on the boundary the neighbour does not have.
@@ -164,18 +189,18 @@ pub(crate) fn triangulate(
let vs = face.vertices();
let mut t = [0u32; 3];
for (slot, v) in t.iter_mut().zip(vs) {
*slot = match index.get(&v.fix()) {
Some(&i) => i,
None => {
let i = u32::try_from(uv.len()).ok()?;
uv.push(from(v.position()));
index.insert(v.fix(), i);
i
}
*slot = if let Some(&i) = index.get(&v.fix()) {
i
} else {
let i = u32::try_from(uv.len()).ok()?;
uv.push(from(v.position()));
index.insert(v.fix(), i);
i
};
}
tris.push(t);
}
// Proven: `uv` starts as `boundary.to_vec()` and only grows
let extra = uv.split_off(boundary.len());
Some((extra, tris))
}
@@ -218,6 +243,7 @@ pub(crate) fn band(
for k in 0..len {
let (a, b) = (
u32::try_from(r.start + k).ok()?,
// Proven: len = r.len() > 0 inside `for k in 0..len`
u32::try_from(r.start + (k + 1) % len).ok()?,
);
if (a as usize) >= n || (b as usize) >= n {
@@ -260,31 +286,37 @@ pub(crate) fn band(
/// under the middle of every non-constraint side that sags more than the tolerance or is
/// longer than [`LONG`] times `h`, until none does or `budget` points were added. Returns the points added.
/// `slits`: constraint sides the loops run along both ways, inside the region on both sides.
/// A point the check needs and the surface does not evaluate at is the error.
fn refine_sag(
cdt: &mut Cdt,
sag: &Sag,
budget: usize,
from: &dyn Fn(Point2<f64>) -> [f64; 2],
slits: &std::collections::HashSet<(FixedVertexHandle, FixedVertexHandle)>,
) -> usize {
) -> Result<usize, NoValue> {
let mut added = 0;
let mut cache: HashMap<FixedVertexHandle, Option<(Vec3, Option<Vec3>)>> = HashMap::new();
let mut cache: HashMap<FixedVertexHandle, (Vec3, Option<Vec3>)> = HashMap::new();
// The widest turn between two corner normals.
let spread = |n: [Option<Vec3>; 3]| match n {
[Some(a), Some(b), Some(c)] => [(a, b), (b, c), (c, a)]
.iter()
// Proven: the clamp bounds are the constants -1.0 <= 1.0, neither NaN.
.map(|(x, y)| x.dot(*y).clamp(-1.0, 1.0).acos())
.fold(0.0, f64::max),
_ => 0.0,
};
for _ in 0..SAG_ROUNDS {
let mut todo: Vec<Point2<f64>> = Vec::new();
let mut at = |h: FixedVertexHandle, p: Point2<f64>| {
*cache.entry(h).or_insert_with(|| {
let mut at =
|h: FixedVertexHandle, p: Point2<f64>| -> Result<(Vec3, Option<Vec3>), NoValue> {
if let Some(&known) = cache.get(&h) {
return Ok(known);
}
let q = from(p);
(sag.eval)(q[0], q[1])
})
};
let known = (sag.eval)(q[0], q[1])?;
cache.insert(h, known);
Ok(known)
};
let region = region(cdt, slits);
for face in cdt.inner_faces() {
if !region.get(face.index()).copied().unwrap_or(false) {
@@ -296,20 +328,15 @@ fn refine_sag(
(ps[0].x + ps[1].x + ps[2].x) / 3.0,
(ps[0].y + ps[1].y + ps[2].y) / 3.0,
);
let (Some((a, na)), Some((b, nb)), Some((d, nd))) = (
at(vs[0].fix(), ps[0]),
at(vs[1].fix(), ps[1]),
at(vs[2].fix(), ps[2]),
) else {
continue;
};
let (a, na) = at(vs[0].fix(), ps[0])?;
let (b, nb) = at(vs[1].fix(), ps[1])?;
let (d, nd) = at(vs[2].fix(), ps[2])?;
// How far the surface lies from the triangle at `q`.
let gap = |q: Point2<f64>, chord: Vec3| {
let gap = |q: Point2<f64>, chord: Vec3| -> Result<f64, NoValue> {
let uv = from(q);
(sag.eval)(uv[0], uv[1]).map_or(0.0, |(s, _)| s.dist(chord))
Ok((sag.eval)(uv[0], uv[1])?.0.dist(chord))
};
let off = |q: Point2<f64>, chord: Vec3| gap(q, chord) > sag.tol;
let middle = gap(c, (a + b + d) * (1.0 / 3.0));
let middle = gap(c, (a + b + d) * (1.0 / 3.0))?;
if middle > sag.tol
|| sag
.turn
@@ -343,7 +370,7 @@ fn refine_sag(
}
let [p, q] = e.positions();
let m = Point2::new(0.5 * (p.x + q.x), 0.5 * (p.y + q.y));
if pa.dist(pb) > LONG * sag.h || off(m, (pa + pb) * 0.5) {
if pa.dist(pb) > LONG * sag.h || gap(m, (pa + pb) * 0.5)? > sag.tol {
todo.push(m);
}
}
@@ -355,7 +382,7 @@ fn refine_sag(
todo.dedup();
for p in todo {
if added >= budget {
return added;
return Ok(added);
}
// The boundary's nodes are its edges', shared with the neighbouring face: a point that
// lands on the boundary (a sliver's middle on a nearly straight stretch, on the chord to
@@ -370,7 +397,7 @@ fn refine_sag(
}
}
}
added
Ok(added)
}
/// The sides a boundary runs along an even number of times (a slit: out along a line and back),
@@ -391,6 +418,7 @@ fn slits(
}
}
runs.into_iter()
// Proven: the divisor is the constant 2, not zero.
.filter(|(_, n)| n % 2 == 0)
.map(|(e, _)| e)
.collect()
@@ -495,7 +523,10 @@ fn scan(
((bbox[1].1 - bbox[1].0) / row).ceil(),
((bbox[0].1 - bbox[0].0) / step[0]).ceil(),
);
if !(rows * cols <= MAX_SEEDS as f64) {
if !matches!(
(rows * cols).partial_cmp(&(MAX_SEEDS as f64)),
Some(Ordering::Less | Ordering::Equal)
) {
return false;
}
let near = Clearance::new(polys, scale, CLEARANCE * step[0] * scale[0]);
@@ -506,6 +537,7 @@ fn scan(
for poly in polys {
let n = poly.len();
for k in 0..n {
// Proven: n = poly.len() > 0 inside `for k in 0..n`
let (Some(a), Some(b)) = (poly.get(k), poly.get((k + 1) % n)) else {
continue;
};
@@ -515,10 +547,12 @@ fn scan(
}
}
xs.sort_by(f64::total_cmp);
// Proven: the divisor is the constant 2, not zero.
let shift = if j % 2 == 1 { 0.5 * step[0] } else { 0.0 };
for i in 0..=cols as usize {
let u = bbox[0].0 + shift + step[0] * i as f64;
if u < bbox[0].1
// Proven: the divisor is the constant 2, not zero.
&& xs.partition_point(|x| *x < u) % 2 == 1
&& !near.within([u, v])
&& !visit([u, v])
@@ -530,20 +564,45 @@ fn scan(
true
}
/// Whether `q` lies inside the region the loops `polys` bound, by the even–odd rule [`scan`]
/// keeps lattice seeds by: an odd number of loop sides cross the line through `q` along u
/// before it.
pub(crate) fn inside(polys: &[&[[f64; 2]]], q: [f64; 2]) -> bool {
let [u, v] = q;
let mut before = 0usize;
for poly in polys {
let n = poly.len();
for k in 0..n {
// Proven: n = poly.len() > 0 inside `for k in 0..n`
let (Some(a), Some(b)) = (poly.get(k), poly.get((k + 1) % n)) else {
continue;
};
if (a[1] > v) != (b[1] > v) && a[0] + (v - a[1]) * (b[0] - a[0]) / (b[1] - a[1]) < u {
before += 1;
}
}
}
// Proven: the divisor is the constant 2, not zero.
before % 2 == 1
}
/// Most lattice seeds of one face.
const MAX_SEEDS: usize = 200_000;
/// A boundary segment's two ends, in the scaled parameters.
type ScaledSide = ([f64; 2], [f64; 2]);
/// Boundary segments bucketed on a square grid (cell = the clearance), for "is this point
/// within the clearance of the boundary" in constant time per point.
struct Clearance {
cells: HashMap<(i64, i64), Vec<([f64; 2], [f64; 2])>>,
cells: HashMap<(i64, i64), Vec<ScaledSide>>,
scale: [f64; 2],
r: f64,
}
impl Clearance {
fn new(polys: &[&[[f64; 2]]], scale: [f64; 2], r: f64) -> Clearance {
let mut cells: HashMap<(i64, i64), Vec<([f64; 2], [f64; 2])>> = HashMap::new();
let mut cells: HashMap<(i64, i64), Vec<ScaledSide>> = HashMap::new();
if !(r.is_finite() && r > 0.0) {
return Clearance {
cells,
@@ -554,6 +613,7 @@ impl Clearance {
for poly in polys {
let n = poly.len();
for k in 0..n {
// Proven: n = poly.len() > 0 inside `for k in 0..n`
let (Some(a), Some(b)) = (poly.get(k), poly.get((k + 1) % n)) else {
continue;
};
@@ -609,6 +669,7 @@ fn dist_to_segment(p: [f64; 2], a: [f64; 2], b: [f64; 2]) -> f64 {
let (dx, dy) = (b[0] - a[0], b[1] - a[1]);
let l2 = dx * dx + dy * dy;
let t = if l2 > 0.0 {
// Proven: the clamp bounds are the constants 0.0 <= 1.0, neither NaN.
(((p[0] - a[0]) * dx + (p[1] - a[1]) * dy) / l2).clamp(0.0, 1.0)
} else {
0.0
@@ -634,7 +695,7 @@ mod tests {
let mut b = vec![[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]];
b.extend([[0.4, 0.4], [0.4, 0.6], [0.6, 0.6], [0.6, 0.4]]);
let loops = [0..4, 4..8];
let flat = |u: f64, v: f64| Some((Vec3::new(u, v, 0.0), Some(Vec3::Z)));
let flat = |u: f64, v: f64| Ok((Vec3::new(u, v, 0.0), Some(Vec3::Z)));
for refine in [false, true] {
let refine = refine.then_some((
Sag {
@@ -647,7 +708,7 @@ mod tests {
2000,
));
let label = refine.is_some();
let Some((extra, tris)) = triangulate(
let Ok(Some((extra, tris))) = triangulate(
&b,
&loops,
Fill {
@@ -686,7 +747,7 @@ mod tests {
fn crossing_loops_are_refused_unless_lenient() {
let b = [[0.0, 0.0], [1.0, 1.0], [1.0, 0.0], [0.0, 1.0]];
let whole = std::slice::from_ref(&(0..4));
assert!(
assert_eq!(
triangulate(
&b,
whole,
@@ -696,11 +757,11 @@ mod tests {
shape: false,
lenient: false
}
)
.is_none()
),
Ok(None)
);
// Lenient, the crossing side is left out and what the rest encloses is filled.
let Some((_, tris)) = triangulate(
let Ok(Some((_, tris))) = triangulate(
&b,
whole,
Fill {
@@ -752,7 +813,7 @@ mod tests {
// A parameter that is zero in exact arithmetic, left at 1e-300 by projection.
let b = [[1e-300, 0.0], [1.0, -1e-310], [1.0, 1.0], [0.0, 1.0]];
let whole = std::slice::from_ref(&(0..4));
let Some((extra, tris)) = triangulate(
let Ok(Some((extra, tris))) = triangulate(
&b,
whole,
Fill {
+24 -11
View File
@@ -30,6 +30,7 @@ pub(crate) fn segments(
.filter_map(|k| {
let (a, b) = (
metric(*uvs.get(r.start + k)?),
// Proven: m > 0: `k` ranges over `0..m`
metric(*uvs.get(r.start + (k + 1) % m)?),
);
Some(0.5 * (a[0] * b[1] - b[0] * a[1]))
@@ -53,9 +54,9 @@ pub(crate) fn segments(
let (mut lo, mut hi) = ([f64::MAX; 2], [f64::MIN; 2]);
for k in r.clone() {
let q = metric(*uvs.get(k)?);
for d in 0..2 {
lo[d] = lo[d].min(q[d]);
hi[d] = hi[d].max(q[d]);
for ((l, h), x) in lo.iter_mut().zip(&mut hi).zip(q) {
*l = l.min(x);
*h = h.max(x);
}
}
Some((lo, hi))
@@ -88,14 +89,16 @@ pub(crate) fn segments(
};
// The arc of loop `r` (in nodes) between its support points `a` and `b`, the way that passes
// neither of the other two.
// Proven: divisor is the constant 2
let side_count = |n: usize| if flat { n.div_ceil(2).max(1) } else { n.max(1) };
let arc = |r: &std::ops::Range<usize>, p: &[usize; 4], k: usize| -> Option<usize> {
let m = r.len();
let (a, b) = (*p.get(k)? - r.start, *p.get((k + 1) % 4)? - r.start);
let fwd = (b + m - a) % m;
let fwd = (b + m - a).checked_rem(m)?;
let others: Vec<usize> = (0..4)
// Proven: the divisor is the constant 4, not zero.
.filter(|&j| j != k && j != (k + 1) % 4)
.filter_map(|j| p.get(j).map(|x| (x - r.start + m - a) % m))
.filter_map(|j| p.get(j).and_then(|x| (x - r.start + m - a).checked_rem(m)))
.collect();
Some(if others.iter().all(|&o| o > fwd) {
fwd
@@ -133,10 +136,12 @@ pub(crate) fn segments(
centre[1] + 0.5 * (q[1] - centre[1]),
]);
}
// Proven: the capacity is the constant 8.
let mut segs = Vec::with_capacity(8);
for k in 0..4 {
segs.push(Segment {
a: End::Point(*corners.get(k)?),
// Proven: the divisor is the constant 4, not zero.
b: End::Point(*corners.get((k + 1) % 4)?),
count: Some(side_count(arc(outer, &p, k)?)),
});
@@ -189,6 +194,7 @@ pub(crate) fn segments(
let (ab, ap) = ([b[0] - a[0], b[1] - a[1]], [p[0] - a[0], p[1] - a[1]]);
let len2 = ab[0] * ab[0] + ab[1] * ab[1];
let t = if len2 > 0.0 {
// Proven: the clamp bounds are the constants 0.0 <= 1.0, neither NaN.
((ap[0] * ab[0] + ap[1] * ab[1]) / len2).clamp(0.0, 1.0)
} else {
0.0
@@ -206,12 +212,14 @@ pub(crate) fn segments(
// Each square: the hole's box grown by half its clearance to the outer loop and a quarter of
// its gap to another hole's box (both squares grow into that gap), so squares never touch.
let m = outer.len();
// Proven: with_capacity of the length of `boxes`, allocated with the same element type
let mut squares: Vec<([f64; 2], [f64; 2])> = Vec::with_capacity(boxes.len());
for (i, &bx) in boxes.iter().enumerate() {
let mut clear = f64::INFINITY;
for k in 0..m {
let (a, b) = (
metric(*uvs.get(outer.start + k)?),
// Proven: m = outer.len() > 0 inside `for k in 0..m`
metric(*uvs.get(outer.start + (k + 1) % m)?),
);
clear = clear.min(to_box(a, bx));
@@ -248,6 +256,7 @@ pub(crate) fn segments(
for k in 0..4 {
segs.push(Segment {
a: End::Point(*c.get(k)?),
// Proven: the divisor is the constant 4, not zero.
b: End::Point(*c.get((k + 1) % 4)?),
count: Some(side_count(arc(hole, p, k)?)),
});
@@ -299,12 +308,16 @@ pub(crate) fn segments(
End::Node(u32::try_from(outer.start + k).ok()?)
}
Stop::Boundary(a, b) => {
// A side of a square: corners `a` and `b` of square `s`.
let (s, la, lb) = (
(a - outer.len()) / 4,
(a - outer.len()) % 4,
(b - outer.len()) % 4,
);
// A side of a square: corners `a` and `b` of square `s`. A trace stops on a
// segment of one loop, so neither end is on the outer loop here; checked, so a
// segment from the outer loop to a square refuses the layout instead.
let (Some(ia), Some(ib)) = (a.checked_sub(outer.len()), b.checked_sub(outer.len()))
else {
return None;
};
// Proven: the divisor 4 is a constant (the corners of a square).
let (s, la, lb) = (ia / 4, ia % 4, ib % 4);
// Proven: the divisor 4 is a constant.
let k = if (la + 1) % 4 == lb { la } else { lb };
End::On(4 * s + k, t.b)
}
+114 -43
View File
@@ -4,9 +4,17 @@
//! A blend that folds (a strongly non-convex boundary) is refused, and the face goes to the
//! constrained Delaunay path.
use std::cmp::Ordering;
use crate::vec::VecExt as _;
use solidcore_record::Vec3;
use super::UvMesh;
/// Whether the surface between two (u, v) points is fine enough as it is; the error is a point
/// the surface does not evaluate at.
pub(crate) type Fine<'a, E> = dyn Fn([f64; 2], [f64; 2]) -> Result<bool, E> + 'a;
/// Smallest turn (radians) of the boundary at a corner, in the tolerance metric.
const CORNER_TURN: f64 = 0.6;
@@ -35,6 +43,7 @@ fn candidates(uv: &[[f64; 2]], scale: [f64; 2]) -> Option<Vec<usize>> {
if n < 4 {
return None;
}
// Proven: n = uv.len() >= 4, checked above.
let p = |i: usize| uv.get(i % n).map(|q| [q[0] * scale[0], q[1] * scale[1]]);
let mut found = Vec::new();
for i in 0..n {
@@ -72,19 +81,20 @@ pub(crate) fn coons_best(
scale: [f64; 2],
max_points: usize,
h: f64,
) -> Option<(Vec<[f64; 2]>, Vec<[u32; 3]>)> {
) -> Option<UvMesh> {
let c = candidates(uv, scale)?;
if c.len() < 4 || c.len() > MAX_CANDIDATES {
return None;
}
let mut best: Option<(f64, (Vec<[f64; 2]>, Vec<[u32; 3]>))> = None;
let mut best: Option<(f64, UvMesh)> = None;
let k = c.len();
for a in 0..k {
for b in a + 1..k {
for d in b + 1..k {
for e in d + 1..k {
let pick = [*c.get(a)?, *c.get(b)?, *c.get(d)?, *c.get(e)?];
let Some(r) = coons_at(uv, fixed, pick, max_points, h, None) else {
let Some(r) = super::infallible(coons_at(uv, fixed, pick, max_points, h, None))
else {
continue;
};
let q = min_angle(uv, &r, scale);
@@ -99,7 +109,7 @@ pub(crate) fn coons_best(
}
/// Smallest corner angle (radians) of the triangles, in the metric.
fn min_angle(uv: &[[f64; 2]], r: &(Vec<[f64; 2]>, Vec<[u32; 3]>), scale: [f64; 2]) -> f64 {
fn min_angle(uv: &[[f64; 2]], r: &UvMesh, scale: [f64; 2]) -> f64 {
let at = |i: u32| {
let i = i as usize;
let q = if i < uv.len() {
@@ -126,18 +136,20 @@ fn min_angle(uv: &[[f64; 2]], r: &(Vec<[f64; 2]>, Vec<[u32; 3]>), scale: [f64; 2
}
fn side(fixed: &[Vec3], n: usize, from: usize, to: usize) -> Option<Side> {
let len = (to + n - from) % n;
let len = (to + n - from).checked_rem(n)?;
if len == 0 {
return None;
}
// Proven: n > 0, or `checked_rem(n)` above returned None; each index is below n, a count of
// a face's boundary nodes, at most MAX_FACE_NODES (checked by `plan_face`), so fits a u32.
let nodes: Vec<u32> = (0..=len).map(|k| ((from + k) % n) as u32).collect();
let mut at = vec![0.0];
let mut total = 0.0;
for w in nodes.windows(2) {
total += fixed.get(w[0] as usize)?.dist(*fixed.get(w[1] as usize)?);
for (a, b) in nodes.iter().zip(nodes.iter().skip(1)) {
total += fixed.get(*a as usize)?.dist(*fixed.get(*b as usize)?);
at.push(total);
}
if !(total > 0.0) {
if total.partial_cmp(&0.0) != Some(Ordering::Greater) {
// A pole side: evenly along it.
let m = (nodes.len() - 1).max(1) as f64;
return Some(Side {
@@ -158,6 +170,7 @@ fn along(side: &Side, uv: &[[f64; 2]], s: f64) -> Option<[f64; 2]> {
let k = side
.at
.partition_point(|x| *x < s)
// Proven: the upper bound is at least the lower bound 1, by `.max(1)`.
.clamp(1, side.at.len().saturating_sub(1).max(1));
let (a0, a1) = (*side.at.get(k - 1)?, *side.at.get(k)?);
let (p, q) = (
@@ -165,6 +178,7 @@ fn along(side: &Side, uv: &[[f64; 2]], s: f64) -> Option<[f64; 2]> {
uv.get(*side.nodes.get(k)? as usize)?,
);
let f = if a1 > a0 {
// Proven: the clamp bounds are the constants 0.0 <= 1.0, neither NaN.
((s - a0) / (a1 - a0)).clamp(0.0, 1.0)
} else {
0.0
@@ -200,6 +214,7 @@ fn lines(a: &Side, b: &Side, h: f64) -> Vec<f64> {
};
let size = if size.is_finite() { size as usize } else { 1 };
let outer = a.at.len().max(b.at.len()).saturating_sub(1);
// Proven: clamp bounds are constants 2 <= MAX_LEVEL = 4096.
let n = outer.max(size).clamp(2, MAX_LEVEL);
(1..n).map(|i| i as f64 / n as f64).collect()
}
@@ -211,27 +226,44 @@ fn lines(a: &Side, b: &Side, h: f64) -> Vec<f64> {
/// Given for a whole face, whose sides are its edges' nodes: where its grid converges (towards a
/// pole) the interior can need more lines than the sides carry, and the lines are split there.
/// A motorcycle patch passes `None`: its sides are cuts seeded at the face's global seed.
pub(crate) fn coons(
/// `fine`'s error (a point the surface does not evaluate at) is the error; `Ok(None)` when the
/// loop takes no Coons patch.
pub(crate) fn coons<E>(
uv: &[[f64; 2]],
fixed: &[Vec3],
scale: [f64; 2],
max_points: usize,
h: f64,
fine: Option<&dyn Fn([f64; 2], [f64; 2]) -> bool>,
) -> Option<(Vec<[f64; 2]>, Vec<[u32; 3]>)> {
let pick = corners(uv, scale)?;
fine: Option<&Fine<'_, E>>,
) -> Result<Option<UvMesh>, E> {
let Some(pick) = corners(uv, scale) else {
return Ok(None);
};
coons_at(uv, fixed, pick, max_points, h, fine)
}
/// [`coons`] with the four corners given (loop indices, in loop order).
pub(crate) fn coons_at(
pub(crate) fn coons_at<E>(
uv: &[[f64; 2]],
fixed: &[Vec3],
pick: [usize; 4],
max_points: usize,
h: f64,
fine: Option<&dyn Fn([f64; 2], [f64; 2]) -> bool>,
) -> Option<(Vec<[f64; 2]>, Vec<[u32; 3]>)> {
fine: Option<&Fine<'_, E>>,
) -> Result<Option<UvMesh>, E> {
super::layout(|stopped| coons_in(uv, fixed, pick, max_points, h, fine, stopped))
}
/// [`coons_at`]'s body: `fine`'s error goes to `stopped`.
fn coons_in<E>(
uv: &[[f64; 2]],
fixed: &[Vec3],
pick: [usize; 4],
max_points: usize,
h: f64,
fine: Option<&Fine<'_, E>>,
stopped: &mut Option<E>,
) -> Option<UvMesh> {
let n = uv.len();
let [c0, c1, c2, c3] = pick;
// Sides in loop order; the bottom (0) and right (1) run with the loop, the top (2) and
@@ -250,24 +282,34 @@ pub(crate) fn coons_at(
along(&left, uv, t)?,
along(&right, uv, t)?,
);
let blend = |d: usize| {
(1.0 - t) * b[d] + t * tp[d] + (1.0 - s) * l[d] + s * r[d]
- ((1.0 - s) * (1.0 - t) * p00[d]
+ s * (1.0 - t) * p10[d]
+ (1.0 - s) * t * p01[d]
+ s * t * p11[d])
// `c` picks the coordinate (u or v).
let blend = |c: fn([f64; 2]) -> f64| {
(1.0 - t) * c(b) + t * c(tp) + (1.0 - s) * c(l) + s * c(r)
- ((1.0 - s) * (1.0 - t) * c(p00)
+ s * (1.0 - t) * c(p10)
+ (1.0 - s) * t * c(p01)
+ s * t * c(p11))
};
Some([blend(0), blend(1)])
Some([blend(|p| p[0]), blend(|p| p[1])])
};
// The lines come from the sides' seeds (see [`lines`]): every side is seeded at the face's
// global seed, which the size rules set for the whole surface, so the interior needs no finer
// spacing than its sides.
let (ss, ts) = (lines(&bottom, &top, h), lines(&left, &right, h));
let (ss, ts) = match fine {
Some(fine) => (
refine(ss, max_points, &|a, b, x| Some(fine(at(a, x)?, at(b, x)?))),
refine(ts, max_points, &|a, b, x| Some(fine(at(x, a)?, at(x, b)?))),
),
Some(fine) => {
// `at` is `None` only for a malformed side (an index past its nodes), whose patch
// `coons_in` refuses below; it is not the surface, so it checks nothing here.
let ss = refine(ss, max_points, &|a, b, x| match (at(a, x), at(b, x)) {
(Some(p), Some(q)) => fine(p, q),
_ => Ok(true),
});
let ts = refine(ts, max_points, &|a, b, x| match (at(x, a), at(x, b)) {
(Some(p), Some(q)) => fine(p, q),
_ => Ok(true),
});
(super::stop(stopped, ss)?, super::stop(stopped, ts)?)
}
None => (ss, ts),
};
if ss.len().checked_mul(ts.len())? > max_points {
@@ -275,12 +317,15 @@ pub(crate) fn coons_at(
}
let mut out: Vec<[f64; 2]> = uv.to_vec();
let base = u32::try_from(n).ok()?;
// Every node index below is under n + ns · nt, checked to fit a u32.
u32::try_from(n.checked_add(ss.len() * ts.len())?).ok()?;
for &t in &ts {
for &s in &ss {
out.push(at(s, t)?);
}
}
let (ns, nt) = (ss.len(), ts.len());
// Proven: i < ns and j < nt, so base + j · ns + i < n + ns · nt, which fits a u32 (checked).
let g = |i: usize, j: usize| base + (j * ns + i) as u32;
let mut tris = Vec::new();
for j in 0..nt.saturating_sub(1) {
@@ -361,6 +406,7 @@ pub(crate) fn coons_at(
}
})
.collect();
// Proven: k = at_t.len(), already allocated with larger (f64) elements.
let mut mid: Vec<u32> = Vec::with_capacity(k);
for &x in &at_t {
let [sx, tx] = place(x);
@@ -402,33 +448,52 @@ pub(crate) fn coons_at(
let first = area(tris.first()?)?;
let sign = if first > 0.0 { 1.0 } else { -1.0 };
for t in &tris {
if !(area(t)? * sign > 0.0) {
if (area(t)? * sign).partial_cmp(&0.0) != Some(Ordering::Greater) {
return None;
}
}
if sign < 0.0 {
for t in &mut tris {
t.swap(1, 2);
let [a, b, c] = *t;
*t = [a, c, b];
}
}
// Proven: `out` starts as `uv.to_vec()` (n = uv.len()) and only grows.
let extra = out.split_off(n);
Some((extra, tris))
}
/// Interior lines (strictly between 0 and 1) with every interval, ends included, split while
/// `fine(a, b, x)` fails at some position `x` across (or the lines reach `max`).
fn refine(lines: Vec<f64>, max: usize, fine: &dyn Fn(f64, f64, f64) -> Option<bool>) -> Vec<f64> {
/// `fine(a, b, x)` fails at some position `x` across (or the lines reach `max`); `fine`'s error
/// ends it.
fn refine<E>(
lines: Vec<f64>,
max: usize,
fine: &dyn Fn(f64, f64, f64) -> Result<bool, E>,
) -> Result<Vec<f64>, E> {
const ACROSS: [f64; 5] = [0.0, 0.25, 0.5, 0.75, 1.0];
let all: Vec<f64> = std::iter::once(0.0)
.chain(lines)
.chain(std::iter::once(1.0))
.collect();
let mut out = vec![0.0];
let mut stack: Vec<(f64, f64, u32)> = all.windows(2).rev().map(|w| (w[0], w[1], 0)).collect();
let mut stack: Vec<(f64, f64, u32)> = all
.iter()
.zip(all.iter().skip(1))
.rev()
.map(|(&a, &b)| (a, b, 0))
.collect();
while let Some((a, b, depth)) = stack.pop() {
let ok = depth >= 16
|| out.len() + stack.len() >= max
|| ACROSS.iter().all(|x| fine(a, b, *x).unwrap_or(true));
let mut ok = depth >= 16 || out.len() + stack.len() >= max;
if !ok {
ok = true;
for x in ACROSS {
if !fine(a, b, x)? {
ok = false;
break;
}
}
}
if ok {
out.push(b);
} else {
@@ -437,7 +502,7 @@ fn refine(lines: Vec<f64>, max: usize, fine: &dyn Fn(f64, f64, f64) -> Option<bo
stack.push((a, m, depth + 1));
}
}
out.into_iter().filter(|x| *x > 0.0 && *x < 1.0).collect()
Ok(out.into_iter().filter(|x| *x > 0.0 && *x < 1.0).collect())
}
fn reversed(s: Side) -> Side {
@@ -514,7 +579,9 @@ mod tests {
b.reverse();
}
let fixed: Vec<Vec3> = b.iter().map(|p| Vec3::new(p[0], p[1], 0.0)).collect();
let Some((extra, tris)) = coons(&b, &fixed, [1.0, 1.0], 10_000, 1.0, None) else {
let Some((extra, tris)) =
super::super::infallible(coons(&b, &fixed, [1.0, 1.0], 10_000, 1.0, None))
else {
panic!("meshes (k {k})")
};
let mut uv = b.clone();
@@ -535,12 +602,14 @@ mod tests {
#[test]
fn the_interior_level_is_the_larger_sides_and_the_sparser_side_steps_down() {
// A 4 × 4 square: bottom in 20 segments, the other sides in 4.
let mut b: Vec<[f64; 2]> = (0..20).map(|k| [k as f64 * 0.2, 0.0]).collect();
b.extend((0..4).map(|k| [4.0, k as f64]));
b.extend((0..4).map(|k| [4.0 - k as f64, 4.0]));
b.extend((0..4).map(|k| [0.0, 4.0 - k as f64]));
let mut b: Vec<[f64; 2]> = (0..20).map(|k| [f64::from(k) * 0.2, 0.0]).collect();
b.extend((0..4).map(|k| [4.0, f64::from(k)]));
b.extend((0..4).map(|k| [4.0 - f64::from(k), 4.0]));
b.extend((0..4).map(|k| [0.0, 4.0 - f64::from(k)]));
let fixed: Vec<Vec3> = b.iter().map(|p| Vec3::new(p[0], p[1], 0.0)).collect();
let Some((extra, tris)) = coons(&b, &fixed, [1.0, 1.0], 10_000, 1.0, None) else {
let Some((extra, tris)) =
super::super::infallible(coons(&b, &fixed, [1.0, 1.0], 10_000, 1.0, None))
else {
panic!("meshes")
};
let mut uv = b.clone();
@@ -573,7 +642,7 @@ mod tests {
fn five_corners_is_not_a_coons_patch() {
let b = [[0.0, 0.0], [1.0, 0.0], [1.0, 0.5], [0.5, 1.0], [0.0, 1.0]];
let fixed: Vec<Vec3> = b.iter().map(|p| Vec3::new(p[0], p[1], 0.0)).collect();
assert!(coons(&b, &fixed, [1.0, 1.0], 100, 1.0, None).is_none());
assert!(super::super::infallible(coons(&b, &fixed, [1.0, 1.0], 100, 1.0, None)).is_none());
}
/// A planar strip with a fillet at one end and a chamfer at the other, in mm (a face of
@@ -665,7 +734,9 @@ mod tests {
.map(|k| 0.5 * (b[k][0] * b[(k + 1) % n][1] - b[(k + 1) % n][0] * b[k][1]))
.sum();
for h in [0.25, 0.5, 0.75, 1.0, 1.5, 2.0, 3.0, 4.0, 6.0, 8.0] {
let Some((extra, tris)) = coons(&b, &fixed, [1.0, 1.0], 100_000, h, None) else {
let Some((extra, tris)) =
super::super::infallible(coons(&b, &fixed, [1.0, 1.0], 100_000, h, None))
else {
continue;
};
let mut uv = b.clone();
+48 -12
View File
@@ -5,10 +5,12 @@
//! added. The boundary stays exactly the shared edge nodes, so the face meets its neighbours
//! exactly, and only the band next to the boundary is irregular (cut-cell meshing).
use std::cmp::Ordering;
use crate::ring::Ring;
use super::cdt;
use super::motorcycle::Trace;
use super::{UvMesh, cdt};
/// A cell is kept when the boundary stays at least this fraction of its smaller side away from
/// it (in the metric): closer, the band triangle between them would be a sliver.
@@ -30,6 +32,7 @@ struct Lines {
fn lines(grid: &[f64], traces: &[f64]) -> Option<(Lines, Vec<usize>)> {
let mut at: Vec<f64> = grid.to_vec();
let mut full = vec![true; at.len()];
// Proven: capacity of allocated `traces`' length, elements of the same size
let mut place = Vec::with_capacity(traces.len());
for &c in traces {
// The full-grid cell around c, and every line inside it.
@@ -48,7 +51,7 @@ fn lines(grid: &[f64], traces: &[f64]) -> Option<(Lines, Vec<usize>)> {
match near {
// An earlier trace, or a box edge: join it.
Some((i, _)) if !full.get(i).copied().unwrap_or(false) || i == 0 || i == last => {
place.push(i)
place.push(i);
}
// An inner grid line: it moves onto the trace.
Some((i, _)) => {
@@ -98,7 +101,7 @@ pub(crate) fn cut_grid(
vs: &[f64],
traces: &[Trace],
scale: [f64; 2],
) -> Option<(Vec<[f64; 2]>, Vec<[u32; 3]>)> {
) -> Option<UvMesh> {
if us.len() < 2 || vs.len() < 2 || !scale.iter().all(|s| s.is_finite() && *s > 0.0) {
return None;
}
@@ -118,6 +121,7 @@ pub(crate) fn cut_grid(
if cells(&us, &vs) <= super::grid::MAX_FACE_GRID || (us.len() <= 2 && vs.len() <= 2) {
break;
}
// Proven: the divisor is the constant 10, not zero.
let cap = us.len().saturating_mul(vs.len()).saturating_mul(9) / 10;
super::grid::thin(&mut us, &mut vs, cap);
}
@@ -154,8 +158,8 @@ pub(crate) fn cut_grid(
// rectangles; anything else (a trace ending in the open after snapping) falls back to
// every line running the whole face.
let rects = rectangles(cu, cv, &vert, &horiz).or_else(|| {
vert.iter_mut().for_each(|s| *s = true);
horiz.iter_mut().for_each(|s| *s = true);
vert.fill(true);
horiz.fill(true);
rectangles(cu, cv, &vert, &horiz)
})?;
// In the metric, so clearances are lengths on the surface.
@@ -168,6 +172,7 @@ pub(crate) fn cut_grid(
(0..n).filter_map(move |k| {
Some((
*boundary.get(r.start + k)?,
// Proven: n = r.len() > 0: `k` ranges over `0..n`
*boundary.get(r.start + (k + 1) % n)?,
))
})
@@ -245,7 +250,9 @@ pub(crate) fn cut_grid(
.iter()
.filter(|((i, j), _)| used.get(j * nu + i).copied().unwrap_or(false))
.count();
// Proven: corners <= ring.len(), allocated with larger elements
let mut poly: Vec<u32> = Vec::with_capacity(corners);
// Proven: corners <= ring.len(), allocated with larger elements
let mut across: Vec<(isize, isize)> = Vec::with_capacity(corners);
for &((i, j), c) in &ring {
if used.get(j * nu + i).copied().unwrap_or(false) {
@@ -288,7 +295,7 @@ pub(crate) fn cut_grid(
all.get(t[2] as usize)?,
);
let area = (b[0] - a[0]) * (c[1] - a[1]) - (c[0] - a[0]) * (b[1] - a[1]);
if !(area > 0.0) {
if area.partial_cmp(&0.0) != Some(Ordering::Greater) {
return None;
}
}
@@ -430,6 +437,7 @@ fn clear_cells(
return None;
};
let xm = 0.5 * (x0 + x1);
// Proven: the divisor is the constant 2, not zero.
let inside = cross.partition_point(|c| *c < xm) % 2 == 1;
if near.get(k).copied().unwrap_or(true) || !inside {
*keep.get_mut(*rects.of.get(k)?)? = false;
@@ -442,9 +450,9 @@ fn clear_cells(
/// Whether the segment `a`–`b` meets the closed box `lo`–`hi` (Liang–Barsky clipping).
fn segment_meets_box(a: [f64; 2], b: [f64; 2], lo: [f64; 2], hi: [f64; 2]) -> bool {
let (mut t0, mut t1) = (0.0f64, 1.0f64);
for d in 0..2 {
let delta = b[d] - a[d];
for (p, q) in [(-delta, a[d] - lo[d]), (delta, hi[d] - a[d])] {
for (((a, b), lo), hi) in a.into_iter().zip(b).zip(lo).zip(hi) {
let delta = b - a;
for (p, q) in [(-delta, a - lo), (delta, hi - a)] {
if p == 0.0 {
if q < 0.0 {
return false;
@@ -556,6 +564,34 @@ mod tests {
assert!(out.1.len() > 100, "{}", out.1.len());
}
/// A square with a hole and lines wanting more cells than a grid may have (1000 × 1000,
/// past [`super::super::grid::MAX_FACE_GRID`]): the cut grid thins its lines until the cells
/// fit, and still meshes the face (it gave the face up, for an under-refined Delaunay: the
/// halves of a split coil measured 1.5% short of the whole).
#[test]
fn lines_past_the_grid_cap_are_thinned_to_fit() {
let mut b = vec![[0.0, 0.0], [4.0, 0.0], [4.0, 4.0], [0.0, 4.0]];
b.extend(circle([2.0, 2.0], 1.0, 32, false));
let loops = [0..4, 4..36];
let hole = 0.5 * 32.0 * (std::f64::consts::TAU / 32.0).sin();
let Some(out) = cut_grid(
&b,
&loops,
&lines(0.0, 4.0, 1000),
&lines(0.0, 4.0, 1000),
&[],
[1.0, 1.0],
) else {
panic!("meshes")
};
check(&b, &loops, 16.0 - hole, &out);
assert!(
out.0.len() <= super::super::grid::MAX_FACE_GRID,
"{} points",
out.0.len()
);
}
#[test]
fn a_square_with_a_hole() {
let mut b = vec![[0.0, 0.0], [4.0, 0.0], [4.0, 4.0], [0.0, 4.0]];
@@ -578,12 +614,12 @@ mod tests {
#[test]
fn a_curved_triangle_in_a_stretched_metric() {
// A three-cornered patch (as a vertex blend in its parameters), u twice as long as v.
let mut b: Vec<[f64; 2]> = (0..=8).map(|k| [k as f64 / 8.0, 0.0]).collect();
let mut b: Vec<[f64; 2]> = (0..=8).map(|k| [f64::from(k) / 8.0, 0.0]).collect();
b.extend((1..8).map(|k| {
let a = std::f64::consts::FRAC_PI_2 * k as f64 / 8.0;
let a = std::f64::consts::FRAC_PI_2 * f64::from(k) / 8.0;
[a.cos(), a.sin()]
}));
b.extend((1..8).rev().map(|k| [0.0, k as f64 / 8.0]));
b.extend((1..8).rev().map(|k| [0.0, f64::from(k) / 8.0]));
let n = b.len();
let loops = one(n);
let area: f64 = (0..n)
+442 -71
View File
@@ -1,6 +1,8 @@
//! Edge nodes: every edge of a body sampled once, so the faces on either side share the very
//! same boundary points (watertight by construction).
use std::cmp::Ordering;
use crate::vec::VecExt as _;
use solidcore::eval::CurveEval;
use solidcore_convert as kernel;
@@ -20,6 +22,8 @@ const MAX_DEPTH: u32 = 24;
pub(crate) const QUARTER_TURN: f64 = std::f64::consts::FRAC_PI_2 * (1.0 + 1e-9);
/// An edge's nodes, from its start vertex (interval min) to its end vertex (interval max).
/// None (the default) for an edge that could not be sampled: every face it bounds is refused.
#[derive(Default)]
pub(crate) struct Nodes {
pub points: Vec<Vec3>,
/// The curve's parameter at each node (the edge's interval, ends included): where each
@@ -29,6 +33,61 @@ pub(crate) struct Nodes {
/// within the chord tolerance (see [`snap`]): in a face that runs both edges such a node is
/// the other's point of the face, a pinch, not a crossing.
pub snapped: Vec<f64>,
/// Where [`sample_edge`] stopped at a bound with a segment still wanting a split: which
/// bound ([`crate::EdgeCap::Nodes`] where both were met), the farthest any of its chords lies
/// from the curve at their quarter points and middles (mm; a sampled lower bound, exact
/// where a chord's farthest point is its middle, as on a circle), and the most any chord
/// exceeds its rules (the largest of its sag over the tolerance, its length over the side
/// asked, its turn over a quarter turn and over the facet angle; 1 at the rule).
pub capped: Option<(crate::EdgeCap, f64, f64)>,
}
/// A segment of [`sample_edge`]'s bisection, measured: its parameters, end points, quarter
/// points and middle, depth, whether it is fine, its chord's sag, and by how much it exceeds the
/// rules it fails (0 when fine). Each segment carries its points, so every parameter is
/// evaluated once (an intersection curve's point is a Newton solve).
struct Seg {
a: f64,
b: f64,
pa: Vec3,
pb: Vec3,
q1: Vec3,
pm: Vec3,
q3: Vec3,
depth: u32,
ok: bool,
sag: f64,
excess: f64,
}
impl Seg {
/// Which of two segments is halved first: the one exceeding its rules more, then the one
/// earlier along the curve.
fn order(&self, other: &Seg) -> Ordering {
self.excess
.total_cmp(&other.excess)
.then_with(|| other.a.total_cmp(&self.a))
}
}
impl PartialEq for Seg {
fn eq(&self, other: &Self) -> bool {
self.order(other) == Ordering::Equal
}
}
impl Eq for Seg {}
impl PartialOrd for Seg {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Ord for Seg {
fn cmp(&self, other: &Self) -> Ordering {
self.order(other)
}
}
/// Nodes of edge `ei` of `b` from its start to its end: every chord within the tolerance of
@@ -59,13 +118,21 @@ pub(crate) fn sample_edge(b: &brep::Brep, ei: usize, rules: &Rules, h: f64) -> R
// 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
// forwards takes the next edge's first node (the vertex) — a crack.
let n = if h > 0.0 {
(front.dist(back) / h)
.ceil()
.clamp(1.0, (MAX_EDGE_NODES - 1) as f64) as usize
} else {
1
};
// A vertex not a number (or the two too far apart for a length) has no nodes between:
// the count below would round it to none.
let length = front.dist(back);
if !length.is_finite() {
return Err(fail(format!(
"its vertices {front:?} and {back:?} are {length} apart"
)));
}
let wanted = if h > 0.0 { (length / h).ceil() } else { 1.0 };
// Proven: clamp bounds are constants 1.0 <= 19 999.0, neither NaN
let n = wanted.clamp(1.0, (MAX_EDGE_NODES - 1) as f64) as usize;
// More segments than an edge may have: its sides are longer than asked (a line's chords
// lie on it, so none sags).
let capped = (wanted > (MAX_EDGE_NODES - 1) as f64)
.then(|| (crate::EdgeCap::Nodes, 0.0, length / n as f64 / h));
let mut pts: Vec<Vec3> = (0..=n)
.map(|i| front + (back - front) * (i as f64 / n as f64))
.collect();
@@ -80,6 +147,7 @@ pub(crate) fn sample_edge(b: &brep::Brep, ei: usize, rules: &Rules, h: f64) -> R
points: pts,
params,
snapped: Vec::new(),
capped,
});
}
let curve_value = kernel::curve(edge.parameters()).map_err(|e| fail(e.to_string()))?;
@@ -100,78 +168,101 @@ pub(crate) fn sample_edge(b: &brep::Brep, ei: usize, rules: &Rules, h: f64) -> R
};
let t_at = |i: u32| t0 + (t1 - t0) * f64::from(i) / f64::from(initial);
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: Vec3,
pb: Vec3,
mid: Option<Vec3>,
depth: u32,
}
let mut out: Vec<(f64, Vec3)> = vec![(t0, front)];
let mut stack: Vec<Seg> = Vec::with_capacity(64);
for (i, w) in ends.windows(2).enumerate().rev() {
// The segment from `a` to `b` (points `pa`, `pb`, its middle `pm` where known), measured.
let measure = |a: f64, b: f64, pa: Vec3, pb: Vec3, pm: Option<Vec3>, depth: u32| {
let pm = match pm {
Some(p) => p,
None => at(0.5 * (a + b))?,
};
let (q1, q3) = (at(a + 0.25 * (b - a))?, at(a + 0.75 * (b - a))?);
let sag = [q1, pm, q3]
.iter()
.map(|q| q.dist_to_segment(pa, pb))
.fold(0.0, f64::max);
let len = pa.dist(pb);
let short = len.partial_cmp(&(h * (1.0 + 1e-9))) != Some(Ordering::Greater);
// The two half chords of an arc turning θ differ by θ/2.
let bend = 2.0 * turn(pm - pa, pb - pm);
let straight = rules.turn.is_none_or(|t| !t.exceeds(bend, sag));
// Whatever the tolerance, no segment spans more than a quarter turn of its curve: a
// small circle split into two half arcs would otherwise be two chords on one line, a
// hole of no area (the 1 mm hole of a 990 mm plate at its 2 mm chord).
let quarter = bend <= QUARTER_TURN;
let ok = short && straight && quarter && sag <= tol;
// How far past its rules a segment that fails them is: the most of each rule's ratio.
let excess = if ok {
0.0
} else {
let mut x = (sag / tol).max(bend / QUARTER_TURN);
if h > 0.0 {
x = x.max(len / h);
}
if let Some(t) = rules.turn.filter(|t| t.exceeds(bend, sag)) {
x = x.max(bend / t.angle);
}
x
};
Ok::<Seg, TessError>(Seg {
a,
b,
pa,
pb,
q1,
pm,
q3,
depth,
ok,
sag,
excess,
})
};
// Bisection, the segment exceeding its rules most first (then the earliest): a segment is
// halved where it fails the rules, whatever the order, so the nodes are the same as any
// order's; where MAX_EDGE_NODES stops it the segments kept whole are those that exceed their
// rules least, and the error is spread along the edge. (MAX_DEPTH stops a segment on its own,
// usually the one exceeding its rules most: a corner no halving reaches.)
let mut heap: std::collections::BinaryHeap<Seg> = std::collections::BinaryHeap::new();
// Proven: the window size is the constant 2, not zero.
for (i, w) in ends.windows(2).enumerate() {
let (Some(&pa), Some(&pb)) = (w.first(), w.get(1)) else {
continue;
};
// Proven: i < initial (at most 4).
let i = i as u32;
stack.push(Seg {
a: t_at(i),
b: t_at(i + 1),
pa,
pb,
mid: None,
depth: 0,
});
heap.push(measure(t_at(i), t_at(i + 1), pa, pb, None, 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) => p,
None => at(m)?,
};
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()
.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 bend = 2.0 * turn(pm - pa, pb - pm);
let straight = rules.turn.is_none_or(|t| !t.exceeds(bend, sag));
// Whatever the tolerance, no segment spans more than a quarter turn of its curve: a
// small circle split into two half arcs would otherwise be two chords on one line,
// a hole of no area (the 1 mm hole of a 990 mm plate at its 2 mm chord).
let quarter = bend <= QUARTER_TURN;
short && straight && quarter && sag <= tol
};
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,
});
// The segments kept: their end parameter and point; the most any sags, and exceeds its rules.
let mut kept: Vec<(f64, Vec3)> = Vec::new();
let (mut most_sag, mut most_excess) = (0.0f64, 0.0f64);
let mut capped: Option<crate::EdgeCap> = None;
while let Some(sg) = heap.pop() {
let deep = sg.depth >= MAX_DEPTH;
// The edge's nodes are its start and the ends of the segments kept, pending and this
// one (2 + kept + pending): halving this one adds a node, past the bound once they
// number MAX_EDGE_NODES.
let full = 2 + kept.len() + heap.len() >= MAX_EDGE_NODES;
if !deep && !full && !sg.ok {
let m = 0.5 * (sg.a + sg.b);
heap.push(measure(sg.a, m, sg.pa, sg.pm, Some(sg.q1), sg.depth + 1)?);
heap.push(measure(m, sg.b, sg.pm, sg.pb, Some(sg.q3), sg.depth + 1)?);
} else {
out.push((b, pb));
// A segment a bound keeps whole although it wants a split: the edge is named
// ([`crate::PlanStats::edges_capped`]).
if !sg.ok && capped != Some(crate::EdgeCap::Nodes) {
capped = Some(if full {
crate::EdgeCap::Nodes
} else {
crate::EdgeCap::Depth
});
}
most_sag = most_sag.max(sg.sag);
most_excess = most_excess.max(sg.excess);
kept.push((sg.b, sg.pb));
}
}
kept.sort_by(|x, y| x.0.total_cmp(&y.0));
let capped = capped.map(|cap| (cap, most_sag, most_excess));
let mut out: Vec<(f64, Vec3)> = std::iter::once((t0, front)).chain(kept).collect();
if let Some(p) = out.last_mut() {
*p = (t1, back);
}
@@ -179,6 +270,7 @@ pub(crate) fn sample_edge(b: &brep::Brep, ei: usize, rules: &Rules, h: f64) -> R
points: out.iter().map(|p| p.1).collect(),
params: out.iter().map(|p| p.0).collect(),
snapped: Vec::new(),
capped,
})
}
@@ -209,6 +301,7 @@ pub(crate) fn split_segments(
.map_err(|e| fail(crate::eval_message(e)))?,
),
};
// Proven: n + segs.len() <= MAX_EDGE_NODES = 20 000, checked above
let (mut points, mut params) = (
Vec::with_capacity(n + segs.len()),
Vec::with_capacity(n + segs.len()),
@@ -220,6 +313,7 @@ pub(crate) fn split_segments(
};
points.push(p);
params.push(t);
// Proven: k < n <= MAX_EDGE_NODES (20 000): it fits a u32.
if next.next_if_eq(&(k as u32)).is_none() {
continue;
}
@@ -266,6 +360,7 @@ pub(crate) fn snap(ei: usize, nodes: &mut Nodes, add: &[(f64, Vec3)]) -> Result<
}
let n = nodes.points.len().min(nodes.params.len());
let (mut points, mut params) = (
// Proven: n + add.len() <= MAX_EDGE_NODES = 20 000, checked above
Vec::with_capacity(n + add.len()),
Vec::with_capacity(n + add.len()),
);
@@ -302,5 +397,281 @@ pub(crate) fn turn(a: Vec3, b: Vec3) -> f64 {
if !(la > 0.0 && lb > 0.0) {
return 0.0;
}
// Proven: the clamp bounds are the constants -1.0 <= 1.0, neither NaN.
(a.dot(b) / (la * lb)).clamp(-1.0, 1.0).acos()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::planner::tests::fixture;
fn sample(b: &brep::Brep, ei: usize, rules: &Rules, h: f64) -> Nodes {
match sample_edge(b, ei, rules, h) {
Ok(n) => n,
Err(e) => panic!("{e}"),
}
}
fn ends(b: &brep::Brep, ei: usize) -> [Vec3; 2] {
b.edges[ei].vertices.map(|v| {
let p = b.vertices[v as usize].point;
Vec3::new(p[0], p[1], p[2])
})
}
/// A line edge whose vertex is not a number is refused, naming the edge: its length is not a
/// number, and the node count it gave was none (a polyline of one point).
#[test]
fn a_line_edge_with_a_vertex_not_a_number_is_refused() {
let mut cyl = fixture("cylinder_r10_h5");
let Some(line) = cyl
.edges
.iter()
.position(|e| matches!(e.curve, Curve::Line { .. }))
else {
panic!("a line edge")
};
let v = cyl.edges[line].vertices[0] as usize;
cyl.vertices[v].point[0] = f64::NAN;
let rules = Rules {
tol: 0.01,
max_len: f64::INFINITY,
turn: None,
};
match sample_edge(&cyl, line, &rules, 1.0) {
Err(e) => assert!(
e.to_string()
.contains(&format!("edge {line}: its vertices")),
"{e}"
),
Ok(n) => panic!("{} nodes", n.points.len()),
}
}
/// A circle of radius 10 sampled to sides of at most 1 µm wants about 63 000 segments, more
/// than an edge may have: it stops at `MAX_EDGE_NODES` and says so. The longest segments are
/// halved first, so when the bound stops it every side kept is at least half the longest
/// (each was halved from one at least as long as any kept whole): its N = 19 999 sides are at
/// most twice the even split's 2πr/N, and as a chord's sag goes with its square, none sags
/// more than 4 r (1 − cos(π/N)), about 4.9e-7 mm (the even split's own: 1.2e-7). The sag
/// recorded is the polyline's own largest. Halving depth first, the bound filled along the
/// start of the circle and kept the rest whole: a quarter turn, 2.9 mm off.
#[test]
fn an_edge_that_stops_at_its_node_bound_says_so() {
let cyl = fixture("cylinder_r10_h5");
let Some(ei) = cyl
.edges
.iter()
.position(|e| !matches!(e.curve, Curve::Line { .. }))
else {
panic!("a circle")
};
let rules = Rules {
tol: 0.01,
max_len: f64::INFINITY,
turn: None,
};
let n = sample(&cyl, ei, &rules, 1e-3);
// Exactly the bound: N = 19 999 sides.
assert_eq!(n.points.len(), MAX_EDGE_NODES);
let Some((crate::EdgeCap::Nodes, sag, excess)) = n.capped else {
panic!("{:?}", n.capped)
};
// Each chord's sag on the circle of radius 10, from its length.
let r = 10.0;
let most = n
.points
.windows(2)
.map(|w| {
let c = w[0].dist(w[1]);
r - (r * r - 0.25 * c * c).sqrt()
})
.fold(0.0, f64::max);
let sides = (MAX_EDGE_NODES - 1) as f64;
let bound = 4.0 * r * (1.0 - (std::f64::consts::PI / sides).cos());
assert!(most <= bound * (1.0 + 1e-6), "{most} against {bound}");
assert!(
(sag - most).abs() <= 1e-6 * bound,
"recorded {sag}, the polyline's {most}"
);
// Its sides are longer than the 1 µm asked: about 2πr / N / 1e-3, π.
assert!(excess > 2.0 && excess < 7.0, "{excess}");
// A line asked for more sides than that too, its chords on it.
let Some(line) = cyl
.edges
.iter()
.position(|e| matches!(e.curve, Curve::Line { .. }))
else {
panic!("a line")
};
let n = sample(&cyl, line, &rules, 1e-6);
assert_eq!(n.points.len(), MAX_EDGE_NODES);
// 5 mm in 19 999 sides, each 0.25 µm against the 1e-6 mm asked.
let side = 5.0 / (MAX_EDGE_NODES - 1) as f64;
let Some((crate::EdgeCap::Nodes, sag, excess)) = n.capped else {
panic!("{:?}", n.capped)
};
assert_eq!(sag, 0.0);
assert!((excess - side / 1e-6).abs() < 1e-9 * excess, "{excess}");
// At sides it can have, neither says so.
assert_eq!(sample(&cyl, ei, &rules, 1.0).capped, None);
assert_eq!(sample(&cyl, line, &rules, 1.0).capped, None);
}
/// A polyline with a corner at a third of its parameter (never a node of the halving): the
/// segment across the corner sags at every depth, so its halving stops at `MAX_DEPTH`, and the
/// edge says so, with the sag left there. That was silent.
#[test]
fn an_edge_that_stops_at_its_depth_bound_says_so() {
let mut b = fixture("box_10x6x4");
// Edge 2 runs (10, 0, 4) to (10, 6, 4): bent up to (10, 2, 6) at t = 1/3.
b.edges[2].curve = Curve::Spline {
knots: vec![0.0, 0.0, 1.0 / 3.0, 1.0, 1.0],
points: vec![[10.0, 0.0, 4.0], [10.0, 2.0, 6.0], [10.0, 6.0, 4.0]],
};
b.edges[2].nurbs = None;
let rules = Rules {
tol: 1e-12,
max_len: f64::INFINITY,
turn: None,
};
let n = sample(&b, 2, &rules, f64::INFINITY);
assert!(n.points.len() < 1000, "{}", n.points.len());
let Some((crate::EdgeCap::Depth, sag, _)) = n.capped else {
panic!("{:?}", n.capped)
};
assert!(sag > 1e-12, "{sag}");
}
#[test]
fn circle_edges_are_within_tolerance_and_end_on_vertices() {
// A cylinder of radius 10 and length 5 up z from the origin.
let cyl = fixture("cylinder_r10_h5");
for tol in [0.1, 0.01, 0.001] {
for (ei, e) in cyl.edges.iter().enumerate() {
let pts = sample(
&cyl,
ei,
&Rules {
tol,
max_len: f64::INFINITY,
turn: None,
},
f64::INFINITY,
)
.points;
assert!(pts.len() >= 2);
let [front, back] = ends(&cyl, ei);
assert_eq!(pts.first().copied(), Some(front));
assert_eq!(pts.last().copied(), Some(back));
if matches!(e.curve, Curve::Line { .. }) {
assert_eq!(pts.len(), 2);
continue;
}
// Circle of radius 10: every chord's middle sags at most `tol` (plus rounding).
for w in pts.windows(2) {
let m = (w[0] + w[1]) * 0.5;
let r = (m.x * m.x + m.y * m.y).sqrt();
assert!(10.0 - r <= tol * 1.0001, "sag {} at tol {tol}", 10.0 - r);
}
}
}
}
#[test]
fn edges_are_split_to_the_side_length_and_turn() {
// A cylinder of radius 10 and length 5 up z from the origin.
let cyl = fixture("cylinder_r10_h5");
for (ei, e) in cyl.edges.iter().enumerate() {
let pts = sample(
&cyl,
ei,
&Rules {
tol: 1.0,
max_len: f64::INFINITY,
turn: None,
},
1.2,
)
.points;
for w in pts.windows(2) {
assert!(w[0].dist(w[1]) <= 1.2 + 1e-9, "side {}", w[0].dist(w[1]));
}
if matches!(e.curve, Curve::Line { .. }) {
// 5 mm in sides of at most 1.2: five equal sides.
assert_eq!(pts.len(), 6);
}
// The ends are the vertices' own points, to the last bit.
let [front, back] = ends(&cyl, ei);
assert_eq!(pts.first().copied(), Some(front));
assert_eq!(pts.last().copied(), Some(back));
}
// A coarse tolerance alone would leave a few long chords; a 30° facet angle keeps every
// side within 30° of arc: at least 12 around each of the two circles.
let thirty = Rules {
tol: 5.0,
max_len: f64::INFINITY,
turn: Some(crate::planner::size::Turn {
angle: std::f64::consts::PI / 6.0,
floor: 0.0,
}),
};
let sides: usize = (0..cyl.edges.len())
.filter(|&ei| !matches!(cyl.edges[ei].curve, Curve::Line { .. }))
.map(|ei| sample(&cyl, ei, &thirty, f64::INFINITY).points.len() - 1)
.sum();
assert!(sides >= 24, "{sides}");
}
/// The nodes of every edge where two curved faces meet carry their curve parameters: each
/// face's pcurve of the edge puts each node on that face's surface, at the node.
#[test]
fn edge_nodes_carry_their_parameters_on_both_faces() {
// A cylinder of radius 2 up z through (5, 5, -1), 6 long, less a cylinder of radius 1
// along y through (5, -1, 2), 12 long.
let body = fixture("cylinder_with_cross_hole");
let rules = Rules {
tol: 0.01,
max_len: f64::INFINITY,
turn: None,
};
let curved = |f: u32| {
!matches!(
body.faces[f as usize].surface,
brep::Surface::Plane { .. } | brep::Surface::PlaneThrough { .. }
)
};
let (mut edges, mut checked) = (0, 0);
for (ei, e) in body.edges.iter().enumerate() {
if e.uses.len() != 2 || !e.uses.iter().all(|u| curved(u.face)) {
continue;
}
let n = sample(&body, ei, &rules, f64::INFINITY);
edges += 1;
for u in &e.uses {
let face = &body.faces[u.face as usize];
let brep::Loop::Edges { edges: loop_edges } = &face.loops[u.r#loop as usize] else {
panic!("an edge loop")
};
let oe = &loop_edges[u.position as usize];
let Ok(s) = crate::planner::surface::eval_surface(face.parameters()) else {
panic!("evaluates")
};
let pcurve = kernel::curve2(&oe.pcurve);
for (t, p) in n.params.iter().zip(&n.points) {
let Some(q) = pcurve.eval(*t) else {
panic!("pcurve at {t}")
};
let Some((at, _, _)) = s.derivs(q[0], q[1]).map(kernel::record_triple) else {
panic!("evaluates")
};
assert!(at.dist(*p) < 1e-6 + e.tolerance, "{at:?} vs {p:?}");
checked += 1;
}
}
}
assert!(edges > 0 && checked > 20, "{edges} edges, {checked} nodes");
}
}
+41 -14
View File
@@ -1,7 +1,11 @@
//! Structured meshing of a face whose boundary runs along its parameter box: a grid of
//! interior lines (spaced by the surface's own curvature), joined to the shared edge nodes on
//! each side by a zipped strip (after Cadium's structured and zipped grids: the larger side
//! fans into the smaller one, so the edge nodes are used exactly as they are).
//! each side by a zipped strip (the larger side fans into the smaller one, so the edge nodes are
//! used exactly as they are).
use std::cmp::Ordering;
use super::UvMesh;
/// Most grid points of one face.
pub(crate) const MAX_FACE_GRID: usize = 150_000;
@@ -31,6 +35,7 @@ pub(crate) fn thin(us: &mut Vec<f64>, vs: &mut Vec<f64>, cap: usize) {
if d.len() > n && n >= 2 {
let len = d.len();
*d = (0..n)
// Proven: n >= 2 checked above, so the divisor n - 1 > 0.
.filter_map(|i| d.get(i * (len - 1) / (n - 1)).copied())
.collect();
}
@@ -85,12 +90,12 @@ pub(crate) fn quad(boundary: &[[f64; 2]]) -> Option<Quad> {
}
let mut bbox = [(f64::MAX, f64::MIN); 2];
for p in boundary {
for d in 0..2 {
bbox[d] = (bbox[d].0.min(p[d]), bbox[d].1.max(p[d]));
for (b, &x) in bbox.iter_mut().zip(p) {
*b = (b.0.min(x), b.1.max(x));
}
}
let ext = [bbox[0].1 - bbox[0].0, bbox[1].1 - bbox[1].0];
if !(ext[0] > 0.0 && ext[1] > 0.0) || !(ext[0].is_finite() && ext[1].is_finite()) {
if !(ext[0] > 0.0 && ext[1] > 0.0 && ext[0].is_finite() && ext[1].is_finite()) {
return None;
}
let eps = [ext[0] * 1e-7, ext[1] * 1e-7];
@@ -123,8 +128,11 @@ pub(crate) fn quad(boundary: &[[f64; 2]]) -> Option<Quad> {
let mut sides: [Vec<u32>; 4] = Default::default();
let mut seen = 0u8;
for (k, &c0) in corners.iter().enumerate() {
// Proven: the divisor is the constant 4, not zero.
let c1 = *corners.get((k + 1) % 4)?;
// Proven: n = boundary.len() >= 4, checked above.
let len = (c1 + n - c0) % n;
// Proven: n = boundary.len() >= 4, checked above.
let chain: Vec<usize> = (0..=len).map(|i| (c0 + i) % n).collect();
let common = chain
.iter()
@@ -135,13 +143,22 @@ pub(crate) fn quad(boundary: &[[f64; 2]]) -> Option<Quad> {
}
seen |= side.bit();
let d = side.along();
// Proven: every index is below n = boundary.len(), a face's boundary nodes, at most
// MAX_FACE_NODES (checked by `plan_face`), so fits a u32.
let mut chain: Vec<u32> = chain.into_iter().map(|i| i as u32).collect();
// Proven: `d` is `side.along()`, 0 or 1, and a node is a `[f64; 2]`.
#[allow(clippy::indexing_slicing)]
let coord = |i: &u32| boundary.get(*i as usize).map_or(0.0, |p| p[d]);
if chain.first().map(coord) > chain.last().map(coord) {
chain.reverse();
}
// The edge nodes must advance along the side.
if chain.windows(2).any(|w| coord(&w[0]) >= coord(&w[1])) && chain.len() > 2 {
if chain
.iter()
.zip(chain.iter().skip(1))
.any(|(a, b)| coord(a) >= coord(b))
&& chain.len() > 2
{
return None;
}
let slot = SIDES.iter().position(|s| *s == side)?;
@@ -164,9 +181,12 @@ pub(crate) fn aligned_lines(boundary: &[[f64; 2]], q: &Quad, d: usize) -> Option
}
let (lo, hi) = q.bbox.get(d).copied()?;
let eps = (hi - lo) * 1e-9;
let mut lines = Vec::with_capacity(a.len());
let mut lines = crate::with_room(a.len())?;
for (i, j) in a.iter().zip(b) {
let (x, y) = (boundary.get(*i as usize)?[d], boundary.get(*j as usize)?[d]);
let (x, y) = (
*boundary.get(*i as usize)?.get(d)?,
*boundary.get(*j as usize)?.get(d)?,
);
if (x - y).abs() > eps {
return None;
}
@@ -203,7 +223,7 @@ pub(crate) fn strip(boundary: &[[f64; 2]], q: &Quad, d: usize) -> Option<Vec<[u3
&mut tris,
)?;
for t in &tris {
if !(signed_area(boundary, *t)? > 0.0) {
if signed_area(boundary, *t)?.partial_cmp(&0.0) != Some(Ordering::Greater) {
return None;
}
}
@@ -230,12 +250,15 @@ pub(crate) fn structured(
q: &Quad,
us: &[f64],
vs: &[f64],
) -> Option<(Vec<[f64; 2]>, Vec<[u32; 3]>)> {
) -> Option<UvMesh> {
let (nu, nv) = (us.len(), vs.len());
if nu == 0 || nv == 0 || nu.checked_mul(nv)? > MAX_FACE_GRID {
return None;
}
let n = u32::try_from(boundary.len()).ok()?;
u32::try_from(boundary.len().checked_add(nu * nv)?).ok()?;
// Proven: i < nu and j < nv, so n + j · nu + i < boundary.len() + nu · nv, which fits a u32
// (checked above; nu · nv <= MAX_FACE_GRID, checked before).
let g = |i: usize, j: usize| n + (j * nu + i) as u32;
let mut uv: Vec<[f64; 2]> = boundary.to_vec();
for &v in vs {
@@ -243,7 +266,7 @@ pub(crate) fn structured(
uv.push([u, v]);
}
}
let mut tris: Vec<[u32; 3]> = Vec::with_capacity(2 * (nu + 1) * (nv + 1) + 2 * boundary.len());
let mut tris: Vec<[u32; 3]> = crate::with_room(2 * (nu + 1) * (nv + 1) + 2 * boundary.len())?;
for j in 0..nv.saturating_sub(1) {
for i in 0..nu.saturating_sub(1) {
let (a, b, c, d) = (g(i, j), g(i + 1, j), g(i + 1, j + 1), g(i, j + 1));
@@ -266,10 +289,11 @@ pub(crate) fn structured(
}
// Every triangle counter-clockwise: one that is not has folded over.
for t in &tris {
if !(signed_area(&uv, *t)? > 0.0) {
if signed_area(&uv, *t)?.partial_cmp(&0.0) != Some(Ordering::Greater) {
return None;
}
}
// Proven: `uv` starts as `boundary.to_vec()` and only grows.
let extra = uv.split_off(boundary.len());
Some((extra, tris))
}
@@ -295,8 +319,11 @@ fn zip(
tris: &mut Vec<[u32; 3]>,
) -> Option<()> {
let mut push = |a: u32, b: u32, c: u32| tris.push(if flip { [a, c, b] } else { [a, b, c] });
let coord =
|chain: &[u32], k: usize| chain.get(k).and_then(|i| uv.get(*i as usize)).map(|p| p[d]);
let coord = |chain: &[u32], k: usize| {
chain
.get(k)
.and_then(|i| uv.get(*i as usize)?.get(d).copied())
};
let (mut a, mut b) = (0usize, 0usize);
while a + 1 < outer.len() || b + 1 < inner.len() {
let advance_outer = if a + 1 >= outer.len() {
File diff suppressed because it is too large Load Diff
+60 -36
View File
@@ -55,6 +55,31 @@ struct Moto {
hit: (usize, usize),
}
impl Moto {
/// The coordinate of `q` along the direction of travel.
fn along(&self, q: [f64; 2]) -> f64 {
if self.d == 0 { q[0] } else { q[1] }
}
/// The coordinate of `q` across the direction of travel (constant on its line).
fn across(&self, q: [f64; 2]) -> f64 {
if self.d == 0 { q[1] } else { q[0] }
}
/// `q` with its coordinate along the direction of travel set to `x`.
fn with_along(&self, q: [f64; 2], x: f64) -> [f64; 2] {
if self.d == 0 { [x, q[1]] } else { [q[0], x] }
}
}
/// A motorcycle stopping, in time order: when, which one, where along its way, the one whose
/// trace stops it and when that one passed there, and the one it meets head-on.
struct Event {
t: f64,
who: usize,
at: f64,
cause: Option<(usize, f64)>,
partner: Option<usize>,
}
/// The traces of the face bounded by `loops` (ranges of `boundary`, even–odd), in (u, v),
/// measured in the metric `scale`. Two motorcycles coming at each other on lines less than
/// `snap` apart (in the metric) meet head-on: a polygon's extreme node is only near the curve's
@@ -76,9 +101,9 @@ pub(crate) fn traces(
let mut lo = [f64::MAX; 2];
let mut hi = [f64::MIN; 2];
for q in &p {
for d in 0..2 {
lo[d] = lo[d].min(q[d]);
hi[d] = hi[d].max(q[d]);
for ((l, h), &x) in lo.iter_mut().zip(&mut hi).zip(q) {
*l = l.min(x);
*h = h.max(x);
}
}
let extent = (hi[0] - lo[0]).max(hi[1] - lo[1]);
@@ -91,6 +116,7 @@ pub(crate) fn traces(
.iter()
.flat_map(|r| {
let n = r.len();
// Proven: n = r.len() > 0: `k` ranges over `0..n`
(0..n).map(move |k| (r.start + k, r.start + (k + 1) % n))
})
.collect();
@@ -117,14 +143,15 @@ pub(crate) fn traces(
}
for k in 0..n {
let (Some(&a), Some(&b), Some(&c)) = (
// Proven (both `% n` here): n = r.len() >= 3, checked above
p.get(r.start + (k + n - 1) % n),
p.get(r.start + k),
p.get(r.start + (k + 1) % n),
) else {
continue;
};
for d in 0..2 {
let (before, after) = (a[d] - b[d], c[d] - b[d]);
for (d, ((&ad, &bd), &cd)) in a.iter().zip(&b).zip(&c).enumerate() {
let (before, after) = (ad - bd, cd - bd);
let flat = |x: f64| x.abs() <= eps;
// A (non-strict) minimum higher on one side, or maximum lower on one side.
let min = (before > eps || flat(before))
@@ -139,8 +166,12 @@ pub(crate) fn traces(
_ => continue,
};
// Concave: the face lies beyond the extremum.
let mut probe = b;
probe[d] += sign * extent * 1e-6;
let step = sign * extent * 1e-6;
let probe = if d == 0 {
[b[0] + step, b[1]]
} else {
[b[0], b[1] + step]
};
if !inside(probe) {
continue;
}
@@ -163,7 +194,6 @@ pub(crate) fn traces(
}
// Where each would reach the boundary.
for m in &mut motos {
let e = 1 - m.d;
for &(ia, ib) in &segs {
if ia == m.node || ib == m.node {
continue;
@@ -171,20 +201,23 @@ pub(crate) fn traces(
let (Some(a), Some(b)) = (p.get(ia), p.get(ib)) else {
continue;
};
// The segment crosses the line other = o[e] at parameter s along d.
let (lo_e, hi_e) = (a[e].min(b[e]), a[e].max(b[e]));
if m.o[e] < lo_e || m.o[e] > hi_e {
// The segment crosses the line across = across(o) at parameter s along d.
let (ae, be, oe) = (m.across(*a), m.across(*b), m.across(m.o));
let (lo_e, hi_e) = (ae.min(be), ae.max(be));
if oe < lo_e || oe > hi_e {
continue;
}
// A segment along the line itself is reached at its nearer end ahead.
let ahead = |x: f64| (x - m.o[m.d]) * m.sign;
let t = if (b[e] - a[e]).abs() <= eps {
[ahead(a[m.d]), ahead(b[m.d])]
let od = m.along(m.o);
let ahead = |x: f64| (x - od) * m.sign;
let (ad, bd) = (m.along(*a), m.along(*b));
let t = if (be - ae).abs() <= eps {
[ahead(ad), ahead(bd)]
.into_iter()
.filter(|t| *t > eps)
.fold(f64::INFINITY, f64::min)
} else {
ahead(a[m.d] + (m.o[e] - a[e]) / (b[e] - a[e]) * (b[m.d] - a[m.d]))
ahead(ad + (oe - ae) / (be - ae) * (bd - ad))
};
if t > eps && t < m.reach {
m.reach = t;
@@ -192,15 +225,7 @@ pub(crate) fn traces(
}
}
}
// Events in time order: (time, the motorcycle that stops, where, the one whose trace stops
// it and when it passed there).
struct Event {
t: f64,
who: usize,
at: f64,
cause: Option<(usize, f64)>,
partner: Option<usize>,
}
// Events in time order (see [`Event`]).
let mut events: Vec<Event> = Vec::new();
for (i, m) in motos.iter().enumerate() {
if m.reach.is_finite() {
@@ -214,9 +239,9 @@ pub(crate) fn traces(
}
for (j, n) in motos.iter().enumerate().skip(i + 1) {
if m.d != n.d {
// Crossing: m's line is other = m.o[1 - m.d]; n travels along n.d = 1 - m.d.
let x_on_m = (n.o[m.d] - m.o[m.d]) * m.sign;
let x_on_n = (m.o[n.d] - n.o[n.d]) * n.sign;
// Crossing: m's line is across = m.across(m.o); n travels along it.
let x_on_m = (m.along(n.o) - m.along(m.o)) * m.sign;
let x_on_n = (n.along(m.o) - n.along(n.o)) * n.sign;
if x_on_m > eps && x_on_m < m.reach && x_on_n > eps && x_on_n < n.reach {
// The later one stops on the earlier one's trace (a tie stops both).
if x_on_m >= x_on_n {
@@ -238,9 +263,9 @@ pub(crate) fn traces(
});
}
}
} else if m.sign != n.sign && (m.o[1 - m.d] - n.o[1 - n.d]).abs() <= eps.max(snap) {
} else if m.sign != n.sign && (m.across(m.o) - n.across(n.o)).abs() <= eps.max(snap) {
// Head-on on one line: they meet halfway if each would get there.
let gap = (n.o[m.d] - m.o[m.d]) * m.sign;
let gap = (m.along(n.o) - m.along(m.o)) * m.sign;
let half = 0.5 * gap;
if gap > eps && half < m.reach && half < n.reach {
events.push(Event {
@@ -257,7 +282,7 @@ pub(crate) fn traces(
events.sort_by(|a, b| a.t.total_cmp(&b.t));
let mut stop: Vec<Option<(f64, Stop)>> = vec![None; motos.len()];
for ev in &events {
if stop.get(ev.who).is_none_or(|s| s.is_some()) {
if stop.get(ev.who).is_none_or(Option::is_some) {
continue;
}
if let Some((c, passed)) = ev.cause {
@@ -277,7 +302,7 @@ pub(crate) fn traces(
(None, None) => Stop::Boundary,
};
if let Some(j) = ev.partner {
if stop.get(j).is_none_or(|s| s.is_some()) {
if stop.get(j).is_none_or(Option::is_some) {
continue;
}
if let Some(s) = stop.get_mut(j) {
@@ -305,12 +330,11 @@ pub(crate) fn traces(
.zip(&stop)
.filter_map(|(m, s)| {
let (len, how) = (*s)?;
let (a, b) = (m.o[m.d], m.o[m.d] + m.sign * len);
let (from, to) = (a.min(b) / scale[m.d], a.max(b) / scale[m.d]);
let (a, b) = (m.along(m.o), m.along(m.o) + m.sign * len);
let (from, to) = (a.min(b) / m.along(scale), a.max(b) / m.along(scale));
// The trace runs along d, so it lies on the iso-line of the other parameter.
let axis = 1 - m.d;
let mut endp = m.o;
endp[m.d] = b;
let endp = m.with_along(m.o, b);
let end = match how {
Stop::Boundary => End::Boundary(m.hit.0, m.hit.1),
Stop::Trace(c) => End::Trace(*out_index.get(c)?),
@@ -318,7 +342,7 @@ pub(crate) fn traces(
};
Some(Trace {
axis,
at: m.o[axis] / scale[axis],
at: m.across(m.o) / m.across(scale),
from,
to,
start: m.node,
+41 -14
View File
@@ -6,6 +6,7 @@
//! boundary node, at a point inside the face (a square's corner, or where two traces met
//! head-on), or on another cut (a T-node, which that cut then passes through).
use std::cmp::Ordering;
use std::collections::HashMap;
use crate::ring::Ring;
@@ -39,18 +40,36 @@ pub(crate) struct Piece {
pub holes: Vec<Ring<u32>>,
}
/// Where to put nodes between two nodes of a cut, given the spacing wanted at each (see
/// [`cut`]); the error is a point the surface does not evaluate at.
pub(crate) type Seeds<'a, E> = dyn Fn([f64; 2], [f64; 2], f64, f64) -> Result<Vec<f64>, E> + 'a;
/// Cut the face bounded by `loops` (ranges of `boundary`) along `segments`, measured in the
/// metric `scale`. `seeds(a, b, sa, sb)`: where to put nodes between two nodes of a cut (in
/// (u, v)), as fractions strictly between 0 and 1, given the spacing wanted at each (in the
/// metric): the boundary's own spacing at a boundary node, unbounded inside the face (the seed
/// rule then sets it). Returns the cut nodes (numbered after the boundary) and the patches;
/// `None` when the cuts do not make a planar subdivision (the face is then meshed whole).
pub(crate) fn cut(
/// `Ok(None)` when the cuts do not make a planar subdivision (the face is then meshed whole);
/// `seeds`' error (a point the surface does not evaluate at) is the error.
#[allow(clippy::type_complexity)] // the cut nodes and the patches, as Option, in a Result
pub(crate) fn cut<E>(
boundary: &[[f64; 2]],
loops: &[std::ops::Range<usize>],
segments: &[Segment],
scale: [f64; 2],
seeds: &dyn Fn([f64; 2], [f64; 2], f64, f64) -> Vec<f64>,
seeds: &Seeds<'_, E>,
) -> Result<Option<(Vec<[f64; 2]>, Vec<Piece>)>, E> {
super::layout(|stopped| cut_in(boundary, loops, segments, scale, seeds, stopped))
}
/// [`cut`]'s body: `seeds`' error goes to `stopped`.
fn cut_in<E>(
boundary: &[[f64; 2]],
loops: &[std::ops::Range<usize>],
segments: &[Segment],
scale: [f64; 2],
seeds: &Seeds<'_, E>,
stopped: &mut Option<E>,
) -> Option<(Vec<[f64; 2]>, Vec<Piece>)> {
let n = boundary.len();
let metric = |q: [f64; 2]| [q[0] * scale[0], q[1] * scale[1]];
@@ -61,6 +80,7 @@ pub(crate) fn cut(
let mut cut_uv: Vec<[f64; 2]> = Vec::new();
let mut points: HashMap<[u64; 2], u32> = HashMap::new();
// Each segment's end nodes, and the T-nodes other segments put on it.
// Proven: capacity of allocated `segments`' length, its elements larger
let mut ends: Vec<(u32, u32)> = Vec::with_capacity(segments.len());
let mut along: Vec<Vec<u32>> = vec![Vec::new(); segments.len()];
for s in segments {
@@ -104,6 +124,7 @@ pub(crate) fn cut(
};
let m = r.len();
let i = k - r.start;
// Proven: m = r.len() > 0, as `r` contains k
let (prev, next) = (r.start + (i + m - 1) % m, r.start + (i + 1) % m);
match (boundary.get(prev), boundary.get(k), boundary.get(next)) {
(Some(&p), Some(&q), Some(&x)) => 0.5 * (dist(p, q) + dist(q, x)),
@@ -117,6 +138,7 @@ pub(crate) fn cut(
for k in 0..m {
let (a, b) = (
u32::try_from(r.start + k).ok()?,
// Proven: m = r.len() > 0 inside `for k in 0..m`
u32::try_from(r.start + (k + 1) % m).ok()?,
);
sides.insert((a.min(b), a.max(b)));
@@ -138,6 +160,7 @@ pub(crate) fn cut(
nodes.sort_by(|a, b| a.0.total_cmp(&b.0));
nodes.dedup_by_key(|x| x.1);
let count = segments.get(i).and_then(|s| s.count);
// Proven: the window size is the constant 2, not zero.
for w in nodes.windows(2) {
let (Some(&(_, a)), Some(&(_, b))) = (w.first(), w.get(1)) else {
continue;
@@ -154,10 +177,11 @@ pub(crate) fn cut(
let fractions: Vec<f64> = match count {
Some(k) if total > 0.0 => {
let share = dist(pa, pb) / total;
// Proven: the clamp bounds are the constants 1 <= 4096.
let k = ((k as f64 * share).round() as usize).clamp(1, 4096);
(1..k).map(|j| j as f64 / k as f64).collect()
}
_ => seeds(pa, pb, spacing(a), spacing(b)),
_ => super::stop(stopped, seeds(pa, pb, spacing(a), spacing(b)))?,
};
let mut prev = a;
for f in fractions.into_iter().take(4096) {
@@ -182,6 +206,7 @@ pub(crate) fn cut(
for k in 0..m {
let (Some(a), Some(b)) = (
boundary.get(r.start + k).map(|p| metric(*p)),
// Proven: m = r.len() > 0 inside `for k in 0..m`
boundary.get(r.start + (k + 1) % m).map(|p| metric(*p)),
) else {
continue;
@@ -201,7 +226,7 @@ pub(crate) fn cut(
let side = |k: usize| -> Option<(u32, u32)> {
Some((
u32::try_from(r.start + k).ok()?,
u32::try_from(r.start + (k + 1) % m).ok()?,
u32::try_from(r.start + (k + 1).checked_rem(m)?).ok()?,
))
};
// Which way round the face lies to the left: test beside the loop's longest side.
@@ -216,7 +241,7 @@ pub(crate) fn cut(
let (a, b) = side(longest)?;
let (pa, pb) = (pos(a)?, pos(b)?);
let len = (pb[0] - pa[0]).hypot(pb[1] - pa[1]);
if !(len > 0.0) {
if len.partial_cmp(&0.0) != Some(Ordering::Greater) {
return None;
}
let left = [
@@ -388,8 +413,9 @@ mod tests {
count: None,
},
];
let Some((cut_uv, pieces)) = cut(&b, &loops, &segs, [1.0, 1.0], &|_, _, _, _| vec![0.5])
else {
let Ok(Some((cut_uv, pieces))) = cut(&b, &loops, &segs, [1.0, 1.0], &|_, _, _, _| {
Ok::<_, ()>(vec![0.5])
}) else {
panic!("cuts")
};
assert_eq!(pieces.len(), 4);
@@ -424,8 +450,9 @@ mod tests {
count: None,
},
];
let Some((cut_uv, pieces)) = cut(&b, &loops, &segs, [1.0, 1.0], &|_, _, _, _| Vec::new())
else {
let Ok(Some((cut_uv, pieces))) = cut(&b, &loops, &segs, [1.0, 1.0], &|_, _, _, _| {
Ok::<_, ()>(Vec::new())
}) else {
panic!("cuts")
};
assert_eq!(pieces.len(), 3, "{pieces:?}");
@@ -438,15 +465,15 @@ mod tests {
#[test]
fn no_segments_no_cut() {
let b = [[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]];
assert!(
assert_eq!(
cut(
&b,
&[std::ops::Range { start: 0, end: 4 }],
&[],
[1.0, 1.0],
&|_, _, _, _| Vec::new()
)
.is_none()
&|_, _, _, _| Ok::<_, ()>(Vec::new())
),
Ok(None)
);
}
}
+610
View File
@@ -0,0 +1,610 @@
//! Faces on a surface the kernel's evaluator refuses on one circle: the planner leaves such a
//! face out whole and names the point, wherever it meets it (a node, a middle the sag check
//! 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 solidcore::eval::{EvalCurve, EvalSurface};
use solidcore_convert as kernel;
use solidcore_record::Vec3;
use crate::vec::VecExt as _;
use super::size::{FaceSize, Rules, face_size};
use super::{NoValue, plan_or_fill, seam, uv};
/// An exact cylinder of radius 3 about z, its profile `u` in [0, 1] running from z = 0 to
/// z = 40 (`z = 40 u`), `v` the angle: a rational cubic whose weight has a double root at
/// `hole` (0.5 or 0.25), where both its numerator and weight vanish. The evaluator has no point
/// on the circle u = `hole` (0 / 0) and the cylinder's own everywhere else. The weights are
/// exact in binary, so the root is exactly zero there.
pub(crate) fn cylinder_without(hole: f64) -> EvalSurface {
// Homogeneous control points (w x, w y, w z, w): the weight is (1 - u / hole)² scaled, and
// w z is 40 u times it, in the cubic Bernstein basis.
let ctrl = if hole == 0.5 {
// Weight 3 (1 - 2u)²: Bernstein (3, -1, -1, 3); w z = 40 u · weight: (0, 40, -80, 120).
vec![
[9.0, 0.0, 0.0, 3.0],
[-3.0, 0.0, 40.0, -1.0],
[-3.0, 0.0, -80.0, -1.0],
[9.0, 0.0, 120.0, 3.0],
]
} else {
assert_eq!(hole, 0.25);
// Weight 3 (1 - 4u)²: Bernstein (3, -5, 3, 27); w z = 40 u · weight: (0, 40, -240, 1080).
vec![
[9.0, 0.0, 0.0, 3.0],
[-15.0, 0.0, 40.0, -5.0],
[9.0, 0.0, -240.0, 3.0],
[81.0, 0.0, 1080.0, 27.0],
]
};
let s = EvalSurface::Revolved {
profile: EvalCurve::new(3, vec![0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0], ctrl),
origin: solidcore::Vec3::ZERO,
axis: solidcore::Vec3::Z,
transform: [
[1.0, 0.0, 0.0, 0.0],
[0.0, 1.0, 0.0, 0.0],
[0.0, 0.0, 1.0, 0.0],
],
swap: false,
};
assert!(
s.derivs(hole, 0.3).is_none(),
"no point on the circle u = {hole}"
);
for u in [0.0, 0.125, 0.375, 0.5, 0.625, 0.75, 1.0]
.into_iter()
.filter(|u| *u != hole)
{
let p = point(&s, [u, 0.3]);
let want = Vec3::new(3.0 * 0.3f64.cos(), 3.0 * 0.3f64.sin(), 40.0 * u);
assert!(p.dist(want) < 1e-9, "the cylinder at u = {u}: {p:?}");
}
s
}
/// The size the planner works the cylinder's faces out at: side `h`, its speeds, no sample
/// left out.
fn size(h: f64) -> FaceSize {
FaceSize {
h,
speed: [40.0, 3.0],
unevaluated: Vec::new(),
}
}
fn point(s: &EvalSurface, q: [f64; 2]) -> Vec3 {
kernel::record_vec3(s.derivs(q[0], q[1]).unwrap().0)
}
/// A quarter turn of that cylinder, u in [0, `top`] and v in [0, pi/2], as a loop
/// counter-clockwise in (u, v): its straight sides (u = 0 to `top` at v = 0 and v = pi/2) one
/// segment each, its arcs (u = 0 and u = `top`) `n` segments.
fn quarter(s: &EvalSurface, n: u32, top: f64) -> seam::Loop {
let quarter = std::f64::consts::FRAC_PI_2;
let at = |k: u32| quarter * f64::from(k) / f64::from(n);
let mut uv: Vec<[f64; 2]> = vec![[0.0, 0.0]];
uv.extend((0..=n).map(|k| [top, at(k)]));
// Down the arc at u = 0, from v = pi/2 to just above the start.
uv.extend((1..=n).rev().map(|k| [0.0, at(k)]));
let pts = uv.iter().map(|q| point(s, *q)).collect();
seam::Loop { uv, pts }
}
/// The face on [`cylinder_without`]`(0.5)`: whatever layout meshes it puts nodes or middles on
/// the circle u = 0.5. The face is refused with the point, not meshed around it, and not filled
/// leniently: no layout meshes a surface that is not there.
#[test]
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);
match r {
Err(e) => assert!(
e.to_string()
.contains("the surface does not evaluate at [0.5"),
"{e}"
),
Ok((fp, stats)) => {
panic!(
"meshed with {} triangles, lenient {:?}",
fp.triangles.len(),
stats.lenient
)
}
}
}
/// On [`cylinder_without`]`(0.25)` the straight direction needs no line inside [0, 1] (the
/// profile is straight), so no node lies on u = 0.25, and the sag check looks at middles
/// (u = 0.5) and centroids (u = 1/3, 2/3) only. The size rules' probes between the lines (the
/// quarter points of an interval, and the iso-lines across it) do reach u = 0.25: there the
/// surface refuses, and the division names the point instead of taking the interval as fine.
#[test]
fn the_division_names_a_point_between_its_lines_the_surface_does_not_evaluate_at() {
let s = cylinder_without(0.25);
let rules = Rules::new(0.01, 40.0, None);
let quarter = (0.0, std::f64::consts::FRAC_PI_2);
assert!(matches!(
uv::divide(&s, 0, (0.0, 1.0), quarter, &rules, 50.0, 1000, &uv::everywhere),
Err(uv::Unmeasured::NoValue(NoValue([u, _]))) if u == 0.25
));
assert!(matches!(
uv::divide(&s, 1, quarter, (0.0, 1.0), &rules, 50.0, 1000, &uv::everywhere),
Err(uv::Unmeasured::NoValue(NoValue([u, _]))) if u == 0.25
));
assert_eq!(
uv::fine_between(&s, &rules, 50.0, [0.0, 0.1], [0.5, 0.1]),
Err(NoValue([0.25, 0.1]))
);
// Off the circle the rules hold: the profile is straight, and 40 mm is within 50.
assert_eq!(
uv::fine_between(&s, &rules, 50.0, [0.5, 0.1], [1.0, 0.1]),
Ok(true)
);
}
/// The face on [`cylinder_without`]`(0.25)`, its arcs fine enough (16 segments on r = 3) that no
/// side across the face sags past the tolerance: its nodes stay off u = 0.25 and so do its
/// triangles' middles (see above). Taking a point the surface does not evaluate at as fine, the
/// planner meshed it as a strip of 32 triangles straight over the circle the surface does not
/// have. The size rules probe u = 0.25, and the face is refused there.
#[test]
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);
match r {
Err(e) => assert!(
e.to_string()
.contains("the surface does not evaluate at [0.25"),
"{e}"
),
Ok((fp, stats)) => {
panic!(
"meshed with {} triangles, lenient {:?}",
fp.triangles.len(),
stats.lenient
)
}
}
}
/// The Delaunay sag check evaluates every triangle's corners and middle: a surface that refuses
/// a middle stops it with that point, instead of taking the triangle as on the surface.
#[test]
fn the_delaunay_sag_check_names_a_middle_the_surface_does_not_evaluate_at() {
use super::cdt::{Fill, Sag, triangulate};
let b = [[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]];
// A plane with no point inside the open square.
let eval = |u: f64, v: f64| {
let inside = u > 0.0 && u < 1.0 && v > 0.0 && v < 1.0;
if inside {
Err(NoValue([u, v]))
} else {
Ok((Vec3::new(u, v, 0.0), Some(Vec3::Z)))
}
};
let sag = Sag {
eval: &eval,
tol: 1e-3,
h: 10.0,
turn: None,
scale: [1.0, 1.0],
};
let fill = Fill {
seeds: &[],
refine: Some((sag, 100)),
shape: false,
lenient: false,
};
match triangulate(&b, std::slice::from_ref(&(0..4)), fill) {
Err(NoValue([u, v])) => assert!(u > 0.0 && u < 1.0 && v > 0.0 && v < 1.0, "{u} {v}"),
other => panic!("{other:?}"),
}
}
/// The size samples cover the surface's whole parameter box, which reaches past a trimmed face.
/// On [`cylinder_without`]`(0.5)` the samples on u = 0.5 (the box's middle row) do not
/// evaluate: they measure nothing, and they refuse only a face they lie inside. A face trimmed
/// to u in [0, 0.25] meshes; the whole face, over u = 0.5, is refused at such a sample.
#[test]
fn a_size_sample_the_surface_refuses_refuses_only_a_face_it_lies_in() {
let s = cylinder_without(0.5);
let rules = Rules::new(0.01, 40.0, None);
let size = face_size(&s, &solidcore::eval::uv::domain(&s), &rules);
assert!(!size.unevaluated.is_empty(), "{size:?}");
assert!(
size.unevaluated.iter().all(|q| q[0] == 0.5),
"{:?}",
size.unevaluated
);
// 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) {
Ok((fp, stats)) => assert!(
!fp.triangles.is_empty() && stats.lenient.is_empty(),
"{stats:?}"
),
Err(e) => panic!("{e}"),
}
// The face over it: refused at the first sample inside it, before any layout.
let whole = [quarter(&s, 16, 1.0)];
let inside = size
.unevaluated
.iter()
.find(|q| q[1] <= std::f64::consts::FRAC_PI_2)
.copied();
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) {
Err(e) => assert!(
e.to_string()
.contains(&format!("does not evaluate at {inside:?}")),
"{e}"
),
Ok((fp, _)) => panic!("meshed with {} triangles", fp.triangles.len()),
}
}
/// The plane z = 0, x = u, y = v over [0, 1]², as a rational bicubic whose weight
/// (u − ½)² + (v − ½)² has its one root at (½, ½), where its numerators vanish too: the
/// evaluator has no point there (0 / 0) and the plane's own everywhere else. All its homogeneous
/// coordinates are whole numbers (36 times the weight's and numerators' Bernstein
/// coefficients), so the root is exactly zero.
fn plane_without_its_middle() -> EvalSurface {
// 36 (t − ½)², 36 t (t − ½)², 3 t and 12 (t − ½)² in the cubic Bernstein basis.
let a36 = [9.0, -3.0, -3.0, 9.0];
let ta36 = [0.0, 3.0, -6.0, 9.0];
let t3 = [0.0, 1.0, 2.0, 3.0];
let a12 = [3.0, -1.0, -1.0, 3.0];
let mut ctrl = Vec::new();
for j in 0..4 {
for i in 0..4 {
// w u = u (u − ½)² + u (v − ½)², w v = (u − ½)² v + v (v − ½)².
let x = ta36[i] + t3[i] * a12[j];
let y = a12[i] * t3[j] + ta36[j];
ctrl.push([x, y, 0.0, a36[i] + a36[j]]);
}
}
let knots = vec![0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0];
let s = EvalSurface::nurbs([3, 3], [knots.clone(), knots], [4, 4], ctrl);
assert!(s.derivs(0.5, 0.5).is_none(), "no point at the middle");
for q in [[0.3, 0.7], [0.5, 0.25], [0.9, 0.5], [0.0, 1.0]] {
let p = point(&s, q);
assert!(p.dist(Vec3::new(q[0], q[1], 0.0)) < 1e-12, "{q:?}: {p:?}");
}
s
}
/// A loop through the corners `corners` (counter-clockwise in (u, v)), each side in pieces of
/// at most 0.1, on the surface `s`.
fn polygon(s: &EvalSurface, corners: &[[f64; 2]]) -> seam::Loop {
let mut uv = Vec::new();
for (k, a) in corners.iter().enumerate() {
let b = corners[(k + 1) % corners.len()];
let n = ((b[0] - a[0]).hypot(b[1] - a[1]) / 0.1).ceil().max(1.0) as u32;
uv.extend((0..n).map(|i| {
let t = f64::from(i) / f64::from(n);
[a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t]
}));
}
let pts = uv.iter().map(|q| point(s, *q)).collect();
seam::Loop { uv, pts }
}
/// The face's divisions run iso-lines across its whole parameter box, which reaches past a
/// trimmed face. On [`plane_without_its_middle`], a C-shaped face whose notch holds the middle
/// (½, ½) has that point in its box but not in itself: the point measures nothing, and the face
/// meshes (the division refused it there). A square face over the middle is refused at it.
#[test]
fn a_point_in_a_faces_notch_the_surface_does_not_evaluate_at_does_not_refuse_it() {
let s = plane_without_its_middle();
let rules = Rules::new(0.01, 1.0, None);
let size = face_size(&s, &solidcore::eval::uv::domain(&s), &rules);
let notched = [polygon(
&s,
&[
[0.0, 0.0],
[1.0, 0.0],
[1.0, 0.4],
[0.4, 0.4],
[0.4, 0.6],
[1.0, 0.6],
[1.0, 1.0],
[0.0, 1.0],
],
)];
match plan_or_fill(0, "spline", &s, &notched, &rules, &size, 0.0, None) {
Ok((fp, stats)) => assert!(
!fp.triangles.is_empty() && stats.lenient.is_empty(),
"{stats:?}"
),
Err(e) => panic!("{e}"),
}
let square = [polygon(
&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) {
Err(e) => assert!(
e.to_string()
.contains("the surface does not evaluate at [0.5, 0.5]"),
"{e}"
),
Ok((fp, _)) => panic!("meshed with {} triangles", fp.triangles.len()),
}
}
/// A plan whose face has a vertex where the kernel's evaluator has no point (on
/// [`cylinder_without`]`(0.5)`, at u = 0.5): the evaluation refuses that face whole, naming the
/// vertex, and keeps the other face. Nothing stands in for the point.
#[test]
fn evaluating_a_vertex_the_surface_has_no_point_at_refuses_its_face() {
use crate::{FacePlan, RefusedFace, TessPlan};
let s = cylinder_without(0.5);
let face = |face: u32, middle: [f64; 2]| FacePlan {
face,
surface: Some(0),
uv: vec![[0.0, 0.0], [1.0, 0.0], middle],
triangles: vec![[0, 1, 2]],
..FacePlan::default()
};
let plan = TessPlan {
surfaces: vec![s],
faces: vec![face(0, [0.25, 1.0]), face(1, [0.5, 1.0])],
..TessPlan::default()
};
let (m, refused) = plan.evaluate();
assert_eq!(m.tri_face, vec![0]);
assert_eq!(m.positions.len(), 3);
assert_eq!(refused.len(), 1, "{refused:?}");
let RefusedFace { face, reason } = &refused[0];
assert_eq!(*face, 1);
assert!(reason.starts_with("vertex 2 at [0.5, 1.0]: "), "{reason}");
}
/// The flips' check of a side evaluates the surface at the middles of the triangles either side
/// of it: one the surface has no point at (on [`cylinder_without`]`(0.5)`, a kite of four nodes
/// off u = 0.5, one of whose triangles has its middle on it) stops the flips with that point,
/// instead of taking the side as fine.
#[test]
fn the_flips_name_a_middle_the_surface_does_not_evaluate_at() {
let s = cylinder_without(0.5);
// The middle of the first triangle (its corners' mean) is at u = 0.5.
let uv = vec![[0.0, 0.0], [0.75, -0.5], [0.75, 0.5], [0.0, 1.0]];
let fixed: Vec<Vec3> = uv.iter().map(|q| point(&s, *q)).collect();
let mut tris = vec![[0, 1, 2], [0, 2, 3]];
match super::sag::flip(
&s,
0.01,
&mut super::sag::Poles::new(&s, 1e-9),
&fixed,
&uv,
&mut tris,
) {
Err(super::sag::Stop::NoValue(NoValue(q))) => assert_eq!(q[0], 0.5, "{q:?}"),
other => panic!("{other:?}, flipped to {tris:?}"),
}
}
/// The parabolic cylinder z = u² (x = u, y = v) over [0, 1]², as a rational bicubic-by-quartic
/// whose weight (u − ½)² + (v − ½)² has its one root at (½, ½), where its numerators vanish too:
/// no point there (0 / 0), the parabola's own everywhere else. Its homogeneous coordinates are
/// 72 times the Bernstein coefficients, all whole numbers, so the root is exactly zero.
fn parabola_without_its_middle() -> EvalSurface {
let ctrl: Vec<[f64; 4]> = [
[0, 0, 0, 36],
[9, 0, 0, 18],
[6, 0, 6, 12],
[9, 0, 0, 18],
[36, 0, 36, 36],
[0, 12, 0, 12],
[3, 6, 0, -6],
[-6, 4, 2, -12],
[-9, 6, -12, -6],
[12, 12, 12, 12],
[0, 0, 0, 12],
[3, -12, 0, -6],
[-6, -16, 2, -12],
[-9, -12, -12, -6],
[12, 0, 12, 12],
[0, 36, 0, 36],
[9, 18, 0, 18],
[6, 12, 6, 12],
[9, 18, 0, 18],
[36, 36, 36, 36],
]
.iter()
.map(|c| c.map(f64::from))
.collect();
let s = EvalSurface::nurbs(
[4, 3],
[
vec![0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0],
vec![0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0],
],
[5, 4],
ctrl,
);
assert!(s.derivs(0.5, 0.5).is_none(), "no point at the middle");
for q in [[0.3, 0.7], [0.5, 0.25], [0.9, 0.5], [0.0, 1.0], [0.53, 0.5]] {
let p = point(&s, q);
assert!(
p.dist(Vec3::new(q[0], q[1], q[0] * q[0])) < 1e-12,
"{q:?}: {p:?}"
);
}
s
}
/// A division whose iso-line runs through the middle (½, ½) of [`parabola_without_its_middle`],
/// which is not the face's (`mine` leaves out a disc of radius 0.05 round it): each interval with
/// an end there is measured from the nearest point inwards that is the face's, so the parabola
/// is divided to the tolerance on either side. The interval was passed over, and the division
/// stopped at [0, ½, 1], its halves sagging about 0.03 against a tolerance of 0.01.
#[test]
fn an_interval_ending_at_a_point_outside_the_face_is_measured_from_inside_it() {
let s = parabola_without_its_middle();
let rules = Rules::new(0.01, 1.0, None);
let outside = |q: [f64; 2]| (q[0] - 0.5).hypot(q[1] - 0.5) < 0.05;
let mine = |q: [f64; 2]| Ok(!outside(q));
let Ok(lines) = uv::divide(&s, 0, (0.0, 1.0), (0.5, 0.5), &rules, 10.0, 1000, &mine) else {
panic!("divides")
};
assert!(lines.len() >= 6, "{lines:?}");
// Every interval clear of the left-out disc sags at most the tolerance from its chord.
let on = |u: f64| Vec3::new(u, 0.5, u * u);
for w in lines.windows(2) {
let (a, b) = (w[0], w[1]);
if b <= 0.45 || a >= 0.55 {
let sag = (1..16)
.map(|k| on(a + (b - a) * f64::from(k) / 16.0).dist_to_segment(on(a), on(b)))
.fold(0.0, f64::max);
assert!(
sag <= 0.01 * (1.0 + 1e-9),
"[{a}, {b}] sags {sag}: {lines:?}"
);
}
}
}
/// A strip of [`cylinder_without`]`(0.5)` running 5000 turns round it (u in [0, 1]): the
/// division meets the circle u = 0.5 the surface has no point on, and the strip's loops span
/// more periods than a point is looked for in, so whether the point is the face's cannot be told.
/// The face is refused for that, as a size sample is, not as though the point were inside it.
#[test]
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) {
Err(e) => assert!(e.to_string().contains("periods, more than the 4096"), "{e}"),
Ok((fp, _)) => panic!("meshed with {} triangles", fp.triangles.len()),
}
}
/// A strip of `s` from u = 0 to 1 running 5000 turns round v, in steps of a quarter turn so it
/// does not wind.
fn long_strip(s: &EvalSurface) -> seam::Loop {
let tau = std::f64::consts::TAU;
let (turns, steps) = (5000.0, 20_000u32);
let up = (0..steps).map(|k| [1.0, turns * tau * f64::from(k) / f64::from(steps)]);
let down = (0..steps).map(|k| [0.0, turns * tau * f64::from(steps - k) / f64::from(steps)]);
let uv: Vec<[f64; 2]> = std::iter::once([0.0, 0.0]).chain(up).chain(down).collect();
let pts = uv.iter().map(|q| point(s, *q)).collect();
let l = seam::Loop { uv, pts };
assert!(!seam::winds(std::slice::from_ref(&l), 1, tau));
l
}
/// The same strip, with a size sample the surface does not evaluate at: whether the sample lies
/// in the face cannot be told past 4096 periods, and the face is refused for that, naming the
/// sample, before any layout.
#[test]
fn a_size_sample_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);
let size = FaceSize {
unevaluated: vec![[0.5, 0.3]],
..size(1.0)
};
match plan_or_fill(0, "revolution", &s, &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"
),
"{e}"
),
Ok((fp, _)) => panic!("meshed with {} triangles", fp.triangles.len()),
}
}
/// The division along v of [`cylinder_without`]`(0.5)` (iso-lines across u, one at u = 0.5,
/// where the surface has no point), at a tolerance that leaves v in [0, 1] one interval, for a
/// face that leaves out v <= 0.05 and v >= 0.95: both ends of the interval at u = 0.5 are not
/// the face's, and the end at v = 0 steps inwards to v = 1/16, which is the face's and does not
/// evaluate. That is the face's refusal, naming the point; the steps were passed over, the
/// iso-line skipped, and the division never met the circle.
#[test]
fn a_step_inwards_the_surface_has_no_point_at_refuses_the_face() {
let s = cylinder_without(0.5);
let rules = Rules::new(1.0, 40.0, None);
let mine = |q: [f64; 2]| Ok(q[1] > 0.05 && q[1] < 0.95);
match uv::divide(&s, 1, (0.0, 1.0), (0.0, 1.0), &rules, 50.0, 1000, &mine) {
Err(uv::Unmeasured::NoValue(NoValue(q))) => assert_eq!(q, [0.5, 0.0625]),
other => panic!("{other:?}"),
}
}
/// The sag a capped face reaches is measured where the surface evaluates: a triangle on
/// [`cylinder_without`]`(0.5)` whose middle lies on the circle the surface has no point on is
/// measured at its sides' middles only, and the face is not refused for a measure it never
/// needed (it was refused there); the middle is counted as unmeasured (it was dropped, and a
/// face over such a point could read as within its tolerance).
#[test]
fn the_worst_sag_is_measured_where_the_surface_evaluates() {
let s = cylinder_without(0.5);
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),
&[],
&uv,
&[[0, 1, 2]],
);
// Measured at its three sides' middles; its middle, unmeasured, is counted, so the face is
// never taken as within its tolerance over it.
assert!(
got.as_ref()
.is_ok_and(|(w, unmeasured)| w.is_finite() && *w > 0.0 && *unmeasured == 1),
"{got:?}"
);
}
/// A face whose surface's poles the kernel's classifier cannot tell: the plane x = u, y = v as a
/// bilinear net over u in [0, 2], whose last column (the span u in [1, 2], away from the face)
/// is not a number. The face, u in [0, 0.5], evaluates everywhere, but no point of it can be
/// classified (the net is not finite along u), so it is refused, named, with the classifier's
/// own reason; nothing takes "not a pole" for it.
#[test]
fn a_face_whose_poles_cannot_be_told_is_refused_with_the_classifiers_reason() {
let nan = f64::NAN;
let s = EvalSurface::nurbs(
[1, 1],
[vec![0.0, 0.0, 1.0, 2.0, 2.0], vec![0.0, 0.0, 1.0, 1.0]],
[3, 2],
vec![
[0.0, 0.0, 0.0, 1.0],
[1.0, 0.0, 0.0, 1.0],
[nan, 0.0, 0.0, 1.0],
[0.0, 1.0, 0.0, 1.0],
[1.0, 1.0, 0.0, 1.0],
[nan, 1.0, 0.0, 1.0],
],
);
let uv: Vec<[f64; 2]> = vec![[0.0, 0.0], [0.5, 0.0], [0.5, 1.0], [0.0, 1.0]];
let pts: Vec<Vec3> = uv.iter().map(|q| point(&s, *q)).collect();
let loops = [seam::Loop { uv, pts }];
let rules = Rules::new(0.01, 2.0, None);
let size = FaceSize {
h: 1.0,
speed: [1.0, 1.0],
unevaluated: Vec::new(),
};
match plan_or_fill(3, "spline", &s, &loops, &rules, &size, 0.0, None) {
Err(e) => {
let why = e.to_string();
assert!(
why.contains("face 3 (spline): the surface's poles cannot be told at [0.0, 0.0]: pole classification: a control point is not finite"),
"{why}"
);
}
Ok((fp, _)) => panic!("meshed with {} triangles", fp.triangles.len()),
}
}
+471 -134
View File
@@ -7,94 +7,127 @@
//! conforming. Sides on the face's boundary are never split: they are the edges' own nodes,
//! which the neighbouring face shares.
use crate::vec::VecExt as _;
use solidcore_convert as kernel;
use std::collections::HashMap;
use crate::fast::EvalSurface;
use crate::vec::VecExt as _;
use solidcore::eval::EvalSurface;
use solidcore_record::Vec3;
use super::{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
/// 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`]).
/// than `tol` from the surface `s`, adding at most `max_points` points to `uv`. `poles`: the
/// classifier of the face's nodes, shared by [`refine`], [`flip`] and [`worst`] (how near a pole a
/// point is taken as one: see [`solidcore::eval::uv::degenerate_within`]).
///
/// A point the surface does not evaluate at (a node, or a middle the check needs) is the
/// evaluator refusing the face: the error names it, and the caller leaves the face out whole.
/// `Ok(true)` when the refinement stopped at `max_points` with a triangle still to split: the
/// caller names the face ([`super::Cap::Refine`]).
/// Why [`refine`], [`flip`] or [`worst`] stopped: a point the surface does not evaluate at, a
/// point whose poles the kernel's classifier cannot tell, or (for [`worst`]) a triangle, by
/// index, with a corner past the nodes.
#[derive(Clone, Debug, PartialEq)]
pub(crate) enum Stop {
NoValue(NoValue),
Unclassified(Unclassified),
Corner(usize),
}
impl From<NoValue> for Stop {
fn from(e: NoValue) -> Self {
Stop::NoValue(e)
}
}
impl From<Unclassified> for Stop {
fn from(e: Unclassified) -> Self {
Stop::Unclassified(e)
}
}
pub(crate) fn refine(
s: &EvalSurface,
tol: f64,
reach: f64,
poles: &mut Poles,
fixed: &[Vec3],
uv: &mut Vec<[f64; 2]>,
tris: &mut Vec<[u32; 3]>,
max_points: usize,
) {
) -> Result<bool, Stop> {
if !(tol.is_finite() && tol > 0.0) || tris.is_empty() {
return;
return Ok(false);
}
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);
let at = |q: [f64; 2]| {
s.derivs(q[0], q[1])
.map(|d| kernel::record_vec3(d.0))
.ok_or(NoValue(q))
};
// 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
// counts. Where the normal is undefined (a pole), the whole distance.
let off = |q: [f64; 2], c: Vec3| match s.derivs(q[0], q[1]) {
Some((p, su, sv)) => su
let off = |q: [f64; 2], c: Vec3| -> Result<f64, NoValue> {
let (p, su, sv) = s
.derivs(q[0], q[1])
.map(kernel::record_triple)
.ok_or(NoValue(q))?;
Ok(su
.cross(sv)
.normalized()
.map_or_else(|| p.dist(c), |n| (p - c).dot(n).abs()),
None => at(q).dist(c),
.map_or_else(|| p.dist(c), |n| (p - c).dot(n).abs()))
};
let mut pos: Vec<Vec3> = uv
let mut pos = uv
.iter()
.enumerate()
.map(|(i, q)| fixed.get(i).copied().unwrap_or_else(|| at(*q)))
.collect();
.map(|(i, q)| fixed.get(i).copied().map_or_else(|| at(*q), Ok))
.collect::<Result<Vec<Vec3>, NoValue>>()?;
// No side is split below this length: a surface whose parameters bend sharply (at a pole) is
// not chased to the last bit.
let shortest = tol * 1e-3;
let mut sides: HashMap<(u32, u32), Vec<u32>> = HashMap::with_capacity(tris.len() * 2);
let mut sides: HashMap<(u32, u32), Vec<u32>> = HashMap::new();
for (ti, t) in tris.iter().enumerate() {
for k in 0..3 {
sides
.entry(key(t[k], t[(k + 1) % 3]))
.or_default()
.push(ti as u32);
for [a, b, _] in turns(*t) {
// Proven: fewer triangles than twice MAX_FACE_NODES, as above: the index fits a u32.
sides.entry(key(a, b)).or_default().push(ti as u32);
}
}
let mut sag_of: HashMap<(u32, u32), f64> = HashMap::new();
let limit = uv.len().saturating_add(max_points);
// Proven: fewer triangles than twice MAX_FACE_NODES (2^28): the count fits a u32.
let mut queue: Vec<u32> = (0..tris.len() as u32).rev().collect();
while let Some(ti) = queue.pop() {
if uv.len() >= limit {
break;
}
let Some(&t) = tris.get(ti as usize) else {
continue;
};
let p = |i: u32| pos.get(i as usize).copied();
let q = |i: u32| uv.get(i as usize).copied();
let (Some(pa), Some(pb), Some(pc), Some(qa), Some(qb), Some(qc)) =
(p(t[0]), p(t[1]), p(t[2]), q(t[0]), q(t[1]), q(t[2]))
else {
let (Some(pa), Some(pb), Some(pc)) = (p(t[0]), p(t[1]), p(t[2])) else {
continue;
};
// Sides inside the face (two triangles on them), long enough to split, with their sag.
// Proven: the capacity is the constant 3.
let mut inner: Vec<(f64, f64, (u32, u32))> = Vec::with_capacity(3);
for k in 0..3 {
let (a, b) = (t[k], t[(k + 1) % 3]);
for [a, b, _] in turns(t) {
let e = key(a, b);
if sides.get(&e).is_none_or(|ts| ts.len() != 2) {
continue;
}
let (Some(xa), Some(xb), Some(ua), Some(ub)) = (p(a), p(b), q(a), q(b)) else {
let (Some(xa), Some(xb)) = (p(a), p(b)) else {
continue;
};
let len = xa.dist(xb);
if len <= shortest {
continue;
}
let sag = *sag_of
.entry(e)
.or_insert_with(|| off(mean(&[ua, ub]), (xa + xb) * 0.5));
let sag = if let Some(&sag) = sag_of.get(&e) {
sag
} else {
let sag = off(poles.mean(uv, &[a, b])?, (xa + xb) * 0.5)?;
sag_of.insert(e, sag);
sag
};
inner.push((sag, len, e));
}
// The surface's departure from a flat triangle is about quadratic over it: at the
@@ -102,12 +135,12 @@ pub(crate) fn refine(
// 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(&[qa, qb, qc]);
let centroid = poles.mean(uv, &t)?;
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,
Some((sag, _, e))
if sag > 0.25 * tol && off(centroid, (pa + pb + pc) * (1.0 / 3.0)) > tol =>
if sag > 0.25 * tol && off(centroid, (pa + pb + pc) * (1.0 / 3.0))? > tol =>
{
e
}
@@ -138,37 +171,171 @@ pub(crate) fn refine(
if from != ti {
queue.push(ti);
}
let (Some(ua), Some(ub)) = (q(split.0), q(split.1)) else {
continue;
};
let m = mean(&[ua, ub]);
// A split is wanted and there is no room for its point.
if uv.len() >= limit {
return Ok(true);
}
let m = poles.mean(uv, &[split.0, split.1])?;
let pm = at(m)?;
// Proven: `plan_face` checks its nodes and the MAX_REFINE more it lets this add are at most
// MAX_FACE_NODES, so the index fits a u32.
let mi = uv.len() as u32;
uv.push(m);
pos.push(at(m));
pos.push(pm);
sag_of.remove(&split);
queue.extend(split_side(tris, &mut sides, split, mi));
}
Ok(false)
}
/// 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, 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_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;
/// Which parameter directions the surface is collapsed along at each node of a face's
/// 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,
reach: f64,
/// Per node index; `None` until asked.
flags: Vec<Option<[bool; 2]>>,
/// Calls of the kernel's classifier made, for the tests.
#[cfg(test)]
asked: usize,
}
impl<'a> Poles<'a> {
pub(crate) fn new(s: &'a EvalSurface, reach: f64) -> Self {
Poles {
s,
reach,
flags: Vec::new(),
#[cfg(test)]
asked: 0,
}
}
out
/// Whether each direction is collapsed at node `i`, whose parameters are `q`.
fn at(&mut self, i: u32, q: [f64; 2]) -> Result<[bool; 2], Unclassified> {
let i = i as usize;
if let Some(Some(f)) = self.flags.get(i) {
return Ok(*f);
}
#[cfg(test)]
{
self.asked += 2;
}
let f = [
super::pole_at(self.s, q, 0, self.reach)?,
super::pole_at(self.s, q, 1, self.reach)?,
];
if self.flags.len() <= i {
self.flags.resize(i + 1, None);
}
if let Some(slot) = self.flags.get_mut(i) {
*slot = Some(f);
}
Ok(f)
}
/// The mean of nodes' parameters, 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 nodes alone. A node whose poles the kernel's
/// classifier cannot tell is the error.
fn mean(&mut self, uv: &[[f64; 2]], nodes: &[u32]) -> Result<[f64; 2], Unclassified> {
let mut qs: Vec<([f64; 2], [bool; 2])> = Vec::new();
for &i in nodes {
if let Some(&q) = uv.get(i as usize) {
qs.push((q, self.at(i, q)?));
}
}
let mut out = [0.0; 2];
// Proven: `d` < 2 enumerates `out` and indexes the `[f64; 2]` parameters and flags.
#[allow(clippy::indexing_slicing)]
for (d, slot) in out.iter_mut().enumerate() {
let free: Vec<f64> = qs
.iter()
.filter(|(_, f)| !f[d])
.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;
}
Ok(out)
}
}
/// The farthest the triangles `tris` (as for [`refine`]) lie from the surface, measured as
/// [`refine`] measures them: at each triangle's middle and at the middles of its three sides
/// (boundary sides too: they are the mesh's), each along the surface's normal at the mean of
/// its parameters (the whole distance where the normal is undefined). Only where the surface
/// evaluates: a middle it has no point at is not measured, and counted (the face's vertices and
/// the middles [`refine`] needed all evaluated). The sag and that count; the error: a triangle
/// (by index) with a corner past `uv` ([`Stop::Corner`]), or a point whose poles the kernel's
/// classifier cannot tell.
pub(crate) fn worst(
s: &EvalSurface,
poles: &mut Poles,
fixed: &[Vec3],
uv: &[[f64; 2]],
tris: &[[u32; 3]],
) -> Result<(f64, usize), Stop> {
let pos: Vec<Option<Vec3>> = uv
.iter()
.enumerate()
.map(|(i, q)| {
fixed
.get(i)
.copied()
.or_else(|| s.derivs(q[0], q[1]).map(|d| kernel::record_vec3(d.0)))
})
.collect();
// How far the flat point `c` lies from the surface at the mean of `qs`, as `refine`'s `off`.
let mut off = |nodes: &[u32], c: Vec3| -> Result<Option<f64>, Unclassified> {
let m = poles.mean(uv, nodes)?;
let Some((x, su, sv)) = s.derivs(m[0], m[1]).map(kernel::record_triple) else {
return Ok(None);
};
Ok(Some(
su.cross(sv)
.normalized()
.map_or_else(|| x.dist(c), |n| (x - c).dot(n).abs()),
))
};
let (mut worst, mut unmeasured) = (0.0f64, 0usize);
for (k, t) in tris.iter().enumerate() {
let corner = |i: u32| Some((*uv.get(i as usize)?, *pos.get(i as usize)?));
let (Some((_, pa)), Some((_, pb)), Some((_, pc))) =
(corner(t[0]), corner(t[1]), corner(t[2]))
else {
return Err(Stop::Corner(k));
};
// The middle, then each side's: `None` where the surface has no point there (or at a
// corner they need).
let mut measured: Vec<Option<f64>> = Vec::new();
measured.push(match (pa, pb, pc) {
(Some(a), Some(b), Some(c)) => off(t, (a + b + c) * (1.0 / 3.0))?,
_ => None,
});
for ((nx, px), (ny, py)) in [
((t[0], pa), (t[1], pb)),
((t[1], pb), (t[2], pc)),
((t[2], pc), (t[0], pa)),
] {
measured.push(match (px, py) {
(Some(x), Some(y)) => off(&[nx, ny], (x + y) * 0.5)?,
_ => None,
});
}
for m in measured {
match m {
Some(x) => worst = worst.max(x),
None => unmeasured += 1,
}
}
}
Ok((worst, unmeasured))
}
/// The longest side of triangle `t` when the angle opposite it is past `degrees` and the side lies
@@ -181,17 +348,27 @@ fn flat_long_side(
degrees: f64,
) -> Option<(u32, u32)> {
let p = |i: u32| pos.get(i as usize).copied();
let len = |k: usize| p(t[k]).zip(p(t[(k + 1) % 3])).map(|(a, b)| a.dist(b));
let lengths = [len(0)?, len(1)?, len(2)?];
let k = (0..3).max_by(|&i, &j| lengths[i].total_cmp(&lengths[j]))?;
let (l, a, b) = (lengths[k], lengths[(k + 1) % 3], lengths[(k + 2) % 3]);
let side = key(t[k], t[(k + 1) % 3]);
let len = |[a, b, _]: [u32; 3]| p(a).zip(p(b)).map(|(a, b)| a.dist(b));
let [t0, t1, t2] = turns(*t);
let lengths = [len(t0)?, len(t1)?, len(t2)?];
// The side from each corner to the next, with the two after it; the last longest one.
let ([l, a, b], [ta, tb, _]) = turns(lengths)
.into_iter()
.zip(turns(*t))
.max_by(|x, y| x.0[0].total_cmp(&y.0[0]))?;
let side = key(ta, tb);
let inside = sides.get(&side).is_some_and(|ts| ts.len() == 2) && l > shortest;
let flat =
a > 0.0 && b > 0.0 && (a * a + b * b - l * l) / (2.0 * a * b) < degrees.to_radians().cos();
(inside && flat).then_some(side)
}
/// The corners of a triangle turned so each comes first in turn, keeping its winding:
/// `[a, b, c]`, `[b, c, a]`, `[c, a, b]`.
fn turns<T: Copy>([a, b, c]: [T; 3]) -> [[T; 3]; 3] {
[[a, b, c], [b, c, a], [c, a, b]]
}
fn key(a: u32, b: u32) -> (u32, u32) {
(a.min(b), a.max(b))
}
@@ -214,16 +391,18 @@ fn split_side(
m: u32,
) -> Vec<u32> {
let on = sides.remove(&e).unwrap_or_default();
// Proven: the capacity is the constant 4.
let mut touched = Vec::with_capacity(4);
for ti in on {
let Some(&t) = tris.get(ti as usize) else {
continue;
};
// Turn the triangle so the side comes first, as it winds: [a, b, c].
let Some(k) = (0..3).find(|&k| key(t[k], t[(k + 1) % 3]) == e) else {
let Some([a, b, c]) = turns(t).into_iter().find(|&[a, b, _]| key(a, b) == e) else {
continue;
};
let (a, b, c) = (t[k], t[(k + 1) % 3], t[(k + 2) % 3]);
// Proven: a triangulation of at most MAX_FACE_NODES (2^28) nodes has fewer than twice as
// many triangles, so the index fits a u32.
let new = tris.len() as u32;
if let Some(slot) = tris.get_mut(ti as usize) {
*slot = [a, m, c];
@@ -251,38 +430,42 @@ const MIN_FLIP_ANGLE: f64 = 1e-3;
/// them down. A side is flipped only when both new triangles keep the orientation of the old ones
/// in (u, v) and against the surface normal (nothing folds), and the new side and the new
/// triangles' middles stay within `tol` of the surface, or no farther than the old side's middle.
/// Sides with one triangle (the face's boundary) are never flipped.
/// Sides with one triangle (the face's boundary) are never flipped. A point the surface does not
/// evaluate at is the evaluator refusing the face, as in [`refine`].
pub(crate) fn flip(
s: &EvalSurface,
tol: f64,
reach: f64,
poles: &mut Poles,
fixed: &[Vec3],
uv: &[[f64; 2]],
tris: &mut [[u32; 3]],
) {
) -> Result<(), Stop> {
if !(tol.is_finite() && tol > 0.0) || tris.len() < 2 {
return;
return Ok(());
}
let dom = super::uv::domain(s);
let mean = |qs: &[[f64; 2]]| mean(s, &dom, reach, qs);
let pos: Vec<Vec3> = uv
let derivs = |q: [f64; 2]| {
s.derivs(q[0], q[1])
.map(kernel::record_triple)
.ok_or(NoValue(q))
};
let pos = uv
.iter()
.enumerate()
.map(|(i, q)| {
fixed
.get(i)
.copied()
.unwrap_or_else(|| s.eval(q[0], q[1]).0)
.map_or_else(|| derivs(*q).map(|d| d.0), Ok)
})
.collect();
.collect::<Result<Vec<Vec3>, NoValue>>()?;
let p = |i: u32| pos.get(i as usize).copied();
let q = |i: u32| uv.get(i as usize).copied();
let off = |m: [f64; 2], c: Vec3| match s.derivs(m[0], m[1]) {
Some((x, su, sv)) => su
let off = |m: [f64; 2], c: Vec3| -> Result<f64, NoValue> {
let (x, su, sv) = derivs(m)?;
Ok(su
.cross(sv)
.normalized()
.map_or_else(|| x.dist(c), |n| (x - c).dot(n).abs()),
None => s.eval(m[0], m[1]).0.dist(c),
.map_or_else(|| x.dist(c), |n| (x - c).dot(n).abs()))
};
// Twice the signed area in (u, v).
let area_uv = |t: [u32; 3]| match (q(t[0]), q(t[1]), q(t[2])) {
@@ -297,56 +480,46 @@ pub(crate) fn flip(
return 0.0;
};
let angle = |o: Vec3, x: Vec3, y: Vec3| match ((x - o).normalized(), (y - o).normalized()) {
// Proven: the clamp bounds are the constants -1.0 <= 1.0, neither NaN.
(Some(u), Some(v)) => u.dot(v).clamp(-1.0, 1.0).acos(),
_ => 0.0,
};
angle(a, b, c).min(angle(b, c, a)).min(angle(c, a, b))
};
// How far a facet's normal turns from the surface normal at its middle (radians, 0 to pi/2).
let deviation = |t: [u32; 3]| -> f64 {
let (Some(a), Some(b), Some(c), Some(qa), Some(qb), Some(qc)) =
(p(t[0]), p(t[1]), p(t[2]), q(t[0]), q(t[1]), q(t[2]))
else {
return 0.0;
let deviation = |poles: &mut Poles, t: [u32; 3]| -> Result<f64, Stop> {
let (Some(a), Some(b), Some(c)) = (p(t[0]), p(t[1]), p(t[2])) else {
return Ok(0.0);
};
let m = mean(&[qa, qb, qc]);
let m = poles.mean(uv, &t)?;
let facet = (b - a).cross(c - a).normalized();
match (
facet,
s.derivs(m[0], m[1])
.and_then(|(_, su, sv)| su.cross(sv).normalized()),
) {
let (_, su, sv) = derivs(m)?;
Ok(match (facet, su.cross(sv).normalized()) {
// Proven: the clamp bounds are the constants 0.0 <= 1.0, neither NaN.
(Some(f), Some(n)) => f.dot(n).abs().clamp(0.0, 1.0).acos(),
_ => 0.0,
}
})
};
// Whether the facet's normal lies on the side of the surface normal that its (u, v)
// orientation `sign` says.
let upright = |t: [u32; 3], sign: f64| -> bool {
let (Some(a), Some(b), Some(c), Some(qa), Some(qb), Some(qc)) =
(p(t[0]), p(t[1]), p(t[2]), q(t[0]), q(t[1]), q(t[2]))
else {
return false;
let upright = |poles: &mut Poles, t: [u32; 3], sign: f64| -> Result<bool, Stop> {
let (Some(a), Some(b), Some(c)) = (p(t[0]), p(t[1]), p(t[2])) else {
return Ok(false);
};
let m = mean(&[qa, qb, qc]);
match s.derivs(m[0], m[1]) {
Some((_, su, sv)) => (b - a).cross(c - a).dot(su.cross(sv)) * sign > 0.0,
None => true,
let (_, su, sv) = derivs(poles.mean(uv, &t)?)?;
Ok((b - a).cross(c - a).dot(su.cross(sv)) * sign > 0.0)
};
let centroid_off = |poles: &mut Poles, t: [u32; 3]| -> Result<f64, Stop> {
match (p(t[0]), p(t[1]), p(t[2])) {
(Some(a), Some(b), Some(c)) => Ok(off(poles.mean(uv, &t)?, (a + b + c) * (1.0 / 3.0))?),
_ => Ok(f64::INFINITY),
}
};
let centroid_off = |t: [u32; 3]| match (p(t[0]), p(t[1]), p(t[2]), q(t[0]), q(t[1]), q(t[2])) {
(Some(a), Some(b), Some(c), Some(qa), Some(qb), Some(qc)) => {
off(mean(&[qa, qb, qc]), (a + b + c) * (1.0 / 3.0))
}
_ => f64::INFINITY,
};
let mut sides: HashMap<(u32, u32), Vec<u32>> = HashMap::with_capacity(tris.len() * 2);
let mut sides: HashMap<(u32, u32), Vec<u32>> = HashMap::new();
for (ti, t) in tris.iter().enumerate() {
for k in 0..3 {
sides
.entry(key(t[k], t[(k + 1) % 3]))
.or_default()
.push(ti as u32);
for [a, b, _] in turns(*t) {
// Proven: fewer triangles than twice MAX_FACE_NODES, as above: the index fits a u32.
sides.entry(key(a, b)).or_default().push(ti as u32);
}
}
let mut queue: Vec<(u32, u32)> = sides
@@ -362,17 +535,16 @@ pub(crate) fn flip(
if budget == 0 {
break;
}
let Some(&[t1, t2]) = sides.get(&e).map(|v| v.as_slice()) else {
let Some(&[t1, t2]) = sides.get(&e).map(Vec::as_slice) else {
continue;
};
let (Some(&ta), Some(&tb)) = (tris.get(t1 as usize), tris.get(t2 as usize)) else {
continue;
};
// [a, b, c] on one side of a -> b, [b, a, d] on the other.
let Some(k) = (0..3).find(|&k| key(ta[k], ta[(k + 1) % 3]) == e) else {
let Some([a, b, c]) = turns(ta).into_iter().find(|&[a, b, _]| key(a, b) == e) else {
continue;
};
let (a, b, c) = (ta[k], ta[(k + 1) % 3], ta[(k + 2) % 3]);
let Some(d) = tb.iter().copied().find(|&v| v != a && v != b) else {
continue;
};
@@ -391,19 +563,20 @@ pub(crate) fn flip(
// The pair the other way must lie closer along the surface (its facets turn less from
// the surface normal), by more than a degree: a pair within a degree of the surface is
// left as the layout made it.
let mut cached = |ti: u32, t: [u32; 3]| match dev_of.get(ti as usize).copied().flatten() {
Some(d) => d,
None => {
let d = deviation(t);
let mut cached = |poles: &mut Poles, ti: u32, t: [u32; 3]| -> Result<f64, Stop> {
if let Some(d) = dev_of.get(ti as usize).copied().flatten() {
Ok(d)
} else {
let d = deviation(poles, t)?;
if let Some(slot) = dev_of.get_mut(ti as usize) {
*slot = Some(d);
}
d
Ok(d)
}
};
let worse = cached(t1, ta).max(cached(t2, tb));
let worse = cached(poles, t1, ta)?.max(cached(poles, t2, tb)?);
let degree = 1f64.to_radians();
if worse < degree || deviation(n1).max(deviation(n2)) >= worse - degree {
if worse < degree || deviation(poles, n1)?.max(deviation(poles, n2)?) >= worse - degree {
continue;
}
// ... and have area in 3D (three corners on one line of a cone have none, however they
@@ -411,21 +584,19 @@ pub(crate) fn flip(
if min_angle(n1).min(min_angle(n2)) < MIN_FLIP_ANGLE {
continue;
}
if !(upright(n1, sign) && upright(n2, sign)) {
if !(upright(poles, n1, sign)? && upright(poles, n2, sign)?) {
continue;
}
let (Some(pa), Some(pb), Some(pc), Some(pd), Some(qa), Some(qb), Some(qc), Some(qd)) =
(p(a), p(b), p(c), p(d), q(a), q(b), q(c), q(d))
else {
let (Some(pa), Some(pb), Some(pc), Some(pd)) = (p(a), p(b), p(c), p(d)) else {
continue;
};
let allowed = tol
.max(off(mean(&[qa, qb]), (pa + pb) * 0.5))
.max(centroid_off(ta))
.max(centroid_off(tb));
if off(mean(&[qc, qd]), (pc + pd) * 0.5) > allowed
|| centroid_off(n1) > allowed
|| centroid_off(n2) > allowed
.max(off(poles.mean(uv, &[a, b])?, (pa + pb) * 0.5)?)
.max(centroid_off(poles, ta)?)
.max(centroid_off(poles, tb)?);
if off(poles.mean(uv, &[c, d])?, (pc + pd) * 0.5)? > allowed
|| centroid_off(poles, n1)? > allowed
|| centroid_off(poles, n2)? > allowed
{
continue;
}
@@ -452,4 +623,170 @@ pub(crate) fn flip(
.filter(|s| sides.get(s).is_some_and(|ts| ts.len() == 2)),
);
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
/// Two triangles of the strip u in [0, 1], v in [0, 1] whose inner side, the diagonal, has its
/// middle on the circle the surface has no point on: refinement stops there and names it,
/// leaving the triangles as they were.
#[test]
fn refine_names_the_middle_the_surface_does_not_evaluate_at() {
let s = super::super::refusals::cylinder_without(0.5);
let mut uv = vec![[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]];
let fixed: Vec<Vec3> = uv
.iter()
.map(|q| s.derivs(q[0], q[1]).map(|d| kernel::record_vec3(d.0)))
.collect::<Option<_>>()
.unwrap_or_default();
assert_eq!(fixed.len(), 4);
let mut tris = vec![[0, 1, 2], [0, 2, 3]];
assert_eq!(
refine(
&s,
0.01,
&mut Poles::new(&s, 1e-9),
&fixed,
&mut uv,
&mut tris,
1000
),
Err(Stop::NoValue(NoValue([0.5, 0.5])))
);
assert_eq!(tris, vec![[0, 1, 2], [0, 2, 3]]);
}
/// A node of the triangulation (not one of the boundary's) the surface does not evaluate at is
/// named, not left out of the check.
#[test]
fn refine_names_a_node_the_surface_does_not_evaluate_at() {
let s = super::super::refusals::cylinder_without(0.5);
let mut uv = vec![[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0], [0.5, 0.5]];
let fixed: Vec<Vec3> = uv
.iter()
.take(4)
.map(|q| s.derivs(q[0], q[1]).map(|d| kernel::record_vec3(d.0)))
.collect::<Option<_>>()
.unwrap_or_default();
assert_eq!(fixed.len(), 4);
let mut tris = vec![[0, 1, 4], [1, 2, 4], [2, 3, 4], [3, 0, 4]];
assert_eq!(
refine(
&s,
0.01,
&mut Poles::new(&s, 1e-9),
&fixed,
&mut uv,
&mut tris,
1000
),
Err(Stop::NoValue(NoValue([0.5, 0.5])))
);
assert_eq!(
flip(&s, 0.01, &mut Poles::new(&s, 1e-9), &fixed, &uv, &mut tris),
Err(Stop::NoValue(NoValue([0.5, 0.5])))
);
}
/// A triangle on a cylinder of radius 3 (its axis z, u along it, v the angle): corners at
/// the angles -0.1 and 0.1 on one circle and 0 a little up. The side along the circle sags
/// S = 3 (1 - cos 0.1) at its middle, the centroid only 2S/3: the measure is the side's.
/// It was the centroid's alone.
#[test]
fn the_worst_sag_is_the_sides_where_it_is_more_than_the_middles() {
use solidcore::eval::EvalCurve;
let s = EvalSurface::Revolved {
profile: EvalCurve::new(
1,
vec![0.0, 0.0, 1.0, 1.0],
vec![[3.0, 0.0, 0.0, 1.0], [3.0, 0.0, 4.0, 1.0]],
),
origin: solidcore::Vec3::ZERO,
axis: solidcore::Vec3::Z,
transform: [
[1.0, 0.0, 0.0, 0.0],
[0.0, 1.0, 0.0, 0.0],
[0.0, 0.0, 1.0, 0.0],
],
swap: false,
};
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]]);
assert!(
got.as_ref()
.is_ok_and(|(w, unmeasured)| (w - side).abs() < 1e-9 * side && *unmeasured == 0),
"{got:?} against {side}"
);
// A corner past the nodes is the triangle's index.
assert_eq!(
worst(
&s,
&mut Poles::new(&s, 0.0),
&[],
&uv,
&[[0, 1, 2], [0, 1, 3]]
),
Err(Stop::Corner(1))
);
}
/// Refining, flipping and measuring a coarse patch of a cylinder (every sag over the
/// tolerance, so the nodes are looked at by many triangles, sides and rounds) asks the
/// kernel's classifier once per node and direction, not once per look.
#[test]
fn the_classifier_is_asked_once_per_node() {
use solidcore::eval::EvalCurve;
let s = EvalSurface::Revolved {
profile: EvalCurve::new(
1,
vec![0.0, 0.0, 1.0, 1.0],
vec![[3.0, 0.0, 0.0, 1.0], [3.0, 0.0, 4.0, 1.0]],
),
origin: solidcore::Vec3::ZERO,
axis: solidcore::Vec3::Z,
transform: [
[1.0, 0.0, 0.0, 0.0],
[0.0, 1.0, 0.0, 0.0],
[0.0, 0.0, 1.0, 0.0],
],
swap: false,
};
let n = 6u32;
let mut uv: Vec<[f64; 2]> = Vec::new();
for i in 0..=n {
for j in 0..=n {
uv.push([
f64::from(i) / f64::from(n),
3.0 * f64::from(j) / f64::from(n),
]);
}
}
let mut tris: Vec<[u32; 3]> = Vec::new();
for i in 0..n {
for j in 0..n {
let a = i * (n + 1) + j;
let (b, c, d) = (a + 1, a + n + 1, a + n + 2);
tris.push([a, c, d]);
tris.push([a, d, b]);
}
}
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.
refine(&s, 5e-2, &mut poles, &[], &mut uv, &mut tris, 300).unwrap();
flip(&s, 5e-2, &mut poles, &[], &uv, &mut tris).unwrap();
worst(&s, &mut poles, &[], &uv, &tris).unwrap();
assert!(uv.len() > 2 * start, "{} nodes: not refined", uv.len());
assert!(poles.asked > 0);
assert!(
poles.asked <= 2 * uv.len(),
"{} calls for {} nodes",
poles.asked,
uv.len()
);
}
}
+184 -86
View File
@@ -7,11 +7,23 @@
//! the two lines inside the face join the pieces into closed loops. Both lines are one curve
//! on the surface, so their nodes are made once and shared: the face stays closed across it.
use crate::fast::EvalSurface;
use solidcore::eval::EvalSurface;
use solidcore_convert as kernel;
use solidcore_record::Vec3;
use super::NoValue;
use super::size::Rules;
/// Why a face could not be cut into one period.
#[derive(Clone, Copy, Debug)]
pub(crate) enum Cut {
/// The loops cannot be cut cleanly: the face would be unbounded along the line, or the two
/// lines disagree.
Unclean,
/// The surface does not evaluate at a node of the cut line.
NoValue(NoValue),
}
/// One loop of a face: parameters (unwrapped along the loop) and the 3D node each stands for.
#[derive(Clone, Debug, Default)]
pub(crate) struct Loop {
@@ -24,14 +36,33 @@ const MAX_COPIES: i64 = 2048;
/// Most nodes a cut line may get between two crossings.
const MAX_SEAM_NODES: usize = 4096;
/// The copies `a..=b` of a loop (whole numbers, as `f64`), as integers: `None` where they are
/// not finite, run backwards, or number more than [`MAX_COPIES`]. Compared in `f64` before the
/// casts, so no cast saturates and no subtraction of the integers overflows.
fn copies(a: f64, b: f64) -> Option<(i64, i64)> {
// Well inside i64, and exact in f64: every whole number up to 2^53 is.
const LIMIT: f64 = 9_007_199_254_740_992.0;
let fits = |x: f64| x.abs() <= LIMIT;
// Proven: both are whole numbers of magnitude at most 2^53 (checked, NaN failing every
// comparison), so the casts are exact.
(fits(a) && fits(b) && a <= b && b - a <= MAX_COPIES as f64).then_some((a as i64, b as i64))
}
/// A run along a cut line joining two pieces: the piece it leaves, its nodes' parameters and
/// points, and the piece it enters.
type Link = (usize, Vec<[f64; 2]>, Vec<Vec3>, usize);
/// How far `l` drifts along `d` going once around it (a whole number of periods for a closed
/// loop: zero unless it winds around the surface).
pub(crate) fn drift(l: &Loop, d: usize, per: f64) -> f64 {
let (Some(first), Some(last)) = (l.uv.first(), l.uv.last()) else {
let (Some(&first), Some(&last)) = (
l.uv.first().and_then(|q| q.get(d)),
l.uv.last().and_then(|q| q.get(d)),
) else {
return 0.0;
};
let close = super::uv::unwrap(first[d], last[d], Some(per)) - last[d];
last[d] - first[d] + close
let close = solidcore::eval::uv::unwrap(first, last, Some(per)) - last;
last - first + close
}
/// Whether any loop winds around the closed direction `d`.
@@ -55,8 +86,9 @@ struct Piece {
}
/// Cut a face whose loops wind around direction `d` (period `per`) into one period. The face
/// lies left of its loops in (u, v). `None` when the loops cannot be cut cleanly (the face would
/// be unbounded along the line, or the two lines disagree); the caller then fails as before.
/// lies left of its loops in (u, v). [`Cut::Unclean`] when the loops cannot be cut cleanly (the
/// face would be unbounded along the line, or the two lines disagree); [`Cut::NoValue`] when the
/// surface does not evaluate at a node of the cut line (the evaluator refuses the face).
pub(crate) fn cut(
s: &EvalSurface,
loops: &[Loop],
@@ -64,9 +96,9 @@ pub(crate) fn cut(
per: f64,
rules: &Rules,
h: f64,
) -> Option<Vec<Loop>> {
) -> Result<Vec<Loop>, Cut> {
if !(per > 0.0 && per.is_finite()) || loops.is_empty() {
return None;
return Err(Cut::Unclean);
}
// Work in (x, y) with x along the closed direction; swapping (u, v) mirrors the plane, so the
// loops are reversed to keep the face on their left.
@@ -86,29 +118,26 @@ pub(crate) fn cut(
Loop { uv, pts }
})
.collect();
let x0 = cut_position(&local, per)?;
let x0 = cut_position(&local, per).ok_or(Cut::Unclean)?;
let x1 = x0 + per;
let mut closed: Vec<Loop> = Vec::new();
let mut pieces: Vec<Piece> = Vec::new();
for l in &local {
let n = l.uv.len();
if n < 2 || l.pts.len() != n {
return None;
return Err(Cut::Unclean);
}
let w = drift(l, 0, per);
let (lo, hi) =
l.uv.iter()
.fold((f64::MAX, f64::MIN), |(a, b), q| (a.min(q[0]), b.max(q[0])));
if !(lo.is_finite() && hi.is_finite()) {
return None;
return Err(Cut::Unclean);
}
if w.abs() <= per * 0.5 {
// A closed loop: each copy along x that reaches into the strip.
let k0 = ((x0 - hi) / per).floor() as i64;
let k1 = ((x1 - lo) / per).ceil() as i64;
if k1 - k0 > MAX_COPIES {
return None;
}
let (k0, k1) =
copies(((x0 - hi) / per).floor(), ((x1 - lo) / per).ceil()).ok_or(Cut::Unclean)?;
for k in k0..=k1 {
let shift = per * k as f64;
let uv: Vec<[f64; 2]> = l.uv.iter().map(|q| [q[0] + shift, q[1]]).collect();
@@ -119,7 +148,11 @@ pub(crate) fn cut(
});
} else if uv.iter().any(|q| q[0] > x0 && q[0] < x1) || crosses(&uv, x0, x1) {
// Start at a node outside the strip, so every run inside is whole.
let start = uv.iter().position(|q| !(q[0] > x0 && q[0] < x1))?;
let start = uv
.iter()
.position(|q| !(q[0] > x0 && q[0] < x1))
.ok_or(Cut::Unclean)?;
// Proven: n = l.uv.len() >= 2, checked above
let idx: Vec<usize> = (0..=n).map(|i| (start + i) % n).collect();
let path: Vec<[f64; 2]> =
idx.iter().filter_map(|&i| uv.get(i).copied()).collect();
@@ -133,28 +166,20 @@ pub(crate) fn cut(
// start and end outside the strip.
let turns = (w / per).round();
if turns.abs() != 1.0 {
return None;
return Err(Cut::Unclean);
}
let reach = (hi - lo) / per;
let span = (reach.ceil() as i64) + 2;
if span > MAX_COPIES {
return None;
// Compared in f64, before any cast: a NaN or a reach past the bound is unclean.
let span = ((hi - lo) / per).ceil() + 2.0;
if span.is_nan() || span > MAX_COPIES as f64 {
return Err(Cut::Unclean);
}
// Copies j with lo + j·w ≤ x1 and hi + j·w ≥ x0, plus one each side.
let (ja, jb) = if w > 0.0 {
(
((x0 - hi) / w).floor() as i64 - 1,
((x1 - lo) / w).ceil() as i64 + 1,
)
(((x0 - hi) / w).floor() - 1.0, ((x1 - lo) / w).ceil() + 1.0)
} else {
(
((x1 - lo) / w).floor() as i64 - 1,
((x0 - hi) / w).ceil() as i64 + 1,
)
(((x1 - lo) / w).floor() - 1.0, ((x0 - hi) / w).ceil() + 1.0)
};
if jb - ja > MAX_COPIES {
return None;
}
let (ja, jb) = copies(ja, jb).ok_or(Cut::Unclean)?;
let mut path = Vec::new();
let mut pts = Vec::new();
for j in ja..=jb {
@@ -163,10 +188,10 @@ pub(crate) fn cut(
pts.extend(l.pts.iter().copied());
}
let (Some(a), Some(b)) = (path.first(), path.last()) else {
return None;
return Err(Cut::Unclean);
};
if (a[0] > x0 && a[0] < x1) || (b[0] > x0 && b[0] < x1) {
return None;
return Err(Cut::Unclean);
}
clip(&path, &pts, x0, x1, &mut pieces);
}
@@ -178,7 +203,7 @@ pub(crate) fn cut(
let (Some(a), Some(b), Some(pa), Some(pb)) =
(p.uv.first(), p.uv.last(), p.pts.first(), p.pts.last())
else {
return None;
return Err(Cut::Unclean);
};
// Entering through the left line (moving +x): the face is above; through the right
// line (moving −x): below. Leaving through the left (−x): below; the right (+x): above.
@@ -193,20 +218,20 @@ pub(crate) fn cut(
above: b[0] == x1,
};
if a[0] == x0 {
left.push((enter, i, true))
left.push((enter, i, true));
} else {
right.push((enter, i, true))
right.push((enter, i, true));
}
if b[0] == x0 {
left.push((leave, i, false))
left.push((leave, i, false));
} else {
right.push((leave, i, false))
right.push((leave, i, false));
}
}
left.sort_by(|a, b| a.0.y.total_cmp(&b.0.y));
right.sort_by(|a, b| a.0.y.total_cmp(&b.0.y));
if left.len() != right.len() || !left.len().is_multiple_of(2) {
return None;
return Err(Cut::Unclean);
}
let scale = loops
.iter()
@@ -214,7 +239,7 @@ pub(crate) fn cut(
.fold(1.0f64, |m, q| m.max(q[0].abs()).max(q[1].abs()));
for (a, b) in left.iter().zip(&right) {
if a.0.above != b.0.above || (a.0.y - b.0.y).abs() > 1e-7 * scale {
return None;
return Err(Cut::Unclean);
}
}
// The same crossing on both lines is one point.
@@ -223,11 +248,11 @@ pub(crate) fn cut(
b.0.p = a.0.p;
}
for (c, i, start) in left.iter().chain(&right) {
let p = pieces.get_mut(*i)?;
let p = pieces.get_mut(*i).ok_or(Cut::Unclean)?;
let k = if *start {
0
} else {
p.uv.len().checked_sub(1)?
p.uv.len().checked_sub(1).ok_or(Cut::Unclean)?
};
if let (Some(q), Some(pt)) = (p.uv.get_mut(k), p.pts.get_mut(k)) {
q[1] = c.y;
@@ -236,30 +261,31 @@ pub(crate) fn cut(
}
// Inside the face along each line: from a crossing with the face above to the next one
// up (which must have it below).
let mut links: Vec<(usize, Vec<[f64; 2]>, Vec<Vec3>, usize)> = Vec::new();
let mut links: Vec<Link> = Vec::new();
// Proven: both chunk sizes are the constant 2, not zero.
for pair in left.chunks(2).zip(right.chunks(2)) {
let (lp, rp) = pair;
let (Some(la), Some(lb), Some(ra), Some(rb)) =
(lp.first(), lp.get(1), rp.first(), rp.get(1))
else {
return None;
return Err(Cut::Unclean);
};
if !(la.0.above && !lb.0.above) {
return None;
if !la.0.above || lb.0.above {
return Err(Cut::Unclean);
}
// Nodes strictly between, made once for both lines.
let inner = seam_nodes(s, swap, x0, la.0.y, lb.0.y, rules, h)?;
let inner = seam_nodes(s, swap, x0, la.0.y, lb.0.y, rules, h).map_err(Cut::NoValue)?;
// Left line, downwards: from the piece leaving at the top to the one entering at the bottom.
let (top_leave, bottom_enter) = (lb.1, la.1);
let down_uv: Vec<[f64; 2]> = inner.iter().rev().map(|(y, _)| [x0, *y]).collect();
let down_pts: Vec<Vec3> = inner.iter().rev().map(|(_, p)| *p).collect();
if lb.2 || !la.2 {
return None;
return Err(Cut::Unclean);
}
links.push((top_leave, down_uv, down_pts, bottom_enter));
// Right line, upwards: from the piece leaving at the bottom to the one entering at the top.
if ra.2 || !rb.2 {
return None;
return Err(Cut::Unclean);
}
let up_uv: Vec<[f64; 2]> = inner.iter().map(|(y, _)| [x1, *y]).collect();
let up_pts: Vec<Vec3> = inner.iter().map(|(_, p)| *p).collect();
@@ -270,10 +296,10 @@ pub(crate) fn cut(
let mut via: Vec<Option<usize>> = vec![None; pieces.len()];
for (li, (from, _, _, to)) in links.iter().enumerate() {
if next.get(*from).copied().flatten().is_some() {
return None;
return Err(Cut::Unclean);
}
*next.get_mut(*from)? = Some(*to);
*via.get_mut(*from)? = Some(li);
*next.get_mut(*from).ok_or(Cut::Unclean)? = Some(*to);
*via.get_mut(*from).ok_or(Cut::Unclean)? = Some(li);
}
let mut used = vec![false; pieces.len()];
for start in 0..pieces.len() {
@@ -283,20 +309,25 @@ pub(crate) fn cut(
let mut out = Loop::default();
let mut at = start;
for _ in 0..=pieces.len() {
if *used.get(at)? {
if *used.get(at).ok_or(Cut::Unclean)? {
break;
}
*used.get_mut(at)? = true;
let p = pieces.get(at)?;
*used.get_mut(at).ok_or(Cut::Unclean)? = true;
let p = pieces.get(at).ok_or(Cut::Unclean)?;
out.uv.extend(&p.uv);
out.pts.extend(&p.pts);
let (_, luv, lpts, _) = links.get((*via.get(at)?)?)?;
let (_, luv, lpts, _) = via
.get(at)
.copied()
.flatten()
.and_then(|li| links.get(li))
.ok_or(Cut::Unclean)?;
out.uv.extend(luv);
out.pts.extend(lpts);
at = (*next.get(at)?)?;
at = next.get(at).copied().flatten().ok_or(Cut::Unclean)?;
}
if at != start {
return None;
return Err(Cut::Unclean);
}
closed.push(out);
}
@@ -315,16 +346,18 @@ pub(crate) fn cut(
let area: f64 = out
.iter()
.map(|l| {
let n = l.uv.len();
(0..n)
.map(|i| {
let (a, b) = (l.uv[i], l.uv[(i + 1) % n]);
a[0] * b[1] - b[0] * a[1]
})
// Each node with the next one round the loop.
l.uv.iter()
.zip(l.uv.iter().cycle().skip(1))
.map(|(a, b)| a[0] * b[1] - b[0] * a[1])
.sum::<f64>()
})
.sum();
(area > 0.0).then_some(out)
if area > 0.0 {
Ok(out)
} else {
Err(Cut::Unclean)
}
}
/// Where to cut: the middle of the widest gap between the loops' nodes around the period, as
@@ -334,13 +367,11 @@ fn cut_position(loops: &[Loop], per: f64) -> Option<f64> {
.iter()
.flat_map(|l| l.uv.iter().map(|q| q[0].rem_euclid(per)))
.collect();
if xs.is_empty() {
return None;
}
xs.sort_by(f64::total_cmp);
let &first = xs.first()?;
let mut best = (0.0, f64::NEG_INFINITY);
for (i, x) in xs.iter().enumerate() {
let next = xs.get(i + 1).copied().unwrap_or(xs[0] + per);
let next = xs.get(i + 1).copied().unwrap_or(first + per);
if next - x > best.1 {
best = (0.5 * (x + next), next - x);
}
@@ -350,8 +381,9 @@ fn cut_position(loops: &[Loop], per: f64) -> Option<f64> {
/// Whether a path jumps over the strip in one segment.
fn crosses(uv: &[[f64; 2]], x0: f64, x1: f64) -> bool {
uv.windows(2)
.any(|w| (w[0][0] <= x0 && w[1][0] >= x1) || (w[1][0] <= x0 && w[0][0] >= x1))
uv.iter()
.zip(uv.iter().skip(1))
.any(|(a, b)| (a[0] <= x0 && b[0] >= x1) || (b[0] <= x0 && a[0] >= x1))
}
/// The runs of an open path (starting and ending outside the strip) inside the strip, each
@@ -366,6 +398,7 @@ fn clip(path: &[[f64; 2]], pts: &[Vec3], x0: f64, x1: f64, out: &mut Vec<Piece>)
.zip(pts.iter().zip(pts.iter().skip(1)))
{
let at = |x: f64| {
// Proven: the clamp bounds are the constants 0.0 <= 1.0, neither NaN.
let t = ((x - a[0]) / (b[0] - a[0])).clamp(0.0, 1.0);
([x, a[1] + (b[1] - a[1]) * t], *pa + (*pb - *pa) * t)
};
@@ -389,7 +422,7 @@ fn clip(path: &[[f64; 2]], pts: &[Vec3], x0: f64, x1: f64, out: &mut Vec<Piece>)
cur = Some(Piece {
uv: vec![q],
pts: vec![p],
})
});
}
}
}
@@ -403,7 +436,8 @@ fn clip(path: &[[f64; 2]], pts: &[Vec3], x0: f64, x1: f64, out: &mut Vec<Piece>)
}
/// Nodes along the cut line between two crossings (`y0` < `y1`, ends excluded): within the
/// tolerance, the side length and the facet angle of the surface along the line.
/// tolerance, the side length and the facet angle of the surface along the line. The parameters
/// of a node the surface does not evaluate at are the error.
fn seam_nodes(
s: &EvalSurface,
swap: bool,
@@ -412,26 +446,90 @@ fn seam_nodes(
y1: f64,
rules: &Rules,
h: f64,
) -> Option<Vec<(f64, Vec3)>> {
) -> Result<Vec<(f64, Vec3)>, NoValue> {
let uv = |y: f64| if swap { [y, x] } else { [x, y] };
let mut ys = vec![y0, y1];
let mut i = 0;
while i + 1 < ys.len() {
let (a, b) = (ys[i], ys[i + 1]);
while let (Some(&a), Some(&b)) = (ys.get(i), ys.get(i + 1)) {
if ys.len() < MAX_SEAM_NODES
&& !super::uv::fine_between(s, rules, h, uv(a), uv(b))
&& !super::uv::fine_between(s, rules, h, uv(a), uv(b))?
&& b - a > 1e-12 * (1.0 + a.abs())
{
// Proven: i + 1 < ys.len(), as `ys.get(i + 1)` is Some
ys.insert(i + 1, 0.5 * (a + b));
} else {
i += 1;
}
}
let inner = ys.get(1..ys.len() - 1)?;
Some(
inner
.iter()
.map(|&y| (y, s.eval(uv(y)[0], uv(y)[1]).0))
.collect(),
)
// `ys` starts as [y0, y1] and only grows, so its inner nodes are all but its first and last.
let inner = ys.iter().skip(1).take(ys.len().saturating_sub(2));
inner
.map(|&y| {
s.derivs(uv(y)[0], uv(y)[1])
.map(|d| (y, kernel::record_vec3(d.0)))
.ok_or_else(|| NoValue(uv(y)))
})
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
/// A seamless face on a revolution whose surface has no point on the circle u = 0.5 (see
/// [`super::super::refusals::cylinder_without`]): its two loops, the circles
/// u = 0 and u = 1 a whole turn round, wind round it and are cut along a line v = const from
/// u = 0 to u = 1. That line's node at u = 0.5 does not evaluate: the cut names it, not drops
/// it from the line.
#[test]
fn the_cut_names_a_node_the_surface_does_not_evaluate_at() {
let s = super::super::refusals::cylinder_without(0.5);
let tau = std::f64::consts::TAU;
let n = 16;
let circle = |u: f64, up: bool| {
let uv: Vec<[f64; 2]> = (0..n)
.map(|k| {
[
u,
tau * f64::from(if up { k } else { n - k }) / f64::from(n),
]
})
.collect();
let pts: Vec<Vec3> = uv
.iter()
.filter_map(|q| s.derivs(q[0], q[1]).map(|d| kernel::record_vec3(d.0)))
.collect();
assert_eq!(pts.len(), uv.len());
Loop { uv, pts }
};
// The face lies left of its loops: up the circle at u = 1, down the one at u = 0.
let loops = [circle(1.0, true), circle(0.0, false)];
assert!(winds(&loops, 1, tau));
let rules = Rules::new(0.01, 40.0, None);
match cut(&s, &loops, 1, tau, &rules, 1.0) {
Err(Cut::NoValue(NoValue(q))) => assert_eq!(q[0], 0.5, "{q:?}"),
Err(Cut::Unclean) => panic!("refused as unclean"),
Ok(l) => panic!("cut into {} loops", l.len()),
}
}
/// A loop whose copies along the closed direction run past what an `i64` holds (a loop
/// spanning ±1e25 periods, rounding to a closed one): its first and last copies saturated in
/// the casts and their difference overflowed. Compared in `f64` first, the cut refuses it as
/// unclean.
#[test]
fn a_loop_whose_copies_overflow_an_integer_is_unclean() {
let s = super::super::refusals::cylinder_without(0.5);
let far = 1e25;
let uv = vec![[0.25, -far], [0.25, far], [0.75, far], [0.75, -far]];
let pts = vec![Vec3::new(0.0, 0.0, 0.0); uv.len()];
let loops = [Loop { uv, pts }];
let tau = std::f64::consts::TAU;
assert!(!winds(&loops, 1, tau));
let rules = Rules::new(0.01, 40.0, None);
assert!(matches!(
cut(&s, &loops, 1, tau, &rules, 1.0),
Err(Cut::Unclean)
));
}
}
+107 -63
View File
@@ -1,18 +1,19 @@
//! Target triangle size: one side length per face, so triangles come out about equilateral
//! (well-shaped triangles shade evenly) and as small as the surface needs.
//!
//! A flat face (a plane, or a spline lying in one) has no size: any triangulation of its
//! boundary is exact, so it gets no interior cells and its straight edges no extra nodes.
//! A curved face's size is the smallest of the body's longest side, the length over which the surface
//! A face's size is the smallest of the body's longest side, the length over which the surface
//! normal turns the caller's facet angle (when one is given, see [`Turn`]), and the chord that
//! sags the tolerance; an edge takes the smaller size of its two faces, so a curved face and
//! its flat neighbour meet at the same spacing.
use crate::fast::EvalSurface;
use std::cmp::Ordering;
use crate::vec::VecExt as _;
use solidcore::eval::EvalSurface;
use solidcore_convert as kernel;
use solidcore_record::Vec3;
use super::uv::Domain;
use solidcore::eval::uv::Domain;
/// Longest side, as a fraction of the body's extent.
const MAX_LEN_FRACTION: f64 = 1.0 / 5.0;
@@ -115,22 +116,48 @@ pub(crate) fn extent(points: impl Iterator<Item = Vec3>) -> f64 {
2.0 * points.iter().map(|p| p.dist(c)).fold(0.0, f64::max)
}
/// A face's size, from samples over its surface's parameter box ([`face_size`]).
#[derive(Clone, Debug, PartialEq)]
pub(crate) struct FaceSize {
/// The target side length (mm).
pub h: f64,
/// The surface's mean speed along u and v (mm per parameter unit).
pub speed: [f64; 2],
/// The samples the surface does not evaluate at, which measure nothing. The box reaches past
/// a trimmed face, so whether one refuses the face is decided against the face's own loops
/// when it is planned: one inside the face does.
pub unevaluated: Vec<[f64; 2]>,
}
/// A face's target side length and the surface's mean speed along u and v (mm per parameter
/// unit), from samples over the surface's parameter box (the whole box of a trimmed surface,
/// so the size is never larger than the face needs).
pub(crate) fn face_size(s: &EvalSurface, dom: &Domain, rules: &Rules) -> (f64, [f64; 2]) {
/// so the size is never larger than the face needs), with the samples the surface does not
/// evaluate at.
pub(crate) fn face_size(s: &EvalSurface, dom: &Domain, rules: &Rules) -> FaceSize {
let plain = |h: f64, speed: [f64; 2]| FaceSize {
h,
speed,
unevaluated: Vec::new(),
};
if let EvalSurface::Plane { du, dv, .. } = s {
return (f64::INFINITY, [du.len(), dv.len()]);
return plain(
rules.max_len,
[
kernel::record_vec3(*du).len(),
kernel::record_vec3(*dv).len(),
],
);
}
let (Some(ru), Some(rv)) = (dom.range[0], dom.range[1]) else {
return (rules.max_len, [1.0, 1.0]);
return plain(rules.max_len, [1.0, 1.0]);
};
let flat = planar(s);
let mut unevaluated: Vec<[f64; 2]> = Vec::new();
// A flat spline (sheet-metal walls, chamfers): no curvature to look for, only the speeds.
let samples = if flat { 3 } else { SAMPLES };
let normal = |u: f64, v: f64| -> Option<(Vec3, Vec3, [f64; 2])> {
let (p, su, sv) = s.derivs(u, v)?;
Some((p, su.cross(sv).normalized()?, [su.len(), sv.len()]))
let samples = if planar(s) { 3 } else { SAMPLES };
// The point, the unit normal (`None` at a pole, where it is undefined) and the speeds.
let normal = |u: f64, v: f64| -> Result<(Vec3, Option<Vec3>, [f64; 2]), [f64; 2]> {
let (p, su, sv) = s.derivs(u, v).map(kernel::record_triple).ok_or([u, v])?;
Ok((p, su.cross(sv).normalized(), [su.len(), sv.len()]))
};
let step = [(ru.1 - ru.0) * 1e-3, (rv.1 - rv.0) * 1e-3];
let mut k_max: f64 = 0.0;
@@ -140,9 +167,14 @@ pub(crate) fn face_size(s: &EvalSurface, dom: &Domain, rules: &Rules) -> (f64, [
// Cell centres, clear of the box edges (poles, seams).
let u = ru.0 + (ru.1 - ru.0) * (i as f64 + 0.5) / samples as f64;
let v = rv.0 + (rv.1 - rv.0) * (j as f64 + 0.5) / samples as f64;
let Some((p, n, sp)) = normal(u, v) else {
continue;
let (p, n, sp) = match normal(u, v) {
Ok(at) => at,
Err(q) => {
unevaluated.push(q);
continue;
}
};
let Some(n) = n else { continue };
speed = [speed[0] + sp[0], speed[1] + sp[1]];
count += 1;
if samples < SAMPLES {
@@ -150,9 +182,14 @@ pub(crate) fn face_size(s: &EvalSurface, dom: &Domain, rules: &Rules) -> (f64, [
}
// How fast the normal turns per mm, along u and along v (twist included).
for (du, dv) in [(step[0], 0.0), (0.0, step[1])] {
let Some((q, m, _)) = normal(u + du, v + dv) else {
continue;
let (q, m, _) = match normal(u + du, v + dv) {
Ok(at) => at,
Err(q) => {
unevaluated.push(q);
continue;
}
};
let Some(m) = m else { continue };
let ds = p.dist(q);
if ds > 0.0 {
k_max = k_max.max((m - n).len() / ds);
@@ -165,25 +202,24 @@ pub(crate) fn face_size(s: &EvalSurface, dom: &Domain, rules: &Rules) -> (f64, [
} else {
[1.0, 1.0]
};
// A flat face is exact for any triangulation of its boundary: no length cap.
let size = if flat {
f64::INFINITY
} else {
rules.for_curvature(k_max)
};
(
size,
speed.map(|x| if x.is_finite() && x > 0.0 { x } else { 1.0 }),
)
FaceSize {
h: rules.for_curvature(k_max),
speed: speed.map(|x| if x.is_finite() && x > 0.0 { x } else { 1.0 }),
unevaluated,
}
}
/// A spline surface whose control points lie in one plane, with positive weights: the
/// surface lies in that plane (it is inside the control points' convex hull).
fn planar(s: &EvalSurface) -> bool {
let EvalSurface::Nurbs { ctrl, .. } = s else {
let EvalSurface::Nurbs(n) = s else {
return false;
};
if ctrl.iter().any(|h| !(h[3] > 0.0)) {
let ctrl = n.ctrl();
if ctrl
.iter()
.any(|h| h[3].partial_cmp(&0.0) != Some(Ordering::Greater))
{
return false;
}
let pts: Vec<Vec3> = ctrl
@@ -210,7 +246,7 @@ fn planar(s: &EvalSurface) -> bool {
#[cfg(test)]
mod tests {
use super::*;
use crate::fast::EvalCurve;
use solidcore::eval::EvalCurve;
const THIRTY: Option<Turn> = Some(Turn {
angle: std::f64::consts::PI / 6.0,
@@ -220,13 +256,13 @@ mod tests {
#[test]
fn a_cylinder_is_sized_by_its_radius() {
let s = EvalSurface::Revolved {
profile: EvalCurve {
degree: 1,
knots: vec![0.0, 0.0, 1.0, 1.0],
ctrl: vec![[3.0, 0.0, 0.0, 1.0], [3.0, 0.0, 40.0, 1.0]],
},
origin: Vec3::ZERO,
axis: Vec3::Z,
profile: EvalCurve::new(
1,
vec![0.0, 0.0, 1.0, 1.0],
vec![[3.0, 0.0, 0.0, 1.0], [3.0, 0.0, 40.0, 1.0]],
),
origin: solidcore::Vec3::ZERO,
axis: solidcore::Vec3::Z,
transform: [
[1.0, 0.0, 0.0, 0.0],
[0.0, 1.0, 0.0, 0.0],
@@ -236,7 +272,7 @@ mod tests {
};
// A 20 mm body: sides from 1 mm (the floor, a quarter of the longest) to 4 mm.
let rules = Rules::new(0.01, 20.0, THIRTY);
let (h, speed) = face_size(&s, &super::super::uv::domain(&s), &rules);
let FaceSize { h, speed, .. } = face_size(&s, &solidcore::eval::uv::domain(&s), &rules);
// r = 3: 30° of turn is 1.57 mm; the 0.01 mm sag chord is √(8·3·0.01) = 0.49 mm, times the
// aspect, unless that is under the floor.
assert!(
@@ -251,9 +287,9 @@ mod tests {
"{speed:?}"
);
// Coarser: the turn limit governs.
let (h, _) = face_size(
let FaceSize { h, .. } = face_size(
&s,
&super::super::uv::domain(&s),
&solidcore::eval::uv::domain(&s),
&Rules::new(0.5, 100.0, THIRTY),
);
assert!(
@@ -264,9 +300,9 @@ mod tests {
"{h}"
);
// Without a facet angle the sag alone: √(8·3·0.5) = 3.46 mm, four times along.
let (h, _) = face_size(
let FaceSize { h, .. } = face_size(
&s,
&super::super::uv::domain(&s),
&solidcore::eval::uv::domain(&s),
&Rules::new(0.5, 100.0, None),
);
assert!(
@@ -278,9 +314,9 @@ mod tests {
angle: std::f64::consts::PI / 6.0,
floor: 1.0,
});
let (h, _) = face_size(
let FaceSize { h, .. } = face_size(
&s,
&super::super::uv::domain(&s),
&solidcore::eval::uv::domain(&s),
&Rules::new(0.5, 100.0, floored),
);
assert!(
@@ -307,29 +343,37 @@ mod tests {
assert!(planar(&patch(0.0)));
assert!(!planar(&patch(0.5)));
let rules = Rules::new(0.01, 100.0, THIRTY);
let (h, speed) = face_size(&patch(0.0), &super::super::uv::domain(&patch(0.0)), &rules);
assert_eq!(h, f64::INFINITY);
let FaceSize { h, speed, .. } = face_size(
&patch(0.0),
&solidcore::eval::uv::domain(&patch(0.0)),
&rules,
);
assert_eq!(h, rules.max_len);
assert!(
(speed[0] - 4.0).abs() < 1e-9 && (speed[1] - 3.0).abs() < 1e-9,
"{speed:?}"
);
// The twisted patch (a hyperbolic paraboloid) has curvature: a smaller side.
assert!(
face_size(&patch(0.5), &super::super::uv::domain(&patch(0.5)), &rules).0
< rules.max_len
face_size(
&patch(0.5),
&solidcore::eval::uv::domain(&patch(0.5)),
&rules
)
.h < rules.max_len
);
}
#[test]
fn a_tiny_radius_does_not_shrink_the_side_below_the_floor() {
let s = EvalSurface::Revolved {
profile: EvalCurve {
degree: 1,
knots: vec![0.0, 0.0, 1.0, 1.0],
ctrl: vec![[0.05, 0.0, 0.0, 1.0], [0.05, 0.0, 80.0, 1.0]],
},
origin: Vec3::ZERO,
axis: Vec3::Z,
profile: EvalCurve::new(
1,
vec![0.0, 0.0, 1.0, 1.0],
vec![[0.05, 0.0, 0.0, 1.0], [0.05, 0.0, 80.0, 1.0]],
),
origin: solidcore::Vec3::ZERO,
axis: solidcore::Vec3::Z,
transform: [
[1.0, 0.0, 0.0, 0.0],
[0.0, 1.0, 0.0, 0.0],
@@ -338,21 +382,21 @@ mod tests {
swap: false,
};
let rules = Rules::new(0.1, 100.0, THIRTY);
let (h, _) = face_size(&s, &super::super::uv::domain(&s), &rules);
let FaceSize { h, .. } = face_size(&s, &solidcore::eval::uv::domain(&s), &rules);
assert!((h - rules.max_len * MIN_LEN_FRACTION).abs() < 1e-9, "{h}");
}
#[test]
fn a_plane_has_no_size_cap() {
fn a_plane_takes_the_longest_side() {
let s = EvalSurface::Plane {
origin: Vec3::ZERO,
du: Vec3::X,
dv: Vec3::Y,
origin: solidcore::Vec3::ZERO,
du: solidcore::Vec3::X,
dv: solidcore::Vec3::Y,
};
let rules = Rules::new(0.01, 100.0, THIRTY);
assert_eq!(
face_size(&s, &super::super::uv::domain(&s), &rules).0,
f64::INFINITY
face_size(&s, &solidcore::eval::uv::domain(&s), &rules).h,
20.0
);
}
}
-139
View File
@@ -1,139 +0,0 @@
//! What a plan reports about its faces and edges: how each face was meshed, and what was
//! refused, limited or left out.
/// How the faces of a plan were meshed.
#[derive(Clone, Debug, Default, PartialEq)]
pub struct PlanStats {
/// Planar faces meshed whole as a box or four-cornered layout, or by Delaunay.
pub planar: usize,
/// Faces meshed on a structured layout: a box or four-cornered grid, patches or a cut grid.
pub structured: usize,
/// Curved faces meshed by constrained Delaunay triangulation.
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<RefusedFace>,
/// Faces even the lenient fill refused, faces whose surface does not evaluate at a point
/// the planner needs (a node, a middle it checks, a node of a cut line), and faces the
/// planner could not read (a surface the evaluator does not take, a pcurve off its edge, a
/// loop too short to bound anything, an edge that cannot be sampled): left out of the plan
/// whole (no triangles), so the rest of the body is still meshed, each with why it could not
/// be planned. Nothing is substituted for a point the surface refuses.
pub unmeshed: Vec<RefusedFace>,
/// Edges that bound no face and could not be sampled: left out of the plan (an empty
/// polyline), so the rest of the body is still meshed, each with why.
pub unsampled_edges: Vec<RefusedEdge>,
/// Faces a memory bound of the planner limited: their grid thinned below the lines their
/// tolerance asked for, or their sag refinement stopped at the points it may add. Each is
/// still meshed, named here with the sag its triangles reach and the bounds that limited it,
/// so a mesh that leaves the tolerance is never so unreported.
pub over_tolerance: Vec<OverTolerance>,
/// Edges whose sampling at a coarser facet angle, for a face crowded with segments, failed:
/// each keeps its nodes at the rules' own level (valid, only finer), no face is refused, and
/// the edge is named here with the coarser sampling's error.
pub kept_strict: Vec<RefusedEdge>,
/// Edges whose sampling stopped at a bound ([`EdgeCap`]) with a segment still wanting a
/// split: their chords may lie off them by more than the tolerance (no refinement follows an
/// edge's sampling), each named with the bound and how far its chords lie off.
pub edges_capped: Vec<CappedEdge>,
/// Poles of higher order (no normal at the point) whose limit normal the kernel refused
/// for one of their triangles (a pole of order four or more, or another refusal): each is
/// meshed on the planner's layout, its normal the area-weighted mean of the face's
/// triangles' normals there, a display normal only, named here with the kernel's reason.
/// The other poles of higher order get the exact limit normal per triangle
/// ([`crate::PoleRule`]).
pub averaged_poles: Vec<crate::AveragedPole>,
}
/// An edge a bound of its sampling stopped (see [`PlanStats::edges_capped`]).
#[derive(Clone, Debug, PartialEq)]
pub struct CappedEdge {
/// The edge's index in the record.
pub edge: usize,
/// The bound: [`EdgeCap::Nodes`] where both were met.
pub cap: EdgeCap,
/// The farthest any of the edge's chords lies from its curve (mm), at their quarter points
/// and middles (a sampled lower bound: exact where a chord's farthest point is its middle,
/// as on a circle; below it at a corner between them). The segments exceeding their rules
/// most are halved first, so at the node bound it is spread along the edge. A line's
/// chords lie on it: 0.
pub sag: f64,
/// The most any of its chords exceeds its rules: the largest of its sag over the tolerance,
/// its length over the side asked, its turn over a quarter turn and over the facet angle
/// (past 1 for a rule broken; a line's, its sides' length over the side asked).
pub excess: f64,
/// The chord tolerance the edge was sampled to (mm).
pub tol: f64,
/// Whether `sag` is within `tol`: the chords follow the curve, and only a side's length or
/// turn is past its rule (as at a corner the facet angle never settles at, or a line).
pub within: bool,
}
/// A bound of an edge's sampling.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum EdgeCap {
/// The edge has the most nodes an edge may have (`MAX_EDGE_NODES`, 20 000): for a line,
/// its sides are longer than the side asked.
Nodes,
/// A segment was halved as often as one may be (24 times).
Depth,
}
/// A face a memory bound limited (see [`PlanStats::over_tolerance`]).
#[derive(Clone, Debug, PartialEq)]
pub struct OverTolerance {
/// The face's index in the record.
pub face: usize,
/// The farthest its triangles lie from its surface (mm), measured as the sag refinement
/// measures them: at each triangle's middle and its sides' middles, along the surface's
/// normal there, wherever the surface evaluates.
pub sag: f64,
/// The measurements `sag` could not make (one per middle per triangle, so a side shared by
/// two triangles counts twice): the surface has no point there.
pub unmeasured: usize,
/// The chord tolerance the face was planned to (mm).
pub tol: f64,
/// Whether every middle was measured and `sag` is within `tol` after all: the refinement
/// made up what a thinned grid left (for [`Cap::Refine`], which stops with a side still
/// over, normally not, unless a flip after the refinement laid that side down).
pub within: bool,
/// The bounds that limited it, one or both.
pub caps: Vec<Cap>,
}
/// A memory bound of the planner.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Cap {
/// The face's grid was thinned to fit `MAX_FACE_GRID` points: the
/// lines along u and v its division gave, and those kept. `asked` is the division's own
/// count, itself bounded (at most `MAX_LINES` lines, 20 halvings an interval); `kept` is before
/// any further thinning a cut grid makes for its traces. The sag refinement's measure of the
/// final triangles ([`OverTolerance::sag`]) covers both.
GridThinned {
/// The lines along u and v the division gave.
asked: [usize; 2],
/// The lines kept.
kept: [usize; 2],
},
/// The sag refinement added all the points it may (`MAX_REFINE`) with triangles still off
/// the surface by more than the tolerance.
Refine,
}
/// An edge the planner could not sample: its index in the record and why.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct RefusedEdge {
/// The edge's index in the record.
pub edge: usize,
/// Why it was refused.
pub reason: String,
}
/// A face a method refused: its index in the record and why.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct RefusedFace {
/// The face's index in the record.
pub face: usize,
/// Why it was refused.
pub reason: String,
}
+3 -38
View File
@@ -1,8 +1,6 @@
//! A face's surface in the kernel evaluator's form ([`EvalSurface`]), which the plan hands to
//! the CPU and GPU evaluators, and the same surface in tess's own display evaluator's form
//! ([`fast::EvalSurface`]), which the planner works on, both on the face's own (u, v): its
//! pcurves and domain are in those parameters, so its boundary takes them from its edges' exact
//! pcurves as they are. A face's
//! A face's surface in the evaluator's form ([`EvalSurface`]), which the planner and the CPU
//! and GPU evaluators share, on the face's own (u, v): its pcurves and domain are in those
//! parameters, so its boundary takes them from its edges' exact pcurves as they are. A face's
//! parameters are a plane or a spline ([`brep::Face::parameters`]): the record holds every
//! cylinder, cone, sphere, ball patch, torus and revolution as a label with its exact NURBS form,
//! made by the kernel ([`solidcore::eval::form`]), and refuses one without it. Tess converts
@@ -10,8 +8,6 @@
//! form is the record's error.
use solidcore::eval::{EvalSurface, SurfaceEval};
use crate::fast;
use solidcore_convert as kernel;
use solidcore_record::brep;
@@ -37,37 +33,6 @@ pub(crate) fn eval_surface(surface: &brep::Surface) -> Result<EvalSurface> {
}
}
/// `s` in the display evaluator's form: the same plane, net or revolution, value for value.
pub(crate) fn fast_surface(s: &EvalSurface) -> fast::EvalSurface {
let v = kernel::record_vec3;
match s {
EvalSurface::Plane { origin, du, dv } => fast::EvalSurface::Plane {
origin: v(*origin),
du: v(*du),
dv: v(*dv),
},
EvalSurface::Nurbs(n) => {
fast::EvalSurface::nurbs(n.degree(), n.knots().clone(), n.count(), n.ctrl().to_vec())
}
EvalSurface::Revolved {
profile,
origin,
axis,
transform,
swap,
} => fast::EvalSurface::Revolved {
profile: fast::EvalCurve {
degree: profile.degree(),
knots: profile.knots().to_vec(),
ctrl: profile.ctrl().to_vec(),
},
origin: v(*origin),
axis: v(*axis),
transform: *transform,
swap: *swap,
},
}
}
#[cfg(test)]
mod tests {
use super::*;
+65 -30
View File
@@ -2,10 +2,11 @@
//! parameters, that cross another chord of the face although the curves they stand for do not
//! (two curves running closer together than their chords sag).
use crate::fast::EvalSurface;
use std::cmp::Ordering;
use solidcore::eval::EvalSurface;
use super::seam;
use super::uv;
/// Two chords cross only where each one's ends lie strictly either side of the other, by more
/// than this fraction of the product of their lengths (a chord ending on another, as at a
@@ -21,11 +22,22 @@ struct Chord {
}
impl Chord {
fn lo(&self, d: usize) -> f64 {
self.a[d].min(self.b[d])
/// The low corner of the chord's parameter box.
fn lo(&self) -> [f64; 2] {
[self.a[0].min(self.b[0]), self.a[1].min(self.b[1])]
}
fn hi(&self, d: usize) -> f64 {
self.a[d].max(self.b[d])
/// The high corner of the chord's parameter box.
fn hi(&self) -> [f64; 2] {
[self.a[0].max(self.b[0]), self.a[1].max(self.b[1])]
}
}
/// `q` moved by `by` along parameter direction `d` (0: u, otherwise v).
fn shifted(q: [f64; 2], d: usize, by: f64) -> [f64; 2] {
if d == 0 {
[q[0] + by, q[1]]
} else {
[q[0], q[1] + by]
}
}
@@ -33,64 +45,87 @@ 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. `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_within(s, &dom, q, d, reach));
/// never cross. `reach`: how near a pole a node is taken as one (see
/// [`solidcore::eval::uv::degenerate_within`]); a node whose poles the kernel's classifier
/// cannot tell is the error.
pub(crate) fn crossing(
s: &EvalSurface,
reach: f64,
loops: &[seam::Loop],
) -> Result<Vec<(usize, usize)>, super::Unclassified> {
let dom = solidcore::eval::uv::domain(s);
let pole = |q: [f64; 2]| super::pole_dir(s, q, reach).map(|d| d.is_some());
let mut chords: Vec<Chord> = Vec::new();
for (li, l) in loops.iter().enumerate() {
let n = l.uv.len();
if n < 3 {
continue;
}
// Each node is on a pole or not whichever two chords it ends: asked once.
let mut on_pole: Vec<Option<bool>> = vec![None; n];
let mut node_pole = |k: usize, q: [f64; 2]| -> Result<bool, super::Unclassified> {
if let Some(p) = on_pole.get(k).copied().flatten() {
return Ok(p);
}
let p = pole(q)?;
if let Some(slot) = on_pole.get_mut(k) {
*slot = Some(p);
}
Ok(p)
};
for k in 0..n {
let (Some(&a), Some(&b)) = (l.uv.get(k), l.uv.get((k + 1) % n)) else {
// Proven: n = l.uv.len() >= 3, checked above
let next = (k + 1) % n;
let (Some(&a), Some(&b)) = (l.uv.get(k), l.uv.get(next)) else {
continue;
};
if !(a.iter().chain(&b).all(|x| x.is_finite())) || pole(a) || pole(b) {
if !(a.iter().chain(&b).all(|x| x.is_finite()))
|| node_pole(k, a)?
|| node_pole(next, b)?
{
continue;
}
// The loop's closing chord of a loop that winds round the surface ends at the copy of
// its first node nearest its last; every chord then starts in one period.
let (mut a, mut b) = (a, b);
for d in 0..2 {
let Some(per) = dom.period[d] else { continue };
b[d] = uv::unwrap(b[d], a[d], Some(per));
let back = (a[d].min(b[d]) / per).floor() * per;
a[d] -= back;
b[d] -= back;
for ((x, y), per) in a.iter_mut().zip(&mut b).zip(dom.period) {
let Some(per) = per else { continue };
*y = solidcore::eval::uv::unwrap(*y, *x, Some(per));
let back = (x.min(*y) / per).floor() * per;
*x -= back;
*y -= back;
}
chords.push(Chord { at: (li, k), a, b });
}
}
// With every chord starting in one period, two chords' copies can only cross where one of
// them is moved a period along at most: each chord also as its copies a period along.
for d in 0..2 {
let Some(per) = dom.period[d] else { continue };
for (d, per) in dom.period.into_iter().enumerate() {
let Some(per) = per else { continue };
let along: Vec<Chord> = chords
.iter()
.map(|c| {
let (mut a, mut b) = (c.a, c.b);
a[d] += per;
b[d] += per;
Chord { at: c.at, a, b }
.map(|c| Chord {
at: c.at,
a: shifted(c.a, d, per),
b: shifted(c.b, d, per),
})
.collect();
chords.extend(along);
}
let adjacent = |p: (usize, usize), q: (usize, usize)| {
let n = loops.get(p.0).map_or(0, |l| l.uv.len());
// Proven: p and q are chords' places, whose loops have n >= 3 nodes (no others make chords)
p == q || (p.0 == q.0 && ((p.1 + 1) % n == q.1 || (q.1 + 1) % n == p.1))
};
// Sweep along u: only chords whose u ranges overlap can cross.
chords.sort_by(|x, y| x.lo(0).total_cmp(&y.lo(0)));
chords.sort_by(|x, y| x.lo()[0].total_cmp(&y.lo()[0]));
let mut out: Vec<(usize, usize)> = Vec::new();
for (i, x) in chords.iter().enumerate() {
for y in chords.iter().skip(i + 1) {
if y.lo(0) > x.hi(0) {
if y.lo()[0] > x.hi()[0] {
break;
}
if y.lo(1) > x.hi(1) || x.lo(1) > y.hi(1) || adjacent(x.at, y.at) {
if y.lo()[1] > x.hi()[1] || x.lo()[1] > y.hi()[1] || adjacent(x.at, y.at) {
continue;
}
if cross(x.a, x.b, y.a, y.b) {
@@ -114,7 +149,7 @@ pub(crate) fn crossing(s: &EvalSurface, reach: f64, loops: &[seam::Loop]) -> Vec
}
out.sort_unstable();
out.dedup();
out
Ok(out)
}
/// Whether `q` lies on the segment `ab` within [`STRADDLE`] (of the segment's squared length,
@@ -122,7 +157,7 @@ pub(crate) fn crossing(s: &EvalSurface, reach: f64, loops: &[seam::Loop]) -> Vec
fn on(a: [f64; 2], b: [f64; 2], q: [f64; 2]) -> bool {
let (dx, dy) = (b[0] - a[0], b[1] - a[1]);
let len2 = dx * dx + dy * dy;
if !(len2 > 0.0) {
if len2.partial_cmp(&0.0) != Some(Ordering::Greater) {
return false;
}
let (qx, qy) = (q[0] - a[0], q[1] - a[1]);
+691 -29
View File
@@ -170,22 +170,16 @@ fn min_angle(m: &Mesh) -> f64 {
least
}
/// A flat face asks for no size: a box's faces are meshed as their boundary alone, two
/// triangles each (SolidCraft #13266), at the display and the export tolerance alike.
#[test]
fn a_box_is_two_triangles_a_face() {
for name in ["box_40x30x20", "box_2x3x4"] {
let b = fixture(name);
let (m, s) = plan_body(&b, 0.05);
assert_eq!(s.planar, 6);
assert_watertight(&m, name);
assert_eq!(m.triangles.len(), 12, "{name}");
// Each face is a rectangle cut along a diagonal: 40 x 20 gives atan(1/2), 26.6 degrees.
assert!(min_angle(&m) > 26.0, "{name}: {}", min_angle(&m));
let size = size_of(name);
let (p, _) = ok(plan(&b, &MeshTolerance::export_of(size), size));
assert_eq!(p.evaluate().0.triangles.len(), 12, "{name} exported");
}
fn a_box_is_a_regular_grid_on_every_face() {
// 40 × 30 × 20: sides at most 1/5 of the diagonal (10.77 mm), so 4 × 3 × 2 cells.
let b = fixture("box_40x30x20");
let (m, s) = plan_body(&b, 0.05);
assert_eq!(s.planar, 6);
assert_watertight(&m, "box");
let cells = 2 * (4 * 3 + 4 * 2 + 3 * 2);
assert_eq!(m.triangles.len(), 2 * cells, "two triangles per grid cell");
assert!(min_angle(&m) > 40.0, "{}", min_angle(&m));
}
#[test]
@@ -422,6 +416,63 @@ fn a_placed_body_meshes_like_the_original() {
assert_eq!(m.tri_face, p.tri_face);
}
/// Seeds on a motorcycle trace start at the spacing of the boundary it leaves and grow
/// geometrically to the spacing at its other end (here 0.1 to 1 over 5 mm).
#[test]
fn trace_seeds_grade_from_one_end_to_the_other() {
let t = graded(
[0.0, 0.0],
[5.0, 0.0],
0.1,
1.0,
10.0,
[1.0, 1.0],
&|_, _| Ok(true),
)
.unwrap();
let all: Vec<f64> = std::iter::once(0.0)
.chain(t.iter().copied())
.chain(std::iter::once(1.0))
.map(|f| f * 5.0)
.collect();
let sizes: Vec<f64> = all.windows(2).map(|w| w[1] - w[0]).collect();
assert!((sizes[0] - 0.1).abs() < 0.03, "first {}", sizes[0]);
assert!(
(sizes[sizes.len() - 1] - 1.0).abs() < 0.3,
"last {}",
sizes[sizes.len() - 1]
);
assert!(
sizes.windows(2).all(|w| w[1] >= w[0] - 1e-12),
"growing: {sizes:?}"
);
// Equal ends: even; a segment no longer than its spacing: no seeds.
let even = graded(
[0.0, 0.0],
[4.0, 0.0],
1.0,
1.0,
10.0,
[1.0, 1.0],
&|_, _| Ok(true),
)
.unwrap();
assert_eq!(even.len(), 3);
assert!(
graded(
[0.0, 0.0],
[0.5, 0.0],
1.0,
1.0,
10.0,
[1.0, 1.0],
&|_, _| Ok(true)
)
.unwrap()
.is_empty()
);
}
/// Whether face `face` of `b` is a plane, cylinder, cone, sphere or torus in its record (the faces
/// [`max_chord`] measures).
fn analytic(b: &brep::Brep, face: usize) -> bool {
@@ -766,6 +817,32 @@ fn a_hole_far_below_the_tolerance_keeps_its_area() {
);
}
/// Boundary nodes at the very same (u, v) are one vertex to the triangulation: two holes that
/// touch at a vertex they share (a ring standing on a perforated plate, tangent to the holes
/// beside it) bound a face the triangles follow, though each hole has its own copy of the node.
#[test]
fn loops_touching_at_a_shared_vertex_bound_the_face() {
let outer = [[0.0, 0.0], [4.0, 0.0], [4.0, 4.0], [0.0, 4.0]];
let left = [[1.0, 1.0], [1.0, 3.0], [2.0, 2.0]];
let right = [[2.0, 2.0], [3.0, 3.0], [3.0, 1.0]];
let uvs: Vec<[f64; 2]> = outer.iter().chain(&left).chain(&right).copied().collect();
let fixed: Vec<Vec3> = uvs.iter().map(|q| Vec3::new(q[0], q[1], 0.0)).collect();
let ranges = vec![0..4, 4..7, 7..10];
let fill = cdt::Fill {
seeds: &[],
refine: None,
shape: false,
lenient: false,
};
let Ok(Some((_, triangles))) = cdt::triangulate(&uvs, &ranges, fill) else {
panic!("triangulates")
};
assert!(
closed_boundary(&ranges, &fixed, &uvs, &triangles),
"the triangles follow both holes through their shared vertex"
);
}
/// A face meeting itself along a line (a hole's wall tangent inside the wall round it, cut from
/// the boss side's top rim down to the hole's bottom): its loop runs up the slit and back down
/// it, with the face on both sides. The iso-line grid's band beside the slit is the face on
@@ -843,6 +920,175 @@ fn a_thin_lune_whose_chords_cross_meshes() {
assert!(volume(&m) > 0.0, "inside out");
}
/// A doubly curved patch 40 mm across, and a loop on it in (u, v) whose right side folds back
/// past itself near the top: the last chord of the fold crosses the side, as a cusp or a sliver
/// whose sides sag across each other leaves a face's boundary.
fn a_face_crossing_itself() -> (EvalSurface, seam::Loop, size::Rules) {
let mut ctrl = Vec::new();
for j in 0..3 {
for i in 0..3 {
let bump = if i == 1 && j == 1 { 12.0 } else { 0.0 };
ctrl.push([20.0 * f64::from(i), 20.0 * f64::from(j), bump, 1.0]);
}
}
let s = EvalSurface::nurbs(
[2, 2],
[vec![0., 0., 0., 1., 1., 1.], vec![0., 0., 0., 1., 1., 1.]],
[3, 3],
ctrl,
);
let corners = [
[0.1, 0.1],
[0.9, 0.1],
[0.9, 0.7],
[0.7, 0.95],
[0.86, 0.8],
[0.1, 0.9],
];
let mut uv: Vec<[f64; 2]> = Vec::new();
for k in 0..corners.len() {
let (a, b) = (corners[k], corners[(k + 1) % corners.len()]);
for t in 0..8 {
let f = f64::from(t) / 8.0;
uv.push([a[0] + (b[0] - a[0]) * f, a[1] + (b[1] - a[1]) * f]);
}
}
let pts: Vec<Vec3> = uv
.iter()
.filter_map(|q| {
s.derivs(q[0], q[1])
.map(kernel::record_triple)
.map(|(p, _, _)| p)
})
.collect();
assert_eq!(pts.len(), uv.len());
(
s,
seam::Loop { uv, pts },
size::Rules::new(0.05, 40.0, None),
)
}
/// A curved face crossing itself in (u, v) is refused by every method; the lenient fill leaves
/// the crossing side out and plans the rest in (u, v), with points inside the face on its surface
/// (the GPU evaluates them there), not flat between boundary nodes.
#[test]
fn a_curved_face_crossing_itself_is_filled_on_its_surface() {
let (s, l, rules) = a_face_crossing_itself();
let size = size::face_size(&s, &solidcore::eval::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,
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.
let mesh = evaluated(&TessPlan {
surfaces: vec![s.clone()],
faces: vec![FacePlan {
surface: Some(0),
..fp.clone()
}],
..TessPlan::default()
});
for (q, p) in fp.uv.iter().zip(&mesh.positions).skip(fp.fixed.len()) {
let Some((on, _, _)) = s.derivs(q[0], q[1]).map(kernel::record_triple) else {
panic!("evaluates at {q:?}")
};
assert!(on.dist(*p) < 1e-9, "{q:?}");
}
}
/// The face filled leniently is named in the plan's stats with why the strict plan refused it, and
/// counted under no method: the display has no hole, and the defect is still reported.
#[test]
fn a_face_filled_leniently_is_named_in_the_stats() {
let (s, l, rules) = a_face_crossing_itself();
let size = size::face_size(&s, &solidcore::eval::uv::domain(&s), &rules);
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)
};
assert_eq!(lenient.face, 7);
assert!(
lenient.reason.contains("face 7 (spline)"),
"{}",
lenient.reason
);
}
/// A face whose boundary still crosses itself after untangling (two edges leaving a vertex
/// tangentially: their chords cross however often they are halved, as on one of Poppy
/// Humanoid's faces) is filled leniently straight away with that reason, and named: the body's
/// other faces are planned as usual instead of the whole body failing with it.
#[test]
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, &solidcore::eval::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,
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);
assert!(lenient.reason.contains(why), "{}", lenient.reason);
}
#[test]
fn a_face_bounded_by_two_short_arcs_keeps_an_area_to_mesh() {
// A lens on z = 0 between two radius-1000 circles crossing at (±10, 0), in a record whose
@@ -1054,6 +1300,128 @@ fn a_small_sphere_void_in_a_large_block_meshes() {
);
}
/// A sheet whose section is an S (a cubic 30 long, 1.5 off its chord each way, its middle on the
/// chord), straight across, meshed as fans with one triangle flat on the chord across its middle
/// (A, B at the sides, C a hair above the chord a third along). The chord's middle lies on the
/// surface while the triangle's short sides sag: splitting those left a child with the chord and
/// its vertex nearer to it, without end (a loft side of a bike frame refined to slivers until the
/// point budget ran out, its area counted three times). The flat triangle now splits the chord,
/// and refinement ends within its budget with no sliver.
#[test]
fn sag_refinement_across_an_inflection_ends_without_slivers() {
let mut ctrl = Vec::new();
for j in 0..2 {
for (i, z) in [0.0, 4.0, -4.0, 0.0].into_iter().enumerate() {
ctrl.push([10.0 * i as f64, 10.0 * f64::from(j), z, 1.0]);
}
}
let s = EvalSurface::nurbs(
[3, 1],
[vec![0., 0., 0., 0., 1., 1., 1., 1.], vec![0., 0., 1., 1.]],
[4, 2],
ctrl,
);
// Boundary: bottom and top rows of n + 1 nodes (their sides within the tolerance, 0.5), A and
// B halfway up the ends. Inside: C. Bottom fan from A, the triangle A B C, top fan from C.
let n = 6u32;
let mut uv: Vec<[f64; 2]> = Vec::new();
for i in 0..=n {
uv.push([f64::from(i) / f64::from(n), 0.0]);
}
for i in 0..=n {
uv.push([f64::from(i) / f64::from(n), 1.0]);
}
let (bottom, top) = (0u32, n + 1);
let (a, b, c) = (2 * n + 2, 2 * n + 3, 2 * n + 4);
uv.extend([[0.0, 0.5], [1.0, 0.5], [1.0 / 3.0, 0.5 + 1e-3]]);
let mut tris: Vec<[u32; 3]> = vec![[a, bottom, bottom + 1]];
tris.extend((1..n).map(|i| [a, bottom + i, bottom + i + 1]));
tris.push([a, bottom + n, b]);
tris.push([a, b, c]);
tris.push([c, b, top + n]);
tris.extend((0..n).rev().map(|i| [c, top + i + 1, top + i]));
tris.push([c, top, a]);
// The sides along the ends are straight.
let fixed: Vec<Vec3> = Vec::new();
let tol = 0.5;
let start = uv.len();
sag::refine(
&s,
tol,
&mut sag::Poles::new(&s, 1e-9),
&fixed,
&mut uv,
&mut tris,
20_000,
)
.unwrap();
assert!(
uv.len() - start < 1_000,
"{} points added: refinement ran to its budget",
uv.len() - start
);
for t in &tris {
let [p, q, r] = t.map(|i| uv[i as usize]);
let area = (q[0] - p[0]) * (r[1] - p[1]) - (q[1] - p[1]) * (r[0] - p[0]);
let longest = [(p, q), (q, r), (r, p)]
.iter()
.map(|(x, y)| (x[0] - y[0]).hypot(x[1] - y[1]))
.fold(0.0, f64::max);
assert!(
area > 1e-6 * longest * longest,
"a sliver {t:?}: {p:?} {q:?} {r:?}"
);
}
}
/// A sheet curved across (a parabola 20 wide, 3.2 high at a tenth in from either side) and two
/// triangles standing on a chord across the curve: their corners on the surface, their planes
/// across it, so they count far more than the area under them. The flip lays them along the
/// sheet, on the straight side along it.
#[test]
fn triangles_standing_across_a_curved_sheet_are_flipped_along_it() {
let mut ctrl = Vec::new();
for j in 0..2 {
for (i, z) in [5.0, -5.0, 5.0].into_iter().enumerate() {
ctrl.push([10.0 * i as f64 - 10.0, 10.0 * f64::from(j), z, 1.0]);
}
}
let s = EvalSurface::nurbs(
[2, 1],
[vec![0., 0., 0., 1., 1., 1.], vec![0., 0., 1., 1.]],
[3, 2],
ctrl,
);
let uv = vec![[0.1, 0.5], [0.9, 0.5], [0.5, 0.52], [0.5, 0.48]];
let mut tris = vec![[0, 1, 2], [1, 0, 3]];
let area = |tris: &[[u32; 3]]| -> f64 {
tris.iter()
.map(|t| {
let [a, b, c] = t.map(|i| {
let q = uv[i as usize];
match s.eval(q[0], q[1]) {
Ok(pn) => kernel::record_pair(pn).0,
Err(e) => panic!("evaluates at {q:?}: {e:?}"),
}
});
(b - a).cross(c - a).len() / 2.0
})
.sum()
};
let before = area(&tris);
sag::flip(&s, 0.1, &mut sag::Poles::new(&s, 1e-9), &[], &uv, &mut tris).unwrap();
let after = area(&tris);
let sides: Vec<(u32, u32)> = tris
.iter()
.flat_map(|t| (0..3).map(move |k| (t[k].min(t[(k + 1) % 3]), t[k].max(t[(k + 1) % 3]))))
.collect();
assert!(
sides.contains(&(2, 3)) && !sides.contains(&(0, 1)),
"{tris:?}"
);
assert!(after < 0.5 * before, "{before} -> {after}");
}
/// A disc with a boss standing on it tangent inside its rim: the disc's top is a ring whose inner
/// circle touches the outer one, one loop through the touching vertex. There both circles leave
/// along one tangent, and with the outer chord twice the inner (as their samplings fall) the inner
@@ -1123,7 +1491,6 @@ fn move_pcurve(b: &mut brep::Brep, face: usize, du: f64) -> u32 {
/// A box one of whose faces has a pcurve 1 mm off its edge: that face is refused, naming the
/// face and the edge, and the other five are meshed. The pcurve's error was the whole body's.
#[test]
#[ignore = "RFC 0003 item 1.2: solidcore-tess's refusing one face whose pcurve is off its edge, not yet in this base"]
fn a_face_whose_pcurve_is_off_its_edge_is_refused_and_the_rest_meshed() {
let mut b = fixture("box_10x6x4");
let edge = move_pcurve(&mut b, 2, 1.0);
@@ -1149,7 +1516,6 @@ fn a_face_whose_pcurve_is_off_its_edge_is_refused_and_the_rest_meshed() {
/// bounds are refused, each naming itself and the edge, with no nodes for the edge. The other
/// four faces are meshed. The edge's error was the whole body's.
#[test]
#[ignore = "RFC 0003 item 1.2: solidcore-tess's refusing the faces of an edge that cannot be sampled, not yet in this base"]
fn an_edge_that_cannot_be_sampled_refuses_both_its_faces_by_name() {
let mut b = fixture("box_10x6x4");
let ei = 3;
@@ -1187,7 +1553,6 @@ fn an_edge_that_cannot_be_sampled_refuses_both_its_faces_by_name() {
/// An edge that bounds no face (a free edge beside a box) and cannot be sampled is left out of
/// the plan and named in the stats with why; the box is meshed whole, not refused as a body.
#[test]
#[ignore = "RFC 0003 item 1.2: solidcore-tess's naming a free edge that cannot be sampled, not yet in this base"]
fn a_free_edge_that_cannot_be_sampled_is_named_and_the_body_meshed() {
let mut b = fixture("box_10x6x4");
let mut free = b.edges[3].clone();
@@ -1217,7 +1582,6 @@ fn a_free_edge_that_cannot_be_sampled_is_named_and_the_body_meshed() {
/// refused, each naming it, and the other four are meshed. It was taken for a free edge, and
/// each face's loop closed with a chord across the gap.
#[test]
#[ignore = "RFC 0003 item 1.2: solidcore-tess's edge-face lists from the faces' loops, not yet in this base"]
fn an_edge_left_out_of_its_uses_but_in_its_faces_loops_refuses_them() {
let mut b = fixture("box_10x6x4");
let ei = 3;
@@ -1270,7 +1634,6 @@ fn a_body_none_of_whose_faces_plan_is_the_first_faces_error() {
/// naming the loop, and the rest of the body is meshed. The loop was left out, and the face
/// meshed without it.
#[test]
#[ignore = "RFC 0003 item 1.2: solidcore-tess's refusing a face with a loop of fewer than two nodes, not yet in this base"]
fn a_face_with_a_loop_of_fewer_than_two_nodes_is_refused_naming_the_loop() {
let mut b = fixture("box_10x6x4");
b.faces[4].loops.push(Loop::Edges { edges: Vec::new() });
@@ -1290,6 +1653,42 @@ fn a_face_with_a_loop_of_fewer_than_two_nodes_is_refused_naming_the_loop() {
assert_eq!(meshed_faces(&p), vec![0, 1, 2, 3, 5]);
}
/// A point a whole number of periods along from inside a face's loops is inside them however
/// many periods the loops span: a strip winding 100 turns has the point's 70th copy inside it.
/// Past 64 periods the copies were not looked through, and the point was "not inside"; past
/// [`MAX_PERIODS`] it is an error, never "not inside".
#[test]
fn a_point_inside_a_face_winding_many_periods_is_found() {
let per = std::f64::consts::TAU;
// A long strip u in [0, 100 periods], v in [0, 1], and a point at u = 0.5 (its copy 70
// periods along lies inside).
let strip = |turns: f64| {
vec![
[0.0, 0.0],
[turns * per, 0.0],
[turns * per, 1.0],
[0.0, 1.0],
]
};
let long = strip(100.0);
let polys = [long.as_slice()];
let bbox = [(0.0, 100.0 * per), (0.0, 1.0)];
let q = [0.5 - 70.0 * per, 0.5];
assert_eq!(within_loops(&polys, bbox, [Some(per), None], q), Ok(true));
// Off the strip across it: not inside, at any copy.
assert_eq!(
within_loops(&polys, bbox, [Some(per), None], [0.5, 1.5]),
Ok(false)
);
let longer = strip(5000.0);
let polys = [longer.as_slice()];
let bbox = [(0.0, 5000.0 * per), (0.0, 1.0)];
assert!(matches!(
within_loops(&polys, bbox, [Some(per), None], q),
Err(TooManyCopies(n)) if n > 4096.0
));
}
/// A plan keeps the record's order: face `i` of the plan is the record's face `i`, and edge `e`'s
/// polyline and faces are the record's edge `e`'s, the polyline from its start vertex to its end
/// vertex and the faces in the order of the edge's uses.
@@ -1341,7 +1740,6 @@ fn faces_and_edges_keep_the_record_order() {
/// lines to fit, and the halves' meshes add up to the coil's closed form, π r² · 2πR · turns,
/// within 1e-3 (SMLib's wall is a spline within a ten-thousandth of the pitch).
#[test]
#[ignore = "RFC 0003 item 1.2: solidcore-tess's naming faces a memory bound limited, not yet in this base"]
fn the_halves_of_a_split_smlib_coil_mesh_as_the_whole() {
let analytic = PI * 2.0 * PI * 20.0 * 6.0;
let mut halves = 0.0;
@@ -1380,11 +1778,33 @@ fn the_halves_of_a_split_smlib_coil_mesh_as_the_whole() {
);
}
/// A face past [`MAX_FACE_NODES`] (its boundary alone, or with what refinement may add) is
/// refused with its count, where every u32 index of it would no longer be proven to fit.
#[test]
fn a_face_past_the_node_cap_is_refused() {
assert_eq!(too_many_nodes(MAX_FACE_NODES, 0), None);
assert_eq!(
too_many_nodes(MAX_FACE_NODES - MAX_REFINE, MAX_REFINE),
None
);
let Some(why) = too_many_nodes(MAX_FACE_NODES + 1, 0) else {
panic!("refused")
};
assert!(
why.starts_with(&format!("its {} boundary nodes", MAX_FACE_NODES + 1)),
"{why}"
);
let Some(why) = too_many_nodes(MAX_FACE_NODES - MAX_REFINE + 1, MAX_REFINE) else {
panic!("refused")
};
assert!(why.contains("with the 20000 refinement may add"), "{why}");
assert!(too_many_nodes(usize::MAX, MAX_REFINE).is_some());
}
/// A cylinder face without its NURBS form (a label alone, an unvalidated record): the kernel's
/// evaluator keeps it in its closed form, which tess does not mesh, and the face is refused,
/// naming it, through `plan()`; its two end discs are meshed.
#[test]
#[ignore = "RFC 0003 item 1.2: solidcore-tess's refusing one face whose surface does not evaluate, not yet in this base"]
fn a_face_whose_surface_does_not_evaluate_is_refused_through_plan() {
let mut b = fixture("cylinder_r10_h5");
assert!(b.faces[0].nurbs.is_some());
@@ -1412,12 +1832,188 @@ fn a_face_whose_surface_does_not_evaluate_is_refused_through_plan() {
assert_eq!(p.faces[0].surface, None);
}
/// A face with a loop of one edge use that leaves a single node (a second loop of one short
/// straight edge on it, 1 µm long, a single segment): its boundary is refused, naming the loop.
#[test]
fn a_face_with_a_loop_of_one_node_is_refused_naming_the_loop() {
let mut b = fixture("box_10x6x4");
// Edge 2 runs (10, 0, 4) to (10, 6, 4); face 4 runs it backwards, its pcurve from (0, 4) to
// (6, 4). A copy 1 µm long from its start, to a vertex of its own.
let mut short = b.edges[2].clone();
let (start, end) = ([10.0, 0.0, 4.0], [10.0, 1e-3, 4.0]);
short.curve = Curve::Line { start, end };
short.uses.clear();
let v = u32::try_from(b.vertices.len()).unwrap();
let mut vertex = b.vertices[short.vertices[0] as usize].clone();
vertex.point = end;
b.vertices.push(vertex);
short.vertices[1] = v;
let e = u32::try_from(b.edges.len()).unwrap();
b.edges.push(short);
let one = brep::OrientedEdge {
edge: e,
forward: true,
pcurve: brep::Curve2::Spline {
knots: vec![0.0, 0.0, 1.0, 1.0],
points: vec![[0.0, 4.0], [1e-3, 4.0]],
},
};
b.faces[4].loops.push(Loop::Edges { edges: vec![one] });
// Through plan() the loop's edge is split to three segments first (split_thin_loops), so the
// boundary is built from the edges as sampled.
let rules = size::Rules::new(0.01, 12.0, None);
let nodes: Vec<edges::Nodes> = (0..b.edges.len())
.map(|e| ok(edges::sample_edge(&b, e, &rules, rules.max_len)))
.collect();
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) {
Err(e) => assert!(
e.to_string()
.ends_with("face 4 (plane): loop 1 has 1 node (1 edge uses)"),
"{e}"
),
Ok(fb) => panic!("{} loops", fb.loops.len()),
}
}
/// A loop's edge that cannot be split where `split_thin_loops` halves it (a spline with no point at
/// its middle, its nodes only its ends): the edge is returned with its error, for every face of
/// it to be refused. Through `plan()` the sampling meets the middle first.
#[test]
fn a_thin_loops_edge_that_cannot_be_split_is_returned_with_why() {
let mut b = fixture("box_10x6x4");
// x = 3, z = 40 t, its weight's double root at t = 0.5 (0 / 0 there).
let mut bad = b.edges[2].clone();
bad.curve = Curve::RationalSpline {
knots: vec![0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0],
homogeneous: vec![
[9.0, 0.0, 0.0, 3.0],
[-3.0, 0.0, 40.0, -1.0],
[-3.0, 0.0, -80.0, -1.0],
[9.0, 0.0, 120.0, 3.0],
],
};
bad.nurbs = None;
bad.uses.clear();
let e = b.edges.len();
b.edges.push(bad);
let one = brep::OrientedEdge {
edge: u32::try_from(e).unwrap(),
forward: true,
pcurve: brep::Curve2::Spline {
knots: vec![0.0, 0.0, 1.0, 1.0],
points: vec![[0.0, 4.0], [1.0, 4.0]],
},
};
b.faces[4].loops.push(Loop::Edges { edges: vec![one] });
let rules = size::Rules::new(0.01, 12.0, None);
let mut nodes: Vec<edges::Nodes> = (0..e)
.map(|k| ok(edges::sample_edge(&b, k, &rules, rules.max_len)))
.collect();
nodes.push(edges::Nodes {
points: vec![Vec3::new(3.0, 0.0, 0.0), Vec3::new(3.0, 0.0, 40.0)],
params: vec![0.0, 1.0],
snapped: Vec::new(),
capped: None,
});
let failed = split_thin_loops(&b, &mut nodes, &[false; 6]);
let [(edge, why)] = failed.as_slice() else {
panic!("{failed:?}")
};
assert_eq!(*edge, e);
assert!(
why.to_string()
.contains(&format!("edge {e}: the curve does not evaluate at t = 0.5")),
"{why}"
);
}
/// A face's crossing chords are halved on their edge; an edge that cannot take the new nodes
/// (here past `MAX_EDGE_NODES`) refuses every face of it, each naming the edge, and untangle
/// passes them over. (A box face's boundary made to cross itself by moving one of its nodes.)
#[test]
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
.faces
.iter()
.map(|f| surface::eval_surface(f.parameters()).ok())
.collect();
let mut edge_faces: Vec<Vec<usize>> = vec![Vec::new(); b.edges.len()];
for (fi, f) in b.faces.iter().enumerate() {
for oe in face_edges(f) {
if !edge_faces[oe.edge as usize].contains(&fi) {
edge_faces[oe.edge as usize].push(fi);
}
}
}
let mut nodes: Vec<edges::Nodes> = (0..b.edges.len())
.map(|e| ok(edges::sample_edge(&b, e, &rules, 1.0)))
.collect();
let face_boundary = |fi: usize, nodes: &[edges::Nodes]| {
boundary(
&b,
fi,
&b.faces[fi],
evals[fi].as_ref().unwrap(),
nodes,
&rules,
)
};
let mut boundaries: Vec<Boundary> = (0..b.faces.len())
.map(|fi| ok(face_boundary(fi, &nodes)))
.collect();
// Face 4's loop with its node 1 moved past the face's opposite side, through its middle.
let uv = &mut boundaries[4].loops[0].uv;
let m = uv.len() as f64;
let c = uv
.iter()
.fold([0.0, 0.0], |a, q| [a[0] + q[0] / m, a[1] + q[1] / m]);
uv[1] = [
c[0] + 1.5 * (c[0] - uv[1][0]),
c[1] + 1.5 * (c[1] - uv[1][1]),
];
let fb = &boundaries[4];
let crossing = tangle::crossing(evals[4].as_ref().unwrap(), fb.reach, &fb.loops).unwrap();
let Some(&(l, k)) = crossing.first() else {
panic!("the swapped loop crosses itself")
};
let e = fb.segments[l][k].0;
// The edge full: one more node is past the cap.
let n = &mut nodes[e as usize];
let (p, t) = (*n.points.last().unwrap(), *n.params.last().unwrap());
n.points.resize(edges::MAX_EDGE_NODES, p);
n.params.resize(edges::MAX_EDGE_NODES, t);
let mut refused = Refused::new(&b);
let tangled = untangle(
&b,
&evals,
&edge_faces,
rules.tol,
&mut nodes,
&mut boundaries,
&face_boundary,
&mut refused,
);
for &f in &edge_faces[e as usize] {
assert!(refused.is(f), "face {f}");
let why = refused.why[f].as_ref().unwrap().to_string();
assert!(
why.contains(&format!("edge {e}: more than 20000 nodes")),
"{why}"
);
assert!(tangled.iter().all(|(t, _)| *t != f), "{tangled:?}");
}
}
/// A torus (radii 6 and 1.5) at a chord of 2e-4 mm, no facet angle: its grid fits, but its sag
/// refinement adds all the points it may ([`MAX_REFINE`]) with triangles still off the surface,
/// and the face is still meshed and named in [`PlanStats::over_tolerance`] with that bound and
/// the sag its triangles reach, past the tolerance. The refinement stopped there silently.
#[test]
#[ignore = "RFC 0003 item 1.2: solidcore-tess's naming faces a memory bound limited, not yet in this base"]
fn a_face_whose_refinement_reaches_its_bound_is_named_with_its_sag() {
let tol = 2e-4;
let b = fixture("torus_6_1_5");
@@ -1438,11 +2034,78 @@ fn a_face_whose_refinement_reaches_its_bound_is_named_with_its_sag() {
assert!(!p.faces[0].triangles.is_empty());
}
/// A crowded face's curved edge whose sampling at the coarser angle fails (a spline with no
/// point at its middle, which its strict nodes, filled in here, never reached): the edge keeps
/// its strict nodes, refuses no face, and is named with the coarser sampling's error.
#[test]
fn an_edge_whose_coarser_sampling_fails_keeps_its_nodes_and_is_named() {
let mut b = fixture("box_10x6x4");
// x = 3, z = 40 t, its weight's double root at t = 0.5 (0 / 0 there).
let mut bad = b.edges[2].clone();
bad.curve = Curve::RationalSpline {
knots: vec![0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0],
homogeneous: vec![
[9.0, 0.0, 0.0, 3.0],
[-3.0, 0.0, 40.0, -1.0],
[-3.0, 0.0, -80.0, -1.0],
[9.0, 0.0, 120.0, 3.0],
],
};
bad.nurbs = None;
bad.uses.clear();
let e = b.edges.len();
b.edges.push(bad);
let one = brep::OrientedEdge {
edge: u32::try_from(e).unwrap(),
forward: true,
pcurve: brep::Curve2::Spline {
knots: vec![0.0, 0.0, 1.0, 1.0],
points: vec![[0.0, 4.0], [1.0, 4.0]],
},
};
b.faces[4].loops.push(Loop::Edges { edges: vec![one] });
let rules = size::Rules::new(
0.01,
12.0,
Some(size::Turn {
angle: crate::DISPLAY_ANGLE,
floor: 1e-5,
}),
);
let mut nodes: Vec<edges::Nodes> = (0..e)
.map(|k| ok(edges::sample_edge(&b, k, &rules, rules.max_len)))
.collect();
// Strict nodes: more segments than a face may have, none at t = 0.5 (an odd count).
let count = MAX_ANGLE_BOUNDARY + 1;
let params: Vec<f64> = (0..=count).map(|k| k as f64 / count as f64).collect();
let points: Vec<Vec3> = params
.iter()
.map(|t| Vec3::new(3.0, 0.0, 40.0 * t))
.collect();
nodes.push(edges::Nodes {
points: points.clone(),
params: params.clone(),
snapped: Vec::new(),
capped: None,
});
let jobs: Vec<(usize, f64)> = (0..=e).map(|k| (k, rules.max_len)).collect();
let kept = relax_crowded_faces(&b, &jobs, &rules, &mut nodes, &[false; 6]);
let [RefusedEdge { edge, reason }] = kept.as_slice() else {
panic!("{kept:?}")
};
assert_eq!(*edge, e);
assert!(
reason.contains(&format!("edge {e}: the curve does not evaluate at t = 0.5")),
"{reason}"
);
assert_eq!(nodes[e].params, params);
assert_eq!(nodes[e].points, points);
}
/// An edge that cannot be sampled, whose uses also list a face that does not run it (a stale
/// use, an unvalidated record): only the two faces whose loops run it are refused; the third
/// is meshed, and is not among the edge's faces. It was refused with them.
#[test]
#[ignore = "RFC 0003 item 1.2: solidcore-tess's edge-face lists from the faces' loops, not yet in this base"]
fn a_stale_use_does_not_make_a_face_an_edges() {
let mut b = fixture("box_10x6x4");
let ei = 3;
@@ -1469,7 +2132,6 @@ fn a_stale_use_does_not_make_a_face_an_edges() {
/// it to a hair, but no segment across the corner turns less than a quarter turn. The plan names it
/// with that bound, its sag within the tolerance, and how far it exceeds its rules.
#[test]
#[ignore = "RFC 0003 item 1.2: solidcore-tess's naming capped edges, not yet in this base"]
fn an_edge_stopped_at_its_depth_bound_is_named_within_its_tolerance() {
let mut b = fixture("box_10x6x4");
b.edges[2].curve = Curve::Spline {
@@ -1486,15 +2148,15 @@ fn an_edge_stopped_at_its_depth_bound_is_named_within_its_tolerance() {
assert!(capped.excess > 1.0, "{capped:?}");
}
/// The coil's triangle counts are SolidCraft's tess's on the same body (the bake-off's
/// `793-coil-split-by-a-plane-across-its-axis`, its lower half): the planner is SolidCraft's.
/// Refining a spline face moves no triangle count: the memo of the pole classifier only drops the
/// repeated question (the coil took 1,089,434 / 1,091,452 calls at display / export before it).
#[test]
fn the_spline_coil_keeps_its_triangles() {
let name = "smlib_coil_lower_half";
let (b, size) = (fixture(name), size_of(name));
for (t, want) in [
(MeshTolerance::display_of(size), 30_630),
(MeshTolerance::export_of(size), 30_662),
(MeshTolerance::display_of(size), 30_776),
(MeshTolerance::export_of(size), 30_808),
] {
let (p, _) = plan(&b, &t, size).unwrap();
let (m, _) = p.evaluate();
+203 -84
View File
@@ -2,17 +2,25 @@
//! the plan stores are the ones the evaluators use), and the tolerance-driven divisions of a
//! face's parameter box.
use crate::fast::EvalSurface;
pub(crate) use crate::fast::uv::{Domain, degenerate_within, domain, unwrap};
use std::cmp::Ordering;
use crate::vec::VecExt as _;
use solidcore::eval::EvalSurface;
use solidcore_convert as kernel;
use solidcore_record::Vec3;
use super::edges::{QUARTER_TURN, turn};
use super::size::Rules;
use super::{NoValue, TooManyCopies};
/// Division of `range` along direction `d` so that between neighbouring lines the surface sags
/// at most the tolerance from its chord, the chord is at most `h` long and the normal turns
/// within the rules' facet angle, checked along iso-lines across `across`; at most `max_lines`
/// lines, ends included.
/// lines, ends included. A point the surface does not evaluate at, where the check needs one, is
/// the error where it is the face's (`mine`: inside its loops): the surface refuses the face
/// there. The iso-lines run across the face's whole parameter box, which reaches past a trimmed
/// face; a point outside the face measures nothing, as a size sample there does
/// ([`super::size::FaceSize::unevaluated`]), and is passed over.
pub(crate) fn divide(
s: &EvalSurface,
d: usize,
@@ -21,20 +29,66 @@ pub(crate) fn divide(
rules: &Rules,
h: f64,
max_lines: usize,
) -> Vec<f64> {
mine: Mine,
) -> Result<Vec<f64>, Unmeasured> {
let (a0, a1) = range;
if !(a1 > a0) {
return vec![a0, a1];
if a1.partial_cmp(&a0) != Some(Ordering::Greater) {
return Ok(vec![a0, a1]);
}
// The whole range first: the limits split it, and no interval spans more than a quarter
// turn of its iso-lines, so a closed iso-line is never taken for a flat one. Both ends
// exactly: the end lines are the face's own parameter box, which the boundary nodes sit on.
split(&[a0, a1], &limits(s, d, across, rules, h), max_lines)
split(&[a0, a1], &limits(s, d, across, rules, h, mine), max_lines)
}
/// Why a division stopped: a point of the face the surface does not evaluate at, or one it does
/// not evaluate at that `Mine` cannot place inside the face or out (its loops span more periods
/// than it looks through), with the point.
#[derive(Clone, Copy, Debug, PartialEq)]
pub(crate) enum Unmeasured {
NoValue(NoValue),
TooManyCopies(TooManyCopies, [f64; 2]),
}
impl From<NoValue> for Unmeasured {
fn from(e: NoValue) -> Self {
Unmeasured::NoValue(e)
}
}
/// `r`'s value; `None` where the surface does not evaluate at a point that is not the face's
/// (`mine`), which measures nothing; the error where it is, or where `mine` cannot tell.
fn kept<T>(mine: Mine, r: Result<T, NoValue>) -> Result<Option<T>, Unmeasured> {
match r {
Ok(v) => Ok(Some(v)),
Err(NoValue(q)) => match mine(q) {
Ok(false) => Ok(None),
Ok(true) => Err(Unmeasured::NoValue(NoValue(q))),
Err(c) => Err(Unmeasured::TooManyCopies(c, q)),
},
}
}
/// Whether a parameter point is the face's (inside its loops): where the surface does not
/// evaluate at a point that is not, the point measures nothing and is passed over. Its error:
/// the face's loops span too many periods to tell.
pub(crate) type Mine<'a> = &'a (dyn Fn([f64; 2]) -> Result<bool, TooManyCopies> + Sync);
/// A point anywhere is the face's: every point the surface does not evaluate at refuses it.
#[cfg(test)]
#[allow(clippy::unnecessary_wraps)] // a `Mine`
pub(crate) fn everywhere(_: [f64; 2]) -> Result<bool, TooManyCopies> {
Ok(true)
}
/// Steps from an excused end of an iso-line interval towards its other end, looking for the
/// nearest step that is the face's.
const INWARD: u32 = 16;
/// `lines` (increasing, ends included) with every interval that fails the same limits as
/// [`divide`] split until it passes: lines taken from a face's edge nodes meet the edges node for
/// node, but nothing else checks how far the surface turns between them inside the face.
/// node, but nothing else checks how far the surface turns between them inside the face. The
/// error as in [`divide`].
pub(crate) fn refine(
s: &EvalSurface,
d: usize,
@@ -43,21 +97,31 @@ pub(crate) fn refine(
rules: &Rules,
h: f64,
max_lines: usize,
) -> Vec<f64> {
let flat = limits(s, d, across, rules, h);
mine: Mine,
) -> Result<Vec<f64>, Unmeasured> {
let flat = limits(s, d, across, rules, h, mine);
split(lines, &flat, max_lines)
}
/// Depth-first bisection of the intervals between `starts` until `flat` holds for each.
fn split(starts: &[f64], flat: &dyn Fn(f64, f64) -> bool, max_lines: usize) -> Vec<f64> {
/// Depth-first bisection of the intervals between `starts` until `flat` holds for each; `flat`'s
/// error ends it.
fn split(
starts: &[f64],
flat: &dyn Fn(f64, f64) -> Result<bool, Unmeasured>,
max_lines: usize,
) -> Result<Vec<f64>, Unmeasured> {
let Some(&first) = starts.first() else {
return Vec::new();
return Ok(Vec::new());
};
let mut out = vec![first];
let mut stack: Vec<(f64, f64, u32)> =
starts.windows(2).rev().map(|w| (w[0], w[1], 0)).collect();
let mut stack: Vec<(f64, f64, u32)> = starts
.iter()
.zip(starts.iter().skip(1))
.rev()
.map(|(&a, &b)| (a, b, 0))
.collect();
while let Some((a, b, depth)) = stack.pop() {
if depth >= 20 || out.len() + stack.len() >= max_lines.max(2) || flat(a, b) {
if depth >= 20 || out.len() + stack.len() >= max_lines.max(2) || flat(a, b)? {
out.push(b);
} else {
let m = 0.5 * (a + b);
@@ -65,44 +129,50 @@ fn split(starts: &[f64], flat: &dyn Fn(f64, f64) -> bool, max_lines: usize) -> V
stack.push((a, m, depth + 1));
}
}
out
Ok(out)
}
/// Whether the surface between the parameter points `a` and `b` is within the tolerance (at
/// their middle), the side length `h` and the facet angle: no point is needed between them.
/// their middle), the side length `h` and the facet angle: no point is needed between them. The
/// parameters of a point the check needs and the surface does not evaluate at are the error.
pub(crate) fn fine_between(
s: &EvalSurface,
rules: &Rules,
h: f64,
a: [f64; 2],
b: [f64; 2],
) -> bool {
let at = |q: [f64; 2]| s.derivs(q[0], q[1]);
let (Some((pa, _, _)), Some((pb, _, _))) = (at(a), at(b)) else {
return true;
) -> Result<bool, NoValue> {
let at = |q: [f64; 2]| {
s.derivs(q[0], q[1])
.map(kernel::record_triple)
.ok_or(NoValue(q))
};
let ((pa, _, _), (pb, _, _)) = (at(a)?, at(b)?);
if pa.dist(pb) > h * (1.0 + 1e-9) {
return false;
return Ok(false);
}
let mid = [0.5 * (a[0] + b[0]), 0.5 * (a[1] + b[1])];
let sag = at(mid).map_or(0.0, |(m, _, _)| m.dist((pa + pb) * 0.5));
let sag = at(mid)?.0.dist((pa + pb) * 0.5);
if sag > rules.tol {
return false;
return Ok(false);
}
let Some(t) = rules.turn else { return Ok(true) };
// At a pole the normal is undefined: take it from just inside the segment.
let normal = |q: [f64; 2], toward: [f64; 2]| {
let n = |q: [f64; 2]| at(q).and_then(|(_, su, sv)| su.cross(sv).normalized());
n(q).or_else(|| {
n([
let normal = |q: [f64; 2], toward: [f64; 2]| -> Result<Option<Vec3>, NoValue> {
let n = |q: [f64; 2]| at(q).map(|(_, su, sv)| su.cross(sv).normalized());
match n(q)? {
Some(n) => Ok(Some(n)),
None => n([
q[0] + 1e-4 * (toward[0] - q[0]),
q[1] + 1e-4 * (toward[1] - q[1]),
])
})
]),
}
};
match (rules.turn, normal(a, b), normal(b, a)) {
(Some(t), Some(na), Some(nb)) => !t.exceeds(na.dot(nb).clamp(-1.0, 1.0).acos(), sag),
Ok(match (normal(a, b)?, normal(b, a)?) {
// Proven: the clamp bounds are the constants -1.0 <= 1.0, neither NaN.
(Some(na), Some(nb)) => !t.exceeds(na.dot(nb).clamp(-1.0, 1.0).acos(), sag),
_ => true,
}
})
}
/// An iso-line's extent below which it is a point, as a fraction of the chord tolerance (which
@@ -112,82 +182,119 @@ const POINT: f64 = 1e-6;
/// Whether the surface between the iso-lines at `a` and `b` (direction `d`, sampled along
/// iso-lines across `across`) is within the tolerance, the side length `h` and the facet angle,
/// and spans at most a quarter turn of each iso-line (as an edge segment does, see
/// [`QUARTER_TURN`]): a turn read from the points, since a spline's parameter is no angle.
/// [`QUARTER_TURN`]): a turn read from the points, since a spline's parameter is no angle. A point
/// the surface does not evaluate at is the error where it is the face's (`mine`), and else
/// measures nothing: an end of an iso-line's interval there is moved inwards, in [`INWARD`]
/// steps, to the nearest step that is the face's, and the interval is measured from there (the
/// surface having no point there refuses the face); an iso-line with no step that is the face's
/// is not measured (a part of the face narrower than a step is left to the sag refinement), and
/// a point between the ends is not taken into its sag, turn or facet angle.
fn limits<'a>(
s: &'a EvalSurface,
d: usize,
across: (f64, f64),
rules: &'a Rules,
h: f64,
) -> impl Fn(f64, f64) -> bool + 'a {
let tol = rules.tol;
let uv = move |x: f64, y: f64| if d == 0 { (x, y) } else { (y, x) };
let at = move |x: f64, y: f64| {
let (u, v) = uv(x, y);
s.derivs(u, v).map(|(p, _, _)| p)
};
let normal = move |x: f64, y: f64| {
let (u, v) = uv(x, y);
s.derivs(u, v)
.and_then(|(_, su, sv)| su.cross(sv).normalized())
};
mine: Mine<'a>,
) -> impl Fn(f64, f64) -> Result<bool, Unmeasured> + 'a {
const ACROSS: usize = 5;
let tol = rules.tol;
let uv = move |x: f64, y: f64| if d == 0 { [x, y] } else { [y, x] };
let derivs = move |x: f64, y: f64| {
let q = uv(x, y);
s.derivs(q[0], q[1])
.map(kernel::record_triple)
.ok_or(NoValue(q))
};
let at = move |x: f64, y: f64| derivs(x, y).map(|(p, _, _)| p);
// `None` at a pole, where the normal is undefined.
let normal = move |x: f64, y: f64| derivs(x, y).map(|(_, su, sv)| su.cross(sv).normalized());
let ys: Vec<f64> = (0..ACROSS)
.map(|k| across.0 + (across.1 - across.0) * k as f64 / (ACROSS - 1) as f64)
.collect();
move |a: f64, b: f64| {
ys.iter().all(|&y| {
let (Some(pa), Some(pb)) = (at(a, y), at(b, y)) else {
return true;
// The end `from` of an interval towards `to` on the iso-line at `y`: itself where it
// evaluates, else the nearest step inwards that is the face's; `None` where no step is.
let end = move |from: f64, to: f64, y: f64| -> Result<Option<(f64, Vec3)>, Unmeasured> {
if let Some(p) = kept(mine, at(from, y))? {
return Ok(Some((from, p)));
}
for k in 1..INWARD {
let x = from + (to - from) * f64::from(k) / f64::from(INWARD);
let q = uv(x, y);
// A step that is the face's is where the interval is measured from, or, the surface
// having no point there, the face's refusal (as `kept` makes it); one that is not the
// face's is passed over.
if mine(q).map_err(|c| Unmeasured::TooManyCopies(c, q))? {
return at(x, y).map(|p| Some((x, p))).map_err(Unmeasured::NoValue);
}
}
Ok(None)
};
move |a0: f64, b0: f64| {
for &y in &ys {
let Some((a, pa)) = end(a0, b0, y)? else {
continue;
};
let Some((b, pb)) = end(b0, a, y)? else {
continue;
};
if pa.dist(pb) > h * (1.0 + 1e-9) {
return false;
return Ok(false);
}
let mut sag: f64 = 0.0;
for f in [0.25, 0.5, 0.75] {
if let Some(p) = kept(mine, at(a + (b - a) * f, y))? {
sag = sag.max(p.dist_to_segment(pa, pb));
}
}
let sag = [0.25, 0.5, 0.75]
.iter()
.filter_map(|f| at(a + (b - a) * f, y))
.map(|q| q.dist_to_segment(pa, pb))
.fold(0.0, f64::max);
if sag > tol {
return false;
return Ok(false);
}
// The two half chords of an arc turning θ differ by θ/2; a closed iso-line's by π.
// An iso-line within a millionth of the tolerance of one point (a pole) turns no
// amount: its directions are rounding.
if let Some(pm) = at(0.5 * (a + b), y)
if let Some(pm) = kept(mine, at(0.5 * (a + b), y))?
&& pa.dist(pm).max(pm.dist(pb)) > POINT * tol
&& 2.0 * turn(pm - pa, pb - pm) > QUARTER_TURN
{
return false;
return Ok(false);
}
let Some(t) = rules.turn else { continue };
// At a pole the normal is undefined: take it from just inside the segment.
let na = normal(a, y).or_else(|| normal(a + 1e-4 * (b - a), y));
let nb = normal(b, y).or_else(|| normal(b - 1e-4 * (b - a), y));
match (rules.turn, na, nb) {
(Some(t), Some(na), Some(nb)) => {
!t.exceeds(na.dot(nb).clamp(-1.0, 1.0).acos(), sag)
}
_ => true,
let normal_near = |x: f64, inward: f64| -> Result<Option<Vec3>, Unmeasured> {
Ok(match kept(mine, normal(x, y))? {
Some(Some(n)) => Some(n),
Some(None) => kept(mine, normal(x + inward, y))?.flatten(),
None => None,
})
};
let na = normal_near(a, 1e-4 * (b - a))?;
let nb = normal_near(b, -1e-4 * (b - a))?;
if let (Some(na), Some(nb)) = (na, nb)
// Proven: the clamp bounds are the constants -1.0 <= 1.0, neither NaN.
&& t.exceeds(na.dot(nb).clamp(-1.0, 1.0).acos(), sag)
{
return Ok(false);
}
})
}
Ok(true)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::fast::EvalCurve;
use solidcore_record::Vec3;
use solidcore::eval::EvalCurve;
fn cylinder() -> EvalSurface {
EvalSurface::Revolved {
profile: EvalCurve {
degree: 1,
knots: vec![0.0, 0.0, 1.0, 1.0],
ctrl: vec![[3.0, 0.0, 0.0, 1.0], [3.0, 0.0, 4.0, 1.0]],
},
origin: Vec3::ZERO,
axis: Vec3::Z,
profile: EvalCurve::new(
1,
vec![0.0, 0.0, 1.0, 1.0],
vec![[3.0, 0.0, 0.0, 1.0], [3.0, 0.0, 4.0, 1.0]],
),
origin: solidcore::Vec3::ZERO,
axis: solidcore::Vec3::Z,
transform: [
[1.0, 0.0, 0.0, 0.0],
[0.0, 1.0, 0.0, 0.0],
@@ -212,7 +319,9 @@ mod tests {
},
f64::INFINITY,
10_000,
);
&everywhere,
)
.unwrap();
let along = divide(
&s,
0,
@@ -225,7 +334,9 @@ mod tests {
},
f64::INFINITY,
10_000,
);
&everywhere,
)
.unwrap();
assert_eq!(along, vec![0.0, 1.0]);
// Chord sag r(1 − cos(θ/2)) ≤ 0.01 with r = 3 needs θ ≤ 0.163: at least 39 segments.
assert!(turn.len() >= 40 && turn.len() < 80, "{}", turn.len());
@@ -242,7 +353,9 @@ mod tests {
},
0.5,
10_000,
);
&everywhere,
)
.unwrap();
assert_eq!(along.len(), 9);
}
@@ -280,8 +393,10 @@ mod tests {
(0.0, 40.0),
&coarse,
f64::INFINITY,
10_000
),
10_000,
&everywhere
)
.unwrap(),
vec![-6.7e-13, q]
);
let whole = divide(
@@ -295,7 +410,9 @@ mod tests {
},
f64::INFINITY,
10_000,
);
&everywhere,
)
.unwrap();
assert_eq!(whole.len(), 5, "{whole:?}");
}
@@ -317,7 +434,9 @@ mod tests {
},
f64::INFINITY,
10_000,
);
&everywhere,
)
.unwrap();
assert_eq!(lines.first().copied(), Some(a0));
assert_eq!(lines.last().copied(), Some(a1));
}
+2 -12
View File
@@ -133,17 +133,6 @@ pub(crate) fn limit_normal(
/// incident triangle its limit normal, else the averaged one, named with the kernel's reason.
/// A boundary node the evaluator refuses otherwise is left to [`crate::TessPlan::evaluate`],
/// which refuses the face there.
/// The first direction (u, then v) `s` is collapsed along at `q` to within `reach`, as the
/// kernel's classifier says; its refusal as its message.
fn pole_dir(s: &EvalSurface, q: [f64; 2], reach: f64) -> Result<Option<usize>, String> {
for d in 0..2 {
if solidcore::eval::uv::degenerate_within(s, q, d, reach).map_err(crate::eval_message)? {
return Ok(Some(d));
}
}
Ok(None)
}
pub(crate) fn plan_poles(
fi: usize,
s: &EvalSurface,
@@ -163,7 +152,8 @@ pub(crate) fn plan_poles(
f.uv.get(k as usize).copied().unwrap_or_default(),
f.fixed.get(k as usize).copied().unwrap_or_default(),
);
let limit = pole_dir(s, q, reach)
let limit = crate::planner::pole_dir(s, q, reach)
.map_err(|e| e.to_string())
.and_then(|d| d.ok_or_else(|| format!("no direction is collapsed at {q:?}")))
.and_then(|along| {
for t in f.triangles.iter().filter(|t| t.contains(&k)) {
-3
View File
@@ -56,7 +56,6 @@ fn pole_nodes(f: &FacePlan) -> Vec<u32> {
/// each triangle's copy of the pole has the closed-form limit normal at its own point of the
/// pole (the mean of its other corners along the collapsed parameter), to rounding.
#[test]
#[ignore = "RFC 0003 item 1.2: solidcore-tess's pole classification by the kernel (a pole of higher order as a pole node), not yet in this base"]
fn a_pole_of_order_two_or_three_has_the_exact_limit_normal_per_triangle() {
for k in Kind::LIMIT {
let (p, stats) = planned(k);
@@ -205,7 +204,6 @@ fn a_pole_of_order_four_is_meshed_with_its_triangles_mean_normal_and_named() {
/// The cone whose apex is of third order (both first derivatives vanish there) is planned like
/// the other faces, 592 triangles a quarter, and meshed whole: its apex takes the limit rule.
#[test]
#[ignore = "RFC 0003 item 1.2: solidcore-tess's layout at a pole of higher order, not yet in this base"]
fn the_cone_apex_of_third_order_is_meshed_on_its_592_triangles() {
let (p, stats) = planned(Kind::Cone);
assert!(stats.unmeshed.is_empty(), "{:?}", stats.unmeshed);
@@ -222,7 +220,6 @@ fn the_cone_apex_of_third_order_is_meshed_on_its_592_triangles() {
/// and 0.02). Its foot is between them, not on the far side of the pole: every copy has the
/// closed-form limit normal at its own point of the pole, to rounding.
#[test]
#[ignore = "RFC 0003 item 1.2: solidcore-tess's pole classification by the kernel (a pole of higher order as a pole node), not yet in this base"]
fn a_pole_across_a_closed_spline_s_end_has_the_limit_normal_between_its_corners() {
let b = pole_faces::closed_cone();
if let Err(e) = b.validate() {
+333 -30
View File
@@ -1123,12 +1123,37 @@ impl AnalyticSupport {
// a surface of revolution: the one the kernel recognises in the form, which must be
// this one, take the form as its NURB and evaluate as the form at the form's own
// parameters
let not_form = || {
SolidError::invalid_input(
OP_BUILD,
"a support's analytic surface is not its NURBS form's parametrisation",
)
};
let surface = self.on_form(k, &form)?;
if !evaluates_as(k, &surface, &form)? {
return Err(not_form());
}
Ok(surface)
}
/// The largest ratio, over the form's Greville points, of this support's analytic
/// surface's distance from `form` to the rounding [`evaluates_as`] derives there (#3185):
/// at most 1 where [`Self::build`]'s parametrisation test passes. The surface is the one
/// `build` makes (the kernel's recognition of `form`, carrying this support's numbers);
/// `lies_on` and the placement's axes are not checked here. `None` for a plane, or a
/// surface the bound does not cover.
///
/// # Errors
///
/// `InvalidInput` where the kernel recognises no surface of this kind in `form`; those of
/// the recognisers, the conversions and the evaluations.
pub fn parametrisation_margin(&self, k: &Kernel, form: &BSplineSurface) -> Result<Option<f64>> {
if matches!(self, Self::Plane { .. }) {
return Ok(None);
}
let surface = self.on_form(k, form)?;
evaluation_margin(k, &surface, form)
}
/// The surface of revolution of this support's kind the kernel recognises in `form`,
/// carrying this support's own numbers and `form` as its NURB ([`Self::build`] after its
/// checks of the point set, before the parametrisation's).
fn on_form(&self, k: &Kernel, form: &BSplineSurface) -> Result<Surface> {
let form = form.copy();
let mut surface = recognise(k, self.kind(), &form)?.ok_or_else(not_form)?;
// the recognised surface must be of this support's kind; the numbers recognition
// recomputed are not compared with the support's, because it recomputes them from
@@ -1178,13 +1203,18 @@ impl AnalyticSupport {
}
_ => return Err(not_form()),
}
if !evaluates_as(k, &surface, &form)? {
return Err(not_form());
}
Ok(surface)
}
}
/// [`AnalyticSupport::build`]'s refusal of a surface that is not its form's parametrisation.
fn not_form() -> SolidError {
SolidError::invalid_input(
OP_BUILD,
"a support's analytic surface is not its NURBS form's parametrisation",
)
}
/// A cylinder's or cone's recognised parametrisation carried by the support's own
/// `placement`: its heights are measured along the axis from the placement's origin, so the
/// recognised heights move by how far along the axis the recognised origin lies from it.
@@ -1259,41 +1289,314 @@ fn recognise(k: &Kernel, kind: SupportKind, form: &BSplineSurface) -> Result<Opt
}
/// Whether `surface` evaluates as `form` at the form's Greville parameters (each control
/// point's), within the rounding of the two evaluations: `gamma_K` times the form's size,
/// `K = 16 (p + q + 2)` for the rational evaluation of degrees `(p, q)` and the analytic
/// one's conversion of the parameters and its trigonometry, a few ulps per operation.
/// point's): at each, the analytic surface's point and the form's agree within the bound
/// [`Rounding::at`] derives for that point, term by term below. The analytic side is the
/// **STEP** evaluation of the converted parameters ([`Surface::convert_uv_from_nurbs_to_step`]
/// and [`Surface::evaluate_step_point`]), which a cone, sphere, torus and surface of
/// revolution compute from their placement, radii, semi-angle and generator.
/// [`Surface::evaluate_point`] would not do: a sphere, a torus and a surface of revolution
/// take it from their NURBS base, and that base is the `form` the caller has just installed,
/// so it would compare the form with itself and read nothing of what the caller imposed
/// (#2588).
///
/// The analytic side is the **STEP** evaluation of the converted parameters
/// ([`Surface::convert_uv_from_nurbs_to_step`] and [`Surface::evaluate_step_point`]),
/// which a cone, sphere, torus and surface of revolution compute from their placement,
/// radii, semi-angle and generator. [`Surface::evaluate_point`] would not do: a sphere, a
/// torus and a surface of revolution take it from their NURBS base, and that base is the
/// `form` the caller has just installed, so it would compare the form with itself and read
/// nothing of what the caller imposed (#2588; the cone already converted and evaluated
/// analytically, which is why only the other kinds went unchecked).
/// What the bound forgives and what it tests (#3185): it forgives the rounding of both
/// evaluations and of the two parameter conversions, nothing else. It does not forgive the
/// label's frame turned about the axis against the converter's, its axis, origin or radii,
/// a form whose parameter is not the analytic surface's (a reparametrised arc), nor the
/// form's distance from the analytic surface: those are what the comparison proves.
///
/// `u` is the unit roundoff, `gamma_n = n u / (1 - n u)`. Stated assumption: the libm
/// `sin`, `cos`, `tan` and `atan2` are within one ulp (relative `2 u`).
///
/// 1. **The form** (`Surface::eval`, Cox-de Boor then two blends): each basis value is a
/// nonnegative combination over `p` levels of at most 6 operations (two knot differences,
/// a sum, a division, a product and a sum), so relative `gamma_6p`; the blends sum
/// `q + 1` then `p + 1` products. With positive weights the homogeneous point's error is
/// at most `gamma_(7 (p + q) + 4) W size` and the weight's `gamma_(7 (p + q) + 4) W`, so
/// the Euclidean point's, after the division, `E_form = (2 gamma_(7 (p + q) + 4) + u)
/// size` (`gamma_(7 (p + q) + 4) size` when polynomial), `size` the net's largest point.
/// 2. **The STEP point**, `O + m (cos u X + sin u Y) + h Z`, `m` and `h` the generator's
/// radial and axial parts (the torus `R + r cos v`, `r sin v`; the sphere `r cos v`,
/// `r sin v`; the cone `r0 + v tan a`, `v`). An angle `x` in degrees goes to radians in
/// two operations of the rounded `PI`: `gamma_3 |x|`; with one ulp of the cosine and sine,
/// each is off by `dc(x) = 2 u + gamma_3 |x|`. Per coordinate (unit axes) the error is at
/// most `gamma_3 (|O| + M + H) + M (gamma_2 + 2 dc(u)) + dm + dh`, `M`, `H` the sizes of
/// `m`, `h` and `dm`, `dh` their errors: the torus `gamma_3 (R + r) + 2 r dc(v)`, the
/// sphere `gamma_1 r + 2 r dc(v)`, the cone `gamma_2 (|r0| + |v t|) + |v| dt` (`dt =
/// (1 + t^2) gamma_3 |a| + 2 u |t|` for the tangent of the half angle `a`); a general
/// surface of revolution rotates its generator's point `G` (a B-spline: item 1's bound of
/// its own net, `E_gen`) about the axis, `gamma_8 (|O| + 3 D) + 2 D dc(u) + E_gen`, `D`
/// the net's reach from `O`. `E_step` is `sqrt 3` times the coordinate bound.
/// 3. **The generator's parameter**: a circle (torus, sphere) inverts its NURBS point by
/// `atan2` about its centre `c`: at its radius `r` the error is that point's (`E_form`)
/// plus `gamma_4 (size + |c|)` and `gamma_4 r |v|` of the angle's own rounding; a line
/// (cone) maps it affinely in 6 operations, `gamma_6 2 (|v0| + |v1|)` along the axis and
/// its slope, times `1 + |t|`; a B-spline generator's parameter is its NURBS's (none).
/// 4. **The sweep's parameter**: the polar converter evaluates its circle's NURBS at the
/// parameter (item 1's bound of that NURBS's own net, `E_polar`) and inverts the point by
/// `atan2` about the circle's centre `c` in its frame: at the circle's radius `rc` the
/// angle is off by `(E_polar + gamma_4 (rc + 2 |c|)) / rc`, plus `gamma_4 |s|` of the
/// angle's own rounding and the degrees' period (`gamma_1 2 pi`), times the STEP point's
/// distance from that axis (the lever). The converter's ends are its domain's, which
/// recognition takes from the form's own end meridians (#3199), so they are the inverted
/// ends to the same rounding.
///
/// A surface of another kind, or a general surface of revolution whose generator is not a
/// B-spline, has no bound here and is not proved.
///
/// # Errors
///
/// Those of the parameter conversion and the evaluations and of the form's knots.
fn evaluates_as(k: &Kernel, surface: &Surface, form: &BSplineSurface) -> Result<bool> {
Ok(evaluation_margin(k, surface, form)?.is_some_and(|m| m <= 1.0))
}
/// [`evaluates_as`]'s largest ratio of distance to bound over the Greville points (infinite
/// for a distance that is not a number); `None` for a surface the bound does not cover.
fn evaluation_margin(k: &Kernel, surface: &Surface, form: &BSplineSurface) -> Result<Option<f64>> {
let (net, p, q) = euclidean_net(form)?;
let size = net.iter().flatten().fold(0.0_f64, |a, x| a.max(x.length()));
let tol = gamma(16 * (p + q + 2)) * size;
let Some(r) = Rounding::of(surface, form.is_rational(), p, q, size, &net)? else {
return Ok(None);
};
let mut worst = 0.0_f64;
for u in greville(form, true)? {
for v in greville(form, false)? {
let uv = Vec2::new(u, v);
let step_uv = surface.convert_uv_from_nurbs_to_step(k, uv)?;
let off =
(surface.evaluate_step_point(k, step_uv)? - form.evaluate_point(uv)?).length();
if !matches!(
off.partial_cmp(&tol),
Some(core::cmp::Ordering::Less | core::cmp::Ordering::Equal)
) {
return Ok(false);
}
let s = surface.evaluate_step_point(k, step_uv)?;
let f = form.evaluate_point(uv)?;
let ratio = (s - f).length() / r.at(step_uv, s, f);
worst = if ratio.is_nan() {
f64::INFINITY
} else {
worst.max(ratio)
};
}
}
Ok(true)
Ok(Some(worst))
}
/// A surface of revolution's generator, for [`Rounding`]'s item 2 and 3.
#[derive(Debug, Clone, Copy)]
enum Generator {
/// `R + r cos v`, `r sin v` about the circle of centre `c`.
Torus { major: f64, minor: f64, c: f64 },
/// `r cos v`, `r sin v` about the circle of centre `c`.
Sphere { radius: f64, c: f64 },
/// `r0 + v t`, `v` (the half angle `a` in radians), its STEP `v` over `[v0, v1]`.
Cone {
r0: f64,
t: f64,
a: f64,
v0: f64,
v1: f64,
},
/// A B-spline generator, its own evaluation's bound.
Spline { e_gen: f64 },
}
/// The pieces of [`evaluates_as`]'s bound that do not depend on the point.
#[derive(Debug, Clone, Copy)]
struct Rounding {
e_form: f64,
origin: Vec3,
/// The net's reach from the origin.
reach: f64,
generator: Generator,
/// The sweep converter's circle: centre, axes.
polar: (Vec3, Vec3, Vec3, f64, f64),
}
/// `|v|`'s largest coordinate.
fn max_coord(v: Vec3) -> f64 {
v.x.abs().max(v.y.abs()).max(v.z.abs())
}
/// A rational (or polynomial) evaluation's bound of degrees summing to `n` over points of
/// size `size` ([`evaluates_as`] item 1).
fn eval_bound(rational: bool, n: usize, size: f64) -> f64 {
let g = gamma(7 * n + 4);
if rational {
(2.0 * g + crate::nurbs::math::UNIT) * size
} else {
g * size
}
}
impl Rounding {
/// The bound's data for `surface` (a cone, sphere, torus or surface of revolution with a
/// B-spline generator) on its form of degrees `(p, q)`, net `net`; `None` for any other.
fn of(
surface: &Surface,
rational: bool,
p: usize,
q: usize,
size: f64,
net: &[Vec<Vec3>],
) -> Result<Option<Self>> {
use crate::curve::curve::CurveKind;
let e_form = eval_bound(rational, p + q, size);
let circle_centre = |rev: &crate::surface::surf_of_revolution::SurfOfRevolution| match rev
.gen_curve
.as_ref()
.map(crate::curve::curve::Curve::kind)
{
Some(CurveKind::Ellipse(e)) => Some(e.position.origin().length()),
_ => None,
};
let (rev, generator) = match surface.kind() {
SurfaceKind::Torus(t) => {
let Some(c) = circle_centre(&t.base) else {
return Ok(None);
};
(
&t.base,
Generator::Torus {
major: t.major_radius,
minor: t.minor_radius,
c,
},
)
}
SurfaceKind::Sphere(sp) => {
let Some(c) = circle_centre(&sp.base) else {
return Ok(None);
};
(
&sp.base,
Generator::Sphere {
radius: sp.radius,
c,
},
)
}
SurfaceKind::Cone(c) => {
let a = c.semi_angle_deg * PI / 180.0;
let d = c.rev.anal_uv_domain;
(
&c.rev,
Generator::Cone {
r0: c.base_radius,
t: c.tan_semi_angle(),
a,
v0: d.v_min(),
v1: d.v_max(),
},
)
}
SurfaceKind::SurfOfRevolution(rev) => {
let Some(CurveKind::BSpline(g)) =
rev.gen_curve.as_ref().map(crate::curve::curve::Curve::kind)
else {
return Ok(None);
};
let pts = g.control_points()?;
let gsize = pts.iter().fold(0.0_f64, |m, c| {
let w = if c.w == NOW { 1.0 } else { c.w };
m.max((Vec3::new(c.x, c.y, c.z) * (1.0 / w)).length())
});
let rational = pts.iter().any(|c| c.w != NOW);
(
&**rev,
Generator::Spline {
e_gen: eval_bound(rational, g.degree()?, gsize),
},
)
}
_ => return Ok(None),
};
let Some(pc) = rev.polar_curve() else {
return Ok(None);
};
let e_polar = {
let pts = pc.base().control_points()?;
let psize = pts.iter().fold(0.0_f64, |m, c| {
let w = if c.w == NOW { 1.0 } else { c.w };
m.max((Vec3::new(c.x, c.y, c.z) * (1.0 / w)).length())
});
eval_bound(pts.iter().any(|c| c.w != NOW), pc.base().degree()?, psize)
};
let origin = rev.position.origin();
let reach = net
.iter()
.flatten()
.fold(0.0_f64, |m, x| m.max((*x - origin).length()));
Ok(Some(Self {
e_form,
origin,
reach,
generator,
polar: (
pc.position.origin(),
pc.position.x_axis(),
pc.position.y_axis(),
pc.radius_x,
e_polar,
),
}))
}
/// The bound at the STEP parameters `step` (degrees, and the generator's), the STEP
/// point `s` and the form's point `f` ([`evaluates_as`] items 1 to 4).
fn at(&self, step: Vec2, s: Vec3, f: Vec3) -> f64 {
let u = crate::nurbs::math::UNIT;
let rad = |deg: f64| deg * PI / 180.0;
let dc = |deg: f64| 2.0 * u + gamma(3) * rad(deg).abs();
let o = max_coord(self.origin);
let size_f = f.length();
// item 2 (per coordinate) and item 3 (as a length)
let (coord, generator) = match self.generator {
Generator::Torus { major, minor, c } => {
let (m, h) = (major + minor, minor);
let dm = gamma(3) * (major + minor) + 2.0 * minor * dc(step.y);
let dh = gamma(1) * minor + 2.0 * minor * dc(step.y);
(
gamma(3) * (o + m + h) + m * (gamma(2) + 2.0 * dc(step.x)) + dm + dh,
self.e_form + gamma(4) * (size_f + c) + gamma(4) * minor * rad(step.y).abs(),
)
}
Generator::Sphere { radius, c } => {
let dm = gamma(1) * radius + 2.0 * radius * dc(step.y);
(
gamma(3) * (o + 2.0 * radius)
+ radius * (gamma(2) + 2.0 * dc(step.x))
+ 2.0 * dm,
self.e_form + gamma(4) * (size_f + c) + gamma(4) * radius * rad(step.y).abs(),
)
}
Generator::Cone { r0, t, a, v0, v1 } => {
let v = step.y;
let m = r0.abs() + (v * t).abs();
let dt = (1.0 + t * t) * gamma(3) * a.abs() + 2.0 * u * t.abs();
let dm = gamma(2) * m + v.abs() * dt;
(
gamma(3) * (o + m + v.abs()) + m * (gamma(2) + 2.0 * dc(step.x)) + dm,
gamma(6) * 2.0 * (v0.abs() + v1.abs()) * (1.0 + t.abs()),
)
}
Generator::Spline { e_gen } => (
gamma(8) * (o + 3.0 * self.reach) + 2.0 * self.reach * dc(step.x) + e_gen,
0.0,
),
};
let e_step = 3.0_f64.sqrt() * coord;
// item 4: the polar converter's inversion of its own circle's point
let (c, x, y, rc, e_polar) = self.polar;
let z = x.cross(y);
let along = |v: Vec3| v - z * v.dot(z);
let lever = along(s - c).length();
let sweep = if rc > 0.0 {
lever
* ((e_polar + gamma(4) * (rc + 2.0 * c.length())) / rc
+ gamma(4) * rad(step.x).abs()
+ gamma(1) * 2.0 * PI)
} else {
// a circle of no radius has no angle: any, at the STEP point's lever
lever * PI
};
self.e_form + e_step + generator + sweep
}
}
const OP_DERIVE: &str = "surface::implicit::AnalyticSupport::derive";
+99 -6
View File
@@ -1061,7 +1061,6 @@ impl Torus {
let ref_frame = ref_iso.reference_frame.ok_or_else(missing)?;
let other_frame = other_iso.reference_frame.ok_or_else(missing)?;
let min_rad = other_iso.radius.ok_or_else(missing)?;
let sweep_ivl = ref_iso.angles.ok_or_else(missing)?;
let common_point = other_iso.common_line_point.ok_or_else(missing)?;
let common_vec = other_iso.common_line_vec.ok_or_else(missing)?;
@@ -1069,6 +1068,17 @@ impl Torus {
let torus_origin =
common_point + common_vec.dot(other_frame.origin() - common_point) * common_vec;
// not upstream (#3199): the x axis and the sweep interval from the form's own first
// sweep row's ends about the axis (`sweep_frame`), not from `is_arc`'s fit of it
let Some((torus_x_axis, torus_y_axis, sweep_ivl)) = sweep_frame(
torus_origin,
ref_frame.z_axis(),
fitted_sides(&ref_frame, ref_iso.angles.ok_or_else(missing)?),
meridians(test, swap_uv)?,
) else {
return Ok(None);
};
// get torus orientation and size
// not upstream (WR-501, #1470, opt-in; tangency-labs/smlib@6136a04): the major
// radius runs from the torus origin, not from the first sweep circle's centre,
@@ -1079,9 +1089,7 @@ impl Torus {
other_frame.origin() - ref_frame.origin()
};
let maj_rad = major_vec.length();
let torus_x_axis = ref_frame.x_axis(); // the frame where all circles are parallel
let torus_y_axis = ref_frame.y_axis();
let torus_z_axis = ref_frame.z_axis();
let torus_z_axis = ref_frame.z_axis(); // the frame where all circles are parallel
// torus must have positive radii
if min_rad <= EFF_ZERO || maj_rad <= EFF_ZERO {
@@ -1294,7 +1302,6 @@ impl Torus {
let mut ref_frame = ref_iso.reference_frame.ok_or_else(missing)?;
let other_frame = other_iso.reference_frame.ok_or_else(missing)?;
let min_rad = other_iso.radius.ok_or_else(missing)?;
let sweep_ivl = ref_iso.angles.ok_or_else(missing)?;
let common_point = other_iso.common_line_point.ok_or_else(missing)?;
let common_vec = other_iso.common_line_vec.ok_or_else(missing)?;
@@ -1302,8 +1309,18 @@ impl Torus {
let torus_origin =
common_point + common_vec.dot(other_frame.origin() - common_point) * common_vec;
// not upstream (#3199): the x axis and the sweep interval from the form's own first
// sweep row's ends about the axis (`sweep_frame`), not from `is_arc`'s fit of it
let (x_axis, y_axis, sweep_ivl) = sweep_frame(
torus_origin,
ref_frame.z_axis(),
fitted_sides(&ref_frame, ref_iso.angles.ok_or_else(missing)?),
meridians(srf, swap_uv)?,
)
.ok_or_else(|| SolidError::sm_err(OP_REBUILD))?;
// Install the origin into pRefFrame. // [B328]
let _ = ref_frame.set_canonical(torus_origin, ref_frame.x_axis(), ref_frame.y_axis());
let _ = ref_frame.set_canonical(torus_origin, x_axis, y_axis);
// get torus orientation and size
let major_vec = other_frame.origin() - ref_frame.origin();
@@ -1587,6 +1604,82 @@ impl Revolved for Torus {
/// `SmTorus::CreateIsoParametricCurve` of the surface of revolution `rev` of a torus
/// of `shape` (also called from the base class's rebuilds, where it is virtual).
/// The control points, Euclidean, of the form's first and last meridians: the columns of its
/// net at the sweep's two ends (the sweep is along u, or along v with `swap_uv`). A clamped
/// end's column is that boundary curve's own control polygon, and a meridian lies in a plane
/// through the axis, so every one of its control points has the meridian's angle about the
/// axis.
fn meridians(srf: &BSplineSurface, swap_uv: bool) -> Result<[Vec<Point3>; 2]> {
let nurb = srf
.gw_nurb()
.ok_or_else(|| SolidError::null_pointer(OP_NURB_TORUS))?;
let (n, m) = (nurb.n(), nurb.m());
let column = |end: bool| -> Result<Vec<Point3>> {
if swap_uv {
let j = if end { m } else { 0 };
(0..=n).map(|i| Ok(nurb.cpt(i, j)?.to_euclid())).collect()
} else {
let i = if end { n } else { 0 };
(0..=m).map(|j| Ok(nurb.cpt(i, j)?.to_euclid())).collect()
}
};
Ok([column(false)?, column(true)?])
}
/// The directions, about the axis, of the fitted first sweep row's ends (`is_arc`'s frame and
/// angles): [`sweep_frame`] reads only their sides from them.
fn fitted_sides(frame: &Axis2Placement, angles: Extent1d) -> [Vec3; 2] {
let at = |deg: f64| {
let a = deg * PI / 180.0;
a.cos() * frame.x_axis() + a.sin() * frame.y_axis()
};
[at(angles.min()), at(angles.max())]
}
/// Not upstream (#3199): a recognised torus's x axis, y axis and sweep interval (degrees) from
/// its form's first and last meridians (`meridians`: their control points, exact data), about
/// the axis through `origin` along the unit `z`. A meridian lies in a plane through the axis,
/// so its control points share its angle on the side the tube's centre is on, and the
/// opposite angle past the axis (a spindle torus's tube crosses it); `sides` (the fitted
/// row's end directions, [`fitted_sides`]) says which side that is, and nothing else. Each
/// meridian's angle is read at its control point farthest from the axis on that side: x is
/// the first meridian's direction square to the axis (so the interval starts at 0), y `z x
/// x`, and the interval's end the last meridian's angle from x about z (`atan2` in that
/// frame), a closed sweep (within `SM_EFF_ZERO_SQRT` of 0) at 360, as `is_arc` maps its end.
/// `is_arc` fitted the first sweep row's circle through three samples, so on a short arc its
/// centre, and with it the x axis and the end angle, was off the axis by its conditioning
/// (1e-14 rad on #3185's grooves); the converter centres the sweep circle on the axis, so its
/// ends and its inside now agree to rounding. `None` when a meridian has no control point off
/// the axis on its side.
fn sweep_frame(
origin: Point3,
z: Vec3,
sides: [Vec3; 2],
[first, last]: [Vec<Point3>; 2],
) -> Option<(Vec3, Vec3, Extent1d)> {
let radial = |p: &Point3| {
let s = *p - origin;
s - s.dot(z) * z
};
let farthest = |ps: &[Point3], side: Vec3| {
ps.iter()
.map(radial)
.filter(|r| r.dot(side) > 0.0)
.max_by(|a, b| a.length().total_cmp(&b.length()))
};
let [first_side, last_side] = sides;
let x = farthest(&first, first_side)?.unitize().ok()?;
let y = z.cross(x);
let e = farthest(&last, last_side)?;
let mut end_deg = e.dot(y).atan2(e.dot(x)) * 180.0 / PI;
if end_deg.abs() < EFF_ZERO_SQRT {
end_deg = 360.0;
} else if end_deg < 0.0 {
end_deg += 360.0;
}
Some((x, y, Extent1d::from_min_max(0.0, end_deg)))
}
fn iso_curve(
rev: &SurfOfRevolution,
shape: Shape,