Files
ocp/docs/adding-symbols.md
stroblme 091a2ad0cf Housekeeping ahead of the Inc 1-4 coverage work
- inventory.py --methods: report the instance methods the app calls per class,
  plus chained calls that constrain a return type. --check says which classes
  to bind; this says what to bind on them, which is what writing ~38 module
  TUs needs.
- Drop StlAPI from the inventory: StlAPI_Writer has no app call site (the only
  use was a test fixture, now on the app's own STL writer). 138 symbols / 47
  modules.
- adding-symbols.md: scope the executing-constructor ban to the BRepAlgoAPI
  booleans, which are the only classes with a deferred Set*/Build form —
  BRepMesh_IncrementalMesh, GeomAPI_*, BRepCheck_Analyzer and friends compute
  in their constructor by design and bind as stock. Replace the per-increment
  app-test guidance: backend/tests/conftest.py imports n3xd.main, so no app
  test can collect until the last module is bound. Increments gate on
  stock-recorded fixtures here; the app suite is the Inc 4 gate.
- parity_venv.sh: run the ocp suite in the swapped venv (it imports only
  OCP/n3xd_ocp, so it works throughout).
- Fix a stale macro name in occt_handle.h (ocp_new, not OCP_TRANSIENT_NEW) and
  drop the unused ocp_transient_class helper.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DfriM8XUkn7uYf5Dwe2xo6
2026-08-10 19:23:27 +02:00

119 lines
5.3 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 && .venv/bin/python -c "from OCP.BRep import BRep_Tool; print(BRep_Tool.Triangulation_s.__doc__)"
```
## 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`.