Kernel rig: plastic cases, bosses, lips and grooves, rests, a snap fit edge case and the plastic rules, each with its answer

A new plastic area with its KNOWN_FAILING (empty: every case passes) and the
sources whose change reruns it; the README lists the area and the new files and
results fields.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
jyviko
2026-10-29 16:59:59 -04:00
committed by Irakli Kourtis
parent 235645db50
commit 5ba60bd9d3
39 changed files with 932 additions and 0 deletions
+3
View File
@@ -39,6 +39,7 @@ One directory per area, one per case: `tests/kernel-cases/<area>/<id>/`.
| `continuity` (designed seams of known order, G0 to G4, that prove the continuity measurement itself) | history rig |
| `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 |
Each area also has its gate files: `KNOWN_FAILING` and `PATHS` (see [the gate](#the-kernel-rig-gate)).
@@ -99,6 +100,8 @@ the frozen truck-geometry PRs, a `kernel-paused` PR), for the report's coverage
| `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`. |
| `files` | The files the case wrote to `$OUT`, each `{path, json \| csv \| dxf}`, matched as a part of what is there: every key the case gives, an array item by item and as long, a whole number exactly, any other number within `rel`. `json`: the file's JSON; `csv`: its rows, as objects keyed by the header row; `dxf`: per layer of its ENTITIES section, how many of each entity (`{"CUT": {"CIRCLE": 1, "ELLIPSE": 0, "length_mm": 31.416}}`; a count may be a range `[min, max]`) and `length_mm`, the total length of its lines, arcs, circles, ellipses and polylines. |
| `results` | The commands' own answers, each `{step, json}`: the answer of the `step`th command (from 1) matched as `files` matches JSON (a flat pattern's bends, a rule's values). |
| `missing` | A capability SolidCraft has no command or parameter for yet (`"solid.thread has no start count"`): the case is reported **not supported yet** without running, so the gap stays visible. |
## The kernel rig gate
+4
View File
@@ -0,0 +1,4 @@
# The plastic cases that fail today, one a line: `<case> <why it fails today>`. Sorted. A gate on
# this area fails on a case that fails and is not listed here, and on a listed case that passes:
# a fix removes its line in the same pull request.
# Baseline: rig-cases-sheet-plastic 45f69c0e5 with SMLib 60b1c6a, aarch64 Linux: every case passes.
+20
View File
@@ -0,0 +1,20 @@
# The sources whose change must rerun the plastic cases (git pathspec globs, one a line).
# The commands and their rules.
crates/engine/src/cmd/plastic.rs
crates/doc/src/plastic.rs
# The features' evaluation (plastic_eval and its helpers) and the join and cut they apply.
crates/doc/src/eval_more.rs
crates/doc/src/eval.rs
# The kernel operations they build with: extrude, tapered extrude, the lip's raised band and the
# groove's rim band, booleans (ops) and the shell the lip cases
# start from, and SMLib underneath.
crates/kernel/src/build.rs
crates/kernel/src/lip.rs
crates/kernel/src/ops.rs
crates/kernel/src/boolean_record.rs
crates/kernel/src/native.rs
crates/kernel/src/shell_groups.rs
crates/smlib/**
# The cases and the rig itself.
tests/kernel-cases/plastic/**
apps/solidcraft-cli/src/kernel_cases.rs
@@ -0,0 +1,8 @@
{
"sources": [],
"what": "plastic.boss 2 across and 12 high with a 5 degree draft: the taper closes the post before its top",
"source": "invariants",
"method": "top radius r0 - h tan(draft) = 1 - 12 tan 5 deg = -0.049864 < 0: no post, a clean refusal",
"outcome": "refused",
"refusal": "the draft closes the boss before its top"
}
@@ -0,0 +1,26 @@
{
"commands": [
{
"command": "solid.box",
"params": {
"length": 60,
"width": 60,
"height": 3,
"body_name": "Plate"
}
},
{
"command": "plastic.boss",
"params": {
"position": [
30,
30,
3
],
"diameter": 2,
"height": 12,
"draft": "5 deg"
}
}
]
}
@@ -0,0 +1,21 @@
{
"sources": [],
"what": "plastic.boss 10 across and 12 high with a 4 mm hole, on a 60 x 60 x 3 plate with no plastic rule (no draft)",
"source": "analytic",
"method": "no rule, no draft: a cylinder r 5, h 12 standing on the face, its hole r 2 from the top down to the face (hole_depth defaults to the height). V = 60*60*3 + pi*5^2*12 - pi*2^2*12 = 11591.681349. Faces: the plate's 6 (the top with the post's foot in it), the post's side, its top ring, the hole's wall and its floor (a disc on the plate's plane, inside the post, apart from the plate's top)",
"body_count": 1,
"volume_mm3": 11591.68134870463,
"faces": 10,
"bbox": {
"min": [
0,
0,
0
],
"max": [
60,
60,
15
]
}
}
@@ -0,0 +1,26 @@
{
"commands": [
{
"command": "solid.box",
"params": {
"length": 60,
"width": 60,
"height": 3,
"body_name": "Plate"
}
},
{
"command": "plastic.boss",
"params": {
"position": [
30,
30,
3
],
"diameter": 10,
"height": 12,
"hole_diameter": 4
}
}
]
}
@@ -0,0 +1,20 @@
{
"sources": [],
"what": "plastic.boss 10 across and 12 high with 4 ribs 1.5 thick, 6 out from the wall, 2 below the top, on a 60 x 60 x 3 plate",
"source": "analytic",
"method": "each rib is a 1.5-wide strip from r0/2 = 2.5 to r0 + 6 = 11 from the axis, 12 - 2 = 10 high from the face; outside the post (r0 = 5) its section is t(r0 + l) - [(t/2)sqrt(r0^2 - t^2/4) + r0^2 asin(t/(2 r0))] = 9.028220693 (the strip less the circle's part of it). V = 60*60*3 + pi*5^2*12 + 4*9.028220693*10 = 12103.606624",
"body_count": 1,
"volume_mm3": 12103.606623800613,
"bbox": {
"min": [
0,
0,
0
],
"max": [
60,
60,
15
]
}
}
@@ -0,0 +1,29 @@
{
"commands": [
{
"command": "solid.box",
"params": {
"length": 60,
"width": 60,
"height": 3,
"body_name": "Plate"
}
},
{
"command": "plastic.boss",
"params": {
"position": [
30,
30,
3
],
"diameter": 10,
"height": 12,
"ribs": 4,
"rib_thickness": 1.5,
"rib_length": 6,
"rib_offset": 2
}
}
]
}
@@ -0,0 +1,20 @@
{
"sources": [],
"what": "plastic.lip type groove, 1 wide and 2 deep along the outside edge of the shelled box's rim, with the ABS (1.5mm) rule active and no gap given",
"source": "analytic",
"method": "a groove's gap defaults to the body's rule's clearance: ABS (1.5mm), 0.1 mm. The groove is the band along the outer edge (80 x 50) w + gap = 1.1 wide, cut h + gap = 2.1 deep: (80*50 - 77.8*47.8)*2.1 = 590.436000. V = 22112 - that = 21521.564000; the height stays 30",
"body_count": 1,
"volume_mm3": 21521.564,
"bbox": {
"min": [
0,
0,
0
],
"max": [
80,
50,
30
]
}
}
@@ -0,0 +1,47 @@
{
"commands": [
{
"command": "plastic.manage_rules",
"params": {
"name": "ABS (1.5mm)",
"active": true
}
},
{
"command": "solid.box",
"params": {
"length": 80,
"width": 50,
"height": 30,
"body_name": "Case"
}
},
{
"command": "solid.shell",
"params": {
"faces": [
[
40,
25,
30
]
],
"thickness": 2
}
},
{
"command": "plastic.lip",
"params": {
"face": [
1,
25,
30
],
"width": 1,
"height": 2,
"type": "groove",
"side": "outside"
}
}
]
}
@@ -0,0 +1,20 @@
{
"sources": [],
"what": "plastic.lip 1 wide and 2 high along the inside edge of the rim of an 80 x 50 x 30 box shelled 2 mm open at the top",
"source": "analytic",
"method": "shelled: 80*50*30 - 76*46*28 = 22112; the lip is the band along the rim's inner edge (76 x 46), 1 wide (to 78 x 48), raised 2: (78*48 - 76*46)*2 = 496. V = 22608; the top rises to 32",
"body_count": 1,
"volume_mm3": 22608.0,
"bbox": {
"min": [
0,
0,
0
],
"max": [
80,
50,
32
]
}
}
@@ -0,0 +1,38 @@
{
"commands": [
{
"command": "solid.box",
"params": {
"length": 80,
"width": 50,
"height": 30,
"body_name": "Case"
}
},
{
"command": "solid.shell",
"params": {
"faces": [
[
40,
25,
30
]
],
"thickness": 2
}
},
{
"command": "plastic.lip",
"params": {
"face": [
1,
25,
30
],
"width": 1,
"height": 2
}
}
]
}
@@ -0,0 +1,8 @@
{
"sources": [],
"what": "plastic.lip on the bottom face of the shelled box: a face with one edge, no rim",
"source": "invariants",
"method": "a lip runs round the opening of a rim face (an inner and an outer edge); the closed bottom has only its outer edge, so it is refused and says what to pick",
"outcome": "refused",
"refusal": "pick the rim face of a shelled body"
}
@@ -0,0 +1,38 @@
{
"commands": [
{
"command": "solid.box",
"params": {
"length": 80,
"width": 50,
"height": 30,
"body_name": "Case"
}
},
{
"command": "solid.shell",
"params": {
"faces": [
[
40,
25,
30
]
],
"thickness": 2
}
},
{
"command": "plastic.lip",
"params": {
"face": [
40,
25,
0
],
"width": 1,
"height": 2
}
}
]
}
@@ -0,0 +1,20 @@
{
"sources": [],
"what": "plastic.assign_rule gives the plate a 3 degree rule; a boss on it with no draft of its own takes it",
"source": "analytic",
"method": "the boss takes the draft of its body's rule: a cone frustum r 5 to 5 - 12 tan 3 deg = 4.371106649, 12 high, less its straight hole r 2, 12 deep (inside the top's radius). V = 60*60*3 + (pi*12/3)(5^2 + 5 r1 + r1^2) - pi*2^2*12 = 11478.107830",
"body_count": 1,
"volume_mm3": 11478.107830371639,
"bbox": {
"min": [
0,
0,
0
],
"max": [
60,
60,
15
]
}
}
@@ -0,0 +1,42 @@
{
"commands": [
{
"command": "solid.box",
"params": {
"length": 60,
"width": 60,
"height": 3,
"body_name": "Plate"
}
},
{
"command": "plastic.manage_rules",
"params": {
"name": "Housing",
"draft": "3 deg"
}
},
{
"command": "plastic.assign_rule",
"params": {
"bodies": [
"Plate"
],
"rule": "Housing"
}
},
{
"command": "plastic.boss",
"params": {
"position": [
30,
30,
3
],
"diameter": 10,
"height": 12,
"hole_diameter": 4
}
}
]
}
@@ -0,0 +1,8 @@
{
"sources": [],
"what": "the active rule drafts 2 degrees (ABS (1.5mm)), the plate's assigned rule 4 degrees: a round rest on the plate takes the assigned 4",
"source": "analytic",
"method": "a body's assigned rule comes before the active one: the rest (round, 10 across, 5 high, solid) is drafted 4 deg: V = 60*40*3 + (pi*5/3)(5^2 + 5 r1 + r1^2), r1 = 5 - 5 tan 4 deg, = 7565.878955 (with the active 2 deg it would be 7579.145355)",
"body_count": 1,
"volume_mm3": 7565.878954822472
}
@@ -0,0 +1,48 @@
{
"commands": [
{
"command": "solid.box",
"params": {
"length": 60,
"width": 40,
"height": 3,
"body_name": "Plate"
}
},
{
"command": "plastic.manage_rules",
"params": {
"name": "ABS (1.5mm)",
"active": true
}
},
{
"command": "plastic.manage_rules",
"params": {
"name": "Steep",
"draft": "4 deg"
}
},
{
"command": "plastic.assign_rule",
"params": {
"bodies": [
"Plate"
],
"rule": "Steep"
}
},
{
"command": "plastic.rest",
"params": {
"position": [
30,
20,
3
],
"width": 10,
"height": 5
}
}
]
}
@@ -0,0 +1,8 @@
{
"sources": [],
"what": "plastic.assign_rule with a rule name that is neither a library nor a design rule",
"source": "invariants",
"method": "only an existing rule can be assigned: a clean refusal that names the rule",
"outcome": "refused",
"refusal": "no plastic rule `Nope`"
}
@@ -0,0 +1,22 @@
{
"commands": [
{
"command": "solid.box",
"params": {
"length": 60,
"width": 40,
"height": 3,
"body_name": "Plate"
}
},
{
"command": "plastic.assign_rule",
"params": {
"bodies": [
"Plate"
],
"rule": "Nope"
}
}
]
}
@@ -0,0 +1,16 @@
{
"sources": [],
"what": "plastic.manage_rules makes an active 5 degree rule, then deletes it; a rest after that has no rule and no draft",
"source": "analytic",
"method": "deleting a design rule also clears it as the active rule, so the rest (16 x 10 x 3, no draft of its own) is straight: V = 60*40*3 + 16*10*3 = 7680",
"body_count": 1,
"volume_mm3": 7680.0,
"results": [
{
"step": 3,
"json": {
"deleted": "Steep"
}
}
]
}
@@ -0,0 +1,46 @@
{
"commands": [
{
"command": "solid.box",
"params": {
"length": 60,
"width": 40,
"height": 3,
"body_name": "Plate"
}
},
{
"command": "plastic.manage_rules",
"params": {
"name": "Steep",
"draft": "5 deg",
"active": true
}
},
{
"command": "plastic.manage_rules",
"params": {
"name": "Steep",
"delete": true
}
},
{
"command": "plastic.rest",
"params": {
"position": [
30,
20,
3
],
"width": 10,
"length": 16,
"along": [
1,
0,
0
],
"height": 3
}
}
]
}
@@ -0,0 +1,8 @@
{
"sources": [],
"what": "plastic.manage_rules asked to start a new rule from a rule that does not exist",
"source": "invariants",
"method": "a new rule copies the values of `from`; with no such rule there is nothing to copy, a clean refusal naming it",
"outcome": "refused",
"refusal": "no plastic rule `No Such Rule` to start from"
}
@@ -0,0 +1,11 @@
{
"commands": [
{
"command": "plastic.manage_rules",
"params": {
"name": "Mine",
"from": "No Such Rule"
}
}
]
}
@@ -0,0 +1,31 @@
{
"sources": [],
"what": "plastic.manage_rules makes a rule from the Polypropylene library rule with a 1 degree draft and makes it active; a round rest with no draft of its own takes it",
"source": "analytic",
"method": "the new rule starts from Polypropylene (PP) (3 mm): thickness 3, nominal radius 0.5*Thickness = 1.5, thickness variation 0.15*Thickness = 0.45, clearance 0.1, min/max 0.8/3.8, its draft set to 1 deg. A solid round rest 10 across and 5 high drafted 1 deg: V = 60*40*3 + (pi*5/3)(5^2 + 5 r1 + r1^2), r1 = 5 - 5 tan 1 deg, = 7585.884376",
"body_count": 1,
"volume_mm3": 7585.884376156436,
"results": [
{
"step": 2,
"json": {
"active": "PP draft 1",
"rule": {
"name": "PP draft 1",
"material": "Polypropylene",
"library": false,
"values": {
"thickness": 3.0,
"nominal_radius": 1.5,
"thickness_variation": 0.45,
"clearance": 0.1,
"draft_deg": 1.0,
"min_thickness": 0.8,
"max_thickness": 3.8,
"min_draft_deg": 0.5
}
}
}
}
]
}
@@ -0,0 +1,34 @@
{
"commands": [
{
"command": "solid.box",
"params": {
"length": 60,
"width": 40,
"height": 3,
"body_name": "Plate"
}
},
{
"command": "plastic.manage_rules",
"params": {
"name": "PP draft 1",
"from": "Polypropylene (PP) (3 mm)",
"draft": "1 deg",
"active": true
}
},
{
"command": "plastic.rest",
"params": {
"position": [
30,
20,
3
],
"width": 10,
"height": 5
}
}
]
}
@@ -0,0 +1,49 @@
{
"sources": [],
"what": "plastic.rules after a design rule is made and assigned to a body: the six library rules, then the design's own, the active one and the assignment",
"source": "analytic",
"method": "the list is the library as the design has it (6 rules, ABS (1.5mm) first) and then the design's own (Housing, from ABS with a 3 deg draft: thickness 1.5, nominal radius 0.75); no rule was made active; Plate is assigned Housing. The plate is unchanged: V = 60*40*3",
"body_count": 1,
"volume_mm3": 7200.0,
"results": [
{
"step": 4,
"json": {
"active": null,
"assigned": {
"Plate": "Housing"
},
"rules": [
{
"name": "ABS (1.5mm)",
"library": true
},
{
"library": true
},
{
"library": true
},
{
"library": true
},
{
"library": true
},
{
"library": true
},
{
"name": "Housing",
"library": false,
"values": {
"thickness": 1.5,
"nominal_radius": 0.75,
"draft_deg": 3.0
}
}
]
}
}
]
}
@@ -0,0 +1,33 @@
{
"commands": [
{
"command": "solid.box",
"params": {
"length": 60,
"width": 40,
"height": 3,
"body_name": "Plate"
}
},
{
"command": "plastic.manage_rules",
"params": {
"name": "Housing",
"draft": "3 deg"
}
},
{
"command": "plastic.assign_rule",
"params": {
"bodies": [
"Plate"
],
"rule": "Housing"
}
},
{
"command": "plastic.rules",
"params": {}
}
]
}
@@ -0,0 +1,21 @@
{
"sources": [],
"what": "plastic.rest, a solid rectangle 16 along x by 10 across and 3 high, on a 60 x 40 x 3 plate with no rule (no draft)",
"source": "analytic",
"method": "no rule, no draft: a 16 x 10 x 3 block joined on the face, wholly inside it. V = 60*40*3 + 16*10*3 = 7680. Faces: the plate's 6 and the rest's 4 sides and top",
"body_count": 1,
"volume_mm3": 7680.0,
"faces": 11,
"bbox": {
"min": [
0,
0,
0
],
"max": [
60,
40,
6
]
}
}
@@ -0,0 +1,31 @@
{
"commands": [
{
"command": "solid.box",
"params": {
"length": 60,
"width": 40,
"height": 3,
"body_name": "Plate"
}
},
{
"command": "plastic.rest",
"params": {
"position": [
30,
20,
3
],
"width": 10,
"length": 16,
"along": [
1,
0,
0
],
"height": 3
}
}
]
}
@@ -0,0 +1,20 @@
{
"sources": [],
"what": "plastic.rest, round, 12 across, 4 high, a hollow ring with a 1.5 mm wall, drafted 2 degrees, on a 60 x 40 x 3 plate",
"source": "analytic",
"method": "sized at the face; the draft takes s = h tan 2 deg = 0.139683078 off each side of the wall by the top (the outer radius 6 to 6 - s, the bore 4.5 to 4.5 + s). V = 60*40*3 + (pi h/3)(R0^2 + R0 R1 + R1^2) for the outer minus that of the bore = 7379.489589",
"body_count": 1,
"volume_mm3": 7379.489589250136,
"bbox": {
"min": [
0,
0,
0
],
"max": [
60,
40,
7
]
}
}
@@ -0,0 +1,27 @@
{
"commands": [
{
"command": "solid.box",
"params": {
"length": 60,
"width": 40,
"height": 3,
"body_name": "Plate"
}
},
{
"command": "plastic.rest",
"params": {
"position": [
30,
20,
3
],
"width": 12,
"height": 4,
"thickness": 1.5,
"draft": "2 deg"
}
}
]
}
@@ -0,0 +1,8 @@
{
"sources": [],
"what": "plastic.rest, round, 12 across with a 6 mm wall: the wall would fill it",
"source": "invariants",
"method": "a hollow rest's wall must be thinner than half its width (6 here): a 6 mm wall leaves no bore, a clean refusal",
"outcome": "refused",
"refusal": "the rest's wall must be thinner than half its width"
}
@@ -0,0 +1,26 @@
{
"commands": [
{
"command": "solid.box",
"params": {
"length": 60,
"width": 40,
"height": 3,
"body_name": "Plate"
}
},
{
"command": "plastic.rest",
"params": {
"position": [
30,
20,
3
],
"width": 12,
"height": 4,
"thickness": 6
}
}
]
}
@@ -0,0 +1,20 @@
{
"sources": [],
"what": "plastic.snap_fit 15 long, 2 thick, 6 wide with a 1 x 3 catch and its base reaching 2 below the face, joined to a 40 x 40 x 4 box",
"source": "analytic",
"method": "the arm's side profile is t x (l + base_height) plus the catch's triangle cd*cl/2; joined, the 2 mm below the face lies inside the box and adds nothing: V = 40*40*4 + (2*15 + 1*3/2)*6 = 6589.0. The arm stands from x 20 - 2 to the catch at 20 + 1 (hook +x), its top at z 4 + 15",
"body_count": 1,
"volume_mm3": 6589.0,
"bbox": {
"min": [
0,
0,
0
],
"max": [
40,
40,
19
]
}
}
@@ -0,0 +1,34 @@
{
"commands": [
{
"command": "solid.box",
"params": {
"length": 40,
"width": 40,
"height": 4,
"body_name": "Box"
}
},
{
"command": "plastic.snap_fit",
"params": {
"position": [
20,
20,
4
],
"hook": [
1,
0,
0
],
"length": 15,
"thickness": 2,
"width": 6,
"catch_depth": 1,
"catch_length": 3,
"base_height": 2
}
}
]
}
@@ -0,0 +1,8 @@
{
"sources": [],
"what": "plastic.snap_fit whose catch is as long as its arm (15)",
"source": "invariants",
"method": "the catch sits at the arm's end and must be shorter than it (catch_length < length); a catch the whole arm long leaves no arm: a clean refusal",
"outcome": "refused",
"refusal": "the snap fit's catch must be shorter than its arm"
}
@@ -0,0 +1,33 @@
{
"commands": [
{
"command": "solid.box",
"params": {
"length": 40,
"width": 40,
"height": 4,
"body_name": "Box"
}
},
{
"command": "plastic.snap_fit",
"params": {
"position": [
20,
20,
4
],
"hook": [
1,
0,
0
],
"length": 15,
"thickness": 2,
"width": 6,
"catch_depth": 1,
"catch_length": 15
}
}
]
}