10C Inc 1: core modeling

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) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DfriM8XUkn7uYf5Dwe2xo6
This commit is contained in:
2026-08-10 19:52:52 +02:00
parent 091a2ad0cf
commit 2c2c4e4712
32 changed files with 2732 additions and 18 deletions

3
.gitignore vendored
View File

@@ -35,3 +35,6 @@ third_party/
!.vscode/extensions.json !.vscode/extensions.json
.idea/ .idea/
.DS_Store .DS_Store
# Stray venv/lock from running uv inside this repo (it is not a uv project)
uv.lock

View File

@@ -55,6 +55,11 @@ target_link_libraries(_OCP PRIVATE
TKGeomBase TKGeomBase
TKBRep TKBRep
TKTopAlgo TKTopAlgo
# Inc 1
TKGeomAlgo # GeomAPI, GCPnts
TKPrim # BRepPrimAPI
TKBO # BRepAlgoAPI, BOPAlgo
TKMesh # BRepMesh
) )
target_compile_definitions(_OCP PRIVATE target_compile_definitions(_OCP PRIVATE

View File

@@ -32,6 +32,18 @@ dev: ## Incremental build + run the test suite (the inner loop)
test: dev ## Alias for dev 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 shell: ## Interactive shell in the builder image
docker run --rm -it -v $(REPO):/io -v $(CACHE):/cache $(IMAGE) bash docker run --rm -it -v $(REPO):/io -v $(CACHE):/cache $(IMAGE) bash

View File

@@ -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: When in doubt about a signature, ask the stock wheel rather than guessing:
```bash ```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<TopoDS_Shape, gp_Vec, bool, bool, bool>` 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 ## 4. New toolkits
If the linker cannot find a symbol, the class lives in a toolkit not yet listed If the linker cannot find a symbol, the class lives in a toolkit not yet listed

View File

@@ -9,7 +9,7 @@ name = "n3xd-ocp"
# .devN suffix: the registry never allows republishing a version, and its # .devN suffix: the registry never allows republishing a version, and its
# cleanup rule collects only the dev ones. CMake asserts the prefix matches # cleanup rule collects only the dev ones. CMake asserts the prefix matches
# the OCCT it found. # 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)" description = "nanobind bindings for the OpenCASCADE geometry kernel (drop-in OCP)"
readme = "README.md" readme = "README.md"
requires-python = ">=3.12" requires-python = ">=3.12"

View File

@@ -31,6 +31,27 @@ void register_TopExp(nb::module_ &);
void register_Poly(nb::module_ &); void register_Poly(nb::module_ &);
void register_BRep(nb::module_ &); void register_BRep(nb::module_ &);
void register_BinTools(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_ &); void register_ext(nb::module_ &);
NB_MODULE(_OCP, m) { NB_MODULE(_OCP, m) {
@@ -51,6 +72,29 @@ NB_MODULE(_OCP, m) {
register_BRep(m); register_BRep(m);
register_BinTools(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); register_ext(m);
m.attr("__all_modules__") = ocp_module_registry(); m.attr("__all_modules__") = ocp_module_registry();

View File

@@ -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 <Adaptor3d_Curve.hxx>
#include <Adaptor3d_Surface.hxx>
#include <gp_Circ.hxx>
#include <gp_Cone.hxx>
#include <gp_Cylinder.hxx>
#include <gp_Elips.hxx>
#include <gp_Lin.hxx>
#include <gp_Pln.hxx>
#include <gp_Pnt.hxx>
#include <gp_Sphere.hxx>
#include <gp_Vec.hxx>
void register_Adaptor3d(nb::module_ &root) {
nb::module_ m = ocp_submodule(root, "Adaptor3d");
nb::class_<Adaptor3d_Curve, Standard_Transient>(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_<Adaptor3d_Surface, Standard_Transient>(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);
}

View File

@@ -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 <BOPAlgo_GlueEnum.hxx>
void register_BOPAlgo(nb::module_ &root) {
nb::module_ m = ocp_submodule(root, "BOPAlgo");
nb::enum_<BOPAlgo_GlueEnum>(m, "BOPAlgo_GlueEnum", nb::is_arithmetic())
.value("BOPAlgo_GlueOff", BOPAlgo_GlueOff)
.value("BOPAlgo_GlueShift", BOPAlgo_GlueShift)
.value("BOPAlgo_GlueFull", BOPAlgo_GlueFull)
.export_values();
}

View File

@@ -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 Surface/Curve hand back the face's or edge's *live* Geom handle when the
the Geom classes arrive; Triangulation is here because it is what the location is identity, and a transformed copy otherwise. The handle caster
tessellation path reads and what the handle spike exercises. 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_module.h"
#include "../common/occt_policies.h" #include "../common/occt_policies.h"
#include <nanobind/stl/tuple.h>
#include <BRep_Builder.hxx> #include <BRep_Builder.hxx>
#include <BRep_Tool.hxx> #include <BRep_Tool.hxx>
#include <Geom2d_Curve.hxx>
#include <Geom_Curve.hxx>
#include <Geom_Surface.hxx>
#include <Poly_Triangulation.hxx> #include <Poly_Triangulation.hxx>
#include <TopLoc_Location.hxx> #include <TopLoc_Location.hxx>
#include <TopoDS_Compound.hxx> #include <TopoDS_Compound.hxx>
@@ -70,4 +76,34 @@ void register_BRep(nb::module_ &root) {
OCP_DEF_S( OCP_DEF_S(
cls, "IsClosed", cls, "IsClosed",
[](const TopoDS_Shape &S) { return BRep_Tool::IsClosed(S); }, "S"_a); [](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);
} }

View File

@@ -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 <Adaptor3d_Curve.hxx>
#include <Adaptor3d_Surface.hxx>
#include <BRepAdaptor_Curve.hxx>
#include <BRepAdaptor_Surface.hxx>
#include <TopoDS_Edge.hxx>
#include <TopoDS_Face.hxx>
void register_BRepAdaptor(nb::module_ &root) {
nb::module_ m = ocp_submodule(root, "BRepAdaptor");
nb::class_<BRepAdaptor_Curve, Adaptor3d_Curve>(m, "BRepAdaptor_Curve")
.def(ocp_new<BRepAdaptor_Curve>())
.def(ocp_new<BRepAdaptor_Curve, const TopoDS_Edge &>(), "E"_a)
.def(ocp_new<BRepAdaptor_Curve, const TopoDS_Edge &, const TopoDS_Face &>(),
"E"_a, "F"_a)
.def("Initialize",
nb::overload_cast<const TopoDS_Edge &>(&BRepAdaptor_Curve::Initialize),
"E"_a)
.def("Edge", &BRepAdaptor_Curve::Edge, nb::rv_policy::copy);
nb::class_<BRepAdaptor_Surface, Adaptor3d_Surface>(m, "BRepAdaptor_Surface")
.def(ocp_new<BRepAdaptor_Surface>())
.def(ocp_new<BRepAdaptor_Surface, const TopoDS_Face &,
const Standard_Boolean>(),
"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);
}

View File

@@ -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 <BOPAlgo_GlueEnum.hxx>
#include <BRepAlgoAPI_BooleanOperation.hxx>
#include <BRepAlgoAPI_BuilderAlgo.hxx>
#include <BRepAlgoAPI_Common.hxx>
#include <BRepAlgoAPI_Cut.hxx>
#include <BRepAlgoAPI_Fuse.hxx>
#include <BRepAlgoAPI_Section.hxx>
#include <BRepAlgoAPI_Splitter.hxx>
#include <BRepBuilderAPI_MakeShape.hxx>
#include <TopTools_ListOfShape.hxx>
#include <TopoDS_Shape.hxx>
#include <gp_Pln.hxx>
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_<BRepAlgoAPI_BuilderAlgo, BRepBuilderAPI_MakeShape>(
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_<BRepAlgoAPI_BooleanOperation, BRepAlgoAPI_BuilderAlgo>(
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_<BRepAlgoAPI_Fuse, BRepAlgoAPI_BooleanOperation>(
m, "BRepAlgoAPI_Fuse")
.def(nb::init<>());
nb::class_<BRepAlgoAPI_Cut, BRepAlgoAPI_BooleanOperation>(m,
"BRepAlgoAPI_Cut")
.def(nb::init<>());
nb::class_<BRepAlgoAPI_Common, BRepAlgoAPI_BooleanOperation>(
m, "BRepAlgoAPI_Common")
.def(nb::init<>());
nb::class_<BRepAlgoAPI_Section, BRepAlgoAPI_BooleanOperation>(
m, "BRepAlgoAPI_Section")
.def(nb::init<>())
.def("Init1",
nb::overload_cast<const TopoDS_Shape &>(&BRepAlgoAPI_Section::Init1),
"S1"_a)
.def("Init2",
nb::overload_cast<const TopoDS_Shape &>(&BRepAlgoAPI_Section::Init2),
"S2"_a)
.def("Init2", nb::overload_cast<const gp_Pln &>(&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_<BRepAlgoAPI_Splitter, BRepAlgoAPI_BuilderAlgo>(
m, "BRepAlgoAPI_Splitter")
.def(nb::init<>())
.def("SetTools", &BRepAlgoAPI_Splitter::SetTools, "theLS"_a)
.def("Tools", &BRepAlgoAPI_Splitter::Tools, OCP_RETURN_COPY);
}

View File

@@ -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 <BRepBndLib.hxx>
#include <Bnd_Box.hxx>
#include <TopoDS_Shape.hxx>
void register_BRepBndLib(nb::module_ &root) {
nb::module_ m = ocp_submodule(root, "BRepBndLib");
nb::class_<BRepBndLib> 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);
}

View File

@@ -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 <BRepBuilderAPI_Command.hxx>
#include <BRepBuilderAPI_Copy.hxx>
#include <BRepBuilderAPI_MakeEdge.hxx>
#include <BRepBuilderAPI_MakeFace.hxx>
#include <BRepBuilderAPI_MakePolygon.hxx>
#include <BRepBuilderAPI_MakeShape.hxx>
#include <BRepBuilderAPI_MakeWire.hxx>
#include <BRepBuilderAPI_Sewing.hxx>
#include <BRepBuilderAPI_Transform.hxx>
#include <Geom2d_Curve.hxx>
#include <gp_Circ.hxx>
#include <gp_Lin.hxx>
#include <gp_Pln.hxx>
#include <gp_Pnt.hxx>
#include <gp_Trsf.hxx>
#include <Geom_Curve.hxx>
#include <Geom_Surface.hxx>
#include <TopTools_ListOfShape.hxx>
#include <TopoDS_Edge.hxx>
#include <TopoDS_Face.hxx>
#include <TopoDS_Shape.hxx>
#include <TopoDS_Wire.hxx>
void register_BRepBuilderAPI(nb::module_ &root) {
nb::module_ m = ocp_submodule(root, "BRepBuilderAPI");
// Abstract bases: registered for the shared surface, never constructed.
nb::class_<BRepBuilderAPI_Command>(m, "BRepBuilderAPI_Command")
.def("IsDone", &BRepBuilderAPI_Command::IsDone);
nb::class_<BRepBuilderAPI_MakeShape, BRepBuilderAPI_Command>(
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_<BRepBuilderAPI_MakeEdge, BRepBuilderAPI_MakeShape>(
m, "BRepBuilderAPI_MakeEdge")
.def(nb::init<const gp_Pnt &, const gp_Pnt &>(), "P1"_a, "P2"_a)
.def(nb::init<const gp_Circ &>(), "L"_a)
.def(nb::init<const gp_Lin &>(), "L"_a)
.def(nb::init<const opencascade::handle<Geom_Curve> &>(), "L"_a)
.def(nb::init<const opencascade::handle<Geom2d_Curve> &,
const opencascade::handle<Geom_Surface> &>(),
"L"_a, "S"_a)
.def(nb::init<const opencascade::handle<Geom2d_Curve> &,
const opencascade::handle<Geom_Surface> &,
const Standard_Real, const Standard_Real>(),
"L"_a, "S"_a, "p1"_a, "p2"_a)
.def("Edge", &BRepBuilderAPI_MakeEdge::Edge, OCP_RETURN_COPY);
nb::class_<BRepBuilderAPI_MakeWire, BRepBuilderAPI_MakeShape>(
m, "BRepBuilderAPI_MakeWire")
.def(nb::init<>())
.def(nb::init<const TopoDS_Edge &>(), "E"_a)
.def(nb::init<const TopoDS_Wire &>(), "W"_a)
.def("Add", nb::overload_cast<const TopoDS_Edge &>(
&BRepBuilderAPI_MakeWire::Add),
"E"_a)
.def("Add", nb::overload_cast<const TopoDS_Wire &>(
&BRepBuilderAPI_MakeWire::Add),
"W"_a)
.def("Add", nb::overload_cast<const TopTools_ListOfShape &>(
&BRepBuilderAPI_MakeWire::Add),
"L"_a)
.def("Wire", &BRepBuilderAPI_MakeWire::Wire, OCP_RETURN_COPY);
nb::class_<BRepBuilderAPI_MakeFace, BRepBuilderAPI_MakeShape>(
m, "BRepBuilderAPI_MakeFace")
.def(nb::init<const TopoDS_Face &>(), "F"_a)
.def(nb::init<const gp_Pln &>(), "P"_a)
.def(nb::init<const gp_Pln &, const Standard_Real, const Standard_Real,
const Standard_Real, const Standard_Real>(),
"P"_a, "UMin"_a, "UMax"_a, "VMin"_a, "VMax"_a)
.def(nb::init<const TopoDS_Wire &, const Standard_Boolean>(), "W"_a,
"OnlyPlane"_a = Standard_False)
.def(nb::init<const opencascade::handle<Geom_Surface> &,
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_<BRepBuilderAPI_MakePolygon, BRepBuilderAPI_MakeShape>(
m, "BRepBuilderAPI_MakePolygon")
.def(nb::init<>())
.def(nb::init<const gp_Pnt &, const gp_Pnt &>(), "P1"_a, "P2"_a)
.def(nb::init<const gp_Pnt &, const gp_Pnt &, const gp_Pnt &,
const Standard_Boolean>(),
"P1"_a, "P2"_a, "P3"_a, "Close"_a = Standard_False)
.def(nb::init<const gp_Pnt &, const gp_Pnt &, const gp_Pnt &,
const gp_Pnt &, const Standard_Boolean>(),
"P1"_a, "P2"_a, "P3"_a, "P4"_a, "Close"_a = Standard_False)
.def("Add", nb::overload_cast<const gp_Pnt &>(
&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_<BRepBuilderAPI_Transform, BRepBuilderAPI_MakeShape>(
m, "BRepBuilderAPI_Transform")
.def(nb::init<const gp_Trsf &>(), "T"_a)
.def(nb::init<const TopoDS_Shape &, const gp_Trsf &,
const Standard_Boolean, const Standard_Boolean>(),
"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_<BRepBuilderAPI_Copy, BRepBuilderAPI_MakeShape>(
m, "BRepBuilderAPI_Copy")
.def(nb::init<>())
.def(nb::init<const TopoDS_Shape &, const Standard_Boolean,
const Standard_Boolean>(),
"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_<BRepBuilderAPI_Sewing, Standard_Transient>(
m, "BRepBuilderAPI_Sewing")
.def(ocp_new<BRepBuilderAPI_Sewing, const Standard_Real,
const Standard_Boolean, const Standard_Boolean,
const Standard_Boolean, const Standard_Boolean>(),
"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);
}

View File

@@ -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 <BRepGProp.hxx>
#include <GProp_GProps.hxx>
#include <TopoDS_Shape.hxx>
void register_BRepGProp(nb::module_ &root) {
nb::module_ m = ocp_submodule(root, "BRepGProp");
nb::class_<BRepGProp> 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);
}

View File

@@ -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 <BRepMesh_IncrementalMesh.hxx>
#include <TopoDS_Shape.hxx>
void register_BRepMesh(nb::module_ &root) {
nb::module_ m = ocp_submodule(root, "BRepMesh");
nb::class_<BRepMesh_IncrementalMesh, Standard_Transient>(
m, "BRepMesh_IncrementalMesh")
.def(ocp_new<BRepMesh_IncrementalMesh>())
.def(ocp_new<BRepMesh_IncrementalMesh, const TopoDS_Shape &,
const Standard_Real, const Standard_Boolean,
const Standard_Real, const Standard_Boolean>(),
"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);
}

View File

@@ -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 <BRepBuilderAPI_MakeShape.hxx>
#include <BRepPrimAPI_MakeBox.hxx>
#include <BRepPrimAPI_MakeCone.hxx>
#include <BRepPrimAPI_MakeCylinder.hxx>
#include <BRepPrimAPI_MakeHalfSpace.hxx>
#include <BRepPrimAPI_MakePrism.hxx>
#include <BRepPrimAPI_MakeRevol.hxx>
#include <TopoDS_Face.hxx>
#include <TopoDS_Shell.hxx>
#include <gp_Ax1.hxx>
#include <gp_Ax2.hxx>
#include <gp_Pnt.hxx>
#include <gp_Vec.hxx>
#include <TopoDS_Shape.hxx>
void register_BRepPrimAPI(nb::module_ &root) {
nb::module_ m = ocp_submodule(root, "BRepPrimAPI");
nb::class_<BRepPrimAPI_MakeBox, BRepBuilderAPI_MakeShape>(
m, "BRepPrimAPI_MakeBox")
.def(nb::init<const Standard_Real, const Standard_Real,
const Standard_Real>(),
"dx"_a, "dy"_a, "dz"_a)
.def(nb::init<const gp_Pnt &, const Standard_Real, const Standard_Real,
const Standard_Real>(),
"P"_a, "dx"_a, "dy"_a, "dz"_a)
.def(nb::init<const gp_Pnt &, const gp_Pnt &>(), "P1"_a, "P2"_a)
.def(nb::init<const gp_Ax2 &, const Standard_Real, const Standard_Real,
const Standard_Real>(),
"Axes"_a, "dx"_a, "dy"_a, "dz"_a);
nb::class_<BRepPrimAPI_MakeCylinder, BRepBuilderAPI_MakeShape>(
m, "BRepPrimAPI_MakeCylinder")
.def(nb::init<const Standard_Real, const Standard_Real>(), "R"_a, "H"_a)
.def(nb::init<const Standard_Real, const Standard_Real,
const Standard_Real>(),
"R"_a, "H"_a, "Angle"_a)
.def(nb::init<const gp_Ax2 &, const Standard_Real,
const Standard_Real>(),
"Axes"_a, "R"_a, "H"_a)
.def(nb::init<const gp_Ax2 &, const Standard_Real, const Standard_Real,
const Standard_Real>(),
"Axes"_a, "R"_a, "H"_a, "Angle"_a);
nb::class_<BRepPrimAPI_MakeCone, BRepBuilderAPI_MakeShape>(
m, "BRepPrimAPI_MakeCone")
.def(nb::init<const Standard_Real, const Standard_Real,
const Standard_Real>(),
"R1"_a, "R2"_a, "H"_a)
.def(nb::init<const gp_Ax2 &, const Standard_Real, const Standard_Real,
const Standard_Real>(),
"Axes"_a, "R1"_a, "R2"_a, "H"_a);
nb::class_<BRepPrimAPI_MakeHalfSpace, BRepBuilderAPI_MakeShape>(
m, "BRepPrimAPI_MakeHalfSpace")
.def(nb::init<const TopoDS_Face &, const gp_Pnt &>(), "Face"_a,
"RefPnt"_a)
.def(nb::init<const TopoDS_Shell &, const gp_Pnt &>(), "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_<BRepPrimAPI_MakePrism, BRepBuilderAPI_MakeShape>(
m, "BRepPrimAPI_MakePrism")
.def(nb::init<const TopoDS_Shape &, const gp_Vec &,
const Standard_Boolean, const Standard_Boolean>(),
"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_<BRepPrimAPI_MakeRevol, BRepBuilderAPI_MakeShape>(
m, "BRepPrimAPI_MakeRevol")
.def(nb::init<const TopoDS_Shape &, const gp_Ax1 &, const Standard_Real,
const Standard_Boolean>(),
"S"_a, "A"_a, "D"_a, "Copy"_a = Standard_False, OCP_NOGIL)
.def(nb::init<const TopoDS_Shape &, const gp_Ax1 &,
const Standard_Boolean>(),
"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);
}

54
src/modules/mod_Bnd.cpp Normal file
View File

@@ -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 <nanobind/stl/tuple.h>
#include <Bnd_Box.hxx>
#include <gp_Pnt.hxx>
#include <gp_Trsf.hxx>
void register_Bnd(nb::module_ &root) {
nb::module_ m = ocp_submodule(root, "Bnd");
nb::class_<Bnd_Box>(m, "Bnd_Box")
.def(nb::init<>())
.def(nb::init<const gp_Pnt &, const gp_Pnt &>(), "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<const Bnd_Box &>(&Bnd_Box::Add), "Other"_a)
.def("Add", nb::overload_cast<const gp_Pnt &>(&Bnd_Box::Add), "P"_a)
.def("Update", nb::overload_cast<const Standard_Real, const Standard_Real,
const Standard_Real, const Standard_Real,
const Standard_Real, const Standard_Real>(
&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<const gp_Pnt &>(&Bnd_Box::IsOut,
nb::const_),
"P"_a)
.def("IsOut", nb::overload_cast<const Bnd_Box &>(&Bnd_Box::IsOut,
nb::const_),
"Other"_a)
.def("SquareExtent", &Bnd_Box::SquareExtent)
.def("Transformed", &Bnd_Box::Transformed, "T"_a, OCP_RETURN_COPY);
}

25
src/modules/mod_GC.cpp Normal file
View File

@@ -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 <GC_MakeArcOfCircle.hxx>
#include <gp_Circ.hxx>
#include <gp_Pnt.hxx>
void register_GC(nb::module_ &root) {
nb::module_ m = ocp_submodule(root, "GC");
nb::class_<GC_MakeArcOfCircle>(m, "GC_MakeArcOfCircle")
.def(nb::init<const gp_Pnt &, const gp_Pnt &, const gp_Pnt &>(), "P1"_a,
"P2"_a, "P3"_a)
.def(nb::init<const gp_Circ &, const gp_Pnt &, const gp_Pnt &,
const Standard_Boolean>(),
"Circ"_a, "P1"_a, "P2"_a, "Sense"_a)
.def("IsDone", &GC_MakeArcOfCircle::IsDone)
.def("Value", &GC_MakeArcOfCircle::Value);
}

57
src/modules/mod_GProp.cpp Normal file
View File

@@ -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 <nanobind/stl/tuple.h>
#include <GProp_GProps.hxx>
#include <GProp_PrincipalProps.hxx>
#include <gp_Pnt.hxx>
#include <gp_Vec.hxx>
void register_GProp(nb::module_ &root) {
nb::module_ m = ocp_submodule(root, "GProp");
nb::class_<GProp_PrincipalProps>(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_<GProp_GProps>(m, "GProp_GProps")
.def(nb::init<>())
.def(nb::init<const gp_Pnt &>(), "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);
}

87
src/modules/mod_Geom.cpp Normal file
View File

@@ -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 <Geom_BSplineCurve.hxx>
#include <Geom_BSplineSurface.hxx>
#include <Geom_CylindricalSurface.hxx>
#include <Geom_OffsetSurface.hxx>
#include <Geom_Surface.hxx>
#include <Geom_TrimmedCurve.hxx>
#include <TColStd_Array1OfInteger.hxx>
#include <TColStd_Array1OfReal.hxx>
#include <TColgp_Array1OfPnt.hxx>
#include <gp_Ax3.hxx>
#include <gp_Pnt.hxx>
void register_Geom(nb::module_ &root) {
nb::module_ m = ocp_submodule(root, "Geom");
// ------------------------------------------------------------ curves --
nb::class_<Geom_Curve, Standard_Transient>(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_<Geom_TrimmedCurve, Geom_Curve>(m, "Geom_TrimmedCurve");
nb::class_<Geom_BSplineCurve, Geom_Curve>(m, "Geom_BSplineCurve")
.def(ocp_new<Geom_BSplineCurve, const TColgp_Array1OfPnt &,
const TColStd_Array1OfReal &,
const TColStd_Array1OfInteger &, const Standard_Integer,
const Standard_Boolean>(),
"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_<Geom_Surface, Standard_Transient>(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_<Geom_BSplineSurface, Geom_Surface>(m, "Geom_BSplineSurface");
nb::class_<Geom_CylindricalSurface, Geom_Surface>(m,
"Geom_CylindricalSurface")
.def(ocp_new<Geom_CylindricalSurface, const gp_Ax3 &,
const Standard_Real>(),
"A3"_a, "Radius"_a)
.def("Radius", &Geom_CylindricalSurface::Radius);
nb::class_<Geom_OffsetSurface, Geom_Surface>(m, "Geom_OffsetSurface")
.def(ocp_new<Geom_OffsetSurface, const opencascade::handle<Geom_Surface> &,
const Standard_Real, const Standard_Boolean>(),
"S"_a, "Offset"_a, "isNotCheckC0"_a = Standard_False)
.def("Offset", &Geom_OffsetSurface::Offset)
.def("BasisSurface", &Geom_OffsetSurface::BasisSurface);
}

View File

@@ -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 <Geom2d_Curve.hxx>
#include <Geom2d_Line.hxx>
#include <Geom2d_TrimmedCurve.hxx>
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_<Geom2d_Curve, Standard_Transient>(m, "Geom2d_Curve")
.def("FirstParameter", &Geom2d_Curve::FirstParameter)
.def("LastParameter", &Geom2d_Curve::LastParameter);
nb::class_<Geom2d_Line, Geom2d_Curve>(m, "Geom2d_Line");
nb::class_<Geom2d_TrimmedCurve, Geom2d_Curve>(m, "Geom2d_TrimmedCurve");
}

View File

@@ -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 <nanobind/stl/tuple.h>
#include <GeomAPI_PointsToBSplineSurface.hxx>
#include <GeomAPI_ProjectPointOnSurf.hxx>
#include <GeomAbs_Shape.hxx>
#include <Geom_BSplineSurface.hxx>
#include <Geom_Surface.hxx>
#include <gp_Pnt.hxx>
#include <TColgp_Array2OfPnt.hxx>
void register_GeomAPI(nb::module_ &root) {
nb::module_ m = ocp_submodule(root, "GeomAPI");
nb::class_<GeomAPI_ProjectPointOnSurf>(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<const gp_Pnt &, const opencascade::handle<Geom_Surface> &>(),
"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_<GeomAPI_PointsToBSplineSurface>(m, "GeomAPI_PointsToBSplineSurface")
.def(nb::init<>())
.def(nb::init<const TColgp_Array2OfPnt &, const Standard_Integer,
const Standard_Integer, const GeomAbs_Shape,
const Standard_Real>(),
"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);
}

View File

@@ -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 <GeomAbs_CurveType.hxx>
#include <GeomAbs_Shape.hxx>
#include <GeomAbs_SurfaceType.hxx>
void register_GeomAbs(nb::module_ &root) {
nb::module_ m = ocp_submodule(root, "GeomAbs");
nb::enum_<GeomAbs_CurveType>(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_<GeomAbs_SurfaceType>(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_<GeomAbs_Shape>(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();
}

View File

@@ -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 <GeomLProp_SLProps.hxx>
#include <Geom_Surface.hxx>
#include <gp_Dir.hxx>
#include <gp_Pnt.hxx>
void register_GeomLProp(nb::module_ &root) {
nb::module_ m = ocp_submodule(root, "GeomLProp");
nb::class_<GeomLProp_SLProps>(m, "GeomLProp_SLProps")
.def(nb::init<const opencascade::handle<Geom_Surface> &, 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);
}

View File

@@ -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 <TColStd_Array1OfInteger.hxx>
#include <TColStd_Array1OfReal.hxx>
#include <TColStd_SequenceOfAsciiString.hxx>
namespace {
/// Bind one NCollection_Array1<T> instantiation with the app's surface.
template <typename A, typename T>
void bind_array1(nb::module_ &m, const char *name) {
nb::class_<A>(m, name)
.def(nb::init<const Standard_Integer, const Standard_Integer>(),
"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<TColStd_Array1OfInteger, Standard_Integer>(
m, "TColStd_Array1OfInteger");
bind_array1<TColStd_Array1OfReal, Standard_Real>(m, "TColStd_Array1OfReal");
nb::class_<TColStd_SequenceOfAsciiString>(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);
}

View File

@@ -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 <TColgp_Array1OfPnt.hxx>
#include <TColgp_Array2OfPnt.hxx>
#include <gp_Pnt.hxx>
void register_TColgp(nb::module_ &root) {
nb::module_ m = ocp_submodule(root, "TColgp");
nb::class_<TColgp_Array1OfPnt>(m, "TColgp_Array1OfPnt")
.def(nb::init<const Standard_Integer, const Standard_Integer>(),
"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_<TColgp_Array2OfPnt>(m, "TColgp_Array2OfPnt")
.def(nb::init<const Standard_Integer, const Standard_Integer,
const Standard_Integer, const Standard_Integer>(),
"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);
}

View File

@@ -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 <TCollection_AsciiString.hxx>
void register_TCollection(nb::module_ &root) {
nb::module_ m = ocp_submodule(root, "TCollection");
nb::class_<TCollection_AsciiString>(m, "TCollection_AsciiString")
.def(nb::init<>())
.def(nb::init<const Standard_CString>(), "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);
}

View File

@@ -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 The quadrics (gp_Cylinder/Cone/Sphere/Elips) are not imported by the app
app also uses) lands with Inc 1, where the curve and surface classes that directly; they arrive as return values of the adaptors' GetType() branches
consume them arrive. (`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_module.h"
#include "../common/occt_policies.h" #include "../common/occt_policies.h"
#include <gp_Ax1.hxx> #include <gp_Ax1.hxx>
#include <gp_Ax2.hxx>
#include <gp_Ax3.hxx>
#include <gp_Circ.hxx>
#include <gp_Cone.hxx>
#include <gp_Cylinder.hxx>
#include <gp_Dir.hxx> #include <gp_Dir.hxx>
#include <gp_Dir2d.hxx>
#include <gp_Elips.hxx>
#include <gp_Lin.hxx>
#include <gp_Pln.hxx>
#include <gp_Pnt.hxx> #include <gp_Pnt.hxx>
#include <gp_Pnt2d.hxx>
#include <gp_Quaternion.hxx>
#include <gp_Sphere.hxx>
#include <gp_Trsf.hxx> #include <gp_Trsf.hxx>
#include <gp_Vec.hxx> #include <gp_Vec.hxx>
#include <gp_Vec2d.hxx>
#include <gp_XYZ.hxx> #include <gp_XYZ.hxx>
void register_gp(nb::module_ &root) { 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("Reversed", &gp_Vec::Reversed, OCP_RETURN_COPY)
.def("Multiplied", &gp_Vec::Multiplied, "Scalar"_a, OCP_RETURN_COPY) .def("Multiplied", &gp_Vec::Multiplied, "Scalar"_a, OCP_RETURN_COPY)
.def("Angle", &gp_Vec::Angle, "Other"_a) .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("Transform", &gp_Vec::Transform, "T"_a)
.def("Transformed", &gp_Vec::Transformed, "T"_a, OCP_RETURN_COPY); .def("Transformed", &gp_Vec::Transformed, "T"_a, OCP_RETURN_COPY);
@@ -123,6 +140,9 @@ void register_gp(nb::module_ &root) {
nb::overload_cast<const gp_Ax1 &, Standard_Real>( nb::overload_cast<const gp_Ax1 &, Standard_Real>(
&gp_Trsf::SetRotation), &gp_Trsf::SetRotation),
"A1"_a, "Ang"_a) "A1"_a, "Ang"_a)
.def("SetRotation",
nb::overload_cast<const gp_Quaternion &>(&gp_Trsf::SetRotation),
"R"_a)
.def("SetScale", &gp_Trsf::SetScale, "P"_a, "S"_a) .def("SetScale", &gp_Trsf::SetScale, "P"_a, "S"_a)
.def("SetMirror", nb::overload_cast<const gp_Ax1 &>(&gp_Trsf::SetMirror), .def("SetMirror", nb::overload_cast<const gp_Ax1 &>(&gp_Trsf::SetMirror),
"A1"_a) "A1"_a)
@@ -135,4 +155,160 @@ void register_gp(nb::module_ &root) {
.def("Multiply", &gp_Trsf::Multiply, "T"_a) .def("Multiply", &gp_Trsf::Multiply, "T"_a)
.def("PreMultiply", &gp_Trsf::PreMultiply, "T"_a) .def("PreMultiply", &gp_Trsf::PreMultiply, "T"_a)
.def("Invert", &gp_Trsf::Invert); .def("Invert", &gp_Trsf::Invert);
nb::class_<gp_Quaternion>(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<Standard_Real, Standard_Real, Standard_Real,
Standard_Real>(),
"x"_a, "y"_a, "z"_a, "w"_a)
.def(nb::init<const gp_Vec &, Standard_Real>(), "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_<gp_Pnt2d>(m, "gp_Pnt2d")
.def(nb::init<>())
.def(nb::init<Standard_Real, Standard_Real>(), "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_<gp_Dir2d>(m, "gp_Dir2d")
.def(nb::init<>())
.def(nb::init<Standard_Real, Standard_Real>(), "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_<gp_Vec2d>(m, "gp_Vec2d")
.def(nb::init<>())
.def(nb::init<Standard_Real, Standard_Real>(), "Xv"_a, "Yv"_a)
.def(nb::init<const gp_Dir2d &>(), "V"_a)
.def("X", &gp_Vec2d::X)
.def("Y", &gp_Vec2d::Y)
.def("Magnitude", &gp_Vec2d::Magnitude);
// -------------------------------------------------------------- axes --
nb::class_<gp_Ax2>(m, "gp_Ax2")
.def(nb::init<>())
.def(nb::init<const gp_Pnt &, const gp_Dir &>(), "P"_a, "V"_a)
.def(nb::init<const gp_Pnt &, const gp_Dir &, const gp_Dir &>(), "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_<gp_Ax3>(m, "gp_Ax3")
.def(nb::init<>())
.def(nb::init<const gp_Ax2 &>(), "theA"_a)
.def(nb::init<const gp_Pnt &, const gp_Dir &>(), "theP"_a, "theN"_a)
.def(nb::init<const gp_Pnt &, const gp_Dir &, const gp_Dir &>(),
"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_<gp_Lin>(m, "gp_Lin")
.def(nb::init<>())
.def(nb::init<const gp_Pnt &, const gp_Dir &>(), "theP"_a, "theV"_a)
.def(nb::init<const gp_Ax1 &>(), "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<const gp_Pnt &>(&gp_Lin::Distance,
nb::const_),
"theP"_a);
nb::class_<gp_Circ>(m, "gp_Circ")
.def(nb::init<>())
.def(nb::init<const gp_Ax2 &, Standard_Real>(), "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_<gp_Elips>(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_<gp_Pln>(m, "gp_Pln")
.def(nb::init<>())
.def(nb::init<const gp_Pnt &, const gp_Dir &>(), "theP"_a, "theV"_a)
.def(nb::init<const gp_Ax3 &>(), "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<const gp_Pnt &>(&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_<gp_Cylinder>(m, "gp_Cylinder")
.def(nb::init<>())
.def(nb::init<const gp_Ax3 &, Standard_Real>(), "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_<gp_Cone>(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_<gp_Sphere>(m, "gp_Sphere")
.def(nb::init<>())
.def(nb::init<const gp_Ax3 &, Standard_Real>(), "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);
} }

View File

@@ -32,5 +32,472 @@
"size": 171, "size": 171,
"faces": 0 "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
]
}
]
}
}
} }
} }

