10C Inc 2: feature tail

119 of the app's 138 symbols now resolve; only the I/O modules and the Inc 4
tail are left. New: GCE2d, GCPnts, BRepFilletAPI, BRepOffsetAPI,
ShapeAnalysis, ShapeFix, ShapeUpgrade, BRepCheck, BRepTools, BRepLib,
BRepExtrema, BRepClass3d, IntCurvesFace, plus TopTools_HSequenceOfShape.

The fillet and chamfer builders derive BRepBuilderAPI_MakeShape, so their
history comes from the base bound in Inc 1 — which is what lets the app read a
blend's provenance the same way it reads a boolean's. The Inc 2 gate compares
that history exactly, alongside the splitter's, which is what sketch-region
attribution depends on.

Two shapes of deviation, both commented where they are bound:

- Trailing enum arguments are left off four constructors (fillet's
  ChFi3d_FilletShape, MakeThickSolidByJoin's mode/join pair, BRepExtrema's
  Extrema flags, and Inc 1's GeomAPI_ProjectPointOnSurf). An unregistered enum
  cannot serve as a default argument — nanobind converts defaults at binding
  time, so it fails the whole module's import with std::bad_cast. The app never
  overrides any of them, so OCCT's own defaults apply and behaviour is
  unchanged.
- ShapeAnalysis_FreeBounds.ConnectEdgesToWires reassigns a Handle& out
  parameter, which the caster cannot honour because it hands C++ a copy of the
  handle. The lambda splices the result into the sequence the caller passed, so
  the pass-empty-then-read shape the app uses still works. This is the one
  place upstream's holder semantics and ours differ observably.

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 20:09:29 +02:00
parent 340315b6e9
commit 0ce43a94aa
20 changed files with 1261 additions and 1 deletions

View File

@@ -60,6 +60,11 @@ target_link_libraries(_OCP PRIVATE
TKPrim # BRepPrimAPI TKPrim # BRepPrimAPI
TKBO # BRepAlgoAPI, BOPAlgo TKBO # BRepAlgoAPI, BOPAlgo
TKMesh # BRepMesh TKMesh # BRepMesh
# Inc 2
TKFillet # BRepFilletAPI
TKOffset # BRepOffsetAPI
TKShHealing # ShapeFix, ShapeAnalysis, ShapeUpgrade
TKBool # BRepAlgo helpers pulled in by the above
) )
target_compile_definitions(_OCP PRIVATE target_compile_definitions(_OCP PRIVATE

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.dev2" version = "7.9.3.1.dev3"
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

@@ -52,6 +52,20 @@ void register_BRepAlgoAPI(nb::module_ &);
void register_BRepPrimAPI(nb::module_ &); void register_BRepPrimAPI(nb::module_ &);
void register_GC(nb::module_ &); void register_GC(nb::module_ &);
void register_BRepMesh(nb::module_ &); void register_BRepMesh(nb::module_ &);
// Inc 2 — feature tail
void register_GCE2d(nb::module_ &);
void register_GCPnts(nb::module_ &);
void register_BRepFilletAPI(nb::module_ &);
void register_BRepOffsetAPI(nb::module_ &);
void register_ShapeAnalysis(nb::module_ &);
void register_ShapeFix(nb::module_ &);
void register_ShapeUpgrade(nb::module_ &);
void register_BRepCheck(nb::module_ &);
void register_BRepTools(nb::module_ &);
void register_BRepLib(nb::module_ &);
void register_BRepExtrema(nb::module_ &);
void register_BRepClass3d(nb::module_ &);
void register_IntCurvesFace(nb::module_ &);
void register_ext(nb::module_ &); void register_ext(nb::module_ &);
NB_MODULE(_OCP, m) { NB_MODULE(_OCP, m) {
@@ -95,6 +109,21 @@ NB_MODULE(_OCP, m) {
register_GC(m); register_GC(m);
register_BRepMesh(m); register_BRepMesh(m);
// Inc 2.
register_GCE2d(m);
register_GCPnts(m);
register_BRepFilletAPI(m);
register_BRepOffsetAPI(m);
register_ShapeAnalysis(m);
register_ShapeFix(m);
register_ShapeUpgrade(m);
register_BRepCheck(m);
register_BRepTools(m);
register_BRepLib(m);
register_BRepExtrema(m);
register_BRepClass3d(m);
register_IntCurvesFace(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,89 @@
/*
OCP.BRepCheck — shape validity.
The analyzer runs on every preview and after every boolean. Its
constructor does the work, so that is what releases the GIL; the per-face
Result() lookups afterwards are cheap.
Only the two status values the app tests for are worth calling out —
it names the self-intersecting/intersecting-wire pair when deciding whether
an imported shape is repairable — but the whole enum is bound, since it is
what Result() reports through.
*/
#include "../common/occt_module.h"
#include "../common/occt_policies.h"
#include <BRepCheck_Analyzer.hxx>
#include <BRepCheck_Result.hxx>
#include <BRepCheck_Status.hxx>
#include <TopoDS_Shape.hxx>
void register_BRepCheck(nb::module_ &root) {
nb::module_ m = ocp_submodule(root, "BRepCheck");
nb::enum_<BRepCheck_Status>(m, "BRepCheck_Status", nb::is_arithmetic())
.value("BRepCheck_NoError", BRepCheck_NoError)
.value("BRepCheck_InvalidPointOnCurve", BRepCheck_InvalidPointOnCurve)
.value("BRepCheck_InvalidPointOnCurveOnSurface",
BRepCheck_InvalidPointOnCurveOnSurface)
.value("BRepCheck_InvalidPointOnSurface", BRepCheck_InvalidPointOnSurface)
.value("BRepCheck_No3DCurve", BRepCheck_No3DCurve)
.value("BRepCheck_Multiple3DCurve", BRepCheck_Multiple3DCurve)
.value("BRepCheck_Invalid3DCurve", BRepCheck_Invalid3DCurve)
.value("BRepCheck_NoCurveOnSurface", BRepCheck_NoCurveOnSurface)
.value("BRepCheck_InvalidCurveOnSurface", BRepCheck_InvalidCurveOnSurface)
.value("BRepCheck_InvalidCurveOnClosedSurface",
BRepCheck_InvalidCurveOnClosedSurface)
.value("BRepCheck_InvalidSameRangeFlag", BRepCheck_InvalidSameRangeFlag)
.value("BRepCheck_InvalidSameParameterFlag",
BRepCheck_InvalidSameParameterFlag)
.value("BRepCheck_InvalidDegeneratedFlag", BRepCheck_InvalidDegeneratedFlag)
.value("BRepCheck_FreeEdge", BRepCheck_FreeEdge)
.value("BRepCheck_InvalidMultiConnexity", BRepCheck_InvalidMultiConnexity)
.value("BRepCheck_InvalidRange", BRepCheck_InvalidRange)
.value("BRepCheck_EmptyWire", BRepCheck_EmptyWire)
.value("BRepCheck_RedundantEdge", BRepCheck_RedundantEdge)
.value("BRepCheck_SelfIntersectingWire", BRepCheck_SelfIntersectingWire)
.value("BRepCheck_NoSurface", BRepCheck_NoSurface)
.value("BRepCheck_InvalidWire", BRepCheck_InvalidWire)
.value("BRepCheck_RedundantWire", BRepCheck_RedundantWire)
.value("BRepCheck_IntersectingWires", BRepCheck_IntersectingWires)
.value("BRepCheck_InvalidImbricationOfWires",
BRepCheck_InvalidImbricationOfWires)
.value("BRepCheck_EmptyShell", BRepCheck_EmptyShell)
.value("BRepCheck_RedundantFace", BRepCheck_RedundantFace)
.value("BRepCheck_InvalidImbricationOfShells",
BRepCheck_InvalidImbricationOfShells)
.value("BRepCheck_UnorientableShape", BRepCheck_UnorientableShape)
.value("BRepCheck_NotClosed", BRepCheck_NotClosed)
.value("BRepCheck_NotConnected", BRepCheck_NotConnected)
.value("BRepCheck_SubshapeNotInShape", BRepCheck_SubshapeNotInShape)
.value("BRepCheck_BadOrientation", BRepCheck_BadOrientation)
.value("BRepCheck_BadOrientationOfSubshape",
BRepCheck_BadOrientationOfSubshape)
.value("BRepCheck_InvalidPolygonOnTriangulation",
BRepCheck_InvalidPolygonOnTriangulation)
.value("BRepCheck_InvalidToleranceValue", BRepCheck_InvalidToleranceValue)
.value("BRepCheck_EnclosedRegion", BRepCheck_EnclosedRegion)
.value("BRepCheck_CheckFail", BRepCheck_CheckFail)
.export_values();
nb::class_<BRepCheck_Result, Standard_Transient>(m, "BRepCheck_Result")
.def("Status", &BRepCheck_Result::Status, OCP_RETURN_COPY)
.def("IsMinimum", &BRepCheck_Result::IsMinimum);
nb::class_<BRepCheck_Analyzer>(m, "BRepCheck_Analyzer")
.def(nb::init<const TopoDS_Shape &, const Standard_Boolean,
const Standard_Boolean, const Standard_Boolean>(),
"S"_a, "GeomControls"_a = Standard_True,
"theIsParallel"_a = Standard_False,
"theIsExact"_a = Standard_False, OCP_NOGIL)
.def("IsValid", nb::overload_cast<>(&BRepCheck_Analyzer::IsValid,
nb::const_))
.def("IsValid",
nb::overload_cast<const TopoDS_Shape &>(&BRepCheck_Analyzer::IsValid,
nb::const_),
"S"_a)
.def("Result", &BRepCheck_Analyzer::Result, "theSubS"_a);
}

View File

@@ -0,0 +1,31 @@
/*
OCP.BRepClass3d — point-in-solid classification.
Answers "is this point inside the material", which the app uses to decide
which side of a face to grow a feature from and to verify that a thread
groove did not gouge its shoulder. The three-argument constructor
classifies immediately; the default one needs a separate Perform.
*/
#include "../common/occt_module.h"
#include "../common/occt_policies.h"
#include <BRepClass3d_SolidClassifier.hxx>
#include <TopoDS_Shape.hxx>
#include <gp_Pnt.hxx>
void register_BRepClass3d(nb::module_ &root) {
nb::module_ m = ocp_submodule(root, "BRepClass3d");
nb::class_<BRepClass3d_SolidClassifier>(m, "BRepClass3d_SolidClassifier")
.def(nb::init<>())
.def(nb::init<const TopoDS_Shape &>(), "S"_a, OCP_NOGIL)
.def(nb::init<const TopoDS_Shape &, const gp_Pnt &,
const Standard_Real>(),
"S"_a, "P"_a, "Tol"_a, OCP_NOGIL)
.def("Load", &BRepClass3d_SolidClassifier::Load, "S"_a, OCP_NOGIL)
.def("Perform", &BRepClass3d_SolidClassifier::Perform, "P"_a, "Tol"_a,
OCP_NOGIL)
.def("State", &BRepClass3d_SolidClassifier::State)
.def("IsOnAFace", &BRepClass3d_SolidClassifier::IsOnAFace);
}

View File

@@ -0,0 +1,34 @@
/*
OCP.BRepExtrema — minimum distance between two shapes.
Used by the measurement tools and the interference check. The constructor
performs the search, so it releases the GIL.
*/
#include "../common/occt_module.h"
#include "../common/occt_policies.h"
#include <BRepExtrema_DistShapeShape.hxx>
#include <TopoDS_Shape.hxx>
#include <gp_Pnt.hxx>
void register_BRepExtrema(nb::module_ &root) {
nb::module_ m = ocp_submodule(root, "BRepExtrema");
nb::class_<BRepExtrema_DistShapeShape>(m, "BRepExtrema_DistShapeShape")
.def(nb::init<>())
// Without the Extrema flag/algo overload: unregistered enums cannot
// serve as default arguments, and the app takes OCCT's defaults.
.def(nb::init<const TopoDS_Shape &, const TopoDS_Shape &>(), "Shape1"_a,
"Shape2"_a, OCP_NOGIL)
.def(
"Perform", [](BRepExtrema_DistShapeShape &self) { self.Perform(); },
OCP_NOGIL)
.def("IsDone", &BRepExtrema_DistShapeShape::IsDone)
.def("NbSolution", &BRepExtrema_DistShapeShape::NbSolution)
.def("Value", &BRepExtrema_DistShapeShape::Value)
.def("PointOnShape1", &BRepExtrema_DistShapeShape::PointOnShape1, "N"_a,
OCP_RETURN_COPY)
.def("PointOnShape2", &BRepExtrema_DistShapeShape::PointOnShape2, "N"_a,
OCP_RETURN_COPY);
}

View File

@@ -0,0 +1,66 @@
/*
OCP.BRepFilletAPI — fillets and chamfers.
Both derive BRepBuilderAPI_MakeShape, so Build/Shape and — importantly —
the Generated/Modified/IsDeleted history come from there. The app reads
that history to attribute the blend faces back to the feature that made
them, the same way it reads a boolean's.
These are the two operations the app runs in an isolated subprocess, since
OCCT can hard-fault on a degenerate blend; nothing here changes that, but
releasing the GIL on Build() is what lets the pool overlap work.
*/
#include "../common/occt_module.h"
#include "../common/occt_policies.h"
#include <BRepBuilderAPI_MakeShape.hxx>
#include <BRepFilletAPI_MakeChamfer.hxx>
#include <BRepFilletAPI_MakeFillet.hxx>
#include <TopoDS_Edge.hxx>
#include <TopoDS_Face.hxx>
#include <TopoDS_Shape.hxx>
void register_BRepFilletAPI(nb::module_ &root) {
nb::module_ m = ocp_submodule(root, "BRepFilletAPI");
nb::class_<BRepFilletAPI_MakeFillet, BRepBuilderAPI_MakeShape>(
m, "BRepFilletAPI_MakeFillet")
// The ChFi3d_FilletShape overload is left out: it exists only to
// carry a default the app never overrides, and binding an unregistered
// enum as a default argument fails at module-init time.
.def(nb::init<const TopoDS_Shape &>(), "S"_a)
.def("Add",
nb::overload_cast<const Standard_Real, const TopoDS_Edge &>(
&BRepFilletAPI_MakeFillet::Add),
"Radius"_a, "E"_a)
.def("Add",
nb::overload_cast<const Standard_Real, const Standard_Real,
const TopoDS_Edge &>(
&BRepFilletAPI_MakeFillet::Add),
"R1"_a, "R2"_a, "E"_a)
.def("NbContours", &BRepFilletAPI_MakeFillet::NbContours)
.def("Reset", &BRepFilletAPI_MakeFillet::Reset);
nb::class_<BRepFilletAPI_MakeChamfer, BRepBuilderAPI_MakeShape>(
m, "BRepFilletAPI_MakeChamfer")
.def(nb::init<const TopoDS_Shape &>(), "S"_a)
.def("Add",
nb::overload_cast<const TopoDS_Edge &>(
&BRepFilletAPI_MakeChamfer::Add),
"E"_a)
.def("Add",
nb::overload_cast<const Standard_Real, const TopoDS_Edge &>(
&BRepFilletAPI_MakeChamfer::Add),
"Dis"_a, "E"_a)
.def("Add",
nb::overload_cast<const Standard_Real, const Standard_Real,
const TopoDS_Edge &, const TopoDS_Face &>(
&BRepFilletAPI_MakeChamfer::Add),
"Dis1"_a, "Dis2"_a, "E"_a, "F"_a)
// Distance-and-angle, which is how the app words an asymmetric chamfer.
.def("AddDA", &BRepFilletAPI_MakeChamfer::AddDA, "Dis"_a, "Angle"_a,
"E"_a, "F"_a)
.def("NbContours", &BRepFilletAPI_MakeChamfer::NbContours)
.def("Reset", &BRepFilletAPI_MakeChamfer::Reset);
}

View File

@@ -0,0 +1,31 @@
/*
OCP.BRepLib — repairs applied to shapes built edge by edge.
BuildCurves3d recovers the 3D curve of an edge that only carries a pcurve,
which is how the thread's helical edges (laid out in the cylinder's UV
space) become real geometry. OrientClosedSolid fixes a solid built from a
sewn shell whose orientation came out inward.
*/
#include "../common/occt_module.h"
#include "../common/occt_policies.h"
#include <BRepLib.hxx>
#include <TopoDS_Shape.hxx>
#include <TopoDS_Solid.hxx>
void register_BRepLib(nb::module_ &root) {
nb::module_ m = ocp_submodule(root, "BRepLib");
nb::class_<BRepLib> cls(m, "BRepLib");
OCP_DEF_S(
cls, "BuildCurves3d",
[](const TopoDS_Shape &S) { return BRepLib::BuildCurves3d(S); }, "S"_a,
OCP_NOGIL);
OCP_DEF_S(
cls, "OrientClosedSolid",
[](TopoDS_Solid &solid) { return BRepLib::OrientClosedSolid(solid); },
"solid"_a, OCP_NOGIL);
}

View File

@@ -0,0 +1,86 @@
/*
OCP.BRepOffsetAPI — sweeps, lofts, shells and drafts.
All five derive BRepBuilderAPI_MakeShape, so Build/Shape/history come from
there. MakeThickSolid is the one with an unusual shape: it is default
constructed and then told what to do by MakeThickSolidBySimple (a sheet
thickened into a wall) or MakeThickSolidByJoin (a solid hollowed with
faces removed), each of which does the work — so those get the GIL release
rather than Build().
*/
#include "../common/occt_module.h"
#include "../common/occt_policies.h"
#include <BRepBuilderAPI_MakeShape.hxx>
#include <BRepOffsetAPI_DraftAngle.hxx>
#include <BRepOffsetAPI_MakePipe.hxx>
#include <BRepOffsetAPI_MakePipeShell.hxx>
#include <BRepOffsetAPI_MakeThickSolid.hxx>
#include <BRepOffsetAPI_ThruSections.hxx>
#include <TopTools_ListOfShape.hxx>
#include <TopoDS_Face.hxx>
#include <TopoDS_Shape.hxx>
#include <TopoDS_Wire.hxx>
#include <gp_Dir.hxx>
#include <gp_Pln.hxx>
void register_BRepOffsetAPI(nb::module_ &root) {
nb::module_ m = ocp_submodule(root, "BRepOffsetAPI");
nb::class_<BRepOffsetAPI_ThruSections, BRepBuilderAPI_MakeShape>(
m, "BRepOffsetAPI_ThruSections")
.def(nb::init<const Standard_Boolean, const Standard_Boolean,
const Standard_Real>(),
"isSolid"_a = Standard_False, "ruled"_a = Standard_False,
"pres3d"_a = 1.0e-06)
.def("AddWire", &BRepOffsetAPI_ThruSections::AddWire, "wire"_a)
.def("CheckCompatibility",
&BRepOffsetAPI_ThruSections::CheckCompatibility, "check"_a = true);
nb::class_<BRepOffsetAPI_MakePipe, BRepBuilderAPI_MakeShape>(
m, "BRepOffsetAPI_MakePipe")
.def(nb::init<const TopoDS_Wire &, const TopoDS_Shape &>(), "Spine"_a,
"Profile"_a, OCP_NOGIL);
nb::class_<BRepOffsetAPI_MakePipeShell, BRepBuilderAPI_MakeShape>(
m, "BRepOffsetAPI_MakePipeShell")
.def(nb::init<const TopoDS_Wire &>(), "Spine"_a)
.def("SetMode",
nb::overload_cast<const Standard_Boolean>(
&BRepOffsetAPI_MakePipeShell::SetMode),
"IsFrenet"_a = Standard_False)
.def("Add",
nb::overload_cast<const TopoDS_Shape &, const Standard_Boolean,
const Standard_Boolean>(
&BRepOffsetAPI_MakePipeShell::Add),
"Profile"_a, "WithContact"_a = Standard_False,
"WithCorrection"_a = Standard_False)
.def("MakeSolid", &BRepOffsetAPI_MakePipeShell::MakeSolid, OCP_NOGIL);
nb::class_<BRepOffsetAPI_MakeThickSolid, BRepBuilderAPI_MakeShape>(
m, "BRepOffsetAPI_MakeThickSolid")
.def(nb::init<>())
.def("MakeThickSolidBySimple",
&BRepOffsetAPI_MakeThickSolid::MakeThickSolidBySimple, "theS"_a,
"theOffsetValue"_a, OCP_NOGIL)
// The trailing mode/join arguments are left off: they are enums the
// app never overrides, and an unregistered enum used as a default
// argument fails at module-init time. OCCT's own defaults apply.
.def(
"MakeThickSolidByJoin",
[](BRepOffsetAPI_MakeThickSolid &self, const TopoDS_Shape &S,
const TopTools_ListOfShape &ClosingFaces,
const Standard_Real Offset, const Standard_Real Tol) {
self.MakeThickSolidByJoin(S, ClosingFaces, Offset, Tol);
},
"S"_a, "ClosingFaces"_a, "Offset"_a, "Tol"_a, OCP_NOGIL);
nb::class_<BRepOffsetAPI_DraftAngle, BRepBuilderAPI_MakeShape>(
m, "BRepOffsetAPI_DraftAngle")
.def(nb::init<const TopoDS_Shape &>(), "S"_a)
.def("Add", &BRepOffsetAPI_DraftAngle::Add, "F"_a, "D"_a, "Angle"_a,
"NeutralPlane"_a, "Flag"_a = Standard_True)
.def("AddDone", &BRepOffsetAPI_DraftAngle::AddDone)
.def("Remove", &BRepOffsetAPI_DraftAngle::Remove, "F"_a);
}

View File

@@ -0,0 +1,70 @@
/*
OCP.BRepTools — assorted topology utilities.
Clean drops an existing triangulation, which the tessellation path does
before re-meshing at a different deflection (OCCT keeps whatever mesh a
shape already carries, so without this the second pass is a no-op).
OuterWire and UVBounds are queries; WireExplorer walks a wire's edges in
connection order rather than map order, which is what wire-following code
needs.
UVBounds reports four scalar out-parameters, so it returns a tuple —
upstream does the same, and `umin, umax, vmin, vmax = ...` is how the app
calls it.
*/
#include "../common/occt_module.h"
#include "../common/occt_policies.h"
#include <nanobind/stl/tuple.h>
#include <BRepTools.hxx>
#include <BRepTools_WireExplorer.hxx>
#include <TopoDS_Face.hxx>
#include <TopoDS_Shape.hxx>
#include <TopoDS_Wire.hxx>
void register_BRepTools(nb::module_ &root) {
nb::module_ m = ocp_submodule(root, "BRepTools");
nb::class_<BRepTools_WireExplorer>(m, "BRepTools_WireExplorer")
.def(nb::init<>())
.def(nb::init<const TopoDS_Wire &>(), "W"_a)
.def(nb::init<const TopoDS_Wire &, const TopoDS_Face &>(), "W"_a, "F"_a)
.def("Init", nb::overload_cast<const TopoDS_Wire &>(
&BRepTools_WireExplorer::Init),
"W"_a)
.def("More", &BRepTools_WireExplorer::More)
.def("Next", &BRepTools_WireExplorer::Next)
.def("Current", &BRepTools_WireExplorer::Current, OCP_RETURN_COPY)
.def("Orientation", &BRepTools_WireExplorer::Orientation)
.def("CurrentVertex", &BRepTools_WireExplorer::CurrentVertex,
OCP_RETURN_COPY)
.def("Clear", &BRepTools_WireExplorer::Clear);
nb::class_<BRepTools> cls(m, "BRepTools");
OCP_DEF_S(
cls, "Clean",
[](const TopoDS_Shape &S, const Standard_Boolean force) {
BRepTools::Clean(S, force);
},
"S"_a, "force"_a = Standard_False, OCP_NOGIL);
OCP_DEF_S(
cls, "OuterWire",
[](const TopoDS_Face &F) { return BRepTools::OuterWire(F); }, "F"_a);
OCP_DEF_S(
cls, "UVBounds",
[](const TopoDS_Face &F) {
Standard_Real umin = 0.0, umax = 0.0, vmin = 0.0, vmax = 0.0;
BRepTools::UVBounds(F, umin, umax, vmin, vmax);
return std::make_tuple(umin, umax, vmin, vmax);
},
"F"_a);
OCP_DEF_S(
cls, "Update",
[](const TopoDS_Shape &S) { BRepTools::Update(S); }, "S"_a);
}

30
src/modules/mod_GCE2d.cpp Normal file
View File

@@ -0,0 +1,30 @@
/*
OCP.GCE2d — 2D geometry constructors.
Used to lay curves out in a surface's UV space before lifting them onto it
with BRepBuilderAPI_MakeEdge: a thread's helix is a straight line in the
cylinder's parameter space, and curved text emboss draws its glyph
outlines the same way.
*/
#include "../common/occt_module.h"
#include <GCE2d_MakeLine.hxx>
#include <GCE2d_MakeSegment.hxx>
#include <gp_Dir2d.hxx>
#include <gp_Pnt2d.hxx>
void register_GCE2d(nb::module_ &root) {
nb::module_ m = ocp_submodule(root, "GCE2d");
nb::class_<GCE2d_MakeLine>(m, "GCE2d_MakeLine")
.def(nb::init<const gp_Pnt2d &, const gp_Dir2d &>(), "P"_a, "V"_a)
.def(nb::init<const gp_Pnt2d &, const gp_Pnt2d &>(), "P1"_a, "P2"_a)
.def("IsDone", &GCE2d_MakeLine::IsDone)
.def("Value", &GCE2d_MakeLine::Value);
nb::class_<GCE2d_MakeSegment>(m, "GCE2d_MakeSegment")
.def(nb::init<const gp_Pnt2d &, const gp_Pnt2d &>(), "P1"_a, "P2"_a)
.def("IsDone", &GCE2d_MakeSegment::IsDone)
.def("Value", &GCE2d_MakeSegment::Value);
}

View File

@@ -0,0 +1,61 @@
/*
OCP.GCPnts — curve discretisation.
TangentialDeflection is what turns every model edge into the polyline the
viewport draws, so it runs once per edge on every tessellation; AbscissaPoint
measures arc length for the edge anchors. Both take an Adaptor3d_Curve,
whose methods are bound on the base in mod_Adaptor3d.cpp.
The constructors compute — there is no deferred form — so they bind as
stock does.
*/
#include "../common/occt_module.h"
#include "../common/occt_policies.h"
#include <Adaptor3d_Curve.hxx>
#include <GCPnts_AbscissaPoint.hxx>
#include <GCPnts_TangentialDeflection.hxx>
#include <GCPnts_UniformDeflection.hxx>
#include <gp_Pnt.hxx>
void register_GCPnts(nb::module_ &root) {
nb::module_ m = ocp_submodule(root, "GCPnts");
nb::class_<GCPnts_TangentialDeflection>(m, "GCPnts_TangentialDeflection")
.def(nb::init<const Adaptor3d_Curve &, const Standard_Real,
const Standard_Real, const Standard_Integer,
const Standard_Real, const Standard_Real>(),
"C"_a, "AngularDeflection"_a, "CurvatureDeflection"_a,
"MinimumOfPoints"_a = 2, "UTol"_a = 1.0e-9,
"theMinLen"_a = 1.0e-7, OCP_NOGIL)
.def("NbPoints", &GCPnts_TangentialDeflection::NbPoints)
.def("Value", &GCPnts_TangentialDeflection::Value, "I"_a,
OCP_RETURN_COPY)
.def("Parameter", &GCPnts_TangentialDeflection::Parameter, "I"_a);
nb::class_<GCPnts_UniformDeflection>(m, "GCPnts_UniformDeflection")
.def(nb::init<>())
.def(nb::init<const Adaptor3d_Curve &, const Standard_Real,
const Standard_Boolean>(),
"C"_a, "Deflection"_a, "WithControl"_a = Standard_True, OCP_NOGIL)
.def("IsDone", &GCPnts_UniformDeflection::IsDone)
.def("NbPoints", &GCPnts_UniformDeflection::NbPoints)
.def("Value", &GCPnts_UniformDeflection::Value, "Index"_a,
OCP_RETURN_COPY)
.def("Parameter", &GCPnts_UniformDeflection::Parameter, "Index"_a);
nb::class_<GCPnts_AbscissaPoint> abscissa(m, "GCPnts_AbscissaPoint");
abscissa.def(nb::init<>());
OCP_DEF_S(
abscissa, "Length",
[](const Adaptor3d_Curve &C) { return GCPnts_AbscissaPoint::Length(C); },
"C"_a, OCP_NOGIL);
OCP_DEF_S(
abscissa, "Length",
[](const Adaptor3d_Curve &C, const Standard_Real U1,
const Standard_Real U2) {
return GCPnts_AbscissaPoint::Length(C, U1, U2);
},
"C"_a, "U1"_a, "U2"_a, OCP_NOGIL);
}

View File

@@ -0,0 +1,42 @@
/*
OCP.IntCurvesFace — ray casting against a shape.
The surface-pattern feature fires a ray from each sample point along the
inverted normal to find the opposite wall, which is how it measures local
wall thickness. Load prepares the shape once; PerformNearest runs per ray
inside a Python loop, so both release the GIL but neither is a long call.
*/
#include "../common/occt_module.h"
#include "../common/occt_policies.h"
#include <IntCurvesFace_ShapeIntersector.hxx>
#include <TopoDS_Shape.hxx>
#include <gp_Lin.hxx>
#include <gp_Pnt.hxx>
void register_IntCurvesFace(nb::module_ &root) {
nb::module_ m = ocp_submodule(root, "IntCurvesFace");
nb::class_<IntCurvesFace_ShapeIntersector>(m,
"IntCurvesFace_ShapeIntersector")
.def(nb::init<>())
.def("Load", &IntCurvesFace_ShapeIntersector::Load, "Sh"_a, "Tol"_a,
OCP_NOGIL)
.def("PerformNearest",
nb::overload_cast<const gp_Lin &, const Standard_Real,
const Standard_Real>(
&IntCurvesFace_ShapeIntersector::PerformNearest),
"L"_a, "PInf"_a, "PSup"_a, OCP_NOGIL)
.def("Perform",
nb::overload_cast<const gp_Lin &, const Standard_Real,
const Standard_Real>(
&IntCurvesFace_ShapeIntersector::Perform),
"L"_a, "PInf"_a, "PSup"_a, OCP_NOGIL)
.def("IsDone", &IntCurvesFace_ShapeIntersector::IsDone)
.def("NbPnt", &IntCurvesFace_ShapeIntersector::NbPnt)
.def("Pnt", &IntCurvesFace_ShapeIntersector::Pnt, "I"_a, OCP_RETURN_COPY)
.def("WParameter", &IntCurvesFace_ShapeIntersector::WParameter, "I"_a)
.def("Face", &IntCurvesFace_ShapeIntersector::Face, "I"_a,
OCP_RETURN_COPY);
}

View File

@@ -0,0 +1,61 @@
/*
OCP.ShapeAnalysis — free-boundary detection.
One use: import health counts the edges bounding a single face after a
virtual sew, which is how it tells a watertight solid from a holed shell.
ConnectEdgesToWires is the one place upstream's holder semantics and this
binding's caster differ observably. Its `wires` parameter is a
Handle& that the function *reassigns*, and the caster hands C++ a copy of
the handle rather than a reference into the Python instance — so the
reassignment would be invisible. The lambda splices the result back into
the sequence the caller passed, preserving the pass-empty-then-read shape
the app uses.
*/
#include "../common/occt_module.h"
#include "../common/occt_policies.h"
#include <ShapeAnalysis_FreeBounds.hxx>
#include <TopTools_HSequenceOfShape.hxx>
#include <TopoDS_Compound.hxx>
#include <TopoDS_Shape.hxx>
void register_ShapeAnalysis(nb::module_ &root) {
nb::module_ m = ocp_submodule(root, "ShapeAnalysis");
nb::class_<ShapeAnalysis_FreeBounds> cls(m, "ShapeAnalysis_FreeBounds");
cls.def(nb::init<>())
.def(nb::init<const TopoDS_Shape &, const Standard_Real,
const Standard_Boolean, const Standard_Boolean>(),
"shape"_a, "toler"_a, "splitclosed"_a = Standard_False,
"splitopen"_a = Standard_True, OCP_NOGIL)
.def(nb::init<const TopoDS_Shape &, const Standard_Boolean,
const Standard_Boolean, const Standard_Boolean>(),
"shape"_a, "splitclosed"_a = Standard_False,
"splitopen"_a = Standard_True,
"checkinternaledges"_a = Standard_False, OCP_NOGIL)
.def("GetClosedWires", &ShapeAnalysis_FreeBounds::GetClosedWires,
OCP_RETURN_COPY)
.def("GetOpenWires", &ShapeAnalysis_FreeBounds::GetOpenWires,
OCP_RETURN_COPY);
OCP_DEF_S(
cls, "ConnectEdgesToWires",
[](opencascade::handle<TopTools_HSequenceOfShape> &edges,
const Standard_Real toler, const Standard_Boolean shared,
opencascade::handle<TopTools_HSequenceOfShape> &wires) {
opencascade::handle<TopTools_HSequenceOfShape> out;
ShapeAnalysis_FreeBounds::ConnectEdgesToWires(edges, toler, shared,
out);
// The caller keeps its own sequence object; fill that rather than
// rebinding a handle it cannot see.
if (!wires.IsNull()) {
wires->Clear();
if (!out.IsNull()) {
wires->Append(out->ChangeSequence());
}
}
},
"edges"_a, "toler"_a, "shared"_a, "wires"_a, OCP_NOGIL);
}

View File

@@ -0,0 +1,63 @@
/*
OCP.ShapeFix — geometry healing.
Used on imported geometry and after the operations that are known to
produce slightly invalid output (curved text emboss, thickened sheets).
These are transients, so their constructors go through ocp_new.
Perform() is where the work happens, not Build(), so that is what releases
the GIL.
*/
#include "../common/occt_module.h"
#include "../common/occt_policies.h"
#include "../common/occt_transient.h"
#include <ShapeFix_Face.hxx>
#include <ShapeFix_Shape.hxx>
#include <ShapeFix_Solid.hxx>
#include <TopoDS_Face.hxx>
#include <TopoDS_Shape.hxx>
#include <TopoDS_Shell.hxx>
#include <TopoDS_Solid.hxx>
void register_ShapeFix(nb::module_ &root) {
nb::module_ m = ocp_submodule(root, "ShapeFix");
nb::class_<ShapeFix_Shape, Standard_Transient>(m, "ShapeFix_Shape")
.def(ocp_new<ShapeFix_Shape>())
.def(ocp_new<ShapeFix_Shape, const TopoDS_Shape &>(), "shape"_a)
.def("Init", nb::overload_cast<const TopoDS_Shape &>(&ShapeFix_Shape::Init),
"shape"_a)
.def(
"Perform", [](ShapeFix_Shape &self) { return self.Perform(); },
OCP_NOGIL)
.def("Shape", &ShapeFix_Shape::Shape, OCP_RETURN_COPY)
.def("SetPrecision", &ShapeFix_Shape::SetPrecision, "preci"_a)
.def("SetMinTolerance", &ShapeFix_Shape::SetMinTolerance, "mintol"_a)
.def("SetMaxTolerance", &ShapeFix_Shape::SetMaxTolerance, "maxtol"_a);
nb::class_<ShapeFix_Face, Standard_Transient>(m, "ShapeFix_Face")
.def(ocp_new<ShapeFix_Face>())
.def(ocp_new<ShapeFix_Face, const TopoDS_Face &>(), "face"_a)
.def(
"Perform", [](ShapeFix_Face &self) { return self.Perform(); },
OCP_NOGIL)
.def("FixOrientation",
nb::overload_cast<>(&ShapeFix_Face::FixOrientation), OCP_NOGIL)
.def("Face", &ShapeFix_Face::Face, OCP_RETURN_COPY);
nb::class_<ShapeFix_Solid, Standard_Transient>(m, "ShapeFix_Solid")
.def(ocp_new<ShapeFix_Solid>())
.def(ocp_new<ShapeFix_Solid, const TopoDS_Solid &>(), "solid"_a)
.def(
"Perform", [](ShapeFix_Solid &self) { return self.Perform(); },
OCP_NOGIL)
.def(
"SolidFromShell",
[](ShapeFix_Solid &self, const TopoDS_Shell &shell) {
return self.SolidFromShell(shell);
},
"shell"_a, OCP_RETURN_COPY, OCP_NOGIL)
.def("Shape", &ShapeFix_Solid::Shape, OCP_RETURN_COPY);
}

View File

@@ -0,0 +1,35 @@
/*
OCP.ShapeUpgrade — merge faces and edges that share a surface.
The app runs this after almost every fuse, so a bidirectional extrude ends
up with the face count of a single prism rather than two prisms glued at
the sketch plane. Not a MakeShape, so Build/Shape are its own.
*/
#include "../common/occt_module.h"
#include "../common/occt_policies.h"
#include "../common/occt_transient.h"
#include <ShapeUpgrade_UnifySameDomain.hxx>
#include <TopoDS_Shape.hxx>
void register_ShapeUpgrade(nb::module_ &root) {
nb::module_ m = ocp_submodule(root, "ShapeUpgrade");
nb::class_<ShapeUpgrade_UnifySameDomain, Standard_Transient>(
m, "ShapeUpgrade_UnifySameDomain")
.def(ocp_new<ShapeUpgrade_UnifySameDomain>())
.def(ocp_new<ShapeUpgrade_UnifySameDomain, const TopoDS_Shape &,
const Standard_Boolean, const Standard_Boolean,
const Standard_Boolean>(),
"aShape"_a, "UnifyEdges"_a = Standard_True,
"UnifyFaces"_a = Standard_True,
"ConcatBSplines"_a = Standard_False)
.def("Initialize", &ShapeUpgrade_UnifySameDomain::Initialize, "aShape"_a,
"UnifyEdges"_a = Standard_True, "UnifyFaces"_a = Standard_True,
"ConcatBSplines"_a = Standard_False)
.def("SetSafeInputMode", &ShapeUpgrade_UnifySameDomain::SetSafeInputMode,
"theValue"_a)
.def("Build", &ShapeUpgrade_UnifySameDomain::Build, OCP_NOGIL)
.def("Shape", &ShapeUpgrade_UnifySameDomain::Shape, OCP_RETURN_COPY);
}

View File

@@ -10,11 +10,13 @@
#include "../common/occt_module.h" #include "../common/occt_module.h"
#include "../common/occt_policies.h" #include "../common/occt_policies.h"
#include "../common/occt_transient.h"
#include <nanobind/make_iterator.h> #include <nanobind/make_iterator.h>
#include <TopTools_IndexedDataMapOfShapeListOfShape.hxx> #include <TopTools_IndexedDataMapOfShapeListOfShape.hxx>
#include <TopTools_IndexedMapOfShape.hxx> #include <TopTools_IndexedMapOfShape.hxx>
#include <TopTools_HSequenceOfShape.hxx>
#include <TopTools_ListOfShape.hxx> #include <TopTools_ListOfShape.hxx>
void register_TopTools(nb::module_ &root) { void register_TopTools(nb::module_ &root) {
@@ -97,4 +99,26 @@ void register_TopTools(nb::module_ &root) {
self.Clear(Standard_True); self.Clear(Standard_True);
}) })
.def("__len__", &TopTools_IndexedDataMapOfShapeListOfShape::Size); .def("__len__", &TopTools_IndexedDataMapOfShapeListOfShape::Size);
// A transient sequence, unlike the maps above — ShapeAnalysis_FreeBounds
// takes and returns it by handle.
nb::class_<TopTools_HSequenceOfShape, Standard_Transient>(
m, "TopTools_HSequenceOfShape")
.def(ocp_new<TopTools_HSequenceOfShape>())
.def(
"Append",
[](TopTools_HSequenceOfShape &self, const TopoDS_Shape &item) {
self.Append(item);
},
"theItem"_a)
.def("Length", &TopTools_HSequenceOfShape::Length)
.def("IsEmpty", &TopTools_HSequenceOfShape::IsEmpty)
.def(
"Value",
[](const TopTools_HSequenceOfShape &self, Standard_Integer index) {
return self.Value(index);
},
"theIndex"_a, OCP_RETURN_COPY)
.def("Clear", [](TopTools_HSequenceOfShape &self) { self.Clear(); })
.def("__len__", &TopTools_HSequenceOfShape::Length);
} }

View File

@@ -499,5 +499,197 @@
] ]
} }
} }
},
"inc2": {
"fillet": {
"sha256": "4da9eb256a1e012b94655f6ecd02b34590c4e058beb336794fb68c3047c0070c",
"volume": 5974.247779607694,
"area": 2172.530964914873,
"bbox": [
-1.000000004440892e-07,
-1.000000004440892e-07,
-1e-07,
10.0000001,
20.0000001,
30.0000001
],
"n_faces": 7,
"n_edges": 15,
"n_solids": 1,
"history": [
{
"pre": 0,
"deleted": false,
"modified": [
3
],
"generated": []
},
{
"pre": 1,
"deleted": false,
"modified": [],
"generated": []
},
{
"pre": 2,
"deleted": false,
"modified": [
37
],
"generated": []
},
{
"pre": 3,
"deleted": false,
"modified": [],
"generated": []
},
{
"pre": 4,
"deleted": false,
"modified": [
32
],
"generated": []
},
{
"pre": 5,
"deleted": false,
"modified": [
13
],
"generated": []
}
]
},
"chamfer": {
"sha256": "cdf8d040a239e6202f94e38fc0fc9595e475180c7067a5f1e2391d9ed95d03af",
"volume": 5966.25,
"area": 2171.389610306789,
"bbox": [
-1.000000001110223e-07,
-1.000000001110223e-07,
-1e-07,
10.0000001,
20.0000001,
30.000000100000005
],
"n_faces": 7,
"n_edges": 15,
"n_solids": 1
},
"splitter": {
"volume": 6000.0,
"area": 3400.0,
"bbox": [
-1.5e-07,
-1.5e-07,
-1.5e-07,
10.00000015,
20.00000015,
30.00000015
],
"n_faces": 16,
"n_edges": 28,
"n_solids": 3,
"history": [
{
"pre": 0,
"deleted": false,
"modified": [
3,
37,
61
],
"generated": [
8,
41
]
},
{
"pre": 1,
"deleted": false,
"modified": [
33,
57,
81
],
"generated": [
24,
53
]
},
{
"pre": 2,
"deleted": false,
"modified": [],
"generated": []
},
{
"pre": 3,
"deleted": false,
"modified": [],
"generated": []
},
{
"pre": 4,
"deleted": false,
"modified": [
25,
54,
78
],
"generated": [
22,
51
]
},
{
"pre": 5,
"deleted": false,
"modified": [
13,
44,
68
],
"generated": [
15,
46
]
}
]
},
"unified": {
"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
},
"distance": {
"is_done": true,
"value": 30.0,
"n_solutions": 4
},
"classifier": {
"inside": 0,
"outside": 1
},
"uv_bounds": [
0.0,
30.0,
-20.0,
0.0
],
"outer_wire_edges": 4
} }
} }

