mirror of
https://github.com/bherbruck/solidcraft.git
synced 2026-11-12 11:41:56 +00:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e37a587251 |
+4
-4
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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(°, 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(°, 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(°, e), get(°, 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;
|
||||
|
||||
@@ -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
@@ -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
@@ -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.
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user