Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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 ogeom umbrella 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:

  • Model owns all data. Geometry, topology, tolerances and history live in one Model. Operations take &mut model. A Shape is 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.shape is the result. The rest of Built is 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 }
ValueMeaning
ForwardThe material is on the surface’s default side.
ReversedThe material is on the other side.
InternalThe boundary lies inside the material (for example, a stiffener edge embedded in a face).
ExternalThe 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_Orientation and TopAbs::Compose.


4. Identity has three levels

There are three distinct equivalences, with three distinct hashers. They are not interchangeable:

PredicateComparesUsed for
is_partnertshape only“Is this the same underlying topology, anywhere, either way round?”
is_sametshape + locationSet membership in most algorithms; the common case
is_equal (==)tshape + location + orientationExact 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 / IsEqual and the ShapeMapHasher family. 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 Tolerance on 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 = 0 and u = 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_CurveRepresentation and 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 / IsDeleted on 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.

ConstantValue at unit scaleMeaning
CONFUSION1e-7two points are the same point
ANGULAR1e-12two directions are parallel
INTERSECTIONCONFUSION * 1e-2intersection convergence
APPROXIMATIONCONFUSION * 1e1curve/surface fitting target
P_CONFUSIONCONFUSION * 1e-2parametric-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 Adaptor family. 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_parts assembles a restored document by replaying the file’s insertion order, which reproduces every handle.
  • Model::absorb uses 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 Failure hierarchy and, in at least one kernel, a facility that turns SIGSEGV into a catchable exception. We do neither.


Checklist for a new algorithm

  1. Written against the geometry traits (§10), not concrete types.
  2. Populates generated / modified / is_deleted (§7).
  3. Assigns provenance to every entity it creates (§8).
  4. Composes location and orientation correctly during traversal (§1, §2, §3).
  5. Uses the right identity predicate, with a matching hasher (§4).
  6. Keeps the tolerance containment rule, or fails loudly (§5).
  7. Keeps edge representations consistent, or clears same_parameter (§6).
  8. 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 sew merges 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_tolerances narrows 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:

LevelFunctions
Vertexmake_vertex
Edgemake_edge (on a curve), make_edge_between (between points)
Wiremake_wire (ordered edges), make_wire_unordered (any order), make_polygon (straight sides)
Facemake_face, make_face_on (over a surface), make_face_with_pcurves (when the boundary’s surface parametrisation matters)
Shell, solid, compoundmake_shell, make_solid, make_compound

Helpers:

  • sew stitches faces that share boundaries within tolerance into shells.
  • is_wire_closed and is_shell_closed check closure before you build the next level.

Sweeps and fitting

  • make_prism extrudes. make_revolution revolves.
  • 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.
  • interpolate fits a curve through points.
  • approximate and approximate_within fit a curve near points to a stated tolerance. The fit reports the deviation it achieved.
  • make_text renders 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

FunctionWhat it does
cellsFull 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_fuzzyTake 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_volumeBuilds the solids enclosed by an arbitrary set of faces.
remove_facesDefeaturing: 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_periodicPrepares 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);
FunctionResult
fillet_edgeConstant-radius round.
fillet_edge_variableRound with a radius law along the edge.
chamfer_edgeSymmetric flat bevel.
chamfer_edge_distancesAsymmetric bevel (two distances).
chamfer_edge_angleBevel 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_edges trims 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_vertex is 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_edges and chamfer_edges_with bevel 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_faces rolls a constant-radius ball between two faces that need not share an edge.
  • march_blend is 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

FunctionWhat it does
offset_shapeMoves a solid’s boundary along its normals (outward, or inward with a negative distance) and rebuilds the intersections where offset surfaces collide.
make_thick_solidShelling: removes the named faces, offsets the rest inward and joins them. Gives a hollow part with an opening.
offset_wire2D offset of a planar wire. The caller picks the Join style (arcs or intersections). Useful for tool-path-like outlines.
apply_draftTilts faces by a draft angle about a neutral plane, for moulded parts.

Sweeps

The general sweeps share this module’s machinery:

  • make_pipe and make_pipe_skinned: sweep along a single spine edge.
  • make_loft and make_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_rib and feature_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.