386
tests/test_inc1_modeling.py Normal file
View File

@@ -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)

View File

@@ -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 Face counts are recorded alongside, pinning the map ordering that face and edge
identity depend on throughout the topology code. 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 from __future__ import annotations
@@ -67,20 +74,27 @@ def _fused() -> TopoDS_Shape:
return op.Shape() return op.Shape()
def _cut_cylinder() -> TopoDS_Shape: def _cut_cylinder_op():
op = BRepAlgoAPI_Cut() """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 from OCP.TopTools import TopTools_ListOfShape
args, tools = TopTools_ListOfShape(), TopTools_ListOfShape() op = BRepAlgoAPI_Cut()
args.Append(_box()) base = _box()
tools.Append( tool = BRepPrimAPI_MakeCylinder(
BRepPrimAPI_MakeCylinder(
gp_Ax2(gp_Pnt(5, 10, 0), gp_Dir(0, 0, 1)), 3.0, 30.0 gp_Ax2(gp_Pnt(5, 10, 0), gp_Dir(0, 0, 1)), 3.0, 30.0
).Shape() ).Shape()
) args, tools = TopTools_ListOfShape(), TopTools_ListOfShape()
args.Append(base)
tools.Append(tool)
op.SetArguments(args) op.SetArguments(args)
op.SetTools(tools) op.SetTools(tools)
op.Build() op.Build()
return op, base, tool
def _cut_cylinder() -> TopoDS_Shape:
op, _base, _tool = _cut_cylinder_op()
return op.Shape() return op.Shape()
@@ -134,6 +148,187 @@ def face_count(shape: TopoDS_Shape) -> int:
return faces.Extent() 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: def main() -> int:
import OCP import OCP
@@ -163,6 +358,8 @@ def main() -> int:
} }
print(f"{name}: {len(data)} bytes, {manifest['shapes'][name]['faces']} faces") 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") (OUT / "manifest.json").write_text(json.dumps(manifest, indent=2) + "\n")
print(f"\nwrote {len(SHAPES)} fixtures + manifest to {OUT}") print(f"\nwrote {len(SHAPES)} fixtures + manifest to {OUT}")
return 0 return 0

156
tools/sigdiff.py Normal file
View File

@@ -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())