The portable record's 4,000,000-item budget refused a real result: the join of a level-4 Menger sponge's level-3 sponges ended with "SMLib export: the mapped B-rep does not validate: portable B-rep: item budget". The budget guards records from outside the kernel, and a body the kernel built in memory from inputs already bounded where they came in isn't hostile. Brep::validate keeps the budget for records read from outside: native design caches (decode_native_body, behind its own 256 MiB, 1M-per-array and 20M-value caps) and STEP import. The new Brep::validate_built runs every other check, without the cap, on what the kernel builds: SMLib's export of an operation's result, a body's record (Body::from_record, after the decoder has checked a file's), a part of a validated record, the record handed back to SMLib, and the validity check. Its checks are linear in a record already in memory. SMLib's export bounded its counts by the same budget; a count is now bounded by SMLib's own longest array (flat::MAX_LEN, checked on entry), which keeps every offset worked out from the counts from overflowing, and the redundant topology-length cap goes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
23 KiB
Native B-rep, schema 4
This is SolidCraft's geometry data model in solidcraft_geom::brep. It has no
modelling-backend dependency. It is the body encoding of a design's model cache
(the container format); it is not a STEP
replacement for external interchange. The container reader, binary codec and
kernel adapters are separate layers. The truck adapter writes it exactly: a body
decoded by the producer that encoded it is that body, number for number.
The records follow SMLib's flat form, SmBrepData. It stores one record per
logical use: a kernel keeps two mated halves of each face use, loop use and edge
use at run time, and it rebuilds them from the one record. The types take STEP's
B-rep names. Each type's documentation names its SMLib record and STEP entity.
Units and numbers
Coordinates are IEEE-754 binary64, in millimetres, right-handed Z-up. Angles and angular parameters are radians, always. (SMLib's STEP-style analytic parameters are degrees; an SMLib adapter converts them, the pcurves on analytic faces included.) References are unsigned 32-bit indices into the body's own lists, never kernel addresses or runtime IDs. Tolerances are millimetres.
Topology
| Record | SMLib | STEP | Holds |
|---|---|---|---|
Brep |
SmBrep |
MANIFOLD_SOLID_BREP, BREP_WITH_VOIDS, SHELL_BASED_SURFACE_MODEL |
tolerance, regions, shells, faces, edges, vertices |
Region |
SmRegionData |
(implied by shells) | its shells, void |
Shell |
SmShellData |
CLOSED_SHELL, OPEN_SHELL, WIRE_SHELL, VERTEX_SHELL |
oriented faces and wire edges; or wire edges; or one vertex |
OrientedFace |
SmFaceuseData |
ORIENTED_FACE |
face, forward |
Face |
SmFaceData |
ADVANCED_FACE |
surface, domain, loops |
Loop |
SmLoopData |
FACE_BOUND with EDGE_LOOP or VERTEX_LOOP |
oriented edges, or one vertex |
OrientedEdge |
SmEUData |
ORIENTED_EDGE and its PCURVE |
edge, forward, pcurve |
Edge |
SmEdgeData |
EDGE_CURVE |
curve, interval, start and end vertex, tolerance, uses |
EdgeUseRef |
SmEUData's radial fields |
— | face, loop, position, upper |
Vertex |
SmVertexData |
VERTEX_POINT |
point, tolerance |
Regions. regions[0] is the infinite region. It has no outer shell and is
never a void. Each bounded region lists its outer shell first, a closed face
shell, then its inner shells. A bounded region is material unless void. A
closed shell separates two regions, so both list it: it is the outer shell of the
region it encloses and an inner shell of the region around it. An open shell, a
wire shell or a vertex shell lies in one region and only that one lists it. A
shell is closed when every edge its faces use is used at least twice among them.
Shells. A face shell lists oriented faces, and the wire edges embedded in it. A wire shell is a connected graph of edges with no faces. A vertex shell is one isolated point. One shell record stands for both runtime sides of a shell.
Oriented faces. A face is used by one oriented face, or by two in different
shells with opposite forward. forward means the face's normal in that shell is
its surface's natural normal, ∂S/∂u × ∂S/∂v. In a closed shell, normals point out
of the material.
Loops. A face has one outer loop and any number of inner ones, in the
producer's order: truck lists some holes before the outer loop, and an SMLib
import puts the outer loop first, as SMLib requires. The first loop is an edge
loop. A vertex loop (a pole, a collapsed boundary) is only ever an inner loop.
Orientation tells outer from inner. Loops are stored
relative to the surface's natural normal, whichever side a shell uses: seen from
that normal, the outer loop runs counter-clockwise and inner loops clockwise.
This lets one face sit in two shells without contradicting itself. An edge loop
is a closed chain: each oriented edge starts at the vertex where the one before
it ends, and the last ends where the first starts. An oriented edge runs from
its edge's start to its end when forward, otherwise from end to start.
Edges. An edge's interval is the part of its curve it covers, min < max.
Its vertices are at interval min and max; they are equal for a closed edge.
uses lists every oriented edge on it, each once: the primary use first (the
first one the canonical walk meets), then the rest in radial order, the
right-hand rule about the edge's direction. A wire edge has no uses. upper says
the radial cycle enters that use on the side of the surface's natural normal.
With loops stored relative to the natural normal it follows from the use:
upper is always !forward of the oriented edge it names.
Tolerances. The body, every edge and every vertex carry a tolerance, finite and positive, never optional. The body's is SMLib's intersection tolerance.
Canonical order
The lists are in one backend-independent order, and every adapter produces it. A
face's index in faces is its face index everywhere: in history, face sources,
paint and persistent names. The same holds for edges and vertices.
- Regions: the infinite region, then bounded regions in the producer's order.
- Shells and faces in order of first appearance, walking regions → shells → oriented faces.
- Edges in order of first appearance, walking faces → loops (in stored order) → oriented edges, then wire edges walking shells. A face whose first oriented face is reversed is walked backwards: each of its loops from its last oriented edge to its first. That is how a reversed face is traversed.
- Vertices in order of first appearance, walking edges (start, then end), then vertex loops walking faces → loops, then vertex shells.
Nothing may be listed that the walk does not reach.
Geometry
Curves and surfaces are unbounded. An edge's bounds are its interval, a face's
its loops inside its domain. Each kind's parameter is stated exactly. For the
kinds the truck adapter writes (line, spline, rational_spline,
surface_intersection, plane_through, placed_revolution) the parameter is
truck's own evaluation (ParametricCurve::subs, ParametricSurface::subs).
Analytic frames store an origin and orthonormal x and y axes; z = x × y.
Curves
| Kind | Point at t | Range of t |
|---|---|---|
line {start, end} |
start + t (end − start): 0 at start, 1 at end | unbounded |
circle {frame, radius} |
origin + r (cos t x + sin t y) | periodic |
ellipse {frame, radii} |
origin + r₀ cos t x + r₁ sin t y | periodic |
polyline {points} |
segment i spans [i, i + 1] | [0, n − 1] |
spline {knots, points} |
B-spline on the full nondecreasing knot vector; degree = knots − points − 1 | [knots[degree], knots[count]] |
rational_spline {knots, homogeneous} |
the same basis, homogeneous [x·w, y·w, z·w, w] coefficients as the producer holds them |
as spline |
surface_intersection {surfaces, leader, tolerance} |
the intersection of two surface records, traced from leader (a polyline or spline near it) to within tolerance mm; t is the leader's |
the leader's |
A surface_intersection is the procedural curve itself, not a fit: another
kernel traces it again. Its point at t is the point of both surfaces in the plane
through the leader's point at t, normal to the leader there (truck's subs
converges to it within 10⁻⁶ mm).
An edge on a line, circle or ellipse may also carry nurbs, a spline
or rational_spline: the NURBS form it is parameterised on, as SMLib holds it
(every SMLib conic is a B-spline curve, the trims on it in that NURBS's
parameter, not the angle). The edge's interval and its pcurves' parameter are
then the NURBS's, and curve only says what the edge is (centre, radius). This
mirrors a face's nurbs: faces and edges are held as SMLib holds them.
Surfaces
Natural normals are ∂S/∂u × ∂S/∂v.
| Kind | Point at (u, v) | u | v |
|---|---|---|---|
plane {origin, u_axis, v_axis} |
origin + u u_axis + v v_axis | unbounded | unbounded |
plane_through {origin, u_point, v_point} |
origin + u (u_point − origin) + v (v_point − origin) | unbounded | unbounded |
cylinder {frame, radius} |
origin + v z + r (cos u x + sin u y) | angle, periodic | height, unbounded |
cone {frame, radius_at_origin, half_angle} |
origin + v z + r(v) (cos u x + sin u y), r(v) = radius_at_origin + v tan(half_angle) | angle, periodic | height, on the side of the apex where r(v) ≥ 0 (a negative half angle narrows along z) |
sphere {frame, radius} |
origin + r (cos v (cos u x + sin u y) + sin v z) | longitude, periodic | latitude, [−π/2, π/2] |
torus {frame, major_radius, minor_radius} |
origin + (R + r cos v)(cos u x + sin u y) + r sin v z | longitude, periodic | meridian angle, periodic |
spline {u_knots, v_knots, points} |
tensor-product B-spline, points[i][j] with i along u |
u knot domain | v knot domain |
rational_spline {u_knots, v_knots, homogeneous} |
the same, homogeneous coefficients | u knot domain | v knot domain |
placed_revolution {profile, origin, axis, matrix, forward} |
M · R(angle) · profile(s): the profile revolved about origin and unit axis by the right-hand rule, then mapped by the column-major affine matrix M | forward: s, the profile's parameter; else the angle |
forward: the angle, [0, 2π); else s |
A plane_through is a plane given by three points, as its producer holds them;
writers use it rather than the axis form, whose axes would be rounded
differences of the points. A placed_revolution keeps the revolution exactly as
its producer holds it (truck's RevolutedCurve under a Processor): nothing is
baked into the profile, and forward: false swaps u and v, which reverses the
natural normal.
A face on a cylinder, cone, sphere or torus may also carry nurbs, a
spline or rational_spline: the NURBS form it is parameterised on, as SMLib
holds it (every SMLib analytic surface is a B-spline surface, and its trims are
in that NURBS's parameters, not the angle). The face's domain and pcurves are
then in the NURBS's (u, v), its natural normal is the NURBS's, and surface
only says what the face is (axis, radius). Pcurves on such a face are SMLib's
own trims, with nothing fitted. A STEP writer writes the analytic surface and
the face's edges without pcurves.
Parameter ranges
An edge interval and each side of a face domain are finite with min < max. In a periodic direction they may start anywhere and run up to one turn (2π), across the seam. Otherwise they lie inside the natural range: a spline's knot domain, a polyline's [0, n − 1], a sphere's latitudes, a cone on its open side of the apex.
Pcurves
Every oriented edge carries its pcurve: the edge in its face's (u, v), as a 2D
B-spline (Curve2: spline, or rational_spline with [u·w, v·w, w]
coefficients). There is no polyline or analytic 2D kind. 2D lines and arcs are
exact as degree-1 and rational quadratic splines; anything else is a fitted
B-spline. A pcurve lies on the edge's own interval and runs from interval min to
max, whatever the oriented edge's direction. Its interval must equal the edge's
within 10⁻⁹ of the interval's magnitude (at least 1), room for a linear remap's
rounding. A seam edge has two uses on one face, each with its own pcurve, a
period apart. On a periodic surface a loop's pcurves are unwrapped, so each one
starts where the one before it ends. A spline surface that closes on itself in a
direction (knots clamped there, first and last control rows the same points with
the same weights, within 10⁻⁹ of the control net's extent) is periodic in it for
pcurves and tessellation, one active knot interval per period: a seam on it has
its two uses an interval apart. The record is unchanged; a parameter outside the
interval is evaluated a whole number of periods back inside it.
Where a kernel holds a use's pcurve exactly in the record's parameters, the
record keeps the kernel's own: the SMLib export takes SMLib's UV trims on
B-spline faces for edges recorded as splines or lines (the face's parameters
are SMLib's, the edge's parameter SMLib's or an affine image of it). SMLib keeps
an analytic face's trims on its internal rational B-spline, whose angle
parameter is not the record's angle, and a circle's or ellipse's on its NURBS
parameter, not its angle; neither is an exact B-spline in the record's
parameters. The rest come from brep::pcurve::pcurves, which computes them from
a face's surface record and each use's curve record and interval, for every
backend (a use's given pcurve is taken as it is). It is exact in closed form
for lines, polylines and whole splines on planes, circles about the axis of a
cylinder, cone, sphere or torus (v fixed, u the circle's angle plus a constant)
and lines along a cylinder's or cone's ruling (u fixed, v linear), each checked
by lifting it; everything else is a cubic B-spline fitted within the tolerance.
Validation and trust
Brep::validate checks structure and resources without a kernel. It caps 1,024
shells, 4,000,000 combined topology and geometry items (pcurve control points included), geometry nesting at 16,
spline degree at 16 and control points per spline direction at 4,096. Numbers
must be finite and at most 1e12 in magnitude. It checks knot order, active spline
domains, multiplicities, rectangular grids, positive rational weights,
nondegenerate planes, orthonormal frames, unit revolution axes, positive radii,
ring tori, cone dimensions and tolerances. Then the topology: every index in
range; the regions and shells as described above; each face used by one or two
oriented faces; first loops are edge loops and vertex loops are inner; loops are
closed chains through shared vertices; every edge's uses lists exactly its uses,
once each, primary first, with upper as stated; pcurve intervals; parameter
ranges; and the canonical order.
The item cap is for records from outside the kernel: a native file, a STEP file,
the control channel. Brep::validate_built runs every other check on a record the
kernel built in memory (an operation's result, a body's own record, a part of one):
that record already sits in memory, its size set by inputs bounded where they came
in, and the checks are linear in it, so a cap there would only refuse a real result.
These are structural and resource guards, not a geometric validity certificate. The validator does not check that edges lie on their surfaces, that pcurves lift onto their edges, that a closed shell is watertight or that shells nest as the regions say. A backend's adapter checks what it needs. The binary reader bounds bytes, allocation and nesting before creating these structures.
Brep::check_geometry spot-checks, on a record validate accepted, that its
geometry agrees with its topology, each distance against the record's own
tolerance: every edge's curve meets its start and end vertices at its interval's
ends (within the edge's and the vertex's tolerances together); every oriented
edge's pcurve, lifted through its face's surface, lies on the edge's curve at the
interval's ends and middle (within the edge's) and starts on the same period as
the use before it ends, except along a direction that collapses at a pole; every
edge loop's pcurves enclose (u, v) area (more than 10⁻⁹ of the square of the
loop's extent), and exactly one loop of each face, the outer, runs
counter-clockwise about the natural normal, the rest clockwise (told apart by
their areas, not their order); every vertex loop's vertex lies on its face's
surface (within the vertex's). It makes a fixed number of evaluations per record, so its work is linear in the
record, and a point it cannot evaluate is an error naming the record. It is not
a certificate either: between the points it checks, an edge may still leave its
surface. The truck adapter runs it before building topology, which truck does
without checks of its own.
Schema tests build and validate one minimal body of each form (an open sheet, a closed shell, a seam edge, a vertex loop with a wire and a vertex shell, a void) and one fixture per defect, without a kernel.
History
The runtime history types (FaceOrigin, Change, FaceProv, Side,
EdgeOrigin, EdgeProv, History) live in solidcraft_geom::brep::history, so
a backend can report history without the kernel crate. Indices are canonical face
and edge indices; operands are numbered by each operation. An operation returns
history beside the B-rep. A backend that cannot report it returns none and never
guesses. The saved form is the envelope's own records (below).
Face-source certificates, operation history, appearances and persistent document identities belong to a versioned SolidCraft envelope around this geometry. They must be preserved by the cache codec, not reconstructed from face counts or floating-point comparisons. A decoded cache remains provisional until authoritative re-evaluation finishes. The document and original blobs remain sufficient to reopen when a cache, representation, producer or adapter is unsupported.
Body binary envelope, version 5
SCBREP05 is the eight-byte envelope signature. Geometry schema 4 is nested in
solid; metadata schema 1 is nested in metadata; runtime holds what the body
measured about itself that later operations read (its size, from which kernel
tolerances follow, and the orientation mesh's missing-face count), so a decoded
body works to the same tolerances. These are separate version numbers because
each contract can evolve independently. Signatures 01 and 02 were development
prototypes; 02 introduced the explicit geometry schema, 03 separated metadata
from runtime Rust types, 04 made the encoding exact and 05 carries schema 4.
Unsupported versions are cache misses, not design-file migration failures: a
design whose cache holds an older signature opens and is evaluated again.
The envelope uses its own explicitly versioned records for color, paint, authored boundaries, face-source certificates, operation history and boundary protection. Plane, region and segment records are also owned by the wire schema. Exhaustive manual adapter conversions separate these from runtime FaceSource, History, FaceProv, EdgeProv, Paint, Seg2, Region2 and Plane types. Their serde derives or internal refactors cannot silently redefine the saved fields. No truck serde struct, pointer, tessellation or STEP string is written. Changing field semantics requires a format version change, even if a Rust type keeps its name.
Values use tags: null=0, false=1, true=2, signed integer=3, unsigned integer=4, binary64=5, UTF-8 string=6, array=7, object=8. Numbers use eight little-endian bytes. Strings and collections use unsigned 32-bit little-endian lengths; object keys are length-prefixed UTF-8, emitted lexicographically. Readers reject duplicate keys, unknown tags, non-finite numbers and trailing bytes. Limits per body are 256 MiB, 20,000,000 values, depth 48, 1,000,000 entries per collection and 1 MiB per string; a design's cache bounds the total (see the container format).
The truck adapter
Truck keeps a face's wires relative to its surface and flips the face with an
orientation flag. That is schema 4's convention: the adapter writes truck's wires
as the loops and the flag as forward, with no wire reversed. It numbers faces
in face_iter order and edges in first appearance through edge_iter, which
walks a reversed face backwards, so the record's indices are the kernel's.
It writes every curve and surface as truck holds it: lines, B-spline and NURBS
coefficients, the three points of a plane, a revolution's profile, axis, matrix
and orientation, and an intersection's two surfaces, leader and tolerance. An
edge's interval is its truck curve's parameter range. Pcurves come from
brep::pcurve on the records just written; a face's domain is the
box around its pcurves' control points, clamped to the surface's natural domain
and at most one turn in a periodic direction. Every tolerance is the body's face
tolerance, 10⁻⁶ of its size and at least 10⁻⁵ mm, how far later operations take
its faces to be from its edges.
Regions come from truck's shells. Open shells lie in the infinite region. One
closed shell bounds one material region. With several closed shells, a
tessellated nesting test (the one Body::lumps uses) finds which bound material
and which voids, and what lies inside what. If truck's shell order is not the
canonical one (a void listed before another lump), or open and several closed
shells mix, the adapter refuses and the body is a cache miss. So it does for an
edge with more than two uses, whose radial order would need geometry.
Decoding builds the same structures without the constructors that would round them (normalizing an axis, composing a matrix with the identity), and rebuilds each shell's topology as recorded, including closed edges on one vertex and seam edges a wire uses twice. It ignores pcurves, regions and domains, which truck does not keep. It refuses what truck cannot hold: wire and vertex shells, wire edges in a face shell, vertex loops, a face used by two shells, standalone polylines, unclamped spline domains and a spline or intersection edge on part of its curve. It loads circles and ellipses as exact rational arcs over the edge's interval, and analytic surfaces as revolutions, which truck parameterizes its own way.
Hashes prove which document and payload a cache belongs to, not who wrote it: the container's key ties a cache to the producer, application build and platform that computed it, and the decoder applies the structural and resource checks above to every body. A second backend needs its own tested adapter before cross-kernel interoperability can be claimed.
The box-and-fillet golden JSON fixture pins the decoded version-5 envelope,
field names, tags, topology, geometry and metadata. Integer and structural values
are exact; floating coefficients allow 1e-10 relative/absolute platform rounding.
The signature is asserted separately. NATIVE_BREP_BLESS=1 rewrites the fixture
for an intended change.
Bounded provisional display
native_preview_mesh samples at most 32 points along each curved edge, caps each
face's boundary at 4,096 points, and bounds spline controls and nested surfaces
before parameter searches. Searches use at most 16 refinement trials. Revolution
profiles involving procedural intersections are refused in this display path.
The separate mesher uses a fixed grid of at most 16 subdivisions per direction
and fixed periodic-seam connections; it never invokes adaptive edge, surface or
aspect-ratio refinement. Planes use a single grid cell.
Before projection, the caller reserves a conservative triangle bound covering duplicated projected boundary points, periodic seams and the grid for the complete body. Output is checked against that reservation and for finite coordinates. Missing faces or exceeded budgets cause a cache miss. This mesh is approximate, read-only display; normal modelling/export tessellation is unchanged.