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

@@ -65,6 +65,15 @@ target_link_libraries(_OCP PRIVATE
TKOffset # BRepOffsetAPI
TKShHealing # ShapeFix, ShapeAnalysis, ShapeUpgrade
TKBool # BRepAlgo helpers pulled in by the above
# Inc 3
TKXSBase # XSControl, Interface, IFSelect
TKDESTEP # STEPControl
TKDEIGES # IGESControl, IGESData
TKDESTL # RWStl
# Inc 4 — TKService carries the BRep font/text builders. The builder image
# asserts it links without libGL/libX11, so this costs the runtime image
# nothing.
TKService
)
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
# cleanup rule collects only the dev ones. CMake asserts the prefix matches
# the OCCT it found.
version = "7.9.3.1.dev3"
version = "7.9.3.1.dev4"
description = "nanobind bindings for the OpenCASCADE geometry kernel (drop-in OCP)"
readme = "README.md"
requires-python = ">=3.12"

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)

234
tests/test_inc3_io.py Normal file
View File

@@ -0,0 +1,234 @@
"""Inc 3 + 4 gate: file I/O and the text tail.
Round-trips rather than digests: STEP and IGES payloads carry timestamps and a
generator banner, so byte comparison is meaningless — what has to survive is
the geometry, which these check by measuring the shape that comes back.
This file also settles open question **S5** from docs/design.md: whether the
wheel has to ship OCCT's share/ resources for the XSTEP layer to resolve units
when no CSF_* variable is set. These tests run in exactly that situation — a
container with the wheel and nothing else — so a pass means the answer is no.
"""
from __future__ import annotations
import os
import tempfile
import pytest
from OCP.BRepPrimAPI import BRepPrimAPI_MakeBox
from OCP.IFSelect import IFSelect_RetDone
from OCP.IGESControl import IGESControl_Controller, IGESControl_Reader, IGESControl_Writer
from OCP.Interface import Interface_Static
from OCP.STEPControl import (
STEPControl_AsIs,
STEPControl_Controller,
STEPControl_Reader,
STEPControl_Writer,
)
from OCP.TColStd import TColStd_SequenceOfAsciiString
from .test_inc1_modeling import assert_close, measure
BOX = (10.0, 20.0, 30.0)
@pytest.fixture(scope="module")
def initialized():
"""The app's own import bootstrap, and the S5 check in one.
If the controllers could not initialise without OCCT's resource files, this
is where it would fail — there is no CSF_* in this environment.
"""
assert not any(k.startswith("CSF_") for k in os.environ), (
"a CSF_* variable is set, so this run cannot answer S5"
)
assert STEPControl_Controller.Init_s()
assert IGESControl_Controller.Init_s()
assert Interface_Static.SetCVal_s("xstep.cascade.unit", "MM")
return True
def _box():
return BRepPrimAPI_MakeBox(*BOX).Shape()
def _same_geometry(got, expected_volume: float, what: str) -> None:
m = measure(got)
assert m["n_solids"] == 1, f"{what}: solid count"
assert m["n_faces"] == 6, f"{what}: face count"
assert_close(m["volume"], expected_volume, f"{what}: volume")
def test_step_round_trip(initialized, tmp_path):
"""Write a box to STEP and read it back with the geometry intact."""
path = tmp_path / "roundtrip.step"
writer = STEPControl_Writer()
assert writer.Transfer(_box(), STEPControl_AsIs) == IFSelect_RetDone
assert writer.Write(str(path)) == IFSelect_RetDone
assert path.stat().st_size > 0
reader = STEPControl_Reader()
assert reader.ReadFile(str(path)) == IFSelect_RetDone
assert reader.NbRootsForTransfer() >= 1
assert reader.TransferRoots() >= 1
_same_geometry(reader.OneShape(), BOX[0] * BOX[1] * BOX[2], "STEP round trip")
def test_step_read_stream_takes_a_python_file_like(initialized, tmp_path):
"""ReadStream is how the app imports an uploaded payload without touching
the filesystem. It goes through the same slurp bridge BinTools uses."""
import io
path = tmp_path / "stream.step"
writer = STEPControl_Writer()
writer.Transfer(_box(), STEPControl_AsIs)
writer.Write(str(path))
reader = STEPControl_Reader()
assert (
reader.ReadStream("stream.step", io.BytesIO(path.read_bytes()))
== IFSelect_RetDone
)
assert reader.TransferRoots() >= 1
_same_geometry(reader.OneShape(), BOX[0] * BOX[1] * BOX[2], "STEP stream")
def test_step_reports_its_file_units(initialized, tmp_path):
"""FileUnits fills three sequences by reference — the out-parameter shape
the app relies on to decide whether to rescale the transfer."""
path = tmp_path / "units.step"
writer = STEPControl_Writer()
writer.Transfer(_box(), STEPControl_AsIs)
writer.Write(str(path))
reader = STEPControl_Reader()
reader.ReadFile(str(path))
length, angle, solid_angle = (
TColStd_SequenceOfAsciiString(),
TColStd_SequenceOfAsciiString(),
TColStd_SequenceOfAsciiString(),
)
reader.FileUnits(length, angle, solid_angle)
assert length.Length() >= 1, "no length unit reported — S5 would be live"
names = [length.Value(i).ToCString() for i in range(1, length.Length() + 1)]
assert any("MM" in n.upper() or "MILLI" in n.upper() for n in names), names
def test_interface_static_round_trips_a_setting(initialized):
"""The unit override the IGES import brackets its read with."""
previous = Interface_Static.CVal_s("xstep.cascade.unit")
try:
assert Interface_Static.SetCVal_s("xstep.cascade.unit", "INCH")
assert Interface_Static.CVal_s("xstep.cascade.unit") == "INCH"
finally:
Interface_Static.SetCVal_s("xstep.cascade.unit", previous or "MM")
assert Interface_Static.CVal_s("xstep.cascade.unit") == (previous or "MM")
def test_iges_round_trip_and_unit_probe(initialized, tmp_path):
"""The IGES half, including the model→global-section→unit-name chain that
forces IGESData to be registered."""
path = tmp_path / "roundtrip.igs"
writer = IGESControl_Writer("MM", 0)
assert writer.AddShape(_box())
writer.ComputeModel()
assert writer.Write(str(path))
reader = IGESControl_Reader()
assert reader.ReadFile(str(path)) == IFSelect_RetDone
assert reader.NbRootsForTransfer() >= 1
section = reader.IGESModel().GlobalSection()
unit_name = section.UnitName()
assert unit_name is not None
assert "MM" in unit_name.ToCString().upper()
assert section.UnitValue() > 0
assert reader.TransferRoots() >= 1
shape = reader.OneShape()
assert not shape.IsNull()
# IGES carries surfaces, not solids, so only the face count is meaningful.
assert measure(shape)["n_faces"] == 6
def test_stl_read_returns_a_triangulation(tmp_path):
"""RWStl hands back a bare Poly_Triangulation; the app wraps it into a
shell itself, which is why nothing here builds a shape."""
from OCP.RWStl import RWStl
path = tmp_path / "cube.stl"
# A minimal ASCII STL: one triangle is enough to prove the reader.
path.write_text(
"solid t\n"
"facet normal 0 0 1\n outer loop\n"
" vertex 0 0 0\n vertex 1 0 0\n vertex 0 1 0\n"
" endloop\nendfacet\n"
"endsolid t\n"
)
triangulation = RWStl.ReadFile_s(str(path))
assert triangulation is not None
assert triangulation.NbTriangles() == 1
assert triangulation.NbNodes() == 3
# --------------------------------------------------------------------------
# Inc 4 — text
# --------------------------------------------------------------------------
def _a_font_path() -> str | None:
"""Any TrueType face on the box, since no font is committed here."""
roots = ("/usr/share/fonts", "/usr/local/share/fonts")
for root in roots:
for dirpath, _dirs, files in os.walk(root):
for name in sorted(files):
if name.lower().endswith((".ttf", ".otf")):
return os.path.join(dirpath, name)
return None
def test_text_builder_renders_glyphs_as_faces():
"""Text emboss builds its glyph outlines this way. The whole chain crosses
four modules: NCollection carries the strings, Graphic3d the alignment,
StdPrs does the work, gp positions the pen."""
from OCP.Graphic3d import Graphic3d_HTA_CENTER, Graphic3d_VTA_CENTER
from OCP.NCollection import NCollection_Utf8String
from OCP.StdPrs import StdPrs_BRepFont, StdPrs_BRepTextBuilder
from OCP.gp import gp_Ax3
from OCP.TopAbs import TopAbs_FACE
from .test_inc1_modeling import sub_shapes
path = _a_font_path()
if path is None:
pytest.skip("no TrueType font in the image to render with")
font = StdPrs_BRepFont()
assert font.Init(NCollection_Utf8String(path), 10.0, 0), path
builder = StdPrs_BRepTextBuilder()
shape = builder.Perform(
font,
NCollection_Utf8String("Ab"),
gp_Ax3(),
Graphic3d_HTA_CENTER,
Graphic3d_VTA_CENTER,
)
assert not shape.IsNull()
# Two glyphs, each at least one face; 'A' contributes a counter too.
assert len(sub_shapes(shape, TopAbs_FACE)) >= 2
def test_font_loading_reports_failure_rather_than_raising():
"""The app treats a bad font as a user error, so Init must return False."""
from OCP.NCollection import NCollection_Utf8String
from OCP.StdPrs import StdPrs_BRepFont
with tempfile.NamedTemporaryFile(suffix=".ttf") as bogus:
bogus.write(b"not a font")
bogus.flush()
font = StdPrs_BRepFont()
assert font.Init(NCollection_Utf8String(bogus.name), 10.0, 0) is False

View File

@@ -45,6 +45,11 @@ def _signatures(owner, name: str) -> list[tuple[str, ...]]:
doc = getattr(getattr(owner, name, None), "__doc__", None)
if not doc:
return []
# A class with no bound constructor still exposes object's __init__ stub,
# whose docstring carries no signature — that would otherwise read as a
# nullary overload the binding does not actually have.
if name == "__init__" and doc.startswith("Initialize self."):
return []
out = set()
for line in doc.splitlines():
line = re.sub(r"^\d+\.\s*", "", line.strip())