|
OCCT-Light 0.1
C ABI and C++ veneer for multi-language CAD workflows
|
The single most consequential design decision in OCCT-Light: the public ABI is built on BRepGraph, not
TopoDS. This document explains why, what that means in C, and how algorithms that still needTopoDSare handled invisibly.
Companion docs: ARCHITECTURE.md · ABI_PATTERNS.md · MODULES.md · BINDINGS.md.
TopoDS_Shape is OCCT's classical tree-shaped topology (TopoDS_TShape* + TopLoc_Location). For a binding-driven wrapper it's a poor fit: identity is by pointer (not stable across operations), adjacency queries need auxiliary maps, metadata requires OCAF/XCAF, and geometry is per-shape rather than per-definition.
BRepGraph is OCCT's incidence-table DAG with explicit Definition vs Usage separation, typed NodeId/UID/RefId/RefUID identity, deduplicated representation storage, VersionStamp invalidation, and a Layer system for metadata. Read/write/traverse/sew/check/mesh/render are graph-native; Booleans, fillets, and offsets still round-trip through TopoDS internally — invisible to ABI users (see §12). Headers live in OCCT 8.0.0 under src/ModelingData/TKBRep/BRepGraph/ and src/ModelingData/TKBRep/BRepGraphInc/.
OCCT 8.0.0 splits the BRepGraph surface across toolkits: BRepGraph / BRepGraphInc are in TKBRep, BRepGraphAlgo / BRepGraphCheck are in TKTopAlgo, BRepGraphMesh is in TKMesh, BRepGraphDE is in TKDE, and BRepGraphPrs / AIS_BRepGraph are in TKV3d.
Choosing BRepGraph as canonical buys: one persistent identity (UID) usable by every external system, O(1) adjacency, color/name/custom metadata on layers without OCAF, and insulation from TopoDS invariants as more OCCT algorithms migrate.
Five sentences:
occtl_graph_t plus value-typed occtl_node_id_t, occtl_ref_id_t, occtl_uid_t, occtl_rep_id_t.TopoDS_* is never named in any public header.Compliance with this contract is a CI grep:
OCCT's BRepGraph uses statically typed IDs (BRepGraph_VertexId, BRepGraph_EdgeId, ...) that are template-tagged with their kind. In C we don't have templates, so we carry the kind explicitly inside a 64-bit packed value:
The ABI uses all-zero bits as the invalid sentinel, so default-initialised values (Python if not id:, C# default(struct), calloc, memset(0)) are invalid. OCCT's BRepGraph_NodeId::Typed uses Index = UINT32_MAX internally; src/topo/TopoMath.hxx translates between the two and is the only file that knows both.
| Type | Stability | Use for |
|---|---|---|
occtl_node_id_t | Transient. Invalidated by Compact() on the graph. Live only within a session. | Loops, neighbor queries, anything within a short scope. |
occtl_ref_id_t | Transient. Invalidated by Compact(). | Addressing reference entries (CoEdge → Wire bindings, etc.) within a session. |
occtl_uid_t | Persistent. Survives Compact(), survives node removal in many cases. | Wire format, external references (a UI's "selected face"), change history, persistence. |
The wrapper makes the distinction visible everywhere a binding might confuse them. The C++ veneer enforces it at the type system level.
occtl_graph_node_id_from_uid is the canonical "I held a UID across an operation; what's its current node id?" call. Returns OCCTL_NOT_FOUND if the entity has been removed.
**compact invalidates all occtl_node_id_t and occtl_ref_id_t values.** UIDs survive. Bindings should expose this loudly; the C++ veneer offers Graph::compact() as an explicit, named call (no implicit invalidation).
A graph holds:
TopoDS_Shape cache for round-trip algorithms (rebuilt on demand).The user never sees TopoDS. Every entry point that creates a graph or adds to one terminates in a node id, not a shape:
Internally, primitives build a TopoDS_Shape via BRepPrimAPI_* and feed it into BRepGraph::ShapesView::Add. That's an implementation detail — the public path is graph-native.
There is no public occtl_graph_to_topods() function. C++ consumers who genuinely need a TopoDS_Shape pull it from an internal-only header that is not part of the installed public surface; bindings cannot reach it.
Read accessors keyed by node id live in the **topo module** (they wrap BRepGraph_Tool, not Geom_*), so the prefix is occtl_topo_*. Examples taken from the shipped header:
For point-evaluation queries the typed POD output (occtl_point3_t, occtl_vector3_t) is a value-in / value-out shape — cheap to marshal across every binding.
When a curve or surface needs to be evaluated repeatedly (offset, intersection trace, sampling), the geom module exposes opaque occtl_curve_t* / occtl_curve2d_t* / occtl_surface_t* handles with kind-tagged extractors. They are part of the default-on geom module — see `MODULES.md §4` and include/occtl/occtl_curves.h / _curves2d.h / _surfaces.h. The same handles feed occtl_topo_make_edge (3D curve) and occtl_topo_make_face (surface).
Zero-copy. Lifetime: valid until the graph is freed or the face's triangulation is replaced. A binding consumer wraps this in NumPy / Span<T> / TypedArray directly.
For 3D-edge polygons (curve approximations), occtl_mesh_edge_polygon3d returns a similar view. For polygons-on-triangulation (shared between adjacent faces), occtl_mesh_face_polygon_on_tri is provided.
The BRepGraph::EditorView exposes 40+ low-level mutators. We do not mirror them 1:1 in C — that would explode the surface and leak invariants. Instead the wrapper offers:
Each *_info_t follows the options-struct convention — versioned, with p_next, with _INIT macro.
Multiple mutations should run inside a deferred scope. This maps onto BRepGraph_DeferredScope and avoids per-call subtree-generation propagation:
While a batch is open, mutating calls on the graph defer cache invalidation; commit applies it once. Abort discards uncommitted changes (where the underlying mutation is reversible — documented per call). Outside a batch, every mutation is its own implicit scope.
BRepGraph's Layer system is the answer to "where do I attach color/name/custom data" without OCAF. The wrapper exposes:
Color and name are first-class because they're the metadata everyone needs. They live in BRepGraph layers under the hood, not in side maps on occtl_graph_t.
Layer placement rules:
NodeId / RefId, must follow removal / replacement / Compact(), or must eventually serialize with a graph snapshot. Name, color, material, units, joints, tags, and exchange labels follow this rule.BRepGraph_TransientCache / BRepGraph_RefTransientCache when it depends on graph contents and can be invalidated by VersionStamp / SubtreeGen. Bounding boxes, geometry classifications, mass-property summaries, selector acceleration data, and expensive relation-query auxiliaries follow this rule.BRepGraphAlgo_*, graph layers in TKBRep, or graph cache kinds there over duplicating reusable topology / metadata algorithms in each wrapper module.Do not add a dependency from the core topo module to data-exchange toolkits just to reuse a metadata layer. If a generic name/color/material layer only exists in a DE package, either keep an OCCT-Light-private layer as an ABI bridge or upstream/move the generic layer into OCCT's BRepGraph package.
Advanced API; documented in a separate "Extending OCCT-Light" guide. The shape:
The wrapper translates layer callbacks (OnNodeRemoved, OnCompact, OnNodeModified) to C callbacks. The wrapper itself catches any error and logs it; layer callbacks must never propagate failure into OCCT (OCCT layer callbacks are noexcept).
UIDs are the wire format identity. They survive Compact(), they survive most node removals, and they're exactly the right key to put into:
Node ids are session-local. They're cheap to use within a function or a frame's render loop, but they must not leave the wrapper's scope. The conversion API today is occtl_graph_node_id_from_uid / occtl_graph_uid_from_node_id (§3.2), with occtl_graph_uid_table and occtl_uid_to_bytes / occtl_uid_from_bytes for a stable UID table snapshot. Reference entries have the same split: occtl_ref_id_t is session-local, while occtl_ref_uid_t resolves through occtl_graph_ref_id_from_ref_uid / occtl_graph_ref_uid_from_ref_id and can be bulk-dumped with occtl_graph_ref_uid_table. File-format-level UID persistence is still format-specific work for the I/O modules.
For algorithms that don't yet have BRepGraph-native variants — Booleans, fillets, offsets, pipe operations — the wrapper round-trips:
TopoDS_Shape via BRepGraph::Shapes().Shape(rootId) (cached).BRepAlgoAPI_Fuse, etc.). Check IsDone() and translate to a status.graph.Shapes().Add(resultShape, opts) to merge the result back into the graph.The user's experience: graph in, graph(s) out, no TopoDS mentioned. The wrapper documents per algorithm whether the original graph receives the result merged in (preferred) or whether a fresh graph is returned.
History API. Booleans and selected topology/feature operations record history on the graph that receives the operation result. Booleans merge their result back into the same graph the inputs came from (in-place semantics — simpler for callers and avoids the NodeId-foreign-graph problem). The full public surface lives on include/occtl/occtl_topo.h and include/occtl/occtl_bool.h:
History uses UIDs (persistent), not NodeIds (transient), because it's by definition crossing operation boundaries. Each accessor follows the two-call buffer pattern (ABI_PATTERNS.md §10). The deleted set is collection-wide — input UIDs do not key into it; callers iterate the returned UID array directly.
Internally, the wrapper absorbs OCCT-side provenance (BRepGraph_History + BRepAlgoAPI_HistoryAdapter::Absorb) into the graph's own BRepGraph_History instance. No separate public history handle exists. The section 12 protocol ("The TopoDS round-trip protocol (internal)") remains the source of truth for which algorithms still round-trip through TopoDS; the in-place-merge signature is the public face of that protocol.
| Issue | Approach |
|---|---|
| Boolean ops, fillets, offsets round-trip through TopoDS. | Documented as implementation detail. Will move to graph-native when BRepGraphAlgo gains support. The public API does not change when that happens. |
| CoEdge / seam semantics surprise users from a TopoDS background. | We don't hide CoEdges — they're part of the value of BRepGraph's half-edge model. We document them in the user guide with diagrams. The query API names them clearly (occtl_topo_coedge_*). |
Layer callbacks must be noexcept per OCCT contract. | Wrapper enforces by translating any C-callback exception or non-OK status into a logged warning; layers never see failures. Documented as part of the layer registration contract. |
Compact() invalidating all NodeIds is a footgun. | The C ABI is explicit: occtl_graph_compact is named, never implicit, and documented as invalidating. The C++ veneer wraps it as Graph::compact(). UIDs survive. |
| Definition vs Usage distinction is unfamiliar. | Most queries hide it (the high-level API operates on instance-level facts). Where it matters (e.g., "how many places does this surface appear?"), we expose it explicitly via occtl_topo_definition_* accessors. |
A 64-bit kind-tagged identity reaches every binding without translation, UIDs persist across operations and serialization without OCAF, adjacency is O(1), and metadata lives on layers. The internal TopoDS round-trip is the price; hiding it from users is the design.