10C Inc 3 + 4: I/O and text — the app's whole OCP surface is bound

138/138 symbols across 53 modules. New: IFSelect, Interface, XSControl,
IGESData, STEPControl, IGESControl, RWStl, Graphic3d, NCollection, StdPrs.

**The full app suite passes against this wheel: 1797 passed, 1 skipped — the
same result as the stock wheel**, run from the parity venv.

Getting there needed six methods that no static analysis could have found:
`inventory --check` only sees symbols reached through an import, so a method
called on an instance is invisible to it. The suite found them in one pass, and
one of them (gp_Vec.Reverse, which every outward-normal probe calls) accounted
for 311 of the 255 failing tests on its own. The others: gp_Trsf.SetMirror over
a plane and a point, Geom_Surface.D0, BRep_Builder.MakeFace from a
triangulation, MakePipeShell.SetMode with a fixed binormal, and MakeFace from a
surface plus tolerance.

Open question **S5 is settled: no**. The wheel does not need OCCT's share/
resources. test_inc3_io.py asserts no CSF_* variable is set and then round-trips
STEP and IGES, reading the declared units back off both — which is exactly the
resource-less container the question was about.

The XSTEP readers keep the GIL, amending the blanket "file readers and writers"
line in design.md's GIL policy. STEP and IGES traffic in process-global
Interface_Static state, the IGES reader is documented as not thread-safe, and
the app already serialises imports behind a lock — so holding it costs nothing
and removes a class of question. RWStl, which touches no global state, releases.

XSControl_Reader and IGESData are registered although the app imports neither:
they are the reader base both concrete readers inherit their transfer surface
from, and the model-to-global-section chain the IGES unit probe walks.

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:31:15 +02:00
parent 0ce43a94aa
commit 01a5bcf188
20 changed files with 723 additions and 1 deletions

View File

