Kernel rig: surfaces area; an offset plane, cylinder and sphere land exactly where the distance puts them, and faces across a sharp edge part

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
jyviko
2026-10-29 17:03:27 -04:00
committed by Irakli Kourtis
parent bc3c9185cd
commit 657c5af579
14 changed files with 323 additions and 0 deletions
+4
View File
@@ -40,6 +40,7 @@ One directory per area, one per case: `tests/kernel-cases/<area>/<id>/`.
| `solids` (cones, scale, align, bounding solids) | tool coverage |
| `frames` (frame members, inserted profiles) | tool coverage |
| `plastic` (boss, lip and groove, rest, snap fit, plastic rules) | tool coverage rig |
| `surfaces` (every `surface.*` tool: offset, ruled, untrim, unstitch, reverse normal, boundary, blend, match, fair, and a normal and an edge case of each of the others) | surfaces rig |
Each area also has its gate files: `KNOWN_FAILING` and `PATHS` (see [the gate](#the-kernel-rig-gate)).
@@ -97,6 +98,9 @@ the frozen truck-geometry PRs, a `kernel-paused` PR), for the report's coverage
| `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); 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. |
| `surface_types` | The set of kinds the faces' surfaces are, over every body, as the records hold them: `plane`, `cylinder`, `cone`, `sphere`, `torus`, `bspline` (a B-spline or NURBS surface) or `revolution` (a revolved curve). Exact: none missing, none extra. |
| `splines` | `[{degree: [p, q], spans?: [m, n]}, …]`: one entry per B-spline face, each matching one face (directions in either order); `spans`, the distinct knot intervals each way, only where the construction fixes them. |
| `boundary_edges` | The edges only one face uses (a sheet's free boundary), over every body, as the records hold them; exact or `[min, max]`. |
| `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`. |
@@ -0,0 +1,5 @@
# The surfaces cases that fail today, one a line: `<case> <why it fails today>`. Sorted. Run as a
# regression gate (`cargo xtask kernel-cases --area surfaces --known-failing <this file>`), a case
# that fails and is not listed fails it, and so does a listed case that passes: a fix removes its
# line in the same pull request.
# Baseline: rig-cases-surfaces 45f69c0e5 with SMLib 60b1c6a, aarch64 Linux.
+32
View File
@@ -0,0 +1,32 @@
# The sources whose change must rerun the surfaces area: one git pathspec glob a line.
# The commands.
crates/engine/src/cmd/surface.rs
crates/engine/src/cmd/surface_boundary.rs
crates/engine/src/cmd/surface_match.rs
crates/engine/src/params.rs
# Their evaluation.
crates/doc/src/eval_surface.rs
crates/doc/src/eval.rs
# The kernel code they call.
crates/kernel/src/boundary.rs
crates/kernel/src/boundary_network.rs
crates/kernel/src/bridge.rs
crates/kernel/src/fair.rs
crates/kernel/src/fill*.rs
crates/kernel/src/loft*.rs
crates/kernel/src/matchsurf.rs
crates/kernel/src/native.rs
crates/kernel/src/surface_extend.rs
crates/kernel/src/surface_offset.rs
crates/kernel/src/surface_ruled.rs
crates/kernel/src/surface_trim.rs
crates/kernel/src/surface_untrim.rs
crates/kernel/src/surfaces.rs
crates/kernel/src/sweep*.rs
crates/kernel/src/smlib_sweeps.rs
crates/geom/src/fill*.rs
crates/geom/src/network*.rs
# SMLib and the rig itself.
crates/smlib/**
apps/solidcraft-cli/src/kernel_cases.rs
tests/kernel-cases/surfaces/**
@@ -0,0 +1,26 @@
{
"sources": [],
"what": "the top face of a 20 x 10 x 10 box offset 5 up: a plane moves exactly",
"source": "analytic",
"method": "the top (z = 10) along its outward normal +z by 5: the square 20 x 10 at z = 15, area 200, one plane face with its 4 edges free; the box is removed so only the offset is graded",
"valid": false,
"body_count": 1,
"faces": 1,
"boundary_edges": 4,
"area_mm2": 200.0,
"surface_types": [
"plane"
],
"bbox": {
"min": [
0,
0,
15
],
"max": [
20,
10,
15
]
}
}
@@ -0,0 +1,35 @@
{
"commands": [
{
"command": "solid.box",
"params": {
"length": 20,
"width": 10,
"height": 10,
"body_name": "Box"
}
},
{
"command": "surface.offset",
"params": {
"faces": [
[
10,
5,
10
]
],
"distance": 5,
"body_name": "Lid"
}
},
{
"command": "solid.remove",
"params": {
"bodies": [
"Box"
]
}
}
]
}
@@ -0,0 +1,26 @@
{
"sources": [],
"what": "the side of a cylinder r 10, 20 high, offset 2 outward: the cylinder r 12",
"source": "analytic",
"method": "a cylinder's offset along its outward normal is the coaxial cylinder r + d = 12: A = 2 pi 12 20 = 1507.96; one face closed on its seam, its two circles free",
"valid": false,
"body_count": 1,
"faces": 1,
"boundary_edges": 2,
"area_mm2": 1507.9644737231006,
"surface_types": [
"cylinder"
],
"bbox": {
"min": [
-12,
-12,
0
],
"max": [
12,
12,
20
]
}
}
@@ -0,0 +1,34 @@
{
"commands": [
{
"command": "solid.cylinder",
"params": {
"radius": 10,
"height": 20,
"body_name": "Post"
}
},
{
"command": "surface.offset",
"params": {
"faces": [
[
10,
0,
10
]
],
"distance": 2,
"body_name": "Sleeve"
}
},
{
"command": "solid.remove",
"params": {
"bodies": [
"Post"
]
}
}
]
}
@@ -0,0 +1,22 @@
{
"sources": [],
"what": "every face of a sphere r 10 (its smooth chain) offset 4 inward: the sphere r 6, closed, so a solid",
"source": "analytic",
"method": "the offset of a sphere by -4 along its outward normal is the concentric sphere r 6: A = 4 pi 36 = 452.389, V = 4/3 pi 216 = 904.779; its faces meet smoothly, so their offsets still share every edge and close up into a solid",
"body_count": 1,
"area_mm2": 452.3893421169302,
"volume_mm3": 904.7786842338603,
"boundary_edges": 0,
"bbox": {
"min": [
-6,
-6,
-6
],
"max": [
6,
6,
6
]
}
}
@@ -0,0 +1,34 @@
{
"commands": [
{
"command": "solid.sphere",
"params": {
"radius": 10,
"body_name": "Ball"
}
},
{
"command": "surface.offset",
"params": {
"faces": [
[
10,
0,
0
]
],
"chain": true,
"distance": -4,
"body_name": "Core"
}
},
{
"command": "solid.remove",
"params": {
"bodies": [
"Ball"
]
}
}
]
}
@@ -0,0 +1,8 @@
{
"sources": [],
"what": "an offset asked to fit within 20 mm: refused, the tolerance is 0.000001 to 10 mm",
"source": "invariants",
"method": "the fitting tolerance has a range; outside it the command refuses with that range rather than building",
"outcome": "refused",
"refusal": "the offset tolerance must be 0.000001…10 mm"
}
@@ -0,0 +1,27 @@
{
"commands": [
{
"command": "solid.box",
"params": {
"length": 20,
"width": 10,
"height": 10,
"body_name": "Box"
}
},
{
"command": "surface.offset",
"params": {
"faces": [
[
10,
5,
10
]
],
"distance": 5,
"tolerance": 20
}
}
]
}
@@ -0,0 +1,27 @@
{
"sources": [],
"what": "the top and the front of a box offset 1 together: across their sharp edge the offsets part, so one body of two separate faces",
"source": "analytic",
"method": "top (z = 10, 20 x 10) to z = 11, front (y = 0, 20 x 10) to y = -1: the two planes no longer meet (their edges are 1.41 apart), so two open shells of 4 free edges each; area 200 + 200",
"valid": false,
"body_count": 1,
"shells": 2,
"faces": 2,
"boundary_edges": 8,
"area_mm2": 400.0,
"surface_types": [
"plane"
],
"bbox": {
"min": [
0,
-1,
0
],
"max": [
20,
10,
11
]
}
}
@@ -0,0 +1,40 @@
{
"commands": [
{
"command": "solid.box",
"params": {
"length": 20,
"width": 10,
"height": 10,
"body_name": "Box"
}
},
{
"command": "surface.offset",
"params": {
"faces": [
[
10,
5,
10
],
[
10,
0,
5
]
],
"distance": 1,
"body_name": "Pair"
}
},
{
"command": "solid.remove",
"params": {
"bodies": [
"Box"
]
}
}
]
}
+3
View File
@@ -0,0 +1,3 @@
[
{"source": "surface.fillet", "why": "planned (#769): registered, not built yet; no geometry to grade until it is"}
]