Skip to content

feat: wasm conversion engine — STEP write, OCCT isolation, IFC serialize (pyodide 0.29.4)#29

Merged
Krande merged 2 commits into
mainfrom
feat/wasm-step-write
Jun 15, 2026
Merged

feat: wasm conversion engine — STEP write, OCCT isolation, IFC serialize (pyodide 0.29.4)#29
Krande merged 2 commits into
mainfrom
feat/wasm-step-write

Conversation

@Krande

@Krande Krande commented Jun 15, 2026

Copy link
Copy Markdown
Owner

Enables adacpp's full conversion surface under pyodide/WASM, coexisting with the upstream ifcopenshell wheel in one pyodide runtime. Three layers:

1. STEP write under wasm (pyodide 0.29.4 / native wasm-EH)

backend.write_step (STEPCAFControl_Writer) aborted the module (RuntimeError: unreachable). Root causes + fixes:

  • step_writer.cpp/helpers.cpp were missing from the BUILD_WASM source list → write_shapes_to_step was an unresolved env import that trapped. Added them.
  • OCAF doc used "XmlOcaf" (driver not registered under OCCT_NO_PLUGINS) → switched to BinXCAFDrivers::DefineFormat + "BinXCAF".
  • Toolchain migrated to pyodide 0.29.4 / emscripten 4.0.9 / Python 3.13 / native wasm-EH (-fwasm-exceptions, drop OCC_CONVERT_SIGNALS, cp313/pyodide_2025_0 tags, dynamic xbuildenv path).

2. OCCT isolation — coexist with ifcopenshell (the key fix)

adacpp and the ifcopenshell wasm wheel both statically link OCCT. With default visibility the emscripten linker interposes OCCT symbols across the two copies, corrupting OCCT's Standard_Type RTTI registry (GeomAdaptor_Curve::BSpline throws once ifcopenshell is loaded — breaking every adacpp OCCT op in that session).

Fix: build OCCT with -fvisibility=hidden. Hidden symbols can't be interposed, so wasm-ld binds adacpp's OCCT refs directly (no GOT imports; adacpp OCCT exports 7154 → 0). OCCT becomes private + entirely intra-module, dodging both the RTTI interposition and emscripten's unreliable cross-module virtual dispatch (#17907) that also defeats a shared-OCCT side module. Dropped -sEXPORT_ALL=1 (it would re-export the hidden OCCT). This keeps the battle-tested "static OCCT in one module" shape (as OCP.wasm/CadQuery do).

3. IFC serialize + exception translation

  • serialize_brep CadBackend verb (BRepTools_ShapeSet text) so adapy's IFC tessellation fallback works without ifcopenshell.geom.occ_utils (absent from the wasm wheel) — unblocks sat/step → ifc. A BREP string crosses the module boundary, so the two private OCCT copies never interpose.
  • nanobind exception translator for OCCT Standard_Failure (which derives from Standard_Transient, not std::exception) → clean Python RuntimeError instead of an untranslatable abort.

Verification (node-pyodide, all co-loaded with ifcopenshell in one instance)

sat/step/mesh → {glb,obj,stl,step,xml}, sat → ifc, ifc → {glb,obj,stl,step,xml,ifc}, FEM bake — all ✅. adacpp standalone unaffected.

CI note

Touches cmake/wasm_occt.cmake → merging triggers publish-occt-wasm-base to rebuild the OCCT base image with -fvisibility=hidden. Downstream wheel/ci-wasm-tests must consume the new base; if they run before the rebuild finishes they'll need a re-run (the fallback builds OCCT from source, which is correct).

🤖 Generated with Claude Code

backend.write_step (STEPCAFControl_Writer via OCCT XCAF) fatally aborted
the pyodide module with `RuntimeError: unreachable`. Two root causes:

1. step_writer.cpp / helpers.cpp were never compiled into the wasm side
   module — the BUILD_WASM source list still carried the stale "OCCT not
   available in wasm" set. write_shapes_to_step was therefore emitted as
   an unresolved `env` import whose function-table slot is a trap stub,
   so the first call trapped. GLB / STEP-read / make_box worked only
   because they are self-contained in cad_py_wrap.cpp.