@@ -66,6 +66,18 @@ void register_BRepLib(nb::module_ &);
void register_BRepExtrema(nb::module_ &);
void register_BRepClass3d(nb::module_ &);
void register_IntCurvesFace(nb::module_ &);
// Inc 3 — I/O
void register_IFSelect(nb::module_ &);
void register_Interface(nb::module_ &);
void register_XSControl(nb::module_ &);
void register_IGESData(nb::module_ &);
void register_STEPControl(nb::module_ &);
void register_IGESControl(nb::module_ &);
void register_RWStl(nb::module_ &);
// Inc 4 — remainder
void register_Graphic3d(nb::module_ &);
void register_NCollection(nb::module_ &);
void register_StdPrs(nb::module_ &);
void register_ext(nb::module_ &);
NB_MODULE(_OCP, m) {
@@ -124,6 +136,20 @@ NB_MODULE(_OCP, m) {
register_BRepClass3d(m);
register_IntCurvesFace(m);
// Inc 3. XSControl declares the reader base both concrete readers use.
register_IFSelect(m);
register_Interface(m);
register_XSControl(m);
register_IGESData(m);
register_STEPControl(m);
register_IGESControl(m);
register_RWStl(m);
// Inc 4. Graphic3d's alignment enums are arguments to StdPrs.
register_Graphic3d(m);
register_NCollection(m);
register_StdPrs(m);
register_ext(m);
m.attr("__all_modules__") = ocp_module_registry();

View File

@@ -34,6 +34,13 @@ void register_BRep(nb::module_ &root) {
.def(nb::init<>())
.def("MakeCompound", &BRep_Builder::MakeCompound, "C"_a)
.def("MakeShell", &BRep_Builder::MakeShell, "S"_a)
.def(
"MakeFace",
[](const BRep_Builder &self, TopoDS_Face &F,
const opencascade::handle<Poly_Triangulation> &T) {
self.MakeFace(F, T);
},
"F"_a, "T"_a)
.def("MakeSolid", &BRep_Builder::MakeSolid, "S"_a)
.def("MakeWire", &BRep_Builder::MakeWire, "W"_a)
.def("Add",

View File

@@ -105,6 +105,9 @@ void register_BRepBuilderAPI(nb::module_ &root) {
"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 Standard_Real>(),
"S"_a, "TolDegen"_a)
.def(nb::init<const opencascade::handle<Geom_Surface> &,
const TopoDS_Wire &, const Standard_Boolean>(),
"S"_a, "W"_a, "Inside"_a = Standard_True)

View File

@@ -50,6 +50,10 @@ void register_BRepOffsetAPI(nb::module_ &root) {
nb::overload_cast<const Standard_Boolean>(
&BRepOffsetAPI_MakePipeShell::SetMode),
"IsFrenet"_a = Standard_False)
.def("SetMode",
nb::overload_cast<const gp_Dir &>(
&BRepOffsetAPI_MakePipeShell::SetMode),
"BiNormal"_a)
.def("Add",
nb::overload_cast<const TopoDS_Shape &, const Standard_Boolean,
const Standard_Boolean>(

View File

@@ -64,6 +64,7 @@ void register_Geom(nb::module_ &root) {
nb::class_<Geom_Surface, Standard_Transient>(m, "Geom_Surface")
.def("Value", &Geom_Surface::Value, "U"_a, "V"_a, OCP_RETURN_COPY)
.def("D0", &Geom_Surface::D0, "U"_a, "V"_a, "P"_a)
.def("IsUPeriodic", &Geom_Surface::IsUPeriodic)
.def("IsVPeriodic", &Geom_Surface::IsVPeriodic)
.def("IsUClosed", &Geom_Surface::IsUClosed)

View File

@@ -0,0 +1,31 @@
/*
OCP.Graphic3d — text alignment.
Only the two alignment enums, which the text-emboss feature passes to the
BRep text builder. Nothing else in Graphic3d is a modelling concept, and
the app draws nothing through OCCT.
*/
#include "../common/occt_module.h"
#include <Graphic3d_HorizontalTextAlignment.hxx>
#include <Graphic3d_VerticalTextAlignment.hxx>
void register_Graphic3d(nb::module_ &root) {
nb::module_ m = ocp_submodule(root, "Graphic3d");
nb::enum_<Graphic3d_HorizontalTextAlignment>(
m, "Graphic3d_HorizontalTextAlignment", nb::is_arithmetic())
.value("Graphic3d_HTA_LEFT", Graphic3d_HTA_LEFT)
.value("Graphic3d_HTA_CENTER", Graphic3d_HTA_CENTER)
.value("Graphic3d_HTA_RIGHT", Graphic3d_HTA_RIGHT)
.export_values();
nb::enum_<Graphic3d_VerticalTextAlignment>(
m, "Graphic3d_VerticalTextAlignment", nb::is_arithmetic())
.value("Graphic3d_VTA_BOTTOM", Graphic3d_VTA_BOTTOM)
.value("Graphic3d_VTA_CENTER", Graphic3d_VTA_CENTER)
.value("Graphic3d_VTA_TOP", Graphic3d_VTA_TOP)
.value("Graphic3d_VTA_TOPFIRSTLINE", Graphic3d_VTA_TOPFIRSTLINE)
.export_values();
}

View File

@@ -0,0 +1,23 @@
/*
OCP.IFSelect — the return status every XSTEP read reports through.
The app only ever compares against IFSelect_RetDone, but the whole enum is
bound so a failure prints as something other than a bare integer.
*/
#include "../common/occt_module.h"
#include <IFSelect_ReturnStatus.hxx>
void register_IFSelect(nb::module_ &root) {
nb::module_ m = ocp_submodule(root, "IFSelect");
nb::enum_<IFSelect_ReturnStatus>(m, "IFSelect_ReturnStatus",
nb::is_arithmetic())
.value("IFSelect_RetVoid", IFSelect_RetVoid)
.value("IFSelect_RetDone", IFSelect_RetDone)
.value("IFSelect_RetError", IFSelect_RetError)
.value("IFSelect_RetFail", IFSelect_RetFail)
.value("IFSelect_RetStop", IFSelect_RetStop)
.export_values();
}

View File

@@ -0,0 +1,57 @@
/*
OCP.IGESControl — IGES read and write.
The reader inherits its transfer surface from XSControl_Reader. What is
specific here is the unit probe: the app reads the file's declared units
off the model's global section (IGESModel().GlobalSection().UnitName()),
which is why IGESData_IGESModel and IGESData_GlobalSection have to be
registered even though the app imports neither by name.
The writer is used only by the test suite, to produce IGES payloads to
import back; it is bound because those tests are part of the Inc 4 gate.
GIL held, per mod_XSControl.cpp — the IGES reader is explicitly documented
as not thread-safe.
*/
#include "../common/occt_module.h"
#include "../common/occt_policies.h"
#include <IGESControl_Controller.hxx>
#include <IGESControl_Reader.hxx>
#include <IGESControl_Writer.hxx>
#include <IGESData_GlobalSection.hxx>
#include <IGESData_IGESModel.hxx>
#include <TCollection_HAsciiString.hxx>
#include <TopoDS_Shape.hxx>
#include <XSControl_Reader.hxx>
void register_IGESControl(nb::module_ &root) {
nb::module_ m = ocp_submodule(root, "IGESControl");
nb::class_<IGESControl_Controller> controller(m, "IGESControl_Controller");
OCP_DEF_S(
controller, "Init", []() { return IGESControl_Controller::Init(); });
nb::class_<IGESControl_Reader, XSControl_Reader>(m, "IGESControl_Reader")
.def(nb::init<>())
.def("IGESModel", &IGESControl_Reader::IGESModel);
nb::class_<IGESControl_Writer>(m, "IGESControl_Writer")
.def(nb::init<>())
.def(nb::init<const Standard_CString, const Standard_Integer>(),
"unit_name"_a, "modecr"_a = 0)
// AddShape carries a Message_ProgressRange the app never supplies.
.def(
"AddShape",
[](IGESControl_Writer &self, const TopoDS_Shape &sh) {
return self.AddShape(sh);
},
"sh"_a, OCP_NOGIL)
.def("ComputeModel", &IGESControl_Writer::ComputeModel, OCP_NOGIL)
.def(
"Write",
[](IGESControl_Writer &self, const Standard_CString file,
const Standard_Boolean fnes) { return self.Write(file, fnes); },
"file"_a, "fnes"_a = Standard_False);
}

View File

@@ -0,0 +1,38 @@
/*
OCP.IGESData — the IGES model's global section.
Not imported by the app, but reached through it: the unit probe walks
reader.IGESModel().GlobalSection().UnitName(), so both classes need to be
registered for that chain to convert. Only the two unit accessors are
bound.
UnitName hands back a Handle(TCollection_HAsciiString), which is why that
transient is registered here too rather than in mod_TCollection.cpp — it
has no other consumer.
*/
#include "../common/occt_module.h"
#include "../common/occt_policies.h"
#include <IGESData_GlobalSection.hxx>
#include <IGESData_IGESModel.hxx>
#include <TCollection_HAsciiString.hxx>
void register_IGESData(nb::module_ &root) {
nb::module_ m = ocp_submodule(root, "IGESData");
nb::class_<TCollection_HAsciiString, Standard_Transient>(
m, "TCollection_HAsciiString")
.def("ToCString", &TCollection_HAsciiString::ToCString)
.def("Length", &TCollection_HAsciiString::Length)
.def("__str__", &TCollection_HAsciiString::ToCString);
nb::class_<IGESData_GlobalSection>(m, "IGESData_GlobalSection")
.def(nb::init<>())
.def("UnitName", &IGESData_GlobalSection::UnitName)
.def("UnitValue", &IGESData_GlobalSection::UnitValue);
nb::class_<IGESData_IGESModel, Standard_Transient>(m, "IGESData_IGESModel")
.def("GlobalSection", &IGESData_IGESModel::GlobalSection,
OCP_RETURN_COPY);
}

View File

@@ -0,0 +1,43 @@
/*
OCP.Interface — XSTEP's process-global settings table.
The app reads and writes "xstep.cascade.unit" around an IGES import to make
the reader interpret the file in its own units, then puts the previous
value back. That is genuinely global state, which is why the app holds a
lock across the whole read — and why these keep the GIL.
*/
#include "../common/occt_module.h"
#include <Interface_Static.hxx>
void register_Interface(nb::module_ &root) {
nb::module_ m = ocp_submodule(root, "Interface");
nb::class_<Interface_Static> cls(m, "Interface_Static");
OCP_DEF_S(
cls, "CVal",
[](const Standard_CString name) { return Interface_Static::CVal(name); },
"name"_a);
OCP_DEF_S(
cls, "SetCVal",
[](const Standard_CString name, const Standard_CString val) {
return Interface_Static::SetCVal(name, val);
},
"name"_a, "val"_a);
OCP_DEF_S(
cls, "IVal",
[](const Standard_CString name) { return Interface_Static::IVal(name); },
"name"_a);
OCP_DEF_S(
cls, "SetIVal",
[](const Standard_CString name, const Standard_Integer val) {
return Interface_Static::SetIVal(name, val);
},
"name"_a, "val"_a);
OCP_DEF_S(
cls, "RVal",
[](const Standard_CString name) { return Interface_Static::RVal(name); },
"name"_a);
}

View File

@@ -0,0 +1,32 @@
/*
OCP.NCollection — the UTF-8 string the font API takes.
NCollection_Utf8String is the only NCollection type the app names directly:
it wraps the font path and the text to emboss. Constructed from a Python
str, which nanobind hands over as UTF-8 already.
*/
#include "../common/occt_module.h"
#include <NCollection_String.hxx>
void register_NCollection(nb::module_ &root) {
nb::module_ m = ocp_submodule(root, "NCollection");
nb::class_<NCollection_Utf8String>(m, "NCollection_Utf8String")
.def(nb::init<>())
// Upstream also offers an explicit-length overload; the app always
// passes a whole Python str, which arrives NUL-terminated, so only the
// one-argument form is bound.
.def(
"__init__",
[](NCollection_Utf8String *self, const char *text) {
new (self) NCollection_Utf8String(text);
},
"theCopy"_a)
.def("ToCString", &NCollection_Utf8String::ToCString)
.def("Size", &NCollection_Utf8String::Size)
.def("Length", &NCollection_Utf8String::Length)
.def("IsEmpty", &NCollection_Utf8String::IsEmpty)
.def("__str__", &NCollection_Utf8String::ToCString);
}

27
src/modules/mod_RWStl.cpp Normal file
View File

@@ -0,0 +1,27 @@
/*
OCP.RWStl — STL read.
Returns a bare Poly_Triangulation, which the app wraps into a shell of
triangle faces itself. Nothing writes STL through OCCT: the app has its
own writer, which welds and de-duplicates first.
Unlike the XSTEP readers this touches no global state, so it releases the
GIL.
*/
#include "../common/occt_module.h"
#include "../common/occt_policies.h"
#include <Poly_Triangulation.hxx>
#include <RWStl.hxx>
void register_RWStl(nb::module_ &root) {
nb::module_ m = ocp_submodule(root, "RWStl");
nb::class_<RWStl> cls(m, "RWStl");
OCP_DEF_S(
cls, "ReadFile",
[](const Standard_CString file) { return RWStl::ReadFile(file); },
"theFile"_a, OCP_NOGIL);
}

View File

@@ -0,0 +1,87 @@
/*
OCP.STEPControl — STEP read and write.
The reader inherits its transfer surface from XSControl_Reader; what is
added here is the unit handling the app needs (FileUnits reports what the
file declares, SetSystemLengthUnit rescales the transfer) and ReadStream,
which takes a Python file-like.
ReadStream reuses the slurp bridge from occt_stream.h rather than wrapping
a streambuf that calls back into Python — same reasoning as BinTools, and
the same one extra copy.
The GIL stays held throughout, per the note in mod_XSControl.cpp: XSTEP is
built on process-global state and the app serialises imports anyway.
This is also where the S5 question lands (design.md): whether the wheel has
to ship OCCT's share/ resources for STEP to resolve units in a container
with no CSF_* environment. tests/test_inc3_io.py answers it.
*/
#include "../common/occt_module.h"
#include "../common/occt_policies.h"
#include "../common/occt_stream.h"
#include <STEPControl_Controller.hxx>
#include <STEPControl_Reader.hxx>
#include <STEPControl_StepModelType.hxx>
#include <STEPControl_Writer.hxx>
#include <TColStd_SequenceOfAsciiString.hxx>
#include <TopoDS_Shape.hxx>
#include <XSControl_Reader.hxx>
#include <sstream>
void register_STEPControl(nb::module_ &root) {
nb::module_ m = ocp_submodule(root, "STEPControl");
nb::enum_<STEPControl_StepModelType>(m, "STEPControl_StepModelType",
nb::is_arithmetic())
.value("STEPControl_AsIs", STEPControl_AsIs)
.value("STEPControl_ManifoldSolidBrep", STEPControl_ManifoldSolidBrep)
.value("STEPControl_BrepWithVoids", STEPControl_BrepWithVoids)
.value("STEPControl_FacetedBrep", STEPControl_FacetedBrep)
.value("STEPControl_FacetedBrepAndBrepWithVoids",
STEPControl_FacetedBrepAndBrepWithVoids)
.value("STEPControl_ShellBasedSurfaceModel",
STEPControl_ShellBasedSurfaceModel)
.value("STEPControl_GeometricCurveSet", STEPControl_GeometricCurveSet)
.value("STEPControl_Hybrid", STEPControl_Hybrid)
.export_values();
nb::class_<STEPControl_Controller> controller(m, "STEPControl_Controller");
OCP_DEF_S(
controller, "Init", []() { return STEPControl_Controller::Init(); });
nb::class_<STEPControl_Reader, XSControl_Reader>(m, "STEPControl_Reader")
.def(nb::init<>())
.def(
"ReadStream",
[](STEPControl_Reader &self, const char *name, nb::handle stream) {
std::istringstream buffer(ocp_slurp(stream));
return self.ReadStream(name, buffer);
},
"theName"_a, "theIStream"_a)
.def("FileUnits", &STEPControl_Reader::FileUnits, "theLengthUnits"_a,
"theAngleUnits"_a, "theSolidAngleUnits"_a)
.def("SetSystemLengthUnit", &STEPControl_Reader::SetSystemLengthUnit,
"theLengthUnit"_a)
.def("SystemLengthUnit", &STEPControl_Reader::SystemLengthUnit);
nb::class_<STEPControl_Writer>(m, "STEPControl_Writer")
.def(nb::init<>())
.def(
"Transfer",
[](STEPControl_Writer &self, const TopoDS_Shape &sh,
const STEPControl_StepModelType mode,
const Standard_Boolean compgraph) {
return self.Transfer(sh, mode, compgraph);
},
"sh"_a, "mode"_a, "compgraph"_a = Standard_True)
.def(
"Write",
[](STEPControl_Writer &self, const Standard_CString filename) {
return self.Write(filename);
},
"filename"_a);
}

View File

@@ -0,0 +1,57 @@
/*
OCP.StdPrs — glyph outlines as BRep.
Text emboss loads a bundled font and asks the builder for the text as a
compound of face outlines, holes already subtracted, which it then extrudes
into or out of the model.
These live in TKService, and both classes are real in 7.9.3 rather than the
deprecated typedefs of Font_BRepFont/Font_BRepTextBuilder that OCCT 8.0
turns them into — so the 8.0 port is a rename here and nothing else. The
builder image already asserts TKService links without libGL or libX11, so
binding them costs the runtime image nothing.
Rendering a glyph is FreeType work with no Python in it, so it releases the
GIL.
*/
#include "../common/occt_module.h"
#include "../common/occt_policies.h"
#include "../common/occt_transient.h"
#include <NCollection_String.hxx>
#include <StdPrs_BRepFont.hxx>
#include <StdPrs_BRepTextBuilder.hxx>
#include <TopoDS_Shape.hxx>
#include <gp_Ax3.hxx>
void register_StdPrs(nb::module_ &root) {
nb::module_ m = ocp_submodule(root, "StdPrs");
nb::class_<StdPrs_BRepFont, Standard_Transient>(m, "StdPrs_BRepFont")
.def(ocp_new<StdPrs_BRepFont>())
.def(
"Init",
[](StdPrs_BRepFont &self, const NCollection_String &path,
const Standard_Real size, const Standard_Integer faceId) {
return self.Init(path, size, faceId);
},
"theFontPath"_a, "theSize"_a, "theFaceId"_a, OCP_NOGIL)
.def("Release", &StdPrs_BRepFont::Release)
.def("Ascender", &StdPrs_BRepFont::Ascender)
.def("Descender", &StdPrs_BRepFont::Descender);
nb::class_<StdPrs_BRepTextBuilder>(m, "StdPrs_BRepTextBuilder")
.def(nb::init<>())
.def(
"Perform",
[](StdPrs_BRepTextBuilder &self, StdPrs_BRepFont &font,
const NCollection_String &text, const gp_Ax3 &penLoc,
const Graphic3d_HorizontalTextAlignment hAlign,
const Graphic3d_VerticalTextAlignment vAlign) {
return self.Perform(font, text, penLoc, hAlign, vAlign);
},
"theFont"_a, "theString"_a, "thePenLoc"_a = gp_Ax3(),
"theHAlign"_a = Graphic3d_HTA_LEFT,
"theVAlign"_a = Graphic3d_VTA_BOTTOM, OCP_RETURN_COPY, OCP_NOGIL);
}

View File

@@ -0,0 +1,33 @@
/*
OCP.XSControl — the reader base STEP and IGES share.
Not imported by the app, but ReadFile/NbRootsForTransfer/TransferRoots/
OneShape are declared here, so binding them once means neither concrete
reader has to.
These keep the GIL, deliberately. The XSTEP layer traffics in
process-global Interface_Static state, the IGES reader is documented as not
thread-safe, and the app already serialises imports behind a lock — so
holding the GIL costs nothing and removes a whole class of question. It is
also what upstream does, which releases nowhere.
*/
#include "../common/occt_module.h"
#include "../common/occt_policies.h"
#include <TopoDS_Shape.hxx>
#include <XSControl_Reader.hxx>
void register_XSControl(nb::module_ &root) {
nb::module_ m = ocp_submodule(root, "XSControl");
nb::class_<XSControl_Reader>(m, "XSControl_Reader")
.def("ReadFile", &XSControl_Reader::ReadFile, "filename"_a)
.def("NbRootsForTransfer", &XSControl_Reader::NbRootsForTransfer)
.def(
"TransferRoots",
[](XSControl_Reader &self) { return self.TransferRoots(); })
.def("NbShapes", &XSControl_Reader::NbShapes)
.def("Shape", &XSControl_Reader::Shape, "num"_a = 1, OCP_RETURN_COPY)
.def("OneShape", &XSControl_Reader::OneShape, OCP_RETURN_COPY);
}

View File

@@ -92,6 +92,7 @@ void register_gp(nb::module_ &root) {
.def("Crossed", &gp_Vec::Crossed, "Right"_a, OCP_RETURN_COPY)
.def("Normalize", &gp_Vec::Normalize)
.def("Normalized", &gp_Vec::Normalized, OCP_RETURN_COPY)
.def("Reverse", &gp_Vec::Reverse)
.def("Reversed", &gp_Vec::Reversed, OCP_RETURN_COPY)
.def("Multiplied", &gp_Vec::Multiplied, "Scalar"_a, OCP_RETURN_COPY)
.def("Angle", &gp_Vec::Angle, "Other"_a)
@@ -146,6 +147,10 @@ void register_gp(nb::module_ &root) {
.def("SetScale", &gp_Trsf::SetScale, "P"_a, "S"_a)
.def("SetMirror", nb::overload_cast<const gp_Ax1 &>(&gp_Trsf::SetMirror),
"A1"_a)
.def("SetMirror", nb::overload_cast<const gp_Ax2 &>(&gp_Trsf::SetMirror),
"A2"_a)
.def("SetMirror", nb::overload_cast<const gp_Pnt &>(&gp_Trsf::SetMirror),
"P"_a)
.def("Form", &gp_Trsf::Form)
.def("ScaleFactor", &gp_Trsf::ScaleFactor)
.def("TranslationPart", &gp_Trsf::TranslationPart, OCP_RETURN_COPY)