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:
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
26
src/core.cpp
26
src/core.cpp
@@ -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();
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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>(
|
||||
|
||||
@@ -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)
|
||||
|
||||
31
src/modules/mod_Graphic3d.cpp
Normal file
31
src/modules/mod_Graphic3d.cpp
Normal 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();
|
||||
}
|
||||
23
src/modules/mod_IFSelect.cpp
Normal file
23
src/modules/mod_IFSelect.cpp
Normal 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();
|
||||
}
|
||||
57
src/modules/mod_IGESControl.cpp
Normal file
57
src/modules/mod_IGESControl.cpp
Normal 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);
|
||||
}
|
||||
38
src/modules/mod_IGESData.cpp
Normal file
38
src/modules/mod_IGESData.cpp
Normal 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);
|
||||
}
|
||||
43
src/modules/mod_Interface.cpp
Normal file
43
src/modules/mod_Interface.cpp
Normal 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);
|
||||
}
|
||||
32
src/modules/mod_NCollection.cpp
Normal file
32
src/modules/mod_NCollection.cpp
Normal 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
27
src/modules/mod_RWStl.cpp
Normal 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);
|
||||
}
|
||||
87
src/modules/mod_STEPControl.cpp
Normal file
87
src/modules/mod_STEPControl.cpp
Normal 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);
|
||||
}
|
||||
57
src/modules/mod_StdPrs.cpp
Normal file
57
src/modules/mod_StdPrs.cpp
Normal 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);
|
||||
}
|
||||
33
src/modules/mod_XSControl.cpp
Normal file
33
src/modules/mod_XSControl.cpp
Normal 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);
|
||||
}
|
||||
@@ -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
234
tests/test_inc3_io.py
Normal 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
|
||||
@@ -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())
|
||||
|
||||
Reference in New Issue
Block a user