2. The OCAF document used "XmlOcaf", whose driver isn't registered under
   OCCT_NO_PLUGINS (TKXml* isn't even linked for wasm). Switched to
   BinXCAFDrivers::DefineFormat + a "BinXCAF" document (TKBinXCAF is
   linked). write_shapes_to_step now also translates Standard_Failure
   into a std::runtime_error so nanobind surfaces a Python exception
   instead of std::terminate.

Requires the wasm toolchain to move to pyodide 0.29.4 / emscripten 4.0.9
/ Python 3.13 with native WebAssembly exception handling (the old
-fexceptions JS-trampoline model trapped in STEPCAFControl_Writer):
- OCCT + adacpp + nanobind all compile with -fwasm-exceptions
  (+ -sSUPPORT_LONGJMP=wasm); OCC_CONVERT_SIGNALS dropped on wasm
  (setjmp inside a catch is invalid under wasm-EH → "br_table: label
  arity inconsistent" CompileError).
- wheel tag cp313 / pyodide_2025_0_wasm32; Python_SOABI cpython-313.
- xbuildenv path resolved dynamically via `pyodide config get` instead
  of a machine-specific content hash (CI-portable).
- native test env + linux preset bumped to python 3.13 to match.

Verified: SAT -> {glb,obj,stl,xml,step} and FEM bake all succeed under
pyodide (node-pyodide harness); native suite 62 passed including
test_cad_write_step / test_basic_write_step.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…n + IFC BRep serialize)

The viewer's wasm engine loads adacpp AND the upstream ifcopenshell wheel in
one pyodide instance. Both statically link OCCT; with default visibility the
emscripten dynamic linker interposes OCCT symbols across the two copies,
corrupting OCCT's Standard_Type RTTI registry (a Geom_Line is misread and
GeomAdaptor_Curve::BSpline() throws Standard_NoSuchObject — every adacpp OCCT
op fails once ifcopenshell is loaded).

Fixes:

* Build OCCT with -fvisibility=hidden (cmake/wasm_occt.cmake). Hidden OCCT
  symbols can't be interposed, so wasm-ld binds adacpp's OCCT references
  DIRECTLY (no GOT imports) — OCCT becomes private + entirely intra-module.
  This dodges BOTH the RTTI interposition AND emscripten's unreliable
  cross-module virtual dispatch (#17907) that also defeats a shared-OCCT
  side module. adacpp drops -sEXPORT_ALL=1 (it would re-export the hidden
  OCCT and undo the isolation); --no-gc-sections keeps OCCT funcs defined
  for intra-module vtable resolution. Adacpp OCCT exports: 7154 -> 0.

* serialize_brep CadBackend verb (BRepTools_ShapeSet text). adapy's IFC
  tessellation fallback serialized shapes via ifcopenshell.geom.occ_utils,
  which the ifcopenshell wasm wheel doesn't ship; routing through adacpp
  emits the same BREP string for ifcopenshell.geom.serialise. Unblocks
  sat/step -> ifc. The string crosses the module boundary as plain text,
  so the two private OCCT copies still never interpose.

* Register a nanobind exception translator for OCCT Standard_Failure
  (Standard_Transient, not std::exception) so OCCT errors surface as a
  Python RuntimeError instead of an untranslatable SystemError/abort.

Verified (node-pyodide, all co-loaded with ifcopenshell): sat/step/mesh ->
{glb,obj,stl,step,xml}, sat -> ifc, ifc -> {glb,obj,stl,step,xml,ifc}, FEM
bake — all pass; adacpp standalone unaffected.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@Krande Krande changed the title feat: STEP write under wasm (pyodide 0.29.4 / native wasm-EH) feat: wasm conversion engine — STEP write, OCCT isolation, IFC serialize (pyodide 0.29.4) Jun 15, 2026
@github-actions

Copy link
Copy Markdown

👋 Hi there! I have checked your PR and found no issues. Thanks for your contribution!

PR Review:

I found no pr-related issues.

  • ✅ PR title is ok
  • ✅ Release label is ok
  • ✅ SOURCE_KEY is set as a secret
  • ✅ Calculated next version: "0.9.0"

Python Review:

I found no python-related issues.

Python Linting results:

  • ✅ Isort
  • ✅ Black
  • ✅ Ruff

Python Packaging results:

@Krande Krande merged commit d7948e7 into main Jun 15, 2026
41 of 45 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant