geom: the patch fill takes the dense spline edges a blend leaves, and holds what its corners allow

- One patch has a space along u and one along v, each holding its two sides that run that way;
  a side's knot keeps its continuity when raised (multiplicity m + q - p), and each side is
  carried in exactly by interpolation at the space's Greville abscissae.
- Refinement halves the spans only in the direction whose sides miss, and one patch's normal
  equations are banded (the shorter direction numbered fastest), so a loop with a long
  intersection curve refines where it needs to.
- At a corner whose neighbours agree to a lower order than asked (their tangent planes, their
  twist between the two edges, or their forms), the stations near it ask no more than that.
- A station is held to the angle tolerance beyond the edge's own lean out of its neighbour's
  tangent plane: a fill that holds the edge cannot come nearer.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
jyviko
2026-10-27 20:35:03 -04:00
committed by Irakli Kourtis
parent 99297495b3
commit ed830a168d
3 changed files with 470 additions and 213 deletions
File diff suppressed because it is too large Load Diff
+4 -4
View File
@@ -130,7 +130,7 @@ fn jump(fill: &IsoFill, edges: &[IsoEdge], k: usize) -> f64 {
fn a_sphere_window_on_rational_arcs_holds_each_order_asked_and_not_the_next() {
let edges = window();
for order in 1..=4 {
let fill = iso_fill(&edges, &mut sphere_beside(&edges, order), &[false; 4], &spec()).unwrap();
let fill = iso_fill(&edges, &mut sphere_beside(&edges, order), &spec()).unwrap();
let a = fill.achieved;
let measured: Vec<f64> = (2..=4).map(|k| jump(&fill, &edges, k)).collect();
eprintln!(
@@ -168,7 +168,7 @@ fn a_sphere_window_on_rational_arcs_holds_each_order_asked_and_not_the_next() {
#[test]
fn hostile_edges_are_refused() {
let edges = window();
let ok = |e: &[IsoEdge]| iso_fill(e, &mut sphere_beside(e, 2), &[false; 4], &spec());
let ok = |e: &[IsoEdge]| iso_fill(e, &mut sphere_beside(e, 2), &spec());
let mut bad = edges.clone();
bad[1].weights = Some(vec![-1.0; bad[1].points.len()]);
assert!(ok(&bad).is_err(), "a negative weight");
@@ -197,7 +197,7 @@ fn hostile_edges_are_refused() {
}
Ok(got)
};
assert!(iso_fill(&edges, &mut too_high, &[false; 4], &spec()).is_err());
assert!(iso_fill(&edges, &mut too_high, &spec()).is_err());
}
/// The rational basis's derivatives (the Leibniz rule on `w N N = R W`) against central
@@ -205,7 +205,7 @@ fn hostile_edges_are_refused() {
#[test]
fn a_rational_patch_s_derivatives_are_its_differences() {
let edges = window();
let q = Quilt::new(&edges, &[], 3).unwrap();
let q = Quilt::new(&edges, &[Vec::new(), Vec::new()], 3).unwrap();
let p = &q.patches[0];
assert!(p.rational);
let (u, v, h) = (0.3, 0.6, 1e-5);
+5 -13
View File
@@ -80,6 +80,9 @@ pub struct PatchReport {
/// corners where the neighbours themselves disagree, and anywhere.
pub normal_deviation_deg: f64,
pub normal_deviation_all_deg: f64,
/// The largest angle (degrees) by which the patch's normal misses a neighbour's beyond the
/// loop edge's own lean out of that neighbour's tangent plane (what the tolerance holds).
pub normal_excess_deg: f64,
/// `jumps[k]`, k = 2 … 4: the largest jump across the edges asked for order k or more, clear
/// of the loop's corners, of the k-th derivative of the linkage curves' height above the
/// neighbour (1/mmᵏ⁻¹): the normal curvature (k = 2), its rate of change
@@ -154,18 +157,6 @@ fn general_fill(
for (&(e, forward), order) in chain.iter().zip(orders) {
beside.push(if *order == Continuity::G0 { None } else { Some(Beside::new(record, e, forward, *order, size)?) });
}
// Where a G1/G2 edge's neighbour and the one before disagree at their corner.
let n = chain.len();
let mut clash = vec![false; n];
for (k, slot) in clash.iter_mut().enumerate() {
let prev = (k + n - 1) % n;
let (Some(Some(a)), Some(Some(b))) = (beside.get(prev), beside.get(k)) else { continue };
let (Some(ea), Some(eb)) = (edges.get(prev), edges.get(k)) else { continue };
let (end, start) = (ea.knots.last().copied().unwrap_or(0.0), eb.knots.first().copied().unwrap_or(0.0));
if let (Some(x), Some(y)) = (a.across(record, end)?, b.across(record, start)?) {
*slot = x.normal.dot(y.normal).abs() < (2.0 * PATCH_ANGLE_TOL_DEG).to_radians().cos();
}
}
let mut across = |k: usize, ts: &[f64]| -> std::result::Result<Vec<Option<Across>>, String> {
match beside.get(k) {
None => Err("no such edge".into()),
@@ -174,7 +165,7 @@ fn general_fill(
}
};
let spec = IsoSpec { tol: face_tolerance(body), angle_tol: PATCH_ANGLE_TOL_DEG.to_radians(), crown, points };
let fill = solidcraft_geom::fill_iso::iso_fill(&edges, &mut across, &clash, &spec).map_err(spline_error)?;
let fill = solidcraft_geom::fill_iso::iso_fill(&edges, &mut across, &spec).map_err(spline_error)?;
let made = quilt_body(body, record, chain, &fill, face_tolerance(body))?;
let a = fill.achieved;
let report = PatchReport {
@@ -182,6 +173,7 @@ fn general_fill(
gap: 0.0,
normal_deviation_deg: a.normal_deviation.to_degrees(),
normal_deviation_all_deg: a.normal_deviation_all.to_degrees(),
normal_excess_deg: a.normal_excess.to_degrees(),
jumps: a.jumps,
point_gap: a.point_deviation,
};