Compare commits

...

2 Commits

Author SHA1 Message Date
Markus Reichart 06e80c0a40 fix: a setback fill takes its rounds' rows in the section's standard form across (#2697)
A round is one rational span across, so its rows fix its end section only
up to a Moebius map of the parameter across (w_j -> w_j c^j). round_jets
took the net's rows as they were, and the G2 fill measured its spoke at the
middle of the round's own parameter across: the fill depended on how the
round is parametrized, not only on what it is.

- round_jets puts the raw rows 0 to k in the section's standard form
  across (standard_form_across): column j times c^j, c = (W_0 / W_q)^(1/q)
  the ratio of the row's end weights, its derivatives along by Leibniz, so
  that both end weights are W_0 at every rail parameter to order k; then
  the weights' share and the rails' mean arc length as before. RoundJets
  no longer carries dt_ds.
- The G2 fill measures each spoke on the rows the patch takes (row_middle,
  now shared with curved.rs), not on the round's own evaluation.

Results: the box corner's G1 fills (r 2; r 2, 3, 4) and G2 fills (order-2
and G^n rounds r 2; order-2 r 2, 3, 4) are unchanged bit for bit (every
control point and weight, base b83367a49 against this commit). The fill G2
to circular rounds and G1 to the faces (FillOrders { rounds: 2, faces: 1 },
which nothing in the kernel calls) moves: its rows 0 and 1 come from
geometric_rows, whose row 1 has weight 0, so the speed of those rows at the
middle is 1 / w(1/2) and the patch's speed across there is now its whole
spoke, as the comment above the rows asks. Measured |R_u(0, 1/2)| of its
three setback patches at r 2, 3, 4: 4.2285, 4.1421, 4.7354 before;
3.6092, 3.5355, 4.0419 now (x 0.85355, the 90-degree arc's mean weight).

The in-module test geometric_rows_are_the_geometry asserted that two
parametrizations of one round (weights scaled 1, 2, 4 across) give
different jets; in the standard form they give the same, so it now asserts
that.

The six tests of the previous commit pass (off by at most 8.7e-14 against
6e-12); fill_is_the_geometry_s too. One Vec::with_capacity(n) of
fill_hole_with goes with dt_ds, so its row leaves plan/panic-sites.tsv.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 16:33:38 -05:00
Markus Reichart a25715bdba test: a setback fill is its rounds' geometry's, not their parametrization across (#2697)
The box corner's rounds r 2, 3, 4, filled once as they are and once with
their sections moved across by a Moebius map (the weights w_j rho^j, the
same surfaces). The fill must be the same, patch for patch, to 1e-12 of
the corner's size. Six cases, each its own test; the rounds as they are
must fill (the precondition, with its own message):

- fill_is_the_geometry_across (G1, 1.7^j): on main refused, "no G1 fill
  up to degree 9 is certified convex at these setbacks"
- fill_is_the_geometry_across_mildly (G1, 1.001^j): on main certified,
  off by 2.094e-3 (allowed 6.3e-12); 1.003^j is refused already, the
  setbacks being the least that certify
- fill_is_the_geometry_across_changing_along (G1, 1.7^j to 1.2^j along):
  on main refused as above
- g2_fill_is_the_geometry_across (G2, 1.7^j): on main off by 1.341e0
  (allowed 6.0e-12)
- g2_fill_is_the_geometry_across_changing_along: on main off by 1.341e0
- g2_fill_is_the_geometry_across_beside_circles (G2 to the circles, G1 to
  the faces): on main off by 3.601e-2 (allowed 6.3e-12); its rows come
  from the arc already, its spoke not

fill_is_the_geometry_s (the fork's guard along the rails, fork main
8eac63c) passes on main: off by at most 4.8e-15.

These six fail until the next commit. Run on main b83367a49:
cargo test -p solidcore --profile goldens --test fork_870c970_blend_setback

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 16:33:38 -05:00
4 changed files with 536 additions and 68 deletions
+119 -53
View File
@@ -42,6 +42,15 @@
//! the faces as well takes rounds eased into a curvature-continuous section first
//! ([`ease_surface`]).
//!
//! A round's jets come in the section's standard form across (`round_jets`): a round is one
//! rational span across, so its section is its rows only up to a Moebius map of the parameter
//! across (`w_j -> w_j c^j`) and a reversal; the standard form (the row's end weights equal, at
//! every rail parameter, to the jets' order) removes the map, the cycle the reversal. With the
//! rails' mean arc length along, two parametrizations of one round give the same jets. The
//! planar fills take them as their rounds' rows; beside a curved face, and where the fill's
//! order at the rounds is above its faces', rows 0 and 1 come from `geometric_rows` instead.
//! A G2 fill's setback patch measures its spoke on the rows it takes.
//!
//! Faces are planes (a curved face is not filled here yet). Every patch lies in the convex
//! hull of its control points (all weights positive), so [`containment`]'s bound is certified.
//!
@@ -91,7 +100,10 @@
//! subtracts) the port returns `SM_ERR`, and a least-squares system with fewer rows than
//! columns (where the fork would allocate a wrapped size) is not solvable (`FALSE`); the
//! fork's cases never reach these. The moved ease's errors name this module as their
//! operation; their statuses are unchanged. No recursion.
//! operation; their statuses are unchanged. Not the fork's (tangency-labs/solidcore#2697):
//! `round_jets` puts its rows in the section's standard form across and returns no
//! `rdDtDs`; the G2 fill's spokes are measured on those rows, not on the round's own
//! evaluation at the middle of its parameter across. No recursion.
#![allow(clippy::neg_cmp_op_on_partial_ord)] // the fork's NaN-safe tests
@@ -105,8 +117,6 @@ use crate::nurbs::b_spline_curve::KnotType;
use crate::nurbs::b_spline_surface::{
BSplineSurface, BSplineSurfaceForm, CanonicalSurfaceData, SurfParamType,
};
use crate::nurbs::b_spline_surface_n_lib::p2::EvaluateFlags;
use crate::nurbs::knots::Side;
use crate::surface::surface::{Surface, SurfaceKind};
mod certify;
@@ -1217,17 +1227,18 @@ fn first(net: &Net, u: f64, v: f64) -> Result<(Vec3, Vec3, Vec3)> {
/// A round's homogeneous jets at its end section (`sm_SbRoundJets`'s out-params).
struct RoundJets {
/// `H[j]` (`j = 0 ..= k`): the across Bezier curve of the j-th derivative by the rail
/// parameter, columns from its across start.
/// `H[j]` (`j = 0 ..= k`): the across Bezier curve of the j-th derivative by s, the mean
/// arc length of its rails, columns from its across start, in the section's standard
/// form across ([`standard_form_across`]).
h: Vec<Vec<Hom>>,
/// Its across degree.
q: usize,
/// `rdDtDs` (the fork's d1de555): `dt/ds` there, s the mean arc length of its rails.
dt_ds: f64,
}
/// `sm_SbRoundJets`: a round's homogeneous derivatives along its rails at its end section,
/// to order `k`, and its across degree.
/// to order `k`, by the mean arc length of its rails, and its across degree; the rows in the
/// section's standard form across, so that they are the round's geometry's, not its
/// parametrization's (not the fork's, #2697).
fn round_jets(
kernel: &Kernel,
round: &Round<'_>,
@@ -1380,10 +1391,12 @@ fn round_jets(
}
}
// the weights' share: G = H / W, W the rails' mean weight (the fork's d1de555)
// the weights' share: G = H / W, W the rails' mean weight (the fork's d1de555), once the
// section is in its standard form across (#2697)
if k > 2 {
return Err(sm_err().into()); // the jets are taken to second order
}
standard_form_across(&mut h, lq, k)?;
let mut sw = [0.0; 3];
for (d, swd) in sw.iter_mut().enumerate().take(k + 1) {
let hd = el_ref(&h, d)?;
@@ -1474,11 +1487,84 @@ fn round_jets(
*el_mut(el_mut(&mut h, 2)?, j)? = axpy(&axpy(&HOM_ZERO, ts * ts, &g2), tss, &g1);
}
}
Ok(RoundJets {
h,
q: lq,
dt_ds: ts,
})
Ok(RoundJets { h, q: lq })
}
/// A round's raw jets at its end (`h[d][j]`: column j's d-th homogeneous derivative along,
/// `d = 0 ..= k`, `k <= 2`) put in the section's standard form across at every rail parameter,
/// to order k: column j times `c^j`, `c = (W_0 / W_q)^(1 / q)` the ratio of the row's end
/// weights, its derivatives along by Leibniz' rule, so that both end weights are `W_0` all
/// along. A one-span rational section of degree q is its rows only up to a Moebius map of its
/// parameter across (the weights `w_j c^j`), so two parametrizations of one round give the
/// same rows. Where the end weights are equal to order k the rows are kept as they are.
///
/// # Errors
/// [`Refusal`] with `SM_ERR_INVALID_INPUT` for an end weight that is not positive; `SM_ERR`
/// for a section of degree 0 or jets past the second order.
fn standard_form_across(
h: &mut [Vec<Hom>],
q: usize,
k: usize,
) -> core::result::Result<(), Refusal> {
if q == 0 || k > 2 || h.len() <= k {
return Err(sm_err().into());
}
// the end weights W_0, W_q and their derivatives along (0 past the jets' order)
let mut w0 = [0.0; 3];
let mut wq = [0.0; 3];
for d in 0..=k {
let row = el_ref(h, d)?;
*el_mut(&mut w0, d)? = el_ref(row, 0)?[3];
*el_mut(&mut wq, d)? = el_ref(row, q)?[3];
}
if !(w0[0] > 0.0) || !(wq[0] > 0.0) {
return Err(Refusal::invalid(
"SmBlendSetback: a round's weights must be positive",
));
}
// ln c = (ln W_0 - ln W_q) / q, and its first two derivatives along
let qf = q as f64;
let l1 = (w0[1] / w0[0] - wq[1] / wq[0]) / qf;
let l2 = ((w0[2] * w0[0] - w0[1] * w0[1]) / (w0[0] * w0[0])
- (wq[2] * wq[0] - wq[1] * wq[1]) / (wq[0] * wq[0]))
/ qf;
let c = (w0[0] / wq[0]).powf(1.0 / qf);
let (c1, c2) = (c * l1, c * (l2 + l1 * l1));
// c^j, c^(j - 1), c^(j - 2) (the last two only where j reaches them: their factors are 0
// below)
let (mut cj, mut cj1, mut cj2) = (1.0, 0.0, 0.0);
for j in 0..=q {
let jf = j as f64;
// (c^j)' and (c^j)''
let d1 = jf * cj1 * c1;
let d2 = jf * (jf - 1.0) * cj2 * c1 * c1 + jf * cj1 * c2;
let h0 = el(el_ref(h, 0)?, j)?;
*el_mut(el_mut(h, 0)?, j)? = axpy(&HOM_ZERO, cj, &h0);
if k >= 1 {
let h1 = el(el_ref(h, 1)?, j)?;
*el_mut(el_mut(h, 1)?, j)? = axpy(&axpy(&HOM_ZERO, cj, &h1), d1, &h0);
if k >= 2 {
let h2 = el(el_ref(h, 2)?, j)?;
let s2 = axpy(&axpy(&HOM_ZERO, cj, &h2), 2.0 * d1, &h1);
*el_mut(el_mut(h, 2)?, j)? = axpy(&s2, d2, &h0);
}
}
cj2 = cj1;
cj1 = cj;
cj *= c;
}
Ok(())
}
/// A row's homogeneous point at the middle of its parameter across: its Bernstein mean,
/// `sum_j C(q, j) / 2^q H_j`.
fn row_middle(row: &[Hom]) -> Result<Hom> {
let q = row.len().checked_sub(1).ok_or_else(sm_err)?;
let mut m = HOM_ZERO;
for (j, h) in row.iter().enumerate() {
m = axpy(&m, binomial(q, j)? / 2.0_f64.powf(q as f64), h);
}
Ok(m)
}
/// A circular round's end rows to order `k` (at most 2) from its GEOMETRY, not its
@@ -2182,14 +2268,10 @@ fn fill_hole_with(
// each round's jets at its end, and the faces its across ends lie on
let mut jets: Vec<Vec<Vec<Hom>>> = Vec::with_capacity(n);
let mut qs: Vec<usize> = Vec::with_capacity(n);
let mut dt_ds: Vec<f64> = Vec::with_capacity(n);
let mut face_at = [vec![n; n], vec![n; n]];
for (r, round) in rounds.iter().enumerate() {
let RoundJets {
h,
q,
dt_ds: r_dt_ds,
} = round_jets(kernel, round, if any_curved { smax_usize(k, 2) } else { k })?;
let RoundJets { h, q } =
round_jets(kernel, round, if any_curved { smax_usize(k, 2) } else { k })?;
// beside circular rounds a fill of a higher order than its faces' (G2 to the rounds,
// G1 to the faces) takes their rows from their geometry
let (h, q) = if kf < k {
@@ -2197,7 +2279,6 @@ fn fill_hole_with(
} else {
(h, q)
};
dt_ds.push(r_dt_ds);
for (e, at_e) in face_at.iter_mut().enumerate() {
let hp = el_ref(el_ref(&h, 0)?, if e == 0 { 0 } else { q })?;
let p = euclid(hp);
@@ -2345,36 +2426,17 @@ fn fill_hole_with(
let mut mids: Vec<Point3> = Vec::with_capacity(n);
let mut centre = Vec3::ZERO;
let mut speeds: Vec<f64> = Vec::with_capacity(n);
let flags = EvaluateFlags {
u_side: Side::Left,
v_side: Side::Left,
only_upper_half: false,
non_zero_tangents: false,
};
for &r in &cycle_round {
let round = el_ref(rounds, r)?;
let dom = round.surface.natural_uv_domain_with_nurb(kernel)?;
let across = if round.along == SurfParamType::U {
dom.v_interval()
} else {
dom.u_interval()
};
let uv = if round.along == SurfParamType::U {
Point2::new(round.end, across.evaluate(0.5))
} else {
Point2::new(across.evaluate(0.5), round.end)
};
let d = round.surface.evaluate(kernel, uv, 1, 1, flags)?;
let row0 = el_ref(&d, 0)?;
let point = el(row0, 0)?;
let speed = if round.along == SurfParamType::U {
el(el_ref(&d, 1)?, 0)?
} else {
el(row0, 1)?
};
// the end section's middle and its speed along there, by s, on the rows the patch
// takes (in the section's standard form, the middle of the geometry's parameter
// across, not of the round's own; #2697)
let jets_r = el_ref(&jets, r)?;
let m0 = row_middle(el_ref(jets_r, 0)?)?;
let m1 = row_middle(el_ref(jets_r, 1)?)?;
let point = euclid(&m0);
let speed = (xyz(&m1) - point * m1[3]) / m0[3];
mids.push(point);
// by s (sm_SbRoundJets; the fork's d1de555)
speeds.push(speed.length() * el(&dt_ds, r)?);
speeds.push(speed.length());
centre += point * (1.0 / nf);
}
for m in 0..n {
@@ -3253,8 +3315,9 @@ mod tests {
/// A circular round's geometric rows are its geometry's: two parametrizations of one round
/// (linear along with the arc's own weights, against quadratic along at an uneven speed with
/// the arc's weights scaled `1, 2, 4`) give the same rows to order 1 and 2, where their
/// jets differ; and a section that is no circular arc is refused.
/// the arc's weights scaled `1, 2, 4`) give the same rows to order 1 and 2, and the same
/// jets (by the rails' mean arc length, in the section's standard form across); and a
/// section that is no circular arc is refused.
#[test]
fn geometric_rows_are_the_geometry() {
let k = Kernel::default();
@@ -3290,7 +3353,7 @@ mod tests {
most <= 1e-12 * (1.0 + r + s),
"order {order}: rows differ by {most}"
);
// their jets do differ
// and so do their jets, the rows in the section's standard form across
let mut jet_gap = 0.0f64;
for (row_a, row_b) in raw[0].iter().zip(&raw[1]) {
for (ha, hb) in row_a.iter().zip(row_b) {
@@ -3299,7 +3362,10 @@ mod tests {
}
}
}
assert!(jet_gap > 1e-3, "order {order}: the jets agree ({jet_gap})");
assert!(
jet_gap <= 1e-12 * (1.0 + r + s),
"order {order}: the jets differ by {jet_gap}"
);
// row 0 is the end section: its ends are the rails' ends
let row0 = &ra[0];
let (pa, pb) = (euclid(&row0[0]), euclid(&row0[*qa]));
@@ -79,8 +79,9 @@ use super::qp::{dual_qp_source, QpOutcome, QpStats};
use super::spring::spring_curve;
use super::{
affine_solutions, along_line, axpy, bern_elevate, bern_mul, binomial, el, el_mut, el_ref,
least_squares, monomials, over_patch, round_jets, second, sm_err, smax, smax_usize, smin, Bern,
Face, FaceKind, Fill, FillStats, Hom, Net, Refusal, Round, RoundJets, Tensor, GW, GX, HOM_ZERO,
least_squares, monomials, over_patch, round_jets, row_middle, second, sm_err, smax, smax_usize,
smin, Bern, Face, FaceKind, Fill, FillStats, Hom, Net, Refusal, Round, RoundJets, Tensor, GW,
GX, HOM_ZERO,
};
use crate::blend::fillet_exact::{
classify_blend_spine, BlendQuadric, BlendSpine, ExactBlend, QuadricKind,
@@ -101,16 +102,6 @@ fn comp_of(v: &[Hom], c: usize) -> Result<Bern> {
v.iter().map(|h| el(h, c)).collect()
}
/// The Bernstein mean of a row: its point at the middle, `sum_j C(q, j) / 2^q H_j`.
fn row_middle(row: &[Hom]) -> Result<Hom> {
let q = row.len().checked_sub(1).ok_or_else(sm_err)?;
let mut m = HOM_ZERO;
for (j, h) in row.iter().enumerate() {
m = axpy(&m, binomial(q, j)? / 2.0_f64.powf(q as f64), h);
}
Ok(m)
}
/// The circumcentre of three points (`B + ((b |a|^2 - a |b|^2) x (a x b)) / 2 |a x b|^2`,
/// `a = A - B'`, `b = M - B'` about the third), `None` where they are collinear.
fn circumcentre(a: Vec3, m: Vec3, b: Vec3) -> Option<Vec3> {
@@ -3,7 +3,10 @@
//! `SETBACK_CASES` of the fork's `capi/tests/ops_kernel.cpp`, with the fork's values and
//! tolerances. `870c970` committed the fill's cases as known failing; `beba428` and `3fdb76b`
//! make every case pass (`52da453` keeps them passing at the whole spoke's speed), which is
//! the end state here: all seven pass.
//! the end state here: all seven pass. Two of the fork's later cases follow them, with G2 and
//! a Moebius map that changes along the rounds added (#2697): `fill_is_the_geometry_s` (fork
//! main `8eac63c`) and `fill_is_the_geometry_across` (the fork's
//! `archive/wip/85c-curved-setback`, `37c9e1d`, known failing there).
//!
//! The box corner is `x, y, z >= 0` at the origin: face f (0, 1, 2) the plane where coordinate
//! f is 0, its normal out of the material `-e_f`; the edge along axis a lies between faces
@@ -32,7 +35,8 @@
)] // test helpers
use solidcore::blend::blend_setback::{
certified_scale, containment, distances, ease_surface, fill_hole, EaseRamp, Edge, Face, Round,
certified_scale, containment, distances, ease_surface, fill_hole, fill_hole_to, EaseRamp, Edge,
Face, Fill, FillOrders, Round,
};
use solidcore::blend::fillet_section::section::{GnFaces, GnSectionFsg};
use solidcore::error::SolidError;
@@ -701,3 +705,411 @@ fn g2_fill_of_uneased_rounds_refused() {
let why = refusal.reason.expect("a reason");
println!(" refused: {why}");
}
// ---- the fill is the rounds' geometry's, not their parametrization's (#2697) ----
/// `C(n, k)`.
fn choose(n: usize, k: usize) -> f64 {
let mut c: u64 = 1;
for i in 0..k as u64 {
c = c * (n as u64 - i) / (i + 1);
}
c as f64
}
/// The product of two polynomials in Bernstein form on `[0, 1]`, in Bernstein form.
fn bernstein_product(a: &[f64], b: &[f64]) -> Vec<f64> {
let (p, q) = (a.len() - 1, b.len() - 1);
let mut c = vec![0.0; p + q + 1];
for (i, &ai) in a.iter().enumerate() {
for (j, &bj) in b.iter().enumerate() {
c[i + j] += choose(p, i) * choose(q, j) / choose(p + q, i + j) * ai * bj;
}
}
c
}
/// A polynomial in Bernstein form raised to degree `deg` (at least its own).
fn bernstein_raised(a: &[f64], deg: usize) -> Vec<f64> {
let mut a = a.to_vec();
while a.len() - 1 < deg {
let p = a.len() - 1;
let mut b = vec![0.0; p + 2];
b[0] = a[0];
b[p + 1] = a[p];
for i in 1..=p {
let t = i as f64 / (p + 1) as f64;
b[i] = t * a[i - 1] + (1.0 - t) * a[i];
}
a = b;
}
a
}
/// `box_corner_round` with its across parameter moved by a Moebius map (the fork's
/// `setback_fill_is_the_geometry_across`): column j's weights times `rho^j`, the same
/// surface.
fn box_corner_round_moved(
a: usize,
r: f64,
s: f64,
len: f64,
order: usize,
rho: f64,
) -> BSplineSurface {
let own = box_corner_round(a, r, s, len, order);
let c = own.canonical().unwrap();
let nv = c.v_degree + 1;
let w: Vec<f64> = c
.weights
.iter()
.enumerate()
.map(|(i, &w)| w * rho.powi((i % nv) as i32))
.collect();
BSplineSurface::create_canonical(&CanonicalSurfaceData {
u_degree: c.u_degree,
v_degree: c.v_degree,
control_points: &c.control_points,
form: BSplineSurfaceForm::Unspecified,
u_multiplicities: &c.u_multiplicities,
v_multiplicities: &c.v_multiplicities,
u_knots: &c.u_knots,
v_knots: &c.v_knots,
knot_type: KnotType::Unspecified,
weights: Some(&w),
uv_domain: None,
})
.unwrap()
}
/// `box_corner_round` with its across parameter moved by a Moebius map that changes along it:
/// column j's homogeneous points (linear along) times `m(u)^j`, m linear from `rho0` at the
/// end section to `rho1` at the far end, of degree `q + 1` along (q its degree across). Each
/// section is the same curve at another parameter, so the surface is the same.
fn box_corner_round_moved_along(
a: usize,
r: f64,
s: f64,
len: f64,
order: usize,
rho0: f64,
rho1: f64,
) -> BSplineSurface {
let own = box_corner_round(a, r, s, len, order);
let c = own.canonical().unwrap();
assert_eq!(c.u_degree, 1);
let nv = c.v_degree + 1;
let deg = nv;
let mut rows = vec![vec![[0.0f64; 4]; nv]; deg + 1];
for j in 0..nv {
let mut power = vec![1.0];
for _ in 0..j {
power = bernstein_product(&power, &[rho0, rho1]);
}
for comp in 0..4 {
let at = |i: usize| {
let (p, w) = (c.control_points[i * nv + j], c.weights[i * nv + j]);
[p.x * w, p.y * w, p.z * w, w][comp]
};
let along = bernstein_raised(&bernstein_product(&power, &[at(0), at(1)]), deg);
for (i, x) in along.into_iter().enumerate() {
rows[i][j][comp] = x;
}
}
}
let (mut pts, mut w) = (Vec::new(), Vec::new());
for h in rows.iter().flatten() {
pts.push(Point3::new(h[0] / h[3], h[1] / h[3], h[2] / h[3]));
w.push(h[3]);
}
BSplineSurface::create_canonical(&CanonicalSurfaceData {
u_degree: deg,
v_degree: nv - 1,
control_points: &pts,
form: BSplineSurfaceForm::Unspecified,
u_multiplicities: &[deg + 1, deg + 1],
v_multiplicities: &[nv, nv],
u_knots: &[s, s + len],
v_knots: &[0.0, 1.0],
knot_type: KnotType::Unspecified,
weights: Some(&w),
uv_domain: None,
})
.unwrap()
}
/// The fork's `box_corner_round_reparametrized`: `box_corner_round` over the same stretch of
/// its edge in another parameter along it, quadratic along, its middle row of sections at
/// `s + mid len` and that row's weights times `wmid`: the same surface, its rails and sections
/// the same curves, at another speed along (not constant unless `mid = 1/2`, `wmid = 1`).
fn box_corner_round_reparametrized(
a: usize,
r: f64,
s: f64,
len: f64,
order: usize,
mid: f64,
wmid: f64,
) -> BSplineSurface {
let lin = box_corner_round(a, r, s, len, order);
let c = lin.canonical().unwrap();
assert_eq!(c.u_degree, 1);
let nv = c.v_degree + 1;
let (mut pts, mut w) = (Vec::new(), Vec::new());
for (share, rho) in [(0.0, 1.0), (mid, wmid), (1.0, 1.0)] {
for j in 0..nv {
// the first row, at s, moved along the edge
let p = c.control_points[j];
let mut x = [p.x, p.y, p.z];
x[a] = s + share * len;
pts.push(Point3::new(x[0], x[1], x[2]));
w.push(rho * c.weights[j]);
}
}
BSplineSurface::create_canonical(&CanonicalSurfaceData {
u_degree: 2,
v_degree: nv - 1,
control_points: &pts,
form: BSplineSurfaceForm::Unspecified,
u_multiplicities: &[3, 3],
v_multiplicities: &[nv, nv],
u_knots: &[s, s + len],
v_knots: &[0.0, 1.0],
knot_type: KnotType::Unspecified,
weights: Some(&w),
uv_domain: None,
})
.unwrap()
}
/// The box corner's setbacks for rounds of `radii`: the owner's rule, and for a G1 fill grown
/// to the least scale that certifies it convex (the fork's capi `box_corner_g1_setbacks`).
fn corner_setbacks(k: &Kernel, radii: [f64; 3], order: usize) -> Vec<f64> {
let faces = box_corner_faces();
let mut setbacks = distances(&faces, &box_corner_edges(radii))
.unwrap()
.setbacks;
if order == 1 {
let scale = certified_scale(k, &faces, &box_corner_edges(radii), &setbacks, None)
.unwrap()
.scale;
assert!(scale >= 1.0);
for x in &mut setbacks {
*x *= scale;
}
}
setbacks
}
/// The box corner's fill to `orders` of `rounds` (u along each, from its end section at its
/// least u, the corner below it) at the fork's tolerance 1e-9, or why it is refused.
fn fill_of(k: &Kernel, rounds: &[BSplineSurface], orders: FillOrders) -> Result<Fill, String> {
let surfaces: Vec<Surface> = rounds.iter().cloned().map(Surface::from).collect();
let input: Vec<Round<'_>> = surfaces
.iter()
.map(|own| Round {
surface: own,
along: SurfParamType::U,
end: own
.natural_uv_domain_with_nurb(k)
.unwrap()
.u_interval()
.min(),
corner_above: false,
})
.collect();
let fill = fill_hole_to(k, &box_corner_faces(), &input, orders, 1e-9)
.map_err(|e| format!("refused ({})", e.reason.unwrap_or("no reason")))?;
assert_eq!(fill.patches.len(), 6);
Ok(fill)
}
/// How far two fills of one corner lie apart, patch for patch, at 9 x 9 parameters of each.
fn fill_gap(a: &Fill, b: &Fill) -> f64 {
assert_eq!(
(&a.round, &a.face, &a.reversed),
(&b.round, &b.face, &b.reversed)
);
assert_eq!(a.patches.len(), b.patches.len());
let mut worst = 0.0f64;
for (pa, pb) in a.patches.iter().zip(&b.patches) {
for i in 0..=8 {
for j in 0..=8 {
let uv = Point2::new(f64::from(i) / 8.0, f64::from(j) / 8.0);
let (p, q) = (
pa.evaluate_point(uv).unwrap(),
pb.evaluate_point(uv).unwrap(),
);
worst = worst.max(p.distance(q));
}
}
}
worst
}
/// The box corner's rounds r 2, 3, 4 (sections of `box_corner_round`'s `section`) at the
/// setbacks a fill to `orders` takes (G1: grown to the least scale that certifies it convex;
/// otherwise the rule's), filled once as they are and once with their sections moved across by a
/// Moebius map (`w_j rho0^j`, and with `rho1` one changing along them from `rho0` at the end
/// section to `rho1` at the far end): the same surfaces at another parameter across. Returns how
/// far the two fills lie apart, patch for patch, against `1e-12` of the corner's size, or why the
/// moved rounds' fill is refused. The rounds as they are must fill.
fn fill_moved_across(
orders: FillOrders,
section: usize,
rho0: f64,
rho1: Option<f64>,
) -> Result<(f64, f64), String> {
let k = Kernel::default();
let radii = [2.0, 3.0, 4.0];
let setbacks = corner_setbacks(&k, radii, orders.faces);
let size = setbacks.iter().copied().fold(f64::MIN, f64::max);
let rounds = |moved: bool| -> Vec<BSplineSurface> {
(0..3)
.map(|a| {
let (r, s) = (radii[a], setbacks[a]);
match (moved, rho1) {
(false, _) => box_corner_round(a, r, s, 10.0, section),
(true, None) => box_corner_round_moved(a, r, s, 10.0, section, rho0),
(true, Some(rho1)) => {
box_corner_round_moved_along(a, r, s, 10.0, section, rho0, rho1)
}
}
})
.collect()
};
let own = fill_of(&k, &rounds(false), orders)
.unwrap_or_else(|why| panic!("the rounds as they are: {why}"));
let other = fill_of(&k, &rounds(true), orders)?;
Ok((fill_gap(&own, &other), 1e-12 * size))
}
/// [`fill_moved_across`]'s case, graded: the same fill to `1e-12` of the corner's size.
fn assert_fill_moved_across(orders: FillOrders, section: usize, rho0: f64, rho1: Option<f64>) {
let name = format!(
"{orders:?}, the rounds moved across by {rho0}^j{}",
rho1.map_or(String::new(), |r| format!(", changing along to {r}^j"))
);
let (gap, tol) = fill_moved_across(orders, section, rho0, rho1)
.unwrap_or_else(|why| panic!("{name}: the fill is {why}"));
println!(" {name}: the fill off by {gap:.3e} (allowed {tol:.1e})");
assert!(gap <= tol, "{name}: the fill is off by {gap:e}");
}
const G1: FillOrders = FillOrders {
rounds: 1,
faces: 1,
};
const G2: FillOrders = FillOrders {
rounds: 2,
faces: 2,
};
/// The fill is the rounds' geometry's across them (the fork's
/// `setback_fill_is_the_geometry_across`, known failing in its `archive/wip/85c-curved-setback`):
/// the G1 fill of circular rounds r 2, 3, 4, their sections moved across by `1.7^j`.
#[test]
fn fill_is_the_geometry_across() {
assert_fill_moved_across(G1, 1, 1.7, None);
}
/// As [`fill_is_the_geometry_across`], moved only by `1.001^j`: the setbacks are the least that
/// certify, so the fill of rounds moved further (`1.003^j` already) may be refused; this one is
/// still certified convex, and it is the fill's distance that is graded.
#[test]
fn fill_is_the_geometry_across_mildly() {
assert_fill_moved_across(G1, 1, 1.001, None);
}
/// As [`fill_is_the_geometry_across`], the map changing along the rounds, `1.7^j` at the end
/// section to `1.2^j` at the far end: the rows' derivatives along move too.
#[test]
fn fill_is_the_geometry_across_changing_along() {
assert_fill_moved_across(G1, 1, 1.7, Some(1.2));
}
/// The G2 fill of order-2 rounds r 2, 3, 4, their sections moved across by `1.7^j`.
#[test]
fn g2_fill_is_the_geometry_across() {
assert_fill_moved_across(G2, 2, 1.7, None);
}
/// As [`g2_fill_is_the_geometry_across`], the map changing along the rounds (`1.7^j` to
/// `1.2^j`): the rows' first and second derivatives along move too.
#[test]
fn g2_fill_is_the_geometry_across_changing_along() {
assert_fill_moved_across(G2, 2, 1.7, Some(1.2));
}
/// The fill G2 to circular rounds and G1 to the faces: its rows come from the arc about the
/// ball's centre already, its setback patches' speed across from the spoke, which must be the
/// geometry's too. The circles moved across by `1.7^j`.
#[test]
fn g2_fill_is_the_geometry_across_beside_circles() {
assert_fill_moved_across(
FillOrders {
rounds: 2,
faces: 1,
},
1,
1.7,
None,
);
}
/// The fill is the rounds' geometry's along them (the fork's `setback_fill_is_the_geometry_s`,
/// fork main `8eac63c`): the same rounds linear along their edges and in other parameters along
/// them (polynomial, the middle row at 0.3 of the way; rational, at 0.65 with weight 2.5: other
/// speeds and accelerations at their ends), and for G2 also the circles eased into the order-2
/// sections over 2 before their ends (the sections there the order-2 ones, flat to second
/// order), give the same fill, patch for patch, at every (u, v) to `1e-12` of its size.
#[test]
fn fill_is_the_geometry_s() {
let k = Kernel::default();
let radii = [2.0, 3.0, 4.0];
for order in [1, 2] {
let setbacks = corner_setbacks(&k, radii, order);
let size = setbacks.iter().copied().fold(f64::MIN, f64::max);
let rounds = |how: usize| -> Vec<BSplineSurface> {
(0..3)
.map(|a| {
let (r, s) = (radii[a], setbacks[a]);
match how {
0 => box_corner_round(a, r, s, 10.0, order),
1 => box_corner_round_reparametrized(a, r, s, 10.0, order, 0.3, 1.0),
2 => box_corner_round_reparametrized(a, r, s, 10.0, order, 0.65, 2.5),
_ => {
let ramp = [EaseRamp {
from: s + 2.0,
to: s,
}];
let circle = box_corner_round(a, r, s, 10.0, 1);
let g2 = box_corner_round(a, r, s, 10.0, 2);
ease_surface(&circle, &g2, SurfParamType::U, &ramp).unwrap()
}
}
})
.collect()
};
let orders = FillOrders {
rounds: order,
faces: order,
};
let own = fill_of(&k, &rounds(0), orders).unwrap();
let ways: &[(usize, &str)] = if order == 1 {
&[(1, "polynomial"), (2, "rational")]
} else {
&[(1, "polynomial"), (2, "rational"), (3, "eased")]
};
for &(how, name) in ways {
let other = fill_of(&k, &rounds(how), orders)
.unwrap_or_else(|why| panic!("G{order}, the rounds {name}: {why}"));
let gap = fill_gap(&own, &other);
println!(" G{order}: the rounds {name} along: the fill off by {gap:.3e}");
assert!(
gap <= 1e-12 * size,
"G{order}, the rounds {name} along: the fill is off by {gap:e}"
);
}
}
}
-1
View File
@@ -83,7 +83,6 @@ crates/solidcore/src/blend/blend_setback.rs fill_hole_with with_capacity 5 open
crates/solidcore/src/blend/blend_setback.rs fill_hole_with with_capacity 6 open Vec::with_capacity(n)
crates/solidcore/src/blend/blend_setback.rs fill_hole_with with_capacity 7 open Vec::with_capacity(n)
crates/solidcore/src/blend/blend_setback.rs fill_hole_with with_capacity 8 open Vec::with_capacity(n)
crates/solidcore/src/blend/blend_setback.rs fill_hole_with with_capacity 9 open Vec::with_capacity(n)
crates/solidcore/src/blend/blend_setback.rs fill_hole_with with_capacity 10 open Vec::with_capacity(n)
crates/solidcore/src/blend/blend_setback.rs fill_hole_with with_capacity 11 open Vec::with_capacity(n)
crates/solidcore/src/blend/blend_setback.rs fill_hole_with with_capacity 12 open Vec::with_capacity(n)
1 # Panic-prone calls in library code that no deny lint catches (#2490). Written by
83 crates/solidcore/src/blend/blend_setback.rs
84 crates/solidcore/src/blend/blend_setback.rs
85 crates/solidcore/src/blend/blend_setback.rs
crates/solidcore/src/blend/blend_setback.rs
86 crates/solidcore/src/blend/blend_setback.rs
87 crates/solidcore/src/blend/blend_setback.rs
88 crates/solidcore/src/blend/blend_setback.rs