* solidcore-record: a face's surface may be a blend by its definition
`Surface::Blend { definition }`: a procedural blend's definition S, or a round of an exact
class's (`brep::blend::BlendDefinition`), with the face's `nurbs` its net B, stored once there.
The records are the kernel's plain data, mirrored here as `eval::geometry` mirrors the record's
surfaces: `BlendDefinitionRecord` and the records it holds, `ProceduralBlendRecord` for a support
that is itself a blend (no face, so it keeps its net), and `ExactRoundDefinition`, the kernel's
`ExactRoundRecord` without the net's form. Validation is structural, as the kernel's records':
every curve and surface, finite numbers, nesting at most three definitions deep, the items
counted against the record's budget, and a blend label needs its NURBS form.
Tests: a blend face validates and round-trips number for number, and is refused without its
net, nested too deep or with a number not finite; a reader whose surface kinds predate the label
refuses it by name ("unknown variant `blend`") rather than misreading it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DtL5M2MmcDrkcWhQeHS2K8
* solidcore: a face's blend definition read off for its record, and given back certified
`convert::face_definition` reads the definition a face's surface carries, without its net (the
face's surface is the net): a procedural blend's, or a round of an exact class's whose net has
no analytic label of its own (a surface of revolution, the rational classes' B-spline). A round
on an analytic surface or a ball patch keeps that label in a record, as before, and a definition
whose supports a record cannot hold goes as its net. `FaceDefinition` and
`ExactRoundDefinition` (an `ExactRoundRecord` without its net's form) are the plain data.
`convert::adopt_definitions` gives faces their definitions back: the net is the face's NURBS
surface as built, the definition is made from the record exactly and the net certified against
it again before the face's surface becomes the blend. A net its definition does not certify
refuses the face with the new `SolidError::DefinitionNotCertified { face, eps }`, never kept with
a larger bound; a cancel passes through.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DtL5M2MmcDrkcWhQeHS2K8
* solidcore-convert: a blend face's definition crosses between the record and the kernel
`blend::blend` and `blend::record_blend` copy a face's blend definition field by field between
the record's `BlendDefinition` and the kernel's `FaceDefinition`. A body written as its record
(`to_record`) writes a blend face as its definition with its net as the NURBS form, wherever the
kernel reads one off it; a record built as the kernel's body (`to_kernel`) builds a blend face on
its net, then gives it its definition back, rebuilt and the net certified again
(`adopt_definitions`), refusing the record for a net its definition does not certify. The
record's blend label has no kernel surface of its own: `surface` refuses it by name, as the
curves and surfaces holding one do.
Test: a procedural definition with a nested blend support, and an exact round's, cross both ways
unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DtL5M2MmcDrkcWhQeHS2K8
* solidcore-tess, solidcore-step: a blend face is meshed and written as its net
A blend label is the face's definition; its geometry is its NURBS form. The mesher names the kind
"blend" in its errors, and the STEP writer writes the face as its NURBS form, pcurves and all, as
it writes a ball patch: STEP has no entity for the definition.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DtL5M2MmcDrkcWhQeHS2K8
* solidcore-ops: a blend face's geometry is its form
A blend label is the face's definition, with no surface or normal of its own: the record's
geometry check reads such a face by its form (its definition is checked structurally and
certified by the kernel when the face is built), and `label::form_turns` says its form turns
nothing, as for a face with no form.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DtL5M2MmcDrkcWhQeHS2K8
* solidcore-ops: a body's blend faces between its record and the kernel
`blend_definition::record_definition` reads a face's definition off its surface as a record keeps
it, and `blend_definition::adopt` gives the faces of a body built from a record their definitions
back, rebuilt and their nets certified again: the modelling layer's calls for a client that
builds bodies its own way (through flat arrays) and needs its blend faces to cross with their
definitions, through the kernel and solidcore-convert.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DtL5M2MmcDrkcWhQeHS2K8
* smlib: a body's blend faces cross to and from its record with their definitions
The flat arrays still carry a blend face as its net alone; the definition crosses beside them
through the modelling layer (`solidcore_ops::blend_definition`). Exporting a body
(`to_native_report`, `to_native_ordered`) writes each face whose surface carries a definition a
record keeps (a procedural blend, a round of an exact class whose net has no analytic label) as
`Surface::Blend` with its net as its form. Importing a record (`from_record`,
`from_record_keeping_splines`) builds a blend face on its net, then gives it its definition
back, rebuilt and the net certified again; a net its definition does not certify refuses the
body (`DefinitionNotCertified`), and so does a body the build gave another number of faces. The
record → arrays mapping sends a blend face as its NURBS form, and record SSI refuses a blend
label as it refuses a ball patch's.
Tests: the round on a bore's rim is written by its definition with its net, survives save and
reopen number for number, comes back from a rebuild with the same definitions, and the rebuilt
body takes a shell.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DtL5M2MmcDrkcWhQeHS2K8
* geom, kernel, cli: a blend face is its definition on its net
A blend label is the face's definition; its geometry is its NURBS form.
- The record's rigid motion (`placement::moved`) refuses a record with one (`None`): moving a
definition's supports, leader and seeds is the kernel's, and `transform_matrix` then moves the
body through the kernel's transform, which moves the definition with its net. The affine map
(`mapped`) refuses it as it refuses every labelled face.
- Hidden lines draw a blend face as its net: the definition is dropped there, for the drawing
only (`hlr::shifted`).
- The record evaluator, iso curves, the face kinds (freeform) and the offset's analytic surfaces
(none) read it as its form, as a ball patch's label is read.
- The rig names it `blend` in `surface_types` (tests/kernel-cases/README.md).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DtL5M2MmcDrkcWhQeHS2K8
* Native B-rep: the blend face, and its design-format note
docs/native-brep.md gains the `blend` {definition} surface row and its paragraph: the face's
`nurbs` is its net, stored once; the kernel builds the face on its net and certifies it against
the definition before it is an operand. A pending design-format change says what an older design
and the last release do with it: as the ball patch before it, no SCBREP bump (an older reader
refuses the unknown kind by name, a model-cache miss; solidcore-record's test shows it).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DtL5M2MmcDrkcWhQeHS2K8
* smlib, solidcore-ops: a body's blend definitions through body-level ops calls only
The application reaches a body's blend faces through the modelling layer alone, adding no kernel
path: `solidcore_ops::blend_definition::definitions(k, body)` walks the body's faces in their
order and reads each face's definition, and `adopt(k, body, &[(index, definition)])` gives faces
named by that index their definitions back, an index past the faces refused as
`Error::Argument`. A net its definition does not certify reaches the application as the new
`solidcore_ops::Error::DefinitionNotCertified { face, eps }`, the face by its index; other kernel
refusals as the operation's. crates/smlib/src/core/definition.rs now calls ops alone and imports
nothing from `solidcore::`.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DtL5M2MmcDrkcWhQeHS2K8
* Blend definition record: CI's compile error and clippy findings
- geom `placement::moved`: a face's or edge's NURBS form is moved by a match (the motion of a
form is an `Option`, so `map(..).transpose()` was the wrong tool).
- The definition enums box their variants (`brep::blend::BlendDefinition` in the record,
`FaceDefinition` in the kernel): the definitions are large and of different sizes.
- solidcore-record `brep::blend`: the weight checks are one `if let` chain each.
- solidcore `procedural_blend::convert`: an unused operation name goes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DtL5M2MmcDrkcWhQeHS2K8
* Kernel cases: a blend face is a blend, its net's degrees read on its NURBS form
A round of the exact class is now recorded as its definition
(Surface::Blend) on its net, so the `surface_types` check names it
`blend`, and the `splines` check reads the degrees on the face's NURBS
form (its net). The eight cases that named these rounds `bspline` now
name them `blend`; their spline degrees are unchanged:
cov-fillet-exact-coaxial-cones-r2, -cone-cylinder-sharing-a-ball-r1,
-cylinders-touching-at-a-point-r1, -oblique-plane-cone-rim-r1-5,
-oblique-plane-cylinder-rim-r2, -sphere-coaxial-cone-r3,
-sphere-plane-rim-r2 and cov-fillet-tee-pipe-r8-into-pipe-r20-r3
(surface_types: bspline -> blend).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DtL5M2MmcDrkcWhQeHS2K8
* solidcore: a variable round's leader is its edge's curve cut to its stations
An edge on an unbounded line (a box's) is held as a two-point NURB a
million times the part's size (control points at y = +-24691340), and a
variable round took that curve whole as its leader. Its control points
round at an ulp of 2.5e7, about 1e-9 mm, and recertification, which
takes the round in its own frame (the blend translated by its centre),
carried that rounding into the leader: leader(1) moved from y = 20 to
19.999999998137355 (2^-29), the re-solved feet moved with it, and the
equal-radius round of blend_variable::variable_radius_on_a_box_edge,
whose net is 7.36e-11 from its definition, was refused against its
certified 2.2e-10 (delta 1.86e-9). Recertified unmoved it passes, so the
bound was right and the leader was not.
The leader is now the edge's curve cut to the stations' parameters
(within its natural interval), its control points of the part's size;
the round is built on the cut curve, so build and recertificate solve
the same stations. Measured: the equal-radius round recertifies at
7.36e-11 against 2.2e-10 after the record round trip; the 2 -> 5 round's
certified bound is 3.16e-6 (it was 5.06e-7 on the uncut leader) and
recertifies at 3.159e-6.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* solidcore: a derived cylinder's and cone's numbers come from the form's rows
The NURBS form is a blend definition's record's one truth for its
supports, and AnalyticSupport::derive must give back the support the
form is. It took a cylinder's and a cone's numbers from the recognisers
(IsNurbSurfaceCone), whose chained computation tilts the axis by about
1e-13 (an ulp of the origin's size): the raked wedge's cylinder supports
(|origin| ~ 70) came back tilted 1.3e-13 and build refused them as not
the form's parametrisation, so the body read back from its record was
refused (boolean_regression_tests::box_intersect_splits_the_end_off_a_
raked_wedge_after_its_end_rounds).
derive now reads them off the form's control structure. Each circle row
is a rational quadratic arc p0 p1 p2 whose tangents meet at p1, so its
centre is c = p1 + (m - p1) |p0 - p1|^2 / |m - p1|^2 (m the chord's
midpoint) and its radius the ends' distance from it. The axis is the
line through the first and last rows' centres, the origin its point
nearest the recogniser's, the x axis the recogniser's made square to
it; a cylinder is a cylinder by its kind, a cone's angle the radii's
change along the axis. The recogniser gives only its conventions (frame
turn, sweep, generator range, swap); build and evaluates_as judge the
result unchanged. Sphere and torus still derive through the recognisers
(a follow-up).
Tests (derive_revolved_supports): the raked wedge's ten cylinder support
forms, form -> derive -> build, within 64 ulps; a tilted quarter cone
at |origin| 1e3 and 1e5, and a tilted quarter cylinder at 1e3.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* solidcore: derive's positive checks in the crate's NaN-refusing form
The circle row's and the axis's positive checks were `!(x > 0.0)`, which
solidcore's clippy refuses (neg_cmp_op_on_partial_ord). They are
`x.partial_cmp(&0.0) != Some(Ordering::Greater)` now, as the file writes
its other NaN-refusing comparisons: a NaN still refuses.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* solidcore: a support is built as its own kind, the recogniser giving conventions only
AnalyticSupport::build's on_form took the kind from the recogniser and
refused a support of another: a cylinder about 1e5 from the world origin
(a tilted quarter cylinder, radius 1.5, at (6e4, -4.8e4, 6.4e4)) is read
by Cone::is_nurb_surface_cone as a cone of semi-angle -1.1e-10 degrees,
so its derived cylinder was refused before evaluates_as was asked. As
derive now does, on_form takes only the recognised surface's
conventions (frame turn, sweep, generator range, swap) and builds the
support's own kind with its own numbers: a cylinder sets its angle to 0
and is a cylinder. evaluates_as judges the result, unchanged. The
recogniser is untouched (its misread is a follow-up issue). The tilted
cylinder at 1e5 is back in derive_revolved_supports, which passes, with
wr698 and the raked-wedge regression.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* solidcore: derive turns its axis with *= (clippy's assign_op_pattern)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* solidcore: derive reads a cone's apex row as its axis point of radius 0
AnalyticSupport::revolved_from_rows took each row of a cone's form as a
circle and dropped a row collapsed to one point: its arc has no chord, so
circle() returned None. A cone up to its tip has two rows, the base circle
and the apex, so one circle was left, the axis had no length, and derive
refused the support as not of its kind. The fillet rig's
cov-fillet-exact-cone-cylinder-sharing-a-ball-r1 (a cone to its apex,
rounded where a cylinder meets it) built its round, then the body read
back from its record was refused (SM_ERR_INVALID_INPUT on Hill).
A row whose control points are one point, apart only by the rounding of
dividing each homogeneous point by its weight (gamma_4 of its size, as
euclidean_net makes them), is now the axis's point of radius 0: the
apex. The axis runs from the base circle's centre to it, and the angle
and base radius follow as for any two rows.
Checked locally (owner-approved for this PR): the case fails at
449e72c1f3 with the reported refusal and passes with this change. Test:
derive_revolved_supports' an_off_axis_tilted_cone_up_to_its_apex_round_trips,
a tilted quarter cone up to its apex (apex row last and first) at about
1e3 and 1e5 from the origin, form -> derive -> build within 64 ulps.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* solidcore: a re-certificate stops its levels on the certified bound's least value
A round's span over its target is halved at construction, and each half is
certified from its parent's finest level (gn_certify_span_reusing's inherit
path). Recertification takes each half fresh from level 0, to the target,
and close_level's least-value stop ended it after level 0: that least value
took the uv curves' term in the fork's form, which steers the levels but is
not under the bound certified in each parameter's own form (#2690). On
cov-junction-tee-offset-round the half [14.92, 15.31] stopped at 4.27e-6
(fork 5.39e-6) against its target 3.97e-6; level 1, the pieces construction
used, certifies it at 3.83e-6. The oblique unequal tee's span 103 likewise
(4.48e-6 -> 2.68e-7).
To the target (SpanReuse::to_target, recertification's), the stop now takes
each parameter's own least uv term (LevelAcc::uv4_lo, folded as the fork's
is), a lower bound on every level's certified bound. Construction's levels
are steered as before. No tolerance or expected value changes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* solidcore: derive takes a cylinder's axis from its columns
AnalyticSupport::revolved_from_rows took every axis as the line through
the first and last rows' circumcentres. A circumcentre of a shallow arc
carries its control points' rounding amplified by the radius over the
sagitta: on the shelled case of the engine's
the_pleats_join_at_every_wall_from_2_4_to_2_6_mm, a 20 mm arc of radius
77.6 mm extruded along Z got an axis tilted by 1.3e-14. build refused it
as not the form's parametrisation, face_surface kept the face as plain
NURBS, and FilletFrontInner and FilletRearInner failed with
SM_ERR_INVALID_INPUT at a 2.4 mm wall.
A cylinder's generators are its form's columns, and each column's first
and last control points differ by the stored extrusion, exactly for an
extruded circle. The axis is now the sum of those differences, through
the first row's centre. A cone's generators meet at its apex, so its
axis stays its centres' line.
Measured locally (the pleat test, approved narrow run): old (recognised)
and new numbers agree within 3.2e-12 in origin and radius on all 31
distinct cylinder forms; the failing form's old axis is exactly Z and the
centres' axis is off by 1.27e-14; with this change all 288 derives build
and the test passes. Test: derive_revolved_supports'
the_pleated_cases_cylinder_supports_round_trip, the 31 forms as derive
met them (data/pleated_case_supports.jsonl), form -> derive -> build
within 64 ulps; it fails with the centres' axis and passes with this.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: jyviko <2501648+jyviko@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
21 KiB
Kernel regression rig
Every geometry case the old kernel (truck) got wrong, recorded in an issue, a pull request or a
crash report, is a use case here with its own right answer. The kernel, solidcore, is held to all
of them. cargo xtask kernel-cases runs them and writes
docs/kernel-cases.md.
This folder takes no new cases. New kernel cases go to solidcore's corpus,
solidcore/crates/solidcore/tests/kernel_cases/<area>/ in this repository (the vendored
kernel, mirrored to tangency-labs/solidcore; see
Exporting to solidcore). CASES lists every case here and only
shrinks: cargo xtask lists fails on a case it does not list, and on a listed case that is gone.
cargo xtask kernel-cases # every case
cargo xtask kernel-cases --cli target/release/solidcraft-cli --out /tmp/run # with a CLI already built
cargo run -p solidcraft-cli -- kernel-cases tests/kernel-cases/booleans/<id> # one case
The run's output folder (--out, default target/kernel-cases)
gets results.json, every case's full result, and dumps/<area>-<case>/: the inputs of each
kernel call a case failed in, for replay (the CLI's --dumps=DIR, which the rig always passes).
--validation MODE runs the cases' features under that mode of the kernel's check
(diagnostics.validation): each_feature (the default: the bodies each feature keeps are checked
once), each_step (after every sweep, boolean and blend, as before) or off; the
KNOWN_FAILING lists are each_feature's.
Rules
- The answer never comes from a kernel. Not from truck's output, nor any commercial kernel's. It comes from analysis or a hand calculation, or else from the invariants below.
- No user designs. A crash report or a repro design becomes a small synthetic recipe that shows the same geometry. Check for private content before committing anything.
- No workaround on SolidCraft's side. A case that fails stays failing until the kernel is
fixed. File SMLib defects as issues here with the
area:kernellabel; the fix goes intosolidcore, and the mirror workflow pushes it to tangency-labs/solidcore.
Layout
One directory per area, one per case: tests/kernel-cases/<area>/<id>/.
| Area | Owner |
|---|---|
booleans (combine, cuts, holes, patterns, split, mirror, draft) |
booleans/shell/import/crash rig |
shell (shell, offset, thicken) |
booleans/shell/import/crash rig |
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 |
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 |
construction (construction planes, axes and points: construct.*) |
tool coverage rig |
inspect (the inspect commands that return numbers) |
tool coverage rig |
history (the timeline's suppress, reorder, delete, roll back, edit and folders; configuration rows, columns and rules; parameters that drive geometry) |
tool coverage rig |
assemblies (joints and couplings, motion studies, contact, components and occurrences, interference and clearance between placed parts: placements and answers worked out by hand) |
rig coverage |
Each area also has its gate files: KNOWN_FAILING and PATHS (see the gate).
<id> is <source number>-<short-name>, such as 1894-prism-cut-touching-walls. A case from a report with no issue or pull request (a failure the maintainers met in the app) is
report-<short-name> and lists no sources; its what says where it was reported. A case that
covers a command with no source at all is cov-<short-name>.
Each case has:
recipe.json: an engine command script ({"commands": [...]}, assolidcraft-cli runtakes) or an oracle recipe (tests/oracle/*/recipe.json).$CASEin a string is the case directory, for an input file such as$CASE/input.step(written by hand from the STEP or IGES specification, never a user's file), and$OUTa scratch directory outside the repository for files the case writes (an export).expected.json: the right answer, graded bysolidcraft-cli kernel-cases.
A script may start with "base": "<case directory>" (relative to its own directory): that case's
commands, and its own base's, run first. The ladder's cases build on each other this way.
Each area also has skipped.json (or several, skipped-<part>.json, where an area is triaged in
parts): every issue and PR looked at that did not become a case, with one line each saying why.
[{"source": "tangency-labs/solidcraft#123", "why": "UI only: the selection highlight, no geometry"}]
sources.json says what kind each source number is (an open or closed issue, a merged PR, one of
the frozen truck-geometry PRs, a kernel-paused PR), for the report's coverage by source.
expected.json
{
"sources": ["tangency-labs/solidcraft#1894", "tangency-labs/solidcraft#1629"],
"what": "a prism cut whose walls only touch the target's walls",
"source": "analytic",
"method": "box 40x30x20 minus the prism 10x30x5 flush with the side: V = 24000 - 1500",
"body_count": 1,
"volume_mm3": 22500.0,
"area_mm2": 5200.0,
"faces": 8
}
| Field | Meaning |
|---|---|
sources |
The issues, PRs or crash reports the case comes from (owner/repo#N). |
what |
One line: the geometry that went wrong. |
source |
analytic (numbers worked out by hand; method gives the formulas) or invariants (no number is known: only the outcome, validity and counts are graded). |
method |
How the numbers were derived, or which invariants hold and why. |
outcome |
solid (default: the script succeeds), refused (the right answer is a clean error, never a panic or a hang), or solid_or_refused (either a clean error or a result graded as usual, where the geometry has no single manifold answer, such as two bodies touching along a curve only). |
refusal |
Required with outcome: refused (optional with solid_or_refused): a part of the refusal's message, the reason it must be refused for. A refusal for another reason (an SMLib status, a different check) fails. |
valid |
Default true: every body is one closed shell (or shells in all) that passes the record's structure and geometry checks and SMLib's check(All). false only where the result is legitimately not a solid (a sheet). Where SMLib can't take the record yet (a surface kind its conversion lacks), its check gives no verdict and is not graded. |
shells |
The total number of shells, where it isn't one per body: a body with a sealed void (2), several lumps kept in one body. |
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). |
volume_mm3, area_mm2 as [min, max] |
An inclusive range, where analysis only bounds the measure: a cut removes at most its tool's volume, a join adds at most the tool's. method derives both ends. |
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, ball_patch (a corner's patch on a sphere, its NURBS net), bspline (a B-spline or NURBS surface), revolution (a revolved curve) or blend (a round by its definition, its NURBS net its geometry). 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]. |
flat_pattern |
A sheet metal case's flat pattern, from the script's last sheet.flat_pattern result: size_mm ([w, h] of the outline), area_mm2 (the outline's area less the cut-outs', relative tolerance as volume) and bend_lines (each [[x, y], [x, y]], a bend's centre line in flat coordinates, matched one for one, either way round, and as many as the result has), positions within the box-diagonal tolerance. |
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, pointer?, json, rel?, abs_mm?}: the answer of the stepth command (from 1, base commands included), or its part at the JSON pointer (RFC 6901), matched as files matches JSON: a whole number exactly (a count), another within rel (the case's relative tolerance unless given) or abs_mm, lists item by item. |
construction |
{name: spec}, a spec being {point: [x,y,z]}, {axis: {through, dir}} or {plane: {through, normal}}: each named construction point, axis or plane as the timeline evaluated it. A point within the position tolerance (tolerance.abs_mm, else 1e-3 of the box diagonal, of 1 mm where there is no body); an axis whose direction is parallel to dir (either way, sine of the angle at most 1e-6) and whose line passes within it of through; a plane likewise with normal. An axis or plane is graded as the line or plane it is, not by the origin it keeps. |
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
Every area is a regression suite: each case is one small case of one issue, with its exact
answer. .github/workflows/kernel-rig.yml runs them, and the ladder, on pull requests (not
drafts). A plan job (scripts/ci/kernel_rig_plan.py) picks the areas from the changed files, one
job builds solidcraft-cli (release) and the rig's runner, and a matrix of shards, one per
selected area plus the ladder, runs them in parallel; the kernel-rig check passes when every
selected shard does (and when none was selected). Pushes to main and develop, merge groups, the
release gate (release-gate.yml) and a run by hand run every area. Each area's shard runs:
cargo xtask kernel-cases --area <area> --cli target/release/solidcraft-cli \
--known-failing tests/kernel-cases/<area>/KNOWN_FAILING
<area>/KNOWN_FAILING lists the cases of that area that fail today, sorted, one a line with its
cause (<case> <why it fails today>; # starts a comment; the header names the baseline: the
SolidCore commit it was taken from). The gate fails on a regression, a
case that fails (or crashes, hangs, or is not supported yet) and is not listed, and on a
fixed case, one listed that passes. With --known-failing the run writes no report;
docs/kernel-cases.md stays the full run's.
Updating a list. The pull request that changes an outcome changes the list in the same pull request, so the list only shrinks and never goes stale:
- a fix (yours, or a SolidCore update): remove the line of each case the shard reports as
FIXED; - a case here that starts failing for a reason the kernel owns (a SolidCore update): add its
line, sorted, with the cause from its result (
results.jsonin the shard'skernel-rig-results-<area>artifact, or a local run); - a regression: fix it in the same pull request.
To check a list locally before pushing: cargo xtask kernel-cases --area <area> --cli target/release/solidcraft-cli --known-failing tests/kernel-cases/<area>/KNOWN_FAILING.
Which areas a pull request runs
<area>/PATHS: the source files whose change reruns that area, one git pathspec glob a line (*within a folder,**across folders;#starts a comment): the engine command modules, doc evaluation and kernel files its commands go through. Only the area's own files: never a filePATHS-ALLcovers, nor a whole crate (the self-test fails on either).PATHS-ALL: the shared core, whose change reruns every area: the kernel's wrapper (crates/smlib/**),crates/geom/**, the kernel's core (bodies, extrude and revolve, booleans, the kernel bridge, tessellation, what the grader measures), timeline evaluation, the solid feature commands, the grader, the runner and the workflow.- A change under
<area>/(a case, itsKNOWN_FAILING, itsPATHS) reruns that area. - The ladder spans every operation: it runs whenever any area does, and on a change under
ladder/.
The workflow's trigger paths are whole folders that hold every PATHS glob (a self-test,
scripts/ci/test_kernel_rig_plan.py, checks it), so a pull request that touches none of them
starts no job at all. To see what a change would run:
git diff --name-only -z origin/develop... | python3 scripts/ci/kernel_rig_plan.py --paths -.
The plan finds the areas from the folders, so the workflow needs no edit when an area's PATHS
changes (unless a glob lies outside the trigger paths, which the self-test reports). No new area
joins: new kernel cases go to solidcore's corpus.
Every command has a case
cargo xtask kernel-cases --coverage --cli target/release/solidcraft-cli (run in the gate's
build job) lists every registry command (solidcraft-cli commands, planned ones aside) that no
recipe.json here runs, and fails unless EXEMPT lists it: one line a command, <id> <why it has no case>, each section sorted. Lines before [to cover] have no geometry to grade (the
view, rendering, appearance, files, component and parameter bookkeeping, ...). Lines after it
make geometry and wait for a case: that section only shrinks. The check also fails on a listed
command that a case now uses (remove its line in the same pull request) or that the registry no
longer has. A new command gets an EXEMPT line before [to cover] whose reason names where it
is tested: its kernel case in solidcore's corpus, or its test in its area's test file. The build
job runs for this check on any change to crates/engine/src/cmd/, a recipe or EXEMPT, even when no area does.
The complexity ladder
ladder/ orders kernel operations by complexity instead of by where they went wrong: tiers from
primitives through degenerate booleans, analytic and spline profiles, holes and taps, threads,
fillets, chamfers, shells, patterns, whole parts, and sheet metal. Later tiers build on earlier tiers' shapes (a
running part grows one tier at a time through base), so the last case carries everything.
ladder/<NN-tier>/<NNN-name>/: cases in the format above, each a test with its own answer.ladder/ladder.json: the tiers in order and, per tier, its cases in order (paths undertests/kernel-cases/, so a tier may also list a rig case from another area).ladder/golden.json: per case the status, the commands' time and what was made (counts, volume, area). Runs compare with it and flag slowdowns (over twice the time and 0.25 s more) and changed counts or statuses. It is re-recorded only with a reason.tools/kernel-ladder/cases.pywrites the recipes and answers (standard-library Python; eachmethodgives the derivation).
cargo xtask kernel-cases --ladder --cli target/release/solidcraft-cli
cargo xtask kernel-cases --ladder --cli target/release/solidcraft-cli --gate
cargo xtask kernel-cases --ladder --cli target/release/solidcraft-cli --record "why"
The ladder needs no known-failing list: the golden is the reference. Its shard in the kernel rig
gate runs with --gate, which fails on any case whose status or counts differ from the golden,
better or worse, and on a case with no golden; times are not compared there (they are the
machine's). A pull request that changes a ladder result re-records the golden with
--record "why the golden changes" and commits golden.json and
ladder.json with it.
Ladder runs measure one case at a time (unless --jobs) and keep their output in
target/kernel-ladder (--out); --record rewrites the golden and sorts each tier by time,
fastest first; commit both files. The report, docs/kernel-ladder.md, gives per tier the counts
per status and the frontier: the first case in ladder order that fails.
Results
Per case: pass; fail with the numbers; not supported yet (the kernel says
so, or the case names a missing capability); crash (a panic caught by the guards, or the process died); hang (no result before
the timeout).
Exporting to solidcore
solidcore holds the kernel cases as a corpus of kernel operations,
solidcore/crates/solidcore/tests/kernel_cases/<area>/<case>.json, described in that
folder's README.
cargo xtask kernel-cases [--ladder] [--area AREA] --export DIR writes each case here as such
an entry: the kernel operations that made its result, their operand records
(<case>.bodies.json.gz), the fix set and the expected values, with DIR/manifest.json listing
every case that is not expressible as kernel input and why.
A new kernel case: draft it here as <area>/<id>/recipe.json and expected.json (the format
above, answers in closed form or exact), export it with --area <area> --export DIR, add
DIR/<area>/<id>.json and its .bodies.json.gz under solidcore, and delete the draft
(it is not in CASES, so it cannot be committed here). An entry can also be written by hand as
solidcore's README describes. The mirror workflow pushes the change to tangency-labs/solidcore. A
case about a command, the UI or a product rule stays in SolidCraft as a test in its area's
existing test file.