geom: a general fill's crown fills out to the side its neighbours' normals face

The crown moved the free points along the patch's own normal, whose sign is only the way the
patch happens to be parameterised: the pentagonal window's crown 0.1 sank 1.56 mm into the
cylinder (the middle 9.959 from the axis plain, 8.395 crowned) where the kernel documents it
filling out to the side the G1/G2 neighbours' normals face. The crown now takes that side: the
sum of the patch's normals against the neighbours' over the stations sets one sign for the fill.
The pentagon's middle goes out to 11.526.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
jyviko
2026-10-30 08:18:50 -04:00
committed by Irakli Kourtis
parent 30b12cea8c
commit d5941c0d06
2 changed files with 33 additions and 2 deletions
+12 -2
View File
@@ -2117,7 +2117,8 @@ fn folds(patch: &Patch) -> bool {
false
}
/// The fill with its middle filled out by `crown` times the loop's `size` along the normals, on
/// The fill with its middle filled out by `crown` times the loop's `size` along the normals (to
/// the side the neighbours' normals face), on
/// control points clear of the rows the conditions hold (n + 1 rows from a G^n edge), so the loop and the conditions across it do not move. One patch
/// rises by `16·u(1−u)·v(1−v)`; patches about a centre by `u(2−u)·v(2−v)`, nought on the loop and
/// one at the centre, the same on a spoke from either side.
@@ -2126,6 +2127,15 @@ fn crowned(mut quilt: Quilt, stations: &[Station], crown: f64, size: f64) -> Spl
return Ok(quilt);
}
let held = 1 + stations.iter().map(Station::order).max().unwrap_or(0);
// Out to the side the neighbours' normals face (the patches all turn one way).
let mut facing = 0.0;
for s in stations {
let (uv, _) = station_on(s.side, s.t);
if let Some(n) = quilt.patches.get(s.patch).and_then(|p| p.normal(uv[0], uv[1])) {
facing += n.dot(s.across.normal);
}
}
let out = if facing < 0.0 { -1.0 } else { 1.0 };
let about_centre = !quilt.spokes.is_empty();
let bump = |x: f64, y: f64| if about_centre { x * (2.0 - x) * y * (2.0 - y) } else { 16.0 * x * (1.0 - x) * y * (1.0 - y) };
let mut moved = std::collections::HashMap::new();
@@ -2138,7 +2148,7 @@ fn crowned(mut quilt: Quilt, stations: &[Station], crown: f64, size: f64) -> Spl
let Some(Some(id)) = quilt.slots.get(k).and_then(|s| s.get(patch.flat(i, j))) else { continue };
let (x, y) = (gu.get(i).copied().unwrap_or(0.5), gv.get(j).copied().unwrap_or(0.5));
let Some(n) = patch.normal(x, y) else { continue };
moved.entry(*id).or_insert_with(|| patch.at(i, j) + n * (crown * size * bump(x, y)));
moved.entry(*id).or_insert_with(|| patch.at(i, j) + n * (out * crown * size * bump(x, y)));
}
}
}
+21
View File
@@ -370,3 +370,24 @@ fn a_loop_whose_edges_run_straight_on_at_seams_is_one_patch_on_its_corners() {
assert!(angle > 0.5 && angle < 179.5, "a flat corner at {c:?}: {angle}°");
}
}
/// A crown fills the middle out to the side the neighbours' normals face, whichever way the
/// patch itself is parameterised: the sphere window, its neighbours' normals outward, rises away
/// from the centre; given them inward, it sinks.
#[test]
fn a_crown_fills_out_to_the_side_the_neighbours_face() {
let edges = window();
let middle = |f: &IsoFill| f.patches[0].surface.eval(0.5, 0.5).unwrap().len();
let plain = middle(&iso_fill(&edges, &mut sphere_beside(&edges, 1), &spec()).unwrap());
let crowned = IsoSpec { crown: 0.1, ..spec() };
let out = middle(&iso_fill(&edges, &mut sphere_beside(&edges, 1), &crowned).unwrap());
let mut inward = {
let mut beside = sphere_beside(&edges, 1);
move |k: usize, ts: &[f64]| -> Result<Vec<Option<Across>>, String> {
Ok(beside(k, ts)?.into_iter().map(|a| a.map(|a| Across { normal: a.normal * -1.0, ..a })).collect())
}
};
let sunk = middle(&iso_fill(&edges, &mut inward, &crowned).unwrap());
eprintln!("the window's middle {plain:.4} from the centre, crowned {out:.4}, crowned to inward neighbours {sunk:.4}");
assert!(out > plain + 0.1 && sunk < plain - 0.1, "{plain} {out} {sunk}");
}