224
tests/test_inc2_features.py Normal file
View File

@@ -0,0 +1,224 @@
"""Inc 2 gate: the feature tail reproduces the stock wheel's answers.
Same contract as test_inc1_modeling.py — see its module docstring for why the
app's own suite cannot gate an increment and what the tolerances mean.
The fillet history is the load-bearing check here: a blend's provenance is read
exactly like a boolean's, and it is what attributes the new faces back to the
feature that made them.
"""
from __future__ import annotations
import pytest
from OCP.BRepAlgoAPI import BRepAlgoAPI_Splitter
from OCP.BRepClass3d import BRepClass3d_SolidClassifier
from OCP.BRepExtrema import BRepExtrema_DistShapeShape
from OCP.BRepFilletAPI import BRepFilletAPI_MakeChamfer, BRepFilletAPI_MakeFillet
from OCP.BRepPrimAPI import BRepPrimAPI_MakeBox
from OCP.BRepTools import BRepTools, BRepTools_WireExplorer
from OCP.ShapeUpgrade import ShapeUpgrade_UnifySameDomain
from OCP.TopAbs import TopAbs_EDGE, TopAbs_FACE
from OCP.TopoDS import TopoDS
from OCP.TopTools import TopTools_ListOfShape
from .test_inc1_modeling import assert_close, brep_sha, history, measure, sub_shapes
@pytest.fixture(scope="module")
def inc2(manifest):
if "inc2" not in manifest:
pytest.skip("manifest predates the Inc 2 reference block")
return manifest["inc2"]
def _box():
return BRepPrimAPI_MakeBox(10.0, 20.0, 30.0).Shape()
def _compare(got: dict, expected: dict, what: str) -> None:
for key in ("n_faces", "n_edges", "n_solids"):
assert got[key] == expected[key], f"{what}.{key}"
for key in ("volume", "area", "bbox"):
assert_close(got[key], expected[key], f"{what}.{key}")
def test_fillet_matches_stock_including_history(inc2):
box = _box()
maker = BRepFilletAPI_MakeFillet(box)
maker.Add(2.0, TopoDS.Edge_s(sub_shapes(box, TopAbs_EDGE)[0]))
maker.Build()
assert maker.IsDone()
filleted = maker.Shape()
expected = inc2["fillet"]
assert brep_sha(filleted) == expected["sha256"]
_compare(measure(filleted), expected, "fillet")
# The blend's provenance, read the same way a boolean's is.
assert history(maker, box, filleted) == expected["history"]
def test_chamfer_matches_stock(inc2):
box = _box()
maker = BRepFilletAPI_MakeChamfer(box)
maker.Add(1.5, TopoDS.Edge_s(sub_shapes(box, TopAbs_EDGE)[0]))
maker.Build()
assert maker.IsDone()
expected = inc2["chamfer"]
assert brep_sha(maker.Shape()) == expected["sha256"]
_compare(measure(maker.Shape()), expected, "chamfer")
def test_splitter_matches_stock_including_history(inc2):
from OCP.gp import gp_Pnt
box = _box()
splitter = BRepAlgoAPI_Splitter()
args, tools = TopTools_ListOfShape(), TopTools_ListOfShape()
args.Append(box)
tools.Append(BRepPrimAPI_MakeBox(gp_Pnt(-5, 5, -5), 20.0, 1.0, 40.0).Shape())
splitter.SetArguments(args)
splitter.SetTools(tools)
splitter.Build()
assert splitter.IsDone()
expected = inc2["splitter"]
_compare(measure(splitter.Shape()), expected, "splitter")
# Region attribution in the sketch builder reads exactly this.
assert history(splitter, box, splitter.Shape()) == expected["history"]
def test_unify_same_domain_matches_stock(inc2, fixture_shapes):
unify = ShapeUpgrade_UnifySameDomain(fixture_shapes["fused"], True, True, True)
unify.Build()
_compare(measure(unify.Shape()), inc2["unified"], "unified")
def test_distance_and_classifier_match_stock(inc2):
from OCP.gp import gp_Pnt
dist = BRepExtrema_DistShapeShape(
_box(), BRepPrimAPI_MakeBox(gp_Pnt(40, 0, 0), 5.0, 5.0, 5.0).Shape()
)
assert dist.IsDone() == inc2["distance"]["is_done"]
assert dist.NbSolution() == inc2["distance"]["n_solutions"]
assert_close(dist.Value(), inc2["distance"]["value"], "distance")
box = _box()
inside = BRepClass3d_SolidClassifier(box, gp_Pnt(5, 10, 15), 1e-7)
outside = BRepClass3d_SolidClassifier(box, gp_Pnt(50, 10, 15), 1e-7)
assert int(inside.State()) == inc2["classifier"]["inside"]
assert int(outside.State()) == inc2["classifier"]["outside"]
def test_brep_tools_queries_match_stock(inc2):
face = TopoDS.Face_s(sub_shapes(_box(), TopAbs_FACE)[0])
assert_close(list(BRepTools.UVBounds_s(face)), inc2["uv_bounds"], "uv_bounds")
outer = BRepTools.OuterWire_s(face)
assert len(sub_shapes(outer, TopAbs_EDGE)) == inc2["outer_wire_edges"]
def test_wire_explorer_walks_in_connection_order():
"""WireExplorer follows connectivity, which is the whole reason to use it
over the map order MapShapes gives."""
face = TopoDS.Face_s(sub_shapes(_box(), TopAbs_FACE)[0])
explorer = BRepTools_WireExplorer(BRepTools.OuterWire_s(face))
walked = []
while explorer.More():
walked.append(explorer.Current())
explorer.Next()
assert len(walked) == 4
# Results outlive the explorer: sub-shapes come back by value.
del explorer
assert all(not e.IsNull() for e in walked)
def test_clean_drops_the_existing_triangulation():
"""OCCT keeps whatever mesh a shape already carries, so re-meshing at a
different deflection is a no-op without this — which is why the
tessellation path calls it before its second pass."""
from OCP.BRep import BRep_Tool
from OCP.BRepMesh import BRepMesh_IncrementalMesh
from OCP.TopLoc import TopLoc_Location
shape = _box()
BRepMesh_IncrementalMesh(shape, 0.1, False, 0.5, True)
face = TopoDS.Face_s(sub_shapes(shape, TopAbs_FACE)[0])
assert BRep_Tool.Triangulation_s(face, TopLoc_Location()) is not None
BRepTools.Clean_s(shape)
face = TopoDS.Face_s(sub_shapes(shape, TopAbs_FACE)[0])
assert BRep_Tool.Triangulation_s(face, TopLoc_Location()) is None
def test_free_bounds_reports_an_open_shell():
"""A watertight solid has no free boundary; a shell missing one face
exposes that face's four edges. This is how import health tells the two
apart, and it exercises Sewing on the way.
Note it wants a genuinely sewn shell: a bare face reports nothing, in this
binding and in the stock wheel alike.
"""
from OCP.BRepBuilderAPI import BRepBuilderAPI_Sewing
from OCP.ShapeAnalysis import ShapeAnalysis_FreeBounds
def free_edges(shape) -> int:
bounds = ShapeAnalysis_FreeBounds(shape, 1e-6)
return sum(
len(sub_shapes(wires, TopAbs_EDGE))
for wires in (bounds.GetClosedWires(), bounds.GetOpenWires())
if not wires.IsNull()
)
assert free_edges(_box()) == 0
box = _box()
sewing = BRepBuilderAPI_Sewing(1e-6)
for face in sub_shapes(box, TopAbs_FACE)[:-1]: # leave one face off
sewing.Add(face)
sewing.Perform()
assert free_edges(sewing.SewedShape()) == 4
def test_ray_intersector_finds_the_opposite_wall():
"""The surface-pattern feature measures wall thickness this way."""
from OCP.gp import gp_Dir, gp_Lin, gp_Pnt
from OCP.IntCurvesFace import IntCurvesFace_ShapeIntersector
inter = IntCurvesFace_ShapeIntersector()
inter.Load(_box(), 1e-7)
inter.PerformNearest(
gp_Lin(gp_Pnt(-5.0, 10.0, 15.0), gp_Dir(1.0, 0.0, 0.0)), 0.0, 1.0e6
)
assert inter.IsDone()
assert inter.NbPnt() > 0
assert inter.WParameter(1) == pytest.approx(5.0, abs=1e-6)
def test_curve_length_matches_the_edge():
from OCP.BRepAdaptor import BRepAdaptor_Curve
from OCP.GCPnts import GCPnts_AbscissaPoint, GCPnts_TangentialDeflection
edge = TopoDS.Edge_s(sub_shapes(_box(), TopAbs_EDGE)[0])
adaptor = BRepAdaptor_Curve(edge)
length = GCPnts_AbscissaPoint.Length_s(adaptor)
assert length in (pytest.approx(10.0), pytest.approx(20.0), pytest.approx(30.0))
# The discretiser the viewport's edge polylines come from.
points = GCPnts_TangentialDeflection(adaptor, 0.15, 0.05)
assert points.NbPoints() >= 2
assert not points.Value(1).IsEqual(points.Value(points.NbPoints()), 1e-9)
def test_thick_solid_by_simple_thickens_a_face():
"""The shape rib and the open-profile extrude both lean on this."""
from OCP.BRepOffsetAPI import BRepOffsetAPI_MakeThickSolid
face = TopoDS.Face_s(sub_shapes(_box(), TopAbs_FACE)[0])
maker = BRepOffsetAPI_MakeThickSolid()
maker.MakeThickSolidBySimple(face, 2.0)
maker.Build()
assert maker.IsDone()
assert measure(maker.Shape())["n_solids"] == 1

