# n3xd-ocp Hand-written [nanobind](https://github.com/wjakob/nanobind) bindings for the OpenCASCADE (OCCT) geometry kernel, covering exactly the surface the N3XD CAD backend uses — 138 symbols across 47 `OCP.*` modules, not all of OCCT. The package installs as a top-level `OCP`, so it is a drop-in replacement for `cadquery-ocp-novtk` and the app's 442 import sites stay untouched. Status: **coverage complete** — all 138 symbols the app imports, across 53 bound modules, published as `7.9.3.1.dev5`. The app's full backend suite passes against it (1797 passed / 1 skipped, the same as the stock wheel), a sweep of the whole project store reproduces every part's geometry exactly (4486 parts; no change in statuses, volume, area, bbox, entity counts, triangles or anchor digests), and BREP serialisation stays byte-identical, which the pools and the content-addressed derive payloads depend on. The app is not swapped yet — that is the cutover, roadmap 10C. Start with [docs/design.md](docs/design.md) for the decisions, [docs/building.md](docs/building.md) to build one, and [docs/adding-symbols.md](docs/adding-symbols.md) to extend the surface. The phase plan lives in the app repo at `docs-private/reference/roadmap.md` (Phase 10). ## Why `cadquery-ocp` lags OCCT (it wraps 7.9.3; OCCT 8.0 shipped 2026-05), builds Windows and macOS wheels we never use, and until recently forced a 638 MB VTK dependency into the image. So this exists for version velocity, footprint, and two defects that a binding we control prevents by construction: - OCCT sub-shapes are returned **by value**, so a wrapper can never alias a `TShape` whose owner has died (this segfaulted a process-global face memo). - Executing constructors (the two-argument `BRepAlgoAPI_*` forms) are **not bound**, so the double-execution footgun is unrepresentable. It also releases the GIL around kernel calls and ships type stubs, neither of which upstream does. **It is also considerably faster, which was not the point and turned out to matter most.** With the app otherwise unchanged, its benchmark suite runs 194 s → 73 s, and rebuild time improves *superlinearly* with model complexity: 3.3x for a 4-feature part, 7.0x for a 32-feature one (13.4 s → 1.9 s). The premise going in was that call overhead is irrelevant because the hotspots live inside the kernel — true of any single call, false of the aggregate, because this backend reaches OCCT once per face, per node and per edge. `tools/bench_ext.py` has the numbers and the two places they contradicted the plan. ## Build OCCT is compiled once into a builder image and reused; it is never built on the production host (4 cores, and a kernel build is multi-hour). Wheels are built here on a dev box and published to the Gitea package registry. ```bash make image # once, ~40 min: compiles OCCT 7.9.3 into the builder image make dev # inner loop: incremental compile + tests make wheel # compile, stubs, auditwheel, self-containment smoke test make publish # -> https://git.stroblme.de/api/packages/N3XD/pypi ``` Credentials go in `.secrets` (gitignored) as `UV_PUBLISH_USERNAME` / `UV_PUBLISH_PASSWORD`. Consumers read anonymously — the package is public: ```bash uv pip install --index-url https://git.stroblme.de/api/packages/N3XD/pypi/simple/ \ --prerelease=allow n3xd-ocp ``` Versions are `.N`, enforced at configure time against the OCCT actually found, so the kernel a wheel wraps is readable from its version alone. The registry refuses to republish a version; iteration builds therefore carry a `.devN` suffix and are the only ones the registry's cleanup rule collects.