Phase 10A/10B Inc 0: build system, handle model, first module surface

Builds n3xd-ocp end to end and publishes 7.9.3.1.dev1 to the Gitea registry,
where it installs anonymously and passes its suite.

- occt/Dockerfile: OCCT 7.9.3 compiled once into a manylinux_2_28 builder
  image (base digest + tarball sha256 pinned), Draw/VTK/Tk/Xlib/OpenGL off,
  FreeType on, -O2 without fast-math or march=native. A final layer asserts
  TKService/TKV3d exist with no libGL/libX11 DT_NEEDED, which is what lets the
  app image drop libgl1/libx11-6. Mounted into, never built FROM.
- scikit-build-core + nanobind STABLE_ABI -> one cp312-abi3 extension that
  registers every OCP.* submodule via PyImport_AddModule, so `import
  OCP.TopoDS` needs no shim and cls.__module__ is right. Version <occt>.N is
  asserted against the OCCT found, keeping occt_version() truthful.
- occt_handle.h: type caster for opencascade::handle<T> over OCCT's intrusive
  refcount. Wrappers are non-owning instances holding exactly one handle in
  their keep-alive list, reusing an existing wrapper so identity survives a
  round trip. Transient constructors go through ocp_new (never nb::init<>,
  which would let OCCT delete nanobind's storage); the caster refuses a
  refcount-0 object rather than corrupt the heap. Verified under ASAN with no
  memory-safety errors, plus an RSS bound over 50k create/destroy cycles.
- Sub-shapes are returned by value everywhere, making the TShape lifetime class
  that segfaulted a process-global face memo unrepresentable.
- Standard_Failure derives RuntimeError, with ~20 concrete types dispatched on
  the dynamic OCCT type (cad_pool marshals failures home by type name).
- Inc 0 surface: gp subset, TopAbs, TopoDS (+ downcasts), TopExp, TopLoc,
  TopTools, BRep, BinTools, Poly, Standard. 34 of the app's 139 symbols.
- n3xd_ocp: additive APIs kept out of the OCP namespace so parity testing stays
  meaningful. bintools (shape <-> bytes, GIL-free, byte-identical) and _debug.

Two findings worth the record, both verified against the stock wheel rather
than assumed: upstream binds __hash__ but leaves __eq__ at identity, which is
exactly what geom_memo.py's hash-bucket + IsSame scan is built around, so we
match it instead of "fixing" it; and BinTools can release the GIL after all, by
slurping the file-like object instead of bridging a streambuf that would call
back into Python.

Gate: BREP round-trips are byte-identical to cadquery-ocp-novtk across six
fixtures (the generator asserts stock idempotency first). That matters beyond
IPC — derive.py content-addresses BREP payloads by sha256 and stores the ref.
This commit is contained in:
2026-08-10 16:10:28 +02:00
parent b757d6e8d6
commit 6139852768
58 changed files with 3446 additions and 13 deletions

View File

@@ -0,0 +1,109 @@
#include "occt_exceptions.h"
#include "occt_module.h"
#include <Standard_ConstructionError.hxx>
#include <Standard_DimensionError.hxx>
#include <Standard_DimensionMismatch.hxx>
#include <Standard_DivideByZero.hxx>
#include <Standard_DomainError.hxx>
#include <Standard_Failure.hxx>
#include <Standard_ImmutableObject.hxx>
#include <Standard_NoSuchObject.hxx>
#include <Standard_NotImplemented.hxx>
#include <Standard_NullObject.hxx>
#include <Standard_NullValue.hxx>
#include <Standard_NumericError.hxx>
#include <Standard_OutOfRange.hxx>
#include <Standard_Overflow.hxx>
#include <Standard_ProgramError.hxx>
#include <Standard_RangeError.hxx>
#include <Standard_TypeMismatch.hxx>
#include <Standard_Underflow.hxx>
#include <StdFail_NotDone.hxx>
#include <StdFail_UndefinedDerivative.hxx>
#include <StdFail_UndefinedValue.hxx>
#include <cstring>
#include <string>
#include <unordered_map>
namespace {
/// OCCT class name -> Python exception type. Populated at import; read-only
/// afterwards, so it is fork-safe and needs no lock.
std::unordered_map<std::string, PyObject *> g_exc_types;
PyObject *g_base = nullptr; // OCP.Standard.Standard_Failure
PyObject *make_exc(nb::module_ &mod, const char *qualified, const char *name,
PyObject *base) {
PyObject *exc = PyErr_NewException(qualified, base, nullptr);
if (!exc)
throw nb::python_error();
mod.attr(name) = nb::borrow(exc);
g_exc_types.emplace(name, exc);
return exc;
}
} // namespace
void register_occt_exceptions(nb::module_ &root) {
nb::module_ std_mod = ocp_submodule(root, "Standard");
nb::module_ fail_mod = ocp_submodule(root, "StdFail");
// Standard_Failure derives RuntimeError: that single fact is what keeps
// the backend's `except RuntimeError` sites working unchanged.
g_base = make_exc(std_mod, "OCP.Standard.Standard_Failure",
"Standard_Failure", PyExc_RuntimeError);
auto sub = [&](nb::module_ &m, const char *qualified, const char *name) {
return make_exc(m, qualified, name, g_base);
};
#define OCP_EXC(modvar, modname, name) \
sub(modvar, "OCP." #modname "." #name, #name)
OCP_EXC(std_mod, Standard, Standard_DomainError);
OCP_EXC(std_mod, Standard, Standard_RangeError);
OCP_EXC(std_mod, Standard, Standard_OutOfRange);
OCP_EXC(std_mod, Standard, Standard_NoSuchObject);
OCP_EXC(std_mod, Standard, Standard_TypeMismatch);
OCP_EXC(std_mod, Standard, Standard_NullObject);
OCP_EXC(std_mod, Standard, Standard_NullValue);
OCP_EXC(std_mod, Standard, Standard_ConstructionError);
OCP_EXC(std_mod, Standard, Standard_DimensionError);
OCP_EXC(std_mod, Standard, Standard_DimensionMismatch);
OCP_EXC(std_mod, Standard, Standard_NumericError);
OCP_EXC(std_mod, Standard, Standard_DivideByZero);
OCP_EXC(std_mod, Standard, Standard_Overflow);
OCP_EXC(std_mod, Standard, Standard_Underflow);
OCP_EXC(std_mod, Standard, Standard_ProgramError);
OCP_EXC(std_mod, Standard, Standard_NotImplemented);
OCP_EXC(std_mod, Standard, Standard_ImmutableObject);
OCP_EXC(fail_mod, StdFail, StdFail_NotDone);
OCP_EXC(fail_mod, StdFail, StdFail_UndefinedDerivative);
OCP_EXC(fail_mod, StdFail, StdFail_UndefinedValue);
#undef OCP_EXC
nb::register_exception_translator(
[](const std::exception_ptr &p, void *) {
try {
std::rethrow_exception(p);
} catch (const Standard_Failure &e) {
// Dispatch on the dynamic OCCT type so the Python type name
// matches what the kernel actually threw; anything we did not
// bind falls back to the RuntimeError-derived base.
PyObject *type = g_base;
const char *name = e.DynamicType()->Name();
if (name) {
auto it = g_exc_types.find(name);
if (it != g_exc_types.end())
type = it->second;
}
const char *msg = e.GetMessageString();
std::string text = std::string(name ? name : "Standard_Failure");
if (msg && *msg)
text = std::string(msg);
PyErr_SetString(type, text.c_str());
}
},
nullptr);
}

View File

@@ -0,0 +1,20 @@
/*
OCCT Standard_Failure -> Python exception translation.
The backend's discipline is `IsDone()` gates (47 sites) plus `except
RuntimeError` (~10 sites); it never names an OCCT exception class. So the
hard requirement is that every kernel failure arrives as something deriving
RuntimeError. The concrete type names matter too, but for a subtler
reason: cad_pool's worker children marshal failures home as
f"{type(exc).__name__}: {exc}" strings, so the name is observable output.
*/
#pragma once
#include <nanobind/nanobind.h>
namespace nb = nanobind;
/// Create the OCP.Standard / OCP.StdFail exception classes and install the
/// translator. Call once, before any module that can raise.
void register_occt_exceptions(nb::module_ &root);

124
src/common/occt_handle.h Normal file
View File

@@ -0,0 +1,124 @@
/*
Type caster for opencascade::handle<T> — the ownership core of this binding.
OCCT transients carry their own intrusive, atomic reference count
(Standard_Transient), so a handle keeps an object alive entirely on the C++
side. That is what lets us release the GIL around kernel calls: OCCT may
copy handles on its own worker threads without ever touching Python.
The design mirrors nanobind's own stl/shared_ptr.h caster:
C++ -> Python the wrapper is a *non-owning* nanobind instance pointing
at the C++ object, plus one handle stored in the object's
keep-alive list. Python holds exactly one OCCT reference
per wrapper, released at deallocation. An existing
wrapper is reused (is_new == false), so `a is b` holds for
as long as a wrapper stays alive.
Python -> C++ a plain handle copy (one incref), balanced when the caster
dies after the call. Unlike shared_ptr we do *not* need
to keep the PyObject alive: the OCCT refcount, not the
Python instance, owns the object's memory.
That last sentence is only true if every wrapped transient was heap
allocated and is handle-owned. See occt_transient.h — transient
constructors are bound through OCP_TRANSIENT_NEW, never nb::init<>, so
Python never owns transient storage. from_python re-checks the invariant
rather than trusting it, because the failure mode is a double free.
*/
#pragma once
#include <nanobind/nanobind.h>
#include <Standard_Handle.hxx>
#include <Standard_Transient.hxx>
#include <type_traits>
NAMESPACE_BEGIN(NB_NAMESPACE)
NAMESPACE_BEGIN(detail)
// Marked NB_NOINLINE so the (identical) body is not duplicated into every
// instantiation of the caster below — there is one per bound transient class.
inline NB_NOINLINE void
occt_handle_keep_alive(opencascade::handle<Standard_Transient> &&h,
PyObject *o) noexcept {
keep_alive(o, new opencascade::handle<Standard_Transient>(std::move(h)),
[](void *p) noexcept {
delete (opencascade::handle<Standard_Transient> *) p;
});
}
template <typename T>
struct type_caster<
opencascade::handle<T>,
enable_if_t<std::is_base_of_v<Standard_Transient, std::decay_t<T>>>> {
static constexpr bool IsClass = true;
using Caster = make_caster<T>;
using Td = std::decay_t<T>;
NB_TYPE_CASTER(opencascade::handle<T>, Caster::Name)
static_assert(is_base_caster_v<Caster>,
"Conversion of opencascade::handle<T> requires that T is "
"bound through nanobind's regular class mechanism.");
bool from_python(handle src, uint8_t flags,
cleanup_list *cleanup) noexcept {
// A null handle is OCCT's absent value and maps to None in both
// directions; several APIs (BRep_Tool::Surface on a degenerate edge,
// for one) legitimately return one.
if (src.is_none()) {
value = Value();
return true;
}
flags &= ~((uint8_t) cast_flags::convert);
Caster caster;
if (!caster.from_python(src, flags, cleanup))
return false;
Td *ptr = caster.operator Td *();
// Invariant check, not defensive programming: a refcount of zero means
// this wrapper owns its storage (nb::init<>, or a by-value return),
// and taking a handle to it would hand OCCT the right to `delete` a
// nanobind instance's memory. Refuse instead — a TypeError beats a
// heap corruption, and it can only be reached by a binding bug.
if (ptr && ptr->GetRefCount() == 0) {
assert(!"OCCT transient is not handle-owned (see occt_transient.h)");
return false;
}
value = Value(ptr);
return true;
}
static handle from_cpp(const Value &v, rv_policy,
cleanup_list *cleanup) noexcept {
Td *ptr = v.get();
if (!ptr)
return none().release();
// Transients are polymorphic, so nb_type_put_p downcasts on the
// dynamic type: BRep_Tool::Surface returning a Geom_Surface handle to
// a plane arrives in Python as Geom_Plane when that class is bound.
bool is_new = false;
handle result = nb_type_put_p(&typeid(Td), &typeid(*ptr), (void *) ptr,
rv_policy::reference, cleanup, &is_new);
// Only a freshly created wrapper takes a reference. Attaching one per
// conversion would pile up redundant handles on a long-lived object
// that crosses the boundary many times.
if (result.is_valid() && is_new)
occt_handle_keep_alive(
opencascade::handle<Standard_Transient>(ptr), result.ptr());
return result;
}
};
NAMESPACE_END(detail)
NAMESPACE_END(NB_NAMESPACE)

View File

@@ -0,0 +1,28 @@
#include "occt_module.h"
#include <algorithm>
std::vector<std::string> &ocp_module_registry() {
static std::vector<std::string> registry;
return registry;
}
nb::module_ ocp_named_module(const char *full_name) {
// Borrowed reference, and already interned in sys.modules — which is what
// makes `import OCP.TopoDS` work without a .py shim per module. Note this
// does not require the parent package to exist yet.
PyObject *mod = PyImport_AddModule(full_name);
if (!mod)
throw nb::python_error();
return nb::borrow<nb::module_>(mod);
}
nb::module_ ocp_submodule(nb::module_ &parent, const char *name) {
nb::module_ m = ocp_named_module((std::string("OCP.") + name).c_str());
parent.attr(name) = m;
auto &reg = ocp_module_registry();
if (std::find(reg.begin(), reg.end(), name) == reg.end())
reg.emplace_back(name);
return m;
}

49
src/common/occt_module.h Normal file
View File

@@ -0,0 +1,49 @@
/*
Submodule and static-method helpers.
The wheel ships one extension, OCP/_OCP, which registers the OCP.*
submodules itself. They are created with PyImport_AddModule so they are
real entries in sys.modules: `import OCP.TopoDS`, `from OCP.TopoDS import
TopoDS_Shape` and cls.__module__ == "OCP.TopoDS" all work without a shim
module per name. (nanobind has no def_submodule.)
*/
#pragma once
#include <nanobind/nanobind.h>
// Included here, not per module, so no translation unit can accidentally bind
// a handle-returning API without the caster in scope: the symptom is a silent
// fallback that reports "unable to convert" only at call time.
#include "occt_handle.h"
#include <string>
#include <vector>
namespace nb = nanobind;
// Every module file wants "name"_a for keyword arguments.
using namespace nb::literals;
/// Names of every OCP.* submodule registered so far, in registration order.
/// OCP/__init__.py re-exports these so `OCP.TopoDS` resolves as an attribute
/// too, not only as an import.
std::vector<std::string> &ocp_module_registry();
/// Create (or fetch) a real module by its full dotted name. Idempotent, and
/// deliberately does not require the parent package to exist — n3xd_ocp's
/// submodules are registered this way so that the Python package's __init__
/// still runs whichever import happens first.
nb::module_ ocp_named_module(const char *full_name);
/// Create (or fetch) "OCP.<name>", attach it to `parent`, and record it for
/// OCP/__init__.py's re-export loop.
nb::module_ ocp_submodule(nb::module_ &parent, const char *name);
/// Bind an OCCT static method under the `_s` suffix.
///
/// Upstream OCP suffixes every static it exposes, and the app calls 176 of
/// them by that name (BRepGProp.SurfaceProperties_s, TopExp.MapShapes_s,
/// BinTools.Write_s, ...). The rule here is blanket rather than clash-driven,
/// so no static can accidentally ship without it.
#define OCP_DEF_S(cls, name, ...) (cls).def_static(name "_s", __VA_ARGS__)

View File

@@ -0,0 +1,35 @@
/*
Return-value and GIL policies.
Two rules live here, and both close a defect class the app currently works
around in Python:
1. Sub-shapes come back BY VALUE. A TopoDS_Shape is a small value holding
a handle to its TShape, so copying it is one incref and the copy owns
what it points at. Returning a reference into a container (an explorer's
Current(), a map's FindKey(), a BOP history list) instead lets the
wrapper outlive its owner — that is what segfaulted a process-global
face memo under upstream OCP, and why cad/topology/geom_memo.py is
scoped to a single rebuild by a ContextVar.
nanobind copies lvalue references by default; OCP_RETURN_COPY states it
explicitly so a reviewer can see the guarantee at the call site.
2. The GIL is released around calls that stay inside the kernel. The list
is deliberate rather than blanket: a call that can re-enter Python (the
BinTools stream bridge, notably) must keep it.
*/
#pragma once
#include <nanobind/nanobind.h>
namespace nb = nanobind;
/// Explicit "returns an owned copy" policy for shape-valued returns.
#define OCP_RETURN_COPY nb::rv_policy::copy
/// Release the GIL for the duration of a kernel call. Only for calls that
/// cannot re-enter the interpreter: Build/Perform, meshing, BRepCheck,
/// file-based readers and writers.
#define OCP_NOGIL nb::call_guard<nb::gil_scoped_release>()

41
src/common/occt_stream.h Normal file
View File

@@ -0,0 +1,41 @@
/*
Python file-like <-> std::stream adaptation for BinTools.
Upstream's signature takes an io.BytesIO, and the app always passes one
(cad_pool and derive.py serialise shapes through BytesIO). Rather than
bridge a streambuf that calls back into Python on every chunk, this slurps:
read the whole payload first, then hand the kernel a pure C++ stream.
Two things fall out of that. It is correct no matter how BinTools seeks
inside the stream, and the kernel half touches no Python at all — so the
GIL can be released around it, which a callback bridge could not do. The
cost is one extra copy of the BREP bytes, which n3xd_ocp.bintools avoids
entirely for the pool paths that care.
*/
#pragma once
#include <nanobind/nanobind.h>
#include <string>
namespace nb = nanobind;
/// Read a Python file-like object to end of stream.
inline std::string ocp_slurp(nb::handle stream) {
nb::object data = nb::getattr(stream, "read")();
char *buf = nullptr;
Py_ssize_t size = 0;
if (PyBytes_AsStringAndSize(data.ptr(), &buf, &size) != 0)
throw nb::python_error();
return std::string(buf, (size_t) size);
}
/// Write a byte payload to a Python file-like object.
inline void ocp_spit(nb::handle stream, const std::string &data) {
nb::object payload = nb::steal(
PyBytes_FromStringAndSize(data.data(), (Py_ssize_t) data.size()));
if (!payload.is_valid())
throw nb::python_error();
nb::getattr(stream, "write")(payload);
}

View File

@@ -0,0 +1,42 @@
/*
Binding helpers for OCCT transient (handle-managed) classes.
Rule: a transient class never gets nb::init<>. nanobind's normal
constructor placement-news the object into the Python instance's own
storage, which OCCT would later try to `delete` when the last handle goes
away. The app hits this on a real path — sketch_builder/edges.py builds a
Geom_BSplineCurve in Python and hands it to BRepBuilderAPI_MakeEdge, which
stores a handle to it that outlives the call.
So constructors are bound through nb::new_ returning a handle: the object
is heap allocated and reaches Python through the handle caster, owning its
own storage from birth. occt_handle.h's from_python check enforces this.
*/
#pragma once
#include <nanobind/nanobind.h>
#include <Standard_Handle.hxx>
#include <Standard_Transient.hxx>
#include <utility>
namespace nb = nanobind;
/// Declare a bound transient class. Bind its constructors with ocp_new<T,
/// Args...>() — nb::init<> on such a class is a bug (see above).
template <typename T, typename... Bases>
nb::class_<T, Bases...> ocp_transient_class(nb::handle scope, const char *name) {
return nb::class_<T, Bases...>(scope, name);
}
/// Heap-allocating constructor for a transient class:
///
/// cls.def(ocp_new<Poly_Triangulation, int, int, bool>(),
/// "nbNodes"_a, "nbTriangles"_a, "hasUVNodes"_a);
template <typename T, typename... Args> auto ocp_new() {
return nb::new_([](Args... args) {
return opencascade::handle<T>(new T(std::forward<Args>(args)...));
});
}

59
src/core.cpp Normal file
View File

@@ -0,0 +1,59 @@
/*
OCP._OCP — the single extension module.
One .so registers every OCP.* submodule rather than shipping one extension
per module: the whole surface traffics in TopoDS_Shape, gp_* and handles,
so sharing types in-process is free here and would otherwise depend on
nanobind's cross-extension registry; registration order stays an explicit
sequence below instead of an import-order puzzle; and cad_pool's forkserver
warms exactly one dlopen. Per-module .cpp files keep incremental compiles
cheap — only the final link is shared.
*/
#include <nanobind/nanobind.h>
#include <nanobind/stl/string.h>
#include <nanobind/stl/vector.h>
#include "common/occt_exceptions.h"
#include "common/occt_module.h"
#include <Standard_Version.hxx>
namespace nb = nanobind;
void register_Standard(nb::module_ &);
void register_gp(nb::module_ &);
void register_TopAbs(nb::module_ &);
void register_TopLoc(nb::module_ &);
void register_TopoDS(nb::module_ &);
void register_TopTools(nb::module_ &);
void register_TopExp(nb::module_ &);
void register_Poly(nb::module_ &);
void register_BRep(nb::module_ &);
void register_BinTools(nb::module_ &);
void register_ext(nb::module_ &);
NB_MODULE(_OCP, m) {
m.doc() = "nanobind bindings for the OpenCASCADE geometry kernel";
// Exceptions first: they create OCP.Standard and OCP.StdFail, and any
// module registered afterwards may raise through the translator.
register_occt_exceptions(m);
register_Standard(m);
register_gp(m);
register_TopAbs(m);
register_TopLoc(m);
register_TopoDS(m);
register_TopTools(m);
register_TopExp(m);
register_Poly(m);
register_BRep(m);
register_BinTools(m);
register_ext(m);
m.attr("__all_modules__") = ocp_module_registry();
m.attr("__occt_version__") = OCC_VERSION_COMPLETE;
m.attr("__version__") = N3XD_OCP_VERSION;
}

53
src/ext/ext_bintools.cpp Normal file
View File

@@ -0,0 +1,53 @@
/*
n3xd_ocp.bintools — shape <-> bytes without the file-object detour.
The pools move every shape as BinTools bytes through a BytesIO purely
because that is the shape of upstream's API. Here the payload is a bytes
object directly: one buffer instead of a BytesIO plus its internal copy,
and the kernel half runs with the GIL released.
Byte-for-byte identical to OCP.BinTools output — same BinTools::Write, same
format version — so it is interchangeable with it, including for the
content-addressed derive payloads.
*/
#include "../common/occt_module.h"
#include <nanobind/stl/string.h>
#include <BinTools.hxx>
#include <TopoDS_Shape.hxx>
#include <sstream>
void register_ext_bintools() {
nb::module_ m = ocp_named_module("n3xd_ocp.bintools");
m.def(
"write_bytes",
[](const TopoDS_Shape &shape) {
std::ostringstream out;
{
nb::gil_scoped_release nogil;
BinTools::Write(shape, out);
}
std::string data = out.str();
return nb::bytes(data.data(), data.size());
},
"shape"_a,
"Serialise a shape to BREP bytes (identical to OCP.BinTools.Write_s).");
m.def(
"read_bytes",
[](nb::bytes data) {
std::string buf(data.c_str(), data.size());
TopoDS_Shape shape;
{
nb::gil_scoped_release nogil;
std::istringstream in(buf);
BinTools::Read(shape, in);
}
return shape;
},
"data"_a, "Deserialise BREP bytes into a shape.");
}

25
src/ext/ext_core.cpp Normal file
View File

@@ -0,0 +1,25 @@
/*
n3xd_ocp — the extension surface, kept out of the OCP namespace on purpose.
OCP.* stays a drop-in replacement for cadquery-ocp-novtk, symbol for symbol,
so parity testing means something. Anything that goes *beyond* upstream —
bulk array extraction, batched measurement, GIL-free serialisation — lives
here instead, in the same wheel. The backend adopts these after the
cutover, one call site at a time.
Only the leaf modules are registered here. Creating the `n3xd_ocp` parent
from C++ would put a bare module in sys.modules, and a later `import
n3xd_ocp` would then find it and never run the package's __init__.py.
*/
#include "../common/occt_module.h"
namespace nb = nanobind;
void register_ext_bintools();
void register_ext_debug();
void register_ext(nb::module_ &) {
register_ext_bintools();
register_ext_debug();
}

78
src/ext/ext_debug.cpp Normal file
View File

@@ -0,0 +1,78 @@
/*
n3xd_ocp._debug — introspection the handle-model tests need.
Not part of the supported surface; it exists so tests can assert ownership
invariants (OCCT refcounts, wrapper identity) that are otherwise invisible
from Python.
*/
#include "../common/occt_handle.h"
#include "../common/occt_module.h"
#include <Poly_Triangulation.hxx>
#include <Standard_Transient.hxx>
#include <vector>
namespace {
/// Holds handles alive on the C++ side so a test can drop every Python
/// reference and check the object survived.
std::vector<opencascade::handle<Standard_Transient>> &cpp_holds() {
static std::vector<opencascade::handle<Standard_Transient>> held;
return held;
}
} // namespace
void register_ext_debug() {
nb::module_ m = ocp_named_module("n3xd_ocp._debug");
m.def(
"refcount",
[](const opencascade::handle<Standard_Transient> &h) {
// The caster's own handle is alive for the duration of the call,
// so subtract it to report what the caller actually holds.
return h.IsNull() ? 0 : h->GetRefCount() - 1;
},
nb::arg("obj").none(),
"OCCT reference count excluding this call's own handle.");
m.def(
"roundtrip",
[](const opencascade::handle<Standard_Transient> &h) { return h; },
nb::arg("obj").none(),
"Send a transient through C++ and back — must return the same wrapper.");
m.def(
"hold",
[](const opencascade::handle<Standard_Transient> &h) {
cpp_holds().push_back(h);
return (int) cpp_holds().size() - 1;
},
"obj"_a, "Keep a C++-side reference; returns its slot index.");
m.def(
"release_held", []() { cpp_holds().clear(); },
"Drop every C++-side reference taken by hold().");
m.def(
"held_is_alive",
[](int slot) {
auto &held = cpp_holds();
return slot >= 0 && slot < (int) held.size() &&
!held[(size_t) slot].IsNull();
},
"slot"_a);
m.def(
"held_nb_nodes",
[](int slot) {
auto &held = cpp_holds();
opencascade::handle<Poly_Triangulation> tri =
opencascade::handle<Poly_Triangulation>::DownCast(
held[(size_t) slot]);
return tri.IsNull() ? -1 : tri->NbNodes();
},
"slot"_a, "Touch a held object's memory — catches a premature free.");
}

73
src/modules/mod_BRep.cpp Normal file
View File

@@ -0,0 +1,73 @@
/*
OCP.BRep — builder plus the Inc 0 half of BRep_Tool.
BRep_Tool::Surface / Curve return Geom handles and land with Inc 1, where
the Geom classes arrive; Triangulation is here because it is what the
tessellation path reads and what the handle spike exercises.
*/
#include "../common/occt_module.h"
#include "../common/occt_policies.h"
#include <BRep_Builder.hxx>
#include <BRep_Tool.hxx>
#include <Poly_Triangulation.hxx>
#include <TopLoc_Location.hxx>
#include <TopoDS_Compound.hxx>
#include <TopoDS_Edge.hxx>
#include <TopoDS_Face.hxx>
#include <TopoDS_Shell.hxx>
#include <TopoDS_Solid.hxx>
#include <TopoDS_Vertex.hxx>
#include <TopoDS_Wire.hxx>
void register_BRep(nb::module_ &root) {
nb::module_ m = ocp_submodule(root, "BRep");
nb::class_<BRep_Builder>(m, "BRep_Builder")
.def(nb::init<>())
.def("MakeCompound", &BRep_Builder::MakeCompound, "C"_a)
.def("MakeShell", &BRep_Builder::MakeShell, "S"_a)
.def("MakeSolid", &BRep_Builder::MakeSolid, "S"_a)
.def("MakeWire", &BRep_Builder::MakeWire, "W"_a)
.def("Add",
[](const BRep_Builder &self, TopoDS_Shape &S,
const TopoDS_Shape &C) { self.Add(S, C); },
"S"_a, "C"_a)
.def("Remove",
[](const BRep_Builder &self, TopoDS_Shape &S,
const TopoDS_Shape &C) { self.Remove(S, C); },
"S"_a, "C"_a);
nb::class_<BRep_Tool> cls(m, "BRep_Tool");
// The location is an in/out parameter, matching upstream and the five call
// sites in the app (`BRep_Tool.Triangulation_s(face, loc)`): nanobind hands
// the lambda a reference into the caller's own instance, so the assignment
// is visible in Python. A face without a mesh yields a null handle, which
// the caster maps to None.
OCP_DEF_S(
cls, "Triangulation",
[](const TopoDS_Face &F, TopLoc_Location &L) {
return BRep_Tool::Triangulation(F, L);
},
"F"_a, "L"_a);
OCP_DEF_S(
cls, "Pnt", [](const TopoDS_Vertex &V) { return BRep_Tool::Pnt(V); },
"V"_a);
OCP_DEF_S(
cls, "Degenerated",
[](const TopoDS_Edge &E) { return BRep_Tool::Degenerated(E); }, "E"_a);
OCP_DEF_S(
cls, "Tolerance",
[](const TopoDS_Face &F) { return BRep_Tool::Tolerance(F); }, "F"_a);
OCP_DEF_S(
cls, "Tolerance",
[](const TopoDS_Edge &E) { return BRep_Tool::Tolerance(E); }, "E"_a);
OCP_DEF_S(
cls, "Tolerance",
[](const TopoDS_Vertex &V) { return BRep_Tool::Tolerance(V); }, "V"_a);
OCP_DEF_S(
cls, "IsClosed",
[](const TopoDS_Shape &S) { return BRep_Tool::IsClosed(S); }, "S"_a);
}

View File

@@ -0,0 +1,51 @@
/*
OCP.BinTools — the IPC-critical path.
Every subprocess pool serialises shapes through here, and derive.py content-
addresses the result (payloads/derived/brep/<sha256>.brep) with the digest
stored in the document. So byte-stability is a correctness requirement,
not merely a round-trip one, and it is what the Inc 0 gate measures.
The GIL is released around the kernel half. That is possible because the
Python file object is drained (or filled) on either side of the call rather
than bridged through a streambuf that would call back into the interpreter
mid-serialisation — see occt_stream.h.
*/
#include "../common/occt_module.h"
#include "../common/occt_stream.h"
#include <BinTools.hxx>
#include <TopoDS_Shape.hxx>
#include <sstream>
void register_BinTools(nb::module_ &root) {
nb::module_ m = ocp_submodule(root, "BinTools");
nb::class_<BinTools> cls(m, "BinTools");
// Matches upstream overload 1: writes with triangulation, at
// BinTools_FormatVersion_CURRENT.
OCP_DEF_S(
cls, "Write",
[](const TopoDS_Shape &theShape, nb::object theStream) {
std::ostringstream out;
{
nb::gil_scoped_release nogil;
BinTools::Write(theShape, out);
}
ocp_spit(theStream, out.str());
},
"theShape"_a, "theStream"_a);
OCP_DEF_S(
cls, "Read",
[](TopoDS_Shape &theShape, nb::object theStream) {
std::string data = ocp_slurp(theStream);
std::istringstream in(data);
nb::gil_scoped_release nogil;
BinTools::Read(theShape, in);
},
"theShape"_a, "theStream"_a);
}

56
src/modules/mod_Poly.cpp Normal file
View File

@@ -0,0 +1,56 @@
/*
OCP.Poly — also the handle-model spike vehicle.
Poly_Triangulation is a transient (handle-managed) class that the app both
receives from the kernel (BRep_Tool::Triangulation) and, in the wider
surface, constructs. It is therefore the natural place to prove the
ownership rules in occt_handle.h before the rest of the surface depends on
them. Constructors go through ocp_new, never nb::init<>.
*/
#include "../common/occt_module.h"
#include "../common/occt_policies.h"
#include "../common/occt_transient.h"
#include <Poly_Triangulation.hxx>
void register_Poly(nb::module_ &root) {
nb::module_ m = ocp_submodule(root, "Poly");
nb::class_<Poly_Triangle>(m, "Poly_Triangle")
.def(nb::init<>())
.def(nb::init<Standard_Integer, Standard_Integer, Standard_Integer>(),
"theN1"_a, "theN2"_a, "theN3"_a)
.def("Value", &Poly_Triangle::Value, "theIndex"_a)
.def("Get", [](const Poly_Triangle &t) {
Standard_Integer n1 = 0, n2 = 0, n3 = 0;
t.Get(n1, n2, n3);
return nb::make_tuple(n1, n2, n3);
});
nb::class_<Poly_Triangulation, Standard_Transient>(m, "Poly_Triangulation")
.def(ocp_new<Poly_Triangulation>())
.def(ocp_new<Poly_Triangulation, Standard_Integer, Standard_Integer,
Standard_Boolean, Standard_Boolean>(),
"theNbNodes"_a, "theNbTriangles"_a, "theHasUVNodes"_a,
"theHasNormals"_a = Standard_False)
.def("NbNodes", &Poly_Triangulation::NbNodes)
.def("NbTriangles", &Poly_Triangulation::NbTriangles)
.def("HasUVNodes", &Poly_Triangulation::HasUVNodes)
.def("HasNormals", &Poly_Triangulation::HasNormals)
.def("Node", &Poly_Triangulation::Node, "theIndex"_a, OCP_RETURN_COPY)
.def("SetNode", &Poly_Triangulation::SetNode, "theIndex"_a,
"thePnt"_a)
.def("Triangle", &Poly_Triangulation::Triangle, "theIndex"_a,
OCP_RETURN_COPY)
.def("SetTriangle", &Poly_Triangulation::SetTriangle, "theIndex"_a,
"theTriangle"_a)
.def("Normal",
nb::overload_cast<Standard_Integer>(&Poly_Triangulation::Normal,
nb::const_),
"theIndex"_a, OCP_RETURN_COPY)
.def("UVNode", &Poly_Triangulation::UVNode, "theIndex"_a,
OCP_RETURN_COPY)
.def("Deflection",
nb::overload_cast<>(&Poly_Triangulation::Deflection, nb::const_));
}

View File

@@ -0,0 +1,25 @@
#include "../common/occt_module.h"
#include <Standard_Transient.hxx>
#include <Standard_Type.hxx>
#include <Standard_Version.hxx>
void register_Standard(nb::module_ &root) {
nb::module_ m = ocp_submodule(root, "Standard");
// Every handle-managed class derives from this, and nanobind needs it
// registered before it can be named as a base. Deliberately not
// constructible from Python: transients are created through ocp_new (see
// occt_transient.h), never by placement-new into instance storage.
nb::class_<Standard_Transient>(m, "Standard_Transient")
.def("GetRefCount", &Standard_Transient::GetRefCount)
.def("IsInstance",
[](const Standard_Transient &self, const char *name) {
return self.IsKind(name);
})
.def("DynamicType", [](const Standard_Transient &self) {
return std::string(self.DynamicType()->Name());
});
m.attr("OCC_VERSION_COMPLETE") = OCC_VERSION_COMPLETE;
}

View File

@@ -0,0 +1,44 @@
#include "../common/occt_module.h"
#include <TopAbs.hxx>
#include <TopAbs_Orientation.hxx>
#include <TopAbs_ShapeEnum.hxx>
#include <TopAbs_State.hxx>
void register_TopAbs(nb::module_ &root) {
nb::module_ m = ocp_submodule(root, "TopAbs");
// is_arithmetic + export_values reproduce the pybind11 enum behaviour the
// app relies on: members at module scope (`from OCP.TopAbs import
// TopAbs_FACE`) and int comparison.
nb::enum_<TopAbs_ShapeEnum>(m, "TopAbs_ShapeEnum", nb::is_arithmetic())
.value("TopAbs_COMPOUND", TopAbs_COMPOUND)
.value("TopAbs_COMPSOLID", TopAbs_COMPSOLID)
.value("TopAbs_SOLID", TopAbs_SOLID)
.value("TopAbs_SHELL", TopAbs_SHELL)
.value("TopAbs_FACE", TopAbs_FACE)
.value("TopAbs_WIRE", TopAbs_WIRE)
.value("TopAbs_EDGE", TopAbs_EDGE)
.value("TopAbs_VERTEX", TopAbs_VERTEX)
.value("TopAbs_SHAPE", TopAbs_SHAPE)
.export_values();
nb::enum_<TopAbs_Orientation>(m, "TopAbs_Orientation", nb::is_arithmetic())
.value("TopAbs_FORWARD", TopAbs_FORWARD)
.value("TopAbs_REVERSED", TopAbs_REVERSED)
.value("TopAbs_INTERNAL", TopAbs_INTERNAL)
.value("TopAbs_EXTERNAL", TopAbs_EXTERNAL)
.export_values();
nb::enum_<TopAbs_State>(m, "TopAbs_State", nb::is_arithmetic())
.value("TopAbs_IN", TopAbs_IN)
.value("TopAbs_OUT", TopAbs_OUT)
.value("TopAbs_ON", TopAbs_ON)
.value("TopAbs_UNKNOWN", TopAbs_UNKNOWN)
.export_values();
nb::class_<TopAbs> cls(m, "TopAbs");
OCP_DEF_S(cls, "Compose", &TopAbs::Compose, "Or1"_a, "Or2"_a);
OCP_DEF_S(cls, "Reverse", &TopAbs::Reverse, "Or"_a);
OCP_DEF_S(cls, "Complement", &TopAbs::Complement, "Or"_a);
}

View File

@@ -0,0 +1,48 @@
#include "../common/occt_module.h"
#include "../common/occt_policies.h"
#include <TopExp.hxx>
#include <TopExp_Explorer.hxx>
#include <TopTools_IndexedDataMapOfShapeListOfShape.hxx>
#include <TopTools_IndexedMapOfShape.hxx>
void register_TopExp(nb::module_ &root) {
nb::module_ m = ocp_submodule(root, "TopExp");
nb::class_<TopExp_Explorer>(m, "TopExp_Explorer")
.def(nb::init<>())
.def(nb::init<const TopoDS_Shape &, TopAbs_ShapeEnum,
TopAbs_ShapeEnum>(),
"S"_a, "ToFind"_a, "ToAvoid"_a = TopAbs_SHAPE)
.def("Init", &TopExp_Explorer::Init, "S"_a, "ToFind"_a,
"ToAvoid"_a = TopAbs_SHAPE)
.def("More", &TopExp_Explorer::More)
.def("Next", &TopExp_Explorer::Next)
.def("Current", &TopExp_Explorer::Current, OCP_RETURN_COPY)
.def("Value", &TopExp_Explorer::Value, OCP_RETURN_COPY)
.def("ReInit", &TopExp_Explorer::ReInit)
.def("Depth", &TopExp_Explorer::Depth)
.def("Clear", &TopExp_Explorer::Clear);
nb::class_<TopExp> cls(m, "TopExp");
// MapShapes is the single most-called static in the backend (54 sites).
OCP_DEF_S(
cls, "MapShapes",
[](const TopoDS_Shape &S, TopAbs_ShapeEnum T,
TopTools_IndexedMapOfShape &M) { TopExp::MapShapes(S, T, M); },
"S"_a, "T"_a, "M"_a);
OCP_DEF_S(
cls, "MapShapes",
[](const TopoDS_Shape &S, TopTools_IndexedMapOfShape &M) {
TopExp::MapShapes(S, M);
},
"S"_a, "M"_a);
OCP_DEF_S(
cls, "MapShapesAndAncestors",
[](const TopoDS_Shape &S, TopAbs_ShapeEnum TS, TopAbs_ShapeEnum TA,
TopTools_IndexedDataMapOfShapeListOfShape &M) {
TopExp::MapShapesAndAncestors(S, TS, TA, M);
},
"S"_a, "TS"_a, "TA"_a, "M"_a);
}

View File

@@ -0,0 +1,24 @@
#include "../common/occt_module.h"
#include "../common/occt_policies.h"
#include <TopLoc_Location.hxx>
void register_TopLoc(nb::module_ &root) {
nb::module_ m = ocp_submodule(root, "TopLoc");
nb::class_<TopLoc_Location>(m, "TopLoc_Location")
.def(nb::init<>())
.def(nb::init<const gp_Trsf &>(), "T"_a)
.def("IsIdentity", &TopLoc_Location::IsIdentity)
.def("Identity", &TopLoc_Location::Identity)
.def("Transformation", &TopLoc_Location::Transformation,
OCP_RETURN_COPY)
.def("Inverted", &TopLoc_Location::Inverted, OCP_RETURN_COPY)
.def("Multiplied", &TopLoc_Location::Multiplied, "Other"_a,
OCP_RETURN_COPY)
.def("Divided", &TopLoc_Location::Divided, "Other"_a, OCP_RETURN_COPY)
.def("Predivided", &TopLoc_Location::Predivided, "Other"_a,
OCP_RETURN_COPY)
.def("IsEqual", &TopLoc_Location::IsEqual, "Other"_a)
.def("IsDifferent", &TopLoc_Location::IsDifferent, "Other"_a);
}

View File

@@ -0,0 +1,100 @@
/*
OCP.TopTools — the shape containers the topology code lives in.
Every accessor returns a shape by value (OCP_RETURN_COPY): a wrapper handed
out by FindKey must not become a dangling view when the map is cleared or
goes out of scope. build_entity_map_delta tests membership through
IndexedMapOfShape::Contains (IsSame semantics), which is why Contains and
FindIndex are bound rather than left to a Python-side scan.
*/
#include "../common/occt_module.h"
#include "../common/occt_policies.h"
#include <nanobind/make_iterator.h>
#include <TopTools_IndexedDataMapOfShapeListOfShape.hxx>
#include <TopTools_IndexedMapOfShape.hxx>
#include <TopTools_ListOfShape.hxx>
void register_TopTools(nb::module_ &root) {
nb::module_ m = ocp_submodule(root, "TopTools");
nb::class_<TopTools_ListOfShape>(m, "TopTools_ListOfShape")
.def(nb::init<>())
.def("Append",
[](TopTools_ListOfShape &self, const TopoDS_Shape &s) {
self.Append(s);
},
"theItem"_a)
.def("Prepend",
[](TopTools_ListOfShape &self, const TopoDS_Shape &s) {
self.Prepend(s);
},
"theItem"_a)
.def("Extent", &TopTools_ListOfShape::Extent)
.def("Size", &TopTools_ListOfShape::Size)
.def("IsEmpty", &TopTools_ListOfShape::IsEmpty)
.def("Clear", [](TopTools_ListOfShape &self) { self.Clear(); })
.def("First",
nb::overload_cast<>(&TopTools_ListOfShape::First, nb::const_),
OCP_RETURN_COPY)
.def("Last",
nb::overload_cast<>(&TopTools_ListOfShape::Last, nb::const_),
OCP_RETURN_COPY)
.def("__len__", &TopTools_ListOfShape::Size)
.def(
"__iter__",
[](const TopTools_ListOfShape &self) {
return nb::make_iterator<nb::rv_policy::copy>(
nb::type<TopTools_ListOfShape>(), "ListOfShapeIterator",
self.begin(), self.end());
},
nb::keep_alive<0, 1>());
nb::class_<TopTools_IndexedMapOfShape>(m, "TopTools_IndexedMapOfShape")
.def(nb::init<>())
.def("Add",
[](TopTools_IndexedMapOfShape &self, const TopoDS_Shape &s) {
return self.Add(s);
},
"theKey"_a)
.def("Contains", &TopTools_IndexedMapOfShape::Contains, "theKey"_a)
.def("FindKey", &TopTools_IndexedMapOfShape::FindKey, "theIndex"_a,
OCP_RETURN_COPY)
.def("FindIndex", &TopTools_IndexedMapOfShape::FindIndex, "theKey"_a)
.def("Extent", &TopTools_IndexedMapOfShape::Extent)
.def("Size", &TopTools_IndexedMapOfShape::Size)
.def("IsEmpty", &TopTools_IndexedMapOfShape::IsEmpty)
.def("Clear",
[](TopTools_IndexedMapOfShape &self) { self.Clear(Standard_True); })
.def("RemoveLast", &TopTools_IndexedMapOfShape::RemoveLast)
.def("__len__", &TopTools_IndexedMapOfShape::Size);
nb::class_<TopTools_IndexedDataMapOfShapeListOfShape>(
m, "TopTools_IndexedDataMapOfShapeListOfShape")
.def(nb::init<>())
.def("Contains", &TopTools_IndexedDataMapOfShapeListOfShape::Contains,
"theKey"_a)
.def("FindKey", &TopTools_IndexedDataMapOfShapeListOfShape::FindKey,
"theIndex"_a, OCP_RETURN_COPY)
.def("FindIndex", &TopTools_IndexedDataMapOfShapeListOfShape::FindIndex,
"theKey"_a)
.def("FindFromKey",
nb::overload_cast<const TopoDS_Shape &>(
&TopTools_IndexedDataMapOfShapeListOfShape::FindFromKey,
nb::const_),
"theKey"_a, OCP_RETURN_COPY)
.def("FindFromIndex",
nb::overload_cast<Standard_Integer>(
&TopTools_IndexedDataMapOfShapeListOfShape::FindFromIndex,
nb::const_),
"theIndex"_a, OCP_RETURN_COPY)
.def("Extent", &TopTools_IndexedDataMapOfShapeListOfShape::Extent)
.def("Size", &TopTools_IndexedDataMapOfShapeListOfShape::Size)
.def("Clear",
[](TopTools_IndexedDataMapOfShapeListOfShape &self) {
self.Clear(Standard_True);
})
.def("__len__", &TopTools_IndexedDataMapOfShapeListOfShape::Size);
}

114
src/modules/mod_TopoDS.cpp Normal file
View File

@@ -0,0 +1,114 @@
/*
OCP.TopoDS — shapes are value types, and that is the point.
A TopoDS_Shape is a small value (a handle to its TShape, a location, an
orientation), so every shape crossing into Python is an owned copy. A
wrapper can therefore never alias storage owned by an explorer, a map or a
BOP history list — which is the lifetime class that segfaulted a
process-global face memo under upstream OCP.
Fidelity note on __hash__ / __eq__, verified against the stock wheel:
upstream binds __hash__ (TShape + Location) and leaves __eq__ at Python's
default identity comparison. That pairing looks odd — two re-extracted
copies of one face hash equal but compare unequal — and it is exactly what
cad/topology/geom_memo.py is built around: it buckets on hash(face) and
disambiguates with IsSame, because == cannot be trusted. Binding __eq__ to
IsEqual here would silently change that memo's behaviour, so we match
upstream rather than improve on it.
*/
#include "../common/occt_module.h"
#include "../common/occt_policies.h"
#include <TopoDS.hxx>
#include <TopoDS_CompSolid.hxx>
#include <TopoDS_Compound.hxx>
#include <TopoDS_Edge.hxx>
#include <TopoDS_Face.hxx>
#include <TopoDS_Iterator.hxx>
#include <TopoDS_Shape.hxx>
#include <TopoDS_Shell.hxx>
#include <TopoDS_Solid.hxx>
#include <TopoDS_Vertex.hxx>
#include <TopoDS_Wire.hxx>
#include <functional>
namespace {
/// Stands in for the TopoDS namespace so its statics can hang off a Python
/// class of that name — see the comment at the downcast block below.
struct TopoDSStatics {};
} // namespace
void register_TopoDS(nb::module_ &root) {
nb::module_ m = ocp_submodule(root, "TopoDS");
nb::class_<TopoDS_Shape>(m, "TopoDS_Shape")
.def(nb::init<>())
.def("IsNull", &TopoDS_Shape::IsNull)
.def("Nullify", &TopoDS_Shape::Nullify)
.def("ShapeType", &TopoDS_Shape::ShapeType)
.def("Orientation",
nb::overload_cast<>(&TopoDS_Shape::Orientation, nb::const_))
.def("Location",
nb::overload_cast<>(&TopoDS_Shape::Location, nb::const_),
OCP_RETURN_COPY)
.def("Closed", nb::overload_cast<>(&TopoDS_Shape::Closed, nb::const_))
.def("Reverse", &TopoDS_Shape::Reverse)
.def("Reversed", &TopoDS_Shape::Reversed, OCP_RETURN_COPY)
.def("Moved", &TopoDS_Shape::Moved, "position"_a,
"raiseExc"_a = Standard_False, OCP_RETURN_COPY)
.def("Located", &TopoDS_Shape::Located, "loc"_a,
"raiseExc"_a = Standard_False, OCP_RETURN_COPY)
.def("IsSame", &TopoDS_Shape::IsSame, "other"_a)
.def("IsEqual", &TopoDS_Shape::IsEqual, "other"_a)
.def("IsPartner", &TopoDS_Shape::IsPartner, "other"_a)
.def("__hash__", [](const TopoDS_Shape &s) {
return std::hash<TopoDS_Shape>{}(s);
});
#define OCP_SHAPE_SUBCLASS(Type) \
nb::class_<Type, TopoDS_Shape>(m, #Type).def(nb::init<>())
OCP_SHAPE_SUBCLASS(TopoDS_Vertex);
OCP_SHAPE_SUBCLASS(TopoDS_Edge);
OCP_SHAPE_SUBCLASS(TopoDS_Wire);
OCP_SHAPE_SUBCLASS(TopoDS_Face);
OCP_SHAPE_SUBCLASS(TopoDS_Shell);
OCP_SHAPE_SUBCLASS(TopoDS_Solid);
OCP_SHAPE_SUBCLASS(TopoDS_CompSolid);
OCP_SHAPE_SUBCLASS(TopoDS_Compound);
#undef OCP_SHAPE_SUBCLASS
nb::class_<TopoDS_Iterator>(m, "TopoDS_Iterator")
.def(nb::init<>())
.def(nb::init<const TopoDS_Shape &, Standard_Boolean,
Standard_Boolean>(),
"S"_a, "cumOri"_a = Standard_True, "cumLoc"_a = Standard_True)
.def("More", &TopoDS_Iterator::More)
.def("Next", &TopoDS_Iterator::Next)
.def("Value", &TopoDS_Iterator::Value, OCP_RETURN_COPY);
// Checked downcasts. These raise Standard_TypeMismatch on a kind
// mismatch, which the translator turns into a RuntimeError subclass.
//
// OCCT 7.9 turned TopoDS from a class into a namespace, but upstream OCP
// still presents it as a class carrying the _s statics, and the app calls
// TopoDS.Face_s(...). An empty carrier type reproduces that surface.
nb::class_<TopoDSStatics> cls(m, "TopoDS");
#define OCP_DOWNCAST(Name) \
OCP_DEF_S( \
cls, #Name, \
[](const TopoDS_Shape &s) { return TopoDS::Name(s); }, "S"_a, \
OCP_RETURN_COPY)
OCP_DOWNCAST(Vertex);
OCP_DOWNCAST(Edge);
OCP_DOWNCAST(Wire);
OCP_DOWNCAST(Face);
OCP_DOWNCAST(Shell);
OCP_DOWNCAST(Solid);
OCP_DOWNCAST(CompSolid);
OCP_DOWNCAST(Compound);
#undef OCP_DOWNCAST
}

138
src/modules/mod_gp.cpp Normal file
View File

@@ -0,0 +1,138 @@
/*
OCP.gp — the Inc 0 subset (points, vectors, directions, axes, transforms).
The rest of gp (gp_Ax2/Ax3/Pln/Circ/Lin/Pnt2d/Dir2d/Quaternion, which the
app also uses) lands with Inc 1, where the curve and surface classes that
consume them arrive.
*/
#include "../common/occt_module.h"
#include "../common/occt_policies.h"
#include <gp_Ax1.hxx>
#include <gp_Dir.hxx>
#include <gp_Pnt.hxx>
#include <gp_Trsf.hxx>
#include <gp_Vec.hxx>
#include <gp_XYZ.hxx>
void register_gp(nb::module_ &root) {
nb::module_ m = ocp_submodule(root, "gp");
nb::enum_<gp_TrsfForm>(m, "gp_TrsfForm", nb::is_arithmetic())
.value("gp_Identity", gp_Identity)
.value("gp_Rotation", gp_Rotation)
.value("gp_Translation", gp_Translation)
.value("gp_PntMirror", gp_PntMirror)
.value("gp_Ax1Mirror", gp_Ax1Mirror)
.value("gp_Ax2Mirror", gp_Ax2Mirror)
.value("gp_Scale", gp_Scale)
.value("gp_CompoundTrsf", gp_CompoundTrsf)
.value("gp_Other", gp_Other)
.export_values();
nb::class_<gp_XYZ>(m, "gp_XYZ")
.def(nb::init<>())
.def(nb::init<Standard_Real, Standard_Real, Standard_Real>(), "X"_a,
"Y"_a, "Z"_a)
.def("X", &gp_XYZ::X)
.def("Y", &gp_XYZ::Y)
.def("Z", &gp_XYZ::Z);
nb::class_<gp_Pnt>(m, "gp_Pnt")
.def(nb::init<>())
.def(nb::init<Standard_Real, Standard_Real, Standard_Real>(), "Xp"_a,
"Yp"_a, "Zp"_a)
.def(nb::init<const gp_XYZ &>(), "Coord"_a)
.def("X", &gp_Pnt::X)
.def("Y", &gp_Pnt::Y)
.def("Z", &gp_Pnt::Z)
.def("SetX", &gp_Pnt::SetX, "X"_a)
.def("SetY", &gp_Pnt::SetY, "Y"_a)
.def("SetZ", &gp_Pnt::SetZ, "Z"_a)
.def("Coord", nb::overload_cast<>(&gp_Pnt::Coord, nb::const_),
OCP_RETURN_COPY)
.def("Distance", &gp_Pnt::Distance, "Other"_a)
.def("SquareDistance", &gp_Pnt::SquareDistance, "Other"_a)
.def("IsEqual", &gp_Pnt::IsEqual, "Other"_a, "LinearTolerance"_a)
.def("Transform", &gp_Pnt::Transform, "T"_a)
.def("Transformed", &gp_Pnt::Transformed, "T"_a, OCP_RETURN_COPY)
.def("Translate",
nb::overload_cast<const gp_Vec &>(&gp_Pnt::Translate), "V"_a)
.def("Translated",
nb::overload_cast<const gp_Vec &>(&gp_Pnt::Translated, nb::const_),
"V"_a, OCP_RETURN_COPY);
nb::class_<gp_Vec>(m, "gp_Vec")
.def(nb::init<>())
.def(nb::init<Standard_Real, Standard_Real, Standard_Real>(), "Xv"_a,
"Yv"_a, "Zv"_a)
.def(nb::init<const gp_Dir &>(), "V"_a)
.def(nb::init<const gp_Pnt &, const gp_Pnt &>(), "P1"_a, "P2"_a)
.def("X", &gp_Vec::X)
.def("Y", &gp_Vec::Y)
.def("Z", &gp_Vec::Z)
.def("Magnitude", &gp_Vec::Magnitude)
.def("SquareMagnitude", &gp_Vec::SquareMagnitude)
.def("Dot", &gp_Vec::Dot, "Other"_a)
.def("Crossed", &gp_Vec::Crossed, "Right"_a, OCP_RETURN_COPY)
.def("Normalize", &gp_Vec::Normalize)
.def("Normalized", &gp_Vec::Normalized, OCP_RETURN_COPY)
.def("Reversed", &gp_Vec::Reversed, OCP_RETURN_COPY)
.def("Multiplied", &gp_Vec::Multiplied, "Scalar"_a, OCP_RETURN_COPY)
.def("Angle", &gp_Vec::Angle, "Other"_a)
.def("Transform", &gp_Vec::Transform, "T"_a)
.def("Transformed", &gp_Vec::Transformed, "T"_a, OCP_RETURN_COPY);
nb::class_<gp_Dir>(m, "gp_Dir")
.def(nb::init<>())
.def(nb::init<Standard_Real, Standard_Real, Standard_Real>(), "Xv"_a,
"Yv"_a, "Zv"_a)
.def(nb::init<const gp_Vec &>(), "V"_a)
.def("X", &gp_Dir::X)
.def("Y", &gp_Dir::Y)
.def("Z", &gp_Dir::Z)
.def("Dot", &gp_Dir::Dot, "Other"_a)
.def("Crossed", &gp_Dir::Crossed, "Right"_a, OCP_RETURN_COPY)
.def("Angle", &gp_Dir::Angle, "Other"_a)
.def("IsParallel", &gp_Dir::IsParallel, "Other"_a, "AngularTolerance"_a)
.def("Reversed", &gp_Dir::Reversed, OCP_RETURN_COPY)
.def("Reverse", &gp_Dir::Reverse)
.def("Transform", &gp_Dir::Transform, "T"_a)
.def("Transformed", &gp_Dir::Transformed, "T"_a, OCP_RETURN_COPY);
nb::class_<gp_Ax1>(m, "gp_Ax1")
.def(nb::init<>())
.def(nb::init<const gp_Pnt &, const gp_Dir &>(), "P"_a, "V"_a)
.def("Location", &gp_Ax1::Location, OCP_RETURN_COPY)
.def("Direction", &gp_Ax1::Direction, OCP_RETURN_COPY)
.def("SetLocation", &gp_Ax1::SetLocation, "P"_a)
.def("SetDirection", &gp_Ax1::SetDirection, "V"_a)
.def("Reversed", &gp_Ax1::Reversed, OCP_RETURN_COPY);
nb::class_<gp_Trsf>(m, "gp_Trsf")
.def(nb::init<>())
.def("SetTranslation",
nb::overload_cast<const gp_Vec &>(&gp_Trsf::SetTranslation),
"V"_a)
.def("SetTranslation",
nb::overload_cast<const gp_Pnt &, const gp_Pnt &>(
&gp_Trsf::SetTranslation),
"P1"_a, "P2"_a)
.def("SetRotation",
nb::overload_cast<const gp_Ax1 &, Standard_Real>(
&gp_Trsf::SetRotation),
"A1"_a, "Ang"_a)
.def("SetScale", &gp_Trsf::SetScale, "P"_a, "S"_a)
.def("SetMirror", nb::overload_cast<const gp_Ax1 &>(&gp_Trsf::SetMirror),
"A1"_a)
.def("Form", &gp_Trsf::Form)
.def("ScaleFactor", &gp_Trsf::ScaleFactor)
.def("TranslationPart", &gp_Trsf::TranslationPart, OCP_RETURN_COPY)
.def("Value", &gp_Trsf::Value, "Row"_a, "Col"_a)
.def("Inverted", &gp_Trsf::Inverted, OCP_RETURN_COPY)
.def("Multiplied", &gp_Trsf::Multiplied, "T"_a, OCP_RETURN_COPY)
.def("Multiply", &gp_Trsf::Multiply, "T"_a)
.def("PreMultiply", &gp_Trsf::PreMultiply, "T"_a)
.def("Invert", &gp_Trsf::Invert);
}