FunctionWhat 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_parameterRe-fits an edge’s pcurves until they agree with its 3D curve within tolerance. The report gives the achieved deviation per edge.
unify_same_domainMerges adjacent faces on the same surface and adjacent edges on the same curve. Undoes the fragmentation that exchange formats cause.
merge_edgesJoins chains of edges into single edges where the geometry allows.
reduce_tolerancesNarrows entity tolerances to what the geometry actually measures. This is the only operation that narrows tolerances, and it does so by re-measuring.
canonical_simplifyReplaces 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_ringsMoves the seam of periodic faces so later algorithms see a consistent parametrisation.
fix_shapeOne-call repair for a shape of unknown quality (see below). Returns a FixReport.
ReshapeThe primitive underneath: a recorded substitution of entities that rebuilds everything referencing them.

fix_shape steps

  1. Diagnose.
  2. Put each wire’s edges end to end.
  3. Collapse edges shorter than their own vertices’ tolerances.
  4. Fit missing pcurves.
  5. Sew loose faces.
  6. Tighten tolerances.
  7. Widen any vertex tighter than its edges (and any edge tighter than its faces) to restore the containment rule.
  8. 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 deflection of 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

FunctionReturns
distance_between_shapesMinimum distance and the ClosestPair that realises it.
project_on_curve, project_on_surface, project_on_planar_curveParameter and distance of a point’s projection onto the geometry.
project_edge_onto_planeAn edge projected onto a plane as an exact 2D curve: point, line, circle, ellipse or B-spline.
curve_lengthLength along a curve.
parameter_at_lengthParameter at a given arc length.
points_by_count, points_by_spacingPoints 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 _exact variants 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_of and polyline_of.
  • triangulate returns one welded mesh for a whole shape.
  • simplify decimates an existing mesh toward a Target.
  • hatch_face cross-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).

  1. Call edge_chords_for once per shape to fix the shared edge points.
  2. Call triangulate_face_with for 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).

  • project takes shapes and a view direction and returns a Drawing of DrawnCurves. 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.

  • section cuts a shape with a plane and returns a SectionView with the cut face outlines, ready for hatching.

  • broken_section is 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 placed Occurrences. 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: Callout polylines, the drawn form.

Both forms, and the distinction between them, survive STEP.

Views and notes

  • A View is a named camera plus the callouts it shows. Annotated models use views to organise PMI into readable sheets.
  • A Note is 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.

OffsetWhat the reader does
Under 1 mmHeals it: fits the trim, widens the edge’s tolerance to the measured offset, and emits a warning.
Over 1 mmTreats 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_document and native::read_document round-trip the whole document (exact geometry, tolerances, structure, PMI, views, notes) with no loss. Use it between ogeom sessions.
  • brep::write and brep::read store a single shape as text, for model-level interchange.

Mesh and drawing formats

FormatReadWrite
STL (ascii and binary)yesyes
glTF / GLByesGLB
OBJyesyes
PLYyesyes
VRML (1.0 and 2.0)yesyes
3MF (deflated or stored, multi-part)yesyes
DXF (2D drawings)yesyes

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 partial row 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:

ModuleWhat it covers
FoundationClassesArithmetic, primitives, solvers, tolerances, errors.
ModelingDataThe geometry and topology vocabularies: curves, surfaces, the b-rep data model.
ModelingAlgorithmsIntersection, booleans, blending, offsets, sweeps, healing, tessellation, hidden-line removal.
DataExchangeSTEP, IGES, STL, VRML, OBJ, glTF, PLY, and the document structure these formats carry.

Three modules are permanently out of scope:

