The wheel is cp312-abi3, so it loads on 3.13 unchanged — the pin that has to
move is the interpreter the parity venv is built with, since its whole point is
resembling what the image ships.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CDYRXGB8tW4NE7b91g7Kdb
The first non-dev version, published once every cutover gate was green: the
app's full suite, a whole-project-store geometry sweep, and the perf
benchmarks. Gitea never allows republishing a version, so this waited for the
exact commit the app now pins.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DfriM8XUkn7uYf5Dwe2xo6
Keeps the numbers next to the code rather than in a commit message, since the
adoption decisions turn on them. Two results are not what the plan assumed:
- face_surface_props gains *nothing* from leaving Python — the serial C++ loop
costs exactly what the Python loop costs. That agrees with the app's own
profile, which found SurfaceProperties to be 94 % of face_candidate_anchors:
the time is inside the kernel, so the interpreter round trip was never the
problem. The whole 10.9x is OSD_Parallel, reachable only because the GIL is
released.
- BinTools is already fast enough. Dropping the BytesIO detour saves nothing
measurable, so the pools' overhead is not in serialisation — worth knowing
before rewriting seventeen call sites for it.
Mesh extraction is the clear win at 35x, where the Python loop overhead really
does dominate; edge polylines 2.6x.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DfriM8XUkn7uYf5Dwe2xo6
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
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
Binds the modeling core the app builds every feature out of — 93 of its 138
symbols now resolve, up from 34. New modules: GeomAbs, Geom2d, Geom,
TCollection, TColgp, TColStd, GProp, BRepGProp, Bnd, BRepBndLib, Adaptor3d,
BRepAdaptor, GeomLProp, GeomAPI, BRepBuilderAPI, BOPAlgo, BRepAlgoAPI,
BRepPrimAPI, GC, BRepMesh; gp and BRep_Tool completed.
Three structural decisions:
- BRepBuilderAPI_MakeShape carries Build/Shape/Generated/Modified/IsDeleted for
every maker in the binding, so the booleans, the primitives and (later) the
fillet builders all answer the app's duck-typed provenance layer through
ordinary virtual dispatch. History lists come back copied, so they outlive
the builder.
- The executing two-argument BRepAlgoAPI constructors stay unbound; operands go
in through SetArguments/SetTools. Section keeps Init1/Init2, which are plain
setters. BOPAlgo moved up from Inc 2 — SetGlue needs its enum.
- Adaptor3d is registered although the app never imports it: every method it
calls on BRepAdaptor_Curve/Surface is a virtual declared there, so binding
them once on the bases leaves mod_BRepAdaptor.cpp with just constructors.
Gate: tests/test_inc1_modeling.py against reference values gen_fixtures.py now
records from the stock wheel — measurements, per-face area/centroid in map
order, mesh counts, and the boolean history map compared exactly, since that is
the substrate the app's topological naming is built on.
tools/sigdiff.py compares our bound surface against stock's, because a wrong
nb::init<> is silent: MakePrism's five-argument form bound OCCT's semi-infinite
gp_Dir overload (gp_Dir converts from gp_Vec), producing a valid solid of the
wrong shape with the flags shifted along. The fixture digest caught it; sigdiff
finds the class of bug directly, and now reports only one deliberate deviation.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DfriM8XUkn7uYf5Dwe2xo6
Builds n3xd-ocp end to end and publishes 7.9.3.1.dev1 to the Gitea registry,
where it installs anonymously and passes its suite.
- occt/Dockerfile: OCCT 7.9.3 compiled once into a manylinux_2_28 builder
image (base digest + tarball sha256 pinned), Draw/VTK/Tk/Xlib/OpenGL off,
FreeType on, -O2 without fast-math or march=native. A final layer asserts
TKService/TKV3d exist with no libGL/libX11 DT_NEEDED, which is what lets the
app image drop libgl1/libx11-6. Mounted into, never built FROM.
- scikit-build-core + nanobind STABLE_ABI -> one cp312-abi3 extension that
registers every OCP.* submodule via PyImport_AddModule, so `import
OCP.TopoDS` needs no shim and cls.__module__ is right. Version <occt>.N is
asserted against the OCCT found, keeping occt_version() truthful.
- occt_handle.h: type caster for opencascade::handle<T> over OCCT's intrusive
refcount. Wrappers are non-owning instances holding exactly one handle in
their keep-alive list, reusing an existing wrapper so identity survives a
round trip. Transient constructors go through ocp_new (never nb::init<>,
which would let OCCT delete nanobind's storage); the caster refuses a
refcount-0 object rather than corrupt the heap. Verified under ASAN with no
memory-safety errors, plus an RSS bound over 50k create/destroy cycles.
- Sub-shapes are returned by value everywhere, making the TShape lifetime class
that segfaulted a process-global face memo unrepresentable.
- Standard_Failure derives RuntimeError, with ~20 concrete types dispatched on
the dynamic OCCT type (cad_pool marshals failures home by type name).
- Inc 0 surface: gp subset, TopAbs, TopoDS (+ downcasts), TopExp, TopLoc,
TopTools, BRep, BinTools, Poly, Standard. 34 of the app's 139 symbols.
- n3xd_ocp: additive APIs kept out of the OCP namespace so parity testing stays
meaningful. bintools (shape <-> bytes, GIL-free, byte-identical) and _debug.
Two findings worth the record, both verified against the stock wheel rather
than assumed: upstream binds __hash__ but leaves __eq__ at identity, which is
exactly what geom_memo.py's hash-bucket + IsSame scan is built around, so we
match it instead of "fixing" it; and BinTools can release the GIL after all, by
slurping the file-like object instead of bridging a streambuf that would call
back into Python.
Gate: BREP round-trips are byte-identical to cadquery-ocp-novtk across six
fixtures (the generator asserts stock idempotency first). That matters beyond
IPC — derive.py content-addresses BREP payloads by sha256 and stores the ref.