View File

@@ -314,6 +314,92 @@ def _constructions() -> dict:
} }
def _inc2_reference() -> dict:
"""Fillet, chamfer, splitter, healing and the query classes.
The fillet history is the point of this block: a blend's provenance is read
exactly like a boolean's, and it is what attributes the new faces back to
the feature that made them.
"""
from OCP.BRepAlgoAPI import BRepAlgoAPI_Splitter
from OCP.BRepBuilderAPI import BRepBuilderAPI_MakeEdge
from OCP.BRepClass3d import BRepClass3d_SolidClassifier
from OCP.BRepExtrema import BRepExtrema_DistShapeShape
from OCP.BRepFilletAPI import BRepFilletAPI_MakeChamfer, BRepFilletAPI_MakeFillet
from OCP.BRepTools import BRepTools
from OCP.ShapeUpgrade import ShapeUpgrade_UnifySameDomain
from OCP.TopAbs import TopAbs_EDGE
from OCP.TopoDS import TopoDS
from OCP.TopTools import TopTools_ListOfShape
box = _box()
edges = _sub_shapes(box, TopAbs_EDGE)
fillet = BRepFilletAPI_MakeFillet(box)
fillet.Add(2.0, TopoDS.Edge_s(edges[0]))
fillet.Build()
filleted = fillet.Shape()
# The edge must belong to the very shape the builder was given, not an
# equal one built separately — OCCT matches by identity.
chamfer_box = _box()
chamfer = BRepFilletAPI_MakeChamfer(chamfer_box)
chamfer.Add(1.5, TopoDS.Edge_s(_sub_shapes(chamfer_box, TopAbs_EDGE)[0]))
chamfer.Build()
chamfered = chamfer.Shape()
# A splitter over an edge, mirroring how sketch regions are cut apart.
splitter = BRepAlgoAPI_Splitter()
split_box = _box()
args, tools = TopTools_ListOfShape(), TopTools_ListOfShape()
args.Append(split_box)
tools.Append(
BRepPrimAPI_MakeBox(gp_Pnt(-5, 5, -5), 20.0, 1.0, 40.0).Shape()
)
splitter.SetArguments(args)
splitter.SetTools(tools)
splitter.Build()
unify = ShapeUpgrade_UnifySameDomain(_fused(), True, True, True)
unify.Build()
dist = BRepExtrema_DistShapeShape(
_box(), BRepPrimAPI_MakeBox(gp_Pnt(40, 0, 0), 5.0, 5.0, 5.0).Shape()
)
inside = BRepClass3d_SolidClassifier(box, gp_Pnt(5, 10, 15), 1e-7)
outside = BRepClass3d_SolidClassifier(box, gp_Pnt(50, 10, 15), 1e-7)
face0 = TopoDS.Face_s(_sub_shapes(box, TopAbs_FACE)[0])
return {
"fillet": {
"sha256": hashlib.sha256(write_bytes(filleted)).hexdigest(),
**_measure(filleted),
"history": _history(fillet, box, filleted),
},
"chamfer": {
"sha256": hashlib.sha256(write_bytes(chamfered)).hexdigest(),
**_measure(chamfered),
},
"splitter": {
**_measure(splitter.Shape()),
"history": _history(splitter, split_box, splitter.Shape()),
},
"unified": _measure(unify.Shape()),
"distance": {
"is_done": bool(dist.IsDone()),
"value": dist.Value(),
"n_solutions": dist.NbSolution(),
},
"classifier": {"inside": int(inside.State()), "outside": int(outside.State())},
"uv_bounds": list(BRepTools.UVBounds_s(face0)),
"outer_wire_edges": len(
_sub_shapes(BRepTools.OuterWire_s(face0), TopAbs_EDGE)
),
}
def _inc1_reference() -> dict: def _inc1_reference() -> dict:
op, base, _tool = _cut_cylinder_op() op, base, _tool = _cut_cylinder_op()
return { return {
@@ -359,6 +445,7 @@ 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() manifest["inc1"] = _inc1_reference()
manifest["inc2"] = _inc2_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}")