Signed-off-by: stroblme <stroblme@posteo.de>
This commit is contained in:
2026-08-11 15:07:47 +02:00
parent ab32514df1
commit 8175b8aff3
5 changed files with 261 additions and 380 deletions

117
README.md
View File

@@ -1,76 +1,67 @@
# 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: **in production use** — all 138 symbols the app imports, across 53
bound modules, published as `8.0.1.1` (OCCT 8.0.1). The app cut over on
2026-08-10 at `7.9.3.1` and took the kernel bump on 2026-08-11; its full
backend suite passes against both (1798 passed / 1 skipped, the same as the
stock wheel it replaced), 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 — across the kernel bump as well, so nothing stored had to
be rewritten.
Hand-written [nanobind](https://github.com/wjakob/nanobind) wrapper for the OpenCASCADE (OCCT) geometry kernel.
In comparison to [`cadquery-ocp](https://github.com/cadquery/OCP), we get superlinear 7.0x speedup on the use cases in `n3xd`.
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).
[docs/adding-symbols.md](docs/adding-symbols.md) to extend the surface.
## Why
## Installation
`cadquery-ocp` lags OCCT (it still wraps 7.9.3; we are on 8.0.1), 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 8.0.1 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:
As packages sit on our Gitea instance for, you must install by providing the specific url, like:
```bash
uv pip install --index-url https://git.stroblme.de/api/packages/N3XD/pypi/simple/ \
--prerelease=allow n3xd-ocp
```
Versions are `<occt-version>.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.
Versions are `<occt-version>.N`, enforced at configure time against the OCCT actually found.
## Usage
`OCP` mirrors [`cadquery-ocp`](https://github.com/cadquery/OCP) symbol-for-symbol, so code written against it runs unchanged:
```python
from OCP.BRepPrimAPI import BRepPrimAPI_MakeBox
from OCP.BRepAlgoAPI import BRepAlgoAPI_Cut
from OCP.TopTools import TopTools_ListOfShape
box = BRepPrimAPI_MakeBox(10.0, 20.0, 30.0).Shape()
hole = BRepPrimAPI_MakeBox(3.0, 3.0, 30.0).Shape()
args, tools = TopTools_ListOfShape(), TopTools_ListOfShape()
args.Append(box)
tools.Append(hole)
cut = BRepAlgoAPI_Cut()
cut.SetArguments(args)
cut.SetTools(tools)
cut.Build()
result = cut.Shape()
```
One deliberate gap from upstream: constructors that run the algorithm immediately (the two-argument `BRepAlgoAPI_Cut(a, b)` form) aren't bound, only the deferred `SetArguments`/`SetTools`/`Build()` sequence above. See [docs/design.md](docs/design.md) for why.
`n3xd_ocp` adds a handful of batch operations OCP doesn't have. They run on the same OCCT build and take/return plain `OCP` shapes:
```python
import n3xd_ocp
areas, centroids = n3xd_ocp.measure.face_surface_props(result) # one call for every face
meshes = n3xd_ocp.tess.extract_meshes(result) # triangulated faces, ready to render
data = n3xd_ocp.bintools.write_bytes(result) # BREP bytes, no temp file needed
```
## Build
Wheels are built on a dev box and published to the Gitea package registry [here](https://git.stroblme.de/api/packages/N3XD/pypi).
If you want to make modifications or build it yourself, here are some shortcuts:
```bash
make image # compiles OCCT 8.0.1 into the builder image
make dev # incremental compile + tests
make wheel # compile, stubs, auditwheel, self-containment smoke test
make publish # publish to Gitea using .secret credentials
```