From 2c2c4e471213fd0b6477b02802385a8e0728f902 Mon Sep 17 00:00:00 2001 From: stroblme Date: Mon, 10 Aug 2026 19:52:52 +0200 Subject: [PATCH] 10C Inc 1: core modeling MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) Claude-Session: https://claude.ai/code/session_01DfriM8XUkn7uYf5Dwe2xo6 --- .gitignore | 3 + CMakeLists.txt | 5 + Makefile | 12 + docs/adding-symbols.md | 17 +- pyproject.toml | 2 +- src/core.cpp | 44 +++ src/modules/mod_Adaptor3d.cpp | 69 +++++ src/modules/mod_BOPAlgo.cpp | 22 ++ src/modules/mod_BRep.cpp | 44 ++- src/modules/mod_BRepAdaptor.cpp | 42 +++ src/modules/mod_BRepAlgoAPI.cpp | 117 ++++++++ src/modules/mod_BRepBndLib.cpp | 36 +++ src/modules/mod_BRepBuilderAPI.cpp | 164 ++++++++++ src/modules/mod_BRepGProp.cpp | 73 +++++ src/modules/mod_BRepMesh.cpp | 35 +++ src/modules/mod_BRepPrimAPI.cpp | 110 +++++++ src/modules/mod_Bnd.cpp | 54 ++++ src/modules/mod_GC.cpp | 25 ++ src/modules/mod_GProp.cpp | 57 ++++ src/modules/mod_Geom.cpp | 87 ++++++ src/modules/mod_Geom2d.cpp | 30 ++ src/modules/mod_GeomAPI.cpp | 59 ++++ src/modules/mod_GeomAbs.cpp | 55 ++++ src/modules/mod_GeomLProp.cpp | 32 ++ src/modules/mod_TColStd.cpp | 67 +++++ src/modules/mod_TColgp.cpp | 59 ++++ src/modules/mod_TCollection.cpp | 24 ++ src/modules/mod_gp.cpp | 184 +++++++++++- tests/data/manifest.json | 467 +++++++++++++++++++++++++++++ tests/test_inc1_modeling.py | 386 ++++++++++++++++++++++++ tools/gen_fixtures.py | 213 ++++++++++++- tools/sigdiff.py | 156 ++++++++++ 32 files changed, 2732 insertions(+), 18 deletions(-) create mode 100644 src/modules/mod_Adaptor3d.cpp create mode 100644 src/modules/mod_BOPAlgo.cpp create mode 100644 src/modules/mod_BRepAdaptor.cpp create mode 100644 src/modules/mod_BRepAlgoAPI.cpp create mode 100644 src/modules/mod_BRepBndLib.cpp create mode 100644 src/modules/mod_BRepBuilderAPI.cpp create mode 100644 src/modules/mod_BRepGProp.cpp create mode 100644 src/modules/mod_BRepMesh.cpp create mode 100644 src/modules/mod_BRepPrimAPI.cpp create mode 100644 src/modules/mod_Bnd.cpp create mode 100644 src/modules/mod_GC.cpp create mode 100644 src/modules/mod_GProp.cpp create mode 100644 src/modules/mod_Geom.cpp create mode 100644 src/modules/mod_Geom2d.cpp create mode 100644 src/modules/mod_GeomAPI.cpp create mode 100644 src/modules/mod_GeomAbs.cpp create mode 100644 src/modules/mod_GeomLProp.cpp create mode 100644 src/modules/mod_TColStd.cpp create mode 100644 src/modules/mod_TColgp.cpp create mode 100644 src/modules/mod_TCollection.cpp create mode 100644 tests/test_inc1_modeling.py create mode 100644 tools/sigdiff.py diff --git a/.gitignore b/.gitignore index 4cea0df..2a11702 100644 --- a/.gitignore +++ b/.gitignore @@ -35,3 +35,6 @@ third_party/ !.vscode/extensions.json .idea/ .DS_Store + +# Stray venv/lock from running uv inside this repo (it is not a uv project) +uv.lock diff --git a/CMakeLists.txt b/CMakeLists.txt index aeaca3c..aa2b75a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -55,6 +55,11 @@ target_link_libraries(_OCP PRIVATE TKGeomBase TKBRep TKTopAlgo + # Inc 1 + TKGeomAlgo # GeomAPI, GCPnts + TKPrim # BRepPrimAPI + TKBO # BRepAlgoAPI, BOPAlgo + TKMesh # BRepMesh ) target_compile_definitions(_OCP PRIVATE diff --git a/Makefile b/Makefile index a705f74..93410e1 100644 --- a/Makefile +++ b/Makefile @@ -32,6 +32,18 @@ dev: ## Incremental build + run the test suite (the inner loop) test: dev ## Alias for dev +check: ## Coverage of the app's symbol inventory, against the in-container build + $(DOCKER_RUN) /cache/venv/bin/python /io/tools/inventory.py --check \ + --inventory /io/inventory.json + +sigdiff: ## Diff our bound surface against the stock wheel's (needs the app venv) + cd $(REPO)/../app && uv run --project backend python \ + $(REPO)/tools/sigdiff.py --dump $(CACHE)/sig-stock.json + $(DOCKER_RUN) /cache/venv/bin/python /io/tools/sigdiff.py \ + --dump /cache/sig-ours.json + python3 $(REPO)/tools/sigdiff.py --compare \ + $(CACHE)/sig-stock.json $(CACHE)/sig-ours.json + shell: ## Interactive shell in the builder image docker run --rm -it -v $(REPO):/io -v $(CACHE):/cache $(IMAGE) bash diff --git a/docs/adding-symbols.md b/docs/adding-symbols.md index 0235cf6..6f61597 100644 --- a/docs/adding-symbols.md +++ b/docs/adding-symbols.md @@ -75,9 +75,24 @@ only in that a base class must precede its derived classes. 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__)" +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` 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 diff --git a/pyproject.toml b/pyproject.toml index fe808d3..3048c97 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,7 +9,7 @@ name = "n3xd-ocp" # .devN suffix: the registry never allows republishing a version, and its # cleanup rule collects only the dev ones. CMake asserts the prefix matches # the OCCT it found. -version = "7.9.3.1.dev1" +version = "7.9.3.1.dev2" description = "nanobind bindings for the OpenCASCADE geometry kernel (drop-in OCP)" readme = "README.md" requires-python = ">=3.12" diff --git a/src/core.cpp b/src/core.cpp index 5ef228c..bb21826 100644 --- a/src/core.cpp +++ b/src/core.cpp @@ -31,6 +31,27 @@ void register_TopExp(nb::module_ &); void register_Poly(nb::module_ &); void register_BRep(nb::module_ &); void register_BinTools(nb::module_ &); +// Inc 1 — core modeling +void register_GeomAbs(nb::module_ &); +void register_TCollection(nb::module_ &); +void register_TColgp(nb::module_ &); +void register_TColStd(nb::module_ &); +void register_GProp(nb::module_ &); +void register_BRepGProp(nb::module_ &); +void register_Bnd(nb::module_ &); +void register_BRepBndLib(nb::module_ &); +void register_Geom2d(nb::module_ &); +void register_Geom(nb::module_ &); +void register_Adaptor3d(nb::module_ &); +void register_BRepAdaptor(nb::module_ &); +void register_GeomLProp(nb::module_ &); +void register_GeomAPI(nb::module_ &); +void register_BRepBuilderAPI(nb::module_ &); +void register_BOPAlgo(nb::module_ &); +void register_BRepAlgoAPI(nb::module_ &); +void register_BRepPrimAPI(nb::module_ &); +void register_GC(nb::module_ &); +void register_BRepMesh(nb::module_ &); void register_ext(nb::module_ &); NB_MODULE(_OCP, m) { @@ -51,6 +72,29 @@ NB_MODULE(_OCP, m) { register_BRep(m); register_BinTools(m); + // Inc 1. Order follows the class hierarchy: a declared base must be + // registered before anything that derives from it. + register_GeomAbs(m); + register_TCollection(m); + register_TColgp(m); + register_TColStd(m); + register_GProp(m); + register_BRepGProp(m); + register_Bnd(m); + register_BRepBndLib(m); + register_Geom2d(m); + register_Geom(m); + register_Adaptor3d(m); // before BRepAdaptor: it declares the bases + register_BRepAdaptor(m); + register_GeomLProp(m); + register_GeomAPI(m); + register_BRepBuilderAPI(m); // declares the MakeShape base the rest use + register_BOPAlgo(m); + register_BRepAlgoAPI(m); + register_BRepPrimAPI(m); + register_GC(m); + register_BRepMesh(m); + register_ext(m); m.attr("__all_modules__") = ocp_module_registry(); diff --git a/src/modules/mod_Adaptor3d.cpp b/src/modules/mod_Adaptor3d.cpp new file mode 100644 index 0000000..816a9b4 --- /dev/null +++ b/src/modules/mod_Adaptor3d.cpp @@ -0,0 +1,69 @@ +/* + OCP.Adaptor3d — the abstract curve/surface interface the app actually + programs against. + + Not imported by the app, but it is where every method it calls on a + BRepAdaptor_Curve/Surface is *declared*: GetType, the parameter bounds, + Value/D1 and the quadric accessors are all virtuals here. Binding them + once on the bases keeps mod_BRepAdaptor.cpp down to constructors, and any + later adaptor (Geom2dAdaptor, Adaptor3d_CurveOnSurface) inherits the + surface for free. + + These are transients in OCCT 7.6+, so they are declared against + Standard_Transient and never get nb::init<>. + + D1 keeps its out-parameters, because the app pre-allocates the gp_Pnt/gp_Vec + and reads them back (`adaptor.D1(u, v, pnt, d1u, d1v)`) — same as stock. +*/ + +#include "../common/occt_module.h" +#include "../common/occt_policies.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +void register_Adaptor3d(nb::module_ &root) { + nb::module_ m = ocp_submodule(root, "Adaptor3d"); + + nb::class_(m, "Adaptor3d_Curve") + .def("GetType", &Adaptor3d_Curve::GetType) + .def("FirstParameter", &Adaptor3d_Curve::FirstParameter) + .def("LastParameter", &Adaptor3d_Curve::LastParameter) + .def("Value", &Adaptor3d_Curve::Value, "U"_a, OCP_RETURN_COPY) + .def("D0", &Adaptor3d_Curve::D0, "U"_a, "P"_a) + .def("D1", &Adaptor3d_Curve::D1, "U"_a, "P"_a, "V"_a) + .def("Line", &Adaptor3d_Curve::Line, OCP_RETURN_COPY) + .def("Circle", &Adaptor3d_Curve::Circle, OCP_RETURN_COPY) + .def("Ellipse", &Adaptor3d_Curve::Ellipse, OCP_RETURN_COPY) + .def("IsClosed", &Adaptor3d_Curve::IsClosed) + .def("IsPeriodic", &Adaptor3d_Curve::IsPeriodic); + + nb::class_(m, "Adaptor3d_Surface") + .def("GetType", &Adaptor3d_Surface::GetType) + .def("FirstUParameter", &Adaptor3d_Surface::FirstUParameter) + .def("LastUParameter", &Adaptor3d_Surface::LastUParameter) + .def("FirstVParameter", &Adaptor3d_Surface::FirstVParameter) + .def("LastVParameter", &Adaptor3d_Surface::LastVParameter) + .def("IsUPeriodic", &Adaptor3d_Surface::IsUPeriodic) + .def("IsVPeriodic", &Adaptor3d_Surface::IsVPeriodic) + .def("UPeriod", &Adaptor3d_Surface::UPeriod) + .def("VPeriod", &Adaptor3d_Surface::VPeriod) + .def("IsUClosed", &Adaptor3d_Surface::IsUClosed) + .def("IsVClosed", &Adaptor3d_Surface::IsVClosed) + .def("Value", &Adaptor3d_Surface::Value, "U"_a, "V"_a, OCP_RETURN_COPY) + .def("D0", &Adaptor3d_Surface::D0, "U"_a, "V"_a, "P"_a) + .def("D1", &Adaptor3d_Surface::D1, "U"_a, "V"_a, "P"_a, "D1U"_a, "D1V"_a) + .def("Plane", &Adaptor3d_Surface::Plane, OCP_RETURN_COPY) + .def("Cylinder", &Adaptor3d_Surface::Cylinder, OCP_RETURN_COPY) + .def("Cone", &Adaptor3d_Surface::Cone, OCP_RETURN_COPY) + .def("Sphere", &Adaptor3d_Surface::Sphere, OCP_RETURN_COPY); +} diff --git a/src/modules/mod_BOPAlgo.cpp b/src/modules/mod_BOPAlgo.cpp new file mode 100644 index 0000000..91ed783 --- /dev/null +++ b/src/modules/mod_BOPAlgo.cpp @@ -0,0 +1,22 @@ +/* + OCP.BOPAlgo — boolean-operation options. + + Only the glue enum: the app's pattern fuse sets BOPAlgo_GlueShift when it + knows the operands cannot intersect, which lets OCCT skip the intersection + stage entirely. It has to land with BRepAlgoAPI rather than later, since + SetGlue takes this type. +*/ + +#include "../common/occt_module.h" + +#include + +void register_BOPAlgo(nb::module_ &root) { + nb::module_ m = ocp_submodule(root, "BOPAlgo"); + + nb::enum_(m, "BOPAlgo_GlueEnum", nb::is_arithmetic()) + .value("BOPAlgo_GlueOff", BOPAlgo_GlueOff) + .value("BOPAlgo_GlueShift", BOPAlgo_GlueShift) + .value("BOPAlgo_GlueFull", BOPAlgo_GlueFull) + .export_values(); +} diff --git a/src/modules/mod_BRep.cpp b/src/modules/mod_BRep.cpp index 94064be..b72e001 100644 --- a/src/modules/mod_BRep.cpp +++ b/src/modules/mod_BRep.cpp @@ -1,16 +1,22 @@ /* - OCP.BRep — builder plus the Inc 0 half of BRep_Tool. + OCP.BRep — the shape builder and BRep_Tool's query statics. - 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. + Surface/Curve hand back the face's or edge's *live* Geom handle when the + location is identity, and a transformed copy otherwise. The handle caster + wraps whichever it is without copying, so mutating the result mutates the + model — upstream behaves the same way, and the app only ever reads. */ #include "../common/occt_module.h" #include "../common/occt_policies.h" +#include + #include #include +#include +#include +#include #include #include #include @@ -70,4 +76,34 @@ void register_BRep(nb::module_ &root) { OCP_DEF_S( cls, "IsClosed", [](const TopoDS_Shape &S) { return BRep_Tool::IsClosed(S); }, "S"_a); + + OCP_DEF_S( + cls, "Surface", + [](const TopoDS_Face &F) { return BRep_Tool::Surface(F); }, "F"_a); + OCP_DEF_S( + cls, "Surface", + [](const TopoDS_Face &F, TopLoc_Location &L) { + return BRep_Tool::Surface(F, L); + }, + "F"_a, "L"_a); + + // Curve reports the parameter range through two scalar out-parameters; + // upstream returns them alongside the handle, so a caller writes + // `curve, first, last = BRep_Tool.Curve_s(edge)`. + OCP_DEF_S( + cls, "Curve", + [](const TopoDS_Edge &E) { + Standard_Real first = 0.0, last = 0.0; + auto curve = BRep_Tool::Curve(E, first, last); + return std::make_tuple(curve, first, last); + }, + "E"_a); + OCP_DEF_S( + cls, "CurveOnSurface", + [](const TopoDS_Edge &E, const TopoDS_Face &F) { + Standard_Real first = 0.0, last = 0.0; + auto curve = BRep_Tool::CurveOnSurface(E, F, first, last); + return std::make_tuple(curve, first, last); + }, + "E"_a, "F"_a); } diff --git a/src/modules/mod_BRepAdaptor.cpp b/src/modules/mod_BRepAdaptor.cpp new file mode 100644 index 0000000..f3f8bdd --- /dev/null +++ b/src/modules/mod_BRepAdaptor.cpp @@ -0,0 +1,42 @@ +/* + OCP.BRepAdaptor — adaptors over topology. + + Constructors only: every method the app calls is a virtual declared on + Adaptor3d_Curve/Adaptor3d_Surface and bound there. These are the app's + single most-used query path — "what kind of surface is this face, and what + are its axis and radius" — behind reference inference, anchor scoring and + hole recognition. +*/ + +#include "../common/occt_module.h" +#include "../common/occt_transient.h" + +#include +#include +#include +#include +#include +#include + +void register_BRepAdaptor(nb::module_ &root) { + nb::module_ m = ocp_submodule(root, "BRepAdaptor"); + + nb::class_(m, "BRepAdaptor_Curve") + .def(ocp_new()) + .def(ocp_new(), "E"_a) + .def(ocp_new(), + "E"_a, "F"_a) + .def("Initialize", + nb::overload_cast(&BRepAdaptor_Curve::Initialize), + "E"_a) + .def("Edge", &BRepAdaptor_Curve::Edge, nb::rv_policy::copy); + + nb::class_(m, "BRepAdaptor_Surface") + .def(ocp_new()) + .def(ocp_new(), + "F"_a, "R"_a = Standard_True) + .def("Initialize", &BRepAdaptor_Surface::Initialize, "F"_a, + "Restriction"_a = Standard_True) + .def("Face", &BRepAdaptor_Surface::Face, nb::rv_policy::copy); +} diff --git a/src/modules/mod_BRepAlgoAPI.cpp b/src/modules/mod_BRepAlgoAPI.cpp new file mode 100644 index 0000000..2e11398 --- /dev/null +++ b/src/modules/mod_BRepAlgoAPI.cpp @@ -0,0 +1,117 @@ +/* + OCP.BRepAlgoAPI — booleans and the splitter. + + This is the one place where the binding deliberately refuses part of + upstream's surface: the two-argument constructors, which execute the + operation during construction. Every app call site also called Build(), + so the boolean ran twice and the first, destructive pass could mutate + inputs shared with the shape cache. Only the default constructor is bound; + operands go in through SetArguments/SetTools and the caller owns Build(). + + Section is the exception in form only: its operands arrive through + Init1/Init2 (Init2 takes a gp_Pln and builds the infinite plane face + itself), which are plain setters, so both are bound. + + BRepAlgoAPI_Algo inherits BOPAlgo_Options *protected* and re-exports the + option setters with using-declarations, so there is no public upcast to + bind against — those go through lambdas on BuilderAlgo. + + Build/Shape/Generated/Modified/IsDeleted all come from + BRepBuilderAPI_MakeShape. The history is the topological-naming substrate: + cad/topology/provenance.py reads it to decide which feature owns which + face, so it must be exact, and un-tracked sub-shapes must raise a catchable + Python exception rather than abort (the app wraps those calls in + try/except). +*/ + +#include "../common/occt_module.h" +#include "../common/occt_policies.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +void register_BRepAlgoAPI(nb::module_ &root) { + nb::module_ m = ocp_submodule(root, "BRepAlgoAPI"); + + // BRepAlgoAPI_Algo is skipped as a declared base: it adds nothing public + // of its own, and nanobind only needs an unambiguous upcast path. + nb::class_( + m, "BRepAlgoAPI_BuilderAlgo") + .def("SetArguments", &BRepAlgoAPI_BuilderAlgo::SetArguments, "theLS"_a) + .def("Arguments", &BRepAlgoAPI_BuilderAlgo::Arguments, OCP_RETURN_COPY) + .def("SetNonDestructive", &BRepAlgoAPI_BuilderAlgo::SetNonDestructive, + "theFlag"_a) + .def("SetGlue", &BRepAlgoAPI_BuilderAlgo::SetGlue, "theGlue"_a) + .def("HasModified", &BRepAlgoAPI_BuilderAlgo::HasModified) + .def("HasGenerated", &BRepAlgoAPI_BuilderAlgo::HasGenerated) + .def("HasDeleted", &BRepAlgoAPI_BuilderAlgo::HasDeleted) + // Inherited from the protected BOPAlgo_Options base through + // using-declarations, so they are reachable by name but not by + // member pointer. + .def( + "SetFuzzyValue", + [](BRepAlgoAPI_BuilderAlgo &self, Standard_Real v) { + self.SetFuzzyValue(v); + }, + "theFuzz"_a) + .def( + "SetRunParallel", + [](BRepAlgoAPI_BuilderAlgo &self, Standard_Boolean f) { + self.SetRunParallel(f); + }, + "theFlag"_a) + .def( + "SetUseOBB", + [](BRepAlgoAPI_BuilderAlgo &self, Standard_Boolean f) { + self.SetUseOBB(f); + }, + "theUseOBB"_a); + + nb::class_( + m, "BRepAlgoAPI_BooleanOperation") + .def("SetTools", &BRepAlgoAPI_BooleanOperation::SetTools, "theLS"_a) + .def("Tools", &BRepAlgoAPI_BooleanOperation::Tools, OCP_RETURN_COPY) + .def("Shape1", &BRepAlgoAPI_BooleanOperation::Shape1, OCP_RETURN_COPY) + .def("Shape2", &BRepAlgoAPI_BooleanOperation::Shape2, OCP_RETURN_COPY); + + nb::class_( + m, "BRepAlgoAPI_Fuse") + .def(nb::init<>()); + nb::class_(m, + "BRepAlgoAPI_Cut") + .def(nb::init<>()); + nb::class_( + m, "BRepAlgoAPI_Common") + .def(nb::init<>()); + + nb::class_( + m, "BRepAlgoAPI_Section") + .def(nb::init<>()) + .def("Init1", + nb::overload_cast(&BRepAlgoAPI_Section::Init1), + "S1"_a) + .def("Init2", + nb::overload_cast(&BRepAlgoAPI_Section::Init2), + "S2"_a) + .def("Init2", nb::overload_cast(&BRepAlgoAPI_Section::Init2), + "Pl"_a) + .def("Approximation", &BRepAlgoAPI_Section::Approximation, "B"_a) + .def("ComputePCurveOn1", &BRepAlgoAPI_Section::ComputePCurveOn1, "B"_a) + .def("ComputePCurveOn2", &BRepAlgoAPI_Section::ComputePCurveOn2, "B"_a); + + nb::class_( + m, "BRepAlgoAPI_Splitter") + .def(nb::init<>()) + .def("SetTools", &BRepAlgoAPI_Splitter::SetTools, "theLS"_a) + .def("Tools", &BRepAlgoAPI_Splitter::Tools, OCP_RETURN_COPY); +} diff --git a/src/modules/mod_BRepBndLib.cpp b/src/modules/mod_BRepBndLib.cpp new file mode 100644 index 0000000..87cf01a --- /dev/null +++ b/src/modules/mod_BRepBndLib.cpp @@ -0,0 +1,36 @@ +/* + OCP.BRepBndLib — bounding box of a shape. + + Only Add is used. ``useTriangulation`` matters to callers: with an + existing mesh it is both faster and tighter than the default, which + enlarges the box by the surfaces' own tolerance. +*/ + +#include "../common/occt_module.h" +#include "../common/occt_policies.h" + +#include +#include +#include + +void register_BRepBndLib(nb::module_ &root) { + nb::module_ m = ocp_submodule(root, "BRepBndLib"); + + nb::class_ cls(m, "BRepBndLib"); + + OCP_DEF_S( + cls, "Add", + [](const TopoDS_Shape &S, Bnd_Box &B, Standard_Boolean useTriangulation) { + BRepBndLib::Add(S, B, useTriangulation); + }, + "S"_a, "B"_a, "useTriangulation"_a = Standard_True, OCP_NOGIL); + + OCP_DEF_S( + cls, "AddOptimal", + [](const TopoDS_Shape &S, Bnd_Box &B, Standard_Boolean useTriangulation, + Standard_Boolean useShapeTolerance) { + BRepBndLib::AddOptimal(S, B, useTriangulation, useShapeTolerance); + }, + "S"_a, "B"_a, "useTriangulation"_a = Standard_True, + "useShapeTolerance"_a = Standard_False, OCP_NOGIL); +} diff --git a/src/modules/mod_BRepBuilderAPI.cpp b/src/modules/mod_BRepBuilderAPI.cpp new file mode 100644 index 0000000..c6c8287 --- /dev/null +++ b/src/modules/mod_BRepBuilderAPI.cpp @@ -0,0 +1,164 @@ +/* + OCP.BRepBuilderAPI — topology construction, and the base class every other + maker in this binding inherits. + + BRepBuilderAPI_MakeShape is where Build/Shape/Generated/Modified/IsDeleted + are declared, so binding them once here covers the booleans, the primitive + makers, the fillet/chamfer builders and the splitter through ordinary + virtual dispatch. That matters beyond tidiness: the app's provenance layer + (cad/topology/provenance.py) is deliberately duck-typed over "anything with + a history", and this is what makes every such builder answer it. + + Generated/Modified return the builder's own list by reference in C++; they + are copied out here, so the Python list stays valid after the builder dies + and its elements follow the by-value sub-shape rule. + + Build() takes a Message_ProgressRange the app never supplies; it is bound + argument-less, per the convention in adding-symbols.md. +*/ + +#include "../common/occt_module.h" +#include "../common/occt_policies.h" +#include "../common/occt_transient.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +void register_BRepBuilderAPI(nb::module_ &root) { + nb::module_ m = ocp_submodule(root, "BRepBuilderAPI"); + + // Abstract bases: registered for the shared surface, never constructed. + nb::class_(m, "BRepBuilderAPI_Command") + .def("IsDone", &BRepBuilderAPI_Command::IsDone); + + nb::class_( + m, "BRepBuilderAPI_MakeShape") + .def( + "Build", + [](BRepBuilderAPI_MakeShape &self) { self.Build(); }, OCP_NOGIL) + .def("Shape", &BRepBuilderAPI_MakeShape::Shape, OCP_RETURN_COPY) + .def("Generated", &BRepBuilderAPI_MakeShape::Generated, "S"_a, + OCP_RETURN_COPY) + .def("Modified", &BRepBuilderAPI_MakeShape::Modified, "S"_a, + OCP_RETURN_COPY) + .def("IsDeleted", &BRepBuilderAPI_MakeShape::IsDeleted, "S"_a); + + // ------------------------------------------------------------ makers -- + + nb::class_( + m, "BRepBuilderAPI_MakeEdge") + .def(nb::init(), "P1"_a, "P2"_a) + .def(nb::init(), "L"_a) + .def(nb::init(), "L"_a) + .def(nb::init &>(), "L"_a) + .def(nb::init &, + const opencascade::handle &>(), + "L"_a, "S"_a) + .def(nb::init &, + const opencascade::handle &, + const Standard_Real, const Standard_Real>(), + "L"_a, "S"_a, "p1"_a, "p2"_a) + .def("Edge", &BRepBuilderAPI_MakeEdge::Edge, OCP_RETURN_COPY); + + nb::class_( + m, "BRepBuilderAPI_MakeWire") + .def(nb::init<>()) + .def(nb::init(), "E"_a) + .def(nb::init(), "W"_a) + .def("Add", nb::overload_cast( + &BRepBuilderAPI_MakeWire::Add), + "E"_a) + .def("Add", nb::overload_cast( + &BRepBuilderAPI_MakeWire::Add), + "W"_a) + .def("Add", nb::overload_cast( + &BRepBuilderAPI_MakeWire::Add), + "L"_a) + .def("Wire", &BRepBuilderAPI_MakeWire::Wire, OCP_RETURN_COPY); + + nb::class_( + m, "BRepBuilderAPI_MakeFace") + .def(nb::init(), "F"_a) + .def(nb::init(), "P"_a) + .def(nb::init(), + "P"_a, "UMin"_a, "UMax"_a, "VMin"_a, "VMax"_a) + .def(nb::init(), "W"_a, + "OnlyPlane"_a = Standard_False) + .def(nb::init &, + const TopoDS_Wire &, const Standard_Boolean>(), + "S"_a, "W"_a, "Inside"_a = Standard_True) + .def("Add", &BRepBuilderAPI_MakeFace::Add, "W"_a) + .def("Face", &BRepBuilderAPI_MakeFace::Face, OCP_RETURN_COPY); + + nb::class_( + m, "BRepBuilderAPI_MakePolygon") + .def(nb::init<>()) + .def(nb::init(), "P1"_a, "P2"_a) + .def(nb::init(), + "P1"_a, "P2"_a, "P3"_a, "Close"_a = Standard_False) + .def(nb::init(), + "P1"_a, "P2"_a, "P3"_a, "P4"_a, "Close"_a = Standard_False) + .def("Add", nb::overload_cast( + &BRepBuilderAPI_MakePolygon::Add), + "P"_a) + .def("Added", &BRepBuilderAPI_MakePolygon::Added) + .def("Close", &BRepBuilderAPI_MakePolygon::Close) + .def("Wire", &BRepBuilderAPI_MakePolygon::Wire, OCP_RETURN_COPY); + + nb::class_( + m, "BRepBuilderAPI_Transform") + .def(nb::init(), "T"_a) + .def(nb::init(), + "theShape"_a, "theTrsf"_a, "theCopyGeom"_a = Standard_False, + "theCopyMesh"_a = Standard_False, OCP_NOGIL) + .def("ModifiedShape", &BRepBuilderAPI_Transform::ModifiedShape, "S"_a, + OCP_RETURN_COPY); + + nb::class_( + m, "BRepBuilderAPI_Copy") + .def(nb::init<>()) + .def(nb::init(), + "S"_a, "copyGeom"_a = Standard_True, + "copyMesh"_a = Standard_False, OCP_NOGIL); + + // Sewing is a transient and not a MakeShape — it has its own result + // accessor rather than Shape(). + nb::class_( + m, "BRepBuilderAPI_Sewing") + .def(ocp_new(), + "tolerance"_a = 1.0e-06, "option1"_a = Standard_True, + "option2"_a = Standard_True, "option3"_a = Standard_True, + "option4"_a = Standard_False) + .def("Add", &BRepBuilderAPI_Sewing::Add, "shape"_a) + .def( + "Perform", [](BRepBuilderAPI_Sewing &self) { self.Perform(); }, + OCP_NOGIL) + .def("SewedShape", &BRepBuilderAPI_Sewing::SewedShape, OCP_RETURN_COPY); +} diff --git a/src/modules/mod_BRepGProp.cpp b/src/modules/mod_BRepGProp.cpp new file mode 100644 index 0000000..5fb56e0 --- /dev/null +++ b/src/modules/mod_BRepGProp.cpp @@ -0,0 +1,73 @@ +/* + OCP.BRepGProp — mass properties of a shape. + + Three statics, all filling a GProp_GProps: LinearProperties (edge length), + SurfaceProperties (area) and VolumeProperties. Both eps overloads are + bound: the default quadrature is inaccurate on spline-bounded faces, and + the app's area readouts may move onto the eps form (see the app's + NOTEPAD). A test already depends on the 3-argument form. + + SurfaceProperties is the single hottest kernel call in a rebuild — 94 % of + face_candidate_anchors — so the GIL is released here and n3xd_ocp.measure + offers the batched form. +*/ + +#include "../common/occt_module.h" +#include "../common/occt_policies.h" + +#include +#include +#include + +void register_BRepGProp(nb::module_ &root) { + nb::module_ m = ocp_submodule(root, "BRepGProp"); + + nb::class_ cls(m, "BRepGProp"); + + OCP_DEF_S( + cls, "LinearProperties", + [](const TopoDS_Shape &S, GProp_GProps &LProps, Standard_Boolean SkipShared, + Standard_Boolean UseTriangulation) { + BRepGProp::LinearProperties(S, LProps, SkipShared, UseTriangulation); + }, + "S"_a, "LProps"_a, "SkipShared"_a = Standard_False, + "UseTriangulation"_a = Standard_False, OCP_NOGIL); + + OCP_DEF_S( + cls, "SurfaceProperties", + [](const TopoDS_Shape &S, GProp_GProps &SProps, Standard_Boolean SkipShared, + Standard_Boolean UseTriangulation) { + BRepGProp::SurfaceProperties(S, SProps, SkipShared, UseTriangulation); + }, + "S"_a, "SProps"_a, "SkipShared"_a = Standard_False, + "UseTriangulation"_a = Standard_False, OCP_NOGIL); + + OCP_DEF_S( + cls, "SurfaceProperties", + [](const TopoDS_Shape &S, GProp_GProps &SProps, Standard_Real Eps, + Standard_Boolean SkipShared) { + return BRepGProp::SurfaceProperties(S, SProps, Eps, SkipShared); + }, + "S"_a, "SProps"_a, "Eps"_a, "SkipShared"_a = Standard_False, OCP_NOGIL); + + OCP_DEF_S( + cls, "VolumeProperties", + [](const TopoDS_Shape &S, GProp_GProps &VProps, Standard_Boolean OnlyClosed, + Standard_Boolean SkipShared, Standard_Boolean UseTriangulation) { + BRepGProp::VolumeProperties(S, VProps, OnlyClosed, SkipShared, + UseTriangulation); + }, + "S"_a, "VProps"_a, "OnlyClosed"_a = Standard_False, + "SkipShared"_a = Standard_False, "UseTriangulation"_a = Standard_False, + OCP_NOGIL); + + OCP_DEF_S( + cls, "VolumeProperties", + [](const TopoDS_Shape &S, GProp_GProps &VProps, Standard_Real Eps, + Standard_Boolean OnlyClosed, Standard_Boolean SkipShared) { + return BRepGProp::VolumeProperties(S, VProps, Eps, OnlyClosed, + SkipShared); + }, + "S"_a, "VProps"_a, "Eps"_a, "OnlyClosed"_a = Standard_False, + "SkipShared"_a = Standard_False, OCP_NOGIL); +} diff --git a/src/modules/mod_BRepMesh.cpp b/src/modules/mod_BRepMesh.cpp new file mode 100644 index 0000000..43397f6 --- /dev/null +++ b/src/modules/mod_BRepMesh.cpp @@ -0,0 +1,35 @@ +/* + OCP.BRepMesh — triangulation. + + One class, always constructed in its executing form: meshing a shape has + no deferred API, and the result is written into the shape rather than + returned. The GIL is released for it — this is one of the longest single + kernel calls a rebuild makes, and the app already runs it with OCCT's own + parallel flag on. + + In 7.9 BRepMesh_IncrementalMesh is a transient (via BRepMesh_DiscretRoot), + so its constructor goes through ocp_new. +*/ + +#include "../common/occt_module.h" +#include "../common/occt_policies.h" +#include "../common/occt_transient.h" + +#include +#include + +void register_BRepMesh(nb::module_ &root) { + nb::module_ m = ocp_submodule(root, "BRepMesh"); + + nb::class_( + m, "BRepMesh_IncrementalMesh") + .def(ocp_new()) + .def(ocp_new(), + "theShape"_a, "theLinDeflection"_a, + "isRelative"_a = Standard_False, "theAngDeflection"_a = 0.5, + "isInParallel"_a = Standard_False, OCP_NOGIL) + .def("IsDone", &BRepMesh_IncrementalMesh::IsDone) + .def("IsModified", &BRepMesh_IncrementalMesh::IsModified); +} diff --git a/src/modules/mod_BRepPrimAPI.cpp b/src/modules/mod_BRepPrimAPI.cpp new file mode 100644 index 0000000..8ee354c --- /dev/null +++ b/src/modules/mod_BRepPrimAPI.cpp @@ -0,0 +1,110 @@ +/* + OCP.BRepPrimAPI — primitive solids and sweeps. + + Every one of these is a BRepBuilderAPI_MakeShape, so Build/Shape and the + history come from there. MakePrism matters for history in particular: an + extrude's provenance is read off it exactly as a boolean's is. + + These constructors do build their shape, and there is no deferred form to + prefer — unlike the BRepAlgoAPI booleans, whose two-argument constructors + duplicate an explicit Build(). They are bound as stock does. + + MakeHalfSpace produces an infinite solid used as a clipping operand; its + result comes back through Solid(), not Shape(). +*/ + +#include "../common/occt_module.h" +#include "../common/occt_policies.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +void register_BRepPrimAPI(nb::module_ &root) { + nb::module_ m = ocp_submodule(root, "BRepPrimAPI"); + + nb::class_( + m, "BRepPrimAPI_MakeBox") + .def(nb::init(), + "dx"_a, "dy"_a, "dz"_a) + .def(nb::init(), + "P"_a, "dx"_a, "dy"_a, "dz"_a) + .def(nb::init(), "P1"_a, "P2"_a) + .def(nb::init(), + "Axes"_a, "dx"_a, "dy"_a, "dz"_a); + + nb::class_( + m, "BRepPrimAPI_MakeCylinder") + .def(nb::init(), "R"_a, "H"_a) + .def(nb::init(), + "R"_a, "H"_a, "Angle"_a) + .def(nb::init(), + "Axes"_a, "R"_a, "H"_a) + .def(nb::init(), + "Axes"_a, "R"_a, "H"_a, "Angle"_a); + + nb::class_( + m, "BRepPrimAPI_MakeCone") + .def(nb::init(), + "R1"_a, "R2"_a, "H"_a) + .def(nb::init(), + "Axes"_a, "R1"_a, "R2"_a, "H"_a); + + nb::class_( + m, "BRepPrimAPI_MakeHalfSpace") + .def(nb::init(), "Face"_a, + "RefPnt"_a) + .def(nb::init(), "Shell"_a, + "RefPnt"_a) + .def("Solid", &BRepPrimAPI_MakeHalfSpace::Solid, OCP_RETURN_COPY); + + // Only the finite (gp_Vec) form is bound. The sibling overload takes a + // gp_Dir for a semi-infinite prism, and gp_Dir converts implicitly from + // gp_Vec — so binding both invites a call meant for one to land silently + // on the other, with valid but wrong geometry. The app builds finite + // prisms only. + nb::class_( + m, "BRepPrimAPI_MakePrism") + .def(nb::init(), + "S"_a, "V"_a, "Copy"_a = Standard_False, + "Canonize"_a = Standard_True, OCP_NOGIL) + .def("FirstShape", + nb::overload_cast<>(&BRepPrimAPI_MakePrism::FirstShape), + OCP_RETURN_COPY) + .def("LastShape", nb::overload_cast<>(&BRepPrimAPI_MakePrism::LastShape), + OCP_RETURN_COPY); + + nb::class_( + m, "BRepPrimAPI_MakeRevol") + .def(nb::init(), + "S"_a, "A"_a, "D"_a, "Copy"_a = Standard_False, OCP_NOGIL) + .def(nb::init(), + "S"_a, "A"_a, "Copy"_a = Standard_False, OCP_NOGIL) + .def("FirstShape", + nb::overload_cast<>(&BRepPrimAPI_MakeRevol::FirstShape), + OCP_RETURN_COPY) + .def("LastShape", nb::overload_cast<>(&BRepPrimAPI_MakeRevol::LastShape), + OCP_RETURN_COPY); +} diff --git a/src/modules/mod_Bnd.cpp b/src/modules/mod_Bnd.cpp new file mode 100644 index 0000000..816b5b6 --- /dev/null +++ b/src/modules/mod_Bnd.cpp @@ -0,0 +1,54 @@ +/* + OCP.Bnd — axis-aligned bounding boxes. + + Get() returns six scalar out-parameters as a tuple, which is how the app + unpacks it (`xmin, ymin, zmin, xmax, ymax, zmax = box.Get()`) and what + upstream OCP does. A void box raises from OCCT, so callers check IsVoid + first. +*/ + +#include "../common/occt_module.h" +#include "../common/occt_policies.h" + +#include + +#include +#include +#include + +void register_Bnd(nb::module_ &root) { + nb::module_ m = ocp_submodule(root, "Bnd"); + + nb::class_(m, "Bnd_Box") + .def(nb::init<>()) + .def(nb::init(), "theMin"_a, "theMax"_a) + .def("Get", + [](const Bnd_Box &self) { + Standard_Real xmin = 0.0, ymin = 0.0, zmin = 0.0; + Standard_Real xmax = 0.0, ymax = 0.0, zmax = 0.0; + self.Get(xmin, ymin, zmin, xmax, ymax, zmax); + return std::make_tuple(xmin, ymin, zmin, xmax, ymax, zmax); + }) + .def("CornerMin", &Bnd_Box::CornerMin, OCP_RETURN_COPY) + .def("CornerMax", &Bnd_Box::CornerMax, OCP_RETURN_COPY) + .def("IsVoid", &Bnd_Box::IsVoid) + .def("SetVoid", &Bnd_Box::SetVoid) + .def("SetGap", &Bnd_Box::SetGap, "Tol"_a) + .def("GetGap", &Bnd_Box::GetGap) + .def("Add", nb::overload_cast(&Bnd_Box::Add), "Other"_a) + .def("Add", nb::overload_cast(&Bnd_Box::Add), "P"_a) + .def("Update", nb::overload_cast( + &Bnd_Box::Update), + "aXmin"_a, "aYmin"_a, "aZmin"_a, "aXmax"_a, "aYmax"_a, "aZmax"_a) + .def("Enlarge", &Bnd_Box::Enlarge, "Tol"_a) + .def("IsOut", nb::overload_cast(&Bnd_Box::IsOut, + nb::const_), + "P"_a) + .def("IsOut", nb::overload_cast(&Bnd_Box::IsOut, + nb::const_), + "Other"_a) + .def("SquareExtent", &Bnd_Box::SquareExtent) + .def("Transformed", &Bnd_Box::Transformed, "T"_a, OCP_RETURN_COPY); +} diff --git a/src/modules/mod_GC.cpp b/src/modules/mod_GC.cpp new file mode 100644 index 0000000..9b2116a --- /dev/null +++ b/src/modules/mod_GC.cpp @@ -0,0 +1,25 @@ +/* + OCP.GC — 3D geometry constructors. + + One use: the sketch builder turns a three-point arc into a trimmed circle + before handing it to BRepBuilderAPI_MakeEdge. +*/ + +#include "../common/occt_module.h" + +#include +#include +#include + +void register_GC(nb::module_ &root) { + nb::module_ m = ocp_submodule(root, "GC"); + + nb::class_(m, "GC_MakeArcOfCircle") + .def(nb::init(), "P1"_a, + "P2"_a, "P3"_a) + .def(nb::init(), + "Circ"_a, "P1"_a, "P2"_a, "Sense"_a) + .def("IsDone", &GC_MakeArcOfCircle::IsDone) + .def("Value", &GC_MakeArcOfCircle::Value); +} diff --git a/src/modules/mod_GProp.cpp b/src/modules/mod_GProp.cpp new file mode 100644 index 0000000..22631d2 --- /dev/null +++ b/src/modules/mod_GProp.cpp @@ -0,0 +1,57 @@ +/* + OCP.GProp — the result object every mass measurement writes into. + + BRepGProp fills a GProp_GProps; the app reads Mass() (which is length, area + or volume depending on which BRepGProp entry point ran) and CentreOfMass(). + The principal-axis block is used once, by cad/measurement.py's inertia + readout. + + OCCT reports the moments and radii through scalar out-parameters; those + become tuples here, matching upstream OCP. Object out-parameters stay + out-parameters (see BRep_Tool.Triangulation_s) — the distinction is that a + caller cannot pre-allocate a Standard_Real to be written through. +*/ + +#include "../common/occt_module.h" +#include "../common/occt_policies.h" + +#include + +#include +#include +#include +#include + +void register_GProp(nb::module_ &root) { + nb::module_ m = ocp_submodule(root, "GProp"); + + nb::class_(m, "GProp_PrincipalProps") + .def(nb::init<>()) + .def("Moments", + [](const GProp_PrincipalProps &self) { + Standard_Real ixx = 0.0, iyy = 0.0, izz = 0.0; + self.Moments(ixx, iyy, izz); + return std::make_tuple(ixx, iyy, izz); + }) + .def("RadiusOfGyration", + [](const GProp_PrincipalProps &self) { + Standard_Real rxx = 0.0, ryy = 0.0, rzz = 0.0; + self.RadiusOfGyration(rxx, ryy, rzz); + return std::make_tuple(rxx, ryy, rzz); + }) + .def("FirstAxisOfInertia", &GProp_PrincipalProps::FirstAxisOfInertia, + OCP_RETURN_COPY) + .def("SecondAxisOfInertia", &GProp_PrincipalProps::SecondAxisOfInertia, + OCP_RETURN_COPY) + .def("ThirdAxisOfInertia", &GProp_PrincipalProps::ThirdAxisOfInertia, + OCP_RETURN_COPY); + + nb::class_(m, "GProp_GProps") + .def(nb::init<>()) + .def(nb::init(), "SLocation"_a) + .def("Mass", &GProp_GProps::Mass) + .def("CentreOfMass", &GProp_GProps::CentreOfMass, OCP_RETURN_COPY) + .def("PrincipalProperties", &GProp_GProps::PrincipalProperties, + OCP_RETURN_COPY) + .def("Add", &GProp_GProps::Add, "Item"_a, "Density"_a = 1.0); +} diff --git a/src/modules/mod_Geom.cpp b/src/modules/mod_Geom.cpp new file mode 100644 index 0000000..c544e3c --- /dev/null +++ b/src/modules/mod_Geom.cpp @@ -0,0 +1,87 @@ +/* + OCP.Geom — the analytic curves and surfaces behind the topology. + + The app imports only three of these, but it *receives* the two abstract + bases: BRep_Tool.Surface_s hands back a Handle(Geom_Surface), and + GC_MakeArcOfCircle a Handle(Geom_TrimmedCurve), both of which then travel + into BRepBuilderAPI_MakeEdge/MakeFace. So the bases have to be registered + for those returns to convert, and the concrete leaves for the constructors + the app calls. + + Deliberately partial: the concrete subclasses OCCT may hand back + (Geom_Plane, Geom_Circle, ...) are not registered, so such a return arrives + as its static type, Geom_Surface. That is fine here — the app reads + surface *kinds* off BRepAdaptor_Surface, never off the Geom object, and no + call site does DownCast or isinstance on one. Add a leaf the day one does. + + Surface_s returns the face's live surface when the location is identity, a + transformed copy otherwise; the handle caster wraps whatever OCCT gives + back without copying, which is upstream's behaviour too. +*/ + +#include "../common/occt_module.h" +#include "../common/occt_policies.h" +#include "../common/occt_transient.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +void register_Geom(nb::module_ &root) { + nb::module_ m = ocp_submodule(root, "Geom"); + + // ------------------------------------------------------------ curves -- + + nb::class_(m, "Geom_Curve") + .def("FirstParameter", &Geom_Curve::FirstParameter) + .def("LastParameter", &Geom_Curve::LastParameter) + .def("IsClosed", &Geom_Curve::IsClosed) + .def("IsPeriodic", &Geom_Curve::IsPeriodic) + .def("Value", &Geom_Curve::Value, "U"_a, OCP_RETURN_COPY); + + nb::class_(m, "Geom_TrimmedCurve"); + + nb::class_(m, "Geom_BSplineCurve") + .def(ocp_new(), + "Poles"_a, "Knots"_a, "Multiplicities"_a, "Degree"_a, + "Periodic"_a = Standard_False) + .def("Degree", &Geom_BSplineCurve::Degree) + .def("NbPoles", &Geom_BSplineCurve::NbPoles) + .def("NbKnots", &Geom_BSplineCurve::NbKnots); + + // ---------------------------------------------------------- surfaces -- + + nb::class_(m, "Geom_Surface") + .def("Value", &Geom_Surface::Value, "U"_a, "V"_a, OCP_RETURN_COPY) + .def("IsUPeriodic", &Geom_Surface::IsUPeriodic) + .def("IsVPeriodic", &Geom_Surface::IsVPeriodic) + .def("IsUClosed", &Geom_Surface::IsUClosed) + .def("IsVClosed", &Geom_Surface::IsVClosed); + + nb::class_(m, "Geom_BSplineSurface"); + + nb::class_(m, + "Geom_CylindricalSurface") + .def(ocp_new(), + "A3"_a, "Radius"_a) + .def("Radius", &Geom_CylindricalSurface::Radius); + + nb::class_(m, "Geom_OffsetSurface") + .def(ocp_new &, + const Standard_Real, const Standard_Boolean>(), + "S"_a, "Offset"_a, "isNotCheckC0"_a = Standard_False) + .def("Offset", &Geom_OffsetSurface::Offset) + .def("BasisSurface", &Geom_OffsetSurface::BasisSurface); +} diff --git a/src/modules/mod_Geom2d.cpp b/src/modules/mod_Geom2d.cpp new file mode 100644 index 0000000..629657b --- /dev/null +++ b/src/modules/mod_Geom2d.cpp @@ -0,0 +1,30 @@ +/* + OCP.Geom2d — parametric-space curves. + + Not imported by the app, but unavoidable: GCE2d_MakeLine/MakeSegment return + these, and BRepBuilderAPI_MakeEdge takes the Geom2d_Curve base when it + builds an edge on a surface (thread.py lays a helix out as a line in the + cylinder's UV space). Only registration matters — no method on them is + ever called. + + The declared base is Standard_Transient, skipping Geom2d_Geometry: nanobind + needs an unambiguous public upcast, not the full chain. +*/ + +#include "../common/occt_module.h" + +#include +#include +#include + +void register_Geom2d(nb::module_ &root) { + nb::module_ m = ocp_submodule(root, "Geom2d"); + + // Abstract: no constructor. Registered so handles to it convert at all. + nb::class_(m, "Geom2d_Curve") + .def("FirstParameter", &Geom2d_Curve::FirstParameter) + .def("LastParameter", &Geom2d_Curve::LastParameter); + + nb::class_(m, "Geom2d_Line"); + nb::class_(m, "Geom2d_TrimmedCurve"); +} diff --git a/src/modules/mod_GeomAPI.cpp b/src/modules/mod_GeomAPI.cpp new file mode 100644 index 0000000..309c841 --- /dev/null +++ b/src/modules/mod_GeomAPI.cpp @@ -0,0 +1,59 @@ +/* + OCP.GeomAPI — point projection and surface fitting. + + Both compute in their constructor and expose no deferred Build(), so they + are bound exactly as stock does. The no-executing-constructor rule targets + the BRepAlgoAPI booleans, where a deferred SetX/Build form exists and the + constructor silently duplicates it; it does not apply here (see + adding-symbols.md). + + LowerDistanceParameters returns its two scalars as a tuple, which is how + the app unpacks it. ProjectPointOnSurf keeps the GIL: it is measured in + microseconds and runs inside per-face Python loops, where the release + handshake would cost more than the call. +*/ + +#include "../common/occt_module.h" +#include "../common/occt_policies.h" + +#include + +#include +#include +#include +#include +#include +#include +#include + +void register_GeomAPI(nb::module_ &root) { + nb::module_ m = ocp_submodule(root, "GeomAPI"); + + nb::class_(m, "GeomAPI_ProjectPointOnSurf") + // The Extrema_ExtAlgo overload is left out: binding it would drag in + // the Extrema enum only to carry a default the app never overrides, + // and an unregistered default argument fails at module-init time. + .def(nb::init &>(), + "P"_a, "Surface"_a) + .def("IsDone", &GeomAPI_ProjectPointOnSurf::IsDone) + .def("NbPoints", &GeomAPI_ProjectPointOnSurf::NbPoints) + .def("LowerDistance", &GeomAPI_ProjectPointOnSurf::LowerDistance) + .def("NearestPoint", &GeomAPI_ProjectPointOnSurf::NearestPoint, + OCP_RETURN_COPY) + .def("LowerDistanceParameters", + [](const GeomAPI_ProjectPointOnSurf &self) { + Standard_Real u = 0.0, v = 0.0; + self.LowerDistanceParameters(u, v); + return std::make_tuple(u, v); + }); + + nb::class_(m, "GeomAPI_PointsToBSplineSurface") + .def(nb::init<>()) + .def(nb::init(), + "Points"_a, "DegMin"_a = 3, "DegMax"_a = 8, + "Continuity"_a = GeomAbs_C2, "Tol3D"_a = 1.0e-3, OCP_NOGIL) + .def("IsDone", &GeomAPI_PointsToBSplineSurface::IsDone) + .def("Surface", &GeomAPI_PointsToBSplineSurface::Surface); +} diff --git a/src/modules/mod_GeomAbs.cpp b/src/modules/mod_GeomAbs.cpp new file mode 100644 index 0000000..192d4e1 --- /dev/null +++ b/src/modules/mod_GeomAbs.cpp @@ -0,0 +1,55 @@ +/* + OCP.GeomAbs — the curve/surface type tags the adaptors report. + + The app reads these off BRepAdaptor_Curve/Surface.GetType() to decide what + a face or edge *is* (plane, cylinder, circle, spline), which drives + reference inference, anchor scoring and hole recognition. Only the + continuity value GeomAbs_C2 is used as an input, by + GeomAPI_PointsToBSplineSurface. +*/ + +#include "../common/occt_module.h" + +#include +#include +#include + +void register_GeomAbs(nb::module_ &root) { + nb::module_ m = ocp_submodule(root, "GeomAbs"); + + nb::enum_(m, "GeomAbs_CurveType", nb::is_arithmetic()) + .value("GeomAbs_Line", GeomAbs_Line) + .value("GeomAbs_Circle", GeomAbs_Circle) + .value("GeomAbs_Ellipse", GeomAbs_Ellipse) + .value("GeomAbs_Hyperbola", GeomAbs_Hyperbola) + .value("GeomAbs_Parabola", GeomAbs_Parabola) + .value("GeomAbs_BezierCurve", GeomAbs_BezierCurve) + .value("GeomAbs_BSplineCurve", GeomAbs_BSplineCurve) + .value("GeomAbs_OffsetCurve", GeomAbs_OffsetCurve) + .value("GeomAbs_OtherCurve", GeomAbs_OtherCurve) + .export_values(); + + nb::enum_(m, "GeomAbs_SurfaceType", nb::is_arithmetic()) + .value("GeomAbs_Plane", GeomAbs_Plane) + .value("GeomAbs_Cylinder", GeomAbs_Cylinder) + .value("GeomAbs_Cone", GeomAbs_Cone) + .value("GeomAbs_Sphere", GeomAbs_Sphere) + .value("GeomAbs_Torus", GeomAbs_Torus) + .value("GeomAbs_BezierSurface", GeomAbs_BezierSurface) + .value("GeomAbs_BSplineSurface", GeomAbs_BSplineSurface) + .value("GeomAbs_SurfaceOfRevolution", GeomAbs_SurfaceOfRevolution) + .value("GeomAbs_SurfaceOfExtrusion", GeomAbs_SurfaceOfExtrusion) + .value("GeomAbs_OffsetSurface", GeomAbs_OffsetSurface) + .value("GeomAbs_OtherSurface", GeomAbs_OtherSurface) + .export_values(); + + nb::enum_(m, "GeomAbs_Shape", nb::is_arithmetic()) + .value("GeomAbs_C0", GeomAbs_C0) + .value("GeomAbs_G1", GeomAbs_G1) + .value("GeomAbs_C1", GeomAbs_C1) + .value("GeomAbs_G2", GeomAbs_G2) + .value("GeomAbs_C2", GeomAbs_C2) + .value("GeomAbs_C3", GeomAbs_C3) + .value("GeomAbs_CN", GeomAbs_CN) + .export_values(); +} diff --git a/src/modules/mod_GeomLProp.cpp b/src/modules/mod_GeomLProp.cpp new file mode 100644 index 0000000..ef666b4 --- /dev/null +++ b/src/modules/mod_GeomLProp.cpp @@ -0,0 +1,32 @@ +/* + OCP.GeomLProp — local surface properties. + + One use: text emboss reads the surface normal at the anchor's UV to decide + which way the glyph solid grows out of a curved face. + + OCCT 8.0 supersedes this package with GeomProp/BRepProp (results come back + as structs carrying IsDefined flags instead of throwing). Only Normal() + is bound, so the port is a one-line move — see design.md's 8.0 watchlist. +*/ + +#include "../common/occt_module.h" +#include "../common/occt_policies.h" + +#include +#include +#include +#include + +void register_GeomLProp(nb::module_ &root) { + nb::module_ m = ocp_submodule(root, "GeomLProp"); + + nb::class_(m, "GeomLProp_SLProps") + .def(nb::init &, const Standard_Real, + const Standard_Real, const Standard_Integer, + const Standard_Real>(), + "S"_a, "U"_a, "V"_a, "N"_a, "Resolution"_a) + .def("Value", &GeomLProp_SLProps::Value, OCP_RETURN_COPY) + .def("Normal", &GeomLProp_SLProps::Normal, OCP_RETURN_COPY) + .def("IsNormalDefined", &GeomLProp_SLProps::IsNormalDefined) + .def("SetParameters", &GeomLProp_SLProps::SetParameters, "U"_a, "V"_a); +} diff --git a/src/modules/mod_TColStd.cpp b/src/modules/mod_TColStd.cpp new file mode 100644 index 0000000..22e5459 --- /dev/null +++ b/src/modules/mod_TColStd.cpp @@ -0,0 +1,67 @@ +/* + OCP.TColStd — the plain integer/real arrays a B-spline definition needs + (knots and multiplicities in sketch_builder/edges.py), plus the string + sequence the IGES reader fills with unit names. + + NCollection's SetValue/Value are overloaded (const ref and rvalue), so they + are bound through lambdas rather than member pointers. +*/ + +#include "../common/occt_module.h" +#include "../common/occt_policies.h" + +#include +#include +#include + +namespace { + +/// Bind one NCollection_Array1 instantiation with the app's surface. +template +void bind_array1(nb::module_ &m, const char *name) { + nb::class_(m, name) + .def(nb::init(), + "theLower"_a, "theUpper"_a) + .def( + "SetValue", + [](A &self, Standard_Integer index, const T &value) { + self.SetValue(index, value); + }, + "theIndex"_a, "theValue"_a) + .def( + "Value", + [](const A &self, Standard_Integer index) { return self.Value(index); }, + "theIndex"_a) + .def("Length", &A::Length) + .def("Lower", &A::Lower) + .def("Upper", &A::Upper) + .def("__len__", &A::Length); +} + +} // namespace + +void register_TColStd(nb::module_ &root) { + nb::module_ m = ocp_submodule(root, "TColStd"); + + bind_array1( + m, "TColStd_Array1OfInteger"); + bind_array1(m, "TColStd_Array1OfReal"); + + nb::class_(m, "TColStd_SequenceOfAsciiString") + .def(nb::init<>()) + .def("Length", &TColStd_SequenceOfAsciiString::Length) + .def("IsEmpty", &TColStd_SequenceOfAsciiString::IsEmpty) + .def( + "Value", + [](const TColStd_SequenceOfAsciiString &self, Standard_Integer i) { + return self.Value(i); + }, + "theIndex"_a) + .def( + "Append", + [](TColStd_SequenceOfAsciiString &self, + const TCollection_AsciiString &v) { self.Append(v); }, + "theItem"_a) + .def("Clear", [](TColStd_SequenceOfAsciiString &self) { self.Clear(); }) + .def("__len__", &TColStd_SequenceOfAsciiString::Length); +} diff --git a/src/modules/mod_TColgp.cpp b/src/modules/mod_TColgp.cpp new file mode 100644 index 0000000..b41a887 --- /dev/null +++ b/src/modules/mod_TColgp.cpp @@ -0,0 +1,59 @@ +/* + OCP.TColgp — point arrays. + + Array1 carries B-spline poles (sketch_builder/edges.py); Array2 carries the + control grid GeomAPI_PointsToBSplineSurface fits a smooth surface through. +*/ + +#include "../common/occt_module.h" + +#include +#include +#include + +void register_TColgp(nb::module_ &root) { + nb::module_ m = ocp_submodule(root, "TColgp"); + + nb::class_(m, "TColgp_Array1OfPnt") + .def(nb::init(), + "theLower"_a, "theUpper"_a) + .def( + "SetValue", + [](TColgp_Array1OfPnt &self, Standard_Integer i, const gp_Pnt &v) { + self.SetValue(i, v); + }, + "theIndex"_a, "theValue"_a) + .def( + "Value", + [](const TColgp_Array1OfPnt &self, Standard_Integer i) { + return self.Value(i); + }, + "theIndex"_a) + .def("Length", &TColgp_Array1OfPnt::Length) + .def("Lower", &TColgp_Array1OfPnt::Lower) + .def("Upper", &TColgp_Array1OfPnt::Upper) + .def("__len__", &TColgp_Array1OfPnt::Length); + + nb::class_(m, "TColgp_Array2OfPnt") + .def(nb::init(), + "theRowLower"_a, "theRowUpper"_a, "theColLower"_a, "theColUpper"_a) + .def( + "SetValue", + [](TColgp_Array2OfPnt &self, Standard_Integer row, + Standard_Integer col, const gp_Pnt &v) { + self.SetValue(row, col, v); + }, + "theRow"_a, "theCol"_a, "theValue"_a) + .def( + "Value", + [](const TColgp_Array2OfPnt &self, Standard_Integer row, + Standard_Integer col) { return self.Value(row, col); }, + "theRow"_a, "theCol"_a) + .def("RowLength", &TColgp_Array2OfPnt::RowLength) + .def("ColLength", &TColgp_Array2OfPnt::ColLength) + .def("LowerRow", &TColgp_Array2OfPnt::LowerRow) + .def("UpperRow", &TColgp_Array2OfPnt::UpperRow) + .def("LowerCol", &TColgp_Array2OfPnt::LowerCol) + .def("UpperCol", &TColgp_Array2OfPnt::UpperCol); +} diff --git a/src/modules/mod_TCollection.cpp b/src/modules/mod_TCollection.cpp new file mode 100644 index 0000000..7327e1d --- /dev/null +++ b/src/modules/mod_TCollection.cpp @@ -0,0 +1,24 @@ +/* + OCP.TCollection — OCCT's own string type. + + Not imported by the app directly: it arrives as the element type of + TColStd_SequenceOfAsciiString, which the IGES reader fills with unit names + (`import_.py` reads them back through ToCString). +*/ + +#include "../common/occt_module.h" + +#include + +void register_TCollection(nb::module_ &root) { + nb::module_ m = ocp_submodule(root, "TCollection"); + + nb::class_(m, "TCollection_AsciiString") + .def(nb::init<>()) + .def(nb::init(), "message"_a) + .def("ToCString", &TCollection_AsciiString::ToCString) + .def("Length", &TCollection_AsciiString::Length) + .def("IsEmpty", &TCollection_AsciiString::IsEmpty) + .def("__len__", &TCollection_AsciiString::Length) + .def("__str__", &TCollection_AsciiString::ToCString); +} diff --git a/src/modules/mod_gp.cpp b/src/modules/mod_gp.cpp index 7bde405..b64d430 100644 --- a/src/modules/mod_gp.cpp +++ b/src/modules/mod_gp.cpp @@ -1,19 +1,33 @@ /* - OCP.gp — the Inc 0 subset (points, vectors, directions, axes, transforms). + OCP.gp — points, vectors, directions, axes, planes and 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. + The quadrics (gp_Cylinder/Cone/Sphere/Elips) are not imported by the app + directly; they arrive as return values of the adaptors' GetType() branches + (`BRepAdaptor_Surface(f).Cylinder().Radius()`), so they have to be + registered for those returns to convert at all. */ #include "../common/occt_module.h" #include "../common/occt_policies.h" #include +#include +#include +#include +#include +#include #include +#include +#include +#include +#include #include +#include +#include +#include #include #include +#include #include void register_gp(nb::module_ &root) { @@ -81,6 +95,9 @@ void register_gp(nb::module_ &root) { .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("Added", &gp_Vec::Added, "Other"_a, OCP_RETURN_COPY) + .def("Subtracted", &gp_Vec::Subtracted, "Right"_a, OCP_RETURN_COPY) + .def("Multiply", &gp_Vec::Multiply, "Scalar"_a) .def("Transform", &gp_Vec::Transform, "T"_a) .def("Transformed", &gp_Vec::Transformed, "T"_a, OCP_RETURN_COPY); @@ -123,6 +140,9 @@ void register_gp(nb::module_ &root) { nb::overload_cast( &gp_Trsf::SetRotation), "A1"_a, "Ang"_a) + .def("SetRotation", + nb::overload_cast(&gp_Trsf::SetRotation), + "R"_a) .def("SetScale", &gp_Trsf::SetScale, "P"_a, "S"_a) .def("SetMirror", nb::overload_cast(&gp_Trsf::SetMirror), "A1"_a) @@ -135,4 +155,160 @@ void register_gp(nb::module_ &root) { .def("Multiply", &gp_Trsf::Multiply, "T"_a) .def("PreMultiply", &gp_Trsf::PreMultiply, "T"_a) .def("Invert", &gp_Trsf::Invert); + + nb::class_(m, "gp_Quaternion") + .def(nb::init<>()) + // (x, y, z, w) — note the scalar comes *last*, which is the opposite + // of the (w, x, y, z) order most serialisations use. + .def(nb::init(), + "x"_a, "y"_a, "z"_a, "w"_a) + .def(nb::init(), "theAxis"_a, + "theAngle"_a) + .def("X", &gp_Quaternion::X) + .def("Y", &gp_Quaternion::Y) + .def("Z", &gp_Quaternion::Z) + .def("W", &gp_Quaternion::W) + .def("Normalize", &gp_Quaternion::Normalize) + .def("Normalized", &gp_Quaternion::Normalized, OCP_RETURN_COPY) + .def("GetRotationAngle", &gp_Quaternion::GetRotationAngle); + + // ---------------------------------------------------------------- 2D -- + + nb::class_(m, "gp_Pnt2d") + .def(nb::init<>()) + .def(nb::init(), "Xp"_a, "Yp"_a) + .def("X", &gp_Pnt2d::X) + .def("Y", &gp_Pnt2d::Y) + .def("SetX", &gp_Pnt2d::SetX, "X"_a) + .def("SetY", &gp_Pnt2d::SetY, "Y"_a) + .def("Distance", &gp_Pnt2d::Distance, "Other"_a); + + nb::class_(m, "gp_Dir2d") + .def(nb::init<>()) + .def(nb::init(), "Xv"_a, "Yv"_a) + .def("X", &gp_Dir2d::X) + .def("Y", &gp_Dir2d::Y) + .def("Angle", &gp_Dir2d::Angle, "Other"_a) + .def("Reversed", &gp_Dir2d::Reversed, OCP_RETURN_COPY); + + nb::class_(m, "gp_Vec2d") + .def(nb::init<>()) + .def(nb::init(), "Xv"_a, "Yv"_a) + .def(nb::init(), "V"_a) + .def("X", &gp_Vec2d::X) + .def("Y", &gp_Vec2d::Y) + .def("Magnitude", &gp_Vec2d::Magnitude); + + // -------------------------------------------------------------- axes -- + + nb::class_(m, "gp_Ax2") + .def(nb::init<>()) + .def(nb::init(), "P"_a, "V"_a) + .def(nb::init(), "P"_a, + "N"_a, "Vx"_a) + .def("Location", &gp_Ax2::Location, OCP_RETURN_COPY) + .def("Direction", &gp_Ax2::Direction, OCP_RETURN_COPY) + .def("XDirection", &gp_Ax2::XDirection, OCP_RETURN_COPY) + .def("YDirection", &gp_Ax2::YDirection, OCP_RETURN_COPY) + .def("Axis", &gp_Ax2::Axis, OCP_RETURN_COPY) + .def("SetLocation", &gp_Ax2::SetLocation, "theP"_a) + .def("SetDirection", &gp_Ax2::SetDirection, "theV"_a) + .def("Rotate", &gp_Ax2::Rotate, "theA1"_a, "theAng"_a) + .def("Rotated", &gp_Ax2::Rotated, "theA1"_a, "theAng"_a, + OCP_RETURN_COPY) + .def("Transform", &gp_Ax2::Transform, "theT"_a) + .def("Transformed", &gp_Ax2::Transformed, "theT"_a, OCP_RETURN_COPY); + + nb::class_(m, "gp_Ax3") + .def(nb::init<>()) + .def(nb::init(), "theA"_a) + .def(nb::init(), "theP"_a, "theN"_a) + .def(nb::init(), + "theP"_a, "theN"_a, "theVx"_a) + .def("Location", &gp_Ax3::Location, OCP_RETURN_COPY) + .def("Direction", &gp_Ax3::Direction, OCP_RETURN_COPY) + .def("XDirection", &gp_Ax3::XDirection, OCP_RETURN_COPY) + .def("YDirection", &gp_Ax3::YDirection, OCP_RETURN_COPY) + .def("Axis", &gp_Ax3::Axis, OCP_RETURN_COPY) + .def("Ax2", &gp_Ax3::Ax2, OCP_RETURN_COPY) + .def("Direct", &gp_Ax3::Direct) + .def("SetLocation", &gp_Ax3::SetLocation, "theP"_a) + .def("SetDirection", &gp_Ax3::SetDirection, "theV"_a) + .def("Rotate", &gp_Ax3::Rotate, "theA1"_a, "theAng"_a) + .def("Rotated", &gp_Ax3::Rotated, "theA1"_a, "theAng"_a, + OCP_RETURN_COPY) + .def("Transform", &gp_Ax3::Transform, "theT"_a) + .def("Transformed", &gp_Ax3::Transformed, "theT"_a, OCP_RETURN_COPY); + + // ------------------------------------------------- curves & quadrics -- + + nb::class_(m, "gp_Lin") + .def(nb::init<>()) + .def(nb::init(), "theP"_a, "theV"_a) + .def(nb::init(), "theA1"_a) + .def("Location", &gp_Lin::Location, OCP_RETURN_COPY) + .def("Direction", &gp_Lin::Direction, OCP_RETURN_COPY) + .def("Position", &gp_Lin::Position, OCP_RETURN_COPY) + .def("Distance", nb::overload_cast(&gp_Lin::Distance, + nb::const_), + "theP"_a); + + nb::class_(m, "gp_Circ") + .def(nb::init<>()) + .def(nb::init(), "theA2"_a, + "theRadius"_a) + .def("Location", &gp_Circ::Location, OCP_RETURN_COPY) + .def("Axis", &gp_Circ::Axis, OCP_RETURN_COPY) + .def("Position", &gp_Circ::Position, OCP_RETURN_COPY) + .def("Radius", &gp_Circ::Radius); + + nb::class_(m, "gp_Elips") + .def(nb::init<>()) + .def("Location", &gp_Elips::Location, OCP_RETURN_COPY) + .def("Axis", &gp_Elips::Axis, OCP_RETURN_COPY) + .def("Position", &gp_Elips::Position, OCP_RETURN_COPY) + .def("MajorRadius", &gp_Elips::MajorRadius) + .def("MinorRadius", &gp_Elips::MinorRadius); + + nb::class_(m, "gp_Pln") + .def(nb::init<>()) + .def(nb::init(), "theP"_a, "theV"_a) + .def(nb::init(), "theA3"_a) + .def("Location", &gp_Pln::Location, OCP_RETURN_COPY) + .def("Axis", &gp_Pln::Axis, OCP_RETURN_COPY) + .def("Position", &gp_Pln::Position, OCP_RETURN_COPY) + .def("XAxis", &gp_Pln::XAxis, OCP_RETURN_COPY) + .def("YAxis", &gp_Pln::YAxis, OCP_RETURN_COPY) + .def("Distance", nb::overload_cast(&gp_Pln::Distance, + nb::const_), + "theP"_a) + .def("Transform", &gp_Pln::Transform, "theT"_a) + .def("Transformed", &gp_Pln::Transformed, "theT"_a, OCP_RETURN_COPY); + + nb::class_(m, "gp_Cylinder") + .def(nb::init<>()) + .def(nb::init(), "theA3"_a, + "theRadius"_a) + .def("Location", &gp_Cylinder::Location, OCP_RETURN_COPY) + .def("Axis", &gp_Cylinder::Axis, OCP_RETURN_COPY) + .def("Position", &gp_Cylinder::Position, OCP_RETURN_COPY) + .def("Radius", &gp_Cylinder::Radius); + + nb::class_(m, "gp_Cone") + .def(nb::init<>()) + .def("Location", &gp_Cone::Location, OCP_RETURN_COPY) + .def("Axis", &gp_Cone::Axis, OCP_RETURN_COPY) + .def("Position", &gp_Cone::Position, OCP_RETURN_COPY) + .def("Apex", &gp_Cone::Apex, OCP_RETURN_COPY) + .def("RefRadius", &gp_Cone::RefRadius) + .def("SemiAngle", &gp_Cone::SemiAngle); + + nb::class_(m, "gp_Sphere") + .def(nb::init<>()) + .def(nb::init(), "theA3"_a, + "theRadius"_a) + .def("Location", &gp_Sphere::Location, OCP_RETURN_COPY) + .def("Position", &gp_Sphere::Position, OCP_RETURN_COPY) + .def("Radius", &gp_Sphere::Radius); } diff --git a/tests/data/manifest.json b/tests/data/manifest.json index f4a5cad..03a85c5 100644 --- a/tests/data/manifest.json +++ b/tests/data/manifest.json @@ -32,5 +32,472 @@ "size": 171, "faces": 0 } + }, + "inc1": { + "measure": { + "box": { + "volume": 6000.0, + "area": 2200.0, + "bbox": [ + -1e-07, + -1e-07, + -1e-07, + 10.0000001, + 20.0000001, + 30.0000001 + ], + "n_faces": 6, + "n_edges": 12, + "n_solids": 1 + }, + "box_meshed": { + "volume": 6000.0, + "area": 2200.0, + "bbox": [ + -1e-07, + -1e-07, + -1e-07, + 10.0000001, + 20.0000001, + 30.0000001 + ], + "n_faces": 6, + "n_edges": 12, + "n_solids": 1 + }, + "fused": { + "volume": 12500.0, + "area": 3650.0, + "bbox": [ + -1.5e-07, + -1.5e-07, + -1.5e-07, + 25.00000015, + 25.00000015, + 30.00000015 + ], + "n_faces": 12, + "n_edges": 30, + "n_solids": 1 + }, + "cut_cylinder": { + "volume": 5151.769983530756, + "area": 2708.9380098815463, + "bbox": [ + -1e-07, + -1e-07, + -1e-07, + 10.0000001, + 20.0000001, + 30.0000001 + ], + "n_faces": 7, + "n_edges": 15, + "n_solids": 1 + }, + "located_compound": { + "volume": 12000.0, + "area": 4400.0, + "bbox": [ + -1e-07, + -1e-07, + -1e-07, + 60.0000001, + 20.0000001, + 30.0000001 + ], + "n_faces": 12, + "n_edges": 24, + "n_solids": 2 + }, + "empty_compound": { + "volume": 0.0, + "area": 0.0, + "bbox": null, + "n_faces": 0, + "n_edges": 0, + "n_solids": 0 + } + }, + "face_props": { + "box": [ + [ + 600.0, + 0.0, + 10.0, + 15.0 + ], + [ + 600.0, + 10.0, + 10.0, + 15.0 + ], + [ + 300.0, + 5.0, + 0.0, + 15.0 + ], + [ + 300.0, + 5.0, + 20.0, + 15.0 + ], + [ + 199.99999999999997, + 5.0, + 10.0, + 0.0 + ], + [ + 199.99999999999997, + 5.0, + 10.0, + 30.0 + ] + ], + "box_meshed": [ + [ + 600.0, + 0.0, + 10.0, + 15.0 + ], + [ + 600.0, + 10.0, + 10.0, + 15.0 + ], + [ + 300.0, + 5.0, + 0.0, + 15.0 + ], + [ + 300.0, + 5.0, + 20.0, + 15.0 + ], + [ + 199.99999999999997, + 5.0, + 10.0, + 0.0 + ], + [ + 199.99999999999997, + 5.0, + 10.0, + 30.0 + ] + ], + "fused": [ + [ + 600.0, + 0.0, + 10.0, + 15.0 + ], + [ + 300.0, + 5.0, + 0.0, + 15.0 + ], + [ + 199.99999999999997, + 5.0, + 10.0, + 30.0 + ], + [ + 200.0, + 3.7500000000000004, + 20.0, + 15.0 + ], + [ + 199.99999999999997, + 5.0, + 10.0, + 0.0 + ], + [ + 300.0, + 10.0, + 7.5, + 14.999999999999996 + ], + [ + 324.99999999999994, + 16.73076923076923, + 15.57692307692308, + 5.0 + ], + [ + 99.99999999999999, + 5.0, + 22.5, + 15.0 + ], + [ + 324.99999999999994, + 16.73076923076923, + 15.57692307692308, + 25.0 + ], + [ + 299.99999999999994, + 17.5, + 5.0, + 15.0 + ], + [ + 399.99999999999994, + 15.0, + 25.0, + 15.0 + ], + [ + 399.99999999999994, + 25.0, + 15.0, + 15.0 + ] + ], + "cut_cylinder": [ + [ + 600.0, + 0.0, + 10.0, + 15.0 + ], + [ + 300.0, + 5.0, + 0.0, + 15.0 + ], + [ + 171.72566611769184, + 5.0, + 10.0, + 30.000000000000004 + ], + [ + 300.0, + 5.0, + 20.0, + 15.0 + ], + [ + 171.72566611769184, + 5.0, + 10.0, + 0.0 + ], + [ + 600.0, + 10.0, + 10.0, + 15.0 + ], + [ + 565.4866776461628, + 5.0, + 10.0, + 15.000000000000002 + ] + ], + "located_compound": [ + [ + 600.0, + 0.0, + 10.0, + 15.0 + ], + [ + 600.0, + 10.0, + 10.0, + 15.0 + ], + [ + 300.0, + 5.0, + 0.0, + 15.0 + ], + [ + 300.0, + 5.0, + 20.0, + 15.0 + ], + [ + 199.99999999999997, + 5.0, + 10.0, + 0.0 + ], + [ + 199.99999999999997, + 5.0, + 10.0, + 30.0 + ], + [ + 600.0, + 50.0, + 10.0, + 15.0 + ], + [ + 600.0, + 60.0, + 10.0, + 15.0 + ], + [ + 300.0, + 55.0, + 0.0, + 15.0 + ], + [ + 300.0, + 55.0, + 20.0, + 15.0 + ], + [ + 199.99999999999997, + 55.0, + 10.0, + 0.0 + ], + [ + 199.99999999999997, + 55.0, + 10.0, + 30.0 + ] + ] + }, + "cut_cylinder_history": [ + { + "pre": 0, + "deleted": false, + "modified": [], + "generated": [] + }, + { + "pre": 1, + "deleted": false, + "modified": [], + "generated": [] + }, + { + "pre": 2, + "deleted": false, + "modified": [], + "generated": [] + }, + { + "pre": 3, + "deleted": false, + "modified": [], + "generated": [] + }, + { + "pre": 4, + "deleted": false, + "modified": [ + 33 + ], + "generated": [] + }, + { + "pre": 5, + "deleted": false, + "modified": [ + 20 + ], + "generated": [] + } + ], + "box_meshed_counts": [ + [ + 0, + 4, + 2 + ], + [ + 1, + 4, + 2 + ], + [ + 2, + 4, + 2 + ], + [ + 3, + 4, + 2 + ], + [ + 4, + 4, + 2 + ], + [ + 5, + 4, + 2 + ] + ], + "constructions": { + "arc_edge": { + "sha256": "2b6eadbe678c8e13d04cfa1fdf6d6d475776d168639dbb38b90452c9f1481535", + "length": 15.707963267948966 + }, + "spline_edge": { + "sha256": "71a9b3c8aee46b338728fc03b98931d4087c5df5962e9745d70fcfd3cc1a3133", + "length": 12.236066959998437 + }, + "prism": { + "sha256": "0730ad0a74dda44760b4a43b4b7f22ed58c672d8aeb8f3fd483969b92cbf3d2f", + "volume": 239.99999999999997, + "area": 247.99999999999994, + "bbox": [ + -1.0000000000000002e-07, + -1.0000000000000002e-07, + -1.0000000000000002e-07, + 10.0000001, + 6.0000001, + 4.0000001 + ], + "n_faces": 6, + "n_edges": 12, + "n_solids": 1, + "history": [ + { + "pre": 0, + "deleted": false, + "modified": [], + "generated": [ + 0 + ] + } + ] + } + } } } diff --git a/tests/test_inc1_modeling.py b/tests/test_inc1_modeling.py new file mode 100644 index 0000000..6d6128e --- /dev/null +++ b/tests/test_inc1_modeling.py @@ -0,0 +1,386 @@ +"""Inc 1 gate: core modeling reproduces the stock wheel's answers. + +The app's own suite cannot run until the last module is bound (its conftest +imports n3xd.main, i.e. the whole OCP surface), so an increment is gated on the +reference block tools/gen_fixtures.py records from the stock wheel. + +What is compared, and why in that form: + + * counts and boolean history maps — **exactly**. They are topological, so no + tolerance is meaningful, and the history *is* the topological-naming + substrate: the app decides which feature owns which face from it. + * measurements — at a relative tolerance. Same kernel and same conservative + optimiser flags, so they agree to far better than this; the tolerance is + there so a last-ulp difference is not reported as a parity failure. + * constructed shapes — by BREP digest, which is the strongest available + statement and the one cad/derive.py's content addressing depends on. +""" + +from __future__ import annotations + +import hashlib +import io + +import pytest + +from OCP.Bnd import Bnd_Box +from OCP.BRep import BRep_Tool +from OCP.BRepAdaptor import BRepAdaptor_Curve, BRepAdaptor_Surface +from OCP.BRepAlgoAPI import BRepAlgoAPI_Cut, BRepAlgoAPI_Fuse +from OCP.BRepBndLib import BRepBndLib +from OCP.BRepBuilderAPI import ( + BRepBuilderAPI_MakeEdge, + BRepBuilderAPI_MakeFace, + BRepBuilderAPI_MakePolygon, +) +from OCP.BRepGProp import BRepGProp +from OCP.BRepMesh import BRepMesh_IncrementalMesh +from OCP.BRepPrimAPI import ( + BRepPrimAPI_MakeBox, + BRepPrimAPI_MakeCylinder, + BRepPrimAPI_MakePrism, +) +from OCP.BinTools import BinTools +from OCP.GC import GC_MakeArcOfCircle +from OCP.Geom import Geom_BSplineCurve +from OCP.GeomAbs import GeomAbs_Cylinder, GeomAbs_Line, GeomAbs_Plane +from OCP.GProp import GProp_GProps +from OCP.gp import gp_Ax2, gp_Dir, gp_Pnt, gp_Vec +from OCP.TColgp import TColgp_Array1OfPnt +from OCP.TColStd import TColStd_Array1OfInteger, TColStd_Array1OfReal +from OCP.TopAbs import TopAbs_EDGE, TopAbs_FACE, TopAbs_SOLID +from OCP.TopExp import TopExp +from OCP.TopLoc import TopLoc_Location +from OCP.TopoDS import TopoDS, TopoDS_Shape +from OCP.TopTools import TopTools_IndexedMapOfShape, TopTools_ListOfShape + +REL = 1e-9 + + +@pytest.fixture(scope="module") +def inc1(manifest): + if "inc1" not in manifest: + pytest.skip("manifest predates the Inc 1 reference block") + return manifest["inc1"] + + +# -------------------------------------------------------------------------- +# Helpers, deliberately mirroring tools/gen_fixtures.py +# -------------------------------------------------------------------------- + + +def sub_shapes(shape, kind=None): + smap = TopTools_IndexedMapOfShape() + if kind is None: + TopExp.MapShapes_s(shape, smap) + else: + TopExp.MapShapes_s(shape, kind, smap) + return [smap.FindKey(i) for i in range(1, smap.Extent() + 1)] + + +def measure(shape) -> dict: + vol, area = GProp_GProps(), GProp_GProps() + BRepGProp.VolumeProperties_s(shape, vol) + BRepGProp.SurfaceProperties_s(shape, area) + box = Bnd_Box() + BRepBndLib.Add_s(shape, box) + return { + "volume": vol.Mass(), + "area": area.Mass(), + "bbox": None if box.IsVoid() else list(box.Get()), + "n_faces": len(sub_shapes(shape, TopAbs_FACE)), + "n_edges": len(sub_shapes(shape, TopAbs_EDGE)), + "n_solids": len(sub_shapes(shape, TopAbs_SOLID)), + } + + +def history(builder, pre, post) -> list[dict]: + all_post = TopTools_IndexedMapOfShape() + TopExp.MapShapes_s(post, all_post) + return [ + { + "pre": i, + "deleted": bool(builder.IsDeleted(face)), + "modified": sorted( + all_post.FindIndex(s) - 1 for s in builder.Modified(face) + ), + "generated": sorted( + all_post.FindIndex(s) - 1 for s in builder.Generated(face) + ), + } + for i, face in enumerate(sub_shapes(pre, TopAbs_FACE)) + ] + + +def brep_sha(shape) -> str: + buf = io.BytesIO() + BinTools.Write_s(shape, buf) + return hashlib.sha256(buf.getvalue()).hexdigest() + + +def linear(shape) -> float: + props = GProp_GProps() + BRepGProp.LinearProperties_s(shape, props) + return props.Mass() + + +def assert_close(actual, expected, what: str) -> None: + if expected is None: + assert actual is None, what + return + if isinstance(expected, list): + assert actual == pytest.approx(expected, rel=REL), what + else: + assert actual == pytest.approx(expected, rel=REL), what + + +# -------------------------------------------------------------------------- +# Measurement parity over the committed fixtures +# -------------------------------------------------------------------------- + + +def test_measurements_match_stock(inc1, fixture_shapes): + for name, expected in inc1["measure"].items(): + got = measure(fixture_shapes[name]) + for key in ("n_faces", "n_edges", "n_solids"): + assert got[key] == expected[key], f"{name}.{key}" + for key in ("volume", "area", "bbox"): + assert_close(got[key], expected[key], f"{name}.{key}") + + +def test_face_props_match_stock(inc1, fixture_shapes): + """Per-face area and centroid, in MapShapes(FACE) order. + + Also the reference n3xd_ocp.measure.face_surface_props has to reproduce: + the anchor path keys on this ordering. + """ + for name, expected in inc1["face_props"].items(): + faces = sub_shapes(fixture_shapes[name], TopAbs_FACE) + assert len(faces) == len(expected), f"{name}: face count" + for i, (face, row) in enumerate(zip(faces, expected, strict=True)): + props = GProp_GProps() + BRepGProp.SurfaceProperties_s(face, props) + c = props.CentreOfMass() + assert_close( + [props.Mass(), c.X(), c.Y(), c.Z()], row, f"{name}.face[{i}]" + ) + + +def test_surface_properties_eps_overload_is_bound(): + """The 3-argument form exists and converges on the same answer for an + analytic face. The app's spline-area accuracy work needs this knob.""" + box = BRepPrimAPI_MakeBox(10.0, 20.0, 30.0).Shape() + default, eps = GProp_GProps(), GProp_GProps() + BRepGProp.SurfaceProperties_s(box, default) + BRepGProp.SurfaceProperties_s(box, eps, 1e-6) + assert eps.Mass() == pytest.approx(default.Mass(), rel=1e-9) + + +# -------------------------------------------------------------------------- +# Boolean history — the topological-naming substrate +# -------------------------------------------------------------------------- + + +def _cut_cylinder_op(): + op = BRepAlgoAPI_Cut() + base = BRepPrimAPI_MakeBox(10.0, 20.0, 30.0).Shape() + tool = BRepPrimAPI_MakeCylinder( + gp_Ax2(gp_Pnt(5, 10, 0), gp_Dir(0, 0, 1)), 3.0, 30.0 + ).Shape() + args, tools = TopTools_ListOfShape(), TopTools_ListOfShape() + args.Append(base) + tools.Append(tool) + op.SetArguments(args) + op.SetTools(tools) + op.Build() + return op, base + + +def test_boolean_history_matches_stock_exactly(inc1): + op, base = _cut_cylinder_op() + assert op.IsDone() + assert history(op, base, op.Shape()) == inc1["cut_cylinder_history"] + + +def test_history_of_an_untracked_subshape_raises_catchably(): + """The app wraps every history call in `except Exception`, because a builder + need not track an arbitrary sub-shape. Whatever OCCT does there must reach + Python as an exception, never as an abort.""" + op, _base = _cut_cylinder_op() + stranger = BRepPrimAPI_MakeBox(1.0, 1.0, 1.0).Shape() + try: + op.Modified(stranger) + op.Generated(stranger) + op.IsDeleted(stranger) + except Exception: # noqa: BLE001 — the point is that it is catchable + pass + + +def test_executing_boolean_constructors_are_not_bound(): + """The two-argument constructors execute during construction; the app's + call sites also called Build(), so the operation ran twice and the first, + destructive pass could mutate shared inputs. Refusing to bind them is what + makes that unrepresentable.""" + a = BRepPrimAPI_MakeBox(10.0, 10.0, 10.0).Shape() + b = BRepPrimAPI_MakeBox(5.0, 5.0, 5.0).Shape() + for cls in (BRepAlgoAPI_Cut, BRepAlgoAPI_Fuse): + with pytest.raises(TypeError): + cls(a, b) + + +# -------------------------------------------------------------------------- +# Constructors +# -------------------------------------------------------------------------- + + +def test_arc_edge_matches_stock(inc1): + arc = GC_MakeArcOfCircle( + gp_Pnt(0, 0, 0), gp_Pnt(5, 5, 0), gp_Pnt(10, 0, 0) + ).Value() + edge = BRepBuilderAPI_MakeEdge(arc).Edge() + expected = inc1["constructions"]["arc_edge"] + assert brep_sha(edge) == expected["sha256"] + assert_close(linear(edge), expected["length"], "arc length") + + +def test_bspline_edge_matches_stock(inc1): + """A Python-constructed transient handed straight to the kernel. + + This is the path that makes nb::init<> unusable for transients: the edge + builder stores a handle to the curve, so the curve must own its own heap + storage rather than live inside the Python instance. + """ + poles = TColgp_Array1OfPnt(1, 4) + for i, (x, y) in enumerate([(0, 0), (3, 6), (7, -4), (10, 2)], start=1): + poles.SetValue(i, gp_Pnt(float(x), float(y), 0.0)) + knots = TColStd_Array1OfReal(1, 2) + knots.SetValue(1, 0.0) + knots.SetValue(2, 1.0) + mults = TColStd_Array1OfInteger(1, 2) + mults.SetValue(1, 4) + mults.SetValue(2, 4) + + curve = Geom_BSplineCurve(poles, knots, mults, 3) + edge = BRepBuilderAPI_MakeEdge(curve).Edge() + del curve, poles, knots, mults # the edge must not depend on them + + expected = inc1["constructions"]["spline_edge"] + assert brep_sha(edge) == expected["sha256"] + assert_close(linear(edge), expected["length"], "spline length") + + +def test_prism_matches_stock_including_history(inc1): + wire = BRepBuilderAPI_MakePolygon( + gp_Pnt(0, 0, 0), gp_Pnt(10, 0, 0), gp_Pnt(10, 6, 0), gp_Pnt(0, 6, 0), True + ).Wire() + face = BRepBuilderAPI_MakeFace(wire, True).Face() + maker = BRepPrimAPI_MakePrism(face, gp_Vec(0.0, 0.0, 4.0)) + prism = maker.Shape() + + expected = inc1["constructions"]["prism"] + assert brep_sha(prism) == expected["sha256"] + got = measure(prism) + for key in ("n_faces", "n_edges", "n_solids"): + assert got[key] == expected[key], key + for key in ("volume", "area", "bbox"): + assert_close(got[key], expected[key], key) + # An extrude's provenance is read off exactly this map. + assert history(maker, face, prism) == expected["history"] + + +def test_meshing_matches_stock_counts(inc1): + shape = BRepPrimAPI_MakeBox(10.0, 20.0, 30.0).Shape() + BRepMesh_IncrementalMesh(shape, 0.1, False, 0.5, True) + + got = [] + for i, face in enumerate(sub_shapes(shape, TopAbs_FACE)): + loc = TopLoc_Location() + tri = BRep_Tool.Triangulation_s(TopoDS.Face_s(face), loc) + if tri is None: + continue # id gaps are preserved, exactly as tessellation.py does + got.append([i, tri.NbNodes(), tri.NbTriangles()]) + assert got == inc1["box_meshed_counts"] + + +# -------------------------------------------------------------------------- +# Adaptors — the app's most-used query path +# -------------------------------------------------------------------------- + + +def test_adaptor_reports_surface_kind_and_quadric(): + """`BRepAdaptor_Surface(f).Cylinder().Radius()` is the shape of nearly every + reference-inference call in the app, and it crosses three modules: the + virtuals live on Adaptor3d_Surface, the enum in GeomAbs, the result in gp. + """ + cyl = BRepPrimAPI_MakeCylinder( + gp_Ax2(gp_Pnt(0, 0, 0), gp_Dir(0, 0, 1)), 4.0, 12.0 + ).Shape() + + kinds = {} + for face in sub_shapes(cyl, TopAbs_FACE): + adaptor = BRepAdaptor_Surface(TopoDS.Face_s(face)) + kinds.setdefault(adaptor.GetType(), []).append(adaptor) + + assert len(kinds[GeomAbs_Plane]) == 2 + lateral = kinds[GeomAbs_Cylinder][0] + assert lateral.Cylinder().Radius() == pytest.approx(4.0) + assert lateral.Cylinder().Axis().Direction().Z() == pytest.approx(1.0) + + +def test_curve_adaptor_reports_line_and_bounds(): + box = BRepPrimAPI_MakeBox(10.0, 20.0, 30.0).Shape() + edge = TopoDS.Edge_s(sub_shapes(box, TopAbs_EDGE)[0]) + adaptor = BRepAdaptor_Curve(edge) + assert adaptor.GetType() == GeomAbs_Line + assert adaptor.LastParameter() > adaptor.FirstParameter() + direction = adaptor.Line().Direction() + assert abs(direction.X()) + abs(direction.Y()) + abs(direction.Z()) == ( + pytest.approx(1.0) + ) + + +def test_surface_out_parameter_derivatives(): + """D1 keeps its out-parameters: the app pre-allocates and reads back.""" + box = BRepPrimAPI_MakeBox(10.0, 20.0, 30.0).Shape() + face = TopoDS.Face_s(sub_shapes(box, TopAbs_FACE)[0]) + adaptor = BRepAdaptor_Surface(face) + point, du, dv = gp_Pnt(), gp_Vec(), gp_Vec() + adaptor.D1(0.5, 0.5, point, du, dv) + assert du.Crossed(dv).Magnitude() > 0.0 + + +def test_brep_tool_surface_round_trips_through_geom(): + """Surface_s hands back a live Geom_Surface, which must survive the trip + back into the kernel — the projection is how feature recognition measures + tangency.""" + from OCP.GeomAPI import GeomAPI_ProjectPointOnSurf + + box = BRepPrimAPI_MakeBox(10.0, 20.0, 30.0).Shape() + face = TopoDS.Face_s(sub_shapes(box, TopAbs_FACE)[0]) + surf = BRep_Tool.Surface_s(face) + proj = GeomAPI_ProjectPointOnSurf(gp_Pnt(-5.0, 10.0, 15.0), surf) + assert proj.IsDone() + assert proj.NbPoints() >= 1 + u, v = proj.LowerDistanceParameters() + assert isinstance(u, float) and isinstance(v, float) + + +def test_bnd_box_get_returns_a_tuple(): + box = BRepPrimAPI_MakeBox(10.0, 20.0, 30.0).Shape() + bbox = Bnd_Box() + BRepBndLib.Add_s(box, bbox) + xmin, ymin, zmin, xmax, ymax, zmax = bbox.Get() + assert xmax - xmin == pytest.approx(10.0, abs=1e-6) + assert ymax - ymin == pytest.approx(20.0, abs=1e-6) + assert zmax - zmin == pytest.approx(30.0, abs=1e-6) + + +def test_history_survives_its_builder(): + """Sub-shapes come back by value, so a history list outlives the operator + that produced it — the app keeps provenance around long after.""" + op, base = _cut_cylinder_op() + pierced = sub_shapes(base, TopAbs_FACE)[4] + modified = list(op.Modified(pierced)) + del op + assert modified + assert all(isinstance(s, TopoDS_Shape) and not s.IsNull() for s in modified) diff --git a/tools/gen_fixtures.py b/tools/gen_fixtures.py index 1cab86a..0c833aa 100644 --- a/tools/gen_fixtures.py +++ b/tools/gen_fixtures.py @@ -17,6 +17,13 @@ Two properties are recorded, and the first is checked here rather than assumed: Face counts are recorded alongside, pinning the map ordering that face and edge identity depend on throughout the topology code. + +From Inc 1 on, the manifest also carries a per-increment reference block, since +the app's own tests cannot gate an increment: backend/tests/conftest.py imports +n3xd.main, so every one of them fails at collection until the last module is +bound. What the blocks record is chosen to be robust: counts and boolean +history maps are topological and compare exactly, measurements compare at a +relative tolerance. See docs/adding-symbols.md. """ from __future__ import annotations @@ -67,20 +74,27 @@ def _fused() -> TopoDS_Shape: return op.Shape() -def _cut_cylinder() -> TopoDS_Shape: - op = BRepAlgoAPI_Cut() +def _cut_cylinder_op(): + """The deferred cut, returned unbuilt operands and all, so the Inc 1 block + can read its history against the same operand shapes.""" from OCP.TopTools import TopTools_ListOfShape + op = BRepAlgoAPI_Cut() + base = _box() + tool = BRepPrimAPI_MakeCylinder( + gp_Ax2(gp_Pnt(5, 10, 0), gp_Dir(0, 0, 1)), 3.0, 30.0 + ).Shape() args, tools = TopTools_ListOfShape(), TopTools_ListOfShape() - args.Append(_box()) - tools.Append( - BRepPrimAPI_MakeCylinder( - gp_Ax2(gp_Pnt(5, 10, 0), gp_Dir(0, 0, 1)), 3.0, 30.0 - ).Shape() - ) + args.Append(base) + tools.Append(tool) op.SetArguments(args) op.SetTools(tools) op.Build() + return op, base, tool + + +def _cut_cylinder() -> TopoDS_Shape: + op, _base, _tool = _cut_cylinder_op() return op.Shape() @@ -134,6 +148,187 @@ def face_count(shape: TopoDS_Shape) -> int: return faces.Extent() +def _sub_shapes(shape: TopoDS_Shape, kind): + smap = TopTools_IndexedMapOfShape() + TopExp.MapShapes_s(shape, kind, smap) + return [smap.FindKey(i) for i in range(1, smap.Extent() + 1)] + + +def _measure(shape: TopoDS_Shape) -> dict: + """Volume, area and bbox, all through the default (non-eps) quadrature.""" + from OCP.Bnd import Bnd_Box + from OCP.BRepBndLib import BRepBndLib + from OCP.BRepGProp import BRepGProp + from OCP.GProp import GProp_GProps + from OCP.TopAbs import TopAbs_EDGE, TopAbs_SOLID + + vol, area = GProp_GProps(), GProp_GProps() + BRepGProp.VolumeProperties_s(shape, vol) + BRepGProp.SurfaceProperties_s(shape, area) + + box = Bnd_Box() + BRepBndLib.Add_s(shape, box) + bbox = None if box.IsVoid() else list(box.Get()) + + return { + "volume": vol.Mass(), + "area": area.Mass(), + "bbox": bbox, + "n_faces": face_count(shape), + "n_edges": len(_sub_shapes(shape, TopAbs_EDGE)), + "n_solids": len(_sub_shapes(shape, TopAbs_SOLID)), + } + + +def _face_props(shape: TopoDS_Shape) -> list[list[float]]: + """Per-face [area, cx, cy, cz] in MapShapes(FACE) order. + + This is the reference for n3xd_ocp.measure.face_surface_props, and the + ordering contract the whole anchor path depends on. + """ + from OCP.BRepGProp import BRepGProp + from OCP.GProp import GProp_GProps + + out = [] + for face in _sub_shapes(shape, TopAbs_FACE): + props = GProp_GProps() + BRepGProp.SurfaceProperties_s(face, props) + c = props.CentreOfMass() + out.append([props.Mass(), c.X(), c.Y(), c.Z()]) + return out + + +def _history(builder, pre: TopoDS_Shape, post: TopoDS_Shape) -> list[dict]: + """The Modified/Generated/IsDeleted map, keyed by pre-shape face index. + + Recorded as *post-shape sub-shape indices* rather than shapes, so the + comparison is purely topological — this is the substrate the app's + topological naming is built on, and it must not drift by a single entry. + Indices come from the untyped MapShapes, because a builder may generate a + shape of any type (MakePrism answers a face with the solid it swept). + """ + all_post = TopTools_IndexedMapOfShape() + TopExp.MapShapes_s(post, all_post) + + def index_of(shape: TopoDS_Shape) -> int: + return all_post.FindIndex(shape) - 1 # -1 when absent, else 0-based + + rows = [] + for i, face in enumerate(_sub_shapes(pre, TopAbs_FACE)): + rows.append( + { + "pre": i, + "deleted": bool(builder.IsDeleted(face)), + "modified": sorted(index_of(s) for s in builder.Modified(face)), + "generated": sorted(index_of(s) for s in builder.Generated(face)), + } + ) + return rows + + +def _mesh_counts(shape: TopoDS_Shape) -> list[list[int]]: + """Per-face [nb_nodes, nb_triangles], skipping faces with no triangulation. + + Mirrors what cad/tessellation.py extracts, including the "faces without a + mesh leave a gap in the id sequence" rule. + """ + from OCP.BRep import BRep_Tool + from OCP.TopoDS import TopoDS + + out = [] + for i, face in enumerate(_sub_shapes(shape, TopAbs_FACE)): + loc = TopLoc_Location() + tri = BRep_Tool.Triangulation_s(TopoDS.Face_s(face), loc) + if tri is None: + continue + out.append([i, tri.NbNodes(), tri.NbTriangles()]) + return out + + +def _constructions() -> dict: + """Small shapes built through the Inc 1 constructors the app uses. + + Recorded by BREP digest plus a measurement, so the test can rebuild them + and compare both the bytes and the geometry. + """ + from OCP.BRepBuilderAPI import ( + BRepBuilderAPI_MakeEdge, + BRepBuilderAPI_MakeFace, + BRepBuilderAPI_MakePolygon, + ) + from OCP.BRepGProp import BRepGProp + from OCP.BRepPrimAPI import BRepPrimAPI_MakePrism + from OCP.GC import GC_MakeArcOfCircle + from OCP.Geom import Geom_BSplineCurve + from OCP.GProp import GProp_GProps + from OCP.TColgp import TColgp_Array1OfPnt + from OCP.TColStd import TColStd_Array1OfInteger, TColStd_Array1OfReal + + def length(shape: TopoDS_Shape) -> float: + props = GProp_GProps() + BRepGProp.LinearProperties_s(shape, props) + return props.Mass() + + arc = GC_MakeArcOfCircle( + gp_Pnt(0, 0, 0), gp_Pnt(5, 5, 0), gp_Pnt(10, 0, 0) + ).Value() + arc_edge = BRepBuilderAPI_MakeEdge(arc).Edge() + + # A clamped cubic through four poles — the shape sketch_builder/edges.py + # builds for a spline element. + poles = TColgp_Array1OfPnt(1, 4) + for i, (x, y) in enumerate([(0, 0), (3, 6), (7, -4), (10, 2)], start=1): + poles.SetValue(i, gp_Pnt(float(x), float(y), 0.0)) + knots = TColStd_Array1OfReal(1, 2) + knots.SetValue(1, 0.0) + knots.SetValue(2, 1.0) + mults = TColStd_Array1OfInteger(1, 2) + mults.SetValue(1, 4) + mults.SetValue(2, 4) + spline_edge = BRepBuilderAPI_MakeEdge( + Geom_BSplineCurve(poles, knots, mults, 3) + ).Edge() + + poly = BRepBuilderAPI_MakePolygon( + gp_Pnt(0, 0, 0), gp_Pnt(10, 0, 0), gp_Pnt(10, 6, 0), gp_Pnt(0, 6, 0), True + ).Wire() + prism_face = BRepBuilderAPI_MakeFace(poly, True).Face() + prism_maker = BRepPrimAPI_MakePrism(prism_face, gp_Vec(0.0, 0.0, 4.0)) + prism = prism_maker.Shape() + + return { + "arc_edge": { + "sha256": hashlib.sha256(write_bytes(arc_edge)).hexdigest(), + "length": length(arc_edge), + }, + "spline_edge": { + "sha256": hashlib.sha256(write_bytes(spline_edge)).hexdigest(), + "length": length(spline_edge), + }, + "prism": { + "sha256": hashlib.sha256(write_bytes(prism)).hexdigest(), + **_measure(prism), + # The prism's history is what an extrude's provenance reads. + "history": _history(prism_maker, prism_face, prism), + }, + } + + +def _inc1_reference() -> dict: + op, base, _tool = _cut_cylinder_op() + return { + "measure": {name: _measure(build()) for name, build in SHAPES.items()}, + "face_props": { + name: _face_props(build()) + for name, build in SHAPES.items() + if name != "empty_compound" + }, + "cut_cylinder_history": _history(op, base, op.Shape()), + "box_meshed_counts": _mesh_counts(_meshed_box()), + "constructions": _constructions(), + } + + def main() -> int: import OCP @@ -163,6 +358,8 @@ def main() -> int: } print(f"{name}: {len(data)} bytes, {manifest['shapes'][name]['faces']} faces") + manifest["inc1"] = _inc1_reference() + (OUT / "manifest.json").write_text(json.dumps(manifest, indent=2) + "\n") print(f"\nwrote {len(SHAPES)} fixtures + manifest to {OUT}") return 0 diff --git a/tools/sigdiff.py b/tools/sigdiff.py new file mode 100644 index 0000000..9c1ba37 --- /dev/null +++ b/tools/sigdiff.py @@ -0,0 +1,156 @@ +"""Compare this binding's bound surface against the stock wheel's. + +`inventory.py --check` answers "does the symbol exist"; this answers "does it +mean the same thing". The gap between those two is where a binding does real +damage: an `nb::init<...>` whose types differ from the constructor you meant +can bind a *different overload* through an implicit conversion and build valid +but wrong geometry, silently. That happened once — BRepPrimAPI_MakePrism's +gp_Vec form resolving to the semi-infinite gp_Dir one, because gp_Dir converts +from gp_Vec — and the fixture digests are what caught it. This finds the class +of bug directly. + +Two runs, then a diff: + + # under the stock wheel, from the app checkout + uv run --project backend python ../ocp/tools/sigdiff.py --dump stock.json + # under ours + make shell # then: /cache/venv/bin/python /io/tools/sigdiff.py --dump /io/ours.json + python tools/sigdiff.py --compare stock.json ours.json + +The comparison is one-sided on purpose: binding *fewer* overloads than stock is +the normal state of this project, so only surface we expose that stock does not +is reported. pybind11 spells the receiver as `self: Cls` and nanobind as plain +`self`, so the leading class name is dropped before comparing. +""" + +from __future__ import annotations + +import argparse +import importlib +import json +import pathlib +import re +import sys +import types + +HERE = pathlib.Path(__file__).resolve().parent +DEFAULT_INVENTORY = HERE.parent / "inventory.json" + +#: Parameter annotations, as both wrappers write them: `name: Some.Type`. +_ANNOTATION = re.compile(r":\s*([A-Za-z_][\w.]*)") + + +def _signatures(owner, name: str) -> list[tuple[str, ...]]: + """Every overload of `name`, reduced to its parameter type names.""" + doc = getattr(getattr(owner, name, None), "__doc__", None) + if not doc: + return [] + out = set() + for line in doc.splitlines(): + line = re.sub(r"^\d+\.\s*", "", line.strip()) + if "(" not in line or ")" not in line: + continue + inner = line[line.index("(") + 1 : line.rindex(")")] + types = tuple(t.split(".")[-1] for t in _ANNOTATION.findall(inner)) + out.add(types) + return sorted(out) + + +def dump(inventory: pathlib.Path, out: pathlib.Path) -> int: + data = json.loads(inventory.read_text()) + result: dict = {} + + for mod, symbols in data["modules"].items(): + try: + module = importlib.import_module(f"OCP.{mod}") + except ImportError: + continue + for name in symbols: + obj = getattr(module, name, None) + if obj is None: + continue + # OCCT namespaces (TopoDS, BRepGProp, ...) have no class to hang + # their statics on: upstream exposes them as a module, this binding + # as an empty carrier class. Both answer `TopoDS.Face_s(...)`, so + # compare their members either way. + if not isinstance(obj, (type, types.ModuleType)): + continue + # An nb::is_arithmetic() enum derives from int, so dir() carries + # int's methods; pybind11's enum does not. Not surface we bound. + inherited = set(dir(int)) if isinstance(obj, type) and issubclass( + obj, int) else set() + result[f"{mod}.{name}"] = { + "init": _signatures(obj, "__init__"), + "members": sorted( + m for m in dir(obj) + if not m.startswith("_") and m not in inherited + ), + } + + out.write_text(json.dumps(result, indent=1) + "\n") + print(f"{len(result)} classes -> {out}") + return 0 + + +def compare(stock_path: pathlib.Path, ours_path: pathlib.Path) -> int: + stock = json.loads(stock_path.read_text()) + ours = json.loads(ours_path.read_text()) + problems = 0 + + def normalized(entry: dict, cls: str) -> set[tuple[str, ...]]: + out = set() + for sig in entry.get("init", []): + sig = list(sig) + if sig and sig[0] == cls: # pybind11's `self: Cls` + sig = sig[1:] + out.add(tuple(sig)) + return out + + for key, mine in sorted(ours.items()): + theirs = stock.get(key) + if theirs is None: + print(f"{key}: not present in the stock wheel") + problems += 1 + continue + + cls = key.split(".", 1)[1] + extra_init = normalized(mine, cls) - normalized(theirs, cls) + if extra_init: + problems += 1 + print(f"{key}: constructor overloads stock does not have") + for sig in sorted(extra_init): + print(f" ours : ({', '.join(sig)})") + for sig in sorted(normalized(theirs, cls)): + print(f" stock: ({', '.join(sig)})") + + extra_members = set(mine["members"]) - set(theirs["members"]) + if extra_members: + problems += 1 + print(f"{key}: members stock does not have: {sorted(extra_members)}") + + if problems: + print(f"\n{problems} difference(s) — each is either a mis-bound overload " + f"or a deliberate deviation worth a comment in its module") + return 1 + print(f"{len(ours)} classes, no surface we expose that stock does not") + return 0 + + +def main() -> int: + ap = argparse.ArgumentParser(description=__doc__) + ap.add_argument("--dump", type=pathlib.Path, help="write this wheel's surface") + ap.add_argument("--compare", nargs=2, type=pathlib.Path, + metavar=("STOCK", "OURS")) + ap.add_argument("--inventory", type=pathlib.Path, default=DEFAULT_INVENTORY) + args = ap.parse_args() + + if args.dump: + return dump(args.inventory, args.dump) + if args.compare: + return compare(*args.compare) + ap.print_help() + return 2 + + +if __name__ == "__main__": + raise SystemExit(main())