# Package URL: /docs/package Install `@taucad/opencascade.js@beta`, call `init()`, build shapes with the full OCCT surface, export STEP or GLB, and integrate into your bundler of choice. Install the package, render a box, export STEP in 4 minutes. Build a filleted box and render it in three.js end-to-end. All 4,587 bound classes — searchable, fragment-stable, LLM-ingestible. Fork status, maintenance model, and how to contribute. ## What's in V3 [#whats-in-v3] * ESM-only single-file build — one wasm, one `init()` Promise, no CommonJS fallback. * Suffix-free symbol generation — `gp_Pnt` (not `gp_Pnt_3`); overloads dispatched in C++. * `using` syntax for every disposable shape — RBV containers integrate `Symbol.dispose`. * Re-architected exception channel — catch JS errors instead of opaque `WebAssembly.Exception`. See `CHANGELOG.md` in the repo for the full release notes. --- # Toolchain URL: /docs/toolchain Pull the multi-arch GHCR image, link a custom YAML, trim symbols to the surface you need, extend with your own C++ via `additionalCppCode`, and ship reproducible CI builds with provenance and SBOM. Pull a GHCR image, link a custom YAML, build a trimmed wasm in 2 minutes. Reduce wasm size by binding only the OCCT classes your app uses. Add custom C++ via additionalCppCode, additionalCppFiles, and additionalBindCode. Reference for the custom build YAML schema. ## Two channels of configuration [#two-channels-of-configuration] Compile-time `OCJS_*` environment variables and link-time `emccFlags` in your build YAML control different aspects of the output. See [Two-channel config model](/docs/toolchain/concepts/two-channel-config-model) for the full picture. --- # Calling OCCT from JavaScript URL: /docs/package/concepts/calling-occt-from-js The OCJS bindings present OCCT to JavaScript through a handful of conventions that hide most of the C++ idioms. This page covers the call-site rules that apply to every API surface — constructors, methods, statics, and downcasts. For the shape of what comes back, see [Return shapes](/docs/package/concepts/return-shapes). ## A worked example, top to bottom [#a-worked-example-top-to-bottom] Most of the rules below show up in this 8-line construction of an arc-edge: ```typescript using p1 = new oc.gp_Pnt(0, 0, 0); using p2 = new oc.gp_Pnt(5, 0, 5); using p3 = new oc.gp_Pnt(10, 0, 0); using arcMaker = new oc.GC_MakeArcOfCircle(p1, p2, p3); using curve = arcMaker.Value(); // Geom_TrimmedCurve smart pointer using edge = new oc.BRepBuilderAPI_MakeEdge(curve); using shape = edge.Edge(); // TopoDS_Edge using wire = new oc.BRepBuilderAPI_MakeWire(shape); ``` No `_2` / `_3` suffixes. No `Handle_*` wrappers. No `.get()` unwraps. No manual `.delete()`. The rest of the page explains why each line works. ## No more `_N` overload suffixes [#no-more-_n-overload-suffixes] OCJS V3 removed the legacy `_N` overload suffixes. You always call the suffix-free constructor or method; the runtime picks the right overload based on arity and argument shape. ```typescript using p = new oc.gp_Pnt(1, 2, 3); // not gp_Pnt_3 using box1 = new oc.BRepPrimAPI_MakeBox(10, 20, 30); using origin = new oc.gp_Pnt(1, 2, 3); using box2 = new oc.BRepPrimAPI_MakeBox(origin, 10, 20, 30); using corner = new oc.gp_Pnt(5, 10, 15); using box3 = new oc.BRepPrimAPI_MakeBox(origin, corner); ``` `gp_Pnt`, `BRepPrimAPI_MakeBox`, `BRepBuilderAPI_MakeEdge` — every multi-arity constructor surface works this way. ## Overload dispatch is by argument shape [#overload-dispatch-is-by-argument-shape] Same-arity overloads are picked at call time by a small JS-side dispatcher. You don't pick the overload — you pass arguments of the right shape and the binding routes the call. Three kinds of dispatch you'll see most often: * **By class type** — `BRepBuilderAPI_MakeEdge(gp_Pnt, gp_Pnt)` and `BRepBuilderAPI_MakeEdge(TopoDS_Vertex, TopoDS_Vertex)` both have arity 2; the dispatcher routes by `instanceof`. * **By integer vs float** — `gp_XY.SetCoord(1, 5.0)` routes to the indexed overload (`Number.isInteger(1) === true`). Float-only forms use the coordinate overload. * **By enum value** — passing `oc.IntSurf_TypeTrans.IntSurf_In` selects the enum-tagged constructor variant. Where C++ has parallel `int` and `size_t` overloads the bindgen collapses them to a single JS signature. `list.FindKey(1)` works without disambiguation. ## Methods returning values use `()` [#methods-returning-values-use-] Every value-returning accessor is a method call, not a property. The most common gotcha is reading a `gp_Pnt` as if its coordinates were fields: ```typescript // Right — method calls. using p = new oc.gp_Pnt(1, 2, 3); console.log(p.X(), p.Y(), p.Z()); // 1 2 3 // Wrong — these are the bound functions, not numbers. console.log(p.X, p.Y, p.Z); // [Function: X] [Function: Y] [Function: Z] ``` The same rule applies to `face.IsNull()`, `list.Size()`, `curve.FirstParameter()`, and every other "looks like a getter" surface in the binding. ## Enums are string-valued object members [#enums-are-string-valued-object-members] Every C++ enum is exposed as a plain JS object whose members are strings whose value equals the member name: ```typescript oc.TopAbs_ShapeEnum.TopAbs_EDGE === 'TopAbs_EDGE'; // true oc.TopAbs_Orientation.TopAbs_FORWARD === 'TopAbs_FORWARD'; ``` Always reach for the object-member spelling — it's the form IntelliSense surfaces, the bindings emit `.d.ts` types for, and the smoke tests use: ```typescript using box = new oc.BRepPrimAPI_MakeBox(10, 10, 10); using shape = box.Shape(); using explorer = new oc.TopExp_Explorer( shape, oc.TopAbs_ShapeEnum.TopAbs_FACE, oc.TopAbs_ShapeEnum.TopAbs_SHAPE, ); ``` Enum-typed return values are strings too: ```typescript const transition = aline.TransitionOnS1(); // 'IntSurf_In' ``` There is no `.value` accessor — the member already *is* the value. ## Trailing default args fill themselves [#trailing-default-args-fill-themselves] C++ default arguments work the way you'd expect: omit trailing parameters and the binding fills the C++ defaults. ```typescript // BRepMesh_IncrementalMesh(shape, linearDeflection, isRelative=false, // angDeflection=0.5, isInParallel=false) using mesh = new oc.BRepMesh_IncrementalMesh(shape, 0.1); // BRepAlgoAPI_Fuse(S1, S2, ProgressRange=...) using fuse = new oc.BRepAlgoAPI_Fuse(s1, s2); ``` Non-trailing positions still need an argument — if you want to override `angDeflection` you must also pass `isRelative`. ## `TopoDS` is the downcast bridge [#topods-is-the-downcast-bridge] `oc.TopoDS` is the namespace bridge for casting a `TopoDS_Shape` down to its concrete subtype. Each member is a free function that takes a generic shape and returns the typed wrapper. ```typescript using explorer = new oc.TopExp_Explorer( shape, oc.TopAbs_ShapeEnum.TopAbs_EDGE, oc.TopAbs_ShapeEnum.TopAbs_SHAPE, ); using current = explorer.Current(); using edge = oc.TopoDS.Edge(current); // typed TopoDS_Edge ``` The same shape exists for `Face`, `Wire`, `Vertex`, `Shell`, `Solid`, `CompSolid`, and `Compound`. Each is a function, not a constructor — no `new`. ## Errors throw `WebAssembly.Exception` [#errors-throw-webassemblyexception] When OCCT raises a C++ exception, it surfaces in JS as a `WebAssembly.Exception` carrying the typed C++ tag. Decoding the tag gives you the actual OCCT error (e.g. `Standard_DomainError`, `Standard_NullObject`). See the dedicated [Debugging WASM exceptions](/docs/package/guides/debugging-wasm-exceptions) guide for the matching pattern; this concepts page intentionally stays out of the runtime-decoding weeds. ## Related [#related] * [Return shapes](/docs/package/concepts/return-shapes) — what comes back from a call: native values, in-place class outputs, envelopes, and Handle elision. * [Handles and collections](/docs/package/concepts/handles-and-collections) — smart-pointer surfaces (`isNull` / `nullify`) and the `NCollection_*` containers. * [Memory and disposables](/docs/package/concepts/memory-and-disposables) — the `using` rule and the `DisposableStack` patterns for ownership transfer. --- # Handles and collections URL: /docs/package/concepts/handles-and-collections OCCT carries two long-running C++ idioms that V3's JS surface unifies under JS-native shapes: the `Handle` smart pointer family and the `NCollection_*` templated container family. This page covers both and the everyday patterns they bring to your call sites. ## Smart pointers are unified [#smart-pointers-are-unified] In OCCT C++, a `Handle` is an intrusive refcount wrapper around a heap-allocated `Geom_Curve`. In the V3 JS bindings, the wrapper and the pointee are the same JS object — every `Standard_Transient` subclass *is* the Handle. ```typescript using pnt = new oc.gp_Pnt(0, 0, 0); using dir = new oc.gp_Dir(0, 0, 1); using ax2 = new oc.gp_Ax2(pnt, dir); using circle = new oc.Geom_Circle(ax2, 5); circle.isNull(); // false ``` You never reach for a `Handle_` wrapper class — they don't exist on the binding object: ```typescript oc['Handle_Geom_Curve']; // undefined ``` You never call `.get()` to unwrap a Handle. Functions that expect a `Handle` accept the `Geom_Line` directly: ```typescript using line = new oc.Geom_Line(ax1); using edge = new oc.BRepBuilderAPI_MakeEdge(line); ``` ## `isNull()` vs `nullify()` [#isnull-vs-nullify] Two methods carry the Handle nullability semantics into JS: * `isNull()` is a read — `true` if the smart pointer holds no object. * `nullify()` clears the smart pointer. **Method calls on a nullified Handle throw.** ```typescript using circle = new oc.Geom_Circle(ax2, 5); circle.isNull(); // false circle.nullify(); circle.isNull(); // true circle.Radius(); // throws — decode via WebAssembly.Exception ``` The throw surfaces as a `WebAssembly.Exception` carrying the C++ tag; see [Debugging WASM exceptions](/docs/package/guides/debugging-wasm-exceptions). Plain primitive value types (`gp_Pnt`, `gp_Dir`, `gp_Vec`) do **not** have `isNull` / `nullify` — they're not smart pointers, they're directly-owned value objects. ## The `NCollection_*` container family [#the-ncollection_-container-family] OCCT predates `std::vector`, so it ships its own container family. The bindgen auto-discovers `NCollection_*` types referenced by surviving bound classes and emits one JS class per template instantiation. The JS class name is the mangled C++ name — `NCollection_List` becomes `NCollection_List_TopoDS_Shape`, the legacy `TopTools_ListOfShape` typedef does **not** appear on the binding object. Auto-discovery is driven by C++ `using` typedef declarations in the generated bindings — not by the JS `using` declaration keyword. These two keywords share a name but solve different problems. ### List — `NCollection_List_TopoDS_Shape` [#list--ncollection_list_topods_shape] The list family is sequence-ordered, supports head/tail access and reversal, and is the workhorse for accumulating shapes during a topological walk. ```typescript using box1 = new oc.BRepPrimAPI_MakeBox(10, 10, 10); using box2 = new oc.BRepPrimAPI_MakeBox(20, 20, 20); using shape1 = box1.Shape(); using shape2 = box2.Shape(); using list = new oc.NCollection_List_TopoDS_Shape(); list.Size(); // 0 using appended1 = list.Append(shape1); // see note below using appended2 = list.Append(shape2); appended1; appended2; // suppress unused-var lint list.Size(); // 2 using first = list.First(); using last = list.Last(); list.Reverse(); list.RemoveFirst(); ``` **Critical:** `list.Append(shape)` returns a disposable iterator handle. Capture it with `using` — leaving it unbound leaks the handle. The `require-using-on-disposable` lint rule catches this automatically. ### Sequence — `NCollection_Sequence_TDF_Label` [#sequence--ncollection_sequence_tdf_label] Sequence is the same conceptual shape as List but a different OCCT template; it shows up most often in XDE / TDocStd workflows. **Its `Append` does not return a disposable** — call it as a statement: ```typescript using seq = new oc.NCollection_Sequence_TDF_Label(); using label = new oc.TDF_Label(); seq.Append(label); // void; no `using` seq.Size(); // 1 ``` The general rule across the `NCollection_*` family: `Append` semantics differ per template. Trust the `.d.ts` types — they're the source of truth — and check the smoke tests when uncertain. ### Indexed map — `NCollection_IndexedMap_TopoDS_Shape_TopTools_ShapeMapHasher` [#indexed-map--ncollection_indexedmap_topods_shape_toptools_shapemaphasher] Indexed maps combine set semantics with insertion-order indexing. The classic use is dedup-then-iterate over the faces of a shape: ```typescript using box = new oc.BRepPrimAPI_MakeBox(10, 10, 10); using shape = box.Shape(); using explorer = new oc.TopExp_Explorer( shape, oc.TopAbs_ShapeEnum.TopAbs_FACE, oc.TopAbs_ShapeEnum.TopAbs_SHAPE, ); using map = new oc.NCollection_IndexedMap_TopoDS_Shape_TopTools_ShapeMapHasher(); while (explorer.More()) { using current = explorer.Current(); map.Add(current); explorer.Next(); } map.Size(); // 6 (cube faces) using face1 = map.FindKey(1); // 1-based map.Contains(face1); // true map.FindIndex(face1); // 1 ``` `FindKey` is **1-based**, matching OCCT's index convention. ### Fixed-size array — `NCollection_Array1_gp_Pnt` [#fixed-size-array--ncollection_array1_gp_pnt] `Array1` is OCCT's fixed-bounds array. The constructor takes lower and upper bounds; the array is 1-indexed by default. ```typescript using arr = new oc.NCollection_Array1_gp_Pnt(1, 5); arr.Length(); // 5 arr.Lower(); // 1 arr.Upper(); // 5 const pts = [ new oc.gp_Pnt(1, 0, 0), new oc.gp_Pnt(2, 0, 0), new oc.gp_Pnt(3, 0, 0), new oc.gp_Pnt(4, 0, 0), new oc.gp_Pnt(5, 0, 0), ]; for (let i = 0; i < pts.length; i++) arr.SetValue(i + 1, pts[i]); using third = arr.Value(3); // gp_Pnt at index 3 using first = arr.First(); using last = arr.Last(); ``` Expect 1-based indexing everywhere in the `NCollection_*` family — `Value(0)` throws `Standard_OutOfRange`. ## Lifetimes at a glance [#lifetimes-at-a-glance] | Holder | Owns the heap allocation? | | --------------------------------------------------------------- | ---------------------------------------------------- | | Smart-pointer subclass (`Geom_Circle`, `Poly_Triangulation`, …) | Yes — refcounted; copying bumps the count. | | Value class (`gp_Pnt`, `gp_Dir`, `gp_Vec`) | Yes — directly owned; dispose to free. | | `T&` parameter | No — borrowed for the duration of the call. | | `NCollection_*` | Yes — disposing the container disposes its contents. | In JS terms: every wrapper class — handles, value types, containers — exposes `[Symbol.dispose]`. Bind them with `using` so the wasm heap releases at scope exit. See [Memory and disposables](/docs/package/concepts/memory-and-disposables) for the full ruleset and the `DisposableStack` patterns for ownership transfer. ## Related [#related] * [Memory and disposables](/docs/package/concepts/memory-and-disposables) — when `using` is required, and the `DisposableStack` patterns. * [Return shapes](/docs/package/concepts/return-shapes) — `NCollection_*` instances showing up inside envelope fields. * [Debugging WASM exceptions](/docs/package/guides/debugging-wasm-exceptions) — decoding the throw from a nullified-Handle method call. --- # Memory and disposables URL: /docs/package/concepts/memory-and-disposables Every OCCT object you allocate in JS — `gp_Pnt`, `TopoDS_Shape`, `BRepPrimAPI_MakeBox`, every handle, every container — owns memory inside the WebAssembly linear memory. Failing to release it leaks the wasm heap, which grows up to the 4 GB wasm32 ceiling and then crashes the runtime. ## The `using` rule [#the-using-rule] Declare every disposable OCCT object with `using` so the runtime invokes `[Symbol.dispose]()` when control leaves the scope. ```typescript { using box = new oc.BRepPrimAPI_MakeBox(10, 10, 10); using shape = box.Shape(); // also disposable // ... use box / shape ... } // ← both disposed here, in reverse declaration order ``` The OCJS repo includes an oxlint rule, `ocjs-lint/require-using-on-disposable`, that flags any plain `const`/`let` declaration of a disposable as an error. It's wired into the in-repo `eslint.config.mjs` for the smoke-test suite but is **not** auto-installed by `pnpm add opencascade.js` — the published npm tarball ships only the wasm, JS loader, `.d.ts`, manifest, provenance, and changelogs. If you want the rule in your own project, copy it from the OCJS repo into your local lint setup. ## Pure-data containers [#pure-data-containers] Return-by-value (RBV) containers whose fields are all primitives (no nested disposables) **do not** auto-emit `[Symbol.dispose]`. You can use them without `using`: ```typescript // gp_Pnt has only doubles — no dispose const point = transform.TransformedPoint(p0); console.log(point.X(), point.Y(), point.Z()); ``` Coordinate accessors like `X` / `Y` / `Z` are methods, not properties — see [Calling OCCT from JS](/docs/package/concepts/calling-occt-from-js#methods-returning-values-use-) for the pattern. Containers with at least one disposable child auto-emit a `[Symbol.dispose]` that walks the children and disposes each. The lint rule above handles the classification automatically. ## When `using` is not required [#when-using-is-not-required] `using` is required for anything that owns wasm memory and surfaces a `[Symbol.dispose]`. Four common shapes do **not** need a `using` on the call site — see [Return shapes](/docs/package/concepts/return-shapes#when-do-i-need-using) for the full decision table. * **`void` methods with class-only outputs** — `curve.D0(u, pt)` returns `void`; the `using` lives on the input `pt`, not the call result. * **Primitive / enum-only envelopes** — `surface.Bounds(0,0,0,0)` returns a plain `{ U1, U2, V1, V2 }` object with no disposer; bind it with `const`. * **Native primitive returns** — `pnt.X()`, `list.Size()`, `face.IsNull()` return numbers / booleans, never disposables. * **Forwarding ownership out of a helper** — when a function passes ownership to its caller, the helper returns the disposable without `using` so the caller binds it. The `DisposableStack.move()` pattern below is the canonical OCCT idiom. ## Composing lifetimes with `DisposableStack` [#composing-lifetimes-with-disposablestack] The TC39 explicit-resource-management proposal also ships a built-in `DisposableStack` — a LIFO container that adopts disposables and disposes each one when the stack itself is disposed. Two OCCT-specific patterns recur often enough to keep in your toolbox. ### `stack.use(...)` — adopt a multi-handle return value as one resource [#stackuse--adopt-a-multi-handle-return-value-as-one-resource] Some BRep\_Tool overloads return an RBV envelope that owns several embind handles (e.g. `PolygonOnTriangulation` in its 2-arg form returns a `{ P, T, L }` triple). Binding the whole envelope through `using` is the easy path, but if you only want to forward it once and free everything in one shot — `stack.use()` adopts the envelope and lets the parent stack cascade through its `[Symbol.dispose]`. ```typescript using stack = new DisposableStack(); const r = stack.use(oc.BRep_Tool.PolygonOnTriangulation(edge, loc)); // ... read r.P / r.T / r.L ... // stack disposes here, which disposes r, which disposes P/T/L in turn. ``` ### `stack.move()` — transfer ownership out of a search loop [#stackmove--transfer-ownership-out-of-a-search-loop] The hardest OCCT lifetime puzzle is "iterate through a topology looking for a match; on hit, hand the owning handles to the caller; on miss, free everything before the next iteration". `using` alone can't express this because a `using` binding always disposes at scope exit. `DisposableStack` solves it: each iteration creates a fresh stack, adopts every interim handle, and on success `move()`s the stack into the return value (which empties the iteration-scoped stack so its scope-exit dispose is a no-op). ```typescript function findFirstTriangulatedEdge(shape: TopoDS_Shape): DisposableStack & { edge: TopoDS_Edge; tri: Poly_Triangulation; loc: TopLoc_Location } { const oc = getOC(); using explorer = new oc.TopExp_Explorer(shape, oc.TopAbs_ShapeEnum.TopAbs_EDGE, oc.TopAbs_ShapeEnum.TopAbs_SHAPE); while (explorer.More()) { using iterStack = new DisposableStack(); using current = explorer.Current(); const edge = iterStack.use(oc.TopoDS.Edge(current)); const loc = iterStack.use(new oc.TopLoc_Location()); // ... walk faces, look up triangulation ... const tri = iterStack.use(oc.BRep_Tool.Triangulation(face, loc, 0)); if (!tri.isNull()) { // Match: transfer ownership. `iterStack` is now empty, its scope-exit // dispose is a no-op, and the returned stack owns edge/tri/loc. return Object.assign(iterStack.move(), { edge, tri, loc }); } explorer.Next(); // No match: `iterStack` disposes here, freeing edge/loc/tri this round. } throw new Error('no triangulated edge'); } // Caller side — one `using` collects three handles: using ctx = findFirstTriangulatedEdge(shape); // The 3-arg PolygonOnTriangulation overload returns a native Handle smart // pointer (disposable). For the elision overload that returns the {P, T, L} // envelope, see the `stack.use()` example above. using polygon = oc.BRep_Tool.PolygonOnTriangulation(ctx.edge, ctx.tri, ctx.loc); ``` `DisposableStack` is part of the JS standard library wherever `[Symbol.dispose]` is — no OCJS-specific runtime is involved. See [MDN: DisposableStack](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DisposableStack) for the full surface (`defer`, `adopt`, `disposed`, etc.). ## Pool pattern for hot loops [#pool-pattern-for-hot-loops] In a render loop you may allocate the same OCCT object hundreds of times per second. Allocate once, reuse, dispose once: ```typescript class TransformPool { private readonly transforms = new Map(); get(key: string): typeof oc.gp_Trsf { let trsf = this.transforms.get(key); if (!trsf) { trsf = new oc.gp_Trsf(); this.transforms.set(key, trsf); } return trsf; } [Symbol.dispose]() { for (const trsf of this.transforms.values()) trsf[Symbol.dispose](); this.transforms.clear(); } } using pool = new TransformPool(); for (let i = 0; i < 1000; i++) { const trsf = pool.get(`rot-${i % 8}`); trsf.SetRotation(axis, i * 0.01); // ... } ``` ## Diagnosing leaks [#diagnosing-leaks] Wasm heap grows monotonically — to detect a leak, measure `oc.HEAP8.byteLength` before and after a representative workload. ```typescript const before = oc.HEAP8.byteLength; runWorkload(); const after = oc.HEAP8.byteLength; console.log(`heap delta: ${(after - before) / 1024} KB`); ``` For deep audits, build with `-sASSERTIONS=2 -fsanitize=leak` (debug builds only) and the runtime prints a leak summary on `process.exit()`. ## What `[Symbol.dispose]` actually does [#what-symboldispose-actually-does] Calls the wrapped object's C++ destructor through the embind glue. For handles, this decrements the refcount; for value types, it frees the C++ allocation. The JS wrapper object becomes invalid — subsequent method calls throw an embind error (text varies, typically along the lines of `BindingError: instance already deleted`). ## Pitfalls [#pitfalls] * **`using` inside an `if (cond)` branch** disposes when control exits the branch, not when the outer block ends. Move the declaration up if you need it across the branch. * **Returning a `using` value** disposes it before the caller sees it. Use a plain `const` for return values and disposal at the call site. * **Passing a disposed object as a parameter** throws on every method. Disciplined `using` placement is the cheapest defence. ## Related [#related] * [Calling OCCT from JS](/docs/package/concepts/calling-occt-from-js) — overload dispatch, enums, defaults, and the `TopoDS` downcast bridge. * [Return shapes](/docs/package/concepts/return-shapes) — when a call returns an envelope, a native value, or an in-place class output, and which of those need `using`. * [Handles and collections](/docs/package/concepts/handles-and-collections) — `isNull` / `nullify` on smart pointers and the `NCollection_*` container family. --- # Return shapes URL: /docs/package/concepts/return-shapes A single OCCT method can have multiple output channels: its native C++ return, plus any number of `T&` output parameters. The bindings collapse these into a small set of JS-shaped return forms. This page covers each shape, how to recognise it from a signature, and whether the result needs `using`. For the rules on *how to call* a method (overload dispatch, enums, defaults), see [Calling OCCT from JS](/docs/package/concepts/calling-occt-from-js). ## TL;DR [#tldr] | What the C++ method outputs | What JS sees | | ----------------------------------------------------------- | -------------------------------------------------------------------------------------- | | Class output param (`Bnd_Box&`, `gp_Pnt&`, …) | The class you passed in is mutated in place. Read it after the call. | | Primitive / enum output param (`double&`, `Standard_Real&`) | An envelope is returned; **keep passing placeholder values** at the slot. | | Non-const `Handle&` output param | The slot is **elided** from the JS signature; the Handle appears as an envelope field. | | Native C++ return alongside any output(s) | Surfaced on the envelope as `returnValue`. | ## Decision tree [#decision-tree] Walk this table top-to-bottom for any signature you're reading off the `.d.ts`: | C++ return | C++ output params | Resulting JS shape | | ---------- | ----------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- | | Non-`void` | None | Native return (no envelope). Smart pointers count as native — see Handles below. | | Non-`void` | Class only (mutated in place) | Native return. Read mutated classes from your input variables; they are not echoed in the return. | | `void` | Class only | `void`. Read mutated classes from your input variables. | | Non-`void` | Primitives / enums / elided Handles (± class outputs) | Envelope with `returnValue` for the C++ return + one named field per non-class, non-elided output. Class outputs are NOT echoed. | | `void` | Primitives / enums / elided Handles (± class outputs) | Same envelope shape, minus `returnValue`. | The four sections below walk each row with worked examples. ## Class outputs mutate in place [#class-outputs-mutate-in-place] When a method takes a non-const class reference (`gp_Pnt&`, `Bnd_Box&`, `GProp_GProps&`, `TopoDS_Shape&`), the binding passes the JS wrapper's underlying C++ pointer straight through. The C++ method mutates the object you allocated; you read the result by querying your own variable after the call. ```typescript using curve = makeSomeCurve(); using inStartPt = new oc.gp_Pnt2d(0, 0); using inEndPt = new oc.gp_Pnt2d(0, 0); curve.D0(curve.FirstParameter(), inStartPt); curve.D0(curve.LastParameter(), inEndPt); console.log(inStartPt.X(), inStartPt.Y()); // mutated by D0 console.log(inEndPt.X(), inEndPt.Y()); ``` Same pattern for whole-shape evaluators: ```typescript using props = new oc.GProp_GProps(); oc.BRepGProp.VolumeProperties(shape, props, false, false, false); const volume = props.Mass(); ``` The call itself returns `void` or a native value — never an envelope echoing the class output. The `using` declaration that matters is the one on your input class; the call site doesn't need its own `using`. ## Primitive / enum envelopes — placeholder inputs are required [#primitive--enum-envelopes--placeholder-inputs-are-required] When the method has primitive or enum output parameters, the binding wraps the return in an envelope object whose fields mirror those outputs. The C++ signature is preserved at the call site — you keep passing values for those slots, but the values are placeholders that C++ overwrites. ```typescript using surface = new oc.Geom_SphericalSurface(ax, 10); const bounds = surface.Bounds(0, 0, 0, 0); // placeholders console.log(bounds.U1, bounds.U2, bounds.V1, bounds.V2); ``` The four `0`s are **not optional**. The binding uses input-passthrough return-by-value: it preserves the C++ argument list so overload resolution still works, then reads back the mutated values into the envelope. Calling `surface.Bounds()` with zero arguments either fails dispatch or picks the wrong overload. Primitive-only envelopes (every field is a number, boolean, or string) **do not carry `[Symbol.dispose]`**. Plain `const` works; you don't need `using` on the result. This is enforced by the type-level contract in `tests/disposable-containers.test-d.ts` — the type tests are the canonical truth even where the `BREAKING_CHANGES.md` §B2 table row still shows a disposer on the `Bounds` shape. ## `returnValue`, not `result` [#returnvalue-not-result] When a method has a non-`void` C++ return *and* primitive/enum/Handle outputs, the envelope grows a `returnValue` field carrying the native return. The OCJS v2 name `result` is gone in V3. ```typescript using batten = new oc.FairCurve_Batten(p1, p2, /* height */ 0.5); const r = batten.Compute( oc.FairCurve_AnalysisCode.FairCurve_OK, // input-passthrough enum /* nbIters */ 50, /* tolerance */ 1e-3, ); console.log(r.returnValue); // boolean, the C++ return console.log(r.Code); // enum output mirror ``` `BRep_Tool.Curve` is another common case: the primitive-only outputs (`First`, `Last`) and the native Handle return appear together on a `{ returnValue, First, Last }` envelope. ## Handle output elision [#handle-output-elision] Non-const `Handle&` output parameters are the one case where the JS signature is *smaller* than the C++ signature. The binding drops those positions from the JS argument list entirely; the freshly-assigned Handle surfaces as an envelope field instead. ```typescript using r = oc.BRep_Tool.PolygonOnTriangulation(edge, loc); // 2 args, not 4 console.log(r.P, r.T); // Handle outputs // `loc` (a class output) is mutated in place; not echoed in r. ``` Compare to the same method called with a triangulation in its overload slot — note that this is a **different overload**, picked by argument shape, that returns a native Handle instead of an envelope: ```typescript using handle = oc.BRep_Tool.PolygonOnTriangulation(edge, tri, loc); // 3 args // handle is a Handle, not an envelope. ``` This is the asymmetry to internalise: * Primitive / enum output slots → **stay** in the JS arg list as placeholders. * `Handle&` output slots → **disappear** from the JS arg list. Other elided signatures: `GeomInt_IntSS.BuildPCurves`, `ShapeAnalysis_Edge.TreatRLine`, the `ShapeConstruct.New*` family, `HelixGeom_BuilderApproxCurve3d.ApprHelix`. Handle-bearing envelopes do carry `[Symbol.dispose]` — they own embind wrappers that must be released. Always bind them with `using`. ## When do I need `using`? [#when-do-i-need-using] | Return shape | `using` on the call site? | | ----------------------------------------------------------------------- | ------------------------------------------------------------ | | `void` with class-only outputs (e.g. `curve.D0(u, pt)`) | No — `using` goes on the input class. | | Native primitive return (`pnt.X()`, `list.Size()`) | No — plain values. | | Native Handle return (`arcMaker.Value() → Geom_TrimmedCurve`) | Yes — Handle is disposable. | | Primitive / enum-only envelope (`surface.Bounds(0,0,0,0)`) | No — envelope has no disposer. | | Handle-bearing envelope (`BRep_Tool.PolygonOnTriangulation(edge, loc)`) | Yes — `[Symbol.dispose]` cascades through the Handle fields. | The canonical type-level contract lives in `tests/disposable-containers.test-d.ts` and `tests/output-params.test-d.ts` in the OCJS repo — when in doubt, the type definitions are the source of truth. ## Disposer idempotency [#disposer-idempotency] Manual `r[Symbol.dispose]()` followed by a `using` scope-exit re-dispose is safe — the second call is a no-op rather than a `BindingError`. This is intentional so try/finally migration paths can co-exist with `using`: ```typescript using r = oc.BRep_Tool.PolygonOnTriangulation(edge, loc); r[Symbol.dispose](); // no-op for the scope-exit dispose ``` ## Related [#related] * [Calling OCCT from JS](/docs/package/concepts/calling-occt-from-js) — overload dispatch, defaults, enums, and the `TopoDS` downcast bridge. * [Handles and collections](/docs/package/concepts/handles-and-collections) — the smart-pointer surface (`isNull` / `nullify`) and the `NCollection_*` containers that show up in envelope fields. * [Memory and disposables](/docs/package/concepts/memory-and-disposables) — when `using` is required vs. optional, and the `DisposableStack` patterns. --- # Boolean logo URL: /docs/package/examples/boolean-logo A worked example of boolean operations — start with a sphere, cut it four times with translated and scaled copies, fuse a rotated duplicate, and visualise the result. ```typescript title="examples/boolean-logo.ts" import type { TopoDS_Shape } from '@taucad/opencascade.js'; import { getOc } from './ocjs-init'; export const buildLogo = async (): Promise => { const oc = await getOc(); using sphere = new oc.BRepPrimAPI_MakeSphere(1); const makeCut = (shape: TopoDS_Shape, translation: readonly [number, number, number], scale: number) => { using tf = new oc.gp_Trsf(); tf.SetTranslation(new oc.gp_Vec(translation[0], translation[1], translation[2])); tf.SetScaleFactor(scale); using loc = new oc.TopLoc_Location(tf); using progress = new oc.Message_ProgressRange(); using cut = new oc.BRepAlgoAPI_Cut(shape, sphere.Shape().Moved(loc, false), progress); cut.Build(progress); return cut.Shape(); }; const cut1 = makeCut(sphere.Shape(), [0, 0, 0.7], 1); const cut2 = makeCut(cut1, [0, 0, -0.7], 1); const cut3 = makeCut(cut2, [0, 0.25, 1.75], 1.825); const cut4 = makeCut(cut3, [4.8, 0, 0], 5); const makeRotation = (rotation: number) => { const tf = new oc.gp_Trsf(); tf.SetRotation(new oc.gp_Ax1(new oc.gp_Pnt(), new oc.gp_Dir(0, 0, 1)), rotation); return new oc.TopLoc_Location(tf); }; using progress = new oc.Message_ProgressRange(); using fuse = new oc.BRepAlgoAPI_Fuse(cut4, cut4.Moved(makeRotation(Math.PI), false), progress); fuse.Build(progress); const result = fuse.Shape().Moved(makeRotation(-30 * Math.PI / 180), false); // XCAF — per-subset PBR materials (brass + gray zones) using doc = new oc.TDocStd_Document(new oc.TCollection_ExtendedString_1()); const shapeTool = oc.XCAFDoc_DocumentTool.ShapeTool(doc.Main()).get(); using it1 = new oc.TopoDS_Iterator(result, true, true); for (; it1.More(); it1.Next()) { let i = 0; using it2 = new oc.TopoDS_Iterator(it1.Value(), true, true); for (; it2.More(); it2.Next()) { const newShape = shapeTool.NewShape(); shapeTool.SetShape(newShape, it2.Value()); const vmtool = oc.XCAFDoc_DocumentTool.VisMaterialTool(newShape).get(); using visMat = new oc.XCAFDoc_VisMaterial(); const matLabel = vmtool.AddMaterial( new oc.Handle_XCAFDoc_VisMaterial(visMat), new oc.TCollection_AsciiString(`logoMat${i}`), ); vmtool.SetShapeMaterial(newShape, matLabel); using visMatPbr = new oc.XCAFDoc_VisMaterialPBR(); if (i === 3) { visMatPbr.BaseColor = new oc.Quantity_ColorRGBA(0.6, 0.5, 0, 1); } else { visMatPbr.BaseColor = new oc.Quantity_ColorRGBA(0.3, 0.3, 0.3, 1); } visMat.SetPbrMaterial(visMatPbr); i++; } } return result; }; ``` ## What's happening [#whats-happening] 1. `BRepPrimAPI_MakeSphere(1)` builds a unit sphere. 2. `makeCut` constructs a transform (translate + scale), wraps it in a `TopLoc_Location`, moves a copy of the sphere, and subtracts it via `BRepAlgoAPI_Cut`. 3. Four sequential cuts produce one half of the OCJS logo glyph. 4. `BRepAlgoAPI_Fuse` welds the half to a 180°-rotated copy of itself. 5. A final 30° rotation tilts the logo. The takeaway: boolean operations allow you to create highly complex shapes that would be difficult or impossible with classical polygon-based modelling. ## Render it [#render-it] ```typescript const logoShape = await buildLogo(); const glb = await shapeToGlb(logoShape); renderGlb(canvas, glb); ``` See [Render with three.js](/docs/package/guides/render-with-three-js) for the GLB → three.js wiring. See also [Visualize shape helper](/docs/package/guides/visualize-shape-helper). --- # Classic bottle URL: /docs/package/examples/classic-bottle The [OpenCASCADE bottle tutorial](https://dev.opencascade.org/doc/overview/html/occt__tutorial.html) ported to V3 TypeScript with `using` syntax and suffix-free API. The example demonstrates **every major OCCT capability** in one script: 2D profile construction, mirroring, extrusion, fillets, cylinder primitives, boolean fuse, hollow-solid generation, threading via `ThruSections`, and final compound assembly. ```typescript title="examples/classic-bottle.ts" import type { TopoDS_Shape } from '@taucad/opencascade.js'; import { getOc } from './ocjs-init'; export type BottleParams = { width: number; // 20–100, default 50 height: number; // 50–120, default 70 thickness: number; // 15–50, default 30 }; export const buildBottle = async ( { width, height, thickness }: BottleParams = { width: 50, height: 70, thickness: 30 }, ): Promise => { const oc = await getOc(); // Profile — define support points const aPnt1 = new oc.gp_Pnt(-width / 2, 0, 0); const aPnt2 = new oc.gp_Pnt(-width / 2, -thickness / 4, 0); const aPnt3 = new oc.gp_Pnt(0, -thickness / 2, 0); const aPnt4 = new oc.gp_Pnt(width / 2, -thickness / 4, 0); const aPnt5 = new oc.gp_Pnt(width / 2, 0, 0); // Profile — define the geometry using arc = new oc.GC_MakeArcOfCircle(aPnt2, aPnt3, aPnt4); using seg1 = new oc.GC_MakeSegment(aPnt1, aPnt2); using seg2 = new oc.GC_MakeSegment(aPnt4, aPnt5); // Profile — define the topology using edge1 = new oc.BRepBuilderAPI_MakeEdge(seg1.Value()); using edge2 = new oc.BRepBuilderAPI_MakeEdge(arc.Value()); using edge3 = new oc.BRepBuilderAPI_MakeEdge(seg2.Value()); using wire = new oc.BRepBuilderAPI_MakeWire(edge1.Edge(), edge2.Edge(), edge3.Edge()); // Mirror the wire across the X axis const xAxis = oc.gp.OX(); using trsf = new oc.gp_Trsf(); trsf.SetMirror(xAxis); using mirroredBuilder = new oc.BRepBuilderAPI_Transform(wire.Wire(), trsf, false); const mirroredShape = mirroredBuilder.Shape(); using fullProfile = new oc.BRepBuilderAPI_MakeWire(); fullProfile.Add(wire.Wire()); fullProfile.Add(oc.TopoDS.Wire(mirroredShape)); // Body — extrude the profile using faceProfile = new oc.BRepBuilderAPI_MakeFace(fullProfile.Wire(), false); using prismVec = new oc.gp_Vec(0, 0, height); using body = new oc.BRepPrimAPI_MakePrism(faceProfile.Face(), prismVec, false, true); let workingBody = body.Shape(); // Body — apply edge fillets using fillet = new oc.BRepFilletAPI_MakeFillet(workingBody, oc.ChFi3d_FilletShape.ChFi3d_Rational); using edgeExp = new oc.TopExp_Explorer(workingBody, oc.TopAbs_ShapeEnum.TopAbs_EDGE); while (edgeExp.More()) { fillet.Add(thickness / 12, oc.TopoDS.Edge(edgeExp.Current())); edgeExp.Next(); } workingBody = fillet.Shape(); // Body — add the neck using neckLocation = new oc.gp_Pnt(0, 0, height); const neckAxis = oc.gp.DZ(); using neckAx2 = new oc.gp_Ax2(neckLocation, neckAxis); const neckRadius = 5; const neckHeight = 5; using cyl = new oc.BRepPrimAPI_MakeCylinder(neckAx2, neckRadius, neckHeight); using progress = new oc.Message_ProgressRange(); using fuse = new oc.BRepAlgoAPI_Fuse(workingBody, cyl.Shape(), progress); workingBody = fuse.Shape(); // Body — hollow the solid (remove the top face of the neck) let faceToRemove: ReturnType | undefined; let zMax = -1; using faceExp = new oc.TopExp_Explorer(workingBody, oc.TopAbs_ShapeEnum.TopAbs_FACE); for (; faceExp.More(); faceExp.Next()) { const aFace = oc.TopoDS.Face(faceExp.Current()); const aSurface = oc.BRep_Tool.Surface(aFace); if (aSurface.get().$$.ptrType.name === 'Geom_Plane*') { const aPlane = new oc.Handle_Geom_Plane(aSurface.get()).get(); const aPnt = aPlane.Location(); if (aPnt.Z() > zMax) { zMax = aPnt.Z(); using topFaceExp = new oc.TopExp_Explorer(aFace, oc.TopAbs_ShapeEnum.TopAbs_FACE); faceToRemove = oc.TopoDS.Face(topFaceExp.Current()); } } } using facesToRemove = new oc.TopTools_ListOfShape(); if (faceToRemove) facesToRemove.Append(faceToRemove); using thickSolid = new oc.BRepOffsetAPI_MakeThickSolid(); thickSolid.MakeThickSolidByJoin( workingBody, facesToRemove, -thickness / 50, 1e-3, oc.BRepOffset_Mode.BRepOffset_Skin, false, false, oc.GeomAbs_JoinType.GeomAbs_Arc, false, progress, ); workingBody = thickSolid.Shape(); // Threading — cylindrical surfaces + elliptical 2D curves using aCyl1 = new oc.Geom_CylindricalSurface(new oc.gp_Ax3(neckAx2), neckRadius * 0.99); using aCyl2 = new oc.Geom_CylindricalSurface(new oc.gp_Ax3(neckAx2), neckRadius * 1.05); const aPnt2d = new oc.gp_Pnt2d(2 * Math.PI, neckHeight / 2); const aDir2d = new oc.gp_Dir2d(2 * Math.PI, neckHeight / 4); using anAx2d = new oc.gp_Ax2d(aPnt2d, aDir2d); const aMajor = 2 * Math.PI; const aMinor = neckHeight / 10; using anEllipse1 = new oc.Geom2d_Ellipse(anAx2d, aMajor, aMinor, true); using anEllipse2 = new oc.Geom2d_Ellipse(anAx2d, aMajor, aMinor / 4, true); using anArc1 = new oc.Geom2d_TrimmedCurve(new oc.Handle_Geom2d_Curve(anEllipse1), 0, Math.PI, true, true); using anArc2 = new oc.Geom2d_TrimmedCurve(new oc.Handle_Geom2d_Curve(anEllipse2), 0, Math.PI, true, true); const tmp1 = anEllipse1.Value(0); const anEllipsePnt1 = new oc.gp_Pnt2d(tmp1.X(), tmp1.Y()); const tmp2 = anEllipse1.Value(Math.PI); const anEllipsePnt2 = new oc.gp_Pnt2d(tmp2.X(), tmp2.Y()); using aSegment = new oc.GCE2d_MakeSegment(anEllipsePnt1, anEllipsePnt2); using anEdge1OnSurf1 = new oc.BRepBuilderAPI_MakeEdge( new oc.Handle_Geom2d_Curve(anArc1), new oc.Handle_Geom_Surface(aCyl1), ); using anEdge2OnSurf1 = new oc.BRepBuilderAPI_MakeEdge( new oc.Handle_Geom2d_Curve(aSegment.Value()), new oc.Handle_Geom_Surface(aCyl1), ); using anEdge1OnSurf2 = new oc.BRepBuilderAPI_MakeEdge( new oc.Handle_Geom2d_Curve(anArc2), new oc.Handle_Geom_Surface(aCyl2), ); using anEdge2OnSurf2 = new oc.BRepBuilderAPI_MakeEdge( new oc.Handle_Geom2d_Curve(aSegment.Value()), new oc.Handle_Geom_Surface(aCyl2), ); using threadingWire1 = new oc.BRepBuilderAPI_MakeWire( anEdge1OnSurf1.Edge(), anEdge2OnSurf1.Edge(), ); using threadingWire2 = new oc.BRepBuilderAPI_MakeWire( anEdge1OnSurf2.Edge(), anEdge2OnSurf2.Edge(), ); oc.BRepLib.BuildCurves3d(threadingWire1.Wire()); oc.BRepLib.BuildCurves3d(threadingWire2.Wire()); using aTool = new oc.BRepOffsetAPI_ThruSections(true, false, 1e-6); aTool.AddWire(threadingWire1.Wire()); aTool.AddWire(threadingWire2.Wire()); aTool.CheckCompatibility(false); const myThreading = aTool.Shape(); // Compound assembly + final rotation using aRes = new oc.TopoDS_Compound(); using aBuilder = new oc.BRep_Builder(); aBuilder.MakeCompound(aRes); aBuilder.Add(aRes, workingBody); aBuilder.Add(aRes, myThreading); using rotTrsf = new oc.gp_Trsf(); rotTrsf.SetRotation(new oc.gp_Ax1(new oc.gp_Pnt(), new oc.gp_Dir(1, 0, 0)), -Math.PI / 2); using rotLoc = new oc.TopLoc_Location(rotTrsf); return aRes.Moved(rotLoc, false); }; ``` Every OCCT API used above has a direct V3 binding under `oc.` with no `_N` suffix. See the [OpenCASCADE tutorial](https://dev.opencascade.org/doc/overview/html/occt__tutorial.html) for the step-by-step walkthrough of the underlying geometry. ## Migration from v2 [#migration-from-v2] The v2 example used `gp_Pnt_3`, `GC_MakeArcOfCircle_4`, `BRepBuilderAPI_MakeEdge_24`, `gp_Trsf_1`, etc. V3 drops every `_N` suffix — overload dispatch happens in C++ via the unified RBV pipeline. Pass arguments by type and the right overload runs automatically. ## Render [#render] See [Render with three.js](/docs/package/guides/render-with-three-js) for the GLB → three.js wiring. See also [Visualize shape helper](/docs/package/guides/visualize-shape-helper). --- # Polygon extrusion URL: /docs/package/examples/polygon-extrusion The minimum useful OCCT example: a four-point polygon extruded along Z. ```typescript title="examples/polygon-extrusion.ts" import type { TopoDS_Shape } from '@taucad/opencascade.js'; import { getOc } from './ocjs-init'; export const buildExtrudedPolygon = async (): Promise => { const oc = await getOc(); using polygon = new oc.BRepBuilderAPI_MakePolygon(); polygon.Add(new oc.gp_Pnt(-50, -50, 0)); polygon.Add(new oc.gp_Pnt(50, -50, 0)); polygon.Add(new oc.gp_Pnt(50, 50, 0)); polygon.Add(new oc.gp_Pnt(-50, 50, 0)); polygon.Close(); using face = new oc.BRepBuilderAPI_MakeFace(polygon.Wire(), false); using prismVec = new oc.gp_Vec(0, 0, 40); using prism = new oc.BRepPrimAPI_MakePrism(face.Face(), prismVec, false, true); return prism.Shape(); }; ``` ## Why this matters [#why-this-matters] * `BRepBuilderAPI_MakePolygon` accepts an arbitrary number of points — three for a triangle, hundreds for a complex contour. * `Close()` adds the implicit closing edge. * `BRepBuilderAPI_MakeFace(wire, false)` faces the wire; `false` means "this is the only wire of the face" (set `true` if you're adding holes later). * `BRepPrimAPI_MakePrism` extrudes along an arbitrary direction vector, not just Z. Swap `gp_Vec(0, 0, 40)` for `gp_Vec(0.5, 0.5, 1)` and you get a slanted prism. Swap the polygon for a `BRepBuilderAPI_MakeWire` with curved edges and you get an extruded curved profile. ## Next steps [#next-steps] * Combine multiple extrusions with `BRepAlgoAPI_Fuse` for additive construction. * Subtract holes with `BRepAlgoAPI_Cut`. * Fillet sharp edges with `BRepFilletAPI_MakeFillet`. See [First shape tutorial](/docs/package/getting-started/first-shape-tutorial) for the fillet pattern. --- # FAQ URL: /docs/package/getting-started/faq ## Is this a fork of OpenCascade? [#is-this-a-fork-of-opencascade] Yes — with nuance. `@taucad/opencascade.js` is a **Tau-maintained fork** of [`donalffons/opencascade.js`](https://github.com/donalffons/opencascade.js), which itself is a port of upstream [OpenCASCADE Technology (OCCT)](https://dev.opencascade.org/) to WebAssembly via Emscripten. The fork exists to ship the **V3 / OCCT V8 release** while upstream is dormant. The intent is to **merge back upstream** into `donalffons/opencascade.js` via a single PR once V8 lands. During this window, issues and releases live at [`taucad/opencascade.js`](https://github.com/taucad/opencascade.js). OpenCascade.js does **not** modify the OCCT C++ source beyond a small set of patches applied at build time. The pipeline downloads a tagged OCCT commit, compiles with Emscripten, auto-generates embind bindings from libclang, and ships the wasm + TypeScript surface as an npm package. ## Who maintains it? [#who-maintains-it] **`donalffons`** remains the maintainer-of-record for OpenCascade.js. **Tau** is the interim driver during the V3 / OCCT V8 release window — shipping docs, the GHCR Docker image, and the `@taucad/opencascade.js@beta` npm package until the merge-back PR lands. ## How can I contribute? [#how-can-i-contribute] Contributions are welcome and upstream-merge-back-aware: 1. Open issues or PRs at [`taucad/opencascade.js`](https://github.com/taucad/opencascade.js). 2. Follow the existing code style and test conventions in the repo. 3. Prefer changes that will survive the upstream merge-back (OCJS-original branding, suffix-free V3 API, no Tau-specific forks of core bindgen logic). For larger architectural questions, start a discussion before opening a PR. --- # First Shape Tutorial URL: /docs/package/getting-started/first-shape-tutorial This tutorial extends the npm quickstart with explanations of every OCCT type involved — useful if you've never touched OCCT before. ## Step 1 — Choose a primitive [#step-1--choose-a-primitive] `BRepPrimAPI_MakeBox` constructs an axis-aligned box from three lengths. ```typescript using box = new oc.BRepPrimAPI_MakeBox(60, 40, 20); const shape = box.Shape(); ``` `shape` is a `TopoDS_Shape` — the universal OCCT geometry handle. All booleans, fillets, and exports take and return `TopoDS_Shape` instances. ## Step 2 — Walk topology [#step-2--walk-topology] OCCT shapes are hierarchical: `SOLID → SHELL → FACE → WIRE → EDGE → VERTEX`. `TopExp_Explorer` iterates entries of a given kind in declaration order. ```typescript using explorer = new oc.TopExp_Explorer(shape, oc.TopAbs_ShapeEnum.TopAbs_EDGE); const edges: typeof oc.TopoDS_Edge[] = []; while (explorer.More()) { edges.push(oc.TopoDS.Edge(explorer.Current())); explorer.Next(); } ``` A box has 12 edges — three groups of 4 parallel edges along each axis. ## Step 3 — Apply a fillet [#step-3--apply-a-fillet] `BRepFilletAPI_MakeFillet` takes the shape and a per-edge radius. Calling `Add(radius, edge)` once per edge marks them for filleting; `Shape()` runs the algorithm and returns the result. ```typescript using fillet = new oc.BRepFilletAPI_MakeFillet(shape); for (const edge of edges) fillet.Add(3, edge); const filletedShape = fillet.Shape(); ``` ## Step 4 — Tessellate and export [#step-4--tessellate-and-export] Until you call `BRepMesh_IncrementalMesh`, the shape has no triangulation — it's still analytic. The mesher caches per-shape triangulation; pass `decreate=true` on the third argument to force regeneration when you change tolerance. ```typescript using _mesh = new oc.BRepMesh_IncrementalMesh(filletedShape, 0.1, false, 0.5, false); ``` The two tolerances are **linear** (mm) and **angular** (radians). 0.1 mm linear is fine for a 60 mm box; tighten it on small features. Export via the XCAF path documented in [Export glTF / GLB](/docs/package/guides/export-gltf) to get a GLB your three.js viewer can load. ## Step 5 — Render [#step-5--render] The npm quickstart shows the full three.js wiring. The minimal version: ```typescript const loader = new GLTFLoader(); loader.parse(glb.buffer, '', (gltf) => scene.add(gltf.scene)); ``` ## What went wrong if… [#what-went-wrong-if] | Symptom | Likely cause | | ---------------------------------------- | ------------------------------------------------------------------------------------- | | `BindingError: ptr<-1>` | You called a method on a disposed object — `using` exited scope earlier than expected | | Black canvas, no errors | Camera is inside the shape — set `camera.position.set(100, 100, 100)` | | Fillet `Shape()` throws | One of your edges is degenerate; check `edge.Orientation()` | | `RWGltf_CafWriter.Perform` returns false | The XCAF doc has no shape attached — verify `ShapeTool.AddShape` succeeded | ## Where to next [#where-to-next] * [Memory and disposables](/docs/package/concepts/memory-and-disposables) for the OCCT memory model in depth. * [Two-channel config model](/docs/package/concepts/two-channel-config-model) for compile-time vs link-time config. * [BRepPrimAPI](/docs/package/api/modeling-algorithms/tk-prim/b-rep-prim-api) for the primitive surface. --- # Projects using OpenCascade.js URL: /docs/package/getting-started/projects-using-opencascade-js OpenCascade.js powers browser-native CAD across several production apps and reference repositories. Tau is listed as a peer entry alongside the original gallery — not promoted to headline status. ## Applications [#applications] * [ArchiYou](https://archiyou.com/) — library, code-CAD design tool, community hub. * [BitByBit](https://bitbybit.dev/) — code- and node-based CAD design tool. * [CascadeStudio](https://github.com/zalo/CascadeStudio) — library and code-CAD design tool. * [Polygonjs](https://polygonjs.com) — procedural design and animation tool for WebGL. * [RepliCAD](https://replicad.xyz/) — library and code-CAD design tool. * [Tau](https://tau.new) — AI-native CAD platform for the web. ## Reference repositories [#reference-repositories] * [opencascade.js-examples](https://github.com/donalffons/opencascade.js-examples) — general examples on how to use the library. --- # Quickstart — npm URL: /docs/package/getting-started/quick-start-npm Target time: **4 minutes** from an empty directory to an orbit-controlled box in your browser. ## Prerequisites [#prerequisites] * Node 22+ and pnpm 9+ (npm and yarn also work). * A bundler that supports wasm imports — Vite 6+, Next 15+, or Bun. ## 1. Install [#1-install] ```bash pnpm add @taucad/opencascade.js@beta three pnpm add -D @types/three typescript ``` ## 2. Initialise OCCT once [#2-initialise-occt-once] ```typescript title="src/ocjs-init.ts" import init from '@taucad/opencascade.js'; import wasmUrl from '@taucad/opencascade.js/wasm?url'; let ocPromise: ReturnType | undefined; export const getOc = () => (ocPromise ??= init({ locateFile: () => wasmUrl })); ``` The memoised Promise guarantees one wasm fetch and one C++ runtime init per page load. Re-calling `getOc()` from anywhere in your app resolves immediately after the first warm-up. ## 3. Build a shape [#3-build-a-shape] ```typescript title="src/build-shape.ts" import { getOc } from './ocjs-init'; export const buildFilletedBox = async () => { const oc = await getOc(); using box = new oc.BRepPrimAPI_MakeBox(60, 40, 20); using fillet = new oc.BRepFilletAPI_MakeFillet(box.Shape()); using explorer = new oc.TopExp_Explorer(box.Shape(), oc.TopAbs_ShapeEnum.TopAbs_EDGE); while (explorer.More()) { fillet.Add(3, oc.TopoDS.Edge(explorer.Current())); explorer.Next(); } return fillet.Shape(); }; ``` Every OCCT object is disposable — `using` invokes `Symbol.dispose()` at scope exit so the C++ heap doesn't leak. ## 4. Export to GLB [#4-export-to-glb] ```typescript title="src/shape-to-glb.ts" import type { TopoDS_Shape } from '@taucad/opencascade.js'; import { getOc } from './ocjs-init'; export const shapeToGlb = async (shape: TopoDS_Shape): Promise => { const oc = await getOc(); using docName = new oc.TCollection_ExtendedString('doc', true); const doc = new oc.TDocStd_Document(docName); oc.XCAFDoc_DocumentTool.ShapeTool(doc.Main()).get().AddShape(shape, false, false); using _mesh = new oc.BRepMesh_IncrementalMesh(shape, 0.1, false, 0.5, false); const path = `/out_${Date.now()}.glb`; using asciiPath = new oc.TCollection_AsciiString(path); using writer = new oc.RWGltf_CafWriter(asciiPath, true); using metadata = new oc.TColStd_IndexedDataMapOfStringString(); using progress = new oc.Message_ProgressRange(); writer.Perform(doc, metadata, progress); const raw = oc.FS.readFile(path) as Uint8Array; oc.FS.unlink(path); return new Uint8Array(raw); }; ``` The `new Uint8Array(raw)` copy is mandatory — `FS.readFile` returns a view into MEMFS that becomes invalid after `unlink`. ## 5. Render in three.js [#5-render-in-threejs] ```typescript title="src/main.ts" import * as THREE from 'three'; import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'; import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'; import { buildFilletedBox } from './build-shape'; import { shapeToGlb } from './shape-to-glb'; const canvas = document.querySelector('#viewer')!; const renderer = new THREE.WebGLRenderer({ canvas, antialias: true }); const scene = new THREE.Scene(); const camera = new THREE.PerspectiveCamera(45, 1, 0.1, 5000); camera.position.set(100, 100, 100); scene.add(new THREE.AmbientLight(0xffffff, 0.5)); const dir = new THREE.DirectionalLight(0xffffff, 1); dir.position.set(1, 1, 1); scene.add(dir); new OrbitControls(camera, renderer.domElement); const shape = await buildFilletedBox(); const glb = await shapeToGlb(shape); new GLTFLoader().parse(glb.buffer, '', (gltf) => scene.add(gltf.scene)); renderer.setAnimationLoop(() => renderer.render(scene, camera)); ``` Visit `localhost:5173` and you'll see a filleted gray box you can orbit. > **Want threading?** For batch meshing and boolean workloads, import `@taucad/opencascade.js/multi` instead of the default export. Browser deployments require COOP/COEP headers — see the [Multi-threaded build guide](/docs/package/guides/multi-threading) and [Bundler & locateFile — Multi-threaded variant](/docs/package/guides/bundler-locatefile#multi-threaded-variant). ## Next steps [#next-steps] * Need a different bundler? See [Bundler & locateFile](/docs/package/guides/bundler-locatefile). * Want a smaller wasm? See [Trim symbols](/docs/package/guides/trim-symbols). * Looking for STEP instead of GLB? See [Export STEP](/docs/package/guides/export-step). --- # What is OpenCascade.js URL: /docs/package/getting-started/what-is-opencascade-js `@taucad/opencascade.js` brings the OpenCASCADE Technology kernel — a 30-year-old production-grade BRep CAD library — into the WebAssembly runtime. Every public OCCT symbol the package configures becomes a TypeScript class with overloaded constructors, typed methods, and full `.d.ts` coverage. ## Why a CAD kernel in WebAssembly [#why-a-cad-kernel-in-webassembly] Browser-native CAD historically meant polygon-only mesh editing. A BRep kernel gives you: * **Exact geometry** — curves and surfaces are analytic, not polygonal. Booleans, fillets, chamfers, threads all run on parametric primitives. * **Standards-grade interchange** — read and write STEP (`AP214` / `AP242`), IGES, STL, and GLB out of the box; no external tooling required. * **Full feature parity with desktop CAD** — `BRepPrimAPI_*` primitives, `BRepAlgoAPI_*` booleans, `BRepFilletAPI_*` fillets, `XCAF*` for assemblies and materials. ## When to use OpenCascade.js [#when-to-use-opencascadejs] | Use case | Good fit | Bad fit | | ---------------------------------------- | --------------------- | -------------------------------------- | | Parametric CAD app | Yes | — | | STEP / IGES interchange | Yes | — | | Procedural mesh generation | Yes | three.js or manifold-3d may be lighter | | Real-time interactive booleans (>60 fps) | Maybe — measure first | Reach for manifold-3d | | Cloud STEP-to-GLB conversion | Yes | — | ## When NOT to use it [#when-not-to-use-it] * You only need polygon meshes — `three.js` + `manifold-3d` is smaller and faster. * You need sub-megabyte wasm — full OCCT is \~38 MB; even trimmed it sits at \~12 MB for a typical "box + boolean + export" surface. * You need synchronous-only call sites — every `init` returns a Promise. ## Where to next [#where-to-next] * [Quickstart — npm](/docs/package/getting-started/quick-start-npm) — render a box in 20 lines of TypeScript. * [Trim symbols](/docs/package/guides/trim-symbols) — cut the wasm from 38 MB down to what your app uses. * [API Reference](/docs/package/api) — search every bound class and method. --- # Bundler & locateFile URL: /docs/package/guides/bundler-locatefile `init()` accepts a `locateFile` callback that returns the URL where the runtime should fetch the wasm binary. The right answer depends on your bundler. The package exposes the binary through the `@taucad/opencascade.js/wasm` subpath export. Every snippet on this page resolves the wasm through that specifier; resolving it any other way (deep `node_modules` paths, direct `dist/*` imports) bypasses the package's `exports` map and breaks under strict bundler resolution. ## Vite 6+ [#vite-6] Vite's `?url` suffix turns any asset import into a content-hashed URL string. Use it for both dev and production: ```typescript import init from '@taucad/opencascade.js'; import wasmUrl from '@taucad/opencascade.js/wasm?url'; const oc = await init({ locateFile: () => wasmUrl }); ``` Add `@taucad/opencascade.js` to `optimizeDeps.exclude` in `vite.config.ts` so Vite skips its dep-optimizer for the binary module: ```typescript title="vite.config.ts" import { defineConfig } from 'vite'; export default defineConfig({ optimizeDeps: { exclude: ['@taucad/opencascade.js'] }, }); ``` ## Next.js 15 (App Router) [#nextjs-15-app-router] Next's App Router lacks a first-class `?url` wasm import. The reliable pattern is to copy the wasm into `public/` at install time via a postinstall script that resolves the subpath: ```javascript title="scripts/copy-wasm.mjs" import { copyFile, mkdir } from 'node:fs/promises'; import { fileURLToPath } from 'node:url'; const src = fileURLToPath(import.meta.resolve('@taucad/opencascade.js/wasm')); await mkdir('public', { recursive: true }); await copyFile(src, 'public/opencascade_full.wasm'); ``` ```json title="package.json" { "scripts": { "postinstall": "node scripts/copy-wasm.mjs" } } ``` Then reference the public path: ```typescript title="lib/ocjs-init.ts" 'use client'; import init from '@taucad/opencascade.js'; let ocPromise: ReturnType | undefined; export const getOc = () => (ocPromise ??= init({ locateFile: () => '/opencascade_full.wasm' })); ``` Mark `@taucad/opencascade.js` as a server external package if you only call it client-side: ```typescript title="next.config.ts" import type { NextConfig } from 'next'; const config: NextConfig = { serverExternalPackages: ['@taucad/opencascade.js'], }; export default config; ``` ## Bun [#bun] Bun resolves wasm imports natively. The `?url` pattern works identically to Vite. No extra config required. ## Node (ESM) [#node-esm] Use `import.meta.resolve` to find the wasm sibling to the loader: ```typescript import { fileURLToPath } from 'node:url'; import { dirname, join } from 'node:path'; import init from '@taucad/opencascade.js'; const WASM_DIR = dirname( fileURLToPath(import.meta.resolve('@taucad/opencascade.js/wasm')), ); const oc = await init({ locateFile: (file: string) => join(WASM_DIR, file) }); ``` ## Deno [#deno] Deno exposes the same `import.meta.resolve` API as Node 22+. The Node snippet above works unchanged. ## Webpack 5 [#webpack-5] Webpack 5 handles wasm via `asset/resource` (preferred) or the legacy `file-loader`. Wire `locateFile` to the emitted URL: ```typescript title="src/ocjs-init.ts" import init from '@taucad/opencascade.js'; import wasmUrl from '@taucad/opencascade.js/wasm'; const oc = await init({ locateFile: (file) => (file.endsWith('.wasm') ? wasmUrl : file), }); ``` ```javascript title="webpack.config.js" module.exports = { module: { rules: [ { test: /\.wasm$/, type: 'asset/resource', }, ], }, resolve: { fallback: { fs: false, perf_hooks: false, os: false, path: false, worker_threads: false, crypto: false, stream: false, }, }, }; ``` Mark `@taucad/opencascade.js` as an external or exclude it from aggressive bundle inlining — the 12+ MB wasm must stay a separate fetch. ## Legacy bundlers [#legacy-bundlers] Create-React-App, `react-app-rewired`, and Webpack 4 are **not supported** in V3 docs. The upstream Docusaurus site preserved CRA recipes in git history at [`website/docs/02-getting-started/02-configure-bundler.md`](https://github.com/taucad/opencascade.js/blob/master/website/docs/02-getting-started/02-configure-bundler.md) (recoverable via `git show HEAD:website/docs/02-getting-started/02-configure-bundler.md` in this repo). Use Vite, Next 15, Bun, Node, Deno, or Webpack 5 instead. ## Common pitfalls [#common-pitfalls] * **`locateFile` is mandatory**. The single-file V3 build does not auto-discover its wasm sibling — every consumer must provide a URL. * **Don't bundle the wasm inline**. Bundling the 12+ MB binary as base64 explodes your JS payload and prevents the browser's wasm streaming compiler. * **Cache the `init` Promise**. Re-calling `init()` re-instantiates the wasm module — always memoize behind a singleton. ## Multi-threaded variant [#multi-threaded-variant] The pthread-enabled build ships under `@taucad/opencascade.js/multi` with wasm at `@taucad/opencascade.js/multi/wasm`. The `init` contract is identical — only the import path and wasm target change. **Browser prerequisite:** every page that loads the threaded wasm must send cross-origin isolation headers: ```http Cross-Origin-Opener-Policy: same-origin Cross-Origin-Embedder-Policy: require-corp ``` Without these headers, browsers refuse to expose `SharedArrayBuffer` and the wasm fails to instantiate. See the [multi-threaded build guide](/docs/package/guides/multi-threading) for benchmarks and when not to ship threaded. Run the following **once** after `await init(...)` in every recipe below — it matches the benchmark harness and is required for full speedup on mesh/boolean workloads: ```typescript oc.BOPAlgo_Options.SetParallelMode(true); oc.BRepMesh_IncrementalMesh.SetParallelDefault(true); const pool = oc.OSD_ThreadPool.DefaultPool(-1); pool.SetNbDefaultThreadsToLaunch(pool.NbThreads()); ``` ### Vite 6+ [#vite-6-1] ```typescript import init from '@taucad/opencascade.js/multi'; import wasmUrl from '@taucad/opencascade.js/multi/wasm?url'; const oc = await init({ locateFile: () => wasmUrl }); oc.BOPAlgo_Options.SetParallelMode(true); oc.BRepMesh_IncrementalMesh.SetParallelDefault(true); const pool = oc.OSD_ThreadPool.DefaultPool(-1); pool.SetNbDefaultThreadsToLaunch(pool.NbThreads()); ``` ### Next.js 15 (App Router) [#nextjs-15-app-router-1] Copy the MT wasm into `public/` at install time: ```javascript title="scripts/copy-wasm-multi.mjs" import { copyFile, mkdir } from 'node:fs/promises'; import { fileURLToPath } from 'node:url'; const src = fileURLToPath(import.meta.resolve('@taucad/opencascade.js/multi/wasm')); await mkdir('public', { recursive: true }); await copyFile(src, 'public/opencascade_full_multi.wasm'); ``` ```typescript title="lib/ocjs-init-multi.ts" 'use client'; import init from '@taucad/opencascade.js/multi'; let ocPromise: ReturnType | undefined; export const getOcMulti = () => (ocPromise ??= init({ locateFile: () => '/opencascade_full_multi.wasm' }).then((oc) => { oc.BOPAlgo_Options.SetParallelMode(true); oc.BRepMesh_IncrementalMesh.SetParallelDefault(true); const pool = oc.OSD_ThreadPool.DefaultPool(-1); pool.SetNbDefaultThreadsToLaunch(pool.NbThreads()); return oc; })); ``` ### Node (ESM) [#node-esm-1] ```typescript import { fileURLToPath } from 'node:url'; import { dirname, join } from 'node:path'; import init from '@taucad/opencascade.js/multi'; const WASM_DIR = dirname( fileURLToPath(import.meta.resolve('@taucad/opencascade.js/multi/wasm')), ); const oc = await init({ locateFile: (file: string) => join(WASM_DIR, file) }); oc.BOPAlgo_Options.SetParallelMode(true); oc.BRepMesh_IncrementalMesh.SetParallelDefault(true); const pool = oc.OSD_ThreadPool.DefaultPool(-1); pool.SetNbDefaultThreadsToLaunch(pool.NbThreads()); ``` Per-call overrides (`SetRunParallel(true)`, `BRepMesh_IncrementalMesh(..., isInParallel=true)`) remain available for granular opt-in. See [Multi-threaded build — Per-call activation](/docs/package/guides/multi-threading#per-call-activation--granular). --- # Debugging wasm exceptions URL: /docs/package/guides/debugging-wasm-exceptions When OCCT throws a `Standard_Failure` inside the wasm module, the value that reaches your JS try/catch is a `WebAssembly.Exception` object. Printing it with `console.error(error)` yields `[object WebAssembly.Exception]` — useless. `@taucad/opencascade.js@beta` exposes `getExceptionMessage(error)` to decode the C++ failure into a string. ## The pattern [#the-pattern] ```typescript import init, { getExceptionMessage } from '@taucad/opencascade.js'; const oc = await init({ locateFile }); try { using fillet = new oc.BRepFilletAPI_MakeFillet(badShape); fillet.Shape(); // may throw } catch (error: unknown) { if (error instanceof WebAssembly.Exception) { console.error('OCCT failure:', getExceptionMessage(error)); } else { throw error; } } ``` `getExceptionMessage` reaches into the wasm-side `Standard_Failure::GetMessageString()` and returns the text OCCT actually attached to the exception (e.g. `"BRepFilletAPI_MakeFillet: Empty argument"`). ## Why not just `error.message`? [#why-not-just-errormessage] `WebAssembly.Exception` is a JS host object — its `message` field is empty for C++-thrown values. The actual text lives in C++ memory, addressable only by walking back into the wasm module. ## Common OCCT exceptions [#common-occt-exceptions] | Message fragment | Likely cause | | ----------------------------------------------- | ----------------------------------------------------------------- | | `Standard_OutOfRange` | Index past the end of an `NCollection_*` container | | `Standard_NullObject` | Method called on a null `Handle` (forgot to check `.IsNull()`) | | `Standard_NoSuchObject` | Map lookup returned nothing | | `Standard_TypeMismatch` | Downcast to the wrong `TopoDS_*` subtype | | `BRepAlgoAPI_*: ...` | Boolean failure — usually self-intersecting input | | `BRepFilletAPI_MakeFillet: ...` | Fillet radius too large, or degenerate edge | | `STEPControl_Writer: write.step.schema not set` | Forgot `Interface_Static.SetIVal('write.step.schema', 5)` | ## Async-safe pattern [#async-safe-pattern] If your call site awaits between the `try` and the throw, wrap the offending synchronous block tightly: ```typescript const filletShape = await runOcctSync(() => { using fillet = new oc.BRepFilletAPI_MakeFillet(shape); for (const edge of edges) fillet.Add(3, edge); return fillet.Shape(); }); ``` Wide async try/catch boundaries swallow the `WebAssembly.Exception` type discriminator on some engines. ## Debug builds [#debug-builds] For deep debugging, build with `-g -fwasm-exceptions -O1` and load [Chrome's wasm DWARF debugger](https://developer.chrome.com/blog/wasm-debugging-2020). You get the original C++ frames in the call stack — invaluable when an OCCT exception originates ten frames deep in `BOPAlgo_PaveFiller`. --- # Export glTF / GLB URL: /docs/package/guides/export-gltf GLB is the binary single-file flavour of glTF and the format three.js, Babylon, Filament, and most realtime renderers consume natively. OCCT writes it via the `RWGltf_CafWriter` against an XCAF document. ## Minimal single-shape GLB [#minimal-single-shape-glb] ```typescript import type { TopoDS_Shape } from '@taucad/opencascade.js'; import { getOc } from './ocjs-init'; export const shapeToGlb = async (shape: TopoDS_Shape): Promise => { const oc = await getOc(); using docName = new oc.TCollection_ExtendedString('doc', true); const doc = new oc.TDocStd_Document(docName); const shapeTool = oc.XCAFDoc_DocumentTool.ShapeTool(doc.Main()).get(); shapeTool.AddShape(shape, false, false); using _mesh = new oc.BRepMesh_IncrementalMesh(shape, 0.1, false, 0.5, false); const path = `/out_${Date.now()}.glb`; using asciiPath = new oc.TCollection_AsciiString(path); using writer = new oc.RWGltf_CafWriter(asciiPath, true); // true = binary GLB using metadata = new oc.TColStd_IndexedDataMapOfStringString(); using progress = new oc.Message_ProgressRange(); writer.Perform(doc, metadata, progress); const raw = oc.FS.readFile(path) as Uint8Array; oc.FS.unlink(path); return new Uint8Array(raw); }; ``` ## Per-shape PBR material [#per-shape-pbr-material] Attach a `XCAFDoc_VisMaterial` to a shape label and the GLB writer emits a glTF `materials[]` entry with PBR base color, metalness, and roughness. ```typescript const shapeLabel = shapeTool.AddShape(shape, false, false); using matName = new oc.TCollection_AsciiString('Brass'); const vmTool = oc.XCAFDoc_DocumentTool.VisMaterialTool(doc.Main()).get(); using visMat = new oc.XCAFDoc_VisMaterial(); using pbr = new oc.XCAFDoc_VisMaterialPBR(); pbr.BaseColor = new oc.Quantity_ColorRGBA(0.85, 0.65, 0.13, 1); pbr.Metallic = 0.9; pbr.Roughness = 0.25; visMat.SetPbrMaterial(pbr); const matLabel = vmTool.AddMaterial(visMat, matName); vmTool.SetShapeMaterial(shapeLabel, matLabel); ``` ## Tessellation tolerances [#tessellation-tolerances] `BRepMesh_IncrementalMesh(shape, linearDeflection, isRelative, angularDeflection, inParallel)` | Param | Typical value | Effect | | ----------------- | ------------- | ---------------------------------------- | | linearDeflection | 0.05 – 0.5 mm | Max chord–surface distance | | isRelative | `false` | When `true`, deflection scales with bbox | | angularDeflection | 0.5 rad | Max angle between facets along a curve | | inParallel | `false` | Single-threaded in browser builds | Tighten linear deflection for small features (M3 threads need \~0.01 mm). OCCT caches triangulation on the shape; pass `decreate=true` as the third positional argument to force regeneration when you change tolerances. ## Coordinate system [#coordinate-system] GLB is **Y-up** by convention; OCCT is **Z-up**. `RWGltf_CafWriter` emits Z-up data and lets the consumer interpret it. Most three.js workflows pre-rotate the scene with `-Math.PI/2` around the X axis to align with glTF Y-up expectations. ## Validating the output [#validating-the-output] * [glTF Viewer (Don McCurdy)](https://gltf-viewer.donmccurdy.com/) — drag-drop validation. * [gltf-validator](https://github.com/KhronosGroup/glTF-Validator) — Node CLI for CI. * Open in Blender — full PBR roundtrip with materials. --- # Export STEP URL: /docs/package/guides/export-step STEP is the de-facto interchange format for parametric CAD. OCCT exposes two writers depending on whether you need an assembly tree. ## Single shape — STEPControl\_Writer [#single-shape--stepcontrol_writer] ```typescript import type { TopoDS_Shape } from '@taucad/opencascade.js'; import { getOc } from './ocjs-init'; export const shapeToStep = async (shape: TopoDS_Shape): Promise => { const oc = await getOc(); using writer = new oc.STEPControl_Writer(); using progress = new oc.Message_ProgressRange(); oc.Interface_Static.SetIVal('write.step.schema', 5); // AP214 const transferStatus = writer.Transfer( shape, oc.STEPControl_StepModelType.STEPControl_AsIs, true, progress, ); if (transferStatus !== oc.IFSelect_ReturnStatus.IFSelect_RetDone) { throw new Error('STEP Transfer failed'); } const path = `/out_${Date.now()}.step`; const writeStatus = writer.Write(path); if (writeStatus !== oc.IFSelect_ReturnStatus.IFSelect_RetDone) { throw new Error('STEP Write failed'); } const raw = oc.FS.readFile(path) as Uint8Array; oc.FS.unlink(path); return new Uint8Array(raw); }; ``` Two return-status checks are mandatory — OCCT silently produces a zero-byte file otherwise. ## Multi-shape assembly — STEPCAFControl\_Writer [#multi-shape-assembly--stepcafcontrol_writer] For assemblies with named parts, per-shape colors, or PBR materials, write the shape into an XCAF document first, then use `STEPCAFControl_Writer.Perform`. **Never** use the empty-filename `Transfer` overload — it silently activates multi-file mode and emits an empty STEP body. ```typescript import { getOc } from './ocjs-init'; export const assemblyToStep = async (shapes: Array<{ name: string; shape: TopoDS_Shape }>) => { const oc = await getOc(); using docName = new oc.TCollection_ExtendedString('doc', true); const doc = new oc.TDocStd_Document(docName); const shapeTool = oc.XCAFDoc_DocumentTool.ShapeTool(doc.Main()).get(); for (const { name, shape } of shapes) { const label = shapeTool.AddShape(shape, false, false); using nameStr = new oc.TCollection_ExtendedString(name, true); oc.TDataStd_Name.Set(label, nameStr); } using writer = new oc.STEPCAFControl_Writer(); using progress = new oc.Message_ProgressRange(); oc.Interface_Static.SetIVal('write.step.schema', 5); const path = `/asm_${Date.now()}.step`; const ok = writer.Perform(doc, path, progress); if (!ok) throw new Error('STEP assembly write failed'); const raw = oc.FS.readFile(path) as Uint8Array; oc.FS.unlink(path); return new Uint8Array(raw); }; ``` ## Schema selection [#schema-selection] | Code | Schema | Use when | | ---- | --------------------------- | ----------------------------------- | | 1 | AP203 (config control) | Legacy mech CAD; avoid for new work | | 4 | AP214 (auto industry) | Default — broad tool support | | 5 | AP214 (alt revision) | Recommended for new projects | | 6 | AP242 (managed model-based) | If you need PMI / PBR materials | AP242 emits the same geometry as AP214 plus product manufacturing information attached to the model. Most consumers (Fusion 360, SolidWorks, FreeCAD) auto-detect schema and treat 5 and 6 interchangeably for pure geometry. ## Bytes out, file in [#bytes-out-file-in] * **Browser**: `URL.createObjectURL(new Blob([bytes], { type: 'application/step' }))` and trigger a download via a hidden `` tag. * **Node**: `fs.writeFileSync('out.step', bytes)`. * **Cloud**: pipe directly to S3 / GCS as `application/octet-stream`. ## Common pitfalls [#common-pitfalls] * Check `IFSelect_RetDone` after **both** `Transfer` and `Write` — partial failures are silent otherwise. * Always `unlink` the MEMFS path after `FS.readFile` to free the wasm heap. * Copy bytes out of MEMFS (`new Uint8Array(raw)`) — the original view becomes detached after `unlink`. --- # Multi-threaded build URL: /docs/package/guides/multi-threading The npm package ships **two** pre-built variants: | Import | Binary | When to use | | ---------------------------------- | ----------------------------- | -------------------------------------------------------------------- | | `@taucad/opencascade.js` (default) | `opencascade_full.wasm` | Embeddable widgets, one-op-per-click UX, no COOP/COEP | | `@taucad/opencascade.js/multi` | `opencascade_full_multi.wasm` | Batch mesh/boolean, STEP→glTF pipelines, COOP/COEP-isolated surfaces | Load the threaded variant the same way as the default — import `@taucad/opencascade.js/multi` and resolve wasm through `@taucad/opencascade.js/multi/wasm`. See [Bundler & locateFile](/docs/package/guides/bundler-locatefile#multi-threaded-variant) for Vite, Next.js, and Node recipes. OCCT can drive multiple worker threads for mesh and boolean kernels once the pthread-enabled wasm is loaded. That requires hard browser prerequisites (`SharedArrayBuffer` + cross-origin isolation) in the browser; Node 22+ exposes `SharedArrayBuffer` without extra headers. ## When to consider it [#when-to-consider-it] | Operation | Speedup with 4 threads | Worth the COOP/COEP cost? | | ------------------------------------------------- | ---------------------: | -------------------------------- | | `BRepMesh_IncrementalMesh` on large assemblies | 2.5–3.5× | Yes, for visualisation pipelines | | `BRepAlgoAPI_*` booleans on hundreds of operands | 2–3× | Yes, for batch CAD operations | | `STEPControl_Reader.Transfer` of large STEP files | 1.5–2× | Marginal — IO usually dominates | | Single-shape build (box + fillet + export) | 1.0× | No | If your app does one boolean and one mesh per user interaction, single-threaded is fine. If you batch hundreds of operations, threading pays off. See [BENCHMARKS.md](https://github.com/taucad/opencascade.js/blob/main/BENCHMARKS.md) for ST vs MT numbers on a representative workload mix. ## Browser prerequisites [#browser-prerequisites] Pthread builds use `SharedArrayBuffer`, which browsers gate behind **cross-origin isolation**. Your server must send: ```http Cross-Origin-Opener-Policy: same-origin Cross-Origin-Embedder-Policy: require-corp ``` …on every page that imports the threaded wasm. Without these headers, browsers refuse to expose `SharedArrayBuffer` and the wasm fails to instantiate. Node 22+ exposes `SharedArrayBuffer` unconditionally — no headers needed. ## Triggering OCCT parallelism [#triggering-occt-parallelism] OCCT respects its own `OSD_Parallel` switches once threads exist. Two levels of activation are available — **per-call** (granular, opt-in at each site) and **global** (sets a process-wide default). ### Global activation — call once at startup [#global-activation--call-once-at-startup] The cleanest option: flip the OCCT-wide defaults once, then any subsequent call to a parallel-aware API inherits the toggle without an extra argument. ```typescript // Run once after `await init({...})`. oc.BOPAlgo_Options.SetParallelMode(true); // booleans oc.BRepMesh_IncrementalMesh.SetParallelDefault(true); // meshing // Right-size the OCCT thread pool to all logical CPUs. -1 means // "use NbLogicalProcessors". SetNbDefaultThreadsToLaunch caps the // fan-out of any single OCCT call; default is the pool size. const pool = oc.OSD_ThreadPool.DefaultPool(-1); pool.SetNbDefaultThreadsToLaunch(pool.NbThreads()); ``` After these four calls every `new BRepMesh_IncrementalMesh(shape, lin)` and every `BRepAlgoAPI_*` automatically fans out across the pool. **No per-call argument required.** Skipping the pool sizing leaves OCCT's lazy default pool smaller than the pre-spawned worker count baked into the binary (`PTHREAD_POOL_SIZE=navigator.hardwareConcurrency`) and caps speedup on mesh/boolean workloads. ### Per-call activation — granular [#per-call-activation--granular] If you want to keep the global default off and opt in selectively: ```typescript using mesh = new oc.BRepMesh_IncrementalMesh( shape, 0.1, // linear deflection false, // not relative 0.5, // angular deflection true, // inParallel — flips multi-threading on ); ``` ```typescript using bop = new oc.BRepAlgoAPI_BuilderAlgo(); bop.SetRunParallel(true); // ... AddArgument / AddTool / Build() ``` `SetRunParallel(false)` is the default for per-instance APIs. OCJS does not auto-enable it unless you flip the global default above. ### Other parallel-aware APIs [#other-parallel-aware-apis] Beyond mesh and boolean, the following APIs accept a parallel flag and benefit when the pool is sized correctly: | API | Activation | | ------------------------------------------------------------------------------------ | ---------------------------------------------------------------------- | | `BRepExtrema_DistShapeShape` | `.SetMultiThread(true)` | | `BRepCheck_Analyzer` | ctor `(shape, /*isParallel*/ true)` | | `RWGltf_CafReader` | `.SetParallel(true)` (glTF **read**) | | `RWGltf_CafWriter` | `.SetParallel(true)` (glTF **write**) | | `BVH_Builder` | `.SetParallel(true)` | | `BRepLib_ValidateEdge`, `BRepLib_CheckCurveOnSurface`, `GeomLib_CheckCurveOnSurface` | `.SetParallel(true)` — used transitively by `BRepCheck_Analyzer` | | `BRepFill_AdvancedEvolved` | already parallel by default — call `SetParallelMode(false)` to disable | **STEP/IGES readers (`STEPControl_Reader`, `IGESControl_Reader`) and `BRepBuilderAPI_Sewing` are sequential** in current OCCT — there is no parallel flag to flip. ## Costs [#costs] * **COOP/COEP headers** lock you out of third-party iframes that don't opt in (e.g. embedded Stripe checkout, some auth providers). Subdomain-isolate your CAD surface if that matters. * **Thread spawn time** adds \~50–200 ms to init, scaling with pool size. Memoise the `init()` Promise. * **Memory** scales with `PTHREAD_POOL_SIZE × STACK_SIZE`. With the shipped `navigator.hardwareConcurrency` sizing, a 12-core box reserves \~96 MB of stack on top of `INITIAL_MEMORY`. * **Debuggability** drops — `console.log` from worker threads doesn't always reach the main thread. ## Performance notes [#performance-notes] Pthread + `-sALLOW_MEMORY_GROWTH=1` carries a documented Emscripten performance penalty (the `-Wpthreads-mem-growth` advisory at link time): every JS↔WASM memory boundary crossing has to re-resolve `HEAP*` views after a potential `memory.grow`, and worker-thread access to `HEAPU8`/ `HEAPF32` is gated on the views being up-to-date in each worker's context. The shipped binaries already mitigate the worst of this: * **mimalloc** is wired into the WASM allocator so per-thread allocation bypasses the global malloc contention point. The `mallinfo` undefined symbol you'll see in custom-build link logs is mimalloc's debug-stats reporter — `wasm-ld` emits a single `-Wjs-compiler` warning for it (the link permits it via the broad `-Wl,--allow-undefined` flag in the shipped `emccFlags`) and the symbol is dead-code-eliminated at module load. The warning is informational; no runtime call ever resolves to it. * **Hoist `HEAP*` references out of hot loops** when calling OCJS from TypeScript. The Emscripten glue re-fetches `Module.HEAPU8` etc. on every access; pulling them into a local once per batch avoids the dispatch penalty: ```typescript // Slow — re-resolves HEAPU8 every iteration. for (let i = 0; i < n; i++) { Module.HEAPU8[buf + i] = bytes[i]; } // Fast — single resolve, then tight loop. const heap = Module.HEAPU8; for (let i = 0; i < n; i++) { heap[buf + i] = bytes[i]; } ``` ### `toResizableBuffer()` support matrix (May 2026) [#toresizablebuffer-support-matrix-may-2026] The newer [`WebAssembly.Memory.prototype.toResizableBuffer()`](https://developer.mozilla.org/en-US/docs/WebAssembly/Reference/JavaScript_interface/Memory) API lets a pthread-enabled binary avoid the HEAP-view re-fetch dance entirely by returning a `SharedArrayBuffer` that automatically tracks `memory.grow`. The shipped `@taucad/opencascade.js/multi` binary keeps `-sGROWABLE_ARRAYBUFFERS=0` (the default) because the runtime matrix is not yet universal: | Runtime | Supports `toResizableBuffer()` | | ---------------------------- | -------------------------------------------------------------------- | | Chrome 144+ / Edge 144+ | Yes | | Firefox 145+ | Yes | | Safari 26.2+ (desktop + iOS) | Yes | | Node.js 22 / 24 | **No** — `TypeError: wasmMemory.toResizableBuffer is not a function` | | Bun (all current) | **No** | | Deno (all current) | **No** | | Samsung Internet | **No** (as of May 2026) | The default `full_multi.yml` therefore keeps `-sGROWABLE_ARRAYBUFFERS=0` so the binary stays compatible with Node-based test runners, Bun, Deno, and the legacy mobile browser. A **browser-only opt-in** variant ships via [`full_multi_browser.yml`](/docs/toolchain/guides/multi-threading#browser-only-mt-build) for deployments that have audited their runtime matrix and want the no-resize-dance fast path on the supported browsers. For most consumers the cost is invisible — OCCT's per-call cost on mesh/boolean dominates the HEAP-view re-fetch by 2-3 orders of magnitude. Reach for `full_multi_browser.yml` only when you've profiled and the JS↔WASM boundary is on the hot path. ## When NOT to ship threaded [#when-not-to-ship-threaded] * You can't (or won't) set COOP/COEP headers — most CDNs/marketing sites can't. * Your workload is one shape per second — Amdahl's law eats the speedup. * Your target users include Safari ≤16 — older Safari refused `SharedArrayBuffer` in cross-origin-isolated contexts. For most consumers the single-threaded default wins on simplicity. Reach for `@taucad/opencascade.js/multi` only when you have measured the bottleneck. ## Custom builds [#custom-builds] Need a smaller threaded binary (trimmed symbol list) or custom Emscripten flags? See [Toolchain — Custom multi-threaded build](/docs/toolchain/guides/multi-threading) for the YAML recipe and `PTHREAD_POOL_SIZE` rationale. --- # Render with three.js URL: /docs/package/guides/render-with-three-js `@taucad/opencascade.js` produces GLB bytes through OCCT's XCAF + `RWGltf_CafWriter` pipeline. three.js consumes GLB via `GLTFLoader`. The whole flow is in-browser — no server round-trip. ## End-to-end snippet [#end-to-end-snippet] ```typescript title="src/render.ts" import * as THREE from 'three'; import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'; import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'; export const renderGlb = (canvas: HTMLCanvasElement, glb: Uint8Array): (() => void) => { const renderer = new THREE.WebGLRenderer({ canvas, antialias: true }); renderer.setSize(canvas.clientWidth, canvas.clientHeight, false); renderer.setPixelRatio(globalThis.devicePixelRatio); const scene = new THREE.Scene(); scene.background = new THREE.Color(0x111111); const camera = new THREE.PerspectiveCamera(45, canvas.clientWidth / canvas.clientHeight, 0.1, 5000); camera.position.set(100, 100, 100); scene.add(new THREE.AmbientLight(0xffffff, 0.5)); const dir = new THREE.DirectionalLight(0xffffff, 1); dir.position.set(1, 1, 1); scene.add(dir); const controls = new OrbitControls(camera, renderer.domElement); controls.target.set(0, 0, 0); new GLTFLoader().parse(glb.buffer, '', (gltf) => scene.add(gltf.scene)); renderer.setAnimationLoop(() => { controls.update(); renderer.render(scene, camera); }); return () => renderer.dispose(); }; ``` ## Why XCAF (and not raw triangle arrays) [#why-xcaf-and-not-raw-triangle-arrays] OCCT can hand you raw `Poly_Triangulation` arrays per face, but the XCAF + GLB path: * preserves per-face colors and PBR materials, * writes valid glTF metadata (asset version, extensions used), * handles multi-shape assemblies as separate primitives, * emits indexed buffers so three.js doesn't need to dedupe vertices. See [Export glTF / GLB](/docs/package/guides/export-gltf) for the full pipeline including PBR materials. ## Common rendering issues [#common-rendering-issues] | Symptom | Likely cause | Fix | | ---------------------------------- | ------------------------------ | ----------------------------------------------------------------------- | | Faceted curves (no smooth shading) | Mesh deflection too coarse | Tighten `BRepMesh_IncrementalMesh` linear deflection (e.g. 0.05) | | Black model | No lights | Add ambient + directional light; check `dir.position` is non-zero | | Model offset from origin | Shape has a transform baked in | Verify `TopLoc_Location` or apply `shape.Moved(loc, false)` | | GLB loads but invisible | Camera inside model | `camera.position.set(100, 100, 100)` and `controls.target.set(0, 0, 0)` | ## Helper libraries [#helper-libraries] For batch shape conversion (multi-shape assemblies, edges-as-fat-lines, hover-highlight), [`replicad-threejs-helper`](https://github.com/sgenoud/replicad) wraps the common patterns and works with `@taucad/opencascade.js@beta` directly. --- # Visualize shape helper URL: /docs/package/guides/visualize-shape-helper Every example in this docs site can inline the XCAF → mesh → GLB → blob URL pipeline. This page documents the canonical helper so you can import it once instead of copying the boilerplate into every script. ## `visualizeDoc` [#visualizedoc] Build a GLB blob URL from an XCAF document that already contains tessellated shapes: ```typescript title="lib/visualize.ts" import type { OpenCascadeInstance } from '@taucad/opencascade.js'; export const visualizeDoc = async ( oc: OpenCascadeInstance, doc: InstanceType, ): Promise => { using writer = new oc.RWGltf_CafWriter(new oc.TCollection_AsciiString_2('out.glb'), true); writer.Perform(doc, new oc.TColStd_IndexedDataMapOfStringString(), new oc.Message_ProgressRange()); const fileName = 'out.glb'; const buffer = oc.FS.readFile(fileName, { encoding: 'binary' }); oc.FS.unlink(fileName); const blob = new Blob([buffer], { type: 'model/gltf-binary' }); return URL.createObjectURL(blob); }; ``` ## `visualizeShapes` [#visualizeshapes] Convenience wrapper — accepts one or more `TopoDS_Shape` values, builds the XCAF document, meshes, and returns a GLB blob URL: ```typescript title="lib/visualize.ts" import type { OpenCascadeInstance, TopoDS_Shape } from '@taucad/opencascade.js'; export const visualizeShapes = async ( oc: OpenCascadeInstance, ...shapes: TopoDS_Shape[] ): Promise => { using doc = new oc.TDocStd_Document(new oc.TCollection_ExtendedString_1()); const shapeTool = oc.XCAFDoc_DocumentTool.ShapeTool(doc.Main()).get(); for (const shape of shapes) { using mesh = new oc.BRepMesh_IncrementalMesh(shape, 0.1, false, 0.1, false); const newShape = shapeTool.NewShape(); shapeTool.SetShape(newShape, shape); } return visualizeDoc(oc, doc); }; ``` ## When to inline vs import [#when-to-inline-vs-import] * **First read**: keep the pipeline inline in tutorials so every step is visible. * **Production code**: import from a single `lib/visualize.ts` module. * **Multi-material assemblies**: build the XCAF document yourself (see [Boolean logo](/docs/package/examples/boolean-logo) for per-subset PBR assignment). See also: [Render with three.js](/docs/package/guides/render-with-three-js). --- # Playground URL: /docs/package/playground The **Playground** replaces the legacy Docusaurus live `js ocjs` code blocks with a Fumadocs `` component backed by [`@taucad/runtime`](https://github.com/taucad/tau). That package is not yet published as a standalone npm release. Until it ships, examples remain static code blocks. Track progress in the [OpenCascade.js repo](https://github.com/taucad/opencascade.js). When the Playground lands, every example under [Examples](/docs/package/examples/boolean-logo) will expose live previews with parameter sliders — the same UX as the original `ocjs.org` site, but built on `@taucad/runtime` instead of a bespoke Comlink worker. --- # Bindgen pipeline URL: /docs/toolchain/concepts/bindgen-pipeline **Maintainer track.** Read this if you rebuild OCJS from source or extend it with custom C++. If you `pnpm add opencascade.js` and call the published wasm from JS, you can skip this page — the consumer-facing rules live in [Calling OCCT from JS](/docs/toolchain/concepts/calling-occt-from-js) and [Return shapes](/docs/toolchain/concepts/return-shapes). The OCJS bindings generator is a Python orchestrator over libclang and emscripten's embind. This page maps the stages and the artifacts they emit. ## Stage diagram [#stage-diagram] ```mermaid `flowchart TB subgraph "Stage 1 — bindgen (libclang)" A1[OCCT headers] --> A2[libclang AST walk] A2 --> A3[bindings.py / generateBindings.py] A3 --> A4["build/bindings///

