mirror of
https://github.com/bherbruck/solidcraft.git
synced 2026-11-11 19:22:01 +00:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 06e80c0a40 | |||
| a25715bdba |
+119
-53
@@ -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> {
|
||||
|
||||
+414
-2
@@ -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}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Vendored
-1
@@ -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)
|
||||
|
||||
|
Reference in New Issue
Block a user