geom: a crowned height field rises by the whole crown at its box's middle

The crown's bump went onto the first net with any control point clear of the loop; a coarse
net could leave points supporting the box's middle without it, so the middle rose by only a
share of crown · size (the pentagonal cup: 7.6558 of 13.3877). The net is now refined (knot
insertion, the surface unchanged) until every point whose basis is not zero at the middle takes
the bump, where the middle is inside the loop: the bubble is then exactly 1 there, as fill_loop
documents.

The kernel test measured against the face's domain, which SMLib holds as the loop's extent; the
documented box is the height field's whole domain, the loop's box and 1% more each way (as the
engine's crown test says, 28 × 28 + 1%). Its diagonal is 1.02 times the face's, so the
documented rise is 0.25 × 53.5506 = 13.387661 = 1.02 × 13.125158, which the patch now meets to
1e-6 for crowns ±0.25.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
jyviko
2026-10-30 08:16:18 -04:00
committed by Irakli Kourtis
parent 62d41a0cfa
commit 30b12cea8c
2 changed files with 27 additions and 8 deletions
+18 -6
View File
@@ -307,8 +307,11 @@ fn fit_once(fit: &[Vec3], all: &[Vec3], tol: f64, crown: f64) -> SplineResult<Fi
/// is. A control point whose support rectangle keeps clear of the loop takes the bump's blossom
/// coefficients, so the surface between the perturbed points is the bubble exactly and the
/// surface over the loop is not moved at all (those points' basis functions are zero all along
/// it). A fitted net is often too coarse to hold one; it is then refined by knot insertion, which
/// leaves the surface itself as it is, until it does. Returns the crowned surface, how many
/// it). A fitted net is often too coarse for that; it is then refined by knot insertion, which
/// leaves the surface itself as it is, until every point whose basis function is not zero at the
/// box's middle takes the bump (where the middle is inside the loop), so the middle rises by the
/// bubble's full `crown · size` there, as documented, not by the share of it a coarse net's few
/// clear points carry. Returns the crowned surface, how many
/// control points took the bump, and the farthest the bump moved the surface at a sampled loop
/// point (0).
fn crown_fill(surface: NurbsSurface, uv: &[[f64; 2]], crown: f64, normal: Vec3) -> SplineResult<(NurbsSurface, usize, f64)> {
@@ -337,7 +340,7 @@ pub(crate) fn crown_fill_along(
let gap = loop_gap(uv);
let mut base = surface;
loop {
if let Some((crowned, points)) = crown_net(&base, uv, gap, crown, along)? {
if let Some((crowned, points)) = crown_net(&base, uv, gap, crown, along)?.filter(|c| c.2).map(|(c, n, _)| (c, n)) {
let mut drift = 0.0f64;
for q in uv {
if let (Ok(a), Ok(b)) = (base.eval(q[0], q[1]), crowned.eval(q[0], q[1])) {
@@ -365,14 +368,15 @@ pub(crate) fn crown_fill_along(
/// The crown's bump in the net `base`: a control point whose support rectangle keeps clear of the
/// loop takes `crown · size · B(ξ, η)`, `B` the bubble's blossom at the point's knots (so the
/// surface between the perturbed points is exactly the bubble, not a fit of it) and `size` the
/// box's diagonal. `None` when the net has no such point.
/// box's diagonal. `None` when the net has no such point; with it, whether every point
/// supporting the box's middle took the bump (or the middle is outside the loop).
fn crown_net(
base: &NurbsSurface,
uv: &[[f64; 2]],
gap: [f64; 2],
crown: f64,
along: &dyn Fn([f64; 2]) -> Option<Vec3>,
) -> SplineResult<Option<(NurbsSurface, usize)>> {
) -> SplineResult<Option<(NurbsSurface, usize, bool)>> {
let [cu, cv] = base.count();
let degree = base.degree();
let knots = base.knots();
@@ -391,10 +395,16 @@ fn crown_net(
let (Some(knots_u), Some(knots_v)) = (knots.first(), knots.get(1)) else { return invalid("the surface has no knot vectors") };
let mut ctrl = base.homogeneous().to_vec();
let mut points = 0usize;
let middle = [0.5 * (domain[0][0] + domain[0][1]), 0.5 * (domain[1][0] + domain[1][1])];
// The points whose basis is not zero at the middle, and those of them that took the bump.
let (mut at_middle, mut raised_middle) = (0usize, 0usize);
let holds = |span: [f64; 2], x: f64| span[0] < x && x < span[1];
for j in 0..cv {
let Some((v_span, v_knots)) = support(knots_v, degree[1], j) else { continue };
for i in 0..cu {
let Some((u_span, u_knots)) = support(knots_u, degree[0], i) else { continue };
let supports = holds(u_span, middle[0]) && holds(v_span, middle[1]);
at_middle += usize::from(supports);
if !clear_of_loop(u_span, v_span, uv, gap) {
continue;
}
@@ -406,12 +416,14 @@ fn crown_net(
let Some(p) = h.project() else { continue };
*slot = HPoint::weighted(p + normal * bump, h.w);
points += 1;
raised_middle += usize::from(supports);
}
}
if points == 0 {
return Ok(None);
}
Ok(Some((NurbsSurface::from_homogeneous(degree, knots.clone(), ctrl)?, points)))
let middle_held = raised_middle == at_middle || !inside_loop(middle, uv);
Ok(Some((NurbsSurface::from_homogeneous(degree, knots.clone(), ctrl)?, points, middle_held)))
}
/// The same surface with a knot inserted in the middle of every span of direction `d` (0 = u):
+9 -2
View File
@@ -1584,10 +1584,17 @@ mod tests {
let off = edges_off_surface(&full);
assert!(off <= super::face_tolerance(&open), "an edge is {off} off the crowned face");
mesh_is_clean(&full);
// The middle of the loop's box moved by crown × its diagonal (the bump is 1 there).
// The middle of the fill's box moved by crown × its diagonal (the bump is 1 there).
let (ra, rb) = (record(&plain), record(&full));
let (a, b) = (SurfaceEval::new(ra.faces[0].parameters()).unwrap(), SurfaceEval::new(rb.faces[0].parameters()).unwrap());
let [[bu0, bu1], [bv0, bv1]] = ra.faces[0].domain;
// The height field's box is its surface's whole domain (the loop's box and 1% more
// each way, where the bubble is nought), not the face's: SMLib holds a face's domain
// as its loop's extent, 2% less across.
let (b::Surface::Spline { u_knots, v_knots, .. } | b::Surface::RationalSpline { u_knots, v_knots, .. }) = ra.faces[0].parameters() else {
panic!("a height field is a spline")
};
let ends = |k: &[f64]| [k[0], k[k.len() - 1]];
let ([bu0, bu1], [bv0, bv1]) = (ends(u_knots), ends(v_knots));
let (uc, vc) = ((bu0 + bu1) / 2.0, (bv0 + bv1) / 2.0);
let size = (bu1 - bu0).hypot(bv1 - bv0);
let bump = a.point([uc, vc]).unwrap().dist(b.point([uc, vc]).unwrap());