Binds the modeling core the app builds every feature out of — 93 of its 138 symbols now resolve, up from 34. New modules: GeomAbs, Geom2d, Geom, TCollection, TColgp, TColStd, GProp, BRepGProp, Bnd, BRepBndLib, Adaptor3d, BRepAdaptor, GeomLProp, GeomAPI, BRepBuilderAPI, BOPAlgo, BRepAlgoAPI, BRepPrimAPI, GC, BRepMesh; gp and BRep_Tool completed. Three structural decisions: - BRepBuilderAPI_MakeShape carries Build/Shape/Generated/Modified/IsDeleted for every maker in the binding, so the booleans, the primitives and (later) the fillet builders all answer the app's duck-typed provenance layer through ordinary virtual dispatch. History lists come back copied, so they outlive the builder. - The executing two-argument BRepAlgoAPI constructors stay unbound; operands go in through SetArguments/SetTools. Section keeps Init1/Init2, which are plain setters. BOPAlgo moved up from Inc 2 — SetGlue needs its enum. - Adaptor3d is registered although the app never imports it: every method it calls on BRepAdaptor_Curve/Surface is a virtual declared there, so binding them once on the bases leaves mod_BRepAdaptor.cpp with just constructors. Gate: tests/test_inc1_modeling.py against reference values gen_fixtures.py now records from the stock wheel — measurements, per-face area/centroid in map order, mesh counts, and the boolean history map compared exactly, since that is the substrate the app's topological naming is built on. tools/sigdiff.py compares our bound surface against stock's, because a wrong nb::init<> is silent: MakePrism's five-argument form bound OCCT's semi-infinite gp_Dir overload (gp_Dir converts from gp_Vec), producing a valid solid of the wrong shape with the flags shifted along. The fixture digest caught it; sigdiff finds the class of bug directly, and now reports only one deliberate deviation. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DfriM8XUkn7uYf5Dwe2xo6
134 lines
6.1 KiB
Markdown
134 lines
6.1 KiB
Markdown
# Adding symbols
|
|
|
|
The routine task: the app needs an OCCT class this binding does not expose yet.
|
|
Read [design.md](design.md) first if you are touching the machinery instead.
|
|
|
|
## 1. Find what is missing
|
|
|
|
```bash
|
|
python tools/inventory.py --emit # re-scan the app for OCP usage
|
|
python tools/inventory.py --check # what the wheel lacks, by module
|
|
python tools/inventory.py --methods --only BRepAdaptor # what to bind on each class
|
|
```
|
|
|
|
`--check` groups gaps by module, which is how increments are scoped. It only
|
|
sees symbols reached through an import (`TopExp.MapShapes_s`), so it answers
|
|
*which* classes to bind but not *what* to bind on them.
|
|
|
|
`--methods` answers the second question: it resolves variables assigned
|
|
straight from a constructor and reports the methods called on them, plus
|
|
chained calls as `Klass.Outer() -> Inner` (those constrain `Outer`'s **return**
|
|
type — `adaptor.Cylinder().Radius()` is a requirement on `gp_Cylinder`). It is
|
|
a heuristic — it does not follow arguments, returns or attributes, and a local
|
|
reassigned from something else shows up as noise — so read it as a starting
|
|
surface, not a specification.
|
|
|
|
## 2. Write the module
|
|
|
|
One file per OCP module: `src/modules/mod_<Name>.cpp`.
|
|
|
|
```cpp
|
|
#include "../common/occt_module.h" // brings in the handle caster and "_a"
|
|
#include "../common/occt_policies.h" // OCP_RETURN_COPY, OCP_NOGIL
|
|
|
|
#include <Some_Class.hxx>
|
|
|
|
void register_Some(nb::module_ &root) {
|
|
nb::module_ m = ocp_submodule(root, "Some");
|
|
|
|
nb::class_<Some_Class>(m, "Some_Class")
|
|
.def(nb::init<>())
|
|
.def("Value", &Some_Class::Value, "index"_a, OCP_RETURN_COPY)
|
|
.def("Build", &Some_Class::Build, OCP_NOGIL);
|
|
}
|
|
```
|
|
|
|
Declare and call `register_Some` in `src/core.cpp`. Registration order matters
|
|
only in that a base class must precede its derived classes.
|
|
|
|
## 3. The checklist
|
|
|
|
- **Shape-returning API** → `OCP_RETURN_COPY`. Explorers, iterators, map
|
|
lookups, `Generated`/`Modified` lists — anything handing out a reference into
|
|
storage the caller does not own.
|
|
- **Static method** → `OCP_DEF_S(cls, "Name", ...)`, which appends `_s`. Every
|
|
static, without exception.
|
|
- **Transient (handle-managed) class** → derive from `Standard_Transient` in the
|
|
`nb::class_` declaration and bind constructors with `ocp_new<T, Args...>()`.
|
|
Never `nb::init<>` — see design.md.
|
|
- **Long kernel call** → `OCP_NOGIL`, but only if it cannot re-enter Python.
|
|
- **Executing constructor** → banned only where a deferred `SetX`/`Build` API
|
|
exists and the constructor duplicates `Build()`: the `BRepAlgoAPI_*` booleans
|
|
and splitter. Bind their default constructor plus the `SetX`/`Build`
|
|
sequence. Classes that only compute in their constructor and have no deferred
|
|
form — `BRepMesh_IncrementalMesh`, `BRepCheck_Analyzer`, `GeomAPI_*`,
|
|
`BRepClass3d_SolidClassifier`, `BRepExtrema_DistShapeShape`, `GCPnts_*`,
|
|
`BRepBuilderAPI_Transform` — bind exactly as stock does.
|
|
- **Enum** → `nb::is_arithmetic()` and `.export_values()`.
|
|
- **`Message_ProgressRange` parameters** → omit them. The app never passes one
|
|
(no `OCP.Message` import anywhere), and leaving them out keeps signatures
|
|
small. Add the module if `--check` ever reports it.
|
|
- **Out-parameters** stay out-parameters. `BRep_Tool.Triangulation_s(F, L)`
|
|
writes through `L` because the app calls it that way; returning a tuple would
|
|
be tidier and wrong.
|
|
|
|
When in doubt about a signature, ask the stock wheel rather than guessing:
|
|
|
|
```bash
|
|
cd ../app && uv run --project backend python -c \
|
|
"from OCP.BRep import BRep_Tool; print(BRep_Tool.Triangulation_s.__doc__)"
|
|
```
|
|
|
|
And after writing a module, run `make sigdiff`, which asks it about every class
|
|
at once. This is not pedantry about matching upstream — it catches the one
|
|
mistake in this codebase that is both easy to make and silent:
|
|
|
|
> `nb::init<TopoDS_Shape, gp_Vec, bool, bool, bool>` for `BRepPrimAPI_MakePrism`
|
|
> compiled fine and bound the **wrong constructor**. OCCT's finite-prism
|
|
> overload takes four arguments; the five-argument one takes a `gp_Dir` for a
|
|
> semi-infinite prism, and `gp_Dir` converts implicitly from `gp_Vec`. The
|
|
> result was a valid solid of the wrong shape, with the `Copy` and `Canonize`
|
|
> flags shifted one position along.
|
|
|
|
Anything `sigdiff` reports is either that bug or a deliberate deviation; if it
|
|
is deliberate, say so in a comment where the class is bound.
|
|
|
|
## 4. New toolkits
|
|
|
|
If the linker cannot find a symbol, the class lives in a toolkit not yet listed
|
|
in `CMakeLists.txt` (`target_link_libraries(_OCP PRIVATE ...)`). Add it there;
|
|
`auditwheel` bundles whatever the linker records, so nothing else changes.
|
|
|
|
## 5. Verify and ship
|
|
|
|
```bash
|
|
make dev # compile + tests
|
|
make test-asan # if you touched ownership or added transients
|
|
make wheel # bump the .devN in pyproject.toml first
|
|
make publish
|
|
tools/parity_venv.sh && python tools/inventory.py --check
|
|
```
|
|
|
|
**The app's own tests cannot gate an individual increment.** `backend/tests/
|
|
conftest.py` imports `n3xd.main`, which pulls in the whole app and therefore the
|
|
whole OCP surface, so every backend test fails at collection until the last
|
|
module is bound. Increments are gated here instead: `tools/gen_fixtures.py`
|
|
records reference values from the *stock* wheel (counts, `Modified`/`Generated`/
|
|
`IsDeleted` history maps, measured floats) into `tests/data/manifest.json`, and
|
|
`tests/test_inc<N>_*.py` reproduces the same constructions under our wheel.
|
|
Counts and history maps must match exactly; floats compare at rel 1e-9.
|
|
|
|
The app's full suite is the **Inc 4** gate, run in the parity venv, alongside
|
|
`pytest -m perf` and `backend/tools/rebuild_sweep.py --diff` over the project
|
|
store.
|
|
|
|
## Adding to `n3xd_ocp` instead
|
|
|
|
Anything that is not a faithful mirror of an upstream symbol belongs in
|
|
`src/ext/` under the `n3xd_ocp` namespace: bulk array APIs, batched measurement,
|
|
anything GIL-free that upstream does not offer. `OCP.*` staying a
|
|
symbol-for-symbol drop-in is what makes parity testing meaningful, so keep
|
|
additive work out of it. Register leaf modules with
|
|
`ocp_named_module("n3xd_ocp.<name>")` and re-export them in
|
|
`python/n3xd_ocp/__init__.py`.
|