kernel-cases: a continuity check, and ladder 07/015 graded by it

expected.json `continuity`: over the tangent seams wholly within `within_mm`
of `near`, the curvature jump across them (exact surfaces, 65 stations a
seam) at most `curvature_jump_per_mm` clear (`clear_mm` along the seam) of the
vertices where two of them turn, where a neighbour's own G1 seam ends.

07/015, the box corner's three r5 rounds with corner: g2: one valid body of 10
faces within the box, its patch's twelve seams jumping at most 1e-3 per mm
clear of the hole's six corners by 1.2 mm (2.7e-4 measured; the ball corner
of 009 jumps 0.2). Its golden entry is the passing run's.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
jyviko
2026-10-26 12:40:11 -04:00
committed by Irakli Kourtis
parent d8282b3361
commit 8fbb575a5e
5 changed files with 115 additions and 6 deletions
+72
View File
@@ -114,6 +114,60 @@ fn open_edges(positions: &[solidcraft_engine::geom::Vec3], triangles: &[[u32; 3]
}
/// An error that means "this kernel can't do it yet", not a wrong answer.
/// Stations along each seam the `continuity` check samples.
const SEAM_SAMPLES: usize = 65;
/// A seam is tangent (G1), and two seams meeting at a vertex turn there, past this angle (°).
const SEAM_ANGLE_DEG: f64 = 1.0;
/// The `continuity` check's measure of `body`: over the tangent seams lying wholly within
/// `within` of `near`, the worst curvature jump (1/mm) at every station and at the stations at
/// least `clear` (mm, along the seam) from each vertex where two of those seams turn (a corner of
/// the patch's boundary, where its neighbours' own G1 seams end and no surface is G2 to both),
/// with how many seams and stations clear there were. An error where the seams can't be measured.
fn curvature_jumps(body: &solidcraft_engine::kernel::Body, near: [f64; 3], within: f64, clear: f64) -> Result<Value, String> {
use solidcraft_engine::geom::Vec3;
let tol = solidcraft_engine::kernel::ContinuityTolerances { gap_mm: 1e-3, angle_deg: SEAM_ANGLE_DEG, curvature_per_mm: 0.0 };
let report = solidcraft_engine::kernel::seam_diagnostics(body, &[], SEAM_SAMPLES, tol).map_err(|e| e.to_string())?;
let at = Vec3::new(near[0], near[1], near[2]);
let seams: Vec<_> = report
.seams
.iter()
.filter(|s| s.normal_angle_deg.value <= SEAM_ANGLE_DEG && s.points.len() >= 2 && s.points.iter().all(|p| p.dist(at) <= within))
.collect();
// Each seam's end directions, pointing into it.
let ends = |s: &solidcraft_engine::kernel::SeamReport| -> Option<[(Vec3, Vec3); 2]> {
let (p0, p1) = (*s.points.first()?, *s.points.last()?);
let (q0, q1) = (*s.points.get(1)?, *s.points.get(s.points.len() - 2)?);
Some([(p0, (q0 - p0).normalized()?), (p1, (q1 - p1).normalized()?)])
};
let all_ends: Vec<(Vec3, Vec3)> = seams.iter().filter_map(|s| ends(s)).flatten().collect();
let weld = 1e-6 * within.max(1.0);
let cos = SEAM_ANGLE_DEG.to_radians().cos();
// A vertex turns where another seam leaves it other than straight on.
let turns = |p: Vec3, d: Vec3| all_ends.iter().any(|(q, e)| q.dist(p) <= weld && (*e - d).len() > 1e-9 && -e.dot(d) < cos);
let (mut worst, mut worst_clear, mut stations) = (0.0f64, 0.0f64, 0usize);
for s in &seams {
let Some(e) = ends(s) else { continue };
let turning: Vec<Vec3> = e.iter().filter(|(p, d)| turns(*p, *d)).map(|(p, _)| *p).collect();
// Arc length along the seam to each station, from its start.
let mut along = vec![0.0f64; s.points.len()];
for k in 1..s.points.len() {
along[k] = along[k - 1] + s.points[k].dist(s.points[k - 1]);
}
let length = along.last().copied().unwrap_or(0.0);
for (k, st) in s.stations.iter().enumerate() {
worst = worst.max(st.curvature_jump_per_mm);
let a = along.get(k).copied().unwrap_or(0.0);
let from_turn = turning.iter().map(|p| if p.dist(s.points[0]) <= weld { a } else { length - a }).fold(f64::INFINITY, f64::min);
if from_turn >= clear {
worst_clear = worst_clear.max(st.curvature_jump_per_mm);
stations += 1;
}
}
}
Ok(json!({"seams": seams.len(), "stations_clear": stations, "worst_per_mm": worst, "worst_clear_per_mm": worst_clear}))
}
fn unsupported(e: &str) -> bool {
e.contains("not supported yet")
}
@@ -256,6 +310,24 @@ fn check_case(dir: &str, dumps: Option<&std::path::Path>) -> Value {
open == 0 && over == 0,
);
}
// Curvature continuity across the seams round a point: the worst jump clear of the
// seams' turns must be within the case's bound.
if let Some(cont) = want.get("continuity").filter(|c| !c.is_null()) {
let (near, within, clear, bound) =
(v3(&cont["near"]), cont["within_mm"].as_f64(), cont["clear_mm"].as_f64(), cont["curvature_jump_per_mm"].as_f64());
match (near, within, clear, bound) {
(Some(near), Some(within), Some(clear), Some(bound)) => match curvature_jumps(&b.body, near, within, clear) {
Ok(got) => {
let ok = got["seams"].as_u64().unwrap_or(0) > 0
&& got["stations_clear"].as_u64().unwrap_or(0) > 0
&& got["worst_clear_per_mm"].as_f64().is_some_and(|w| w <= bound);
c.push("curvature_jump", got, json!({"worst_clear_per_mm": format!("<= {bound}")}), ok);
}
Err(e) => c.push("curvature_jump", json!(format!("{}: {e}", b.name)), json!("measurable"), false),
},
_ => c.push("curvature_jump", json!("continuity needs near, within_mm, clear_mm and curvature_jump_per_mm"), json!("a bound"), false),
}
}
if want_valid {
// Closed shells, and every validity check this build has.
if want_shells.is_none() {
+1
View File
@@ -88,6 +88,7 @@ the frozen truck-geometry PRs, a `kernel-paused` PR), for the report's coverage
| `body_count`, `faces`, `edges`, `vertices` | Exact counts, where the geometry fixes them. Faces and edges count whole analytic surfaces and curves once (a full cylinder side is one face). |
| `volume_mm3`, `area_mm2`, `center_of_mass`, `bbox` | As in `tests/oracle`: totals over every body, relative tolerance 1e-3 for volume and area, 1e-3 of the box diagonal for positions. |
| `faces`, `edges`, `vertices`, `shells`, `body_count` as `[min, max]` | An inclusive range, where the geometry only bounds a count (a modelled thread's faces). |
| `continuity` | `{near, within_mm, clear_mm, curvature_jump_per_mm}`: over every tangent seam lying wholly within `within_mm` of `near` (a corner's patch and its neighbours), the curvature jump across it (the faces' normal curvatures square to the seam, from their exact surfaces, at 65 stations a seam) is at most `curvature_jump_per_mm` at every station at least `clear_mm` along the seam from a vertex where two of those seams turn (where a neighbour's own G1 seam ends, which no surface is G2 to both sides of). A rolling ball's corner jumps 1 / r everywhere. |
| `watertight` | `true`: each body's display mesh (chord tolerance `tolerance.mesh_mm`, default 0.01 mm) has no open and no over-shared edges, with vertices closer than `tolerance.weld_mm` (default 1e-4 mm) taken as one. |
| `timeout_s` | A longer timeout for this case than the run's (at most 1800 s), for cases that are slow by nature. A case that only needs it on a loaded machine shouldn't set it. |
| `tolerance` | Optional overrides: `rel`, `abs_mm`, `mesh_mm`, `weld_mm`. |
@@ -2,7 +2,29 @@
"sources": [],
"what": "three r5 rounds at a corner with the g2 corner patch",
"source": "invariants",
"method": "the g2 patch replaces the sphere octant; no closed-form volume; one valid body, the same faces as 009",
"method": "the corner set back 1.5 r = 7.5 and filled with one patch G2 to the three rounds and the three faces; no closed-form volume; one valid body, the same faces as 009, within the box; across the patch's seams the curvature jump is at most 1e-3 per mm (the ball's corner, 009, jumps 1/r = 0.2) clear of the six points where a round's section ends on a face, where the round's own G1 seam with the face ends (the patch passes from one curvature to the other within 15% of each side: 1.2 mm covers it)",
"body_count": 1,
"faces": 10
"faces": 10,
"bbox": {
"min": [
0,
0,
0
],
"max": [
60,
40,
20
]
},
"continuity": {
"near": [
60,
40,
20
],
"within_mm": 15,
"clear_mm": 1.2,
"curvature_jump_per_mm": 0.001
}
}
+10 -2
View File
@@ -1189,8 +1189,16 @@
"status": "pass"
},
"ladder/07-fillets/015-corner-g2-patch": {
"seconds": 0.00302384,
"status": "unsupported"
"measured": {
"area_mm2": 8520.38925544218,
"body_count": 1,
"edges": 24,
"faces": 10,
"vertices": 16,
"volume_mm3": 47370.10741440046
},
"seconds": 2.184099933,
"status": "pass"
},
"ladder/07-fillets/016-corner-three-sequential-r5-r3-r7": {
"seconds": 0.045599545,
+8 -2
View File
@@ -863,8 +863,14 @@ case(T7, "014-curved-edge-r2", "an r2 round on the top edge of a cylinder r10 h2
{"body_count": 1, "volume_mm3": PI * 100 * 20 - SPANDREL * 4 * 2 * PI * (10 - XBAR * 2), "faces": 4},
"a torus round: Pappus on the removed section (1-pi/4) r^2 at radius 10 - r(10-3pi)/(12-3pi)")
case(T7, "015-corner-g2-patch", "three r5 rounds at a corner with the g2 corner patch",
[F(corner, 5, corner="g2")], {"body_count": 1, "faces": 10},
"the g2 patch replaces the sphere octant; no closed-form volume; one valid body, the same faces as 009", base=B1, source="invariants")
[F(corner, 5, corner="g2")],
{"body_count": 1, "faces": 10, "bbox": {"min": [0, 0, 0], "max": [60, 40, 20]},
"continuity": {"near": [60, 40, 20], "within_mm": 15, "clear_mm": 1.2, "curvature_jump_per_mm": 0.001}},
"the corner set back 1.5 r = 7.5 and filled with one patch G2 to the three rounds and the three faces; no closed-form volume; "
"one valid body, the same faces as 009, within the box; across the patch's seams the curvature jump is at most 1e-3 per mm "
"(the ball's corner, 009, jumps 1/r = 0.2) clear of the six points where a round's section ends on a face, where the round's "
"own G1 seam with the face ends (the patch passes from one curvature to the other within 15% of each side: 1.2 mm covers it)",
base=B1, source="invariants")
V7 = V5 - 2 * SPANDREL * 25 * 20 - SPANDREL * 9 * 60 + SPANDREL * 1 * 2 * PI * (8 + XBAR * 1)
case(T7, "017-part-rounds", "the running part: r5 on the front vertical edges, r3 on the back wall's top front edge, r1 where the boss meets the top",
[F([[0, 0, 10], [60, 0, 10]], 5), F([[30, 30, 35]], 3), F([[23, 15, 20]], 1)],