/.hxx"] A3 --> A5["build/bindings///

/.d.ts.json"] end subgraph "Stage 2 — compile (em++)" A4 --> B1[em++ -c per class] B1 --> B2["build/bindings/.../.o (cached)"] end subgraph "Stage 3 — link (wasm-ld)" B2 --> C1[wasm-ld + emcc glue] C1 --> C2[dist/opencascade_full.wasm] C1 --> C3[dist/opencascade_full.js] A5 --> C4[dist/opencascade_full.d.ts] end ` ``` ## Stage 1 — bindgen [#stage-1--bindgen] `scripts/build-wasm.sh bindings` invokes the Python bindgen which: 1. Walks every header listed in the active YAML via libclang. 2. Builds an AST per class, applies the `bindgen-filters.yaml` exclusions, and classifies each member as constructor / static method / instance method / property. 3. Resolves typedefs (including the OCCT `occ::handle<>` family) against a shared cache. 4. Emits one `.hxx` per class with `EMSCRIPTEN_BINDINGS(...)` registrations and one `.d.ts.json` shard per class describing the TypeScript shape. The bindgen is deterministic: same headers + same filter YAML = identical output bytes. ## Stage 2 — compile [#stage-2--compile] `em++` compiles each `.hxx` to a `.o`. The cache key is the `.hxx` content hash plus the compile-time flag fingerprint. Cached `.o` files survive across consumer builds — that's the speedup that makes a custom-trimmed build take 60 seconds instead of 30 minutes. ## Stage 3 — link [#stage-3--link] `emcc` links the selected `.o` files (per the consumer YAML's `bindings:` list) against the pre-compiled OCCT library archives in `dist/libs/`. The output: * `.wasm` — the wasm binary. * `.js` — the emscripten loader glue. * `.d.ts` — the merged TypeScript declarations (from `.d.ts.json` shards filtered to the bound symbol set). * `.build-manifest.json` — symbol coverage report (requested vs compiled, wasm bytes, validation flags). ## How custom C++ enters the pipeline [#how-custom-c-enters-the-pipeline] `additionalCppCode` + `additionalCppFiles` get concatenated into one TU which flows through a smaller variant of stages 1+2 — bindgen discovers Handle/NCollection references, em++ compiles, the result links into the final wasm alongside the auto-generated bindings. `additionalBindCode` skips bindgen entirely — it's a literal `.cpp` snippet that gets compiled with `` already included. ## When the pipeline fails [#when-the-pipeline-fails] | Symptom | Stage | Fix | | ------------------------------------------------------- | ----- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `libclang: cannot find ` | 1 | OCCT headers not mounted into the Docker image | | `undefined symbol: _ZN10TopoDS_...` | 3 | Remove a `bindings:` entry whose `.o` no longer exists, or the class transitively needs another class you trimmed | | `BindingError: invalid type` at runtime | 3 | Duplicate `EMSCRIPTEN_BINDINGS()` group across `additionalBindCode` and a generated `.hxx` | | Codegen emits `any` for a known type | 1 | The link step always prints a triage summary to stderr when this happens. The build still proceeds by default; set `OCJS_STRICT_TYPES=1` in CI to fail the build instead of shipping a poisoned `.d.ts`. File an issue with the printed triage summary. | | Codegen emits `unknown` / surfaces an unbound reference | 1 | Same gate as above. Warning printed by default; `OCJS_STRICT_TYPES=1` escalates to a hard failure for CI consumers. | ## What the pipeline produces — JS-side contract [#what-the-pipeline-produces--js-side-contract] The artifacts above are an implementation detail. The contract those artifacts expose to JS consumers — overload-dispatched calls, in-place class outputs, `returnValue` envelopes, Handle elision — lives in two consumer-facing concept pages: * [Calling OCCT from JS](/docs/toolchain/concepts/calling-occt-from-js) — overload dispatch, enums, defaults, and the `TopoDS` downcast bridge. * [Return shapes](/docs/toolchain/concepts/return-shapes) — class outputs, envelopes, `returnValue`, and Handle elision. ## Docker stage mapping [#docker-stage-mapping] The pipeline stages map directly to the published Docker stages described in [Docker image](/docs/toolchain/reference/docker-image#image-stages): | Pipeline stage | Docker stage | Published tag | | ---------------------- | -------------------------------------------- | ------------------------------------- | | 1 — discover | `bindgen-base` | `:bindgen-base` | | 2 — emit (TUs + .d.ts) | `bindgen-base` | `:bindgen-base` | | 3 — compile + link | `compiled-{threading}` + `final-{threading}` | `:single-threaded`, `:multi-threaded` | `:bindgen-base` is both a build stage and a published image — it carries the patched OCCT tree, the PCH, and the `.d.ts.json` index but not the pre-compiled `.o` files. Custom-bindings consumers pull `:bindgen-base`, re-run `generate` against their own YAML, and compile from there. ## Related [#related] * [Extend with C++](/docs/toolchain/guides/extend-with-cpp) — how to inject custom C++ into the pipeline. * [Trim symbols](/docs/toolchain/guides/trim-symbols) — controlling which classes survive into stage 3. * [YAML schema](/docs/toolchain/reference/yaml-schema) — every YAML key the pipeline consumes. --- # Two-channel config model URL: /docs/toolchain/concepts/two-channel-config-model **Maintainer track.** Skip this page if you consume the published npm package — every shipped build already pins the right channel-1 flags. The channels matter when you rebuild OCJS from source. OpenCascade.js exposes two configuration channels with different lifecycles and different scope. Treat them as orthogonal — confusion between the two is the most common cause of build errors. ## Channel 1 — compile-time `OCJS_*` env vars [#channel-1--compile-time-ocjs_-env-vars] Set at the **bindgen + C++ compile** stage. Bake into every `.o` file the final wasm is linked from. | Variable | Effect | | ------------------------- | ----------------------------------------------------------------------------------------------------------------------------- | | `OCJS_EXCEPTIONS=1` | Compile with `-fwasm-exceptions` everywhere | | `OCJS_SIMD=1` | Compile with `-msimd128` | | `OCJS_RELAXED_SIMD=1` | Additionally emit `-mrelaxed-simd` (Chrome/Firefox only) | | `OCJS_BIGINT=1` | Compile with `-sWASM_BIGINT` | | `OCJS_EVAL_CTORS_LEVEL=2` | `-sEVAL_CTORS=2` | | `OCJS_STRICT_TYPES=1` | Escalate missing-typedef warning to a build failure (default `0`: warn-only — the triage summary is always printed to stderr) | [^lto]: `OCJS_LTO` exists but is intentionally off in every shipped preset. The workspace benchmarks showed LTO increasing the wasm size for OCJS's object distribution, so it stays disabled. Flip it on locally only if you're benchmarking a specific bindings trim. These flags pin into a **build-flags manifest** alongside each cached `.o`. Mixing builds with mismatched flags fails-loud at link time — never silent ABI breakage. ## Channel 2 — link-time `emccFlags` [#channel-2--link-time-emccflags] Set per-consumer-build in your YAML. Apply only to the final `emcc` link step. ```yaml mainBuild: emccFlags: - -O3 - -sMODULARIZE=1 - -sEXPORT_ES6=1 - -sALLOW_MEMORY_GROWTH=1 ``` These flags control loader shape (ESM vs CommonJS), memory limits, and environment detection. They cannot retroactively change the exception model or SIMD configuration the `.o` files were compiled with. ## Why the split? [#why-the-split] The bindgen produces \~4,400 `.o` files in the maintainer Docker pipeline. Caching them across consumer builds turns a 30-minute full build into a 60-second link. The cache key must be deterministic — that's what the compile-time channel fingerprint guards. If consumers could change compile-time flags via YAML, the cache invariant would break: a downstream `-fexceptions` request would silently rebuild every class, defeating the cache. Splitting the channels makes the contract explicit. ## When you actually need to change channel 1 [#when-you-actually-need-to-change-channel-1] Almost never as a consumer. The published build is the named `single-threaded` preset from `build-configs/configurations.json`, which pins: * `OCJS_EXCEPTIONS=1` + `OCJS_EH_MODE=wasm` (native wasm exceptions) * `OCJS_SIMD=1` (baseline SIMD, Safari-compatible) * `OCJS_BIGINT=1` (no i64 legalisation) * `OCJS_EVAL_CTORS=true` + `OCJS_EVAL_CTORS_LEVEL=2` * `OCJS_CLOSURE=true` + `OCJS_CONVERGE=true` * `THREADING=single-threaded` Presets are addressed by name, not by raw env vars — see [Named compile-time configurations](/docs/toolchain/reference/configurations) for the full list (`single-threaded`, `single-threaded-smallest`, `multi-threaded`, `debug`). The combinations not yet pre-built are exotic — `OCJS_RELAXED_SIMD=1` for Chrome-only deploys, custom allocator pairings, non-default WASM-opt budgets. To get one, fork the Docker image build pipeline and rebuild from source. ## Diagnostic checklist [#diagnostic-checklist] If a build acts strangely: 1. Confirm channel-1 fingerprint is what you expect. The full flag set is recorded in the in-repo build manifest (regenerated by every `bindings` stage): ```bash cat build/build-flags.json ``` For the **published** tarball, the consumer-facing equivalent is `dist/opencascade_full.provenance.json`, which captures the active preset, compile flags, and commit SHA the wasm was built from. 2. Confirm channel-2 flags in the YAML actually made it into the wasm: ```bash strings dist/my-build.wasm | grep -E 'STACK_SIZE|MAXIMUM_MEMORY' ``` 3. Recompare against a known-good cached build — drift here points at channel-1 cache poisoning (rebuild the deps layer). --- # Quickstart — Docker URL: /docs/toolchain/getting-started/quick-start-docker For when you need a custom-trimmed `opencascade.wasm` without setting up emsdk, libclang, and Python locally. ## 1. Pull the image [#1-pull-the-image] ```bash docker pull ghcr.io/taucad/opencascade.js:single-threaded ``` The `:single-threaded` tag carries pre-compiled OCCT static libraries so your build only pays for the link step. For threaded builds (requires COOP/COEP on consumer pages), pull `:multi-threaded` instead. For custom-bindings work that needs the pre-PCH/generate state but not the pre-compiled libraries, see `:bindgen-base` in [Reference → Docker image](/docs/toolchain/reference/docker-image). ### Supported platforms [#supported-platforms] Release tags (`:single-threaded`, `:multi-threaded`, `:bindgen-base`, versioned variants) ship as **multi-architecture manifest lists** — `linux/amd64` and `linux/arm64` — so Apple Silicon and ARM Linux hosts pull the native arch automatically. No `--platform` flag required. Branch tags (`:branch-`, `:multi-threaded-branch-`, `:bindgen-base-branch-`) are **`linux/amd64` only** by design — branch publishes need to be fast for reviewers, and the per-arch image build cost is the dominant factor. ARM hosts pulling a branch tag will run under Rosetta / QEMU. Use a release tag whenever native performance matters. ## 2. Write a build YAML [#2-write-a-build-yaml] Create `mybuild.yml` next to your project — list the OCCT symbols you actually use. See [Trim symbols](/docs/toolchain/guides/trim-symbols) for a worked example and [YAML schema](/docs/toolchain/reference/yaml-schema) for the full schema. ```yaml title="mybuild.yml" mainBuild: name: my-occt bindings: - symbol: package glob: 'gp' - symbol: package glob: 'BRepPrim*' - symbol: package glob: 'BRepAlgoAPI' - symbol: package glob: 'BRepBuilderAPI' - symbol: package glob: 'BRepFilletAPI' - symbol: package glob: 'TopoDS' - symbol: package glob: 'TopExp' - symbol: package glob: 'STEPControl' - symbol: package glob: 'RWGltf*' emccFlags: - -O3 - -fwasm-exceptions - -sMODULARIZE=1 - -sEXPORT_ES6=1 - -sEXPORT_NAME=initOpenCascade ``` ## 3. Run the image [#3-run-the-image] ```bash docker run --rm \ -v "$(pwd):/src" \ -u "$(id -u):$(id -g)" \ ghcr.io/taucad/opencascade.js:single-threaded \ link mybuild.yml ``` `link` is the end-to-end command. Inside the container Nx's `dependsOn` graph walks every upstream step (apply-patches → pch → generate → compile-bindings → compile-sources → link) with cache reuse, so a fresh container performs a full build and cached re-runs replay only the link step. Output lands next to `mybuild.yml`: * `my-occt.wasm` — the trimmed wasm binary * `my-occt.js` — the emscripten loader * `my-occt.d.ts` — generated TypeScript declarations * `my-occt.build-manifest.json` — symbol coverage and size report ## 4. Wire the build output [#4-wire-the-build-output] ```typescript import init from './my-occt.js'; import wasmUrl from './my-occt.wasm?url'; const oc = await init({ locateFile: () => wasmUrl }); ``` ## 5. Pin by manifest-list digest in production [#5-pin-by-manifest-list-digest-in-production] Tags can be re-pointed; digests cannot. Resolve the manifest-list digest once and pin it in CI so every reproducer of your build pulls the exact same multi-arch image set: ```bash docker buildx imagetools inspect ghcr.io/taucad/opencascade.js:single-threaded \ --format '{{json .Manifest.Digest}}' # → "sha256:abc123…" # Pin in CI / docker-compose / Dockerfile: ghcr.io/taucad/opencascade.js@sha256:abc123… ``` The manifest-list digest covers both `linux/amd64` and `linux/arm64` sub-images — pulling by digest from any arch resolves to the same deterministic build. See [Reproducible CI](/docs/toolchain/guides/reproducible-ci) for the full workflow. ## 6. Verify the supply-chain signature (optional) [#6-verify-the-supply-chain-signature-optional] Every published image is signed with [cosign](https://github.com/sigstore/cosign) via OIDC keyless signing — no rotating private keys, signatures published to the Sigstore Rekor transparency log. To verify before pulling: ```bash cosign verify ghcr.io/taucad/opencascade.js:single-threaded \ --certificate-identity-regexp 'https://github.com/taucad/opencascade\.js/\.github/workflows/docker\.yml@.*' \ --certificate-oidc-issuer https://token.actions.githubusercontent.com ``` A successful verification confirms the image was built by the `taucad/opencascade.js` GitHub Actions workflow and has not been tampered with since publication. ## Where the time goes [#where-the-time-goes] Typical wall-clock for a 22-50 symbol trim on the GitHub Actions `ubuntu-latest` runner (the e2e gate budget; M1 Pro local times are 30-50% faster): | Step | Warm cache | Cold cache | | -------------------------------------- | ------------: | ------------: | | Image pull (compressed \~1.3 GB) | 0 s | 30-60 s | | `generate` against your YAML | 5-15 s | 5-15 s | | `compile-bindings` (cached `.o` files) | 0 s | 1-3 min | | `compile-sources` (cached OCCT `.a`) | 0 s | 5-10 min | | Link against precompiled OCCT objects | 30-90 s | 30-90 s | | **Total** | **\~1-2 min** | **\~3-5 min** | The CI e2e gate budgets a hard 5-minute warm-link ceiling and tags get published only if both `:single-threaded` and `:multi-threaded` clear that budget on both architectures. Cold-cache runs (first pull on a new machine) add the image pull time; subsequent runs on the same machine reuse the pulled image and Nx cache. Full surface builds (no symbol filter, i.e. `build-configs/full.yml`) take 25-40 minutes — they're for the maintainer pipeline, not consumer machines. --- # Custom emcc flags URL: /docs/toolchain/guides/custom-emcc-flags Once your YAML symbol list is right, the second knob is `emccFlags`. Use it to tune optimisation level, exception model, SIMD, and target environments. ## Recommended baseline [#recommended-baseline] ```yaml mainBuild: emccFlags: - -O3 - -fwasm-exceptions # native wasm exceptions; faster than JS exceptions - -msimd128 # baseline SIMD (Safari + everyone else) - -sWASM_BIGINT # i64 stays as BigInt, no legalisation glue - -sEVAL_CTORS=2 # static-init evaluation at build time - -sMODULARIZE=1 # init() returns Promise - -sEXPORT_ES6=1 # ESM output - -sENVIRONMENT=web,worker,node - -sALLOW_MEMORY_GROWTH=1 - -sMAXIMUM_MEMORY=4GB # wasm32 ceiling ``` ## Flag-by-flag rationale [#flag-by-flag-rationale] | Flag | Why | | ------------------------------- | ---------------------------------------------------------------------------------------------------------------------- | | `-O3` | Full LLVM optimisation. `-Os` is \~5% smaller, \~10% slower at runtime. `-O0` for debugging only. | | `-fwasm-exceptions` | Uses Wasm `try_table`. \~5–10% smaller and faster than `-fexceptions` (JS-based) when supported (every modern engine). | | `-msimd128` | Baseline SIMD. \~15% perf win on mesh and boolean kernels. Safari-compatible. | | `-mrelaxed-simd` | Chrome/Firefox-only additional SIMD ops. Adds another \~5% perf but breaks Safari — only ship if you can fall back. | | `-sWASM_BIGINT` | Removes the i64↔i32-pair shim. Saves \~30 KB JS glue. | | `-sEVAL_CTORS=2` | Runs static initialisers at build time. Smaller payload + faster startup. | | `-sMODULARIZE=1 -sEXPORT_ES6=1` | Required for `import init from '...'`. | | `-sENVIRONMENT=web,worker,node` | Strips dead env detection. Without this the runtime probes for `process`/`window`/`importScripts`. | | `-sALLOW_MEMORY_GROWTH=1` | Required for any non-trivial geometry — initial 16 MB heap is not enough. | | `-sMAXIMUM_MEMORY=4GB` | wasm32 hard ceiling (2³² bytes). | ## Trade-offs [#trade-offs] ### Wasm exceptions vs JS exceptions [#wasm-exceptions-vs-js-exceptions] `-fwasm-exceptions` requires that **all** `.o` files AND the linker use the flag consistently. Mixed builds produce `__cpp_exception` as an unresolved import at link time. The default OCJS build sets `OCJS_EXCEPTIONS=1` everywhere; only override if you have a very narrow constraint (e.g. a wasm engine without `try_table` support). ### SIMD: baseline vs relaxed [#simd-baseline-vs-relaxed] Browsers diverged. Baseline `-msimd128` is universal. Relaxed-SIMD (`-mrelaxed-simd`) is Chrome + Firefox at the time of writing — Safari (as of 26.x) refuses to parse the relaxed opcodes and the wasm module fails to instantiate. Ship relaxed-SIMD only if you maintain a fallback baseline build. ### EVAL\_CTORS levels [#eval_ctors-levels] | Level | Behaviour | | ----- | --------------------------------------------------- | | 0 | Off — every static init runs at startup | | 1 | Eval ctors with safe side effects | | 2 | Recommended — full ctor evaluation, requires `-O2`+ | `EVAL_CTORS=2` is the OCJS shipped default and the right answer almost always. ## What you can NOT change at link time [#what-you-can-not-change-at-link-time] The wasm bitwidth (`wasm32` vs `wasm64`), the C++ stdlib version, the OCCT commit pin, and the libclang version are all baked in during the bindgen pipeline that produces the published Docker image. Customise via a fork of the Docker image if you need any of those. --- # Derive a C++ class in JavaScript URL: /docs/toolchain/guides/derive-cpp-class-in-js OpenCascade reports progress and supports user-initiated cancellation through `Message_ProgressIndicator`. Because OCCT requires a **derived class** with overridden virtual methods, using progress callbacks in OpenCascade.js requires a **custom WASM build** that exposes `allow_subclass<>` bindings. This guide consolidates the legacy three-page `progress-indicators-user-break` series into a single V3 tutorial. ## Overview [#overview] 1. Add a C++ bridge struct and `EMSCRIPTEN_WRAPPER` in your custom build YAML. 2. Register `allow_subclass<>` in `additionalBindCode`. 3. Derive the class in JavaScript with `.extend()` and pass `Start()` into long-running algorithms like `BRepAlgoAPI_Fuse`. Three methods matter: | Method | Required | Purpose | | ----------- | -------- | ------------------------------------------------------ | | `Show` | Yes | Called on every progress update (pure virtual in OCCT) | | `UserBreak` | Optional | Return `true` to cancel the current operation | | `Reset` | Optional | Called when a new long-running process starts | ## Step 1 — Custom build bindings [#step-1--custom-build-bindings] Create `wrappers/progress-indicator-js.cpp`: ```cpp title="wrappers/progress-indicator-js.cpp" #include #include #include using namespace emscripten; struct Message_ProgressIndicator_JS : public Message_ProgressIndicator { using Message_ProgressIndicator::Show; using Message_ProgressIndicator::UserBreak; using Message_ProgressIndicator::Reset; }; struct Message_ProgressIndicator_JSWrapper : public wrapper { EMSCRIPTEN_WRAPPER(Message_ProgressIndicator_JSWrapper); void Show(const Message_ProgressScope& theScope, const Standard_Boolean isForce) { val valTheScope = val::object(); valTheScope.set("current", &theScope); return call("Show", valTheScope, isForce); } Standard_Boolean UserBreak() { return call("UserBreak"); } void Reset() { return call("Reset"); } }; ``` Add to your build YAML: ```yaml title="build-configs/with-progress-callback.yml" additionalCppFiles: - wrappers/progress-indicator-js.cpp mainBuild: bindings: - symbol: Message_ProgressIndicator - symbol: Message_ProgressScope - symbol: Message_ProgressRange - symbol: BRepAlgoAPI_Fuse - symbol: BRepPrimAPI_MakeBox - symbol: gp_Pnt additionalBindCode: | EMSCRIPTEN_BINDINGS(progress_indicator_js) { class_>( "Message_ProgressIndicator_JS") .function("Show", &Message_ProgressIndicator_JS::Show, pure_virtual()) .function("UserBreak", optional_override([](Message_ProgressIndicator_JS& self) { return self.Message_ProgressIndicator_JS::UserBreak(); })) .function("Reset", optional_override([](Message_ProgressIndicator_JS& self) { return self.Message_ProgressIndicator_JS::Reset(); })) .allow_subclass("Message_ProgressIndicator_JSWrapper"); } ``` Build with the [Toolchain quickstart](/docs/toolchain/getting-started/quick-start-docker). ## Step 2 — Derive in JavaScript [#step-2--derive-in-javascript] ```typescript title="examples/progress-indicator.ts" import { getOc } from './ocjs-init'; let shouldCancel = false; export const runFuseWithProgress = async (): Promise => { const oc = await getOc(); const MyProgress = oc.Message_ProgressIndicator_JS.extend('Message_ProgressIndicator_JS', { Show(_scope, _isForce) { console.log('progress', this.GetPosition()); }, UserBreak() { return shouldCancel; }, }); using p = new MyProgress(); using box1 = new oc.BRepPrimAPI_MakeBox(new oc.gp_Pnt(0, 0, 0), 2, 1, 1); using box2 = new oc.BRepPrimAPI_MakeBox(new oc.gp_Pnt(1, 0, 0), 2, 1, 1); using fuse = new oc.BRepAlgoAPI_Fuse(box1.Shape(), box2.Shape(), p.Start()); fuse.Build(new oc.Message_ProgressRange()); }; ``` V3 uses suffix-free `Start()` — the v2 `_1` / `_2` overload suffixes no longer exist. Overload dispatch happens in C++ via the unified RBV pipeline. ## Step 3 — Cancellation [#step-3--cancellation] Set `shouldCancel = true` from a UI button or timeout. The next `UserBreak()` call returns `true` and OCCT aborts the in-flight operation. See also: [Extend with C++](/docs/toolchain/guides/extend-with-cpp) for the general custom-build mechanics, and [Debugging WASM exceptions](/docs/package/guides/debugging-wasm-exceptions) if the derived class throws across the wasm boundary. --- # Extend with C++ URL: /docs/toolchain/guides/extend-with-cpp OCCT exposes hundreds of free functions, POD structs, and helper utilities the bindgen does not (and cannot) reach automatically. When you need one of them — or want an ergonomic wrapper around an OCCT API — the YAML config gives you three mechanisms: 1. **`additionalCppCode`** — inline C++ embedded directly in the YAML scalar. 2. **`additionalCppFiles`** — one or more `.cpp` files concatenated onto `additionalCppCode`. 3. **`mainBuild.additionalBindCode`** — per-build raw `EMSCRIPTEN_BINDINGS(...)` registrations. ## Decision tree [#decision-tree] ```text Are you adding new C++ implementation code (wrapper class, free function, POD)? ├── YES → Will the impl grow beyond ~100 lines or want syntax highlighting? │ ├── YES → additionalCppFiles │ └── NO → additionalCppCode (inline) └── NO → You are only adding raw embind registrations on existing symbols → mainBuild.additionalBindCode ``` The three compose. A typical custom binding ships a `.cpp` file (`additionalCppFiles`) plus an `EMSCRIPTEN_BINDINGS(...)` block (`additionalBindCode`) that registers what the `.cpp` defined. ## Mechanism 1 — `additionalCppCode` (inline) [#mechanism-1--additionalcppcode-inline] For short snippets that belong with the YAML for context — handle typedefs, a single free function, a small POD. ```yaml additionalCppCode: | #include Standard_Real addReals(Standard_Real a, Standard_Real b) { return a + b; } mainBuild: name: my_build additionalBindCode: | EMSCRIPTEN_BINDINGS(my_build_extras) { emscripten::function("addReals", &addReals); } ``` Now `oc.addReals(1.5, 2.25)` returns `3.75`. ## Mechanism 2 — `additionalCppFiles` (multi-file) [#mechanism-2--additionalcppfiles-multi-file] When the wrapper grows past \~100 lines or you want real `.cpp` files for editor support and review, move it to `additionalCppFiles`. The files are concatenated onto `additionalCppCode` (in that order) into one translation unit before custom-binding generation runs. ```yaml additionalCppFiles: - wrappers/fair-curve.cpp mainBuild: additionalBindCode: | EMSCRIPTEN_BINDINGS(my_build_faircurve) { emscripten::function("computeFairCurve", &computeFairCurve); } ``` Paths in `additionalCppFiles` are resolved relative to the YAML file's directory. A missing file fails-loud at validate time. ## Mechanism 3 — `additionalBindCode` (raw embind) [#mechanism-3--additionalbindcode-raw-embind] Use this when bindgen cannot emit the registration you need. Common cases: * Binding a free function (bindgen registers classes, not free functions). * Defining a `value_object` POD authored in `additionalCppCode` / `additionalCppFiles`. * Registering an `emscripten::vector` / `emscripten::map` an NCollection auto-discovery pass does not cover. ```yaml additionalCppCode: | struct PointXY { double X; double Y; }; mainBuild: additionalBindCode: | EMSCRIPTEN_BINDINGS(my_build_pointxy) { emscripten::value_object("PointXY") .field("X", &PointXY::X) .field("Y", &PointXY::Y); } ``` Every `EMSCRIPTEN_BINDINGS()` group **must use a unique name** across the block and any auto-generated binding TUs — embind enforces uniqueness at module load time. ## Worked example — wrapper class with constructor + methods [#worked-example--wrapper-class-with-constructor--methods] ```cpp title="wrappers/shape-cast.cpp" #include #include #include #include class ShapeCast { public: static TopoDS_Edge toEdge(const TopoDS_Shape& s) { return TopoDS::Edge(s); } static TopoDS_Face toFace(const TopoDS_Shape& s) { return TopoDS::Face(s); } }; ``` ```yaml title="build-configs/my-config.yml" additionalCppFiles: - wrappers/shape-cast.cpp mainBuild: bindings: - symbol: TopoDS_Shape - symbol: TopoDS_Edge - symbol: TopoDS_Face additionalBindCode: | EMSCRIPTEN_BINDINGS(my_build_shape_cast) { emscripten::class_("ShapeCast") .class_function("toEdge", &ShapeCast::toEdge) .class_function("toFace", &ShapeCast::toFace); } ``` JS: ```typescript const edge = oc.ShapeCast.toEdge(genericShape); ``` ## Pitfalls [#pitfalls] * **Duplicate `EMSCRIPTEN_BINDINGS` group names** crash the module at load time. * **Forgetting to bind the underlying OCCT class** in `bindings:` causes the wrapper to compile but the runtime cast to throw. * **Using `Handle` without the typedef** works in C++ but produces no useful JS shape. Add `typedef opencascade::handle Handle_T;`. ## Related [#related] * [YAML schema](/docs/toolchain/reference/yaml-schema) — schema for every YAML key. * [Emscripten embind reference](https://emscripten.org/docs/porting/connecting_cpp_and_javascript/embind.html). --- # Custom multi-threaded build URL: /docs/toolchain/guides/multi-threading The npm package already ships a pre-built multi-threaded variant at `@taucad/opencascade.js/multi` — see [Package — Multi-threaded build](/docs/package/guides/multi-threading) for the import recipe, COOP/COEP headers, and OCCT activation calls. Rebuild from source only when you need: * A trimmed symbol list (the shipped MT binary builds from `build-configs/full_multi.yml` with the same \~4,400 symbols as the ST binary). * Custom `emccFlags` (alternative `STACK_SIZE`, `INITIAL_MEMORY`, capped `PTHREAD_POOL_SIZE`). * A relaxed-SIMD sibling for non-Safari deployments (see the callout below). ## Build YAML [#build-yaml] ```yaml title="build-configs/threaded.yml" mainBuild: name: occt_threaded bindings: # ... your symbol list ... emccFlags: - -O3 - -fwasm-exceptions - -msimd128 - -pthread - -sUSE_PTHREADS=1 # Pre-spawn one worker per logical CPU on the host. The expression is # evaluated by the generated JS glue at module-instantiation time, so a # single binary adapts to whatever hardware loads it. - -sPTHREAD_POOL_SIZE=navigator.hardwareConcurrency - -sSHARED_MEMORY=1 - -sMODULARIZE=1 - -sEXPORT_ES6=1 - -sENVIRONMENT=web,worker,node - -sALLOW_MEMORY_GROWTH=1 - -sMAXIMUM_MEMORY=4GB ``` Build with: ```bash ./build-wasm.sh --config multi-threaded full build-configs/threaded.yml ``` `PTHREAD_POOL_SIZE=navigator.hardwareConcurrency` evaluates per-host: an M2 Pro browser gets 12 workers, a mobile device gets 4–6, a Node server gets whatever `os.cpus().length` reports. Each worker costs `STACK_SIZE` (default 8 MB) at module init, so a 12-core box reserves \~96 MB of stack on top of `INITIAL_MEMORY`. If you need a hard cap, wrap the expression: `Math.min(navigator.hardwareConcurrency, 16)`. > ### Why not a small fixed pool size? [#why-not-a-small-fixed-pool-size] > > A round-number pool like `-sPTHREAD_POOL_SIZE=4` is tempting but > silently caps your speedup. On a 12-core machine OCCT's > `OSD_ThreadPool` requests 11 workers (`NbLogicalProcessors-1`); with a > pool of 4 the parallel sections degrade to 4-way fan-out and the > headline speedup stalls around 1.3×. Sizing the pool to > `navigator.hardwareConcurrency` recovers the full 2.0–2.5× on > mesh-dominated workloads, and shrinks gracefully on lower-core devices > without a separate build. > ### Relaxed-SIMD — advanced, non-default [#relaxed-simd--advanced-non-default] > > Do **not** add `-mrelaxed-simd` to the binary you ship to > browsers. Safari / WebKit (all versions as of May 2026) crash the JIT > when a relaxed-SIMD opcode actually executes — validation passes but > execution does not. If you want the 5–15 % uplift on Chromium / > Firefox / Node, build a sibling `occt_threaded_rsimd.wasm` with > `-mrelaxed-simd` and `wasm-opt --enable-relaxed-simd`, then UA-sniff in > your loader to serve the plain binary to WebKit and the relaxed > binary to everyone else. > ### `EVAL_CTORS=2` is dropped under threading [#eval_ctors2-is-dropped-under-threading] > > `full.yml` enables `-sEVAL_CTORS=2` for the single-threaded build, but > `full_multi.yml` drops it — constructor evaluation order is > non-deterministic under pthread workers. The trade-off is a slightly > larger binary in exchange for race-free startup. > > **Companion observation:** the `wasm-opt` post-link pass reports only > \~0.1% size reduction on MT builds vs the 4-7% it achieves on ST. That > is **not** a regression — `wasm-opt` would normally fold static > ctor calls during the same pass `EVAL_CTORS=2` enables; with that > flag dropped, the constructors stay live and wasm-opt has less to > eliminate. No action required. ## Browser-only MT build [#browser-only-mt-build] If you can guarantee your consumers run a recent browser (Chrome / Edge ≥ 144, Firefox ≥ 145, Safari ≥ 26.2 — May 2026 baseline) and you do **not** need to load the binary under Node.js, Bun, or Deno, you can opt into the `multi-threaded-browser` preset for a measurable runtime win on memory-growth-heavy workloads. The preset layers `-sGROWABLE_ARRAYBUFFERS=1` and `-sENVIRONMENT=web,worker` on top of the standard `multi-threaded` flag set. Internally Emscripten emits JS glue that wraps the pthread `SharedArrayBuffer` in a [resizable `ArrayBuffer` view](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/SharedArrayBuffer/grow) via `WebAssembly.Memory.prototype.toResizableBuffer()`. With that view in place, consumers can hoist `HEAP*` references out of hot loops — they no longer go stale after a memory-growth event, so the `-Wpthreads-mem-growth` link-time advisory disappears entirely and per-call HEAP re-fetches drop out of the inner loop. Build with: ```bash ./build-wasm.sh --config multi-threaded-browser full build-configs/full_multi_browser.yml ``` The shipped recipe lives at [`build-configs/full_multi_browser.yml`](https://github.com/taucad/opencascade.js/blob/main/build-configs/full_multi_browser.yml); diff it against [`full_multi.yml`](https://github.com/taucad/opencascade.js/blob/main/build-configs/full_multi.yml) to see the exact two-flag delta. > ### Runtime support matrix — `toResizableBuffer()` (May 2026) [#runtime-support-matrix--toresizablebuffer-may-2026] > > **Supported** — ship `full_multi_browser` artefacts to: > > * Chrome / Edge ≥ 144 > * Firefox ≥ 145 > * Safari ≥ 26.2 (desktop + iOS) > > **NOT supported** — fall back to plain `full_multi` for: > > * Node.js (all current LTS) — throws `TypeError: wasmMemory.toResizableBuffer is not a function` on module init. > * Bun and Deno (all current versions). > * Samsung Internet and other Chromium derivatives that lag mainline. > > Because Node.js is the dominant test-harness runtime, the **default** > shipped multi-threaded build keeps `-sGROWABLE_ARRAYBUFFERS=0` so it > stays loadable under `vitest`, `jest`, and other Node-based test runners. > Opt into the browser variant only for production payloads you serve > directly to browser clients. ## After building [#after-building] Wire the resulting `.js` / `.wasm` triple into your bundler the same way as the shipped variant — see [Package — Multi-threaded build](/docs/package/guides/multi-threading) for activation calls, parallel-aware OCCT APIs, and benchmarks. ## Related [#related] * [Configurations reference](/docs/toolchain/reference/configurations) — `multi-threaded` preset and how to author a custom one. * [Custom emcc flags](/docs/toolchain/guides/custom-emcc-flags) — link-time `emccFlags` rationale. * [Trim symbols](/docs/toolchain/guides/trim-symbols) — shrink the symbol set before building. --- # Reproducible CI URL: /docs/toolchain/guides/reproducible-ci A reproducible OCJS build means: same inputs, same wasm bytes, every time. Three controls close the loop. ## 1. Pin the image by manifest-list digest [#1-pin-the-image-by-manifest-list-digest] `:single-threaded`, `:multi-threaded`, and `:bindgen-base` are moving tags that roll forward with every release. CI should pin by digest so a new publish doesn't silently change your wasm output. Pinning the manifest-list digest (not the per-arch image digest) covers both `linux/amd64` and `linux/arm64` sub-images in a single deterministic pin. ```bash # Resolve the manifest-list digest once docker buildx imagetools inspect ghcr.io/taucad/opencascade.js:single-threaded \ --format '{{json .Manifest.Digest}}' # → "sha256:abc123…" # Use the digest in CI (resolves to the matching native arch transparently) docker pull ghcr.io/taucad/opencascade.js@sha256:abc123... docker run --rm \ -v "$(pwd):/src" \ -u "$(id -u):$(id -g)" \ ghcr.io/taucad/opencascade.js@sha256:abc123... \ link mybuild.yml ``` ## 2. Verify the cosign signature [#2-verify-the-cosign-signature] Every published image is signed via cosign keyless signing (OIDC) of the manifest-list digest — one signature verifies regardless of which arch the consumer pulls. ```bash cosign verify ghcr.io/taucad/opencascade.js@sha256:abc123... \ --certificate-identity-regexp 'https://github.com/taucad/opencascade\.js/\.github/workflows/docker\.yml@.*' \ --certificate-oidc-issuer https://token.actions.githubusercontent.com ``` A successful verification confirms the image was built by the `taucad/opencascade.js` GitHub Actions `docker.yml` workflow and has not been tampered with since publication. CI failing this step on a sudden mismatch is a smoking gun that something tampered with the supply chain. ## 2b. Verify SLSA provenance attestation [#2b-verify-slsa-provenance-attestation] In addition to the cosign signature, the image ships a SLSA provenance attestation: ```bash cosign verify-attestation \ --type slsaprovenance \ --certificate-identity-regexp 'https://github\.com/taucad/opencascade\.js/\.github/workflows/docker\.yml@.*' \ --certificate-oidc-issuer https://token.actions.githubusercontent.com \ ghcr.io/taucad/opencascade.js@sha256:abc123... ``` The attestation records the source commit, the runner, and the workflow that produced the image. ## 3. Extract SBOM [#3-extract-sbom] ```bash docker buildx imagetools inspect \ --format '{{ json .SBOM }}' \ ghcr.io/taucad/opencascade.js@sha256:abc123... ``` The SBOM lists every apt package and pinned commit (OCCT, freetype, rapidjson) embedded in the image. Diff it against the prior digest's SBOM in CI to flag unexpected dep bumps. ## 4. Lock downstream consumers [#4-lock-downstream-consumers] In your `package.json`, pin the npm tarball alongside the docker digest: ```json { "dependencies": { "@taucad/opencascade.js": "3.0.0-beta.5" }, "pnpm": { "supportedArchitectures": { "os": ["linux", "darwin"], "cpu": ["x64", "arm64"] } } } ``` Commit `pnpm-lock.yaml`. The lockfile records the integrity hash of the published tarball; if it ever changes, `pnpm install --frozen-lockfile` fails in CI. ## End-to-end CI snippet [#end-to-end-ci-snippet] ```yaml title=".github/workflows/wasm-build.yml" jobs: build-wasm: runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v4 - name: Verify cosign signature run: | DIGEST=$(cat .ocjs-digest) cosign verify "ghcr.io/taucad/opencascade.js@${DIGEST}" \ --certificate-identity-regexp 'https://github.com/taucad/opencascade\.js/\.github/workflows/docker\.yml@.*' \ --certificate-oidc-issuer https://token.actions.githubusercontent.com - name: Verify image provenance run: | DIGEST=$(cat .ocjs-digest) cosign verify-attestation --type slsaprovenance \ --certificate-identity-regexp 'https://github\.com/taucad/opencascade\.js/\.github/workflows/docker\.yml@.*' \ --certificate-oidc-issuer https://token.actions.githubusercontent.com \ "ghcr.io/taucad/opencascade.js@${DIGEST}" - name: Build wasm run: | DIGEST=$(cat .ocjs-digest) docker run --rm \ -v "$(pwd):/src" \ -u "$(id -u):$(id -g)" \ "ghcr.io/taucad/opencascade.js@${DIGEST}" \ link build-configs/my-config.yml - name: Assert wasm hash run: | EXPECTED=$(cat .wasm-hash) ACTUAL=$(sha256sum my-config.wasm | cut -d' ' -f1) [ "$EXPECTED" = "$ACTUAL" ] || { echo "wasm hash drift"; exit 1; } ``` The trailing `assert wasm hash` step is the safety net. If `EXPECTED` and `ACTUAL` ever diverge, something in the published image changed — either intentionally (bump your `.wasm-hash`) or by accident (investigate). --- # Trim symbols URL: /docs/toolchain/guides/trim-symbols `opencascade_full.wasm` binds \~4,400 OCCT classes and weighs roughly 27 MB. Most consumers need a fraction of that surface — a STEP round-tripper might touch \~120 classes, a glTF mesher even fewer. Trimming the YAML symbol list shrinks the wasm payload and reduces startup time. This guide drives the entire workflow through the published Docker image so you do not need `emsdk`, `libclang`, or Python installed locally. The image ships with `emsdk`, `libclang`, Python, the precompiled OCCT object cache, and the reference `full.yml` already in place. ## Prerequisites [#prerequisites] * Docker (or any OCI runtime — Colima, Rancher Desktop, OrbStack all work). * A working directory under your home folder (`/Users/...` on macOS, `C:\Users\...` on Windows). Other top-level folders like `/tmp` and `/opt` are not shared into the Docker VM by default and will silently drop build outputs. * Familiarity with the YAML schema. See [YAML schema](/docs/toolchain/reference/yaml-schema) for the full reference. If you have not used the image before, run the [Quickstart — Docker](/docs/toolchain/getting-started/quick-start-docker) once to verify your setup before trimming. ## Size budget [#size-budget] Each bound class contributes roughly 15–25 KB to the linked wasm. A reasonable target by use case: | Use case | Symbols | Approximate wasm size | | ----------------------------------------------- | -----------------: | --------------------: | | Single-format viewer (read STEP → mesh) | 80–150 | 2–4 MB | | Round-trip pipeline (STEP/IGES edit + write) | 200–400 | 5–10 MB | | Full code-CAD tool (booleans + fillets + sweep) | 600–1,200 | 12–20 MB | | Reference / kitchen sink | 4,400 (`full.yml`) | \~27 MB | Numbers are after `-O3 -msimd128 -sWASM_BIGINT -sEVAL_CTORS=2`. ## Steps [#steps] ### 1. Extract the reference `full.yml` [#1-extract-the-reference-fullyml] The image ships the reference `full.yml` at `/opencascade.js/build-configs/full.yml`. Pull it out with an entrypoint override so you can use it as your starting point: ```bash docker run --rm --entrypoint cat \ ghcr.io/taucad/opencascade.js:single-threaded \ /opencascade.js/build-configs/full.yml \ > my-config.yml ``` If you already know exactly which classes you need you can also hand-write `my-config.yml` from scratch — see the worked example below. ### 2. Delete the classes you don't need [#2-delete-the-classes-you-dont-need] Open `my-config.yml` and remove any `bindings` entry you don't call from JavaScript. There is no transitive closure to compute — Handle typedefs and `NCollection_*` members for the surviving classes are auto-discovered at codegen time, so you only list classes you instantiate or pass references to. ### 3. Validate [#3-validate] ```bash docker run --rm \ -v "$(pwd):/src" \ -u "$(id -u):$(id -g)" \ ghcr.io/taucad/opencascade.js:single-threaded \ validate /src/my-config.yml ``` `validate` parses the YAML against the schema and fails non-zero on any malformed entry, unknown key, or duplicated symbol. It does not run the C++ pipeline, so it returns in under a second. ### 4. Link [#4-link] ```bash docker run --rm \ -v "$(pwd):/src" \ -u "$(id -u):$(id -g)" \ ghcr.io/taucad/opencascade.js:single-threaded \ link my-config.yml ``` `link` reuses the precompiled `.o` files baked into the image for every still-bound class, so a trim that strips half of `full.yml` typically completes in 60–180 seconds. Output lands next to `my-config.yml` (per `OCJS_OUTPUT_DIR=/src` default): * `.wasm` — the trimmed wasm binary * `.js` — the emscripten loader * `.d.ts` — generated TypeScript declarations * `.build-manifest.json` — symbol coverage and size report If you removed a class another bound class transitively needs, the link step fails with `undefined symbol`. Add it back and re-run. ### 5. Iterate with a persistent cache [#5-iterate-with-a-persistent-cache] Repeated trims benefit from caching the Nx graph between runs. Create a named volume once, then mount it on every subsequent invocation: ```bash docker volume create ocjs-nx-cache docker run --rm \ -v ocjs-nx-cache:/opencascade.js/.nx \ -v "$(pwd):/src" \ -u "$(id -u):$(id -g)" \ ghcr.io/taucad/opencascade.js:single-threaded \ link my-config.yml ``` Cache hits drop subsequent link times to \~10–30 seconds when only the YAML changed and the touched classes were already compiled. ## Worked example: STEP round-tripper [#worked-example-step-round-tripper] ```yaml title="my-config.yml" mainBuild: name: step_roundtrip bindings: - symbol: Standard_Failure - symbol: Message_ProgressRange - symbol: TCollection_AsciiString - symbol: TCollection_ExtendedString - symbol: TopoDS - symbol: TopoDS_Shape - symbol: TopoDS_Compound - symbol: TopoDS_Solid - symbol: TopoDS_Shell - symbol: TopoDS_Face - symbol: TopoDS_Wire - symbol: TopoDS_Edge - symbol: TopoDS_Vertex - symbol: TopExp - symbol: TopExp_Explorer - symbol: gp_Pnt - symbol: gp_Dir - symbol: gp_Vec - symbol: gp_Ax2 - symbol: STEPControl_Reader - symbol: STEPControl_Writer - symbol: STEPControl_StepModelType - symbol: IFSelect_ReturnStatus - symbol: Interface_Static emccFlags: - -O3 - -msimd128 - -sWASM_BIGINT - -sEVAL_CTORS=2 - -sMODULARIZE=1 - -sEXPORT_ES6=1 - -sENVIRONMENT=web,worker,node - -sALLOW_MEMORY_GROWTH=1 ``` Link it with the step-4 invocation above. On a warm cache, expect a 1.5–3 MB binary in `./output/step_roundtrip.wasm`. ## Variations [#variations] ### Pin the image by digest [#pin-the-image-by-digest] The `:single-threaded` tag is mutable — a new release rolls forward at any time. For reproducible CI, replace the tag with an immutable manifest-list digest: ```bash docker run --rm \ -v "$(pwd):/src" \ -u "$(id -u):$(id -g)" \ ghcr.io/taucad/opencascade.js@sha256: \ link my-config.yml ``` See [Reproducible CI](/docs/toolchain/guides/reproducible-ci) for the full pinning recipe. ### Drop into the image for interactive exploration [#drop-into-the-image-for-interactive-exploration] To browse `build-configs/*.yml` presets, the OCCT source tree, or the toolchain layout, override the entrypoint: ```bash docker run --rm -it \ -v "$(pwd):/src" \ --entrypoint bash \ ghcr.io/taucad/opencascade.js:single-threaded ``` The image's working directory is `/opencascade.js/`. `build-configs/`, `deps/OCCT/`, and the precompiled object cache under `dist/libs/` all live there. ### When trimming feeds back into your design [#when-trimming-feeds-back-into-your-design] A trimmed binary makes your application's OCCT dependency graph explicit. Adding a class to recover from a link error is a signal: either the class is genuinely part of your call graph, or your code reaches into an OCCT subsystem it doesn't need. The second case is worth investigating. ## Related [#related] * [Quickstart — Docker](/docs/toolchain/getting-started/quick-start-docker) — end-to-end first build. * [Docker image](/docs/toolchain/reference/docker-image) — tags, mounts, OCI labels, provenance. * [YAML schema](/docs/toolchain/reference/yaml-schema) — every YAML key and its semantics. * [Custom emcc flags](/docs/toolchain/guides/custom-emcc-flags) — tuning link-time flags. * [Reproducible CI](/docs/toolchain/guides/reproducible-ci) — pinning by digest for repeatable builds. * [Bindgen pipeline](/docs/toolchain/concepts/bindgen-pipeline) — why the trim works without computing a transitive closure. --- # build-wasm.sh CLI URL: /docs/toolchain/reference/cli-build-wasm `build-wasm.sh` is the orchestration entry point for the OCJS build pipeline. Each subcommand maps to a stage of the bindgen → compile → link sequence. ## Synopsis [#synopsis] ```bash ./build-wasm.sh [options] [] ``` ## Subcommands [#subcommands] ### `validate` [#validate] ```bash ./build-wasm.sh validate build-configs/my-config.yml ``` Parses the YAML against `src/customBuildSchema.py` and fails non-zero on any malformed entry, unknown key, duplicated symbol, or missing `additionalCppFiles` path. Does **not** run the C++ pipeline. ### `bindings` [#bindings] ```bash ./build-wasm.sh bindings ``` Runs the libclang-driven bindgen against the OCCT headers, emitting one `.hxx` per class under `build/bindings////` plus sidecar `.d.ts.json` shards. Cache: re-uses prior output when header contents and bindgen-filter fingerprint match. Force regeneration with `--force`. ### `pch` [#pch] ```bash ./build-wasm.sh pch ``` Builds the precompiled header used by every bindings TU. Always passes `-Xclang -fno-pch-timestamp` so Nx/Docker cache restores don't invalidate the PCH on disk-mtime drift. ### `link` [#link] ```bash ./build-wasm.sh link build-configs/my-config.yml ``` Compiles every `.hxx` referenced by the YAML's `bindings:` list (cached `.o` files reused), links them against the precompiled OCCT objects under `dist/libs/`, and emits the final wasm + JS + `.d.ts` + manifest sibling to the YAML. ### `clean` [#clean] ```bash ./build-wasm.sh clean # rm build/, cache/, dist/ ./build-wasm.sh clean --cache # rm cache/ only ./build-wasm.sh clean --dist # rm dist/ only ``` ## Exit codes [#exit-codes] | Code | Meaning | | ---- | ------------------------------------------------------- | | 0 | Success | | 1 | YAML validation failure | | 2 | bindgen failure (libclang error) | | 3 | Compile failure | | 4 | Link failure (undefined symbol or unresolved reference) | | 5 | Missing input file | | 64 | Invalid usage (unknown subcommand or flag) | ## Common flags [#common-flags] | Flag | Effect | | ----------------- | ------------------------------------------------------------------------------------------------------ | | `--config ` | Override `OCJS_CONFIG` for this run — selects a named preset from `build-configs/configurations.json`. | | `--force` | Bypass cache; rebuild from scratch | | `--verbose` | Equivalent to `OCJS_VERBOSE=1` | | `--jobs N` | Set `OCJS_PARALLEL_JOBS=N` | ## Typical CI invocation [#typical-ci-invocation] ```bash ./build-wasm.sh validate build-configs/my-config.yml ./build-wasm.sh link --jobs 8 build-configs/my-config.yml sha256sum dist/my-config.wasm ``` ## Related [#related] * [Bindgen pipeline](/docs/toolchain/concepts/bindgen-pipeline) — stage diagram and artifact layout. * [Environment variables](/docs/toolchain/reference/env-vars) — env var catalogue. * [YAML schema](/docs/toolchain/reference/yaml-schema) — the YAML the CLI consumes. --- # Named compile-time configurations URL: /docs/toolchain/reference/configurations Compile-time presets live in [`build-configs/configurations.json`](https://github.com/taucad/opencascade.js/blob/main/build-configs/configurations.json). Each entry is a flat map of `OCJS_*` environment-variable names to values; the build CLI loads the entry and exports each key before driving `emcc` and `wasm-opt`. All shipped presets ship with the same *feature* set — native WASM exceptions (`OCJS_EXCEPTIONS=1`, `OCJS_EH_MODE=wasm`), `EVAL_CTORS=2`, and the Closure Compiler on. They differ on optimisation level, threading mode, and the wasm-opt budget. ## Shipped presets [#shipped-presets] | Preset | Use case | Differentiating flags | | -------------------------- | ---------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------- | | `single-threaded` | Default `@taucad/opencascade.js` build (`opencascade_full.*`). Browser default. | `OCJS_OPT=-O3`, `OCJS_WASM_OPT_LEVEL=-O4`, `THREADING=single-threaded` | | `single-threaded-smallest` | Size-tuned variant — smaller binary at a \~5–10% runtime cost. | `OCJS_OPT=-Os`, `OCJS_WASM_OPT_LEVEL=-O3`, `THREADING=single-threaded` | | `multi-threaded` | Published `@taucad/opencascade.js/multi` build. SAB/COOP+COEP-isolated deployments. | `OCJS_OPT=-O3`, `OCJS_WASM_OPT_LEVEL=-O4`, `THREADING=multi-threaded` | | `debug` | Fastest build for local iteration. Not for production — no SIMD, no converge, no BigInt. | `OCJS_OPT=-O0`, `OCJS_WASM_OPT_LEVEL=-O0`, `OCJS_SIMD=0`, `OCJS_CONVERGE=false` | For the full `OCJS_*` matrix that each preset sets — including the flags they share — see [BUILD\_SYSTEM.md](https://github.com/taucad/opencascade.js/blob/main/BUILD_SYSTEM.md#configurationsjson). ## Selecting a preset [#selecting-a-preset] `build-wasm.sh` reads the `OCJS_CONFIG` env var or the `--config ` CLI flag (the CLI flag wins if both are set). When neither is set, the script falls back to `single-threaded`: ```bash OCJS_CONFIG=debug ./build-wasm.sh link build-configs/my-config.yml ``` Or via the CLI flag: ```bash ./build-wasm.sh --config debug link build-configs/my-config.yml ``` ## Adding a custom preset [#adding-a-custom-preset] Append a new entry to `build-configs/configurations.json`. The shape is a flat `OCJS_*` env-var map — same keys the shipped presets use: ```json { "single-threaded-no-simd": { "OCJS_OPT": "-Os", "OCJS_LTO": "0", "OCJS_EXCEPTIONS": "1", "OCJS_EH_MODE": "wasm", "OCJS_SIMD": "0", "THREADING": "single-threaded", "OCJS_DEFINES": "OCCT_NO_DUMP", "OCJS_UNDEFINES": "OCC_CONVERT_SIGNALS", "OCJS_WASM_OPT_LEVEL": "-O3", "OCJS_CLOSURE": "true", "OCJS_EVAL_CTORS": "true", "OCJS_EVAL_CTORS_LEVEL": "2", "OCJS_CONVERGE": "true", "OCJS_BIGINT": "1", "OCJS_MALLOC": "mimalloc", "BINARYEN_EXTRA_PASSES": "" } } ``` Custom presets share the same compile-`.o` cache as shipped ones — the cache is keyed by the compile-flag fingerprint, not by the preset name. Two presets with identical compile-time flags share cache entries automatically. ## Cache invalidation [#cache-invalidation] Changing any compile flag invalidates the cached `.o` files that depended on it. The build manifest records the active fingerprint: ```bash cat build/build-flags.json ``` The published npm tarball ships `dist/opencascade_full.provenance.json` and `dist/opencascade_full_multi.provenance.json` — same preset / flag information for each variant, with the commit SHA the wasm was built from. CI scripts should diff these files across builds to detect surprise cache turnover. ## Related [#related] * [Two-channel config model](/docs/toolchain/concepts/two-channel-config-model) — why compile-time and link-time config are separate channels. * [Custom emcc flags](/docs/toolchain/guides/custom-emcc-flags) — link-time `emccFlags` rationale. * [Environment variables](/docs/toolchain/reference/env-vars) — every `OCJS_*` env var. --- # Docker image URL: /docs/toolchain/reference/docker-image The maintainer-distributed Docker image lets consumers run custom-trimmed wasm builds without setting up emsdk, libclang, and Python locally. ## Pulling [#pulling] ```bash docker pull ghcr.io/taucad/opencascade.js:single-threaded ``` ## Tags [#tags] | Tag | Points to | | ----------------------------------------------------------------- | ------------------------------------------------------------------------------------ | | `:single-threaded` | Latest release, single-threaded warm cache (default for browser CAD UIs) | | `:multi-threaded` | Latest release, multi-threaded warm cache (requires COOP/COEP on consumer pages) | | `:bindgen-base` | Latest release, post-PCH/generate but pre-compile (custom-bindings starting point) | | `:{{version}}-single-threaded`
`:{{version}}-multi-threaded` | Pinned release (e.g. `:3.0.0-single-threaded`); manifest list of `linux/amd64+arm64` | | `:{{version}}-bindgen-base` | Pinned release, bindgen-base | | `:branch-` / `:branch--` | Branch tip, single-threaded, **`linux/amd64`-only**, 7-day GHCR retention | | `:multi-threaded-branch-` / `…-` | Branch tip, multi-threaded, amd64-only, 7-day retention | | `:bindgen-base-branch-` / `…-` | Branch tip, bindgen-base, amd64-only, 7-day retention | | `@sha256:` | Immutable pin — use in CI | **Pin by digest in production.** The bare-name tags (`:single-threaded`, `:multi-threaded`, `:bindgen-base`) are mutable and roll forward with every release. See [Reproducible CI](/docs/toolchain/guides/reproducible-ci) for the pinning workflow. The legacy `:beta`, `:rolling`, and `:latest` tags are **not published** by this fork. Use a version-pinned tag (e.g. `:3.0.0-single-threaded`) or the manifest-list digest for explicit version control. ## Entrypoint [#entrypoint] ```bash docker run --rm \ -v "$(pwd):/src" \ -u "$(id -u):$(id -g)" \ ghcr.io/taucad/opencascade.js:single-threaded \ link mybuild.yml ``` The entrypoint dispatches subcommands through `npx nx run ocjs:` so runs benefit from Nx's content-addressed cache: | Subcommand | What it does | | ----------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `link ` | End-to-end build. Nx walks `apply-patches → pch → generate → compile-bindings → compile-sources → link` with cache reuse — fresh container = full build, cached re-run = link only. | | `compile-bindings`, `compile-sources`, `pch`, … | Run an individual Nx target | | `validate ` | Validate YAML without building | | `nx ` | Pass-through to `npx nx` (escape hatch) | Outputs land in `/src` next to your YAML (`OCJS_OUTPUT_DIR=/src` default). ### Override the entrypoint [#override-the-entrypoint] ```bash docker run --rm -it -v "$(pwd):/src" --entrypoint bash \ ghcr.io/taucad/opencascade.js:single-threaded ``` …drops you into a shell with `emsdk`, libclang, and Python on the PATH. ## Multi-arch matrix [#multi-arch-matrix] | Tag family | Manifest list | Built on | | ------------ | ----------------------------- | ----------------------------------------------------------------------------------- | | Release tags | `linux/amd64` + `linux/arm64` | `ubuntu-latest` (amd64) + `ubuntu-24.04-arm` (arm64), GitHub Actions native runners | | Branch tags | `linux/amd64` only | `ubuntu-latest`, GitHub Actions | Releases ship full manifest lists so Apple Silicon and ARM Linux hosts pull the native arch transparently. Branch tags are amd64-only by design (R12 in the [production-readiness blueprint](https://github.com/taucad/opencascade.js/blob/main/docs/research/ocjs-docker-production-readiness-blueprint.md)) — ARM hosts will run under Rosetta / QEMU for branch tags. ## OCI labels [#oci-labels] Inspect via `docker inspect ghcr.io/taucad/opencascade.js:single-threaded`: | Label | Purpose | | -------------------------------------- | ------------------------------------------------------------------------------------ | | `org.opencontainers.image.title` | Stage-specific title (single-threaded, multi-threaded, …) | | `org.opencontainers.image.description` | Threading model and consumer prerequisites | | `org.opencontainers.image.source` | [https://github.com/taucad/opencascade.js](https://github.com/taucad/opencascade.js) | | `org.opencontainers.image.url` | Same as source | | `org.opencontainers.image.revision` | Git commit the image was built from | | `org.opencontainers.image.version` | Semver tag | | `org.opencontainers.image.licenses` | LGPL-2.1-only | | `org.opencontainers.image.vendor` | taucad | ## Cosign signatures [#cosign-signatures] Every published image is signed with [cosign](https://github.com/sigstore/cosign) via OIDC keyless signing — no rotating private keys, signatures published to the Sigstore Rekor transparency log. Release tags carry **one signature on the manifest-list digest** that verifies regardless of which arch the consumer pulls; branch tags carry **per-arch image-digest signatures**. ```bash cosign verify ghcr.io/taucad/opencascade.js:single-threaded \ --certificate-identity-regexp 'https://github.com/taucad/opencascade\.js/\.github/workflows/docker\.yml@.*' \ --certificate-oidc-issuer https://token.actions.githubusercontent.com ``` A successful verification confirms the image was built by the `taucad/opencascade.js` GitHub Actions `docker.yml` workflow and has not been tampered with since publication. ## Provenance [#provenance] ```bash cosign verify-attestation \ --type slsaprovenance \ --certificate-identity-regexp 'https://github\.com/taucad/opencascade\.js/\.github/workflows/docker\.yml@.*' \ --certificate-oidc-issuer https://token.actions.githubusercontent.com \ ghcr.io/taucad/opencascade.js:single-threaded ``` The attestation records the source commit, the workflow, and the runner that produced the image. ## SBOM [#sbom] ```bash docker buildx imagetools inspect \ --format '{{ json .SBOM }}' \ ghcr.io/taucad/opencascade.js:single-threaded ``` Diff the SBOM across image digests in CI to flag unexpected dep bumps. ## Image stages [#image-stages] The [Dockerfile](https://github.com/taucad/opencascade.js/blob/main/Dockerfile) is multi-stage with five logical stages, three of which are published: | Stage | Published as | Contents | | -------------------------- | ------------------ | ------------------------------------------------------------------------------- | | `deps-base` | *(not published)* | emsdk + apt + Node 24 + uv + Python + OCCT/rapidjson/freetype + LLVM 17 headers | | `bindgen-base` | `:bindgen-base` | deps + npm ci + patches + PCH + `.d.ts.json` index | | `compiled-single-threaded` | *(not published)* | bindgen + compiled `.o` files + OCCT `.a` (single-threaded) | | `compiled-multi-threaded` | *(not published)* | bindgen + compiled `.o` files + OCCT `.a` (multi-threaded) | | `final-single` | `:single-threaded` | compiled-single + OCI labels + entrypoint | | `final-multi` | `:multi-threaded` | compiled-multi + OCI labels + entrypoint | Each stage is independently rebuildable via `docker buildx build --target `. See [Bindgen pipeline](/docs/toolchain/concepts/bindgen-pipeline) for how the stages compose with the build pipeline. --- # Environment variables URL: /docs/toolchain/reference/env-vars The build system reads these env vars before compile and link. Set them on the command line, in CI, or in `build-configs/configurations.json`. ## Compile-time flags [#compile-time-flags] | Variable | Default | Effect | | ----------------------- | --------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `OCJS_EXCEPTIONS` | `1` | Compile every TU with `-fwasm-exceptions`. Mixed builds fail at link. | | `OCJS_SIMD` | `1` | Compile every TU with `-msimd128` (baseline SIMD). | | `OCJS_RELAXED_SIMD` | `0` | Additionally emit `-mrelaxed-simd`. Chrome/Firefox-only — breaks Safari. | | `OCJS_BIGINT` | `1` | Compile with `-sWASM_BIGINT`. Saves \~30 KB JS glue. | | `OCJS_EVAL_CTORS_LEVEL` | `2` | `-sEVAL_CTORS=N`. Levels: 0 off, 1 safe-side-effect, 2 full. | | `OCJS_LTO` | `0` | Enable LLVM LTO. Smaller binary, \~3× slower build. | | `OCJS_STRICT_TYPES` | `0` (warn-only) | When the link-time `.d.ts` post-processor rewrites method signatures to `unknown` (or codegen collects unbound-reference diagnostics), the build always prints a triage summary to stderr. Set `=1` to escalate that condition to a build failure. | ## Build orchestration [#build-orchestration] | Variable | Default | Effect | | -------------------- | ----------------- | ----------------------------------------------------------------------------------------------------------------------- | | `OCJS_CONFIG` | `single-threaded` | Named preset from `configurations.json`. Alternative to the `--config ` CLI flag — both end up at the same place. | | `OCJS_PARALLEL_JOBS` | CPU count | Max parallel compile jobs. | | `OCJS_CACHE_DIR` | `./cache` | Where to store cached `.o` files and bindgen output. | | `OCJS_BUILD_DIR` | `./build` | Intermediate build outputs. | | `OCJS_DIST_DIR` | `./dist` | Final wasm + JS + .d.ts artifacts. | ## Docker-specific [#docker-specific] | Variable | Default | Effect | | ------------------- | --------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `OCJS_DEPS_VERSION` | from `DEPS.json` | Override the dep pinning (OCCT, freetype, rapidjson). | | `OCJS_EMSDK_DIR` | `/deps/emsdk` | Location of the emscripten SDK inside the image. | | `OCJS_PYTHON` | `python3` | Python interpreter for the bindgen. | | `OCJS_OUTPUT_DIR` | `/src` (in container) / `./dist` (host) | Where the link step writes the final `.wasm`, `.js`, `.d.ts`, and `.build-manifest.json`. In-container default is `/src` so the canonical single-mount Quickstart (`docker run -v "$(pwd):/src" …`) writes outputs next to the YAML automatically. Override to a separate path if you need outputs outside the bind-mount. | ## Diagnostics [#diagnostics] | Variable | Default | Effect | | ---------------------- | ------- | ------------------------------------------------------------------------- | | `OCJS_VERBOSE` | `0` | Print every compile / link command. | | `OCJS_DUMP_CACHE_KEYS` | `0` | Print the cache-key contents on miss (debug spurious cache invalidation). | ## Worked example [#worked-example] ```bash # Custom-trimmed Os build for an embedded edge runtime. # OCJS_STRICT_TYPES=1 escalates the missing-typedef warning to a build # failure -- recommended in CI; omit it for local builds (warn-only is # the default). OCJS_CONFIG=single-threaded-smallest \ OCJS_STRICT_TYPES=1 \ OCJS_PARALLEL_JOBS=8 \ ./build-wasm.sh link build-configs/edge-runtime.yml ``` ## Related [#related] * [Named compile-time configurations](/docs/toolchain/reference/configurations) — named preset list. * [Two-channel config model](/docs/toolchain/concepts/two-channel-config-model) — compile-time vs link-time channels. --- # YAML schema URL: /docs/toolchain/reference/yaml-schema The build YAML controls which OCCT classes get bound, which C++ wrapper code is injected, and which emscripten linker flags drive the final wasm. ## Top-level shape [#top-level-shape] ```yaml mainBuild: name: bindings: - symbol: emccFlags: - additionalBindCode: | EMSCRIPTEN_BINDINGS(custom) { ... } extraBuilds: - name: bindings: [...] emccFlags: [...] additionalBindCode: | ... additionalCppCode: | typedef opencascade::handle Handle_Geom_Curve; class MyWrapper { ... }; additionalCppFiles: - path/to/extra.cpp generateTypescriptDefinitions: true ``` The canonical Cerberus definition lives in `src/customBuildSchema.py`. ## `mainBuild` [#mainbuild] The primary wasm artifact produced by the YAML. ### `mainBuild.name` [#mainbuildname] Output filename without extension. `name: my-occt` produces `my-occt.wasm`, `my-occt.js`, `my-occt.d.ts`, and `my-occt.build-manifest.json`. ### `mainBuild.bindings` [#mainbuildbindings] Allowlist of OCCT classes to expose to JS via embind. Only classes listed here (and their transitive base classes) are accessible at runtime. ```yaml bindings: - symbol: BRepPrimAPI_MakeBox - symbol: TopoDS_Shape - symbol: gp_Pnt ``` The symbol name must match exactly the C++ class name in the generated binding `.cpp` files under `build/bindings/`. Base classes are auto-included if missing from the list. ### `mainBuild.emccFlags` [#mainbuildemccflags] Emscripten linker flags. See [Custom emcc flags](/docs/toolchain/guides/custom-emcc-flags) for the recommended baseline and per-flag rationale. ### `mainBuild.additionalBindCode` [#mainbuildadditionalbindcode] Per-build raw `EMSCRIPTEN_BINDINGS(...)` registrations compiled with `` already included. See [Extend with C++](/docs/toolchain/guides/extend-with-cpp). ## Symbol resolution classes [#symbol-resolution-classes] Every YAML-requested symbol becomes a linked binding through exactly one of four mechanisms. The post-link `build-manifest.json` (schema `build-manifest-v2`) buckets each requested symbol into one of these categories under `symbols`: 1. **Direct compilation.** `bindings: - symbol: gp_Pnt` causes the generator to emit `build/bindings/gp_Pnt.cpp`, which `compileBindings.py` compiles into `build/compiled-bindings/gp_Pnt.cpp.o`. Detected by `ocjs_bindgen.link.manifest_registry.collect_compiled_symbols`. Reported as `satisfied_by_compiled` (count surfaces as `symbols.compiled`). 2. **NCollection typedef alias.** `bindings: - symbol: TColgp_Array1OfPnt` resolves via the canonical mangled spelling `NCollection_Array1_gp_Pnt`; the linker substitutes the typedef at link time. Mapping lives in `build/ncollection-manifest.json`. Detected by `manifest_registry.load_ncollection_alias_index`. Reported under `symbols.alias_resolved` as `{alias, canonical}` entries. 3. **Embind builtin.** OCJS's `BUILTIN_ADDITIONAL_BIND_CODE` block (`OCJS`, `TopoDS`, `TColStd_IndexedDataMapOfStringString`) registers Embind class wrappers with no `.cpp.o` of their own. Detected by `manifest_registry.builtin_binding_symbols` reading `build/additional-bind-symbols.json`. 4. **Consumer `additionalBindCode`.** YAML's own `mainBuild.additionalBindCode` block undergoes the same Embind pathway. The link stage compiles `BUILTIN_ADDITIONAL_BIND_CODE + consumer additionalBindCode` into ONE translation unit so the AST producer emits a single `additional-bind-symbols.json` manifest covering both sources. Reported under `symbols.builtin` (no separate bucket). Anything that survives all four lookups lands in `symbols.missing` and triggers `validation_passed=false`. The link step also raises immediately via `yaml_build.verifyBindings` (no env-var gate) so a YAML asking for a symbol the toolchain cannot provide fails the link, not just the post-link audit. Auto-discovered NCollection canonicals (entries the YAML never named directly, but that became reachable from the YAML's scope) are tracked separately in `.provenance.json::nCollectionManifest.{linked, total, dropped}` (schema `wasm-build-provenance-v1.1`). They never appear in `symbols.requested` because they're produced by the discovery pass, not requested by the operator. ## Producer-side manifest contract [#producer-side-manifest-contract] Every mechanism above has exactly one **producer** — a pipeline stage with the semantic knowledge to compute it — that writes a JSON manifest in `build/` or the dist sidecar. Every downstream **consumer** (link-time `verifyBindings`, post-link `validate-build.py`, `generate-docs.mjs`, `docker-e2e-validate.sh`) reads the manifest through the corresponding `manifest_registry` loader. No consumer re-parses C++, runs regex against source, or re-derives set-difference math. | Manifest | Producer | Consumer loader | | ------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------- | | `build/ncollection-manifest.json` | `ocjs_bindgen.discover` | `manifest_registry.load_ncollection_alias_index` | | `build/additional-bind-symbols.json` | `runBuild::getAdditionalBindCodeO()` (libclang AST via `ocjs_bindgen.ast.parse_additional_bind_code` + `ocjs_bindgen.ast.walker.extract_class_registrations`) | `manifest_registry.builtin_binding_symbols` | | `build/compiled-bindings/*.cpp.o` | `compileBindings.py` | `manifest_registry.collect_compiled_symbols` | | `build/compiled-bindings/binding-report.json` | `compileBindings.py` | `validate-build.py::validate_binding_report` | | `.provenance.json::nCollectionManifest` | `yaml_build.main` via `provenance.add_linking(ncollection_linked=, ncollection_total=, ncollection_dropped=)` | `generate-docs.mjs`, `scripts/docker-e2e-validate.sh` | | `build/any-type-report.json` | `generate.py` | `validate-build.py::merge_any_reasons` | When a manifest is missing, consumers fail loudly with a pointer at `pnpm nx run ocjs:build`. Stale artifacts are stale by definition; rendering them with degraded math produces docs whose numbers contradict the build that produced them. ## `extraBuilds` [#extrabuilds] Same schema as `mainBuild`. Each entry produces a sibling wasm artifact. Useful for shipping multiple variants (e.g. baseline + relaxed-SIMD) from one YAML pass. ## `additionalCppCode` [#additionalcppcode] Inline C++ injected into a generated `.cpp` file compiled with the same flags as the bindings TU. Common uses: 1. Handle typedefs (`typedef opencascade::handle Handle_T;`). 2. Free-function helpers reachable from `additionalBindCode`. 3. Small `value_object` POD definitions. ## `additionalCppFiles` [#additionalcppfiles] List of `.cpp` files concatenated onto `additionalCppCode` before custom-binding generation runs. ```yaml additionalCppCode: | typedef opencascade::handle Handle_Geom_BSplineSurface; additionalCppFiles: - wrappers/fair-curve.cpp - wrappers/shape-cast.cpp ``` * Paths resolve relative to the YAML file's directory; absolute paths also accepted. * File contents are appended in declaration order. * `additionalCppCode` wins on conflict because it precedes file contents in the TU. * A missing file fails-loud at validate time. ## `generateTypescriptDefinitions` [#generatetypescriptdefinitions] Default `true`. Set to `false` to skip `.d.ts` generation (rare — useful only for ultra-fast iteration builds). ## Validation [#validation] ```bash ./build-wasm.sh validate build-configs/my-config.yml ``` `validate` checks the YAML against the schema and exits non-zero on any malformed entry, unknown key, duplicated symbol, or missing `additionalCppFiles` path. It does not run the C++ pipeline — pair it with `./build-wasm.sh link` to actually produce the wasm. ## Related [#related] * [Trim symbols](/docs/toolchain/guides/trim-symbols) — end-to-end workflow. * [Extend with C++](/docs/toolchain/guides/extend-with-cpp) — when to reach for which mechanism. * [Custom emcc flags](/docs/toolchain/guides/custom-emcc-flags) — flag-by-flag rationale. --- # Exception classes URL: /docs/package/reference/ocjs-package-api/exception-classes OCCT throws subclasses of `Standard_Failure`. Inside the wasm module, every throw becomes a `WebAssembly.Exception` instance whose payload is the C++ object. The OCJS bindings expose: * The exception class hierarchy via bound classes (`oc.Standard_Failure`, `oc.Standard_OutOfRange`, etc.). * `getExceptionMessage(error)` — a helper that decodes a `WebAssembly.Exception` into the OCCT failure text. ## Hierarchy [#hierarchy] ```text Standard_Failure ├── Standard_OutOfRange ├── Standard_NullObject ├── Standard_NoSuchObject ├── Standard_TypeMismatch ├── Standard_DomainError ├── Standard_DivideByZero └── Standard_ProgramError └── Standard_NotImplemented ``` Every subclass surfaces a `GetMessageString()` and inherits the `What()` method returning a static identifier. ## `getExceptionMessage(error)` [#getexceptionmessageerror] Named export of `@taucad/opencascade.js`. Accepts a `WebAssembly.Exception` and returns a string. ```typescript import init, { getExceptionMessage } from '@taucad/opencascade.js'; try { riskyOcctCall(); } catch (error: unknown) { if (error instanceof WebAssembly.Exception) { console.error('OCCT failure:', getExceptionMessage(error)); return; } throw error; } ``` ## What about JS-side `Error.message`? [#what-about-js-side-errormessage] `WebAssembly.Exception.message` is empty for C++-thrown values. The real message lives in C++ memory and must be reached through the helper above. ## Related [#related] * [Debugging WASM exceptions](/docs/package/guides/debugging-wasm-exceptions) — narrative debugging guide with common failure modes and chrome wasm debugger setup. --- # init() function URL: /docs/package/reference/ocjs-package-api/init-function `init` is the default export of `@taucad/opencascade.js`. It instantiates the WASM module and returns a Promise resolving to the bound OCCT runtime. ```typescript import init from '@taucad/opencascade.js'; import wasmUrl from '@taucad/opencascade.js/wasm?url'; const oc = await init({ locateFile: () => wasmUrl }); ``` The `@taucad/opencascade.js/wasm` subpath export resolves to the shipped `opencascade_full.wasm`. The multi-threaded variant uses the same `init` signature from `@taucad/opencascade.js/multi` with wasm at `@taucad/opencascade.js/multi/wasm`. See [Bundler & locateFile](/docs/package/guides/bundler-locatefile) for Vite, Next.js, Bun, Node, and Deno-specific recipes (including the MT variant). ## Options [#options] **`InitOpenCascadeOptions`** — Options accepted by `init()` from `@taucad/opencascade.js`. Passed straight through to emscripten's `Module` factory. - **`locateFile`** (`(file: string) => string`, required) — Resolve the URL where the runtime will fetch a sibling asset (most importantly `opencascade_full.wasm`). Mandatory for the V3 single-file build — the runtime no longer auto-discovers its wasm sibling. The canonical pattern is to point at the `@taucad/opencascade.js/wasm` subpath export and return its URL verbatim. Tags: @example `() => wasmUrl // import wasmUrl from '@taucad/opencascade.js/wasm?url'` - **`wasmBinary`** (`ArrayBuffer | Uint8Array | undefined`, optional, default `undefined`) — Override the wasm binary bytes directly. Use this when you've fetched the wasm via a custom transport (e.g. an OPFS cache) and want to skip the runtime's own `fetch(locateFile(...))`. - **`wasmMemory`** (`WebAssembly.Memory | undefined`, optional, default `undefined`) — Pre-allocated memory for the wasm instance. Most consumers leave this unset and let `ALLOW_MEMORY_GROWTH` size the heap on demand. - **`print`** (`((text: string) => void) | undefined`, optional, default `console.log`) — Print stdout (e.g. `printf` from custom C++) to a sink. - **`printErr`** (`((text: string) => void) | undefined`, optional, default `console.error`) — Print stderr (e.g. OCCT diagnostic messages) to a sink. ## Memoised singleton pattern [#memoised-singleton-pattern] `init` is expensive — wasm instantiation, runtime bootstrap, and class registration sum to \~200 ms warm / \~2 s cold. Always memoise the Promise. ```typescript let ocPromise: ReturnType | undefined; export const getOc = () => (ocPromise ??= init({ locateFile: () => wasmUrl })); ``` Calling `getOc()` from multiple call sites returns the same Promise; only the first call triggers init. ## Return type [#return-type] The resolved value is the bound OCCT module — a plain object whose properties are the bound classes and namespaces (`oc.BRepPrimAPI_MakeBox`, `oc.TopoDS`, `oc.Interface_Static`) plus emscripten runtime methods (`oc.FS`, `oc.HEAP8`, `oc.HEAPF32`). The full surface is documented under the [API Reference](/docs/package/api). ## Multi-threaded subpath [#multi-threaded-subpath] Import `@taucad/opencascade.js/multi` with wasm at `@taucad/opencascade.js/multi/wasm`. The `init` signature is identical; Emscripten pre-spawns pthread workers during instantiation (`PTHREAD_POOL_SIZE=navigator.hardwareConcurrency` is baked into the binary). No extra JS worker wiring is required beyond `locateFile`. After `await init(...)`, run this **once** before any parallel-aware OCCT call: ```typescript oc.BOPAlgo_Options.SetParallelMode(true); oc.BRepMesh_IncrementalMesh.SetParallelDefault(true); const pool = oc.OSD_ThreadPool.DefaultPool(-1); pool.SetNbDefaultThreadsToLaunch(pool.NbThreads()); ``` Skipping the pool sizing leaves OCCT's lazy default pool smaller than the pre-spawned worker count and caps speedup on mesh/boolean workloads. See [Multi-threaded build](/docs/package/guides/multi-threading#global-activation--call-once-at-startup) for the full activation walkthrough and [BENCHMARKS.md](https://github.com/taucad/opencascade.js/blob/main/BENCHMARKS.md) for ST vs MT numbers. --- # Module shape URL: /docs/package/reference/ocjs-package-api/module-shape The object returned from `init()` carries every bound class plus emscripten runtime helpers. ## Bound classes and namespaces [#bound-classes-and-namespaces] Direct properties named after OCCT classes: ```typescript const oc = await init({ locateFile }); const box = new oc.BRepPrimAPI_MakeBox(10, 10, 10); const point = new oc.gp_Pnt(0, 0, 0); ``` OCCT namespaces (e.g. `TopoDS`, `Interface_Static`, `XCAFDoc_DocumentTool`) appear as static-method-only objects: ```typescript oc.Interface_Static.SetIVal('write.step.schema', 5); const edge = oc.TopoDS.Edge(genericShape); const shapeTool = oc.XCAFDoc_DocumentTool.ShapeTool(doc.Main()).get(); ``` ## Filesystem (`oc.FS`) [#filesystem-ocfs] The emscripten MEMFS bridge. Used to write OCCT output (`STEP`, `GLB`, `IGES`) to an in-memory path, then read the bytes out. ```typescript const path = `/out_${Date.now()}.step`; writer.Write(path); const bytes = oc.FS.readFile(path) as Uint8Array; oc.FS.unlink(path); ``` | Method | Purpose | | --------------------------- | -------------------------------------------------------------------------------- | | `FS.readFile(path)` | Returns bytes as `Uint8Array` (view into wasm memory — copy out before `unlink`) | | `FS.writeFile(path, bytes)` | Write input file (e.g. for `STEPControl_Reader.ReadFile`) | | `FS.unlink(path)` | Free the MEMFS path | | `FS.mkdir(path)` | Create directory (rare — OCCT writes to `/` by default) | | `FS.readdir(path)` | List directory contents | Full FS API: [emscripten FS reference](https://emscripten.org/docs/api_reference/Filesystem-API.html). ## Heap views [#heap-views] | Property | Type | | ----------------------- | ------------------------------- | | `oc.HEAP8` | `Int8Array` view of wasm memory | | `oc.HEAPU8` | `Uint8Array` | | `oc.HEAP16`, `HEAPU16` | 16-bit views | | `oc.HEAP32`, `HEAPU32` | 32-bit views | | `oc.HEAPF32`, `HEAPF64` | Float views | Use for advanced interop (e.g. reading bytes at a `Standard_Address` pointer returned from a low-level OCCT API). Most consumers never need these. ## Exception helpers [#exception-helpers] ```typescript import init, { getExceptionMessage } from '@taucad/opencascade.js'; // see /docs/package/guides/debugging-wasm-exceptions ``` ## Module re-init [#module-re-init] Calling `init()` a second time **re-instantiates** the wasm module — expensive, and creates two parallel C++ heaps that don't share state. Always memoise behind a singleton; see [init function](/docs/package/reference/ocjs-package-api/init-function). --- # API Reference The full bound OCCT API (5 000+ classes) is exposed as one synthesised Markdown page per package at `/docs/package/api///.mdx`. Use the search endpoint at `/api/search?query=…` to discover entries.