Kernel rig: designed continuity cases, G0 to G4, that prove the measurement

A new `continuity` area: seams whose order is certain by construction, each graded
`exact` (reads its order and not the next) at picked edges with Continuity Analysis's
default stations and tolerances: a box's crease (g0), a chamfer in place of a round (g0),
an r5 round on planes (g1), and a profile closed by a g1, g2, g3 or g4 blend curve between
lines, extruded (g1 … g4). `continuity.edges` may now be a list of points, picked as
`inspect.continuity` picks them, and `continuity.exact` asks for the order and no more.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
jyviko
2026-10-28 19:15:43 -04:00
committed by Irakli Kourtis
parent c37b0d2f02
commit 39bf63b16a
17 changed files with 490 additions and 25 deletions
+39 -24
View File
@@ -169,19 +169,32 @@ fn curvature_jumps(body: &solidcraft_engine::kernel::Body, near: [f64; 3], withi
Ok(json!({"seams": seams.len(), "stations_clear": stations, "worst_per_mm": worst, "worst_clear_per_mm": worst_clear}))
}
/// The seams a `continuity: {order, edges}` check measures on `b`, by edge index, with how each
/// lies: `blend`, every seam of a face a fillet or chamfer made (a round, a chamfer face or a
/// corner patch) with the faces either side of the edge it replaced, and every seam of a corner
/// patch with the rounds it joins: what the blend is built to meet. Where two rounds meet
/// without a patch, their seam is the input's (tangent only where the rounded edges were) and is
/// not one. `corner`, those of them on a corner patch. An error for any other selection.
fn blend_seams(b: &solidcraft_engine::doc::ModelBody, edges: &str) -> Result<Vec<solidcraft_engine::doc::naming::BlendSeam>, String> {
/// The seams a `continuity: {order, edges}` check measures on `b`, by edge index, with what
/// each lies between: `blend` (the default), every seam of a face a fillet or chamfer made (a
/// round, a chamfer face or a corner patch) with the faces either side of the edge it replaced,
/// and every seam of a corner patch with the rounds it joins: what the blend is built to meet.
/// Where two rounds meet without a patch, their seam is the input's (tangent only where the
/// rounded edges were) and is not one. `corner`, those of them on a corner patch. A list of
/// points `[[x,y,z], …]`, the edges nearest them, as Continuity Analysis picks them. An error for
/// anything else, or a point near no edge.
fn continuity_seams(b: &solidcraft_engine::doc::ModelBody, edges: &Value) -> Result<Vec<(usize, &'static str)>, String> {
use solidcraft_engine::doc::naming::BlendSeamKind;
if let Some(points) = edges.as_array() {
let picks = points
.iter()
.map(|p| v3(p).map(|[x, y, z]| solidcraft_engine::kernel::EdgePick::Point(solidcraft_engine::geom::Vec3::new(x, y, z))))
.collect::<Option<Vec<_>>>()
.ok_or("continuity `edges` points must each be [x,y,z]")?;
let picked = solidcraft_engine::kernel::pick_edges(&b.body, &picks).map_err(|e| e.to_string())?;
return Ok(picked.into_iter().map(|e| (e, "picked")).collect());
}
let all = solidcraft_engine::doc::naming::blend_seams(b).into_iter().filter(|s| s.kind == BlendSeamKind::Contact || s.corner);
match edges {
"blend" => Ok(all.collect()),
"corner" => Ok(all.filter(|s| s.corner).collect()),
_ => Err(format!("continuity `edges` must be \"blend\" or \"corner\", not {edges:?}")),
let label =
|s: &solidcraft_engine::doc::naming::BlendSeam| (s.edge, if s.kind == BlendSeamKind::Blend { "with_blends" } else { "with_contact_faces" });
match edges.as_str().unwrap_or("blend") {
"blend" => Ok(all.map(|s| label(&s)).collect()),
"corner" => Ok(all.filter(|s| s.corner).map(|s| label(&s)).collect()),
other => Err(format!("continuity `edges` must be \"blend\", \"corner\" or a list of points, not {other:?}")),
}
}
@@ -189,15 +202,16 @@ fn blend_seams(b: &solidcraft_engine::doc::ModelBody, edges: &str) -> Result<Vec
/// Analysis (`inspect.continuity`) measures them, at its default stations and tolerances, and the
/// lowest order among them with the seam that has it (the worst: of those, the one whose first
/// failing metric is furthest past its tolerance), its faces' names and that metric's worst value.
/// `Ok((report, ok))`, `ok` where every seam meets `want`.
/// `Ok((report, ok))`, `ok` where every seam meets `want` (`exact`: reaches it and no more).
fn continuity_order(
b: &solidcraft_engine::doc::ModelBody,
want: solidcraft_engine::kernel::Continuity,
edges: &str,
edges: &Value,
exact: bool,
) -> Result<(Value, bool), String> {
use solidcraft_engine::kernel::{self as k, Continuity};
let seams = blend_seams(b, edges)?;
let selected: Vec<usize> = seams.iter().map(|s| s.edge).collect();
let seams = continuity_seams(b, edges)?;
let selected: Vec<usize> = seams.iter().map(|s| s.0).collect();
if selected.is_empty() {
return Ok((json!(format!("{}: no {edges} seams", b.name)), false));
}
@@ -221,17 +235,14 @@ fn continuity_order(
let edge_names = solidcraft_engine::doc::naming::edge_names(b);
let order_word = |o: Option<Continuity>| o.map_or("none (the faces do not meet)", Continuity::as_str);
// How many seams reach each order, by what the blend meets there.
let kind = |edge: usize| match seams.iter().find(|s| s.edge == edge) {
Some(s) if s.kind == solidcraft_engine::doc::naming::BlendSeamKind::Blend => "with_blends",
_ => "with_contact_faces",
};
let kind = |edge: usize| seams.iter().find(|s| s.0 == edge).map_or("picked", |s| s.1);
let mut orders = json!({});
for s in &report.seams {
let n = &mut orders[kind(s.edge)][order_word(s.order)];
*n = json!(n.as_u64().unwrap_or(0) + 1);
}
let mut got = json!({
"body": b.name, "edges": edges, "seams": report.seams.len(), "unmeasured": report.unmeasured, "orders": orders,
"body": b.name, "edges": if edges.is_array() { "picked" } else { edges.as_str().unwrap_or("blend") }, "seams": report.seams.len(), "unmeasured": report.unmeasured, "orders": orders,
});
if let Some(s) = worst {
got["order"] = json!(order_word(s.order));
@@ -241,7 +252,9 @@ fn continuity_order(
json!({"metric": metric.get(i), "value": w.value, "tolerance": limits.get(i), "at": w.at, "edge_fraction": w.edge_fraction});
}
}
let ok = report.unmeasured == 0 && !report.seams.is_empty() && report.seams.iter().all(|s| s.order.is_some_and(|o| o >= want));
let ok = report.unmeasured == 0
&& !report.seams.is_empty()
&& report.seams.iter().all(|s| s.order.is_some_and(|o| if exact { o == want } else { o >= want }));
Ok((got, ok))
}
@@ -402,10 +415,12 @@ fn check_case(dir: &str, dumps: Option<&std::path::Path>) -> Value {
}
// The order every blend seam reaches, as Continuity Analysis measures it.
if let Some(order) = want["continuity"].get("order").filter(|o| !o.is_null()) {
let edges = want["continuity"]["edges"].as_str().unwrap_or("blend");
let need = format!(">= {} at every {edges} seam", order.as_str().unwrap_or("?"));
let edges = &want["continuity"]["edges"];
let exact = want["continuity"]["exact"].as_bool().unwrap_or(false);
let which = if edges.is_array() { "picked" } else { edges.as_str().unwrap_or("blend") };
let need = format!("{} {} at every {which} seam", if exact { "exactly" } else { ">=" }, order.as_str().unwrap_or("?"));
match order.as_str().and_then(solidcraft_engine::kernel::Continuity::parse) {
Some(o) => match continuity_order(b, o, edges) {
Some(o) => match continuity_order(b, o, edges, exact) {
Ok((got, ok)) => c.push("continuity_order", got, json!(need), ok),
Err(e) => c.push("continuity_order", json!(format!("{}: {e}", b.name)), json!(need), false),
},
+2 -1
View File
@@ -36,6 +36,7 @@ One directory per area, one per case: `tests/kernel-cases/<area>/<id>/`.
| `import` (STEP and IGES geometry) | booleans/shell/import/crash rig |
| `crashes` (crashes and hangs inside kernel operations) | booleans/shell/import/crash rig |
| `fillets`, `sweeps`, `threads`, `sheet-metal`, `tessellation` | history rig |
| `continuity` (designed seams of known order, G0 to G4, that prove the continuity measurement itself) | history rig |
`<id>` is `<source number>-<short-name>`, such as `1894-prism-cut-touching-walls`.
@@ -89,7 +90,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` | `{order, edges}`: every seam `edges` names reaches `order` (`g0` … `g4`), as Continuity Analysis (`inspect.continuity`) measures it: from the faces' exact surfaces, at its default 33 stations a seam and its default tolerances (1 µm, 1°, 0.01 / mm, 0.001 / mm², 0.0001 / mm³), never loosened per case. A failure names the worst seam (lowest order; of those, the one furthest past the next order's tolerance), its faces, the order it reaches and that next order's worst value. `edges`: `blend` (default), every seam of a face a fillet or chamfer made (a round, a chamfer face or a corner patch) with the faces either side of the edge it replaced, and every seam of a corner patch with the rounds it joins: what the blend is built to meet (not where a round runs into an end face, nor where two rounds meet without a patch: that seam is the input's, tangent only where the rounded edges were); `corner`, those of them on a corner patch (a blend made from several replaced edges). A constant round meets its faces G1 (curvature jumps by 1 / r), a `curvature` round G2, a chamfer G0; a `corner: g2` patch is built G2 to the rounds and faces round it. |
| `continuity` | `{order, edges}`: every seam `edges` names reaches `order` (`g0` … `g4`), as Continuity Analysis (`inspect.continuity`) measures it: from the faces' exact surfaces, at its default 33 stations a seam and its default tolerances (1 µm, 1°, 0.01 / mm, 0.001 / mm², 0.0001 / mm³), never loosened per case. A failure names the worst seam (lowest order; of those, the one furthest past the next order's tolerance), its faces, the order it reaches and that next order's worst value. `edges`: `blend` (default), every seam of a face a fillet or chamfer made (a round, a chamfer face or a corner patch) with the faces either side of the edge it replaced, and every seam of a corner patch with the rounds it joins: what the blend is built to meet (not where a round runs into an end face, nor where two rounds meet without a patch: that seam is the input's, tangent only where the rounded edges were); `corner`, those of them on a corner patch (a blend made from several replaced edges); or a list of points `[[x,y,z], …]`, the edges nearest them, as Continuity Analysis picks them. `exact: true`: every seam reaches `order` and not the next (a case built to an order proves the measurement reads that order, no more and no less). A constant round meets its faces G1 (curvature jumps by 1 / r), a `curvature` round G2, a chamfer G0; a `corner: g2` patch is built G2 to the rounds and faces round it. |
| `continuity` (older form) | `{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. |
@@ -0,0 +1,20 @@
{
"sources": [],
"body_count": 1,
"what": "two planes meeting at a right angle: the seam is a crease, G0 and not G1",
"source": "analytic",
"method": "box 20x20x20: its edge at x=20, y=0 joins the planes y=0 and x=20 with a 90 degree normal angle, so it is position continuous only. V = 8000, 6 faces.",
"volume_mm3": 8000.0,
"faces": 6,
"continuity": {
"order": "g0",
"exact": true,
"edges": [
[
20,
0,
10
]
]
}
}
@@ -0,0 +1,12 @@
{
"commands": [
{
"command": "solid.box",
"params": {
"length": 20,
"width": 20,
"height": 20
}
}
]
}
@@ -0,0 +1,25 @@
{
"sources": [],
"body_count": 1,
"what": "the G1 case's edge cut by a chamfer instead of rounded: the chamfer meets each plane at 45 degrees, one order short of G1",
"source": "analytic",
"method": "box 20x20x20, its edge at x=20, y=0 chamfered 5: the chamfer plane x - y = 15 meets y=0 along x=15 and x=20 along y=5 at 45 degrees (G0, not G1). V = 8000 - 20 * 12.5 = 7750, 7 faces.",
"volume_mm3": 7750.0,
"faces": 7,
"continuity": {
"order": "g0",
"exact": true,
"edges": [
[
15,
0,
10
],
[
20,
5,
10
]
]
}
}
@@ -0,0 +1,25 @@
{
"commands": [
{
"command": "solid.box",
"params": {
"length": 20,
"width": 20,
"height": 20
}
},
{
"command": "solid.chamfer",
"params": {
"edges": [
[
20,
0,
10
]
],
"distance": 5
}
}
]
}
@@ -0,0 +1,25 @@
{
"sources": [],
"body_count": 1,
"what": "a plane continued by a circular round: tangent, with the curvature jumping from 0 to 1/r, G1 and not G2",
"source": "analytic",
"method": "box 20x20x20, its edge at x=20, y=0 rounded r5: the quarter cylinder meets y=0 along x=15 and x=20 along y=5, tangent to both; its normal curvature across the seam is 1/5 = 0.2 per mm against the plane's 0, past the G2 tolerance. V = 8000 - 20 (r^2 - pi r^2 / 4) = 7892.6991, 7 faces.",
"volume_mm3": 7892.6990816987245,
"faces": 7,
"continuity": {
"order": "g1",
"exact": true,
"edges": [
[
15,
0,
10
],
[
20,
5,
10
]
]
}
}
@@ -0,0 +1,25 @@
{
"commands": [
{
"command": "solid.box",
"params": {
"length": 20,
"width": 20,
"height": 20
}
},
{
"command": "solid.fillet",
"params": {
"edges": [
[
20,
0,
10
]
],
"radius": 5
}
}
]
}
@@ -0,0 +1,24 @@
{
"sources": [],
"body_count": 1,
"what": "a plane continued by a surface extruded from a g2 blend curve: exactly G2",
"source": "invariants",
"method": "profile: lines (20,10)-(20,20)-(0,20)-(0,0)-(10,0) closed by sketch.blend_curve g2 from (10,0) to (20,10), extruded 10. The blend is one Bezier of degree 5 matching each line's derivatives through order 2 at its end; a line's derivatives past the first are 0, so the curve's derivatives 2..2 vanish there and its curvature vanish: G2 with each line, and its derivative of order 3 is not 0 (the control polygon bends there), so the next order fails. The extrusion carries the profile's continuity to both vertical seams (x=10, y=0 and x=20, y=10). No closed-form volume: 7 faces.",
"faces": 7,
"continuity": {
"order": "g2",
"exact": true,
"edges": [
[
10,
0,
5
],
[
20,
10,
5
]
]
}
}
@@ -0,0 +1,55 @@
{
"commands": [
{
"command": "sketch.create",
"params": {
"plane": "XY"
}
},
{
"command": "sketch.line",
"params": {
"points": [
[
20,
10
],
[
20,
20
],
[
0,
20
],
[
0,
0
],
[
10,
0
]
]
}
},
{
"command": "sketch.blend_curve",
"params": {
"a": "l4.end",
"b": "l1.start",
"continuity": "g2"
}
},
{
"command": "sketch.finish",
"params": {}
},
{
"command": "solid.extrude",
"params": {
"distance": 10
}
}
]
}
@@ -0,0 +1,24 @@
{
"sources": [],
"body_count": 1,
"what": "the G2 case built one order short (blend continuity g1): it must read G1, not G2",
"source": "invariants",
"method": "profile: lines (20,10)-(20,20)-(0,20)-(0,0)-(10,0) closed by sketch.blend_curve g1 from (10,0) to (20,10), extruded 10. The blend is one Bezier of degree 3 matching each line's derivatives through order 1 at its end; a line's derivatives past the first are 0, so the curve's derivatives 2..1 vanish there and its curvature: tangent, but its second derivative is not 0, so the curvature jumps (G1, not G2). The extrusion carries the profile's continuity to both vertical seams (x=10, y=0 and x=20, y=10). No closed-form volume: 7 faces.",
"faces": 7,
"continuity": {
"order": "g1",
"exact": true,
"edges": [
[
10,
0,
5
],
[
20,
10,
5
]
]
}
}
@@ -0,0 +1,55 @@
{
"commands": [
{
"command": "sketch.create",
"params": {
"plane": "XY"
}
},
{
"command": "sketch.line",
"params": {
"points": [
[
20,
10
],
[
20,
20
],
[
0,
20
],
[
0,
0
],
[
10,
0
]
]
}
},
{
"command": "sketch.blend_curve",
"params": {
"a": "l4.end",
"b": "l1.start",
"continuity": "g1"
}
},
{
"command": "sketch.finish",
"params": {}
},
{
"command": "solid.extrude",
"params": {
"distance": 10
}
}
]
}
@@ -0,0 +1,24 @@
{
"sources": [],
"body_count": 1,
"what": "a plane continued by a surface extruded from a g3 blend curve: exactly G3",
"source": "invariants",
"method": "profile: lines (20,10)-(20,20)-(0,20)-(0,0)-(10,0) closed by sketch.blend_curve g3 from (10,0) to (20,10), extruded 10. The blend is one Bezier of degree 7 matching each line's derivatives through order 3 at its end; a line's derivatives past the first are 0, so the curve's derivatives 2..3 vanish there and its curvature and its first arc-length derivative vanish: G3 with each line, and its derivative of order 4 is not 0 (the control polygon bends there), so the next order fails. The extrusion carries the profile's continuity to both vertical seams (x=10, y=0 and x=20, y=10). No closed-form volume: 7 faces.",
"faces": 7,
"continuity": {
"order": "g3",
"exact": true,
"edges": [
[
10,
0,
5
],
[
20,
10,
5
]
]
}
}
@@ -0,0 +1,55 @@
{
"commands": [
{
"command": "sketch.create",
"params": {
"plane": "XY"
}
},
{
"command": "sketch.line",
"params": {
"points": [
[
20,
10
],
[
20,
20
],
[
0,
20
],
[
0,
0
],
[
10,
0
]
]
}
},
{
"command": "sketch.blend_curve",
"params": {
"a": "l4.end",
"b": "l1.start",
"continuity": "g3"
}
},
{
"command": "sketch.finish",
"params": {}
},
{
"command": "solid.extrude",
"params": {
"distance": 10
}
}
]
}
@@ -0,0 +1,24 @@
{
"sources": [],
"body_count": 1,
"what": "a plane continued by a surface extruded from a g4 blend curve: exactly G4",
"source": "invariants",
"method": "profile: lines (20,10)-(20,20)-(0,20)-(0,0)-(10,0) closed by sketch.blend_curve g4 from (10,0) to (20,10), extruded 10. The blend is one Bezier of degree 9 matching each line's derivatives through order 4 at its end; a line's derivatives past the first are 0, so the curve's derivatives 2..4 vanish there and its curvature and its first two arc-length derivatives vanish: G4 with each line, and its derivative of order 5 is not 0 (the control polygon bends there), so the next order fails. The extrusion carries the profile's continuity to both vertical seams (x=10, y=0 and x=20, y=10). No closed-form volume: 7 faces.",
"faces": 7,
"continuity": {
"order": "g4",
"exact": true,
"edges": [
[
10,
0,
5
],
[
20,
10,
5
]
]
}
}
@@ -0,0 +1,55 @@
{
"commands": [
{
"command": "sketch.create",
"params": {
"plane": "XY"
}
},
{
"command": "sketch.line",
"params": {
"points": [
[
20,
10
],
[
20,
20
],
[
0,
20
],
[
0,
0
],
[
10,
0
]
]
}
},
{
"command": "sketch.blend_curve",
"params": {
"a": "l4.end",
"b": "l1.start",
"continuity": "g4"
}
},
{
"command": "sketch.finish",
"params": {}
},
{
"command": "solid.extrude",
"params": {
"distance": 10
}
}
]
}
@@ -0,0 +1 @@
[]