Migrate Replicad to OCCT v8 bindings - #263
Conversation
Rebuilt both WASM variants against OCCT V8 with -O3 optimization: - replicad_single: compact build without exception support (18.96 MB) - replicad_with_exceptions: full build with native WASM exceptions (22.22 MB) Also includes replicad source updates for OCCT V8 API compatibility. Made-with: Cursor
…ollection equivalents
orts and add multi-threaded build
|
|
||
| The generated bindings now make the trailing `Message_ProgressRange` optional for `TransferRoots` and most `Build` and `Perform` methods. Omitting it materializes OCCT's default progress range internally, so this migration removes 15 manual progress-range allocations. | ||
|
|
||
| Two writer APIs still require an explicit range: `STEPControl_Writer.Transfer(..., theProgress)` and `STEPCAFControl_Writer.Perform(..., theProgress)`. Replicad retains progress-range allocations at exactly those two callsites. |
There was a problem hiding this comment.
🗒 note (non-blocking): Calling this out as a PR comment since the ProgressRange removal features significantly through the PR. Providing a ProgressRange was pure ceremony at many callsites and now OpenCascade.js handles such optional params internally, so it can be safely omitted.
| .map(([a, b, c]) => draw(a).lineTo(b).lineTo(c).close()); | ||
| const drawings = triangles.map(([a, b, c]) => | ||
| draw(a).lineTo(b).lineTo(c).close() | ||
| ); |
There was a problem hiding this comment.
🗒 note (non-blocking): I'm not sure why this test used randomized values earlier, but it makes the generated test snapshot non-deterministic. To make the snapshot generation deterministic, which it should be as a test fixture, I've removed the randomization here.
| const { Curve1, Curve2 } = intersector.Segment(i); | ||
| Curve2.delete(); | ||
| yield new Curve2D(Curve1); |
There was a problem hiding this comment.
🗒 note (non-blocking): In my testing I couldn't reproduce this failure mode, and the repo has no regression tests for it, but I'm leaving this block inside the try/catch as a precaution.
If this previous commentary sounds familiar, it would be great to add a regression to verify if the try/catch is still needed, if the bug is resolved in OCCT 8 then the try/catch could be removed.
There was a problem hiding this comment.
🗒 note (non-blocking): This tiny shim maintains CJS compatibility alongside the new ESM whilst avoiding the need for a heavier vite-config approach. A similar shim can be seen below for the single threaded build.
| ctool.SetColor( | ||
| shapeNode, | ||
| wrapColor(color || "#f00", alpha ?? 1), | ||
| // @ts-expect-error the type system does not work for these |
There was a problem hiding this comment.
🗒 note (non-blocking): Enums have correct typings in the new OpenCascade.js 🎉
| writer.SetLayerMode(true); | ||
| writer.SetNameMode(true); | ||
| oc.Interface_Static.SetIVal("write.surfacecurve.mode", true); | ||
| oc.Interface_Static.SetIVal("write.surfacecurve.mode", 1); |
There was a problem hiding this comment.
🗒 note (non-blocking): This API changed from a boolean to a number from OCCT-7 -> OCCT-8
| [xMin.current, yMin.current], | ||
| [xMax.current, yMax.current], | ||
| [this.wrapped.GetXMin(), this.wrapped.GetYMin()], | ||
| [this.wrapped.GetXMax(), this.wrapped.GetYMax()], |
There was a problem hiding this comment.
🗒 note (non-blocking): Pass-by-reference APIs, which are non-idiomatic JS, are now resolved via functions in the new OpenCascade.js.
| ); | ||
| } | ||
| return curve; | ||
| gc(); |
There was a problem hiding this comment.
🗒 note (non-blocking): It's necessary to garbage-collect after accessing the X + Y points now, since those values are obtained from memory with the new OpenCascade.js dynamic pointer management approach. Early garbage-collection would clear their values before they could be accessed.
| smoothing[2], | ||
| degMax, | ||
| oc.GeomAbs_Shape.GeomAbs_C2, | ||
| tolerance |
There was a problem hiding this comment.
🗒 note (non-blocking): in OCCT-8 splines are now initialized separate to their constructors.
| this.wrapped, | ||
| true | ||
| true, | ||
| false |
There was a problem hiding this comment.
🗒 note (non-blocking): OCCT-8 exposes a fourth copyMesh option on this binding, the old binding exposed on the three-argument form. Passing false preserves the historical behavior of copying geometry without copying an existing triangulation.
| ]; | ||
| cornerMin.delete(); | ||
| cornerMax.delete(); | ||
| return result; |
There was a problem hiding this comment.
🗒 note (non-blocking): As before, pass-by-reference idioms have been replaced by functional JS getters in the new OpenCascade.js
|
|
||
| const poles = convertToJSArray(baseSurface.Poles_2()); | ||
| const transform = new EllpsoidTransform(aLength, bLength, cLength); | ||
|
|
||
| poles.forEach((columns, r) => { | ||
| columns.forEach((value, c) => { | ||
| const newPoint = transform.applyToPoint(value); | ||
| baseSurface.SetPole_1(r + 1, c + 1, newPoint); | ||
| }); | ||
| }); | ||
| for (let u = 1; u <= baseSurface.NbUPoles(); u++) { | ||
| for (let v = 1; v <= baseSurface.NbVPoles(); v++) { | ||
| const newPoint = transform.applyToPoint(baseSurface.Pole(u, v)); | ||
| baseSurface.SetPole(u, v, newPoint); | ||
| } | ||
| } | ||
| const shell = cast( | ||
| r(new oc.BRepBuilderAPI_MakeShell_2(baseSurface.UReversed(), false)).Shell() | ||
| r(new oc.BRepBuilderAPI_MakeShell(baseSurface.UReversed(), false)).Shell() |
There was a problem hiding this comment.
🗒 note (non-blocking): The previously used convertToJSArray function is replaced here with a simpler iterator.
| const hash = item.HashCode(HASH_CODE_MAX); | ||
| if (!hashes.get(hash)) { | ||
| hashes.set(hash, true); | ||
| const isDuplicate = seen.some((s) => s.IsSame(item)); |
There was a problem hiding this comment.
🗒 note (non-blocking): .IsSame() is the native OCCT predicate for shape equality and provides stronger matching guarantees with drastically less chance of collision (32-bit hash-code versus 64-bit OCCT matching).
|
|
||
| const filteredFaces = filter.find(this); | ||
| const facesToRemove = r(new this.oc.TopTools_ListOfShape_1()); | ||
| const facesToRemove = r(new this.oc.NCollection_List_TopoDS_Shape()); |
There was a problem hiding this comment.
🗒 note (non-blocking): NCollection is the new list primitive in OCCT-8
There was a problem hiding this comment.
🗒 note (non-blocking): TopoDS lackages a CompSolid cast so we need to add a Cpp helper to downcast this shape type. This link points to the cast consumer in replicad within this PR.
| ); | ||
|
|
||
| if (triangulation.IsNull()) return null; | ||
| if (!triangulation || triangulation.isNull()) return null; |
There was a problem hiding this comment.
🗒 note (non-blocking): Due to OpenCascade.js now transparently dereferencing pointers, we now need to ensure both the handle and the pointee are not null.
|
@sgenoud this PR is ready for review 🎉 The last close-out item I need to do before it's merge-ready is move from |
Summary
replicad-opencascadejswith a default single-threaded entry point and a pthread-enabled./multientry point; both expose the same API and use native WebAssembly exceptions.Native mesh extraction
The face and edge extractors implement Replicad's rendering contracts, so they remain in this repository. They replace per-face, per-node, and per-edge JavaScript traversal with one packed-buffer WASM call per extraction.
The face extractor clears cached triangulations before remeshing at the requested tolerance, preserves face locations, triangle winding, normal orientation, and bounded face hashes, and returns packed vertex/normal/triangle/group arrays. The edge extractor reuses face triangulation polygons when available, applies face locations, deduplicates shared edges by exact OCCT shape identity, tessellates free edges with the historical tolerance ordering, and returns packed line/group arrays.
The direct A/B builds the Birdhouse and Vase once, then measures the historical JavaScript face and edge implementations directly against the generated OpenCascade.js bindings and the native extractors. Model construction is not timed and no package dependency was added. With 3 warmups and 100 measured iterations:
Parity checks cover the complete outputs rather than only their sizes. For both models, face vertices agree within
1e-4, triangle topology and face group start/count/hash values agree, and both normal buffers contain one unit normal per vertex. Normal components are deliberately not compared for equality: the native extractor uses analytic OCCT surface normals, while the historical JavaScript path used mesh-derivedPoly_Triangulation.ComputeNormals(). Edge traversal order is not contractual; after normalizing traversal and segment direction, every bounded edge hash, group count, and segment geometry agrees within2e-3(the packed native line buffer isfloat32, while the JavaScript baseline retains doubles).Full Birdhouse + Vase face + edge benchmark script used for the reported A/B measurements
Face meshing deliberately cleans and remeshes so a coarser request cannot be served by an older finer cached mesh. Edge-only extraction first checks whether a suitable triangulation already exists and avoids an unnecessary second mesh pass.
ReplicadShapeHasher.HashCode(shape, upperBound)preserves Replicad's historical public contract: orientation-independent labels in[1, upperBound]. Internal shared-edge deduplication does not use that bounded label; it usesNCollection_Map<TopoDS_Shape, TopTools_ShapeMapHasher>so hash collisions cannot merge distinct edges.Generated artifacts
Both checked-in modules were regenerated and validated from these pinned canary inputs, which contain the required OCCT 8.0.1 build:
ghcr.io/taucad/opencascade.js:canary-ebd263f1-single-threaded—sha256:215198af0e2ca4c5f308e5540869f2419784dc290062d3eb03d34e4f22e0188cghcr.io/taucad/opencascade.js:canary-ebd263f1-multi-threaded—sha256:5cb67064edc903ae50c254e32417da9662fa88ec2e734386c28a3806949862ceBuild manifests and provenance reports are intentionally ignored rather than published or tracked; the pinned image coordinates, build source/configuration, JS glue, declarations, symbols, and WASM remain committed. Adoption of a later stable 3.0.0 image is a separate follow-up.
Review cleanup
FinalizationRegistrylifecycle.Verification
pnpm install --frozen-lockfilepnpm --filter replicad typecheckpnpm --filter replicad buildnpm pack --dry-run --jsonforreplicad-opencascadejsandreplicad; no diagnostic sidecars includedgit diff --checkcanaryimages to3.0.0release imagesStudio/app builds retain their existing dependency, duplicate-WASM-emission, CSS nesting, Browserslist, and chunk-size warnings; they complete successfully.
AI Disclosure