- n3xd_ocp.sample.face_grid: a face's UV grid of points and outward
normals in one GIL-free call, sampling exactly where np.linspace does
so a caller's fitted surface does not move.
- n3xd_ocp.helix: OCCT 8.0's TKHelix, which upstream does not bind at
all. Takes Python lists rather than NCollection_Array1. Two things the
header does not say, both found by probing: SetParameters wants N+1
diameters for N segments (one per boundary, so a taper interpolates),
and the builder is right-hand only -- a negative pitch is error status
12, not a mirrored helix.
- Bind BRepPrimAPI_MakeSphere and give inventory.py an EXTRA_SYMBOLS
addendum for symbols no app source imports. assay's gen_flow_fixtures
has been unrunnable since the 10C cutover for want of it; the gap was
wider than --check, since sigdiff is inventory-driven too.
Gates: 105 tests, 139/139 symbols, sigdiff clean, ASAN clean, wheel
self-contained with no libGL/libX11 DT_NEEDED.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HbTQ2HYWQwdtGmGJwypt6Z
cad/tessellation.py walks every node and every triangle of every face in
Python — three interpreter loops per face — to turn OCCT's mesh into flat
arrays the kernel already holds. extract_meshes hands them over in one call
with the GIL released; extract_edge_polylines does the same for the per-edge
GCPnts discretisation behind the viewport's edge overlay.
The output reproduces the Python loops exactly, including the parts that look
like quirks: unmeshed faces are omitted so face_index keeps its gaps (it is the
app-wide face identity, not a list position), triangles are rewound and normals
negated for reversed faces, and nodes come back float64 — the binary transport
narrows to float32 on its way out but the JSON one does not, so narrowing here
would quietly change what the REST payload says.
Plain dicts of arrays rather than a bound class, because the cad pool sends
meshes across a pipe and dicts of ndarrays pickle natively. There is a test for
that, and one asserting the arrays outlive the shape.
The equality tests re-implement the Python loops in full and compare with
array_equal, not approx: a difference here moves what the viewport draws or
what a face pick resolves to.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DfriM8XUkn7uYf5Dwe2xo6
BRepGProp.SurfaceProperties is the hottest kernel call the app makes — 94 % of
face_candidate_anchors, 0.99 s of 1.05 s for 690 faces — not because the kernel
is slow but because it is reached once per face from Python, so a rebuild pays
the round-trip hundreds of times per feature over a growing shape.
face_surface_props(shape, *, parallel=True, eps=None) runs the whole scan
C++-side with the GIL released, optionally over OSD_Parallel, and returns
(areas[F], centroids[F,3]) in MapShapes(FACE) order — the face identity the
topology layer already keys on, so a caller indexes straight into it. A face
OCCT cannot integrate reports zeros, matching what the app's own try/except
substitutes.
Tested against the same stock-recorded per-face reference the Inc 1 gate uses,
and against the one-call-per-face loop it replaces. Parallel and serial are
compared with array_equal rather than approx: the parallel path shares one
TopoDS_Shape across threads, so an exact match is the evidence that nothing
reachable from it gets mutated while measuring.
Backend adoption comes after the cutover, so this changes nothing yet.
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.