Compare commits

...

1 Commits

Author SHA1 Message Date
bherbruck e37a587251 solidcore: one pole test, the exact collapse test (#14025)
Agreement, the pcurve's loop closing and the STEP reader's seam check
ask SurfaceEval::pole_within at the reach they already pass, and a
refusal is an error, not "no pole". form::extent_by and
pcurve::pole_by_speed (one speed under 1e-9 of the other) are deleted:
after step 1 nothing else calls them.

pole_within takes a spline point off its knot range (a pcurve's end
fitted a little past it) at the nearest point of the range, with the
reach less a certified bound on the shift; a pole there is a pole
within the reach by the triangle inequality. A shift not under the
reach, or a rational net off the range, is refused as
EvalError::OffRange.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-12 05:29:03 -05:00
10 changed files with 360 additions and 73 deletions
+4 -4
View File
@@ -48,10 +48,10 @@ impl SurfaceEval {
pub(crate) fn project_with(&self, starts: &Starts, p: Vec3, near: Option<[f64; 2]>, accept: f64) -> Option<[f64; 2]> {
self.0.project_with(starts, c::vec3(p), near, accept)
}
/// Whether `uv` is at a pole along each direction, or within `reach` of one, by the
/// surface's speeds there ([`k::pcurve::pole_by_speed`]).
pub(crate) fn pole_within(&self, uv: [f64; 2], reach: f64) -> [bool; 2] {
k::pcurve::pole_by_speed(&self.0, &self.0.domain(), uv, reach)
/// Whether `uv` is at a pole along each direction, or within `reach` mm of one
/// ([`k::SurfaceEval::pole_within`]); refused where that cannot be decided.
pub(crate) fn pole_within(&self, uv: [f64; 2], reach: f64) -> Result<[bool; 2], String> {
self.0.pole_within(uv, reach).map_err(|e| e.to_string())
}
}
+7 -2
View File
@@ -1662,7 +1662,12 @@ impl<'b, 'a> Builder<'b, 'a> {
// tolerance (a corner patch with one side shrunk to a few µm) is run along between them,
// its boundary iso line. As the record check (`brep::geometry`) takes it.
let poles = SurfaceEval::new(&surface).ok();
let at_pole = |uv: [f64; 2], reach: f64| poles.as_ref().is_some_and(|e| e.pole_within(uv, reach).iter().any(|p| *p));
let at_pole = |uv: [f64; 2], reach: f64| -> R<bool> {
Ok(match &poles {
Some(e) => e.pole_within(uv, reach)?.iter().any(|p| *p),
None => false,
})
};
for (l, pc) in edge_loops.iter().zip(&pcs) {
let mut ends = Vec::with_capacity(l.len());
for (&(_, forward), c) in l.iter().zip(pc) {
@@ -1677,7 +1682,7 @@ impl<'b, 'a> Builder<'b, 'a> {
let v = self.edges.get(e as usize).ok_or("edge reference")?.vertices;
let v = if forward { v[0] } else { v[1] };
let reach = self.vertices.get(v as usize).ok_or("vertex reference")?.tolerance.max(tol);
if at_pole(*start, reach) || at_pole(*end, reach) {
if at_pole(*start, reach)? || at_pole(*end, reach)? {
continue;
}
for d in 0..2 {
+2 -1
View File
@@ -80,7 +80,8 @@ pub(crate) fn eval_message(e: solidcore::eval::EvalError) -> String {
solidcore::eval::EvalError::Invalid(what)
| solidcore::eval::EvalError::TooLarge(what)
| solidcore::eval::EvalError::Unevaluable(what)
| solidcore::eval::EvalError::Degenerate(what) => what,
| solidcore::eval::EvalError::Degenerate(what)
| solidcore::eval::EvalError::OffRange(what) => what,
}
}
+7
View File
@@ -62,6 +62,13 @@ pub enum EvalError {
/// limit from the second derivatives vanishes too).
#[error("degenerate: {0}")]
Degenerate(String),
/// A pole query at parameters off a spline's knot range that the query's reach does not
/// cover: moving them onto the range shifts the surface by as much as the reach, or more
/// (the reason's own name in tests and logs: `OffRangeBeyondReach`), or the patch there is
/// rational, whose extension past the range is not bounded
/// ([`collapse::collapse_near`]).
#[error("off the surface's range: {0}")]
OffRange(String),
}
#[cfg(test)]
+8 -11
View File
@@ -352,12 +352,14 @@ pub fn check(brep: &View<'_>) -> Result<()> {
})
.and_then(|v| brep.vertices.get(v as usize))
.map_or(0.0, |v| v.tolerance);
let pole = super::pcurve::pole_by_speed(
&surface,
&surface.domain(),
*start,
reach,
);
let record = Record::Use {
face,
r#loop,
position: index(position),
};
let pole = surface
.pole_within(*start, reach)
.map_err(|err| unevaluable(record, err))?;
for d in 0..2 {
let (Some(Some(p)), Some(false)) = (period.get(d), pole.get(d)) else {
continue;
@@ -368,11 +370,6 @@ pub fn check(brep: &View<'_>) -> Result<()> {
let gap = (a - b).abs();
#[allow(clippy::neg_cmp_op_on_partial_ord)] // a NaN gap is a jump
if !(gap < 0.5 * p) {
let record = Record::Use {
face,
r#loop,
position: index(position),
};
return Err(Mismatch {
record,
problem: Problem::Period { gap, period: *p },
+162
View File
@@ -845,5 +845,167 @@ pub fn collapse(s: &EvalSurface, uv: [f64; 2], reach: f64) -> Result<Collapse, E
]))
}
/// [`collapse`] at `uv`, or, where `uv` is off a spline's knot range (past it by more than the
/// evaluator's rounding, as a pcurve's end fitted a little beyond the surface is), at the
/// nearest point of the range with `reach` less `onto_range`'s certified bound `δ`: every
/// point of an iso-line through `uv` (the surface's end polynomials continued past the range)
/// lies within `δ / 2` of a point of the one through that nearest point, so a line of diameter
/// at most `reach − δ` there has one of at most `reach` at `uv` (the triangle inequality), and
/// a pole answered is a pole within `reach`. A shift not under `reach` (at no reach, any shift)
/// is refused ([`EvalError::OffRange`], `OffRangeBeyondReach`), never answered; so is a
/// rational net off the range. Parameters on the range, a closed direction (wrapped by whole
/// periods: the same point), and any other surface go to [`collapse`] unchanged.
pub fn collapse_near(s: &EvalSurface, uv: [f64; 2], reach: f64) -> Result<Collapse, EvalError> {
check(uv, reach)?;
let Some((near, shift)) = onto_range(s, uv)? else {
return collapse(s, uv, reach);
};
if !(shift < reach) {
return Err(EvalError::OffRange(format!(
"OffRangeBeyondReach: ({}, {}) is off the spline's knot range; moving it onto the \
range shifts the surface by up to {shift} mm, not under the reach {reach} mm",
uv[0], uv[1]
)));
}
// The difference rounded down, so the reach asked is never above reach − δ.
collapse(s, near, (reach - shift) * (1.0 - 2.0 * UNIT))
}
/// Where `uv` is off a polynomial spline's knot range: the nearest point of the range and a
/// certified bound `δ` on twice the distance a move between them shifts the surface's points,
/// along either iso-line; `None` on the range.
///
/// For a direction `e` that `uv` is off by `o_e`, take the strip of the net over `e`'s end knot
/// span (the span holding the nearest point) and the whole of the other direction `f`. On it
/// the partial `∂S/∂e` is the polynomial whose B-spline coefficients are
/// `p_e (P_(i+1) − P_i) / (t_(i+p_e+1) − t_(i+1))` over the strip's control points, within their
/// largest length `M_e` (a convex combination of them, in each patch's Bézier form after knot
/// insertion). Continued past a span of length `h` by `o` at degree `q`, the Bernstein basis
/// sums in absolute value to `(1 + 2o/h)^q`, so on the strip continued past the range
/// `|∂S/∂e| ≤ M_e (1 + 2o_e/h_e)^(p_e − 1) (1 + 2o_f/h_f)^(p_f)`, `h_f` the shortest span along
/// `f`. Every point of an iso-line through `uv` along `f` is then within `o_e` times that of the
/// line through the nearest point (moving across `e`), and the piece of an iso-line along `e`
/// past the range within the same of its end; `δ` is twice the sum over the directions, rounded
/// up. A strip with a weight other than the net's first, or not positive, is refused
/// ([`EvalError::OffRange`]): a rational continuation is not bounded here.
fn onto_range(s: &EvalSurface, uv: [f64; 2]) -> Result<Option<([f64; 2], f64)>, EvalError> {
let EvalSurface::Nurbs(n) = s else {
return Ok(None);
};
let q = s.wrap(uv);
let (deg, knots, count, ctrl) = (n.degree(), n.knots(), n.count(), n.ctrl());
let bad = || malformed("knots or a net that do not fit");
let none: &[f64] = &[];
let kn = |d: usize| knots.get(d).map_or(none, Vec::as_slice);
let get = |a: &[usize; 2], d: usize| a.get(d).copied().unwrap_or(0);
let mut near = q;
let mut off = [0.0f64; 2];
for d in 0..2 {
let (p, m, k) = (get(&deg, d), get(&count, d), kn(d));
let (&lo, &hi) = (k.get(p).ok_or_else(bad)?, k.get(m).ok_or_else(bad)?);
let (Some(x), Some(o)) = (near.get_mut(d), off.get_mut(d)) else {
return Err(bad());
};
if onto_knots(k, p, m, *x).is_none() {
let t = *x;
*x = if t < lo { lo } else { hi };
*o = (t - *x).abs() * (1.0 + UNIT);
}
}
if off == [0.0, 0.0] {
return Ok(None);
}
// Each direction's end span at the nearest point (as de Boor takes it): its first control
// point index and length, and the direction's shortest span.
let mut span = [(0usize, 0.0f64, 0.0f64); 2];
for d in 0..2 {
let (p, m, k) = (get(&deg, d), get(&count, d), kn(d));
let c = onto_knots(k, p, m, near.get(d).copied().unwrap_or(f64::NAN)).ok_or_else(bad)?;
let last = m.checked_sub(1).ok_or_else(bad)?;
let mut at = p;
while at < last && k.get(at + 1).is_some_and(|y| *y <= c) {
at += 1;
}
let h = k.get(at + 1).ok_or_else(bad)? - k.get(at).ok_or_else(bad)?;
let shortest = (p..=last)
.filter_map(|i| Some(k.get(i + 1)? - k.get(i)?))
.filter(|h| *h > 0.0)
.fold(f64::INFINITY, f64::min);
let spans = h > 0.0 && shortest > 0.0 && shortest.is_finite();
if !spans {
return Err(bad());
}
if let Some(slot) = span.get_mut(d) {
*slot = (at.checked_sub(p).ok_or_else(bad)?, h, shortest);
}
}
let nu = get(&count, 0);
let w0 = ctrl.first().ok_or_else(bad)?[3];
let rational = || {
EvalError::OffRange(format!(
"({}, {}) is off the spline's knot range on a rational net, whose continuation past \
the range is not bounded",
uv[0], uv[1]
))
};
// Control point `i` along e, `j` across, as a Euclidean point of the one weight w0.
let point = |e: usize, i: usize, j: usize| -> Result<Vec3, EvalError> {
let (iu, iv) = if e == 0 { (i, j) } else { (j, i) };
let c = ctrl
.get(
iv.checked_mul(nu)
.and_then(|x| x.checked_add(iu))
.ok_or_else(bad)?,
)
.copied()
.ok_or_else(bad)?;
let polynomial = c[3] == w0 && w0 > 0.0 && c.iter().all(|x| x.is_finite());
if !polynomial {
return Err(rational());
}
Ok(Vec3::new(c[0], c[1], c[2]) * (1.0 / w0))
};
let grow = |o: f64, h: f64, power: usize| -> f64 {
(0..power).fold(1.0, |acc, _| acc * (1.0 + 2.0 * o / h))
};
let mut shift = 0.0f64;
for e in 0..2 {
let f = 1 - e;
let (o_e, o_f) = (
off.get(e).copied().unwrap_or(0.0),
off.get(f).copied().unwrap_or(0.0),
);
let (p, pf, m_f, k) = (get(&deg, e), get(&deg, f), get(&count, f), kn(e));
if o_e == 0.0 || p == 0 {
continue;
}
let (Some(&(first, h_e, _)), Some(&(_, _, h_f))) = (span.get(e), span.get(f)) else {
return Err(bad());
};
let mut top = 0.0f64;
for a in first..first + p {
let step = k.get(a + p + 1).ok_or_else(bad)? - k.get(a + 1).ok_or_else(bad)?;
if !(step > 0.0) {
return Err(bad());
}
for b in 0..m_f {
let (x, y) = (point(e, a, b)?, point(e, a + 1, b)?);
// The difference's length with its rounding on the points' own sizes (each
// point's division, the difference, the length).
let len = (y - x).len() + gamma(8) * (x.len() + y.len());
top = top.max(p as f64 * len / step);
}
}
shift += o_e * top * grow(o_e, h_e, p - 1) * grow(o_f, h_f, pf);
}
// Twice the bound, rounded up over its few dozen operations (each point's division, the
// differences, lengths, quotients, powers and products).
let delta = 2.0 * shift * (1.0 + gamma(64));
if !delta.is_finite() {
return Err(refused("a shift onto the knot range is not finite"));
}
Ok(Some((near, delta)))
}
#[cfg(test)]
mod tests;
+145
View File
@@ -568,3 +568,148 @@ fn a_swapped_mapped_revolution_and_a_producer_tolerance_row() {
assert_eq!(collapse(&s, [0.0, 0.3], 0.0).unwrap(), Collapse::Neither);
assert_eq!(collapse(&s, [0.0, 0.3], 1e-6).unwrap(), Collapse::V);
}
// ---- Off the knot range (`collapse_near`, `SurfaceEval::pole_within`) ----
/// The degree (1, 1) polynomial patch on knots `[lo, lo, hi, hi]` along u and v whose corners
/// are `p00, p10, p01, p11` (first index u).
fn bilinear(u: (f64, f64), v: (f64, f64), corners: [[f64; 3]; 4]) -> EvalSurface {
let h = |p: [f64; 3]| [p[0], p[1], p[2], 1.0];
EvalSurface::nurbs(
[1, 1],
[vec![u.0, u.0, u.1, u.1], vec![v.0, v.0, v.1, v.1]],
[2, 2],
corners.iter().map(|p| h(*p)).collect(),
)
}
/// The patch `scale × (u, v, 0)` over `u` × `v`: speed `scale` along both, no pole.
fn flat(u: (f64, f64), v: (f64, f64), scale: f64) -> EvalSurface {
let p = |a: f64, b: f64| [scale * a, scale * b, 0.0];
bilinear(u, v, [p(u.0, v.0), p(u.1, v.0), p(u.0, v.1), p(u.1, v.1)])
}
fn off_range(r: Result<Collapse, EvalError>) -> bool {
matches!(r, Err(EvalError::OffRange(m)) if m.starts_with("OffRangeBeyondReach"))
}
#[test]
fn a_point_past_the_range_is_asked_at_the_range_with_the_reach_less_its_shift() {
// Speed 10 along u: 1e-5 past u = 1 shifts the surface by 1e-4, so δ = 2e-4.
let s = flat((0.0, 1.0), (0.0, 1.0), 10.0);
let uv = [1.0 + 1e-5, 0.5];
let (near, delta) = onto_range(&s, uv).unwrap().unwrap();
assert_eq!(near, [1.0, 0.5]);
let want = 2.0 * (uv[0] - 1.0) * 10.0;
assert!(
(want..=want * (1.0 + 1e-9)).contains(&delta),
"{delta} {want}"
);
assert!(
collapse(&s, uv, 1e-3).is_err(),
"collapse itself still refuses it"
);
// Under the reach: answered, as at the range's end.
assert_eq!(collapse_near(&s, uv, 1e-3).unwrap(), Collapse::Neither);
// At or over it, and at no reach: refused by name, never answered.
assert!(off_range(collapse_near(&s, uv, 2e-4)));
assert!(off_range(collapse_near(&s, uv, 1e-4)));
assert!(off_range(collapse_near(&s, uv, 0.0)));
// On the range, and past it by rounding only: collapse's own answer.
for q in [[0.5, 0.5], [1.0 + 1e-12, 0.5]] {
assert_eq!(onto_range(&s, q).unwrap(), None);
assert_eq!(collapse_near(&s, q, 1e-3), collapse(&s, q, 1e-3));
}
// Below the range too, and off both ways at once (each partial's bound grown by the
// other direction's continuation).
let (near, delta) = onto_range(&s, [-1e-5, 0.5]).unwrap().unwrap();
assert_eq!(near, [0.0, 0.5]);
assert!((2e-4..=2e-4 * (1.0 + 1e-9)).contains(&delta), "{delta}");
let (near, delta) = onto_range(&s, [1.0 + 1e-6, 1.0 + 1e-6]).unwrap().unwrap();
assert_eq!(near, [1.0, 1.0]);
let o = (1.0 + 1e-6) - 1.0;
let want = 2.0 * 2.0 * o * 10.0 * (1.0 + 2.0 * o);
assert!(
(want..=want * (1.0 + 1e-9)).contains(&delta),
"{delta} {want}"
);
}
#[test]
fn a_pole_past_the_range_is_a_pole_within_the_reach() {
// The side u = 1 is one point (10, 5, 0): collapsed along v there. The u partial's
// coefficients are (10, 5, 0) and (10, −5, 0), of length √125.
let s = bilinear(
(0.0, 1.0),
(0.0, 1.0),
[
[0.0, 0.0, 0.0],
[10.0, 5.0, 0.0],
[0.0, 10.0, 0.0],
[10.0, 5.0, 0.0],
],
);
let uv = [1.0 + 1e-6, 0.3];
let (_, delta) = onto_range(&s, uv).unwrap().unwrap();
let want = 2.0 * (uv[0] - 1.0) * 125f64.sqrt();
assert!(
(want..=want * (1.0 + 1e-9)).contains(&delta),
"{delta} {want}"
);
assert_eq!(collapse_near(&s, uv, 1e-3).unwrap(), Collapse::V);
assert!(off_range(collapse_near(&s, uv, 2e-6)));
}
#[test]
fn a_rational_net_past_the_range_is_refused() {
let mut rows = vec![
vec![([0.0, 0.0, 0.0], 1.0), ([0.0, 1.0, 0.0], 1.0)],
vec![([1.0, 0.0, 0.0], 1.0), ([1.0, 1.0, 0.0], 2.0)],
];
let s = spline([1, 1], &rows);
assert!(matches!(
collapse_near(&s, [1.1, 0.5], 1.0),
Err(EvalError::OffRange(_))
));
rows[1][1].1 = 1.0;
let s = spline([1, 1], &rows);
assert_eq!(
collapse_near(&s, [1.0 + 1e-3, 0.5], 1.0).unwrap(),
Collapse::Neither
);
}
#[test]
fn the_step_faces_whose_pcurves_end_past_their_patch_are_answered() {
// 278_moon_buggy faces #6105 and #6165: speed 25.4, v 1.8087e-5 past its range, at a
// vertex reach of 2.54e-3 (δ ≈ 9.2e-4).
let moon = flat(
(0.076579574233648, 14.393741175797247),
(2.314966172279155, 7.627145784997975),
25.4,
);
for uv in [
[2.080136380739515, 7.62716387189905],
[2.0801363807395163, 7.627163871899048],
] {
let (_, delta) = onto_range(&moon, uv).unwrap().unwrap();
assert!(delta < 1e-3, "{delta}");
assert_eq!(
collapse_near(&moon, uv, 2.54e-3).unwrap(),
Collapse::Neither
);
}
// 291_tork faces #22815, #24450, #24132 and #25564: speed 10, u 9.765e-8 past ±0.3556 on
// either side, at a reach of 1e-3.
let r = 0.355599999999977;
let tork = flat((-r, r), (-r, r), 10.0);
for uv in [
[0.35560009764949707, 2.8421709430403982e-14],
[0.35560009764920997, -3.552713678800497e-14],
[-0.35560009764949985, -2.0605739337042883e-14],
[-0.3556000976491943, 3.339550858072468e-14],
] {
assert!(collapse(&tork, uv, 1e-3).is_err());
assert_eq!(collapse_near(&tork, uv, 1e-3).unwrap(), Collapse::Neither);
}
}
+4 -17
View File
@@ -885,33 +885,20 @@ pub fn extent(
label: &Surface,
loops: &[Vec<(&Curve, [f64; 2], bool)>],
reach: f64,
) -> Result<Option<[[f64; 2]; 2]>, EvalError> {
extent_by(label, loops, &|eval: &SurfaceEval, q: [f64; 2]| {
eval.pole_within(q, reach)
})
}
/// [`extent`] with the boundary points on a pole told apart by `pole` (for each direction,
/// whether the label is collapsed there): the speed rule of [`super::pcurve::pole_by_speed`] for
/// a reader that bounds its faces as the pcurves it then fits do.
pub fn extent_by(
label: &Surface,
loops: &[Vec<(&Curve, [f64; 2], bool)>],
pole: &dyn Fn(&SurfaceEval, [f64; 2]) -> Result<[bool; 2], EvalError>,
) -> Result<Option<[[f64; 2]; 2]>, EvalError> {
let mut refused = None;
let out = extent_in(label, loops, pole, &mut refused);
let out = extent_in(label, loops, reach, &mut refused);
match refused {
Some(e) => Err(e),
None => Ok(out),
}
}
/// [`extent_by`]'s body: the classifier's refusal goes to `refused`.
/// [`extent`]'s body: the classifier's refusal goes to `refused`.
fn extent_in(
label: &Surface,
loops: &[Vec<(&Curve, [f64; 2], bool)>],
pole_at: &dyn Fn(&SurfaceEval, [f64; 2]) -> Result<[bool; 2], EvalError>,
reach: f64,
refused: &mut Option<EvalError>,
) -> Option<[[f64; 2]; 2]> {
let eval = SurfaceEval::new(label).ok()?;
@@ -953,7 +940,7 @@ fn extent_in(
};
let q = at(t, prev)?;
// At a pole the collapsed parameter is anything: it bounds nothing.
let pole = match pole_at(&eval, q) {
let pole = match eval.pole_within(q, reach) {
Ok(p) => p,
Err(e) => {
*refused = Some(e);
+12 -38
View File
@@ -24,9 +24,8 @@
//! the structural checks of a record (counts, budgets, a use's interval in its curve's natural
//! range) are the caller's, before it converts the record (`solidcore-ops`' `pcurve`).
//!
//! Deviations: the pole test is the evaluator's speed test this routine has always used
//! ([`pole_by_speed`]), not [`super::collapse`]'s exact one; the B-spline cut and the knot span
//! search are kept here ([`segment`], [`find_span`]) as the routine had them.
//! Deviations: the B-spline cut and the knot span search are kept here ([`segment`],
//! [`find_span`]) as the routine had them.
use super::spline::EvalSurface;
use super::uv::{self, Domain};
use super::vec::{v3, VecExt};
@@ -56,9 +55,10 @@ impl From<EvalError> for Error {
match e {
EvalError::Invalid(_) => Error::Invalid(e.to_string()),
EvalError::TooLarge(_) => Error::TooLarge(e.to_string()),
EvalError::Unsupported(_) | EvalError::Unevaluable(_) | EvalError::Degenerate(_) => {
Error::Failed(e.to_string())
}
EvalError::Unsupported(_)
| EvalError::Unevaluable(_)
| EvalError::Degenerate(_)
| EvalError::OffRange(_) => Error::Failed(e.to_string()),
}
}
}
@@ -697,11 +697,10 @@ impl On {
Some(q)
}
/// Whether `uv` is at a pole along each direction, or within `reach` of one: moving that
/// parameter by half its period (or across its range) moves the point by no more than `reach`
/// at its speed there. There the parameter is as free as at the pole itself.
fn pole(&self, uv: [f64; 2], reach: f64) -> [bool; 2] {
pole_by_speed(&self.surface, &self.dom, uv, reach)
/// Whether `uv` is at a pole along each direction, or within `reach` mm of one
/// ([`SurfaceEval::pole_within`]): there the parameter is as free as at the pole itself.
fn pole(&self, uv: [f64; 2], reach: f64) -> Result<[bool; 2]> {
Ok(self.surface.pole_within(uv, reach)?)
}
/// The loop's uses moved by whole periods so each starts where the one before it ends (in
@@ -737,8 +736,8 @@ impl On {
// offsets up to the next pole.
let at_pole: Vec<bool> = ends
.iter()
.map(|(s, _)| self.pole(*s, tol).get(d).copied().unwrap_or(false))
.collect();
.map(|(s, _)| Ok(self.pole(*s, tol)?.get(d).copied().unwrap_or(false)))
.collect::<Result<_>>()?;
let first = at_pole.iter().position(|&p| p).unwrap_or(0);
let order: Vec<usize> = (first..n).chain(0..first).collect();
let mut shift = vec![0.0; n];
@@ -959,31 +958,6 @@ fn basis(knots: &[f64], p: usize, span: usize, t: f64) -> Option<Vec<f64>> {
Some(n)
}
/// Whether `uv` is at a pole along each direction, or within `reach` of one, by the surface's
/// speeds there: a speed below 10⁻⁹ of the other's, or one that moves the point no more than
/// `reach` over half the period (or the whole range) of its parameter.
pub fn pole_by_speed(s: &SurfaceEval, dom: &Domain, uv: [f64; 2], reach: f64) -> [bool; 2] {
let Some((_, su, sv)) = s.derivs(uv[0], uv[1]) else {
return [false; 2];
};
let speed = [su.len(), sv.len()];
let [a, b] = speed;
let mut out = [a <= 1e-9 * b.max(1e-12), b <= 1e-9 * a.max(1e-12)];
for ((flag, sp), (period, range)) in out
.iter_mut()
.zip(speed)
.zip(dom.period.iter().zip(dom.range.iter()))
{
let span = match (period, range) {
(Some(period), _) => 0.5 * period,
(None, Some((lo, hi))) => (hi - lo).abs(),
(None, None) => continue,
};
*flag |= sp * span <= reach;
}
out
}
/// Rounding allowed at the ends of a knot domain, in units of `f64::EPSILON` times its size.
const DOMAIN_ROUNDING: f64 = 8.0;
/// Most knots a cut takes.
+9
View File
@@ -528,7 +528,16 @@ impl SurfaceEval {
/// [`Self::pole`], or within `reach` mm of one: moving along a direction across its whole
/// range moves the point no more than `reach`, certified (a boundary point that lies at a
/// pole to within a tolerance, where its periodic parameter is free).
///
/// A spline's parameters off its knot range (a pcurve's end fitted a little past it) are
/// asked at the nearest point of the range with the reach less a certified bound on the
/// shift that move makes ([`super::collapse::collapse_near`]); by the triangle inequality a
/// pole there is a pole within `reach` at `uv`. A shift not under `reach` is refused
/// ([`EvalError::OffRange`]), never answered.
pub fn pole_within(&self, uv: [f64; 2], reach: f64) -> Result<[bool; 2], EvalError> {
if let SurfaceEval::Eval(e, _) = self {
return Ok(super::collapse::collapse_near(e, uv, reach)?.flags());
}
Ok(self.collapse(uv, reach)?.flags())
}
}