Introduction
ogeom is a boundary-representation (B-rep) CAD kernel written from scratch in Rust. It does not wrap, vendor or link any existing CAD kernel.
What it covers
- Parametric curves and surfaces.
- Shared B-rep topology with per-entity tolerances and operation history.
- Booleans, blends and chamfers, offsets and shells, sweeps and lofts.
- Healing and tessellation.
- Product structure (assemblies) and 2D drawings with hidden-line removal.
- Data exchange:
- STEP, read and write, with assemblies, colours, semantic PMI and saved views.
- IGES, read and write.
- The native document format.
- Mesh formats. Meshes can be turned back into solids, with their curved regions recognized.
How this book is kept honest
- Examples are tested. Every code block in this guide comes from a test file that runs in CI, so a broken or false example fails the build. The test suite checks every numeric claim against a closed form or a round trip.
- Completeness is audited. Scope sets the target: parity with the reference kernel’s four modelling modules, plus meshes into solids. The parity ledger is committed and machine-checked: it maps every public header of those modules to a named capability, and each verdict cites symbols and tests the build verifies.
Where things are
- This guide: concepts and workflows, in reading order.
- The API reference:
rustdoc for the
ogeomumbrella crate. Depend on that crate. The crates under it are an implementation detail and will change. - The repository: source, issues, and the governing documents this book includes verbatim.
Getting started
The kernel is not on crates.io yet. Depend on it by git:
[dependencies]
ogeom = { git = "https://github.com/gilbertorconde/ogeom-rs" }
Use the ogeom umbrella crate. It re-exports the whole API as modules
(ogeom::algo, ogeom::boolean, ogeom::topo, ogeom::io, …). The
ogeom-* crates underneath are an implementation detail and their
boundaries will change.
A first solid
A block with a hole through it: two primitives, one boolean, and a volume check.
let mut model = Model::new();
let tol = Tolerances::millimetres();
// A 20×20×10 block, and a Ø8 hole through its middle.
let block = ogeom::algo::make_box(&mut model, Frame::WORLD, (20.0, 20.0, 10.0), tol)
.unwrap()
.shape;
let axis = Frame::new(Point::new(10.0, 10.0, 0.0), Direction::Z, Direction::X, tol).unwrap();
let drill = ogeom::algo::make_cylinder(&mut model, axis, 4.0, 10.0, tol)
.unwrap()
.shape;
let part = ogeom::boolean::cut(&mut model, &block, &drill, tol)
.unwrap()
.shape;
// The result is measured, not assumed: volume against the closed form.
let volume = ogeom::algo::volume_properties(&model, &part, Deflection::default(), tol)
.unwrap()
.mass;
let exact = 20.0 * 20.0 * 10.0 - core::f64::consts::PI * 4.0 * 4.0 * 10.0;
assert!((volume - exact).abs() / exact < 0.01);
The same patterns apply to the whole API:
Modelowns all data. Geometry, topology, tolerances and history live in oneModel. Operations take&mut model. AShapeis a cheap handle into the model; copying it copies no geometry. See the data model.- Every operation takes a
Tolerances. There is no global epsilon.Tolerances::millimetres()is the preset for models in millimetres. See Tolerances. - Every operation returns a
Built.built.shapeis the result. The rest ofBuiltis the history: which input entities generated or were modified into which outputs. Every operation fills it in, so parametric applications can rely on it. - Errors are values. Everything returns
Result. When the kernel cannot produce a correct result, it returns a named refusal instead of bad geometry.
The data model
This chapter is the kernel’s normative data-model document, included
verbatim from
docs/DATA_MODEL.md.
It is written for people implementing against the model. The rest of this
guide refers to it.
The ogeom data model
Status: normative. Everything in crates/ implements this document. A
change here is a design change and must be argued as one (see
CONTRIBUTING.md).
Why this document exists
A B-rep kernel’s public API is a thin layer over its data model. What the model can express limits what the kernel can do. Two examples:
- Flatten a location chain into a 4×4 matrix, and assembly instancing becomes impossible.
- Give an edge one curve instead of a list, and boolean face splitting has nothing to split with.
Neither can be fixed later at the API layer.
The field has converged on the model below over thirty years, because the alternatives do not work. It is described here in its own terms, not as a port of any implementation. Each invariant is:
- cheap now: a few days of design attention;
- effectively impossible to retrofit: adding it later means reworking every algorithm in the kernel;
- load-bearing: its failure mode is written out, so the cost of dropping it is concrete.
In two places the conventional design is wrong and we deliberately differ: stable identity (§8) and predicate abstraction (§9).
Notes marked Elsewhere give the conventional name for a concept, because that
is how the field talks about it. They are a glossary, not a dependency: ogeom
links against no existing CAD kernel. Where a note cites usage counts, they come
from api_surface.json, a profile of one large application. The counts show a
requirement is real; they do not define scope. See SCOPE.md.
1. A shape is a triple
pub struct Shape {
tshape: TShapeId, // arena key: the shared, positionless topology node
location: Location, // where this instance sits
orientation: Orientation, // which way its boundary faces
}
Shape is cheap to copy and is passed by value everywhere. The heavy data
(children, geometry, tolerances) is stored once, in an arena, behind
TShapeId.
This separation is why B-rep scales. The same TShape can appear at many
locations with many orientations without copying any geometry.
Consequence: traversal composes.
- A sub-shape’s effective location is the product of every location from the root down to it.
- Its effective orientation is the composition of every orientation on that path.
An explorer that yields sub-shapes without composing both is wrong. It produces plausible-looking garbage, not a crash.
Elsewhere:
TopoDS_Shape={Handle(TopoDS_TShape), TopLoc_Location, TopAbs_Orientation}.
2. Location is a chain, not a matrix
pub struct Location {
// (datum, power) pairs. Empty == identity.
chain: SmallVec<[(DatumId, i32); 2]>,
// composed transform, computed on demand
cached: OnceCell<Trsf>,
}
A Datum is a reference-counted rigid transform. A Location is a sequence
of (datum, integer power) pairs.
Why not a 4×4 matrix:
- Composition is concatenation. Roughly O(1), no matrix multiply, and no drift from repeated floating-point composition.
- Identity comparison is structural. Two shapes are at “the same place” if their chains are equal. No need to compare 16 floats against a tolerance. This is what lets 10,000 identical bolts in an assembly share one piece of geometry and be recognized as instances of it.
- Inverses are exact. Negate the power instead of inverting a matrix.
The composed Trsf is computed lazily and cached. Nothing outside ogeom-topo
should ever need to read the chain itself.
Elsewhere:
TopLoc_Location, a linked list of(datum, power)pairs.
3. Orientation composes multiplicatively
pub enum Orientation { Forward, Reversed, Internal, External }
| Value | Meaning |
|---|---|
Forward | The material is on the surface’s default side. |
Reversed | The material is on the other side. |
Internal | The boundary lies inside the material (for example, a stiffener edge embedded in a face). |
External | The boundary lies outside the material (reference geometry). |
Composition is a monoid, applied at every level of descent:
compose(Forward, x) = x
compose(Reversed, Forward) = Reversed
compose(Reversed, Reversed) = Forward
compose(Internal, _) = Internal
compose(External, _) = External
An edge’s orientation within a face depends on that face’s orientation within its shell, which depends on the shell’s orientation within the solid. Reversing a solid must not require touching any child.
Elsewhere:
TopAbs_OrientationandTopAbs::Compose.
4. Identity has three levels
There are three distinct equivalences, with three distinct hashers. They are not interchangeable:
| Predicate | Compares | Used for |
|---|---|---|
is_partner | tshape only | “Is this the same underlying topology, anywhere, either way round?” |
is_same | tshape + location | Set membership in most algorithms; the common case |
is_equal (==) | tshape + location + orientation | Exact identity; ordered containers |
Every map and set type names the equivalence it uses and enforces it in its
hasher. A HashMap keyed on is_equal semantics that hashes only the tshape is
a silent correctness bug.
Elsewhere:
IsPartner/IsSame/IsEqualand theShapeMapHasherfamily. Mixing them up is a common, well-documented source of bugs in applications built on kernels that expose all three.
5. Tolerances are per entity, and they only grow
Every vertex, edge and face carries its own tolerance: the radius of the sphere, pipe or slab within which the entity is considered to lie.
Containment rule (an invariant), for entities in a boundary relationship:
tol(vertex) >= tol(edge) >= tol(face)
- Operations may only increase tolerances, never silently decrease them.
- An operation that cannot satisfy the rule has failed and must say so.
This is not a workaround for sloppy code. Exact arithmetic cannot represent the intersection curve of two NURBS surfaces (it is transcendental). A tolerance-carrying topology is the only known way to build a kernel whose models close. Every production kernel works this way. See §9 for what exact predicates can do.
Elsewhere: a per-entity
Toleranceon the vertex, edge and face records; a validity checker that enforces the rule; and a boolean post-pass that increases tolerances until the rule holds.
6. An edge carries a list of representations
An edge has a list of representations, not one curve:
pub enum EdgeRepr {
Curve3d { curve: CurveId, location: Location, range: (f64, f64) },
PCurve { curve: Curve2dId, surface: SurfaceId, location: Location },
PCurveClosed { curve: Curve2dId, curve2: Curve2dId, surface: SurfaceId, location: Location },
Polygon3d { polygon: PolygonId, location: Location },
PolygonOnTri { polygon: PolygonOnTriId, triangulation: TriangulationId, location: Location },
}
One edge holds, at the same time:
- a 3D curve;
- one pcurve per adjacent face;
- two pcurves where it is a seam on a closed surface;
- cached discretizations.
Why it must be a list:
- A boolean splits faces in 2D parameter space. Without a pcurve on each face, there is nothing to split with.
- Different surfaces have different parameterizations, so one 2D curve cannot serve two faces.
- A seam edge on a cylinder appears at both
u = 0andu = 2π. One pcurve cannot express that.
The same_parameter flag asserts that all representations agree on the
parameterization: for the same t, curve3d(t) and surface(pcurve(t)) are
the same point within tolerance. The claim can be false. A repair routine
re-establishes it, possibly by increasing the edge’s tolerance.
Elsewhere:
BRep_CurveRepresentationand its subclasses;SameParameter.
7. Every operation emits history
pub trait Operation {
fn generated(&self, input: Shape) -> &[Shape]; // new entities made *from* input
fn modified(&self, input: Shape) -> &[Shape]; // what input *became*
fn is_deleted(&self, input: Shape) -> bool; // input has no image in the result
}
History is not optional, not deferred, and not “added when something needs
it”. Every operation in ogeom-algo, ogeom-bool, ogeom-fillet and
ogeom-offset fills these in from the first commit that introduces it.
The reason is downstream. A parametric application records “fillet that edge” and must still find that edge after the model is rebuilt with different dimensions. It finds it again by walking history. This is the topological naming problem. Every application built on a kernel that identifies topology by pointer has had to solve it this way; in one well-known case, it took a decade of work layered on the kernel’s history maps.
Adding history later means revisiting every algorithm. Incomplete history is worse than none: it fails silently and corrupts documents instead of raising an error.
Elsewhere:
Generated/Modified/IsDeletedon the operation base class, plus a standalone history object.
8. Entity identity is stable (deliberate divergence)
The conventional design identifies topology by pointer. Every modelling operation allocates new nodes, so every reference into a previous result is lost. That is the topological naming problem. Every downstream fix tries to reconstruct identity afterwards by walking history maps.
We record identity at creation instead:
pub struct EntityId(u64); // stable for the lifetime of a document
pub enum Provenance {
Primitive { op: OpId, role: PrimitiveRole }, // "the +Z face of box #3"
Derived { op: OpId, from: SmallVec<[EntityId; 2]>, role: DerivedRole },
Imported { file: FileId, external: ExternalRef },
}
An entity’s identity is what produced it, and from what, not where it sits in memory.
- When a boolean splits a face, each fragment knows it came from that face.
- A rebuild with different parameters produces entities with the same provenance, so a reference like “the fillet on this edge” survives.
History (§7) is still required. An embedding application consumes it, and it is the honest answer where provenance alone cannot resolve a reference. But provenance is the primary mechanism, and it only works if it is designed in from the start.
9. Numerics are abstracted; the tolerance model is not negotiable
pub trait Predicates {
fn orient3d(a: Point, b: Point, c: Point, d: Point) -> Sign;
fn insphere(a: Point, b: Point, c: Point, d: Point, e: Point) -> Sign;
// ...
}
Algorithms are written against this trait. Implementations may be fast-filtered floating point, adaptive exact (Shewchuk), or interval-based, and can change without touching any algorithm.
What this does and does not buy:
- Exact predicates solve the polyhedral robustness problem: the orientation of a point against a plane, in-sphere tests for Delaunay.
- They do not solve the CAD problem. The intersection curve of two curved surfaces has no exact representation to be exact about. That is why §5 exists and cannot be traded away.
- Predicates decide exactly what can be decided exactly. Tolerances handle the rest.
Tolerance constants live in ogeom-core. The model’s unit scale is
explicit, not assumed to be millimetres. Kernels often assume millimetres,
and then misbehave silently on models authored in metres or inches.
| Constant | Value at unit scale | Meaning |
|---|---|---|
CONFUSION | 1e-7 | two points are the same point |
ANGULAR | 1e-12 | two directions are parallel |
INTERSECTION | CONFUSION * 1e-2 | intersection convergence |
APPROXIMATION | CONFUSION * 1e1 | curve/surface fitting target |
P_CONFUSION | CONFUSION * 1e-2 | parametric-space confusion |
10. Geometry is reached through traits
pub trait Curve3d {
fn range(&self) -> (f64, f64);
fn value(&self, u: f64) -> Point;
fn d1(&self, u: f64) -> (Point, Vector);
fn d2(&self, u: f64) -> (Point, Vector, Vector);
fn continuity(&self) -> Continuity;
fn kind(&self) -> CurveKind; // for analytic fast paths
// ...
}
Every intersection, projection, extrema and tessellation algorithm is written
against Curve3d / Curve2d / Surface, never against a concrete type. To a
caller, a face adaptor (surface + location + trimming) and a bare analytic plane
are the same thing.
kind() lets algorithms opt into analytic fast paths (plane/plane
intersection should not go through a marching intersector). The general path
never needs to know what it is looking at.
Elsewhere: the
Adaptorfamily. This is the best idea in the conventional design, and we adopt it as is.
11. Memory: arenas, not reference counting
Topology lives in typed index arenas (Vec<T> plus a typed u32 key), not
behind Arc or an intrusive reference count.
- No reference cycles to reason about. Intrusive reference counting has no cycle collection, so kernels built on it avoid cycles by convention only.
- Cache-friendly traversal.
- Keys are small,
Copy, and serializable. - It is what makes §8 possible at all.
The cost: a shape only has meaning relative to the arena that owns it. That is the right trade, and the API makes it explicit instead of hiding it.
Append-only arenas, and what relies on it
In practice the arenas are append-only: nothing in the kernel removes entries. Two non-builder paths rely on this.
Model::from_partsassembles a restored document by replaying the file’s insertion order, which reproduces every handle.Model::absorbuses the same engine on a model that already contains data. Another document’s parts are appended, with every handle shifted past what the target already holds, so an absorbed shape is indistinguishable from one built in place. This is how a serialized tool body meets a live one in a boolean.
Absorption:
- preserves the source’s identities under a plain offset (the remap table says where each one landed);
- keeps the source’s provenance verbatim, including source
OpIds, which only have meaning in the source document’s own rebuild; - refuses, by name, a document at a different unit scale. Rescaling is a feature, not a default.
Elsewhere: a transient base class with intrusive reference counting, plus a custom small-block allocator. We need none of it.
12. Errors are values
Operations return Result<T, OgeomError>. The variants cover the failures a
kernel actually needs: construction, domain, range, dimension mismatch, null
object, not done, numeric failure, invariant violation. They match the
categories applications already handle, so they translate cleanly into any
host’s error model.
There are no exceptions, no setjmp, and no conversion of hardware signals into
throwable objects.
An algorithm that “did not converge” returns that fact. It does not return a null shape and set a flag that the caller may forget to check.
Elsewhere: a thrown
Failurehierarchy and, in at least one kernel, a facility that turns SIGSEGV into a catchable exception. We do neither.
Checklist for a new algorithm
- Written against the geometry traits (§10), not concrete types.
- Populates
generated/modified/is_deleted(§7). - Assigns provenance to every entity it creates (§8).
- Composes location and orientation correctly during traversal (§1, §2, §3).
- Uses the right identity predicate, with a matching hasher (§4).
- Keeps the tolerance containment rule, or fails loudly (§5).
- Keeps edge representations consistent, or clears
same_parameter(§6). - Returns
Result; never a silently invalid shape (§12).
Tolerances
Imported models have gaps, intersections are computed numerically, and points that should coincide rarely match exactly. ogeom handles this with three rules.
1. No global epsilon
Every operation takes a Tolerances argument. The examples in this guide
use Tolerances::millimetres().
confusion()is the distance below which two points count as the same point.- The other thresholds derive from it.
This keeps the units and expected precision visible at each call site.
2. Tolerances are per entity and only grow
Each vertex, edge and face carries its own tolerance: the radius within which its stated geometry is trusted.
- Entities of a clean primitive sit at the baseline.
- Imported or heavily modified models have wider tolerances where the geometry is less certain.
- Operations may widen a tolerance to record real uncertainty. Example:
when
sewmerges two vertices a micron apart, the surviving vertex widens to cover both. - Nothing narrows a tolerance silently, because that would claim precision
nobody measured.
ogeom::heal::reduce_tolerancesnarrows tolerances by re-measuring the geometry.
3. The containment rule
- An edge must lie within the tolerance regions of its faces.
- A vertex must lie within the tolerance regions of its edges.
The validity checker (ogeom::algo::check) accepts a gap within the
stated tolerance and rejects one outside it. It is no stricter than the
builders, so it never rejects what the kernel legitimately builds.
The full semantics (what each entity kind’s tolerance means, and why) are in §5 and §9 of the data model.
Making shapes
Everything here is in ogeom::algo unless stated otherwise. Every
operation returns a Built with the result and its history.
Primitives
make_box, make_cylinder, make_cone, make_sphere, make_torus,
make_wedge and make_half_space each take a Frame (origin and
orientation) plus their dimensions. Degenerate dimensions are refused by
name, not clamped.
Bottom-up construction
Build other shapes in B-rep order, from vertices up:
| Level | Functions |
|---|---|
| Vertex | make_vertex |
| Edge | make_edge (on a curve), make_edge_between (between points) |
| Wire | make_wire (ordered edges), make_wire_unordered (any order), make_polygon (straight sides) |
| Face | make_face, make_face_on (over a surface), make_face_with_pcurves (when the boundary’s surface parametrisation matters) |
| Shell, solid, compound | make_shell, make_solid, make_compound |
Helpers:
sewstitches faces that share boundaries within tolerance into shells.is_wire_closedandis_shell_closedcheck closure before you build the next level.
Sweeps and fitting
make_prismextrudes.make_revolutionrevolves.- The general sweeps live in
ogeom::offset, which holds their shared machinery:make_pipe(along a wire),make_loft(through sections),make_evolved(along a planar profile). See Offsets, shells and features. interpolatefits a curve through points.approximateandapproximate_withinfit a curve near points to a stated tolerance. The fit reports the deviation it achieved.make_textrenders text as wires, for engraving.
History
Every constructor and every operation in later chapters records history:
which inputs generated which outputs, which were modified, and which
is_deleted. Stable references into a rebuilt model (for example “fillet
that edge”) are resolved through this history. §7 of
the data model defines the contract.
Booleans
The four standard booleans are in ogeom::boolean and share one
signature:
let out = ogeom::boolean::fuse(&mut model, &a, &b, tol)?.shape; // union
let out = ogeom::boolean::common(&mut model, &a, &b, tol)?.shape; // intersection
let out = ogeom::boolean::cut(&mut model, &a, &b, tol)?.shape; // difference
let out = ogeom::boolean::section(&mut model, &a, &b, tol)?.shape; // the curves where they meet
The getting-started example is a
cut, checked against its exact volume.
Other operations
| Function | What it does |
|---|---|
cells | Full cellular decomposition of two solids: every region classified against both inputs. The four booleans select from this. Use it when you need a different selection. |
fuse_fuzzy, cut_fuzzy | Take an explicit fuzz distance, for inputs whose faces almost coincide (mostly imported geometry). Avoids the sliver faces an exact operation would create along the near-contact. |
make_volume | Builds the solids enclosed by an arbitrary set of faces. |
remove_faces | Defeaturing: deletes a feature’s faces from a solid and closes the gap. Neighbouring faces extend to fill it, or the band is re-intersected where extension cannot close it. |
make_periodic | Prepares shapes for repetition along an axis. |
Guarantees
- Tangent cases work. A tool tangent to a face, even at a parametrisation singularity of that face’s surface, produces the correct section curve. The test suite checks these cases against closed forms (sphere octants, blend corners).
- Unresolvable cases are refused. If two inputs interfere in a way the algorithm cannot resolve correctly, the operation returns a named error instead of a shape that is wrong.
Blends and chamfers
Everything here is in ogeom::fillet.
Rounding one edge
let block = ogeom::algo::make_box(&mut model, Frame::WORLD, (40.0, 30.0, 12.0), T)
.unwrap()
.shape;
// Pick the top edge along y = 0 and round it at radius 2.
let edge = edge_near(&model, &block, Point::new(20.0, 0.0, 12.0));
let rounded = ogeom::fillet::fillet_edge(&mut model, &block, &edge, 2.0, T)
.unwrap()
.shape;
// A fillet removes the square corner and leaves the quarter cylinder:
// ΔV = (1 − π/4)·r²·length, exactly.
let volume = ogeom::algo::volume_properties(&model, &rounded, Deflection::default(), T)
.unwrap()
.mass;
let exact = 40.0 * 30.0 * 12.0 - (1.0 - core::f64::consts::FRAC_PI_4) * 4.0 * 40.0;
assert!((volume - exact).abs() / exact < 0.01);
| Function | Result |
|---|---|
fillet_edge | Constant-radius round. |
fillet_edge_variable | Round with a radius law along the edge. |
chamfer_edge | Symmetric flat bevel. |
chamfer_edge_distances | Asymmetric bevel (two distances). |
chamfer_edge_angle | Bevel from a distance and an angle. |
Fillets are not limited to planes. Between cylinders, cones, spheres, tori and fitted patches, the rolling ball is marched numerically instead of solved in closed form. The blend surface is fitted through its cross-section arcs and merged into the solid the same way.
Several edges at once
Edges filleted or chamfered in separate calls each stop flush against whatever they end on. Passed in one call, they join:
let block = ogeom::algo::make_box(&mut model, Frame::WORLD, (20.0, 20.0, 10.0), T)
.unwrap()
.shape;
let top = [
Point::new(10.0, 0.0, 10.0),
Point::new(20.0, 10.0, 10.0),
Point::new(10.0, 20.0, 10.0),
Point::new(0.0, 10.0, 10.0),
]
.map(|at| edge_near(&model, &block, at));
// Four bevels as one operation mitre where they meet: each corner loses
// the overlap of two prisms, a third of the cube of the distance.
let bevelled = ogeom::fillet::chamfer_edges(&mut model, &block, &top, 1.0, T)
.unwrap()
.shape;
let volume = ogeom::algo::volume_properties(&model, &bevelled, Deflection::default(), T)
.unwrap()
.mass;
assert!((volume - (4000.0 - 4.0 * 10.0 + 4.0 / 3.0)).abs() < 1e-2);
// Three fillets meeting at a corner close it with the ball's own patch,
// an octant of a sphere, instead of leaving the bands' caps standing.
let corner = [
Point::new(10.0, 20.0, 10.0),
Point::new(20.0, 10.0, 10.0),
Point::new(20.0, 20.0, 5.0),
]
.map(|at| edge_near(&model, &block, at));
let rounded = ogeom::fillet::fillet_edges(&mut model, &block, &corner, 2.0, T)
.unwrap()
.shape;
let spheres = ogeom::topo::explore_unique(&model, &rounded, ogeom::topo::ShapeType::Face)
.unwrap()
.iter()
.filter(|face| {
let data = model.node(face).unwrap().data().as_face().unwrap();
matches!(
model.geometry().surface(data.surface),
Some(ogeom::geom::SurfaceGeometry::Sphere(_))
)
})
.count();
assert_eq!(spheres, 1);
fillet_edgestrims neighbouring bands against each other, joins tangent chains without a seam, and fills every vertex where three or more filleted edges meet with a rolling-ball corner patch.round_vertexis that corner tool on its own:- where one ball can touch all the faces, it produces a sphere patch;
- where no single ball can (for example the apex of a rectangular pyramid), it produces the exact envelope of the rolling ball: spheres joined by cylinders.
chamfer_edgesandchamfer_edges_withbevel a set of edges in one operation. Every wedge is built on the solid as it was before the call, so the bevels mitre where they meet.
Blends between faces without a shared edge
blend_facesrolls a constant-radius ball between two faces that need not share an edge.march_blendis the underlying marcher. It traces the contact circle and reports why it stopped (BlendStop). Use it to blend up to an obstruction on purpose.
Tangency checks
A blend must end tangent to the faces it joins. analyse_blend measures
the achieved contact and returns it as BlendContact. Fillets report their
own tangency deviation, and a blend that cannot reach tangency within
tolerance is refused.
Offsets, shells and features
Everything here is in ogeom::offset.
Offsetting
| Function | What it does |
|---|---|
offset_shape | Moves a solid’s boundary along its normals (outward, or inward with a negative distance) and rebuilds the intersections where offset surfaces collide. |
make_thick_solid | Shelling: removes the named faces, offsets the rest inward and joins them. Gives a hollow part with an opening. |
offset_wire | 2D offset of a planar wire. The caller picks the Join style (arcs or intersections). Useful for tool-path-like outlines. |
apply_draft | Tilts faces by a draft angle about a neutral plane, for moulded parts. |
Sweeps
The general sweeps share this module’s machinery:
make_pipeandmake_pipe_skinned: sweep along a single spine edge.make_loftandmake_loft_skinned: loft through profile sections.make_evolved: sweep a profile along a planar spine.make_filling: build an N-sided patch face that fills a boundary.
Features
Feature operations combine a sketch with a solid in one step:
feature_prism: bosses and pockets.feature_revol: revolved bosses and grooves.feature_ribandfeature_slot.
Each is a constrained boolean internally and records history like every other operation.
normal_projection projects a wire onto a shape along the shape’s normals
(for engraving). It returns the Projected curves on the target faces.
Healing
Imported geometry is often imperfect: gaps between faces, edges whose 3D
curve and surface curves (pcurves) disagree, one surface split into many
patches. ogeom::heal repairs what it can within tolerance and reports
what it did. Each repair measures what it achieved. A shape that cannot be
repaired within the stated tolerance comes back with a named diagnosis.
| Function | What it does |
|---|---|
sew (in ogeom::algo) | Stitches faces into shells by merging boundaries that coincide within tolerance. Uses each entity’s own tolerance: a vertex merges with what lies inside its tolerance, and the survivor widens to cover what it absorbed. |
repair_same_parameter | Re-fits an edge’s pcurves until they agree with its 3D curve within tolerance. The report gives the achieved deviation per edge. |
unify_same_domain | Merges adjacent faces on the same surface and adjacent edges on the same curve. Undoes the fragmentation that exchange formats cause. |
merge_edges | Joins chains of edges into single edges where the geometry allows. |
reduce_tolerances | Narrows entity tolerances to what the geometry actually measures. This is the only operation that narrows tolerances, and it does so by re-measuring. |
canonical_simplify | Replaces NURBS geometry that is exactly analytic with the analytic form (for example a plane stored as a bicubic patch, or a circle stored as a rational spline). Each match is verified at every sample, and the report includes the worst deviation. A surface that is only almost a cylinder stays a spline. |
reanchor_periodic_rings | Moves the seam of periodic faces so later algorithms see a consistent parametrisation. |
fix_shape | One-call repair for a shape of unknown quality (see below). Returns a FixReport. |
Reshape | The primitive underneath: a recorded substitution of entities that rebuilds everything referencing them. |
fix_shape steps
- Diagnose.
- Put each wire’s edges end to end.
- Collapse edges shorter than their own vertices’ tolerances.
- Fit missing pcurves.
- Sew loose faces.
- Tighten tolerances.
- Widen any vertex tighter than its edges (and any edge tighter than its faces) to restore the containment rule.
- Diagnose again.
The FixReport lists what it did and what the checker still finds. Small
faces and small solids are left in place, because removing them is
defeaturing (see remove_faces in Booleans).
Measurement and checking
Everything here is in ogeom::algo.
Mass properties
volume_properties, surface_properties and linear_properties return
MassProperties: the measure (mass), the centroid, and the inertia
tensor about the centroid.
- Area and volume are integrated on the exact surfaces, trim and all,
whenever every face has pcurves. The result then reports a
deflectionof zero and does not depend on the one passed in. - Otherwise they are computed on a tessellation at the given
Deflection, which the result reports. The answer converges as the deflection gets smaller. Lengths are always measured this way. - An inside-out shell would measure a negative volume. It is reported as an error, not returned as a negative number.
Distances and projections
| Function | Returns |
|---|---|
distance_between_shapes | Minimum distance and the ClosestPair that realises it. |
project_on_curve, project_on_surface, project_on_planar_curve | Parameter and distance of a point’s projection onto the geometry. |
project_edge_onto_plane | An edge projected onto a plane as an exact 2D curve: point, line, circle, ellipse or B-spline. |
curve_length | Length along a curve. |
parameter_at_length | Parameter at a given arc length. |
points_by_count, points_by_spacing | Points spread along a curve. |
Bounds and classification
shape_bounds,curve_bounds,surface_bounds,vertex_bounds: axis-aligned bounding boxes.oriented_bounds: a tight oriented box (Obb).classify_in_solid: is a point inside, outside or on a solid. The_exactvariants work on the exact geometry.classify_on_face: the same test for a point on a face.
Validity
check verifies the rules the builders promise: edges within their faces’
tolerance, vertices within their edges’, wires closed where they claim to
be. It returns a Diagnosis of named Problems, each with a Severity.
It accepts everything the kernel legitimately builds, so what it flags is
really broken.
check_self_intersection and check_tessellation are separate because
they are more expensive. Call them when needed.
Tessellation and drawings
Meshing
ogeom::mesh::tessellate triangulates a shape at a given Deflection:
the maximum distance between the mesh and the exact geometry, plus an
angular bound.
- The triangulation is stored on the model. Read it back with
triangulation_ofandpolyline_of. triangulatereturns one welded mesh for a whole shape.simplifydecimates an existing mesh toward aTarget.hatch_facecross-hatches a face, for section fills.
One mesh per face
A viewer that picks and colours individual faces needs one mesh per face. Adjacent face meshes must use the same points along every shared edge, or cracks appear (for example where a narrow face samples its edges more finely than requested).
- Call
edge_chords_foronce per shape to fix the shared edge points. - Call
triangulate_face_withfor each face using those points.
tessellate stores its faces the same way.
Deflection controls accuracy downstream
Everything that consumes the mesh (mass properties, the mesh exchange formats) carries exactly the error you chose here.
Drawings
ogeom::hlr produces 2D drawings by exact hidden-line removal (not a
rendered image).
-
projecttakes shapes and a view direction and returns aDrawingofDrawnCurves. Each curve has:- a
Visibility(visible or hidden); - a
Source: the model edge, silhouette or outline that produced it.
Silhouettes of curved faces are traced on the exact surfaces.
- a
-
sectioncuts a shape with a plane and returns aSectionViewwith the cut face outlines, ready for hatching. -
broken_sectionis the partial-depth variant.
Because each drawn curve has a Source, a dimension attached to a drawn
line can find the model edge it measures after a rebuild, using the same
history as every other operation.
Documents, assemblies and PMI
A Model holds geometry. An ogeom::doc::Document holds the product
information around it: parts, the assembly tree, names, colours and
annotations. The exchange formats read and write documents, because that
is what a STEP file contains.
let mut document = ogeom::doc::Document::over(model);
let bolt = document.add_part("bolt", bolt_shape);
Assemblies
Products form a tree:
- A part is a leaf.
- An assembly places other products as
Instances.
Instances share geometry. Two bolts in an assembly point to one shape node under different location chains, so a thousand fasteners are not a thousand copies.
roots()returns the top-level products.occurrences_of(root)flattens the tree into placedOccurrences. Each has a path string and its placed shape.
Attributes and PMI
Colours and named attributes attach to products and to individual faces.
PMI (the dimensions, geometric tolerances and datums of a manufacturing drawing) is stored in two forms:
- Semantic: a dimension knows which faces it measures, through the same stable references used everywhere else.
- Presentation:
Calloutpolylines, the drawn form.
Both forms, and the distinction between them, survive STEP.
Views and notes
- A
Viewis a named camera plus the callouts it shows. Annotated models use views to organise PMI into readable sheets. - A
Noteis authored text, optionally attached to a product.
Both survive the native format and STEP.
Undo
The document records every structural change as a step. Undo and redo walk
those steps. undo_depth() returns how many steps are available in each
direction.
Exchange
Everything here is in ogeom::io.
- Exact formats (STEP, IGES, native) carry whole documents: the model plus product structure, PMI and views.
- Mesh formats carry tessellations at the deflection you chose.
STEP
Read and write, at document level:
let block = ogeom::algo::make_box(&mut model, Frame::WORLD, (20.0, 10.0, 5.0), T)
.unwrap()
.shape;
// Exchange works on documents: a model plus product structure, colours,
// PMI, views. A bare part is a document with one product.
let mut document = ogeom::doc::Document::over(model);
document.add_part("block", block);
let text = ogeom::io::write_step(&document, T).unwrap();
let import = ogeom::io::read_step(&text, T).unwrap();
// What came back is the same solid, measured.
let back = &import.document;
let root = back.roots()[0];
let occurrence = &back.occurrences_of(root).unwrap()[0];
let volume =
ogeom::algo::volume_properties(back.model(), &occurrence.shape, Deflection::default(), T)
.unwrap()
.mass;
assert!((volume - 1000.0).abs() / 1000.0 < 0.01);
Round trips preserve assemblies with instancing, names, colours, semantic and presentation PMI, datum systems and saved views.
Bodies come back as:
solids;shells, for parts exported as faces instead of a solid. A surface model stays a shell, under its product like any other body.
read_step returns a StepImport. Its report lists by name every
entity the reader met but did not translate, so nothing is dropped
silently.
Boundary curves off their surface
Real exports often have boundary curves that sit off the surfaces they trim.
| Offset | What the reader does |
|---|---|
| Under 1 mm | Heals it: fits the trim, widens the edge’s tolerance to the measured offset, and emits a warning. |
| Over 1 mm | Treats the boundary as not describing that surface. The face is read untrimmed and refuses to mesh. It is listed in report.untrimmed_faces with its file id and face shape, so you can highlight it or pass it to the healer. check also reports these faces as broken. |
report.summary groups the per-edge warnings (often thousands) into one
entry per kind: count, worst measured value and an example id. Use it for
a status bar. warnings keeps the full text.
Progress and cancellation
Scope a Watch around the call to receive each stage. The readers report
solids as (done, total), so a progress bar can be determinate. The
Watch’s canceller stops the work at the next checkpoint:
use ogeom::core::progress::{self, Stage, Watch};
// A watch scopes a long operation: its sink hears each stage as the
// operation reaches it, and its canceller stops the work at the next
// checkpoint. Stages that know their numbers say them: "step: solid"
// arrives as (done, total), which is what a determinate progress bar
// is made of.
let watch = Watch::with_stage_sink(|stage: Stage<'_>| {
if let Some((done, total)) = stage.progress {
// e.g. hand (done, total) to the status bar
assert!(done <= total);
}
});
let stop = watch.canceller(); // send this to the cancel button
let import = progress::watched(&watch, || ogeom::io::read_step(&text, T)).unwrap();
drop(stop);
// The report is the import's honest ledger: entities the reader met
// and did not translate, warnings one line each, and the faces that
// read without a complete trim, by file id, because their boundary
// sat too far from the surface for any honest pcurve. Nothing here:
// this file is clean.
assert!(import.report.untrimmed_faces.is_empty());
IGES
read_iges and write_iges work at document level like STEP. They cover
the core entity set real files use:
- curve and surface entities, including conic arcs of every kind, ruled surfaces, and offset curves and surfaces;
- trimmed surfaces;
- transforms;
- colour;
- the manifold solid B-rep.
IgesReport names anything outside that set. IGES round trips are tested
by volume like STEP, including periodic cases (spheres, tori) where seam
handling is error-prone.
Native format and .brep
native::write_documentandnative::read_documentround-trip the whole document (exact geometry, tolerances, structure, PMI, views, notes) with no loss. Use it between ogeom sessions.brep::writeandbrep::readstore a single shape as text, for model-level interchange.
Mesh and drawing formats
| Format | Read | Write |
|---|---|---|
| STL (ascii and binary) | yes | yes |
| glTF / GLB | yes | GLB |
| OBJ | yes | yes |
| PLY | yes | yes |
| VRML (1.0 and 2.0) | yes | yes |
| 3MF (deflated or stored, multi-part) | yes | yes |
| DXF (2D drawings) | yes | yes |
read_3mf returns one placed mesh per build item. It flattens components,
follows multi-part packages from the production extension, scales the
model’s unit to millimetres, keeps a uniform object colour when the file
has one, and warns about anything read with a caveat.
read_vrml returns one placed mesh per shape the scene draws: face sets
and the box, ball, drum and cone primitives, with DEF/USE, transforms,
switches and material colours honoured.
The mesh writers take the tessellation you built, so the error is the deflection you chose. DXF is the output for HLR drawings: visible and hidden polylines.
Meshes to solids
algo::solid_from_mesh turns a mesh from any of these formats into a solid
you can model on:
- It builds topology from the mesh’s own connectivity.
- It merges coplanar triangles into planar faces. An STL cube comes back as six faces.
- It rebuilds regions lying on a cylinder, cone, sphere or torus as that surface. A meshed bore becomes a cylinder again, and a meshed ball one spherical face.
Its report says where an open mesh is open, and which curved regions could not be rebuilt exactly and stayed faceted.
Refusals
A plausible wrong answer is worse than an error: a boolean that leaves a sliver of the tool inside, a blend that looks tangent but is not, an import that silently drops a face. ogeom’s rule: when the kernel cannot produce a correct result, it refuses, and the error names the reason.
// A cylinder of zero radius is not a small cylinder; it is a mistake,
// and the kernel says which one instead of producing garbage geometry.
let err = ogeom::algo::make_cylinder(&mut model, Frame::WORLD, 0.0, 10.0, T).unwrap_err();
assert!(err.to_string().contains("cylinder radius"));
Where this applies:
- Degenerate inputs are refused at construction (a zero radius, an empty wire, a face whose boundary does not close). The error names the offending parameter.
- Restricted capabilities refuse outside their limits. Example: the
medial axis supports convex polygonal faces only. Given a reflex corner,
a hole or an arc, it returns an error saying which one it found, instead
of a wrong axis. Every
partialrow in the parity ledger states its restriction, and the code refuses at that same limit. - Exchange readers report what they skip. An unsupported entity appears in the import report by name and number. Everything that was translated can be trusted.
- Repairs report what they achieved. Healing operations return measured deviations. A repair that cannot reach tolerance says so.
For callers: treat every Err as information. The message says which
input, which limit and which capability boundary you hit. The parity
ledger tells you whether that boundary is a known, scoped restriction.
Scope
Included verbatim from
docs/SCOPE.md,
the normative statement of what belongs in this kernel.
Scope
This file says what belongs in the kernel, what does not, and how to decide a case.
The rule
ogeom targets parity with the reference kernel’s modelling modules, and nothing else. There is one deliberate addition: turning meshes into solids (see below).
Four modules are in scope:
| Module | What it covers |
|---|---|
| FoundationClasses | Arithmetic, primitives, solvers, tolerances, errors. |
| ModelingData | The geometry and topology vocabularies: curves, surfaces, the b-rep data model. |
| ModelingAlgorithms | Intersection, booleans, blending, offsets, sweeps, healing, tessellation, hidden-line removal. |
| DataExchange | STEP, IGES, STL, VRML, OBJ, glTF, PLY, and the document structure these formats carry. |
Three modules are permanently out of scope:
| Module | Why it is out |
|---|---|
| Visualization | Rendering, viewers, interactive selection. A kernel is not a renderer. |
| ApplicationFramework | The generic label-and-attribute document tree. The exchange document is in scope, because it is part of DataExchange. The framework beneath it belongs to the application. |
| Draw | A test harness with its own scripting language. |
Anything the reference kernel does not do at all is out of scope by default. Examples: constraint solving, feature recognition and process planning. These are real disciplines, but they are not this kernel.
The one addition: meshes into solids
ogeom turns a mesh into a solid and recognizes its surfaces
(solid_from_mesh, recognize_points).
What the reference kernel does: its modelling algorithms build a shape on a mesh (one planar face per triangle, with shared edges) and merge coplanar faces.
What ogeom adds: it finds which regions of triangles lie on a cylinder, a cone, a sphere or a torus, and rebuilds those regions on those surfaces.
Why: the same reason the exchange module exists. Printers, slicers and model sites exchange meshes. A kernel that reads STL, OBJ and 3MF but can only display the result leaves the application to rebuild the geometry itself.
Recognition meets the kernel’s standard, not a heuristic’s:
- every surface is verified against every sample, at a stated tolerance;
- every edge between recognized faces is placed exactly on both surfaces;
- a region that cannot be built this way stays faceted, and is counted.
This is what makes recognition a construction the kernel can stand behind.
Still out of scope: fitting free-form surfaces to scans, and reading design intent back out of topology.
Code that is out of scope: outside/
Some out-of-scope disciplines were built here before this rule existed, and
they work. Instead of deleting working code, it lives in outside/.
outside/is a separate workspace.- The kernel’s
Cargo.tomlexcludes it by name. - So no path dependency can pull it back in unless someone deliberately deletes that exclusion. This makes the rule structural, not just a statement of intent.
outside/README.md explains why each crate there is out of scope.
How to decide a case
The answer comes from the reference tree’s own files, not from opinion:
adm/MODULESmaps each module to its toolkits.src/<Toolkit>/PACKAGESmaps each toolkit to its packages.src/<Package>/*.hxxare the package’s classes.
Across the four in-scope modules this gives 276 packages and 6,267 public headers. Then:
- If a capability’s counterpart is in that set, it is in scope.
- If it is in Visualization, ApplicationFramework or Draw, it is out.
- If it has no counterpart at all, it is out.
docs/parity/reference-index.tsv is that set, committed, so you can answer the
question without a reference checkout. docs/PARITY.md records where ogeom
stands against it.
What the scope rule does not mean
It does not allow mirroring. CONTRIBUTING.md forbids copying another
kernel’s class hierarchy, decomposition or file layout, and that still holds.
Parity is about capability, not structure:
- the parity record is keyed on what a caller would ask for;
- each entry names the reference packages it accounts for;
- a capability we deliberately provide differently is recorded as
divergent, with the reasoning. It is not a gap.
It is not driven by usage data. docs/api_surface.json profiles how one
large application uses the reference kernel.
- It is a sequencing input: it says what to get right first.
- It appears in the parity index as a column for that purpose only.
- It has never been a scope input. Its own generator says so: “What it is emphatically not good for: deciding what to build.”
- A capability inside the four modules is in scope whether or not that application ever calls it.
It is not a size target. 6,267 headers are not 6,267 things to build. Most
are generic instantiations (TColStd_Array1OfReal and several hundred similar
headers), which Rust generics give for free. The triage rules in
tools/apisurf/apisurf.py reduce the count to the underlying capabilities.
Each rule is recorded with the headers it removed, so the reduction can be
audited.
How the scope changes
Only by editing this file, with the reasoning written down. Never through a pull request that quietly adds a crate.
The parity audit
Most projects describe how complete they are with adjectives. ogeom measures it with an audit that the build enforces. This chapter explains how the audit works, so that the ledger can be read as a measurement.
What is measured
The reference target is fixed by Scope:
- four modelling modules;
- 276 packages;
- 6,267 public headers.
This set is committed as docs/parity/reference-index.tsv, so no reference
checkout is needed to use it.
The audit is keyed on capabilities, not headers:
docs/parity/parity.tomllists 98 capabilities.- Each capability claims the reference headers it accounts for.
- The gate requires the claims to be total and disjoint: every kept header is claimed exactly once, or the build fails.
This turns “nothing was forgotten” into a checked property.
Some headers are not capabilities: generic instantiations that Rust generics replace, containers, superseded internals. Written triage rules remove them. Each rule has a stable id and records the headers it removed, so the reduction can be audited.
Verdicts and evidence
Every capability has one of six verdicts. The gate enforces the evidence each verdict requires:
| Verdict | Means | Must cite |
|---|---|---|
covered | built and tested | symbols that resolve in the built rustdoc, and tests that exist in the tree |
partial | built, with a stated restriction | the restriction in words, plus symbols and tests |
divergent | deliberately different | the reasoning |
absent | not built | a plan entry |
n/a | excluded by a triage rule | the rule id |
unreviewed | not yet audited | counted against a ratchet that can only go down |
The citations are checked on every build. Rename a cited symbol or delete a
cited test, and the audit fails. docs/PARITY.md is generated from the index
and the ledger, committed, and checked for staleness, so the rendered page
cannot drift from the data.
Current state
As of the audit’s completion, no capability is absent. The remaining
work is exactly what the ledger lists: the partial restrictions, and the
unreviewed count, which the ratchet holds at its floor. The
ledger chapter shows the current committed state and is
rebuilt with the book.
The parity ledger
The committed audit, included verbatim from
docs/PARITY.md.
It is generated from the reference index and the hand-written ledger.
tools/check.sh fails if it goes stale, so this chapter is as current as
the commit the book was built from.
Parity
Generated by tools/parity.py generate; do not edit. The inputs
are docs/parity/reference-index.tsv (generated from the reference tree by
apisurf scope) and docs/parity/parity.toml (hand-written; the only file a
person edits). docs/SCOPE.md defines the target; this file measures
against it.
The primary key here is our capabilities. Each claims the reference headers it answers for; the index is the completeness check. One capability per thing a caller would ask for, not per class and not per package.
Where the audit stands
| verdict | capabilities |
|---|---|
covered | 77 |
partial | 7 |
divergent | 9 |
n/a | 5 |
2704 reference headers in the reviewable pool; 2704 claimed by 98 capabilities, 0 awaiting a claim. Ratchet: unreviewed_max = 0.
Capabilities
ogeom-algo
- algo.bounds: Guaranteed and tight bounding boxes, axis-aligned and oriented, for curves, surfaces and shapes ·
covered· 16 headers claimed - algo.builders: Building topology by hand: vertices, edges, wires, faces, shells, solids ·
covered· 35 headers claimed - algo.classification: Classifying a point against a solid or a face: in, out, or on, exactly ·
covered· 22 headers claimed - algo.curve-sampling: Sampling curves: by count, by spacing, by deflection, by abscissa ·
covered· 14 headers claimed - algo.fitting: Interpolating and approximating points with B-splines, curves and surfaces ·
covered· 82 headers claimed - algo.global-properties: Volume, area, mass, centroid and principal moments of shapes ·
covered· 24 headers claimed - algo.history: Every operation emits history: what was generated, modified, deleted ·
covered· 1 header claimed - algo.medial-axis: The medial axis of a planar region ·
covered· 18 headers claimed - algo.nurbs-conversion: Converting a shape’s geometry to NURBS form ·
covered· 2 headers claimed - algo.place-copy: Copying and transforming shapes, rigidly or generally, sharing what can be shared ·
covered· 10 headers claimed - algo.point-projection: Projecting a point onto a curve or a surface, nearest first, all minima found ·
covered· 26 headers claimed - algo.primitives: The primitive solids: box, wedge, cylinder, cone, sphere, torus, half-space, prism, revolution ·
covered· 23 headers claimed - algo.sewing: Sewing faces into shells along coincident edges, with spatial acceleration ·
covered· 11 headers claimed - algo.shape-distance: Distance, proximity and overlap between whole shapes, and self-intersection ·
covered· 18 headers claimed - algo.solid-from-mesh: A solid from a triangle mesh: coplanar triangles merged into planar faces, curved regions recognized as cylinders, cones, spheres and tori ·
covered· 1 header claimed - algo.spatial-acceleration: Generic bounding-volume hierarchies ·
divergent· 33 headers claimed- reasoning: Acceleration structures here are internal to the algorithms that need them (sewing’s cell filter, the classifier’s bounds, the distance walk’s pruning) rather than a public generic BVH container library. The one consumer that wants a standing BVH over triangles is picking, which is outside the kernel (outside/crates/ogeom-select carries it). If a kernel algorithm ever needs a shared BVH, promoting select’s is the move.
- algo.validity: Diagnosing a shape’s validity, by entity, with named problems ·
covered· 12 headers claimed
ogeom-bool
- bool.booleans: The boolean operations: cut, fuse, common, section and split, exact and fuzzy, with argument checking ·
covered· 99 headers claimed - bool.cells: The cells builder: arbitrary take/remove over the fused arrangement ·
covered· 1 header claimed - bool.defeaturing: Removing a set of faces from a solid by extending and re-intersecting its neighbours ·
partial· 2 headers claimed- restriction: Inner-loop features (bores, bosses, pockets whose rim is a surviving face’s inner wire) remove in full generality: wire surgery, no re-intersection, the block back to the last bit. Band features (fillets and chamfers along an edge) close for any number of bands in one call, each running straight through with two end faces or wrapping with none, and bands meeting at a corner close together: each recovers its own crease, the corner is where one crease pierces the other’s side, and a wedge’s flush cap named with its band folds into the band’s crease; a tangent chain’s bands close together too (a stadium’s rim rounded in one call), the junction being the foot on either crease of the cross-section edge the two bands share, and a band across a circular crease’s seam read as one run about its own centre rather than its complement; a rebuilt face whose surface has no closed-form pcurves fits them by projection, tolerances widened by the measured offset. A rim blend (a drum’s top, a bore’s mouth, a boss’s seat) takes a whole ring out of its neighbours and closes on the circle they meet along: the neighbours’ surfaces tell that wound from a bore’s, whose faces never meet and whose rings are simply dropped, and a neighbour’s own outer boundary may be the ring, so a drum’s cap grows back to its rim. The wall’s seam reaches the recovered circle, which is where it is cut and what the seam extends to meet. Wires are spliced in the face’s own order, since a seam stands in its wire twice, and a gap that leaves and arrives at one vertex is wound the way the rim it replaces was. What stays refused by name: a wound whose sides meet in no curve, a removal that would leave a face with no boundary and no edge to grow to, and a gap the recovered edges do not bridge.
- bool.glue: Gluing shapes along known-coincident boundaries ·
divergent· 1 header claimed- reasoning: Subsumed, and recorded as a settled decision in docs/PLAN.md: the boolean’s same-domain unification already skips nothing it needs and unifies what glue would, so a separate glue mode would be a second spelling of the fuse with a faster wrong answer available. MakeConnected’s job (conformal multi-body assembly) falls out of the fuse plus history.
- bool.make-periodic: Making a shape periodic so instances tile without seam duplication ·
covered· 1 header claimed - bool.make-volume: Making volumes from a soup of faces: the arrangement’s closed cells ·
covered· 1 header claimed
ogeom-core
- core.progress: Cancellable, staged progress through long operations ·
covered· 4 headers claimed - core.tolerances: The tolerance vocabulary: confusion, angular, intersection, and per-entity widening ·
covered· 1 header claimed
ogeom-doc
- doc.appearance: Colours, layers, materials and textures, per shape and per sub-shape, with inheritance resolved ·
covered· 18 headers claimed - doc.application-bootstrap: The OCAF application object the exchange document hangs from ·
n/a· 1 header claimed- reasoning: ogeom-doc is deliberately not a label-and-attribute tree, so there is no framework application to bootstrap; a Document is constructed like any other value. The exchange-document capability itself (XCAFDoc) is in scope and audited separately.
- doc.colour-values: Colour values and their names ·
covered· 4 headers claimed - doc.pmi: Semantic PMI: dimensions, tolerances, datums, datum targets, and their presentation ·
covered· 30 headers claimed - doc.product-structure: Products, occurrences and instances with placements: the assembly tree ·
covered· 14 headers claimed - doc.properties: Names, user properties, validation properties, lengths and centroids to check a transfer by ·
covered· 5 headers claimed - doc.saved-views: Saved views and standalone note objects in the exchange document ·
covered· 10 headers claimed - doc.time-types: Dates and periods ·
n/a· 4 headers claimed- reasoning: Time types are the standard library’s; the one place the exchange layer writes a timestamp it formats a string. A kernel-owned date class would duplicate std::time to no end.
ogeom-fillet
- fillet.bitangent: Bi-tangent blend construction ·
divergent· 4 headers claimed- reasoning: Subsumed, per the settled decision in docs/PLAN.md: the 2D repertoire carries bi-tangency in the plane and the blend family’s own envelope carries it in space, so a separate bi-tangent constructor would be a third spelling of two existing ones.
- fillet.chamfers: Chamfering edges: symmetric, two-distance, distance-and-angle ·
covered· 1 header claimed - fillet.corners-2d: Filleting and chamfering the corners of planar wires ·
covered· 19 headers claimed - fillet.edge-blends: Blending edges: constant and variable radius fillets, rolling-ball, marched where no closed form exists ·
partial· 127 headers claimed- restriction: Single edges, tangent chains and full rims blend, constant and variable radius, and the corner where three blends meet is closed: docs/PLAN.md §B (B2) with §A (A6) behind it. The marched fillet carries the rolling ball to topology on seats no closed form speaks (a fitted seam between two analytic walls, a conic seat re-opened to its full loop), with the blend fitted through the ball’s own arcs and the legs melting on the hosts’ exact surfaces. An open seat runs out: where the crease ends at a wall the band runs on until the ball has left the solid and the cut trims it against the wall; where it ends at a split (a seam vertex, or a neighbouring blend’s own rail), the band is capped in the end section’s own plane, and two such blends meet cap to cap along the shared arc, both caps consumed, in either order. An L-bracket’s end rim blends over its re-entrant band as one tangent chain (line, the band’s own end arc, line), the ball rolling on the concave cylinder between the lines, to the closed form. A fill and a wedge asked together stop at each other, since only blends that round the same way run on through one another’s bands; whichever is asked first takes the corner, and both orders are exact. Cone, sphere and torus hosts march and blend: the chart inverted in closed form, a band’s connector fitted where no closed form lifts the chart segment, a looping seat with a corner at its join steered by a smooth refit of itself, the band’s first station re-solved on the apex column so a rim opened at the host’s own seam leaves no sliver; a full circular rim whose hosts are not a cap and its coaxial wall takes the march too. A fitted host marches: its chart inverts by projection, a closed patch’s seam wraps as a period does, the patch is continued past the face by a few radii so the ball can run out, a seat the boolean split into arcs is closed back through the neighbours the hosts share and marched as the loop it is, the rail loop slid into the host’s own window, and the wedge melts: a straight edge of a converted box, the crease round a converted post split at its seam, a converted cone’s rim, a converted drum’s rim. Under it a marched section the centripetal fit misses is fitted again by chord length, and a loop cut at a closed patch’s seam is closed on it; a melt the boolean still cannot resolve refuses by name. A rim beside a sphere’s pole whose rail passes over the pole blends with the sphere’s leg running from the rail to the pole, the rim cut from it. Owed there: a rail passing within a hair of the pole, and a bore whose wall toward the pole is thinner than the ball. A seat whose hosts turn tangent at an end of the crease (the seam of two equal drums crossing, at the points where they touch) pinches there: the stations are solved one by one toward the pole, the pole’s collapsed section closes the band, and the wedge has no cap at that end. A tangency inside a crease with none at its ends is refused by name. A straight crease whose faces are planes or drums parallel to it (a wall meeting a drum along a ruling) blends exactly: the section swept straight, its band a drum of the fillet’s radius. A vertex where a curved face meets rounds with the one ball touching its three surfaces, its centre walked to a radius in from each and its compartment bounded by the planes through the centre and each pair of touch points, so each band’s end section is the ball’s own;
fillet_edgescloses such a corner, and the bands one at a time with the corner tool after agree with it to rounding when the ruling’s band goes first, or when the rim arc’s band goes first: the straight band’s section through the rim’s torus runs tangent to the torus’s end meridian into the corner, and ends at that vertex. The straight band before the rim arc’s closes too when the ruling’s band follows. Owed: the ruling’s band before the rim arc’s after the straight one, where the rim’s section through the point on top that the cap, the straight band and the torus all touch leaves a stub the corner tool’s cut does not meet. The corner tool rounds any convex planar vertex. Where one ball touches every face (a square or oblique trihedral corner, a square pyramid’s four-edged apex), its block is the polyhedron of the N host planes and the N planes through the ball’s centre square to the edges, where each band’s rim and the ball’s coincide. Where no single ball does (a rectangular pyramid’s apex, an irregular pentagonal one), the region the ball’s centre may occupy has a tip of several vertices joined by ridges, and the corner is the exact envelope of the rolling ball: a sphere at each tip vertex, cut with its own compartment, and the flush fillet of a virtual crease along each ridge, the compartments meeting cap to cap on the planes square to the ridges; a ridge seven microns long is kept as the sliver of cylinder it is. At more than three edges the corner goes first and the flush fillets follow, since bands built before the corner crash into each other at the apex. The ball’s pole stands along a ridge where there is one, its seam turned from the corner, and otherwise the tool is offered on each of the corner’s 2N labellings in turn, the first that closes standing; the boolean closes every labelling of an oblique corner and of a square pyramid’s apex, one solid each time. The flush fillets follow at a sharp apex too, and after a corner whose sphere clears a fourth plane by a few hundredths of a millimetre, where the envelope keeps a sliver of that plane beside the patch and the band meeting the sliver runs a straight end tangent to the sphere’s rim: a line and a circle share no stretch, so the tangency splits neither. The curved-seat corner is closed in either order: throughfillet_edgesa marched blend and a straight one meet at their corners, the later’s run-out walking on under the earlier band, and the boolean decides once per shared edge piece whether it is dust. A sub-piece of one edge lies in several charts, each with its own snap, so deciding once means a sliver that one chart collapses is collapsed in every chart. Where three or more edges of onefillet_edgescall meet at a vertex, the corner tool closes it with the rolling ball’s patch before the bands, which stop flush against it: a box corner’s sphere octant, a pyramid apex’s sphere or spheres and cylinders; a corner the tool does not speak keeps the bands’ caps.
- restriction: Single edges, tangent chains and full rims blend, constant and variable radius, and the corner where three blends meet is closed: docs/PLAN.md §B (B2) with §A (A6) behind it. The marched fillet carries the rolling ball to topology on seats no closed form speaks (a fitted seam between two analytic walls, a conic seat re-opened to its full loop), with the blend fitted through the ball’s own arcs and the legs melting on the hosts’ exact surfaces. An open seat runs out: where the crease ends at a wall the band runs on until the ball has left the solid and the cut trims it against the wall; where it ends at a split (a seam vertex, or a neighbouring blend’s own rail), the band is capped in the end section’s own plane, and two such blends meet cap to cap along the shared arc, both caps consumed, in either order. An L-bracket’s end rim blends over its re-entrant band as one tangent chain (line, the band’s own end arc, line), the ball rolling on the concave cylinder between the lines, to the closed form. A fill and a wedge asked together stop at each other, since only blends that round the same way run on through one another’s bands; whichever is asked first takes the corner, and both orders are exact. Cone, sphere and torus hosts march and blend: the chart inverted in closed form, a band’s connector fitted where no closed form lifts the chart segment, a looping seat with a corner at its join steered by a smooth refit of itself, the band’s first station re-solved on the apex column so a rim opened at the host’s own seam leaves no sliver; a full circular rim whose hosts are not a cap and its coaxial wall takes the march too. A fitted host marches: its chart inverts by projection, a closed patch’s seam wraps as a period does, the patch is continued past the face by a few radii so the ball can run out, a seat the boolean split into arcs is closed back through the neighbours the hosts share and marched as the loop it is, the rail loop slid into the host’s own window, and the wedge melts: a straight edge of a converted box, the crease round a converted post split at its seam, a converted cone’s rim, a converted drum’s rim. Under it a marched section the centripetal fit misses is fitted again by chord length, and a loop cut at a closed patch’s seam is closed on it; a melt the boolean still cannot resolve refuses by name. A rim beside a sphere’s pole whose rail passes over the pole blends with the sphere’s leg running from the rail to the pole, the rim cut from it. Owed there: a rail passing within a hair of the pole, and a bore whose wall toward the pole is thinner than the ball. A seat whose hosts turn tangent at an end of the crease (the seam of two equal drums crossing, at the points where they touch) pinches there: the stations are solved one by one toward the pole, the pole’s collapsed section closes the band, and the wedge has no cap at that end. A tangency inside a crease with none at its ends is refused by name. A straight crease whose faces are planes or drums parallel to it (a wall meeting a drum along a ruling) blends exactly: the section swept straight, its band a drum of the fillet’s radius. A vertex where a curved face meets rounds with the one ball touching its three surfaces, its centre walked to a radius in from each and its compartment bounded by the planes through the centre and each pair of touch points, so each band’s end section is the ball’s own;
- fillet.osculating-cache: Cached osculating surfaces along a blend’s tangency curves ·
divergent· 1 header claimed- reasoning: An implementation detail of the reference’s blend pipeline: it caches osculating approximations to march against. The marching blend here solves the ball’s two contact points directly at each section (ogeom-fillet’s march module), so there is no cache to keep coherent.
ogeom-geom
- geom.adaptors: Evaluating topology as geometry: an edge’s curve and a face’s surface, placed ·
covered· 22 headers claimed - geom.conversion: Converting between curve and surface forms: to B-spline, to Bézier segments, degree elevation ·
covered· 40 headers claimed - geom.curves-2d: Parametric plane curves, the pcurve vocabulary ·
covered· 18 headers claimed - geom.curves-3d: Parametric space curves: lines, conics, Bézier, B-spline, trimmed, offset ·
covered· 21 headers claimed - geom.extension: Extending curves and surfaces beyond their domains ·
covered· 13 headers claimed - geom.handle-wrappers: Geometry-layer wrappers for points, vectors, placements and transforms ·
divergent· 16 headers claimed- reasoning: The reference wraps every gp value in a reference-counted handle class so geometry can sit in documents. Here geometry lives in shared arenas keyed by id (docs/DATA_MODEL.md), and points, vectors and placements are plain values. A second, handle-shaped copy of the gp vocabulary would exist only to be a different allocation discipline. The capability those wrappers deliver is the arena’s.
- geom.local-properties: Local properties along curves and across surfaces: tangent, normal, curvature ·
covered· 30 headers claimed - geom.surfaces: Parametric surfaces: planes, quadrics, swept, Bézier, B-spline, trimmed, offset ·
covered· 26 headers claimed
ogeom-heal
- heal.canonical-simplification: Recognizing that exact geometry is secretly analytic: a B-spline that is a cylinder ·
covered· 1 header claimed - heal.custom-remodelling: Rebuilding a shape’s geometry wholesale: baking transforms, converting representations ·
covered· 12 headers claimed - heal.fix-shape: Fixing broken shapes: wires, faces, shells, solids, free bounds, small features ·
partial· 35 headers claimed- restriction:
fix_shapeis the standalone entry point: diagnose, put a wire’s edges end to end where an order exists, collapse edges shorter than their own vertices’ tolerances, fit missing pcurves, sew loose faces, tighten tolerances, restore tolerance containment by widening what is bounded (restore_containment, which the STEP and IGES readers run on every body they build), diagnose again, and report what it did and what remains. Beneath it: reanchoring periodic rings, sewing, validity diagnosis, the reader’s inline heal sequence, and the instructed fixes: fix_face_pcurves fits the trims the reader refused at a caller’s cap, and reanchor_boundaries moves a boundary onto the surface it bounds with the displacement recorded in widened tolerances, both measured on community assemblies with boundaries millimetres off. fix_small_faces collapses a spot face to a point and a strip face to one long side, rebuilding its neighbours so the shell stays closed; remove_small_solids drops debris solids under a volume. A STEP grid of patches reads as one spline surface. Healing here is measured by the imported corpus rather than claimed in general.
- restriction:
- heal.same-parameter: Diagnosing and repairing the same-parameter law between a curve and its pcurves ·
covered· 26 headers claimed - heal.scripted-pipeline: Resource-file-driven sequences of healing operators ·
divergent· 11 headers claimed- reasoning: The job (run a heal sequence on import) exists and is done with a fixed inline sequence in the exchange readers, measured by the corpus. A pipeline scripted from resource files is configuration the applications that need it can build from the same functions; the kernel keeping a config-file interpreter would be an application affordance.
- heal.status-reporting: Statuses, messages and traversal support for the healing pipeline ·
divergent· 7 headers claimed- reasoning: Healing outcomes here are values:
SameParameterReport,StepReport, counts fromreduce_tolerances. These are Rust Results rather than status bitfields read back through a registrator. A message-registration framework would add a second channel for what the return values already say.
- reasoning: Healing outcomes here are values:
- heal.substitution: Recording shape substitutions and applying them across a model ·
covered· 6 headers claimed - heal.tolerances: Reading and tightening the tolerances a shape actually needs ·
covered· 2 headers claimed - heal.upgrade: Upgrading shapes in place: same-domain unification, edge merging, subdivision ·
covered· 34 headers claimed
ogeom-hlr
- hlr.projection: Projecting a model into a view with visibility classified: hidden line removal, exact and polygonal ·
covered· 77 headers claimed
ogeom-intersect
- intersect.analytic-sections: Closed-form intersections of planes and quadrics ·
covered· 8 headers claimed - intersect.curve-curve: Intersecting two curves, in the plane and in space: crossings, tangencies, overlaps ·
covered· 26 headers claimed - intersect.curve-surface: Intersecting a curve with a surface: piercings and lying segments, with transitions ·
covered· 7 headers claimed - intersect.extrema: Extrema between two curves, a curve and a surface, or two surfaces ·
covered· 28 headers claimed - intersect.surface-surface-march: The general surface/surface intersection: seeding, marching, branch assembly, approximation ·
covered· 82 headers claimed
ogeom-io
- io.exchange-framework: The exchange session framework: interface models, transfer processes, selections, work sessions ·
divergent· 215 headers claimed- reasoning: The reference decouples file model from transfer through a session framework (Interface models, Transfer processes and actors, IFSelect work sessions), largely so many formats and an interactive shell can share one machinery. Readers here parse the file model and build the document directly; what a session would report lives in the returned reports, and there is no interactive shell to serve. The capability the framework delivers to an application (read the file, know what happened) is the readers’ contract.
- io.iges: IGES, both directions: solids as manifold B-rep, surface files sewn, units converted ·
partial· 128 headers claimed- restriction: The core entity set reads and writes (30 entity types, the
figure
tools/parity.py exchangeregenerates from the module’s own table), and eight round-trip cases hold it to measured volumes: planes, a periodic cylinder wall with its seam, a doubly periodic torus, a seam-only sphere, a boolean result, a spline-walled prism through 126/128, and inch-unit scaling. The reader also takes every axis-aligned conic arc (104: ellipse, hyperbola, parabola, each as its own curve), ruled surfaces (118) as the degree-one patch between the two curves’ exact spline forms, and constant offset curves and offset surfaces (130, 140). A conic whose axes turn is read in the frame where its cross term vanishes and turned back, exactly; parametric spline surfaces (114) convert exactly to bicubic B-splines; an offset curve whose distance varies (linearly with arc length, or as a function curve’s coordinate) is fitted same-parameter with its base; and a trim given only in a B-spline surface’s parameters is lifted through the surface. Model-space annotation (notes, leaders, labels, symbols and the linear, radial, diametral, angular and ordinate dimensions) reads as PMI callouts of the lines the file draws, a dimension whose text states a number carrying that value as a semantic dimension. Subfigure instances (408) place their definition (308), built once and shared under each instance’s placement, translation and scale; levels (with 406 level lists) and groups (402) read as the document’s layers. Constructive solids read as the solids they describe: the block, wedge, cylinder, cone frustum, sphere, torus and ellipsoid primitives (150 to 168), solids of revolution and linear extrusion, boolean trees (180) through the kernel’s own booleans, solid assemblies (184) and solid instances (430). A trim given only in the parameters of a plane, cylinder, cone, sphere or torus is lifted through the format’s parameterization, each surface framed by the reference direction it names; on a tabulated cylinder (fractions of its directrix and generator) and a surface of revolution whose generatrix is a line or a spline (the generatrix’s own parameter and the angle turned). Refused by name: a trim on a surface of revolution of another generatrix, whose own IGES parameter this reader does not carry; a drawing’s own annotation stays on the sheet and is counted as skipped.
- restriction: The core entity set reads and writes (30 entity types, the
figure
- io.mesh-formats: The mesh exchange formats: STL, OBJ, PLY, glTF/GLB, 3MF, with welding on import ·
covered· 69 headers claimed - io.native-format: The native shape interchange format, versioned, with location and triangulation sets ·
covered· 24 headers claimed - io.step: STEP, both directions: shapes, assemblies, colours, validation properties, semantic and presentation PMI ·
covered· 211 headers claimed - io.vrml: VRML scenes ·
covered· 102 headers claimed
ogeom-math
- math.analytic-carriers: The analytic curve and surface carriers: lines, conics, quadrics ·
covered· 15 headers claimed - math.bspline-basis: The B-spline basis: knots, evaluation, derivatives, insertion, elevation, splitting ·
covered· 14 headers claimed - math.curve-constructors: Constructing curves from constraints: through points, from centre and radius, trimmed arcs ·
covered· 59 headers claimed - math.elementary-evaluation: Evaluating and parameterizing the elementary curves and surfaces in closed form ·
covered· 2 headers claimed - math.equation-solving: Roots and minima of functions and systems: Newton, Brent, bisection, polynomial roots, global minima ·
covered· 31 headers claimed - math.expressions: A symbolic expression interpreter ·
n/a· 68 headers claimed- reasoning: A symbolic algebra layer serves parametric applications (dimension formulas, feature trees), not the geometry kernel; nothing in the modelling pipeline evaluates an expression tree. An application wanting formulas brings its own interpreter, as the consumers surveyed do.
- math.linear-algebra: Dense and sparse linear systems, least squares, eigenvalues ·
divergent· 16 headers claimed- reasoning: Dense linear algebra is nalgebra’s, on purpose: the workspace is generic over RealField so extended-precision scalars can be swapped in (docs/DATA_MODEL.md), and reimplementing SVD/LU under that constraint buys nothing but bugs. What is ours is what the reference lacks a direct twin for: the sparse matrix and conjugate-gradient path the fitting pipeline uses (ogeom_math::SparseMatrix, ogeom_math::least_squares_cgnr).
- math.primitives: Points, vectors, directions and frames, in the plane and in space ·
covered· 17 headers claimed - math.quadrature: Numerical integration ·
covered· 7 headers claimed - math.tangency-constructions: 2D tangency constructions: circles and lines tangent to points, lines and circles ·
covered· 58 headers claimed - math.transforms: Rigid and general transforms, quaternions, and their interpolation ·
covered· 12 headers claimed
ogeom-mesh
- mesh.editing: Editing triangulations: connectivity, welding, decimation, boundary loops ·
covered· 8 headers claimed - mesh.hatching: Hatching faces: iso and free-direction line families clipped to the trim ·
covered· 20 headers claimed - mesh.shape-wrapping: Wrapping triangulations and point clouds as shapes ·
n/a· 3 headers claimed- reasoning: Faces whose geometry is a triangle set, point-cloud stand-ins and preview boxes exist to feed viewers progressively; a viewer consumes the triangulation directly here. A solid built on a mesh, its surfaces recognized, is algo.solid-from-mesh.
- mesh.tessellation: Triangulating shapes to a stated deflection, deterministically, in parallel ·
covered· 91 headers claimed
ogeom-offset
- offset.draft: Drafting faces about a neutral plane for mould release ·
partial· 8 headers claimed- restriction: Planar faces turn about their neutral line, walls of revolution (cylinders and cones) turn about their neutral circle into exact cones, and extruded walls (a spline profile swept straight) turn ruling by ruling about their neutral crossing’s own tangent and re-fit, with the drafted angle held along the height. Every other face (a raw fitted patch, a wall of revolution about an oblique neutral) is drafted the way a mould-maker drafts: the ruled surface through the face’s crossing with the neutral plane, rulings the pull turned by the angle, read off the face’s own mesh and corrected onto the surface. A draft whose turned rulings cross inside the drafted window (a profile curled tighter than the draft’s reach) is refused by name, as is a face the neutral plane crosses twice or not at all.
- offset.filling: Filling a boundary with a face: the plate surface ·
covered· 32 headers claimed - offset.form-features: The form features: prism, revolution, rib and slot against a base ·
covered· 39 headers claimed - offset.loft: Lofting through sections, ruled or smoothed ·
partial· 1 header claimed- restriction: The ruled loft takes two closed wire sections (coaxial parallel circles, or polygons of the same corner count whose ruled walls come out planar) or a section and a point: a cone on a circle’s own axis, an exact pyramid over any straight loop. The skinned loft takes N closed sections, planar or not (a planar end is capped by its plane, a wavy end by a patch skinned from the rim to a point inside it), ends at a point on request, loops back on itself, and takes per-section alignment hints. A ruled wall between two segments that are not coplanar is the bilinear patch through its four corners, exact, so polygon sections may be turned against each other; mixed edge counts are authorship, not geometry, and stay with the skinned resampling. The skinned loft holds its sections exactly where they are exact: through two sections it is the ruled loft; through more with matching corners, one strip per edge meeting along seams through the corners, a plane wherever its rows share one; through coaxial circles, the revolution of a meridian through their radii.
- offset.middle-path: Extracting the middle path of a pipe-like solid ·
covered· 1 header claimed - offset.projection: Projecting wires normally onto faces, pcurves riding along ·
covered· 2 headers claimed - offset.shell-thicken: Offsetting shapes and thickening shells into solids ·
covered· 16 headers claimed - offset.sweeps: Sweeping profiles along spines: pipes, pipe shells, evolved shapes, the frame laws ·
partial· 120 headers claimed- restriction: Pipes run a circular section along a single spine edge, exactly for straight and circular spines and skinned for free-form and helical ones. The evolved sweep runs a profile along a planar spine, exactly, by composition. The pipe shell sweeps an arbitrary planar profile, holes and all, along an open spine wire under a rotation-minimizing or Frenet frame law, and it also sweeps a profile round a closed spine, smooth or faceted, holes and all: the loop’s holonomy paid off, a faceted profile skinned as one C1-closed strip per facet, each hole a void tunnel of its own shell. A sharp-cornered ring mitres exactly when its corners turn in the plane: the wrap is one more mitre, the seam must stand on a corner, and the planar Pappus volumes land to the last digit. The Frenet law rides the loop too (single-valued round it, so it owes no reconciliation), with every corner loop one rail shared by the two strips meeting there. A ring seamed mid-leg butts its two half-legs on the seam’s own ring, and a skew-cornered ring closes on its mitres: the frame is reflected across each mitre plane, the loop’s holonomy spread along the legs as a twist. A corner against a curved leg, on an open spine or a ring, ends both walls on the crossing of their generators, exact where the corner turns in the leg’s plane. A skew corner against a curved leg keeps the reflected frame and mitres in pieces: each side is swept on straight past the corner, trimmed by the mitre plane, and the pieces fused, the difference between the two sections standing as a face of the mitre plane; a closed wire profile sweeps there as the walls of the face it bounds, the end caps taken off. Each spine edge skins its own run, so a spine whose curvature steps at a smooth join (an arc into its tangent line) is followed exactly, and a run whose sections share a plane is that plane. A spine of lines and arcs meeting tangent is swept exactly: each line leg an extrusion of the section, each arc leg its revolution about the arc’s axis, the legs fused, so every wall is the plane, drum, cone, ball or torus its profile edge sweeps.
make_helical_sweepis the screw motion of a profile in a plane through the axis, cylindrical or tapered, either hand: every point runs its own helix and the walls are fitted through those images in quarter turns.
- restriction: Pipes run a circular section along a single spine edge, exactly for straight and circular spines and skinned for free-form and helical ones. The evolved sweep runs a profile along a planar spine, exactly, by composition. The pipe shell sweeps an arbitrary planar profile, holes and all, along an open spine wire under a rotation-minimizing or Frenet frame law, and it also sweeps a profile round a closed spine, smooth or faceted, holes and all: the loop’s holonomy paid off, a faceted profile skinned as one C1-closed strip per facet, each hole a void tunnel of its own shell. A sharp-cornered ring mitres exactly when its corners turn in the plane: the wrap is one more mitre, the seam must stand on a corner, and the planar Pappus volumes land to the last digit. The Frenet law rides the loop too (single-valued round it, so it owes no reconciliation), with every corner loop one rail shared by the two strips meeting there. A ring seamed mid-leg butts its two half-legs on the seam’s own ring, and a skew-cornered ring closes on its mitres: the frame is reflected across each mitre plane, the loop’s holonomy spread along the legs as a twist. A corner against a curved leg, on an open spine or a ring, ends both walls on the crossing of their generators, exact where the corner turns in the leg’s plane. A skew corner against a curved leg keeps the reflected frame and mitres in pieces: each side is swept on straight past the corner, trimmed by the mitre plane, and the pieces fused, the difference between the two sections standing as a face of the mitre plane; a closed wire profile sweeps there as the walls of the face it bounds, the end caps taken off. Each spine edge skins its own run, so a spine whose curvature steps at a smooth join (an arc into its tangent line) is followed exactly, and a run whose sections share a plane is that plane. A spine of lines and arcs meeting tangent is swept exactly: each line leg an extrusion of the section, each arc leg its revolution about the arc’s axis, the legs fused, so every wall is the plane, drum, cone, ball or torus its profile edge sweeps.
- offset.wire-offset: Offsetting planar wires, with the join styles ·
covered· 1 header claimed
ogeom-topo
- topo.data-model: The B-rep data model: shapes as (node, location, orientation) handles into shared arenas ·
covered· 36 headers claimed - topo.identity: The same / equal / partner identity trichotomy, each with a matching hasher ·
covered· 2 headers claimed - topo.location-chain: Placement as a chain of transforms, so instancing shares geometry ·
covered· 4 headers claimed - topo.multi-representation-edges: Edges and vertices carrying several representations: space curve, pcurves per face, polygons on triangulations ·
covered· 16 headers claimed - topo.shared-state-locking: Mutex provision for shapes shared across threads ·
n/a· 1 header claimed- reasoning: Exclusive access is the borrow checker’s job: mutation needs
&mut Model, which is the lock, held at compile time. A class that hands out mutexes per shape has no counterpart because the failure it guards against does not compile here.
- reasoning: Exclusive access is the borrow checker’s job: mutation needs
- topo.traversal: Exploring a shape: filtered descent, unique enumeration, ancestry maps ·
covered· 3 headers claimed - topo.triangulation-store: Triangulations as first-class model data: triangles, polygons on them, parameters ·
covered· 8 headers claimed
Exchange entities (collapsed)
One row per entity package; the coverage figures regenerate from the
entity tables in ogeom-io and are audited in tranche 3, not per
header.
| rule | package | headers |
|---|---|---|
| C-IGES | IGESAppli | 49 |
| C-IGES | IGESBasic | 46 |
| C-IGES | IGESDefs | 22 |
| C-IGES | IGESDimen | 55 |
| C-IGES | IGESDraw | 37 |
| C-IGES | IGESGeom | 57 |
| C-IGES | IGESGraph | 39 |
| C-IGES | IGESSolid | 62 |
| C-STEP | RWStepAP203 | 11 |
| C-STEP | RWStepAP214 | 29 |
| C-STEP | RWStepAP242 | 4 |
| C-STEP | RWStepBasic | 117 |
| C-STEP | RWStepDimTol | 49 |
| C-STEP | RWStepElement | 15 |
| C-STEP | RWStepFEA | 52 |
| C-STEP | RWStepGeom | 83 |
| C-STEP | RWStepKinematics | 74 |
| C-STEP | RWStepRepr | 67 |
| C-STEP | RWStepShape | 91 |
| C-STEP | RWStepVisual | 97 |
| C-STEP | StepAP203 | 41 |
| C-STEP | StepAP209 | 1 |
| C-STEP | StepAP214 | 80 |
| C-STEP | StepAP242 | 6 |
| C-STEP | StepBasic | 166 |
| C-STEP | StepDimTol | 76 |
| C-STEP | StepElement | 67 |
| C-STEP | StepFEA | 87 |
| C-STEP | StepGeom | 115 |
| C-STEP | StepKinematics | 86 |
| C-STEP | StepRepr | 94 |
| C-STEP | StepShape | 127 |
| C-STEP | StepVisual | 177 |
Dropped headers, and on whose authority
Regenerate the full list with apisurf scope; sample it with
--sample N. A dispute with a rule is a change to apisurf.py, and
shows up as a diff naming exactly which headers moved.
| rule | headers | justification |
|---|---|---|
| R1 | 643 | CDL generic instantiations: container words, The-/My- internals, chained Ofs. Rust generics express every one of these without a named type per instantiation; there is no capability to audit. |
| R2 | 237 | The dedicated container packages (NCollection, TCol*, TCollection, TShort). Same ground as R1, package-shaped. |
| R3 | 236 | Operating-system and infrastructure surface: memory, threads, signals, streams, strings, resources, persistence plumbing. The standard library’s job. Cancellable progress is carved out and sits in the keep pool (V-PROGRESS). |
| R4 | 242 | The TopOpeBRep* boolean family, superseded inside the reference by the BOPAlgo pipeline. Compatibility residue is not parity. |
| R5 | 36 | OCAF persistence drivers inside DataExchange. The exchange document is in scope; the framework’s serialization of it is not. |
Integration and bindings
Included verbatim from
docs/INTEGRATION.md:
what embedding a kernel into a real application demands, and the rules
that keep it possible without letting it drive the design.
Integration
ogeom’s product is its own API. Integration layers for other languages and host applications are downstream and optional. None of them constrains the kernel’s design.
This file has one purpose: to record what embedding a kernel in a real application requires, so that today’s decisions do not rule it out by accident. Nothing here should drive work. It can only veto a design that would make embedding impossible.
The design pressure that matters
We surveyed a large application that embeds a B-rep kernel (about 4,100 call sites). One finding applies beyond that application:
Consumers do not just call a kernel. They extend it. The survey found seven classes that derive from kernel types and override their virtual methods:
- a shape subclass that intercepts every mutating member to keep an element map consistent;
- several operation subclasses;
- custom message and progress sinks.
Two consequences:
- A pure C ABI is never enough for a host that wants to specialise kernel behaviour. A C++ integration layer will need real classes with real virtual dispatch on top of the FFI surface.
- Extension points belong in the kernel’s design, not in the shim. Progress reporting, cancellation, diagnostics, custom tolerance policy and history observation should be traits in the Rust API. If a host has to subclass to reach them, the design is wrong.
The second point is the actionable one. It is kernel work, not integration work.
What the kernel must already do
DATA_MODEL.md covers all of these. Each row gives the consequence of getting
it wrong.
| Requirement | § | Consequence of getting it wrong |
|---|---|---|
Shape is (tshape, location, orientation) and cheap to copy | §1 | Every by-value shape parameter in every host becomes an allocation |
| Location is a chain, not a flat matrix | §2 | Assembly instancing breaks; placement identity can no longer be decided structurally |
| Orientation composes on descent | §3 | Face normals flip inconsistently. This is silent, and catastrophic downstream |
is_same / is_equal / is_partner with matching hashers | §4 | Shape maps use the wrong keys. Silent wrong answers |
| Per-entity tolerances with the containment rule | §5 | Imported geometry cannot be modelled with at all |
| Edges carry a representation list including per-face pcurves | §6 | Boolean face splitting has nothing to split with |
generated / modified / is_deleted on every operation | §7 | Downstream naming breaks silently and corrupts user documents |
| Stable provenance | §8 | References into a rebuilt model cannot be resolved at all |
Result, no exceptions, no signal conversion | §12 | Failures cannot be mapped cleanly into a host’s error model |
Watch the history row most closely: it is the only failure above that is silent. A parametric application records “fillet that edge” and, after a rebuild, finds the edge again by walking history. Incomplete history does not raise an error. It reopens the document with the wrong faces filleted.
Planned integration layers
None of these is scheduled. They are listed so their requirements stay visible.
| Layer | Notes |
|---|---|
C ABI (og-capi) | The base for everything else. Opaque handles, POD structs, explicit ownership. Straightforward once the native API is stable. |
| Python | By far the most valuable binding: it is how most people would try the kernel. Built with PyO3 over the native API, not over the C ABI. |
| C++ | Real classes with virtual methods over the C ABI, for hosts that want to specialise behaviour. |
| WASM | The kernel is pure Rust with no C dependencies, so this is nearly free. Keep it that way: weigh WASM before adding any dependency that could break it. |
Drop-in replacement for another kernel’s headers. This is technically possible: a source-compatible façade that exposes another kernel’s class names and signatures, built into libraries with the names that kernel’s build-system probes expect, so a consumer recompiles without code changes. It is feasible, large, and firmly a downstream project. It is a poor thing to design toward, for two reasons:
- it imports the other kernel’s mistakes wholesale, including the
pointer-identity model that
DATA_MODEL.md§8 exists to avoid; - some things cannot be supported at all. A host API that passes a raw shape pointer to a third-party binding runtime needs binary layout compatibility. That is not a goal, and pursuing it would damage the design.
Rules that protect integration without constraining the kernel
- The native Rust API is designed for Rust. No parameter exists just because a binding might want it.
- Extension points are traits in the native API: progress, cancellation, diagnostics, tolerance policy, history observation.
- No public type is compromised to make it representable in C. The C ABI deals in handles; that is its job.
- No dependency that would break WASM or require a C toolchain, unless an explicit decision is recorded here.
Contributing
Included verbatim from
CONTRIBUTING.md.
The independence rule and the correctness bar are the two sections to
read before anything else.
Contributing to ogeom
Independence
ogeom is an independent implementation. It does not depend on, vendor, link
against, bundle or commit any existing CAD kernel, and nothing in this
repository may pull one in. cargo build needs a Rust toolchain and nothing
else: no C compiler, no system libraries, no submodules. Keep it that way.
Never take another kernel’s code into this one. Not as a dependency, a
vendored subtree, a file, or a fragment pasted into a function. Nothing in
crates/ may be part of someone else’s build.
Do not build a copy of another kernel. This is the rule the others serve. A kernel that mirrors another’s class hierarchy, decomposition, call graph and file layout is a translation with different identifiers. That structure is what we avoid, not the field’s vocabulary. What you may take from a reference implementation is understanding: what an algorithm must handle, which cases exist, what a format’s records mean. The design itself is worked out here.
Use the field’s vocabulary. A boundary representation is a b-rep, a point
in the plane is a Point2, a curve in a surface’s parameters is a pcurve, a
blend is a fillet. Private jargon would make the kernel harder to read for no
gain. docs/PLAN.md and docs/DATA_MODEL.md use conventional names on
purpose. Naming a concept is not naming a dependency.
Do not name another kernel in anything committed. The vocabulary belongs to the field; product names do not belong to us. Where a format needs its own magic bytes to be readable, those bytes are data and are committed as data.
The legal side
- Most existing kernels are copyleft. ogeom is MIT OR Apache-2.0.
- Renaming identifiers does not stop a work being derived from another. Copyright follows the expression, not the names. That is why the rule above is about structure, not words.
- File formats are the safe case. A file format is not copyrightable, and
implementing one from its published description is interoperation. The STEP
and
.brepsupport were both built this way.
Sources, in order of preference
- Published algorithm specifications and papers: Shewchuk on robust predicates, Piegl & Tiller on NURBS, the marching-intersection and surface-surface literature, the published specifications for boolean pipelines.
- Format standards: ISO 10303 for STEP, and the rest.
- First principles, and your own tests.
A local reference checkout goes under vendor/ or reference/. Both are
gitignored. A checkout is never a build or test dependency, and it is never
named in anything committed.
If you have contributed to another CAD kernel’s source, say so in your pull request, so we can be careful about which areas you work on.
Scope
Parity with the reference kernel’s modelling modules (FoundationClasses, ModelingData, ModelingAlgorithms, DataExchange) and nothing else. Visualization, the application framework and the test harness are out.
docs/SCOPE.md is normative. It states the rule, how to decide a case
mechanically from the reference tree’s own module and toolkit files, and what
the rule does not mean.
Two points are repeated here because both have been misread before:
- Parity is about capability, not structure. It does not allow mirroring
another kernel’s class hierarchy or decomposition; Independence above still
holds in full. Where we deliberately do a job differently, the parity record
says
divergentand gives the reasoning. That is an answer, not a gap. - Usage data sets the order of work, never the scope.
docs/api_surface.jsonprofiles how one application uses the reference kernel, which tells us what to get right first. A capability inside the four modules is in scope whether or not that application ever calls it.
Invariants cannot be changed in a pull request
docs/DATA_MODEL.md is normative. A change that breaks one of its invariants
is a design change and must be argued as one, not slipped in. Examples:
flattening the location chain, giving an edge a single curve, adding an
operation that does not emit history, conflating same/equal/partner.
The reason is practical. Each invariant is cheap to keep now and effectively impossible to add back later across all of a kernel’s algorithms. Each one has its concrete failure mode written next to it.
Correctness
Geometry code fails quietly. A boolean that returns a plausible but wrong solid does not throw; it corrupts a document six operations later. So:
- State the property, then test it. Round-trips, composition laws, tolerance
containment, orientation consistency, antisymmetry of predicates. Property
tests over laws are worth more than many examples. See
crates/ogeom-core/tests/properties.rs. - Validate against ground truth you can compute independently. Analytic results for analytic inputs, closed-form volumes and areas, known benchmark datasets. “It looks right in the viewer” does not count.
- There is no external oracle. Comparing against another kernel is not possible, because it would mean vendoring one. Instead, check against closed forms, round trips, invariants (volume, area, validity), and properties over random inputs. The cheapest useful check is usually: build the same result two ways and confirm they agree.
- Never loosen a tolerance to make a test pass without explaining why in the same commit. Numerical tolerances in tests are explicit and justified in a comment.
- Failures are values. An algorithm that did not converge returns that fact. It does not return an empty shape and set a flag.
Practical rules
- Comments describe current behaviour, never the change that produced it.
- No “used to”, “formerly”, “now returns”, “since X landed”.
- No issue, PR or commit references as the reason for a behaviour. Those belong in the commit message.
node tools/lint-comment-rot.mjs --allenforces this intools/check.sh.--pedanticadds an advisory tier. Putlint-comment-rot: ignoreon a line to exempt it where a reference is genuinely needed.- The tracked pre-commit hook runs the lint over the lines a commit adds.
Enable it once per clone with
git config core.hooksPath .githooks.
- Run
./tools/check.shbefore review. It runs formatting, lints, tests and docs, and repeats the test suite so a property test that fails only on some seeds does not slip through. Do not verify by grepping cargo’s output for “ok”: a run with a failing suite still prints “ok” for every suite that passed, so a real failure can hide behind a green-looking summary. - Workspace lints forbid
unsafe, and warn onunwrap/expectand lossy numeric casts in library code. A kernel is arithmetic from end to end, and these are how wrong answers get shipped. A deliberate exception needs an#[allow(..., reason = "...")]and a documented# Panicssection. - New dependencies need a permissive license (
deny.tomlenforces this). They must not require a C toolchain or break WASM. If you think an exception is warranted, raise it explicitly.- Record your decision there either way. The record stops the question being argued again.
- Public items are documented.
missing_docsis a warning, and CI runs with-D warnings.
Releasing
The fifteen library crates under crates/ are published together, at one
version, from [workspace.package]. The three crates under tools/ have
publish = false; they exist only for this repository.
cargo publish --dry-run --workspace # packages, verifies and orders, uploads nothing
cargo publish --workspace # the same, for real
Cargo works out the order from the dependency graph, so one command publishes all fifteen. Know these three points before changing the arrangement:
- Versions move together. Every crate inherits
version.workspace, and the internal requirements in[workspace.dependencies]are pinned to the same number. Bump both, or the workspace stops resolving. Also bump the requirements inoutside/Cargo.toml, because that workspace depends on these crates by version. ogeom-meshdev-depends onogeom-algoby path only.ogeom-algodepends onogeom-mesh, so a version requirement here would be a cycle no registry can resolve. Cargo drops a path-only dev-dependency from the published manifest, so the cycle stays inside this repository. Do not “tidy” it to.workspace = true.- The corpus is not published.
tests/corpus/sits at the repository root, outside every package, and each crate’sexcludelists the suites that read it. A published tarball contains only tests it can run. The repository andtools/check.shstill run all of them.