Commit Graph

17 Commits

Author SHA1 Message Date
c006ef1665 Add NOTEPAD with the leftovers from the 8.0.1 bump
- the bound surface is inventoried from the app alone, but assay is a
  consumer too — which is why BRepPrimAPI_MakeSphere is unbound and
  assay's fixture generator has been unrunnable since the cutover.
- the GIL rationale for STEP/IGES predates 8.0's thread-safety
  contract and should cite why that contract does not apply to us.
- TKHelix is compiled by the builder image but not in the link list.
2026-08-11 13:21:32 +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
fab8e93a4b parity_venv.sh follows the app to 3.13; the abi3 tag stays at cp312
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
2026-08-11 07:24:08 +02:00
6b8a79ba36 Release 7.9.3.1
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
2026-08-10 23:17:55 +02:00
a0b7e43985 README: coverage complete, and the speedup the plan did not expect
138 symbols, 53 modules, published as 7.9.3.1.dev5. The app's full suite passes
against it and the whole project store sweeps identical.

Corrects the "call overhead is not a bottleneck" claim: it is true of any single
call and false of the aggregate. The app's benchmark suite runs 194 s -> 73 s
with nothing but the wheel swapped, and the win grows with face count — 3.3x at
4 features, 7.0x at 32.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DfriM8XUkn7uYf5Dwe2xo6
2026-08-10 21:03:41 +02:00
c5f3248fbc tools/bench_ext.py: measure the bulk APIs against the loops they replace
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
2026-08-10 20:48:45 +02:00
8255fb949c n3xd_ocp.tess: a shape's triangulation without the per-node round trip
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
2026-08-10 20:40:25 +02:00
4bb4805ccb n3xd_ocp.measure: every face's area and centroid in one call
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
2026-08-10 20:37:01 +02:00
6b419c1c81 design.md: record what the increments settled
- GIL policy gains the XSTEP exception (STEP/IGES hold it; RWStl releases).
- A new rule, learned three times the hard way: an unregistered type cannot be
  a default argument, because nanobind converts defaults at binding time and
  the failure is a bare std::bad_cast at import with no file or line.
- A section on matching upstream: what sigdiff exists for, why static analysis
  cannot see instance methods, and why the app's suite cannot gate a single
  increment.
- An OCCT 8.0 watchlist, so the 10E bump is a known quantity.
- S5 marked settled: no CSF_* resources needed in the wheel.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DfriM8XUkn7uYf5Dwe2xo6
2026-08-10 20:32:39 +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
340315b6e9 parity_venv.sh: swap the environment it claims to swap
The script exported UV_PROJECT_ENVIRONMENT and then reached for `uv pip`, which
does not honour it — it discovers an environment the way pip does, so the
uninstall and install landed in the app's own .venv. The sanity check that
follows could not notice, because `uv run` *does* honour the variable and so
read the untouched parity venv. Net effect: the swap happened in the wrong
place and reported success. Inc 0's parity claim never exercised the parity
venv at all.

Two fixes, since the second only surfaced once the first was in:

- `uv pip` gets --python pointing at the parity venv.
- everything afterwards runs that venv's interpreter directly. `uv run` re-syncs
  the environment against the app's manifest before running, and the manifest
  still asks for cadquery-ocp-novtk, so it reinstalled the stock wheel on top of
  the swap. The usage note at the top said to drive the app's tests that way
  too; it now says to use the venv's python.

Also pins `uv sync --python 3.12`, so the parity venv keeps resembling what the
image ships instead of following whatever interpreter is newest on the box, and
turns the sanity print into an assertion on __occt_version__ — an attribute
only our wheel defines, which makes it a check that the swap landed.

Verified end to end against the published 7.9.3.1.dev2: installs anonymously
from the Gitea index, 31 modules, ocp suite 48 passed, and the app's main venv
is left on stock.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DfriM8XUkn7uYf5Dwe2xo6
2026-08-10 19:59:18 +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
091a2ad0cf Housekeeping ahead of the Inc 1-4 coverage work
- inventory.py --methods: report the instance methods the app calls per class,
  plus chained calls that constrain a return type. --check says which classes
  to bind; this says what to bind on them, which is what writing ~38 module
  TUs needs.
- Drop StlAPI from the inventory: StlAPI_Writer has no app call site (the only
  use was a test fixture, now on the app's own STL writer). 138 symbols / 47
  modules.
- adding-symbols.md: scope the executing-constructor ban to the BRepAlgoAPI
  booleans, which are the only classes with a deferred Set*/Build form —
  BRepMesh_IncrementalMesh, GeomAPI_*, BRepCheck_Analyzer and friends compute
  in their constructor by design and bind as stock. Replace the per-increment
  app-test guidance: backend/tests/conftest.py imports n3xd.main, so no app
  test can collect until the last module is bound. Increments gate on
  stock-recorded fixtures here; the app suite is the Inc 4 gate.
- parity_venv.sh: run the ocp suite in the swapped venv (it imports only
  OCP/n3xd_ocp, so it works throughout).
- Fix a stale macro name in occt_handle.h (ocp_new, not OCP_TRANSIENT_NEW) and
  drop the unused ocp_transient_class helper.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DfriM8XUkn7uYf5Dwe2xo6
2026-08-10 19:23:27 +02:00
bfeb089542 up
Signed-off-by: stroblme <stroblme@posteo.de>
2026-08-10 17:18:21 +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
b757d6e8d6 Scaffold the n3xd-ocp binding repo
Placeholder for the hand-written nanobind bindings over OCCT (roadmap Phase 10
in the app repo). Carries the gitignore for a scikit-build-core/CMake
extension plus the OCCT build trees, and keeps .secrets — the Gitea publish
PAT — out of history.
2026-08-10 11:58:34 +02:00