ModuleWhy it is out
VisualizationRendering, viewers, interactive selection. A kernel is not a renderer.
ApplicationFrameworkThe 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.
DrawA 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.toml excludes 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:

  1. adm/MODULES maps each module to its toolkits.
  2. src/<Toolkit>/PACKAGES maps each toolkit to its packages.
  3. src/<Package>/*.hxx are 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.toml lists 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:

VerdictMeansMust cite
coveredbuilt and testedsymbols that resolve in the built rustdoc, and tests that exist in the tree
partialbuilt, with a stated restrictionthe restriction in words, plus symbols and tests
divergentdeliberately differentthe reasoning
absentnot builta plan entry
n/aexcluded by a triage rulethe rule id
unreviewednot yet auditedcounted 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

verdictcapabilities
covered77
partial7
divergent9
n/a5

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_edges closes 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: through fillet_edges a 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 one fillet_edges call 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.
  • 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_shape is 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.
  • 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 from reduce_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.
  • 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 exchange regenerates 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.
  • 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_sweep is 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.
  • 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.
  • 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.

rulepackageheaders
C-IGESIGESAppli49
C-IGESIGESBasic46
C-IGESIGESDefs22
C-IGESIGESDimen55
C-IGESIGESDraw37
C-IGESIGESGeom57
C-IGESIGESGraph39
C-IGESIGESSolid62
C-STEPRWStepAP20311
C-STEPRWStepAP21429
C-STEPRWStepAP2424
C-STEPRWStepBasic117
C-STEPRWStepDimTol49
C-STEPRWStepElement15
C-STEPRWStepFEA52
C-STEPRWStepGeom83
C-STEPRWStepKinematics74
C-STEPRWStepRepr67
C-STEPRWStepShape91
C-STEPRWStepVisual97
C-STEPStepAP20341
C-STEPStepAP2091
C-STEPStepAP21480
C-STEPStepAP2426
C-STEPStepBasic166
C-STEPStepDimTol76
C-STEPStepElement67
C-STEPStepFEA87
C-STEPStepGeom115
C-STEPStepKinematics86
C-STEPStepRepr94
C-STEPStepShape127
C-STEPStepVisual177

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.

ruleheadersjustification
R1643CDL 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.
R2237The dedicated container packages (NCollection, TCol*, TCollection, TShort). Same ground as R1, package-shaped.
R3236Operating-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).
R4242The TopOpeBRep* boolean family, superseded inside the reference by the BOPAlgo pipeline. Compatibility residue is not parity.
R536OCAF 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:

  1. 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.
  2. 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§1Every by-value shape parameter in every host becomes an allocation
Location is a chain, not a flat matrix§2Assembly instancing breaks; placement identity can no longer be decided structurally
Orientation composes on descent§3Face normals flip inconsistently. This is silent, and catastrophic downstream
is_same / is_equal / is_partner with matching hashers§4Shape maps use the wrong keys. Silent wrong answers
Per-entity tolerances with the containment rule§5Imported geometry cannot be modelled with at all
Edges carry a representation list including per-face pcurves§6Boolean face splitting has nothing to split with
generated / modified / is_deleted on every operation§7Downstream naming breaks silently and corrupts user documents
Stable provenance§8References into a rebuilt model cannot be resolved at all
Result, no exceptions, no signal conversion§12Failures 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.

LayerNotes
C ABI (og-capi)The base for everything else. Opaque handles, POD structs, explicit ownership. Straightforward once the native API is stable.
PythonBy 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.
WASMThe 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

  1. The native Rust API is designed for Rust. No parameter exists just because a binding might want it.
  2. Extension points are traits in the native API: progress, cancellation, diagnostics, tolerance policy, history observation.
  3. No public type is compromised to make it representable in C. The C ABI deals in handles; that is its job.
  4. 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.

  • 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 .brep support were both built this way.

Sources, in order of preference

  1. 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.
  2. Format standards: ISO 10303 for STEP, and the rest.
  3. 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 divergent and gives the reasoning. That is an answer, not a gap.
  • Usage data sets the order of work, never the scope. docs/api_surface.json profiles 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 --all enforces this in tools/check.sh. --pedantic adds an advisory tier. Put lint-comment-rot: ignore on 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.sh before 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 on unwrap/expect and 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 # Panics section.
  • New dependencies need a permissive license (deny.toml enforces 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_docs is 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 in outside/Cargo.toml, because that workspace depends on these crates by version.
  • ogeom-mesh dev-depends on ogeom-algo by path only. ogeom-algo depends on ogeom-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’s exclude lists the suites that read it. A published tarball contains only tests it can run. The repository and tools/check.sh still run all of them.