mirror of
https://github.com/bherbruck/solidcraft.git
synced 2026-11-11 03:01:58 +00:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6d01515911 | |||
| 8ffb97d541 |
+12
-14
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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() {
|
||||
|
||||
+1282
-463
File diff suppressed because it is too large
Load Diff
+60
-36
@@ -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
@@ -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)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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, ¬ched, &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
@@ -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
@@ -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
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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)) {
|
||||
|
||||
@@ -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
@@ -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
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user