6 Commits

Author SHA1 Message Date
9f86845c25 Add bulk UV sampler, TKHelix and MakeSphere (8.0.1.2)
- 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
2026-08-11 16:26:20 +02:00
153f2e4cca Bump to OCCT 8.0.1
Four mechanical binding edits; the bound Python surface is unchanged
(sigdiff compared the 7.9.3.1 dump against 8.0.1.1's in both directions
and found no class, member or constructor arity moved), so the app needs
no change.

- NCollection_Utf8String is gone; NCollection_String is the same UTF-8
  type. Python name kept, since the app imports it.
- Standard_Failure lost its Standard_Transient RTTI when it moved to
  deriving std::exception; ExceptionType() replaced DynamicType()->Name()
  and returns the same class names the exception dispatch keys on.
- FindKey gained a size_t overload beside the int one, so the plain
  member pointer is ambiguous; overload_cast picks the int form, which
  keeps the negative-index guard.
- StdPrs moved from TKService to TKV3d, so CMakeLists links both.

The byte-identity fixtures did NOT retire, contrary to the watchlist:
all six round-trip to the same digests under the new kernel, as do the
measurement, history and mesh-count blocks, so tests/data is untouched
and the app's content-addressed BREP payloads stay valid.

Gates: binding suite 83 passed, 138/138 symbols, sigdiff clean both
directions, ASAN clean, app suite 1798 passed / 1 skipped (unchanged),
sweep over 4486 parts with anchors_digest and every status unchanged,
-m perf 16% faster on the 32-feature chain.
2026-08-11 12:43:54 +02:00
01a5bcf188 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
2026-08-10 20:31:15 +02:00
0ce43a94aa 10C Inc 2: feature tail
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
2026-08-10 20:09:29 +02:00
2c2c4e4712 10C Inc 1: core modeling
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
2026-08-10 19:52:58 +02:00
6139852768 Phase 10A/10B Inc 0: build system, handle model, first module surface
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.
2026-08-10 16:10:28 +02:00