# Package URL: /docs/package Install `libcascade`, import the initialised instance, 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 bound classes — searchable, fragment-stable, LLM-ingestible. Project lineage, maintenance, and how to contribute. ## What's in V3 [#whats-in-v3] * ESM-only build — import the ready instance from the root, or `createInstance()` it yourself. 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 `@libcascade/toolchain` is the dev-time package for building your own OCCT WASM: a typed `libcascade.config.ts`, a CLI that drives the digest-pinned container for you, and an assemble step that turns the artifacts into an npm package surface. If you do not need a custom binary, install the prebuilt [`libcascade`](/docs/package/getting-started/quick-start-npm) package instead. The toolchain is a `devDependency`, it never runs on `postinstall`, and it does nothing until you invoke it. Install, write `libcascade.config.ts`, run `libcascade build`, consume `dist/`. Field-by-field mapping from hand-written yml, ytt templating, and `docker run` scripts. Every `defineBuild` field and the generated unions that check it. One bindings list, N binaries, one `types.d.ts`, `createInstance`. Cut the binding set to the surface your app uses. Declare your own wrapper files and the symbols they provide. ## What the types check [#what-the-types-check] ```typescript notypecheck import { defineBuild } from '@libcascade/toolchain'; export default defineBuild({ name: 'myapp', bindings: ['BRepPrimAPI_MakeBoox'], // ✗ did you mean 'BRepPrimAPI_MakeBox'? settings: { INITIAL_MEMORY: '100mb', // ✗ not a MemorySize ENVIRONMENT: 'web,worker', // ✗ expects an array of literals EXPORT_ES7: true, // ✗ no such -s setting }, variants: [{ name: 'single' }], }); ``` Symbol names come from a 6,257-literal union generated from the release's own API reference. Settings come from the pinned image's own emsdk, with their upstream documentation attached. A toolchain version cannot express a config its own images cannot build. ## Two channels of configuration [#two-channels-of-configuration] Compile-time `OCJS_*` variables are baked into the published image; link-time `settings` and `compilerFlags` are yours. See [Two-channel config model](/docs/toolchain/concepts/two-channel-config-model) for why the split exists and what it buys. --- # Calling OCCT from JavaScript URL: /docs/package/concepts/calling-occt-from-js The libcascade 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] libcascade 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 libcascade source repository 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 libcascade` — the published npm tarball ships the wasm, JS loader, `.d.ts`, API-reference feed, manifests, provenance, and changelogs. If you want the rule in your own project, copy it from the libcascade repo into your local lint setup. ## Primitive-only result objects [#primitive-only-result-objects] Result objects whose fields are all primitives (no nested OCCT objects) do **not** emit `[Symbol.dispose]`. You can use them without `using`: ```typescript const bounds = surface.Bounds(0, 0, 0, 0); console.log(bounds.U1, bounds.U2, bounds.V1, bounds.V2); ``` An OCCT class is still disposable even when it represents a small value. For example, `gp_Pnt` owns a wasm-side C++ object, so both constructed and returned points need `using`: ```typescript using point = p0.Transformed(transform); 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 }` pair). 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 import type { Poly_Triangulation, TopLoc_Location, TopoDS_Edge, TopoDS_Shape, } from 'libcascade'; 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 libcascade-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 the live memory buffer before and after a representative workload. ```typescript const before = oc.wasmMemory.buffer.byteLength; runWorkload(); const after = oc.wasmMemory.buffer.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 generated declarations are the canonical truth. ## `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 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 libcascade source repository — 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. --- # FAQ URL: /docs/package/getting-started/faq ## What is the project lineage? [#what-is-the-project-lineage] libcascade was originally created by [Sebastian Alff (`donalffons`)](https://github.com/donalffons) as a port of [OpenCASCADE Technology (OCCT)](https://dev.opencascade.org/) to WebAssembly via Emscripten. The current project is developed and released from [`taucad/opencascade.js`](https://github.com/taucad/opencascade.js). libcascade does **not** modify the OCCT C++ source beyond a 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] The maintainers and contributors at [`taucad/opencascade.js`](https://github.com/taucad/opencascade.js) own current development, documentation, the GHCR image, and the `libcascade` npm package. Sebastian Alff is credited as the original author. ## How can I contribute? [#how-can-i-contribute] Contributions are welcome: 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. Add focused tests for behavior changes and keep generated bindings generic at the C++ type-system level. 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/toolchain/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 libcascade URL: /docs/package/getting-started/projects-using-libcascade libcascade 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 libcascade three pnpm add -D @types/three typescript ``` ## 2. Import the instance [#2-import-the-instance] ```typescript title="src/build-shape.ts" import oc from 'libcascade'; ``` The root entry probes the host, picks the variant it supports, and initialises the WASM module with a top-level `await`. Module evaluation happens once per realm, so there is nothing to memoise — every importer shares one C++ heap. Need to control *when* that happens, or pass `locateFile` for a relocated binary? Import `createInstance` from `libcascade/init` instead; see [Entry points](/docs/package/reference/libcascade-api/init-function). ## 3. Build a shape [#3-build-a-shape] ```typescript title="src/build-shape.ts" import oc from 'libcascade'; export const buildFilletedBox = async () => { 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 oc, { type TopoDS_Shape } from 'libcascade'; export const shapeToGlb = async (shape: TopoDS_Shape): Promise => { 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 bytes = oc.FS.readFile(path) as Uint8Array; oc.FS.unlink(path); return bytes; }; ``` ## 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, ask for the pthread variant with `createInstance({ variant: 'multi' })` from `libcascade/init`. 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/toolchain/guides/trim-symbols). * Looking for STEP instead of GLB? See [Export STEP](/docs/package/guides/export-step). --- # What is libcascade URL: /docs/package/getting-started/what-is-libcascade libcascade 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. The project is developed at [`taucad/opencascade.js`](https://github.com/taucad/opencascade.js) and published on npm as [`libcascade`](https://www.npmjs.com/package/libcascade). It was originally created by Sebastian Alff (`donalffons`) and is not an official Open CASCADE Technology distribution. ## 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 libcascade [#when-to-use-libcascade] | 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 \~40 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/toolchain/guides/trim-symbols) — cut the wasm from 40 MB down to what your app uses. * [API Reference](/docs/package/api) — search every bound class and method. --- # 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 'libcascade'; import { getOc } from './libcascade-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 libcascade 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 'libcascade'; import { getOc } from './libcascade-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 'libcascade'; import { getOc } from './libcascade-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 rather than only 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. --- # Bundler & locateFile URL: /docs/package/guides/bundler-locatefile Start with `import oc from 'libcascade'`. The package resolves its adjacent WASM asset without configuration in Node and compatible bundlers. Reach for `createInstance` from `libcascade/init` — the entry that accepts options — only when a bundler or deployment copies the binary to another URL. The package exposes the binary through the `libcascade/wasm` subpath export. Every relocation recipe 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 when you want Vite to emit a content-hashed asset URL: ```typescript import { createInstance } from 'libcascade/init'; import wasmUrl from 'libcascade/wasm?url'; const oc = await createInstance({ locateFile: () => wasmUrl }); ``` Add `libcascade` 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: ['libcascade'] }, }); ``` ## 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('libcascade/wasm')); await mkdir('public', { recursive: true }); await copyFile(src, 'public/opencascade_single.wasm'); ``` ```json title="package.json" { "scripts": { "postinstall": "node scripts/copy-wasm.mjs" } } ``` Then reference the public path: ```typescript title="lib/libcascade-init.ts" 'use client'; import { createInstance } from 'libcascade/init'; let ocPromise: ReturnType | undefined; export const getOc = () => (ocPromise ??= createInstance({ locateFile: () => '/opencascade_single.wasm' })); ``` Mark `libcascade` 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: ['libcascade'], }; 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 { createInstance } from 'libcascade/init'; const WASM_DIR = dirname( fileURLToPath(import.meta.resolve('libcascade/wasm')), ); const oc = await createInstance({ 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/libcascade-init.ts" import { createInstance } from 'libcascade/init'; import wasmUrl from 'libcascade/wasm'; const oc = await createInstance({ 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 `libcascade` 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. Use Vite, current Next.js, Bun, Node, Deno, or Webpack 5 instead. ## Common pitfalls [#common-pitfalls] * **Prefer zero configuration**. Add `locateFile` only when the deployed WASM URL differs from the package-adjacent default. * **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 `createInstance` Promise**. Each call instantiates another wasm module with its own C++ heap — memoize behind a singleton. The eager root entry (`import oc from 'libcascade'`) needs no memoisation: ES module evaluation already happens once per realm. ## Multi-threaded variant [#multi-threaded-variant] The pthread-enabled build is a variant of the same package: ask for it with `createInstance({ variant: 'multi' })`. That entry also owns the worker plumbing the raw `libcascade/multi` glue leaves to you. Its wasm is exported at `libcascade/multi/wasm`. **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. **Vite prerequisite:** Emscripten spawns its pthread workers as ES modules with top-level await, which Vite's default `iife` worker format cannot emit: ```typescript title="vite.config.ts" import { defineConfig } from 'vite'; export default defineConfig({ worker: { format: 'es' }, optimizeDeps: { exclude: ['libcascade'] }, }); ``` Run the following **once** after `await createInstance(...)` 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); ``` ### Vite 6+ [#vite-6-1] ```typescript import { createInstance } from 'libcascade/init'; import wasmUrl from 'libcascade/multi/wasm?url'; const oc = await createInstance({ variant: 'multi', locateFile: () => wasmUrl }); oc.BOPAlgo_Options.SetParallelMode(true); oc.BRepMesh_IncrementalMesh.SetParallelDefault(true); ``` ### 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('libcascade/multi/wasm')); await mkdir('public', { recursive: true }); await copyFile(src, 'public/opencascade_multi.wasm'); ``` ```typescript title="lib/libcascade-init-multi.ts" 'use client'; import { createInstance } from 'libcascade/init'; let ocPromise: ReturnType | undefined; export const getOcMulti = () => (ocPromise ??= createInstance({ variant: 'multi', locateFile: () => '/opencascade_multi.wasm', }).then((oc) => { oc.BOPAlgo_Options.SetParallelMode(true); oc.BRepMesh_IncrementalMesh.SetParallelDefault(true); return oc; })); ``` ### Node (ESM) [#node-esm-1] ```typescript import { fileURLToPath } from 'node:url'; import { dirname, join } from 'node:path'; import { createInstance } from 'libcascade/init'; const WASM_DIR = dirname( fileURLToPath(import.meta.resolve('libcascade/multi/wasm')), ); const oc = await createInstance({ variant: 'multi', locateFile: (file: string) => join(WASM_DIR, file), }); oc.BOPAlgo_Options.SetParallelMode(true); oc.BRepMesh_IncrementalMesh.SetParallelDefault(true); ``` 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 OCCT failures compiled with native WASM exception handling reach JavaScript as `WebAssembly.Exception` values. Decode them through the initialized runtime: ```typescript import oc from 'libcascade'; try { using fillet = new oc.BRepFilletAPI_MakeFillet(badShape); fillet.Shape(); } catch (error: unknown) { if (error instanceof WebAssembly.Exception) { const [type, message] = oc.getExceptionMessage(error); console.error(`${type}: ${message}`); } else { throw error; } } ``` `error.message` is not the C++ failure text. The helper returns a `[type, message]` tuple such as `['Standard_Failure', 'BRepFilletAPI_MakeFillet: Empty argument']`. ## Common failures [#common-failures] | Type or message | Likely cause | | -------------------------- | ------------------------------------------ | | `Standard_OutOfRange` | Index past an OCCT collection boundary | | `Standard_NullObject` | Method called through a null `Handle` | | `Standard_TypeMismatch` | Downcast to the wrong `TopoDS_*` subtype | | `BRepAlgoAPI_*` | Invalid or self-intersecting boolean input | | `BRepFilletAPI_MakeFillet` | Radius too large or a degenerate edge | ## Debug builds [#debug-builds] For source-level debugging, build with `-g -fwasm-exceptions -O1` and use [Chrome's WASM DWARF debugger](https://developer.chrome.com/blog/wasm-debugging-2020). --- # 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 'libcascade'; import { getOc } from './libcascade-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 bytes = oc.FS.readFile(path) as Uint8Array; oc.FS.unlink(path); return bytes; }; ``` ## 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 'libcascade'; import { getOc } from './libcascade-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 bytes = oc.FS.readFile(path) as Uint8Array; oc.FS.unlink(path); return bytes; }; ``` 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 './libcascade-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 bytes = oc.FS.readFile(path) as Uint8Array; oc.FS.unlink(path); return bytes; }; ``` ## 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. * `FS.readFile` returns owned bytes that remain valid after `unlink`. --- # Multi-threaded build URL: /docs/package/guides/multi-threading The npm package ships **two** pre-built variants of one binding set: | Variant | Binary | When to use | | -------- | ------------------------- | -------------------------------------------------------------------- | | `single` | `opencascade_single.wasm` | Embeddable widgets, one-op-per-click UX, no COOP/COEP | | `multi` | `opencascade_multi.wasm` | Batch mesh/boolean, STEP→glTF pipelines, COOP/COEP-isolated surfaces | Ask for the threaded one by name: ```typescript import { createInstance } from 'libcascade/init'; const oc = await createInstance({ variant: 'multi' }); ``` `createInstance` owns the worker plumbing — the glue self-reference Emscripten spawns pthread workers from, Node `file:` URL → path conversion, and OCCT thread-pool sizing. Omit `variant` and it selects the most capable variant this host supports, which is `multi` wherever the capability probe passes. Pass `threadCount` to cap OCCT's default pool. Both variants share one `OpenCascadeInstance` type, so a shape from either is assignable wherever the other is expected. See [Bundler & locateFile](/docs/package/guides/bundler-locatefile#multi-threaded-variant) for Vite, Next.js, and Node recipes, and [Entry points](/docs/package/reference/libcascade-api/init-function) for the full options contract. 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] | Measured workload | Speedup with 12 workers | Worth the COOP/COEP cost? | | ------------------------------ | ----------------------: | -------------------------------------- | | STEP import + incremental mesh | 1.33× | Yes, for visualisation pipelines | | Boolean cut grid | 1.81× | Yes, for batch CAD operations | | Incremental mesh | 1.06× | Only after profiling the real assembly | | Tiny two-shape boolean fuse | 0.44× | No — pool overhead dominates | | Complete 11-sample suite | 1.24× | Yes, for sustained mixed workloads | 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. Under Vite, also set `worker: { format: 'es' }`: Emscripten's pthread workers are ES modules with top-level await, which the default `iife` worker format cannot emit. ## 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 createInstance({ variant: 'multi' })`. oc.BOPAlgo_Options.SetParallelMode(true); // booleans oc.BRepMesh_IncrementalMesh.SetParallelDefault(true); // meshing // Optional: inspect the lazily-created pool. -1 means "use all logical // processors" and is already the default. const pool = oc.OSD_ThreadPool.DefaultPool(-1); console.log(pool.NbThreads()); ``` After the two global toggles every `new BRepMesh_IncrementalMesh(shape, lin)` and every `BRepAlgoAPI_*` automatically fans out across the pool. **No per-call argument required.** OCCT's default pool already uses `NbLogicalProcessors`, and its default per-launch fan-out is the full pool. Only call `Init` or `SetNbDefaultThreadsToLaunch` when deliberately capping concurrency, and do so before the pool's first job. ### 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. libcascade 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** added 188 ms in the 12-worker benchmark, scaling with pool size. Memoise the `createInstance()` 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. A typed-array view can become stale after `memory.grow`, so advanced pointer interop must create fresh views from `oc.wasmMemory.buffer` after calls that may allocate. 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` entry in the shipped build's `rawFlags`) and the symbol is dead-code-eliminated at module load. The warning is informational; no runtime call ever resolves to it. * **Create one typed-array view per allocation-free batch**. Do not retain it across a call that may grow memory: ```typescript const heap = new Uint8Array(oc.wasmMemory.buffer); for (let i = 0; i < n; i++) { heap[buf + i] = bytes[i]; } ``` ### `toResizableBuffer()` support matrix (August 2026) [#toresizablebuffer-support-matrix-august-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 keep typed-array views valid across growth entirely by returning a `SharedArrayBuffer` that automatically tracks `memory.grow`. The shipped `libcascade/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 154+ | Yes | | Firefox ≤153 | **No** — [WebAssembly worker transfer can hang](https://bugzilla.mozilla.org/show_bug.cgi?id=2021136) despite exposing the API | | 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 shipped `multi` variant therefore keeps `GROWABLE_ARRAYBUFFERS` off so the binary stays compatible with Node-based test runners, Bun, Deno, and the legacy mobile browser. A custom build can add a [browser-only variant](/docs/toolchain/guides/multi-threading#browser-only-mt-build) for deployments that have audited their runtime matrix and want the resizable-buffer fast path. For most consumers the cost is invisible — OCCT's per-call cost on mesh/boolean dominates the typed-view refresh cost by 2-3 orders of magnitude. Reach for the browser-only variant 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 variant wins on simplicity. Force it with `createInstance({ variant: 'single' })`, or globally before any import: ```javascript globalThis[Symbol.for('libcascade.select')] = 'single'; ``` ## Custom builds [#custom-builds] Need a smaller threaded binary (trimmed symbol list) or different Emscripten settings? See [Toolchain — Custom multi-threaded build](/docs/toolchain/guides/multi-threading) for the config recipe and `PTHREAD_POOL_SIZE` rationale. --- # Render with three.js URL: /docs/package/guides/render-with-three-js libcascade 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 `libcascade` 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 'libcascade'; 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 'libcascade'; 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). --- # Bindgen pipeline URL: /docs/toolchain/concepts/bindgen-pipeline **Maintainer track.** Read this if you rebuild libcascade from source or extend it with custom C++. If you `pnpm add libcascade` and call the published wasm from JS, you can skip this page — the consumer-facing rules live in [Calling OCCT from JS](/docs/package/concepts/calling-occt-from-js) and [Return shapes](/docs/package/concepts/return-shapes). The libcascade 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_single.wasm] C1 --> C3[dist/opencascade_single.js] A5 --> C4["dist/opencascade_single.d.ts (build intermediate)"] end subgraph "Stage 4 — package assembly" C4 --> D1[libcascade assemble] D1 --> D2[dist/types.d.ts + dist/variant.d.ts] end ` ``` ## Stage 1 — bindgen [#stage-1--bindgen] The in-image `bindings` stage invokes the Python bindgen which: 1. Walks every header reachable from the rendered yml's `bindings:` list 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 — the ones your config's `bindings` array resolved to — 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). `.d.ts` is a build intermediate. `libcascade assemble` verifies that the variant declarations agree, then publishes the shared surface as `types.d.ts` and `variant.d.ts`; npm packages do not ship per-variant declaration files. ## How custom C++ enters the pipeline [#how-custom-c-enters-the-pipeline] A `customBindings` entry with the default `scope: 'all'` is concatenated into one translation unit which flows through a smaller variant of stages 1+2 — bindgen discovers Handle/NCollection references, `em++` compiles, and the result links into the final wasm alongside the auto-generated bindings. `scope: 'main'` skips bindgen entirely. Those `.cpp` contents compile as raw Embind registrations and may include their helper implementations. See [Extend with C++](/docs/toolchain/guides/extend-with-cpp). ## 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 a `scope: 'main'` file 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/package/concepts/calling-occt-from-js) — overload dispatch, enums, defaults, and the `TopoDS` downcast bridge. * [Return shapes](/docs/package/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 libcascade from source; a custom build configures channel 2 only. libcascade 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_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 libcascade'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 settings and flags [#channel-2--link-time-settings-and-flags] Set per-consumer-build in `libcascade.config.ts`. Apply only to the final `emcc` link step. ```typescript notypecheck settings: { MODULARIZE: true, EXPORT_ES6: true, ALLOW_MEMORY_GROWTH: true, WASM_BIGINT: true, EVAL_CTORS: 2, }, compilerFlags: { optimize: 'O3' }, ``` 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 one `.o` file per generated binding 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 from their own config, 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_CLOSURE=true` + `OCJS_CONVERGE=true` * `THREADING=single-threaded` The matching config carries `WASM_BIGINT: true` and `EVAL_CTORS: 2` at link time. The threaded variant retains BigInt but removes eval-ctors with `EVAL_CTORS: null`. 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_single.provenance.json`, which captures the active preset, compile flags, and commit SHA the wasm was built from. 2. Confirm channel-2 settings actually made it into the wasm (`libcascade build --render-only` prints the rendered flag list first): ```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). --- # Variants and assemble URL: /docs/toolchain/concepts/variants-and-assemble A custom build usually ships more than one binary from the same symbol set: a single-threaded one that loads anywhere, and a pthread-enabled one for hosts that are cross-origin isolated. `variants` expresses that in the config, and `libcascade assemble` turns the resulting artifacts into a package surface. ## Variants share one bindings list [#variants-share-one-bindings-list] ```typescript notypecheck variants: [ { name: 'single', settings: { EVAL_CTORS: 2 } }, { name: 'multi', compilerFlags: { threads: true }, settings: { PTHREAD_POOL_SIZE: 'navigator.hardwareConcurrency', SHARED_MEMORY: true }, }, ], ``` Two binaries, one `bindings` array, a handful of flags of difference. Nothing templates one config into two. Note what the multi variant does *not* declare. `compilerFlags: { threads: true }` renders `-pthread`, and the `requires: ['threads']` that drives image selection and the generated capability probe is **inferred** from exactly that — a build against shared memory cannot run without `SharedArrayBuffer`, so the capability is a consequence of the flags rather than something you restate. Declare `requires` yourself only to add a capability the flags do not imply. That sharing is load-bearing. When variants can drift in their symbol lists, their generated `.d.ts` files drift too, and a consumer holding a `TopoDS_Shape` from one variant cannot pass it to a function typed against the other — the two `OpenCascadeInstance` types are structurally incomparable despite describing the same classes. Sharing the list makes one shared surface possible. A per-variant `bindings` extension is allowed but discouraged for exactly that reason. ## A capability is both a build and a runtime fact [#a-capability-is-both-a-build-and-a-runtime-fact] A variant's `threads` capability does two things: * **At build time** it selects the multi-threaded container image for that variant. * **At load time** it becomes the capability the generated selector probes: `SharedArrayBuffer` present ∧ (`globalThis.crossOriginIsolated` ?? running under Node). Capabilities are inferred from the variant's effective build flags — `compilerFlags.threads`, a `-pthread` raw flag, or a `SHARED_MEMORY` / `USE_PTHREADS` setting all imply `threads`. An explicit `requires` array is unioned with what was inferred, so it can add a capability but never remove one: a build that genuinely needs shared memory cannot be configured to skip its own probe. A variant with no capabilities is loadable everywhere. ## What `assemble` writes [#what-assemble-writes] ```bash npx libcascade assemble ``` | File | Role | | ------------------------- | --------------------------------------------------------------------------------------------------- | | `types.d.ts` | One d.ts unioning every variant's surface. Symbols only some variants bind are typed optional. | | `init.js` / `init.d.ts` | The `./init` subpath — `createInstance`, the selector, and the override symbol. | | `init..js` | The `.//init` subpath — the same entry pinned to one variant. Multi-variant packages only. | | `index.js` / `index.d.ts` | The root entry, in `eager` or `factory` mode. | | `variant.d.ts` | Types for the raw per-variant glue subpaths (`./single`, `./multi`, …). | | `exports.json` | The `exports` fragment to merge into `package.json`. | The shared d.ts and the eager barrel are rendered from one symbol list computed once, so the declaration and the runtime export cannot disagree. ## `createInstance` [#createinstance] `./init` is the entry every consumer can use, in every mode: ```typescript notypecheck import { createInstance } from 'my-occt-package/init'; const oc = await createInstance(); // most capable variant const single = await createInstance({ variant: 'single' }); const capped = await createInstance({ variant: 'multi', threadCount: 4 }); ``` | Option | Effect | | ------------- | ------------------------------------------------------------------------------------------------------- | | `variant` | Load a named variant instead of the selected one. | | `threadCount` | Size OCCT's default thread pool and raise its launch cap to match. Omitted, OCCT sizes the pool itself. | | `locateFile` | Emscripten's WASM URL resolver, for bundlers that relocate the binary. | | `wasmBinary` | Pre-fetched bytes, instead of a network fetch. | | `wasmMemory` | A `WebAssembly.Memory` you own. | It owns the plumbing every consumer used to reimplement: * **Glue self-reference.** Emscripten spawns pthread workers from the glue's own URL; `createInstance` passes it as `mainScriptUrlOrBlob` so you do not have to know that name. * **Node `file:` URL → path conversion.** Node's `Worker` takes a path, and a `file:` URL's pathname keeps a leading slash before a Windows drive letter that Node rejects. * **OCCT thread-pool sizing.** For a `threads` variant it calls `OSD_ThreadPool.DefaultPool(…)` and `SetNbDefaultThreadsToLaunch(…)` so the lazily-created default pool does not stay smaller than the worker count baked into the binary. * **An actionable error** when you ask for a variant this host cannot run, naming the unmet capability and how to provide it. ## `.//init` — one variant, one glue [#variantinit--one-variant-one-glue] A multi-variant package also generates a **pinned** entry per variant: ```typescript notypecheck import { createInstance } from 'my-occt-package/single/init'; const oc = await createInstance(); // always the single variant ``` It exports the same `createInstance` and owns the same plumbing. The one thing it does differently is name exactly one glue file. That matters because of how bundlers treat asset URLs. The shared `./init` must be able to *reach* every variant — the choice is made at runtime — so it contains one `new URL('./.js', import.meta.url)` per variant. Vite's `vite:asset-import-meta-url` transform emits an asset for every one of those at transform time, **before** tree-shaking, and no branch is ever statically dead. An app that only ever loads the single variant therefore still ships the multi variant's glue JS — 60–80 KB it never fetches. The pinned entry contains one such URL, so nothing else is emitted. The `.wasm` is unaffected either way. The glue import is deliberately opaque to bundlers, so only the binary a consumer references itself — via `.//wasm` or `locateFile` — enters a build. That property holds for both entries and is pinned by a Vite host-app test in the toolchain suite. | | `./init` | `.//init` | | ------------------------------------- | ------------------------ | -------------------------------------------------- | | Variant | selected at load time | fixed at import time | | Glue assets in the bundle | one per declared variant | one | | Capability probe + actionable error | yes | yes | | `Symbol.for('.select')` override | picks the variant | must name this entry's variant, or the call throws | Pick `./init` when the app should adapt to the host (a library, or a page that may or may not be cross-origin isolated). Pick `.//init` when the answer is already known — a Node CLI that always wants threads, a page that deliberately serves the single-threaded build — and the bytes matter. Single-variant packages get no pinned entry: their `./init` already resolves exactly one glue. A pinned entry knows about one variant and nothing else, so both `createInstance({ variant: 'other' })` and a `Symbol.for('.select')` override naming another variant fail with `Unknown variant "other". Declared: single.` — loudly, rather than by quietly handing back a variant you did not ask for. ## Root modes [#root-modes] ### `factory` [#factory] ```typescript notypecheck assemble: { exports: 'factory' } ``` The root re-exports `createInstance`. Nothing is instantiated until you ask. The factory entries re-export the shared surface with `export type *`, so `import type { TopoDS_Shape } from 'my-occt-package'` keeps working in type position without promising a runtime value the root does not export. Pick this when your package is a library whose consumers decide when — and whether — the WASM module comes up. ### `eager` [#eager] ```typescript notypecheck assemble: { exports: 'eager' } ``` The root probes capabilities, selects a variant, initialises it with a top-level `await`, and re-exports every bound symbol as a named value: ```typescript notypecheck import oc, { BRepPrimAPI_MakeBox } from 'libcascade'; using box = new BRepPrimAPI_MakeBox(10, 10, 10); using also = new oc.BRepPrimAPI_MakeBox(10, 10, 10); ``` That is the mode the `libcascade` package itself ships. Named value exports are what make `gp_Pnt` usable as a value in an external editor instead of failing with "cannot be used as a value". Importing `./init` never evaluates the eager root, so a consumer who wants the factory never pays for an instantiation they did not ask for. ## Selection and the override symbol [#selection-and-the-override-symbol] The selector picks the **most capable** variant whose `requires` all probe true; configuration order breaks ties. To force a choice, set the override before importing the root or calling `createInstance`: ```javascript notypecheck globalThis[Symbol.for('libcascade.select')] = 'single'; ``` The symbol key is `.select`. It is a pre-import override: set it in a module that runs first, not after the root has already evaluated. ## The exports fragment [#the-exports-fragment] ```bash npx libcascade assemble --write-exports ``` ```json title="package.json (generated subpaths)" { "exports": { ".": { "types": "./dist/index.d.ts", "default": "./dist/index.js" }, "./init": { "types": "./dist/init.d.ts", "default": "./dist/init.js" }, "./single": { "types": "./dist/variant.d.ts", "default": "./dist/myapp_single.js" }, "./single/init": { "types": "./dist/init.d.ts", "default": "./dist/init.single.js" }, "./single/wasm": "./dist/myapp_single.wasm", "./multi": { "types": "./dist/variant.d.ts", "default": "./dist/myapp_multi.js" }, "./multi/init": { "types": "./dist/init.d.ts", "default": "./dist/init.multi.js" }, "./multi/wasm": "./dist/myapp_multi.wasm" } } ``` The pinned entries reuse `init.d.ts` — they export the same surface, so there is no reason to generate N copies of the same declarations. The merge keeps every subpath you declared yourself — hand-written aliases such as `./wasm` or `./api-reference.json` keep resolving. It does not touch `files`; add the generated files to that list once. ## What `dist/` holds, and what ships [#what-dist-holds-and-what-ships] `dist/` is not a publish manifest. It holds three classes of file, and **`files` in `package.json` is the authoritative statement of which ones ship**: | Class | Files | Ships | | ---------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------- | | Shipped surface | `types.d.ts`, `init.js` / `init.d.ts`, `init..js`, `index.js` / `index.d.ts`, `variant.d.ts`, `.js`, `.wasm` | Yes — this is what `exports` points at | | Durable records | `.build-manifest.json`, `.provenance.json`, `.js.symbols` | Your call — `libcascade` ships all three so consumers can audit the build | | In-repo build products | `.d.ts` (one per variant), `exports.json` | No | The per-variant `.d.ts` files look like leftovers next to the generated `types.d.ts`, and they are not. They are the container `dts` step's artifact, with two real consumers: * **`assemble` parses every one of them** to build the shared `types.d.ts`. Delete them and `assemble` cannot run. * **The bindgen's own type tests import them directly.** In this repository, 30 files under `tests/` do `import type { … } from '../dist/opencascade_single'`, because the per-variant d.ts *is* the artifact under test. They cannot be repointed at `types.d.ts`, which is a different thing — a cross-variant union. So they are build products consumed in-repo that do not publish. Leave them in `dist/` and leave them out of `files`; `exports.json` is likewise a review aid for `--write-exports` and never ships. ## Consumer prerequisites for a threads variant [#consumer-prerequisites-for-a-threads-variant] ### Cross-origin isolation [#cross-origin-isolation] Browsers gate `SharedArrayBuffer` behind cross-origin isolation. Every page that loads the threaded binary must be served with: ```http Cross-Origin-Opener-Policy: same-origin Cross-Origin-Embedder-Policy: require-corp ``` Without them the capability probe fails, the selector falls back to a variant without `requires`, and an explicit `createInstance({ variant: 'multi' })` throws with that remediation in the message. Node exposes `SharedArrayBuffer` unconditionally — no headers needed. ### Vite needs ES-module workers [#vite-needs-es-module-workers] Emscripten spawns its pthread workers as ES modules with top-level await. Vite's default worker format is `iife`, which cannot emit them: ```typescript title="vite.config.ts" notypecheck export default { worker: { format: 'es' }, }; ``` ### Bundlers and the glue [#bundlers-and-the-glue] The generated `init` entries reference each glue with `new URL(…, import.meta.url)`, so bundlers emit it as a first-class asset, but load it through an import that is deliberately opaque to them (`import(/* webpackIgnore: true */ /* @vite-ignore */ href)`). Cutting that graph edge is what keeps the `.wasm` out: a statically analysable import makes a bundler follow the glue, and a pthread build contains `new Worker(new URL(, import.meta.url))`, so the glue gets re-bundled as a worker entry and every variant's binary comes with it. No configuration is needed either way. The glue resolves its `.wasm` sibling relative to its own URL; pass `locateFile` only when your deployment moves the binary away from the glue. See [Bundler and locateFile](/docs/package/guides/bundler-locatefile) for per-bundler recipes. ## Related [#related] * [Config reference](/docs/toolchain/reference/config) — the `variants` and `assemble` fields. * [CLI reference](/docs/toolchain/reference/cli) — `assemble` flags. * [Custom multi-threaded build](/docs/toolchain/guides/multi-threading) — the threads variant end to end. --- # Migrate from a yml build URL: /docs/toolchain/getting-started/migrate-from-yaml If your package builds a custom OCCT WASM today, it probably owns some combination of: a hand-written build yml, a templating layer to express variants, a `docker run … && mv …` script per variant, one `.d.ts` per variant, and a hand-maintained image tag. All five collapse into one config file and two commands. This guide follows the shape of the largest real migration — `replicad-opencascadejs`, which had every one of those pieces. ## Before [#before] ```json title="package.json (before)" { "scripts": { "build": "pnpm run generateConfig && pnpm run buildSingle && pnpm run buildMulti", "generateConfig": "ytt -f build-source/ --output-files build-config", "buildSingle": "cd build-config && docker run --rm -v \"$(pwd):/src\" -u \"$(id -u):$(id -g)\" ghcr.io/taucad/opencascade.js:canary-ebd263f1-single-threaded link custom_build_single.yml && mkdir -p ../dist && mv replicad_single.js replicad_single.wasm replicad_single.d.ts ../dist/ && cd -", "buildMulti": "cd build-config && docker run --rm … multi-threaded link custom_build_multi.yml && mv … && cd -" } } ``` ```text build-source/defaults.yml # bindings + buildFlags, ~300 lines build-source/custom_build_single.yml # ytt template: name + EVAL_CTORS build-source/custom_build_multi.yml # ytt template: name + pthread flags build-config/custom_build_single.yml # generated, committed build-config/custom_build_multi.yml # generated, committed dist/replicad_single.d.ts # ~260k lines dist/replicad_multi.d.ts # ~260k near-identical lines ``` ## After [#after] ```json title="package.json (after)" { "scripts": { "build": "pnpm run buildSingle && pnpm run buildMulti && pnpm run assemble", "buildSingle": "libcascade build --variant single", "buildMulti": "libcascade build --variant multi", "assemble": "libcascade assemble --write-exports" }, "devDependencies": { "@libcascade/toolchain": "3.0.0" } } ``` ```text libcascade.config.ts # the only build input build-config/wrappers/*.cpp # unchanged, referenced in place dist/types.d.ts # one shared surface dist/init.js, dist/index.js # generated entries ``` ## Step 1 — install the toolchain [#step-1--install-the-toolchain] ```bash npm install --save-dev @libcascade/toolchain ``` ## Step 2 — run the migrator [#step-2--run-the-migrator] ```bash npx libcascade migrate build-config/custom_build_single.yml \ build-config/custom_build_multi.yml \ --out libcascade.config.ts ``` Pass every variant's yml to the one invocation. Two ymls that differ only in a name and a handful of flags are one config with two variants, so that is what comes out: the flags they share become the base, the rest become each variant's delta. The command writes nothing over an existing file without `--force`, and prints what a human has to check to stderr. The rest of this step is the mapping it applied — read it to review the result, or to do the translation by hand if your build is far enough from the reference shape that `migrate` refuses it. ### The mapping [#the-mapping] The container-side yml has six concepts. Each maps to one `defineBuild` field. | yml | `defineBuild` | | -------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `mainBuild.name: replicad_single.js` | `name: 'replicad'` + `variants: [{ name: 'single' }]` — the artifact base name is derived, and the `.js` suffix disappears. Keep an exact legacy filename with `variants[].outputName`. | | `mainBuild.bindings[].symbol` | `bindings: [...]` — flat strings instead of a list of one-key maps. | | `mainBuild.emccFlags: -sNAME=value` | `settings: { NAME: value }` — typed, one entry per `-s` flag. | | `mainBuild.emccFlags: -O3 / -msimd128 / -fwasm-exceptions / --no-entry / -flto` | `compilerFlags: { optimize, simd, exceptions, noEntry, lto }`. | | any other `emccFlags` entry (`-pthread`, `-Wl,--allow-undefined`, `--emit-symbol-map`) | `rawFlags: [...]`. | | `mainBuild.additionalBindFiles` | `customBindings: [{ file, symbols, scope: 'main' }]`. | | `additionalCppFiles` (top level) | `customBindings: [{ file, symbols }]` — `scope` defaults to `'all'`. | | `extraBuilds[]` | `variants[]`. | | `generateTypescriptDefinitions` | same name, same meaning. | Value translations worth calling out: | yml flag | `settings` entry | | ------------------------------------------------ | ------------------------------------------------ | | `-sALLOW_MEMORY_GROWTH=1` | `ALLOW_MEMORY_GROWTH: true` | | `-sINITIAL_MEMORY=100MB` | `INITIAL_MEMORY: '100MB'` | | `-sSTACK_SIZE=8388608` | `STACK_SIZE: 8_388_608` | | `-sEXPORTED_RUNTIME_METHODS=["FS","wasmMemory"]` | `EXPORTED_RUNTIME_METHODS: ['FS', 'wasmMemory']` | | `-sENVIRONMENT=web,worker,node` | `ENVIRONMENT: ['web', 'worker', 'node']` | | `-sERROR_ON_UNDEFINED_SYMBOLS=0` | `ERROR_ON_UNDEFINED_SYMBOLS: false` | | `-sWASM_BIGINT` (bare) | `WASM_BIGINT: true` | Any `emccFlags` entry none of those rows claims is kept in `rawFlags` verbatim and listed in the generated header — `migrate` never drops a flag. An unknown *key* is the other way round: it stops the command, because the yml schema has no pass-through bucket for one. The ytt layer has no counterpart because it has nothing left to do: the shared list is the base config, and the templates were only ever adding a variant name and a handful of flags. For replicad's two ymls, the emitted config reads: ```typescript title="libcascade.config.ts" import { defineBuild } from '@libcascade/toolchain'; export default defineBuild({ name: 'replicad', bindings: [/* the flat contents of defaults.yml */], customBindings: [ { file: 'build-config/wrappers/shape-hasher.cpp', symbols: ['OCJS_ShapeHasher'] }, { file: 'build-config/wrappers/mesh-extractor.cpp', symbols: ['ReplicadMeshData', 'ReplicadMeshExtractor'] }, // …one entry per wrapper file ], settings: { EXPORT_ES6: true, MODULARIZE: true, ALLOW_MEMORY_GROWTH: true, INITIAL_MEMORY: '100MB', MAXIMUM_MEMORY: '4GB', EXPORTED_RUNTIME_METHODS: [ 'FS', 'wasmMemory', 'getExceptionMessage', 'incrementExceptionRefcount', 'decrementExceptionRefcount', ], ENVIRONMENT: ['web', 'worker', 'node'], ERROR_ON_UNDEFINED_SYMBOLS: false, STACK_SIZE: 8_388_608, WASM_BIGINT: true, EVAL_CTORS: 2, }, compilerFlags: { exceptions: 'wasm', noEntry: true, simd: true, optimize: 'O3' }, variants: [ { name: 'single' }, { name: 'multi', compilerFlags: { threads: true }, settings: { EVAL_CTORS: null, PTHREAD_POOL_SIZE: 'navigator.hardwareConcurrency', SHARED_MEMORY: true, }, }, ], assemble: { exports: 'factory' }, }); ``` The base holds what both ymls agreed on; the `multi` variant holds the three lines they differed by. `EVAL_CTORS: null` is how a variant **removes** an inherited setting — the single-threaded yml had it and the multi-threaded one did not, because constructor evaluation order is non-deterministic under pthread workers. `requires: ['threads']` is absent on purpose: `compilerFlags.threads` implies it. ### Review 1 — `customBindings` needs a symbol list your yml never declared [#review-1--custombindings-needs-a-symbol-list-your-yml-never-declared] The yml listed wrapper file paths and, separately, the symbols those files provide — in the same `bindings:` array as the OCCT ones, with nothing tying the two together. `customBindings` makes the link explicit: each file declares what it provides, which is what lets `bindings` stay type-checked (custom names are accepted precisely because a wrapper declared them) and what keeps [`detect` and `check`](/docs/toolchain/guides/detect-and-check) from reporting your own classes as unknown. `migrate` reconstructs the link by reading each `.cpp` for the classes it registers — top-level `class` / `struct` definitions and Embind `class_("Name")` calls. That is a derivation, not a declaration, so it is the first thing to check. Where it finds nothing it writes a `TODO(libcascade migrate)` marker with the candidate names instead of guessing; `libcascade build` refuses the config until the marker is resolved. Each `Replicad*`-style name stays in `bindings` too — that is what requests it. ### Review 2 — the exception-helpers delta [#review-2--the-exception-helpers-delta] emsdk 6.0.5 removed `-sEXPORT_EXCEPTION_HANDLING_HELPERS`. If your yml carried it, `migrate` deletes it and exports the three helpers directly — because with `-fwasm-exceptions` the link pipeline **hard-fails** without them: ```diff - -sEXPORT_EXCEPTION_HANDLING_HELPERS - -sEXPORTED_RUNTIME_METHODS=["FS","wasmMemory"] + EXPORTED_RUNTIME_METHODS: [ + 'FS', + 'wasmMemory', + 'getExceptionMessage', + 'incrementExceptionRefcount', + 'decrementExceptionRefcount', + ], ``` This is the same delta the upstream `full.yml` received. The other rewrite `migrate` applies is `-sUSE_PTHREADS=1` → `compilerFlags: { threads: true }`: emcc keeps that setting name only as a deprecated legacy alias of the `-pthread` your yml already carried beside it, so the config states the request once. Both rewrites are commented at their site in the generated file. ### Review 3 — the rest of the generated header [#review-3--the-rest-of-the-generated-header] The header block `migrate` writes names everything else it decided for you: | Item | What to check | | ----------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `assemble.exports: 'factory'` | The yml has no equivalent concept. `'factory'` exposes only the variant-selecting `createInstance`; switch to `'eager'` if consumers import OCCT names off the package root. | | Variant names | Split from the artifact names (`replicad_single` → `single`). A name that does not fit `_` keeps its exact published filename through `variants[].outputName`. | | `rawFlags` | Every flag no typed member models, listed. Each is passed to emcc unchanged; the question is whether you still want it. | ## Step 3 — replace the build scripts [#step-3--replace-the-build-scripts] ```bash npx libcascade build --render-only ``` Renders the yml the container will actually receive, without needing an engine. Diff it against your committed `build-config/*.yml` — flag order differs (the renderer emits a canonical order; emcc is order-insensitive for distinct flags), but the flag *set* should match. That diff is the migration's first gate. Then build for real: ```bash npx libcascade build ``` The `mv` chains are gone: the driver mounts a scratch output directory, points `OCJS_OUTPUT_DIR` at it, and moves artifacts into `dist/` only after the run exits 0. `cd build-config && … && cd -` is gone too — paths resolve relative to the config file. The image tag is gone as well. The digest for your toolchain version is embedded in the package and verified after the pull, so the tag-drift problem — a tag string in a shell script that had to be kept in sync by hand with the `libcascade` version you depend on — no longer exists. ## Step 4 — assemble the package surface [#step-4--assemble-the-package-surface] ```bash npx libcascade assemble --write-exports ``` This is where the duplicated `.d.ts` files die. One `types.d.ts` describes both variants, so a shape produced by the single-threaded instance is assignable to a function typed against the multi-threaded one. Downstream `as unknown as` erasure written to work around that incomparability can be deleted. `--write-exports` merges the generated subpaths (`.`, `./init`, `./single`, `./single/wasm`, `./multi`, `./multi/wasm`) into your `package.json` while preserving hand-written ones such as `./wasm`. Add the generated files to `files` once. ## Step 5 — move consumers onto `createInstance` [#step-5--move-consumers-onto-createinstance] Whatever your package taught before — a hand-written `initOCSingle.js`, tests passing `mainScriptUrlOrBlob` themselves, per-variant loader modules in a downstream app — collapses into the generated `./init` entry: ```typescript notypecheck import { createInstance } from 'replicad-opencascadejs/init'; const oc = await createInstance(); // best available const mt = await createInstance({ variant: 'multi' }); // explicit ``` See [Variants and assemble](/docs/toolchain/concepts/variants-and-assemble) for the selector, the override symbol, and the cross-origin-isolation requirement. ## Step 6 — delete [#step-6--delete] * `build-source/` and every ytt template. * The generated `build-config/*.yml`. * `ytt` from your tooling — the dependency and any CI install step. * The `generateConfig` script. * The `docker run` / `mkdir -p` / `mv` / `cd -` script bodies. * The per-variant `.d.ts` files from `dist/` and from `files`. * Any hard-coded `ghcr.io/taucad/opencascade.js:` string. Keep your `wrappers/*.cpp` exactly where they are — `customBindings` references them in place. Add `.libcascade/` to `.gitignore`. ## Step 7 — prove parity [#step-7--prove-parity] 1. `build-manifest.json` deltas — requested, compiled, and alias-resolved counts — match the pre-migration build for each variant. 2. Your package's own test suite passes against the regenerated artifacts. 3. Downstream consumers build and run with the loaders replaced. Then wire the drift guard in, so the class of failure custom builds actually suffer from cannot come back silently: ```bash npx libcascade check src ``` ## Related [#related] * [Config reference](/docs/toolchain/reference/config) — every field and its type story. * [CLI reference](/docs/toolchain/reference/cli) — `build`, `assemble`, `detect`, `check`. * [Container yml contract](/docs/toolchain/reference/yaml-schema) — what the renderer emits, for when you diff it. --- # Quickstart — custom build URL: /docs/toolchain/getting-started/quick-start Most applications should install the prebuilt [`libcascade`](/docs/package/getting-started/quick-start-npm) package and stop there. Reach for the toolchain when you need a WASM binary the prebuilt one cannot be: a trimmed symbol set, your own C++ wrappers, or different Emscripten link settings. **Pre-release.** `@libcascade/toolchain` ships lockstep-versioned with `libcascade` — the toolchain that built the binary you depend on is the one whose types describe it. Both are at `3.0.0-beta.0` until the v3 release. ## Prerequisites [#prerequisites] * Node 22+. * A container engine: Docker Desktop, [colima](https://github.com/abiosoft/colima), Rancher Desktop, OrbStack, or Podman. The toolchain drives the published image for you — you never write a `docker` command. * A working directory under your home folder on macOS/Windows. Docker Desktop does not share `/tmp` or `/opt` into its VM by default, and outputs vanish silently. GitHub-hosted macOS runners ship no container engine at all. Run toolchain builds on a Linux runner. ## 1. Install [#1-install] ```bash npm install --save-dev @libcascade/toolchain ``` It is a `devDependency` on purpose: it shells out to a container engine and must never appear in a production dependency graph. It has no `postinstall` hook and does nothing until you invoke it. ## 2. Write the config [#2-write-the-config] ```typescript title="libcascade.config.ts" import { defineBuild } from '@libcascade/toolchain'; export default defineBuild({ name: 'myapp', bindings: [ 'BRepPrimAPI_MakeBox', 'BRepAlgoAPI_Cut', 'BRepMesh_IncrementalMesh', 'STEPControl_Reader', 'STEPControl_Writer', 'TopExp_Explorer', 'TopoDS_Shape', 'gp_Pnt', ], settings: { MODULARIZE: true, EXPORT_ES6: true, ALLOW_MEMORY_GROWTH: true, INITIAL_MEMORY: '100MB', MAXIMUM_MEMORY: '4GB', // Required with `exceptions: 'wasm'` on emsdk 6.0.5 — the link fails // without these three. See the exception-helpers note below. EXPORTED_RUNTIME_METHODS: [ 'FS', 'getExceptionMessage', 'incrementExceptionRefcount', 'decrementExceptionRefcount', ], ENVIRONMENT: ['web', 'worker', 'node'], // OCCT's OSD_MemInfo references mallinfo, which Emscripten does not provide. ERROR_ON_UNDEFINED_SYMBOLS: false, }, compilerFlags: { optimize: 'O3', simd: true, exceptions: 'wasm', noEntry: true }, variants: [{ name: 'single' }], assemble: { exports: 'factory' }, }); ``` Everything in that file is compile-checked. A typo in `bindings` is a TypeScript error with a "did you mean" suggestion, an unknown `-s` name in `settings` does not exist on the type, and `INITIAL_MEMORY: '100mb'` — which emcc silently misparses — does not match the `MemorySize` template literal. See [Config reference](/docs/toolchain/reference/config) for every field. Do not have a bindings list yet? `npx libcascade detect src` scans your source and prints one to paste in. Read [detect and check](/docs/toolchain/guides/detect-and-check) first — it is an onboarding tool, not a size optimizer. ## 3. Build [#3-build] ```bash npx libcascade build ``` For each variant the CLI renders the container-side yml into `.libcascade/`, pulls the digest-pinned image, runs the link with the artifact directory mounted, and moves the results into `dist/` only after the run exits 0 — a failed build never leaves half an artifact behind. ```text dist/ myapp_single.js # Emscripten glue myapp_single.wasm # the binary myapp_single.d.ts # generated TypeScript declarations myapp_single.js.symbols # symbol map myapp_single.build-manifest.json # requested / compiled / missing symbols myapp_single.provenance.json # toolchain + source commits ``` Add `.libcascade/` to your `.gitignore`. The rendered ymls and the raw container output stay there for inspection after a failure. Useful flags: ```bash npx libcascade build --variant single # one variant instead of all npx libcascade build --render-only # render the yml(s) and stop — no engine needed npx libcascade build --config path/to/libcascade.config.ts ``` `build` fails loudly when the container's own `build-manifest.json` reports `validation_passed: false`, printing the missing symbols. That matters because a **missing binding links successfully and fails at runtime** with a `BindingError` — the link step alone proves nothing. ## 4. Assemble the package surface [#4-assemble-the-package-surface] ```bash npx libcascade assemble ``` `assemble` never touches a container. It reads the per-variant `.d.ts` and build manifests from `dist/` and writes the npm-publishable surface next to them: one shared `types.d.ts`, an `./init` entry exposing `createInstance`, a root entry, and an `exports.json` fragment. Add `--write-exports` to merge that fragment straight into your `package.json`. See [Variants and assemble](/docs/toolchain/concepts/variants-and-assemble) for what each generated file does and when to pick `exports: 'eager'` over `'factory'`. ## 5. Consume the artifacts [#5-consume-the-artifacts] With `assemble: { exports: 'factory' }`, the root entry re-exports the factory and nothing is instantiated until you ask: ```typescript title="src/oc.ts" notypecheck import { createInstance } from 'my-occt-package'; export const oc = await createInstance(); ``` Inside the package that produced `dist/`, import the generated entry directly: ```typescript notypecheck import { createInstance } from './dist/init.js'; const oc = await createInstance(); using box = new oc.BRepPrimAPI_MakeBox(10, 10, 10); const shape = box.Shape(); ``` `createInstance` owns the plumbing consumers used to copy by hand: variant selection, the glue self-reference pthread workers spawn from, Node `file:` URL → path conversion, and OCCT thread-pool sizing. ## Exception helpers on emsdk 6.0.5 [#exception-helpers-on-emsdk-605] The image's Emscripten removed `-sEXPORT_EXCEPTION_HANDLING_HELPERS`. With `compilerFlags: { exceptions: 'wasm' }` the link pipeline **hard-fails** unless `EXPORTED_RUNTIME_METHODS` exports the three helpers directly: ```typescript notypecheck EXPORTED_RUNTIME_METHODS: [ 'getExceptionMessage', 'incrementExceptionRefcount', 'decrementExceptionRefcount', ], ``` Add whatever else your app needs (`'FS'`, `'wasmMemory'`) to the same array. ## Keep it honest in CI [#keep-it-honest-in-ci] ```bash npx libcascade check src ``` `check` recomputes the symbols your source references and exits non-zero when any of them is missing from `bindings`. It converts the runtime-`BindingError` failure class into a build-time one. Put it next to your typecheck. ## Next steps [#next-steps] Every `defineBuild` field, its type, and what the generated unions check. One bindings list, N binaries, one `types.d.ts`, and the generated entries. Field-by-field mapping from `defaults.yml` + ytt + `docker run` scripts. Declare your own wrapper files and the symbols they provide. --- # Emscripten settings and flags URL: /docs/toolchain/guides/custom-emcc-flags Once the `bindings` list is right, the second knob is how the binary is linked. Three fields cover it: | Field | Holds | | --------------- | --------------------------------------------------------------------------------------------------------------- | | `settings` | Emscripten `-s` settings, typed against the image's own emsdk. | | `compilerFlags` | The closed set of non-`-s` flags worth typing (`-O3`, `-msimd128`, `-fwasm-exceptions`, `-flto`, `--no-entry`). | | `rawFlags` | Everything else, passed through verbatim after the typed flags. | ## Recommended baseline [#recommended-baseline] ```typescript title="libcascade.config.ts" notypecheck settings: { MODULARIZE: true, // init() returns Promise EXPORT_ES6: true, // ESM output ALLOW_MEMORY_GROWTH: true, // the 16 MB initial heap is not enough INITIAL_MEMORY: '100MB', MAXIMUM_MEMORY: '4GB', // the wasm32 ceiling STACK_SIZE: 8_388_608, WASM_BIGINT: true, // no i64 legalisation shim EXPORTED_RUNTIME_METHODS: [ 'FS', 'getExceptionMessage', 'incrementExceptionRefcount', 'decrementExceptionRefcount', ], ENVIRONMENT: ['web', 'worker', 'node'], ERROR_ON_UNDEFINED_SYMBOLS: false, // OSD_MemInfo references mallinfo EVAL_CTORS: 2, // static-init evaluation at build time }, compilerFlags: { optimize: 'O3', simd: true, exceptions: 'wasm', noEntry: true }, ``` ## Setting-by-setting rationale [#setting-by-setting-rationale] | Setting | Why | | ----------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | | `optimize: 'O3'` | Production LLVM optimisation. `'Os'` is a size-tuned alternative — benchmark it against your workload. `'O0'` is for debugging only. | | `exceptions: 'wasm'` | Native Wasm exception instructions instead of JavaScript `invoke_*` trampolines. | | `simd: true` | Baseline WebAssembly SIMD, supported across the package's browser matrix. | | `WASM_BIGINT: true` | Removes the i64↔i32-pair legalisation shim. | | `EVAL_CTORS: 2` | Runs static initialisers at build time. Smaller payload, faster startup. Requires `O2` or better. | | `MODULARIZE` + `EXPORT_ES6` | Required for the ESM glue the generated entries import. | | `ENVIRONMENT` | Strips dead environment detection. Without it the runtime probes for `process` / `window` / `importScripts`. | | `ALLOW_MEMORY_GROWTH` | Required for any non-trivial geometry. | | `MAXIMUM_MEMORY: '4GB'` | The wasm32 hard ceiling (2³² bytes). | | `ERROR_ON_UNDEFINED_SYMBOLS: false` | OCCT's `OSD_MemInfo` references `mallinfo`, which Emscripten does not provide. | Every setting name carries its upstream emsdk documentation as JSDoc, so hovering it in your editor shows what emcc says about it. There are 312 of them; the ones above are the ones a custom build normally touches. ## The exception helpers are not optional [#the-exception-helpers-are-not-optional] emsdk 6.0.5 removed `-sEXPORT_EXCEPTION_HANDLING_HELPERS`. With `exceptions: 'wasm'` the link pipeline **hard-fails** unless these three are in `EXPORTED_RUNTIME_METHODS`: ```typescript notypecheck EXPORTED_RUNTIME_METHODS: [ 'getExceptionMessage', 'incrementExceptionRefcount', 'decrementExceptionRefcount', ], ``` Add whatever else you need (`'FS'`, `'wasmMemory'`) to the same array. ## When to reach for `rawFlags` [#when-to-reach-for-rawflags] `settings` has no index signature — an unknown `-s` name is a compile error, not a silently-ignored flag. Anything the typed surface cannot express goes here, verbatim, after every typed flag: ```typescript notypecheck rawFlags: ['-Wl,--allow-undefined', '--emit-symbol-map'], variants: [ { name: 'debug', rawFlags: ['-gsource-map'] }, ], ``` Variant `rawFlags` are appended after the base ones. Reach for `compilerFlags` first. `-pthread`, `-msimd128`, `-O3`, `-fwasm-exceptions`, `-flto`, and `--no-entry` are all typed there, on the base config or per variant — a flag that lands in `rawFlags` today and turns out to be common is a candidate for `compilerFlags` tomorrow, not a permanent resident. ## Trade-offs [#trade-offs] ### Wasm exceptions vs JavaScript exceptions [#wasm-exceptions-vs-javascript-exceptions] `-fwasm-exceptions` requires that **all** object files and the linker use the flag consistently. Mixed builds surface `__cpp_exception` as an unresolved import at link time. The published images compile everything with wasm exceptions; only override if you target a wasm engine without `try_table` support. ### SIMD: baseline vs relaxed [#simd-baseline-vs-relaxed] Baseline `simd: true` (`-msimd128`) is universal across the supported matrix. Relaxed SIMD is not: Safari 26.x refuses to parse the relaxed opcodes and the module fails to instantiate. If you ship it, ship it as an extra variant alongside a baseline one: ```typescript notypecheck variants: [ { name: 'single' }, { name: 'relaxed', rawFlags: ['-mrelaxed-simd'] }, ], ``` and select between them yourself — the generated capability probes cover `threads`, not SIMD flavours. ### `EVAL_CTORS` levels [#eval_ctors-levels] | Level | Behaviour | | ----- | --------------------------------------------------------- | | `0` | Off — every static initialiser runs at startup | | `1` | Evaluates constructors with safe side effects | | `2` | Recommended — full constructor evaluation, requires `O2`+ | Drop it in a threads variant. Constructor evaluation order is non-deterministic under pthread workers, which is exactly what `settings: { EVAL_CTORS: null }` on the variant is for — `null` removes an inherited base setting. ## What you cannot change at link time [#what-you-cannot-change-at-link-time] The wasm bitwidth (`wasm32` vs `wasm64`), the C++ standard-library version, the OCCT commit pin, and the libclang version are all baked into the published image during the bindgen pipeline. Those live on the other side of the [two-channel split](/docs/toolchain/concepts/two-channel-config-model); changing them means forking the image build. ## Related [#related] * [Config reference](/docs/toolchain/reference/config) — the full `settings` type story. * [Custom multi-threaded build](/docs/toolchain/guides/multi-threading) — the pthread settings set. * [Two-channel config model](/docs/toolchain/concepts/two-channel-config-model) — compile-time vs link-time. --- # Derive a C++ class in JavaScript URL: /docs/toolchain/guides/derive-cpp-class-in-js OCCT progress callbacks require a derived `Message_ProgressIndicator` class. That needs Embind's `allow_subclass`, so it is a `customBindings` entry with `scope: 'main'` — raw Embind — rather than a generated binding. ## 1. Add one self-contained binding file [#1-add-one-self-contained-binding-file] Create `bindings/progress-indicator.cpp` with the bridge class, wrapper, and registration in the same file: ```cpp #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&, bool isForce) { call("Show", GetPosition(), isForce); } bool UserBreak() { return call("UserBreak"); } void Reset() { call("Reset"); } }; 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) { self.Message_ProgressIndicator_JS::Reset(); })) .allow_subclass( "Message_ProgressIndicator_JSWrapper"); } ``` The repository's executable Docker fixture contains the complete production registration, including inherited methods needed by its runtime test. ## 2. Declare the file [#2-declare-the-file] ```typescript title="libcascade.config.ts" import { defineBuild } from '@libcascade/toolchain'; export default defineBuild({ name: 'progress-callback', bindings: [ 'BRepAlgoAPI_Fuse', 'BRepPrimAPI_MakeBox', 'Message_ProgressIndicator', 'Message_ProgressRange', 'Message_ProgressScope', 'Message_ProgressIndicator_JS', 'gp_Pnt', ], customBindings: [ { file: 'bindings/progress-indicator.cpp', symbols: ['Message_ProgressIndicator_JS'], scope: 'main', }, ], variants: [{ name: 'single' }], }); ``` The registration name goes in `symbols`, which is what admits it to `bindings` without it being an `OcctSymbol`. Build with `npx libcascade build` — see the [Quickstart](/docs/toolchain/getting-started/quick-start). Raw Embind files skip the bindgen, so add a project-local TypeScript declaration for `Message_ProgressIndicator_JS` if the consumer is TypeScript. ## 3. Derive in JavaScript [#3-derive-in-javascript] ```javascript notypecheck import { createInstance } from './dist/init.js'; const oc = await createInstance(); const Progress = oc.Message_ProgressIndicator_JS.extend('Progress', { Show(position) { console.log('progress', position); }, UserBreak() { return false; }, }); using progress = new Progress(); // Pass progress.Start() to a long-running OCCT algorithm. ``` See [Extend with C++](/docs/toolchain/guides/extend-with-cpp) for the distinction between generated and raw bindings. --- # detect and check URL: /docs/toolchain/guides/detect-and-check Two commands answer one question — *which OCCT symbols does this code actually reference?* — in the two directions that matter. ```bash npx libcascade detect src # onboarding: seed a bindings list npx libcascade check src # CI: fail when a referenced symbol is not bound ``` **Neither command is a size tool, and neither ever removes anything.** Read the caveats below before acting on either output. Both are marked experimental. ## Why they exist: the failure asymmetry [#why-they-exist-the-failure-asymmetry] A `bindings` list that is missing a symbol **links successfully**. The wasm builds, the manifest can pass, the package publishes — and the first time your code touches the missing class it throws a `BindingError` at runtime. `libcascade build` cannot catch that. Only running the code path can. `check` converts that failure class into a build-time error. That is the whole value proposition, and it is a correctness one, not a size one. ## Why they are not size tools [#why-they-are-not-size-tools] The measurement: dropping **14% of the symbols bought 0.9% of brotli size**. The \~5,400 embind registrations are GC roots, so unbound symbols free glue, not kernel code. (`--gufa`, the obvious next lever, is a measured size *regression* on top of that.) Trimming a binding set is still worth doing for the reasons in [Trim symbols](/docs/toolchain/guides/trim-symbols) — startup work, honest dependency surface — but do not expect `detect` to hand you megabytes. ## `detect` — the first bindings list [#detect--the-first-bindings-list] Writing the initial `bindings` array is the scariest step of custom-build onboarding. `detect` scans your source for symbol references, closes over the symbol catalog, and prints a paste-ready fragment with per-symbol provenance: ```bash npx libcascade detect src lib ``` ```text bindings: [ 'BRepBuilderAPI_MakeShape', // closure: base of BRepPrimAPI_MakeBox 'BRepPrimAPI_MakeBox', // seed: src/shapes.ts:41 'gp_XYZ', // closure: member type of gp_Pnt ], ``` `--json` emits the same result machine-readably. The output is a **starting set, not a minimal one**. Symbols your source does not reference today include roadmap-reserved capacity and anything your own C++ wrapper files call. Review the list. Never diff it against an existing config and delete the difference. ## `check` — the drift guard [#check--the-drift-guard] ```bash npx libcascade check src ``` `check` recomputes the referenced set from your source and fails when any of it is missing from `bindings ∪ customBindings[].symbols`, naming each symbol, the first `file:line` that references it, and the fix: ```text libcascade check: 1 referenced symbol is not bound by libcascade.config.ts. ChFi2d_FilletAPI first referenced at src/fillet.ts:13 ``` It exits non-zero, so it belongs in CI next to your typecheck: ```yaml title=".github/workflows/ci.yml" - run: npx tsc --noEmit - run: npx libcascade check src ``` Symbols bound under an OCCT typedef alias (`TColgp_Array1OfPnt` for `NCollection_Array1_gp_Pnt`) count as bound. Names that are not in the catalog at all — your `customBindings` symbols, Emscripten runtime members such as `oc.FS`, plain typos — are never failures. `--verbose` lists them as ignored along with the scan's caveats. ## How the scan works [#how-the-scan-works] | Rule | Behaviour | | ----------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Strong signal | `oc.Symbol` (also `this.oc.Symbol`) | | Weak signal | any bare identifier that exactly matches a catalog name **and contains an underscore** — this is what catches type-only imports such as `import type { TopoDS_Shape }` | | Single-word names | `Draft`, `Expr`, `BRepTools` and friends are excluded from bare matching because they collide with ordinary identifiers. Write them as `oc.BRepTools` to be seen. | | Overload suffixes | `Geom2d_Line_1` resolves to `Geom2d_Line`, but only when the full name is not itself a symbol | | Excluded paths | `.d.ts`, `node_modules`, `dist`, `build`, `out`, `coverage` | | Comments | blanked before matching, so a comment naming a deliberately-omitted class is not a reference | | Strings | scanned — which is what makes `oc['gp_Pnt']` visible | | Builtins | `OCJS`, `TopoDS` and friends are registered unconditionally, so they are never detected or demanded | `.d.ts` files are excluded for a specific reason: an OCCT `.d.ts` declares every symbol, which would make the scan vacuous. ## Limits you must not forget [#limits-you-must-not-forget] The scanner is **regex and token based, not AST based** — the toolchain's runtime dependencies stay at two small packages. It therefore cannot see: * dynamic access — `oc[name]` where `name` is a variable * names built by concatenation * symbols only your C++ wrapper files call * anything reached through a dependency's compiled code So: * **`check` passing is not a proof.** It proves no symbol is missing from the *written* references it can see. An unexercised code path can still hit a runtime `BindingError`. * **`detect` output is a seed.** It is not an audit of what you could drop. * **Neither command edits your config.** They print; you decide. ## Related [#related] * [CLI reference](/docs/toolchain/reference/cli) — flags and exit behaviour. * [Trim symbols](/docs/toolchain/guides/trim-symbols) — the bindings list as a design signal. * [Config reference](/docs/toolchain/reference/config) — where the list lives. --- # Extend with C++ URL: /docs/toolchain/guides/extend-with-cpp A custom build can compile your own C++ alongside OCCT. You declare each file and the symbols it provides in `customBindings`: ```typescript title="libcascade.config.ts" notypecheck customBindings: [ { file: 'wrappers/fair-curve.cpp', symbols: ['FairCurve'] }, { file: 'bindings/helpers.cpp', symbols: ['addReals'], scope: 'main' }, ], ``` Paths resolve relative to the config file's directory and are checked for existence when the config loads, so a moved or misspelled file fails immediately with the resolved absolute path — not twenty minutes into a build. ## Why the symbols are declared [#why-the-symbols-are-declared] `bindings` is type-checked against the generated `OcctSymbol` union. Your classes are not in it. Declaring them in `customBindings[].symbols` is what admits them: ```typescript notypecheck bindings: ['gp_Pnt', 'FairCurve'], customBindings: [{ file: 'wrappers/fair-curve.cpp', symbols: ['FairCurve'] }], ``` The custom-symbol union is inferred **only** from `customBindings`, never from `bindings`. A typo'd OCCT name therefore cannot widen the union and type itself as valid. Declaring them also keeps [`detect` and `check`](/docs/toolchain/guides/detect-and-check) from reporting your own classes as unknown symbols. ## `scope: 'all'` — generated bindings [#scope-all--generated-bindings] The default. The file is inspected by the bindgen, which generates JavaScript and TypeScript bindings for the classes it finds. ```cpp title="wrappers/fair-curve.cpp" #include class FairCurve { public: TopoDS_Shape Build() const { /* ... */ } }; ``` ```typescript title="libcascade.config.ts" notypecheck bindings: ['FairCurve'], customBindings: [{ file: 'wrappers/fair-curve.cpp', symbols: ['FairCurve'] }], ``` The generator discovers classes, constructors, methods, enums, referenced OCCT types, and `Handle` / NCollection aliases from these files, and their declarations become part of the build's `.d.ts` — and therefore of the shared `types.d.ts` that `libcascade assemble` writes. ## `scope: 'main'` — raw Embind [#scope-main--raw-embind] Use `scope: 'main'` when you need Embind constructs the generator does not emit: free functions, `value_object`, collection registrations, or `allow_subclass`. ```cpp title="bindings/helpers.cpp" #include #include Standard_Real addReals(Standard_Real a, Standard_Real b) { return a + b; } EMSCRIPTEN_BINDINGS(custom_helpers) { emscripten::function("addReals", &addReals); } ``` ```typescript title="libcascade.config.ts" notypecheck customBindings: [{ file: 'bindings/helpers.cpp', symbols: ['addReals'], scope: 'main' }], ``` A `scope: 'main'` file may contain both its helper implementation and its `EMSCRIPTEN_BINDINGS(...)` block. It compiles directly and skips the bindgen, so you own any TypeScript declaration for what it registers: ```typescript notypecheck import { createInstance } from './dist/init.js'; import type { OpenCascadeInstance } from './dist/types.js'; type CustomInstance = OpenCascadeInstance & { addReals(a: number, b: number): number; }; const oc = (await createInstance()) as CustomInstance; console.log(oc.addReals(1.5, 2.25)); ``` ## Which one? [#which-one] * Prefer `scope: 'all'` for ordinary classes and generated types. * Use `scope: 'main'` only for a binding construct the bindgen cannot emit. * Do not declare the same file twice. Both scopes record the file path and its SHA-256 digest in the build manifest and the provenance sidecar, so a change to your C++ is part of the artifact's identity. The two scopes map onto the container yml's `additionalCppFiles` (top level, shared by every output) and `mainBuild.additionalBindFiles` (per output). You never write that yml — see [Container yml contract](/docs/toolchain/reference/yaml-schema) if you want to read what gets rendered. ## Build [#build] ```bash npx libcascade build --render-only # check the rendered yml first npx libcascade build ``` ## Related [#related] * [Config reference](/docs/toolchain/reference/config) — the `customBindings` field. * [Derive a C++ class in JavaScript](/docs/toolchain/guides/derive-cpp-class-in-js) — an `allow_subclass` example under `scope: 'main'`. * [Variants and assemble](/docs/toolchain/concepts/variants-and-assemble) — how your generated declarations reach the shared `types.d.ts`. --- # Custom multi-threaded build URL: /docs/toolchain/guides/multi-threading The npm package already ships a pre-built multi-threaded variant — see [Package — Multi-threaded build](/docs/package/guides/multi-threading) for the import recipe, COOP/COEP headers, and OCCT activation calls. Build your own only when you need: * A trimmed symbol list. * Different link settings (alternative `STACK_SIZE`, `INITIAL_MEMORY`, a capped `PTHREAD_POOL_SIZE`). * A browser-only variant with growable array buffers (see below). ## Declare the variant [#declare-the-variant] A threaded build is not a separate config. It is a variant of the same one: ```typescript title="libcascade.config.ts" import { defineBuild } from '@libcascade/toolchain'; export default defineBuild({ name: 'occt', bindings: ['BRepMesh_IncrementalMesh', 'TopoDS_Shape', 'gp_Pnt'], settings: { MODULARIZE: true, EXPORT_ES6: true, ALLOW_MEMORY_GROWTH: true, MAXIMUM_MEMORY: '4GB', STACK_SIZE: 8_388_608, WASM_BIGINT: true, ENVIRONMENT: ['web', 'worker', 'node'], EXPORTED_RUNTIME_METHODS: [ 'FS', 'wasmMemory', 'getExceptionMessage', 'incrementExceptionRefcount', 'decrementExceptionRefcount', ], ERROR_ON_UNDEFINED_SYMBOLS: false, EVAL_CTORS: 2, }, compilerFlags: { optimize: 'O3', simd: true, exceptions: 'wasm', noEntry: true }, variants: [ { name: 'single' }, { name: 'multi', compilerFlags: { threads: true }, settings: { // Constructor evaluation order is non-deterministic under pthread // workers, so the base EVAL_CTORS is removed for this variant. EVAL_CTORS: null, SHARED_MEMORY: true, PTHREAD_POOL_SIZE: 'navigator.hardwareConcurrency', }, }, ], }); ``` `compilerFlags: { threads: true }` renders `-pthread`, and it is the line that matters twice over. From it the toolchain infers the variant's `threads` capability, which selects the multi-threaded container image at build time and becomes the capability the generated `createInstance` probes before handing you an instance at load time. You can still write `requires: ['threads']` explicitly, but you do not need to: a build against shared memory cannot instantiate without `SharedArrayBuffer`, so the capability follows from the flags. An explicit `requires` is *added to* what is inferred — declaring `requires: []` does not opt a threaded build out of its own probe, by design. ```bash npx libcascade build --variant multi npx libcascade assemble ``` ## Load it [#load-it] ```typescript notypecheck import { createInstance } from 'my-occt-package/init'; const oc = await createInstance(); // best variant this host supports const mt = await createInstance({ variant: 'multi' }); // explicit const capped = await createInstance({ variant: 'multi', threadCount: 4 }); ``` There is no `mainScriptUrlOrBlob` to pass, no `file:` URL to convert for Node workers, and no thread-pool call to remember: `createInstance` does all three. Asking for `'multi'` on a host without cross-origin isolation throws with the remediation in the message rather than failing deep inside module instantiation. Browsers gate `SharedArrayBuffer` behind cross-origin isolation, so every page loading the threaded binary needs: ```http Cross-Origin-Opener-Policy: same-origin Cross-Origin-Embedder-Policy: require-corp ``` Node exposes `SharedArrayBuffer` unconditionally. Under Vite, add `worker: { format: 'es' }` — Emscripten's workers are ES modules with top-level await and the default `iife` format cannot emit them. Both are covered in [Variants and assemble](/docs/toolchain/concepts/variants-and-assemble). ## Sizing the pool [#sizing-the-pool] `PTHREAD_POOL_SIZE: 'navigator.hardwareConcurrency'` is evaluated by the generated glue at module-instantiation time, so one binary adapts to whatever hardware loads it: an M2 Pro browser gets 12 workers, a mobile device 4–6, a Node server whatever `os.cpus().length` reports. Each worker costs `STACK_SIZE` (8 MB above) at module init, so a 12-core box reserves \~96 MB of stack on top of `INITIAL_MEMORY`. To cap it, either wrap the expression in the setting or pass `threadCount` at load: ```typescript notypecheck const oc = await createInstance({ variant: 'multi', threadCount: 8 }); ``` `threadCount` sizes OCCT's default pool and raises its launch cap to match. It does not shrink the pre-spawned worker pool baked into the binary — that is a build-time number. > ### Why not a small fixed pool size? [#why-not-a-small-fixed-pool-size] > > A round number like `PTHREAD_POOL_SIZE: 4` caps concurrency on larger > machines and oversubscribes smaller ones. > `'navigator.hardwareConcurrency'` follows the host without a separate build. > Add a cap only after benchmarking your workload and target devices; the > published benchmark measures 12 workers and does not claim a universal > optimum. ## Browser-only MT build [#browser-only-mt-build] If you can guarantee recent browsers (Chrome / Edge ≥ 144, Firefox ≥ 154, Safari ≥ 26.2 — August 2026 baseline) and do **not** need the binary under Node.js, Bun, or Deno, add a third variant so typed-array views track memory growth: ```typescript notypecheck { name: 'multi-browser', compilerFlags: { threads: true }, settings: { EVAL_CTORS: null, SHARED_MEMORY: true, PTHREAD_POOL_SIZE: 'navigator.hardwareConcurrency', GROWABLE_ARRAYBUFFERS: true, ENVIRONMENT: ['web', 'worker'], }, } ``` Emscripten then 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()`. Typed arrays created from `oc.wasmMemory.buffer` stay valid across memory growth, and the `-Wpthreads-mem-growth` advisory disappears. > ### Runtime support matrix — `toResizableBuffer()` (August 2026) [#runtime-support-matrix--toresizablebuffer-august-2026] > > **Supported** — ship browser-variant artefacts to: > > * Chrome / Edge ≥ 144 > * Firefox ≥ 154 > * Safari ≥ 26.2 (desktop + iOS) > > **NOT supported** — fall back to the plain threads variant 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. > * Firefox ≤ 153 — WebAssembly worker transfer can hang even when the API exists; [the engine fix ships in Firefox 154](https://bugzilla.mozilla.org/show_bug.cgi?id=2021136). > > Because Node.js is the dominant test-harness runtime, the shipped > multi-threaded build keeps `GROWABLE_ARRAYBUFFERS` off so it stays loadable > under `vitest`, `jest`, and other Node-based runners. Opt in only for > production payloads served directly to browser clients. The selector picks the most capable variant whose requirements probe true and breaks ties by configuration order, so a browser-only variant needs an explicit `createInstance({ variant: 'multi-browser' })` — or the pre-import override symbol — rather than automatic selection. > ### Relaxed SIMD — advanced, non-default [#relaxed-simd--advanced-non-default] > > Safari 26.x does not support relaxed SIMD. If profiling justifies it for > Chromium, Firefox, or Node, add a sibling variant with > `rawFlags: ['-mrelaxed-simd']`, retain the baseline binary, and select it > yourself whenever relaxed-SIMD validation fails. ## After building [#after-building] Activation calls, parallel-aware OCCT APIs, and benchmarks are the same as for the shipped binary — see [Package — Multi-threaded build](/docs/package/guides/multi-threading). ## Related [#related] * [Variants and assemble](/docs/toolchain/concepts/variants-and-assemble) — selection, overrides, and the generated entries. * [Emscripten settings and flags](/docs/toolchain/guides/custom-emcc-flags) — the settings baseline. * [Trim symbols](/docs/toolchain/guides/trim-symbols) — shrink the symbol set before building. --- # Reproducible CI URL: /docs/toolchain/guides/reproducible-ci A reproducible custom build means: same inputs, same wasm bytes, every time. The toolchain closes most of that loop for you — what is left is choosing where to run it and what to assert afterwards. ## 1. Pin the toolchain, not a tag [#1-pin-the-toolchain-not-a-tag] ```json title="package.json" { "devDependencies": { "@libcascade/toolchain": "3.0.0", "libcascade": "3.0.0" } } ``` Commit your lockfile. The toolchain package ships `images.json`, in which each image tag was resolved to an immutable `ghcr.io/taucad/opencascade.js@sha256:…` digest at publish time. `@libcascade/toolchain@3.0.0` therefore names one reproducible build environment forever, and `npm ls` answers "which toolchain built this artifact". The driver runs `repository@digest` — never a tag — and verifies the local repo digest after pulling. A mismatch is an error naming both digests, not a silent roll-forward. This replaces the pattern it supersedes: an image tag pasted into a shell script that had to be kept in sync by hand with the `libcascade` version the application depended on. Because the toolchain and `libcascade` are lockstep-versioned, keeping the two dependencies on the same version is the whole compatibility story. ## 2. Run it where a container engine exists [#2-run-it-where-a-container-engine-exists] ```yaml title=".github/workflows/wasm-build.yml" jobs: build-wasm: runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 22 - run: npm ci - run: npx libcascade check src - run: npx libcascade build - run: npx libcascade assemble - name: Assert wasm hash run: | EXPECTED=$(cat .wasm-hash) ACTUAL=$(sha256sum dist/myapp_single.wasm | cut -d' ' -f1) [ "$EXPECTED" = "$ACTUAL" ] || { echo "wasm hash drift"; exit 1; } ``` GitHub-hosted **macOS runners ship no container engine at all** — run toolchain builds on a Linux runner. The driver's engine probe order is `$LIBCASCADE_CONTAINER_CMD`, then `docker`, then `podman`; if none responds it fails with install options rather than a cryptic `ENOENT`. Three gates are doing work in that job: * `check` fails when your source references a symbol the config does not bind — the failure that otherwise reaches production as a runtime `BindingError`. * `build` fails when the container's `build-manifest.json` reports `validation_passed: false`, printing the unsatisfied symbols. * The hash assertion catches everything else. If `EXPECTED` and `ACTUAL` diverge, something changed — either intentionally (bump `.wasm-hash`) or by accident (investigate). ## 3. Keep the provenance sidecars [#3-keep-the-provenance-sidecars] Every variant build emits `.provenance.json` next to the binary, recording the active compile preset, flags, and the source commit the wasm was built from. Diff those across builds to detect surprise cache turnover, and publish them with your package. ## 4. Verify the image supply chain (optional) [#4-verify-the-image-supply-chain-optional] The driver already proves the image is the digest the toolchain pinned. If your threat model wants the signature too, verify it before the build step. Every published image is signed with [cosign](https://github.com/sigstore/cosign) via OIDC keyless signing, with the signature on the manifest-list digest — one signature verifies regardless of which architecture pulls it. ```bash IMAGES=node_modules/@libcascade/toolchain/generated/images.json REF="$(jq -r '.repository + "@" + .singleThreaded.digest' "$IMAGES")" cosign verify "$REF" \ --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. The image also ships a SLSA provenance attestation and an SBOM: ```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 \ "$REF" docker buildx imagetools inspect --format '{{ json .SBOM }}' "$REF" ``` The SBOM lists every apt package and pinned commit (OCCT, freetype, rapidjson) in the image. Diff it against the previous digest's SBOM to flag unexpected dependency bumps. ## 5. Never opt out in CI [#5-never-opt-out-in-ci] `$LIBCASCADE_IMAGE` points the driver at any locally reachable image and skips digest verification, printing a provenance warning when it does. That is a dev loop, not a CI setting. Leave it unset in every automated build. ## Upstream reproducibility [#upstream-reproducibility] The repository runs `.github/workflows/reproducibility.yml` weekly and on demand: two isolated Linux/amd64 cold builds in parallel, runtime smoke for each, and an exact artifact-ledger comparison. Stable npm publication invokes that same exact-commit gate; canary and beta builds keep the single-candidate path and rely on scheduled cold coverage. ## Related [#related] * [Driver environment](/docs/toolchain/reference/env-vars) — engine probing, overrides, and what each one forfeits. * [CLI reference](/docs/toolchain/reference/cli) — the commands this workflow runs. * [Docker image](/docs/toolchain/reference/docker-image) — tags, labels, and the stage layout behind the digests. --- # Trim symbols URL: /docs/toolchain/guides/trim-symbols `opencascade_single.wasm` binds OCCT classes and weighs roughly 40 MB. Most applications need a fraction of that surface — a STEP round-tripper might touch \~120 classes, a glTF mesher fewer. The `bindings` array in `libcascade.config.ts` is where you say so. ## Prerequisites [#prerequisites] * The [Quickstart](/docs/toolchain/getting-started/quick-start) run once, so you know your container engine works. * A working directory under your home folder on macOS/Windows. Docker Desktop does not share `/tmp` or `/opt` into its VM by default and outputs vanish silently. ## 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 | | \~40 MB | Numbers are after `optimize: 'O3'`, `simd: true`, `WASM_BIGINT: true`, and `EVAL_CTORS: 2`. Two things that budget does **not** buy you. First, the returns are front-loaded: below a few hundred symbols the embind registration glue, not the kernel, is what remains — dropping 14% of a trimmed list measured 0.9% of brotli size. Second, a missing symbol is not a build error. It links fine and throws a `BindingError` at runtime. ## 1. Seed the list [#1-seed-the-list] ```bash npx libcascade detect src ``` `detect` scans your source, closes over the symbol catalog, and prints a paste-ready fragment with per-symbol provenance. It is an onboarding tool with real limits — read [detect and check](/docs/toolchain/guides/detect-and-check) before you trust the output. In particular it is a *starting set*, never a minimal one, and it never proposes a removal. ## 2. Put it in the config [#2-put-it-in-the-config] ```typescript title="libcascade.config.ts" import { defineBuild } from '@libcascade/toolchain'; export default defineBuild({ name: 'step_roundtrip', bindings: [ 'IFSelect_ReturnStatus', 'Interface_Static', 'Message_ProgressRange', 'STEPControl_Reader', 'STEPControl_StepModelType', 'STEPControl_Writer', 'Standard_Failure', 'TCollection_AsciiString', 'TCollection_ExtendedString', 'TopExp', 'TopExp_Explorer', 'TopoDS', 'TopoDS_Compound', 'TopoDS_Edge', 'TopoDS_Face', 'TopoDS_Shape', 'TopoDS_Shell', 'TopoDS_Solid', 'TopoDS_Vertex', 'TopoDS_Wire', 'gp_Ax2', 'gp_Dir', 'gp_Pnt', 'gp_Vec', ], settings: { MODULARIZE: true, EXPORT_ES6: true, ALLOW_MEMORY_GROWTH: true, WASM_BIGINT: true, EXPORTED_RUNTIME_METHODS: [ 'FS', 'getExceptionMessage', 'incrementExceptionRefcount', 'decrementExceptionRefcount', ], ENVIRONMENT: ['web', 'worker', 'node'], ERROR_ON_UNDEFINED_SYMBOLS: false, }, compilerFlags: { optimize: 'O3', simd: true, exceptions: 'wasm', noEntry: true }, variants: [{ name: 'single', settings: { EVAL_CTORS: 2 } }], }); ``` You list classes you instantiate or pass around — not their transitive closure. Base classes and the `NCollection_*` members of a bound class are auto-discovered at codegen time. Every name is checked against the generated `OcctSymbol` union as you type, so the class of mistake that used to surface as a link failure ten minutes into a build is now a red squiggle. ## 3. Build [#3-build] ```bash npx libcascade build ``` The link reuses the precompiled object files baked into the image for every still-bound class, so a trim that strips most of the full surface typically completes in 60–180 seconds on a warm image. Artifacts land in `dist/`: ```text dist/step_roundtrip_single.wasm # the trimmed binary dist/step_roundtrip_single.js # Emscripten glue dist/step_roundtrip_single.d.ts # generated declarations dist/step_roundtrip_single.build-manifest.json # symbol coverage ``` The build fails if the manifest reports `validation_passed: false`, printing the symbols the container could not satisfy. Add them back and re-run. ## 4. Guard the list [#4-guard-the-list] ```bash npx libcascade check src ``` Run it in CI. A symbol you start referencing but forget to bind is otherwise a runtime crash for your users, and a build that succeeds proves nothing about it. ## Iterating [#iterating] Repeated trims benefit from the engine's own image cache — the image is pulled once and verified by digest thereafter. To render and inspect the container-side yml without running anything: ```bash npx libcascade build --render-only ``` To iterate against a locally built image instead of the pinned one, see the [dev loop](/docs/toolchain/reference/env-vars#local-image-dev-loop). ## 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 `check` failure is a signal: either the class is genuinely part of your call graph, or your code reaches into an OCCT subsystem it does not need. The second case is worth investigating. ## Related [#related] * [Quickstart](/docs/toolchain/getting-started/quick-start) — end-to-end first custom build. * [detect and check](/docs/toolchain/guides/detect-and-check) — seeding and guarding the list. * [Config reference](/docs/toolchain/reference/config) — every field, including `bindings`. * [Bindgen pipeline](/docs/toolchain/concepts/bindgen-pipeline) — why the trim works without computing a transitive closure. --- # build-wasm.sh (internals) URL: /docs/toolchain/reference/cli-build-wasm **Internals.** Custom builds are driven by the `libcascade` CLI — see [CLI reference](/docs/toolchain/reference/cli). `build-wasm.sh` is the orchestrator **inside** the container image and in a from-source checkout; it is what the container's entrypoint dispatches to. Read this when you are building the image itself. `build-wasm.sh` is the orchestration entry point for the libcascade 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] * [CLI reference](/docs/toolchain/reference/cli) — the `libcascade` bin, which is what custom builds use. * [Bindgen pipeline](/docs/toolchain/concepts/bindgen-pipeline) — stage diagram and artifact layout. * [Driver environment](/docs/toolchain/reference/env-vars) — the `OCJS_*` catalogue this script reads. * [Container yml contract](/docs/toolchain/reference/yaml-schema) — the yml this script consumes. --- # libcascade CLI URL: /docs/toolchain/reference/cli `@libcascade/toolchain` installs one bin, `libcascade`. Run it with `npx` (or your package manager's equivalent) from the directory holding `libcascade.config.ts`. ```text libcascade build [--variant ] [--config ] [--render-only] libcascade assemble [--config ] [--write-exports] libcascade detect [--json] libcascade check [--config ] [--verbose] libcascade migrate [--out ] [--force] ``` `libcascade --help` prints the same synopsis plus the environment variables. ## `build` [#build] Links one WASM binary per variant through the container. ```bash npx libcascade build npx libcascade build --variant multi npx libcascade build --render-only ``` What one invocation does, per variant: 1. Load and validate the config (see [Config reference](/docs/toolchain/reference/config)). 2. Render the container-side yml into `.libcascade/`. 3. Resolve the image — the digest pinned for this toolchain version, or your override — and verify the local repo digest after pulling. 4. Run the engine with the config directory mounted at `/src` and a scratch directory mounted at `/out`, with `OCJS_OUTPUT_DIR` pointed at it. 5. On exit 0, move the artifacts into `dist/`. 6. Read `.build-manifest.json` and fail if `validation_passed` is not `true`, printing the missing symbols and the binding-report deltas. Step 6 is the reason to run this instead of the engine directly: **a missing binding links successfully and fails at runtime** with a `BindingError`. Artifacts per variant, in `dist/`: | File | Contents | | ---------------------------------- | ------------------------------------------------------- | | `.js` | Emscripten glue | | `.wasm` | The binary | | `.d.ts` | Generated TypeScript declarations | | `.js.symbols` | Symbol map | | `.build-manifest.json` | Requested / compiled / alias-resolved / missing symbols | | `.provenance.json` | Toolchain and source commits | | Flag | Effect | | ------------------ | ----------------------------------------------------------------------------- | | `--variant ` | Build one variant. Default: every variant in the config. | | `--config ` | Config file path. Default: `./libcascade.config.{ts,js,mjs}`. | | `--render-only` | Render the yml(s), print their paths, and stop. No container engine required. | Failures leave `.libcascade/` in place — the rendered yml and the container's raw output directory are what you inspect. Add it to `.gitignore`. ## `assemble` [#assemble] Generates the npm packaging surface from the artifacts `build` produced. Pure Node; it never touches a container. ```bash npx libcascade assemble npx libcascade assemble --write-exports ``` Reads `.d.ts` and `.build-manifest.json` for every declared variant and writes, next to them: | File | Contents | | ------------------------- | ----------------------------------------------------------------------------------------------------- | | `types.d.ts` | One d.ts unioning every variant's surface. Symbols only some variants bind are typed optional. | | `init.js` / `init.d.ts` | The `./init` subpath: `createInstance({ variant, threadCount, wasmBinary, wasmMemory, locateFile })`. | | `index.js` / `index.d.ts` | The root entry, per `assemble.exports`. | | `variant.d.ts` | Types for the raw per-variant glue subpaths (`./single`, `./multi`, …). | | `exports.json` | The generated `exports` fragment. | `--write-exports` merges that fragment into the package's own `package.json`: generated subpaths win, every other subpath you declared is preserved in place. The `files` list is not touched — add the generated files to it once. `assemble` fails with a pointer at `libcascade build --variant ` when a variant's `.d.ts` is missing. It packages artifacts; it never builds them. See [Variants and assemble](/docs/toolchain/concepts/variants-and-assemble) for the generated entries in detail. ## `detect` [#detect] Scans your source for OCCT symbol references, closes over the catalog, and prints a paste-ready `bindings` fragment with per-symbol provenance. ```bash npx libcascade detect src npx libcascade detect src lib --json ``` ```text bindings: [ 'BRepBuilderAPI_MakeShape', // closure: base of BRepPrimAPI_MakeBox 'BRepPrimAPI_MakeBox', // seed: src/shapes.ts:41 'gp_XYZ', // closure: member type of gp_Pnt ], ``` The output is a **starting set, not a minimal one**, and `detect` never removes anything. Read [detect and check](/docs/toolchain/guides/detect-and-check) before acting on it. ## `check` [#check] The inverse direction, for CI: recompute the referenced set and fail when any of it is missing from `bindings ∪ customBindings[].symbols`. ```bash npx libcascade check src npx libcascade check src --verbose ``` ```text libcascade check: 1 referenced symbol is not bound by libcascade.config.ts. ChFi2d_FilletAPI first referenced at src/fillet.ts:13 ``` Exits non-zero on a miss. Symbols bound under an OCCT typedef alias count as bound. Names not in the catalog at all — your custom symbols, `oc.FS`, typos — are never failures; `--verbose` lists them as ignored, along with the scan's caveats. ## `migrate` [#migrate] Converts v2-style container ymls into a typed `libcascade.config.ts`. Pure Node, one shot, run once per package — it is an onboarding tool, not a sync. ```bash npx libcascade migrate build-config/custom_build_single.yml \ build-config/custom_build_multi.yml \ --out libcascade.config.ts ``` Pass **every** variant's yml to one invocation. Sibling ymls that differ only in flags and artifact name are one config with one variant each, and that is what `migrate` emits: the values they all agree on become the base, each yml's differences become its variant. Ymls that disagree on `bindings`, `additionalCppFiles`, or `additionalBindFiles` are not variants of one build — the config has no per-variant form for those — so `migrate` names the disagreement and refuses. Migrate those separately. | Flag | Effect | | -------------- | -------------------------------------------------------------------------------------------------------- | | `--out ` | Write the config here. Default: stdout. | | `--force` | Overwrite an existing `--out` file. Without it an existing file is an error, never a silent replacement. | Findings go to stderr, the config to stdout — so `> libcascade.config.ts` works too, and the findings still reach you. ### Where each flag lands [#where-each-flag-lands] | `emccFlags` entry | Config | | ----------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `-sNAME=VALUE`, or a bare `-sNAME` (which emcc reads as `=1`) | `settings: { NAME: … }`, the value deserialized with the generated grammar — memory sizes, bracketed lists, the `ENVIRONMENT` comma list, and the 0/1 integers that mean booleans | | `-O0`…`-O3`, `-Os`, `-Oz` | `compilerFlags.optimize` | | `-msimd128`, `-flto`, `--no-entry`, `-pthread` | `compilerFlags.simd` / `.lto` / `.noEntry` / `.threads` | | `-fwasm-exceptions`, `-fexceptions` | `compilerFlags.exceptions` | | Anything else — and any `-sNAME=VALUE` whose value the typed grammar cannot express | `rawFlags`, verbatim | That last row is what makes the output trustworthy: a flag nobody modelled is passed to emcc unchanged **and** listed in the emitted header, so it can never go missing between the yml and the build. An unknown *yml key* is the opposite case — the schema has no verbatim bucket for one, so it is an error. ### Two rewrites it applies [#two-rewrites-it-applies] Both are required by the pinned emsdk, and each leaves a comment at its site in the emitted config: | yml | Config | Why | | ------------------------------------- | --------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------- | | `-pthread` + `-sUSE_PTHREADS=1` | `compilerFlags: { threads: true }` | `USE_PTHREADS` survives only as emcc's deprecated legacy alias of `-pthread`; the pair is one request written twice | | `-sEXPORT_EXCEPTION_HANDLING_HELPERS` | `getExceptionMessage`, `incrementExceptionRefcount`, `decrementExceptionRefcount` added to `EXPORTED_RUNTIME_METHODS` | emsdk 6.0.5 removed the setting and **hard-fails** a `-fwasm-exceptions` link without the three helpers | `requires: ['threads']` is not emitted at all: `threads` is inferred from the flags that cause it. See [Config reference](/docs/toolchain/reference/config). ### What it cannot know [#what-it-cannot-know] Two things the yml format never recorded, both named in the emitted header as your review list: * **`customBindings[].symbols`.** The yml lists wrapper file *paths*; which symbols each provides is what the typed config needs. `migrate` reads them out of the `.cpp` — top-level `class` / `struct` definitions and Embind `class_("Name")` registrations — and where it finds none it emits a `TODO(libcascade migrate)` marker plus the candidate names (the `bindings` entries that are not OCCT symbols and that no wrapper claims). It never guesses, and the config will not build until you fill the marker in. * **`assemble.exports`.** No yml equivalent exists. It defaults to `'factory'`; switch to `'eager'` if consumers import OCCT names off the package root. The full walkthrough, with the hand-mapping tables and the review checklist, is [Migrate from a yml build](/docs/toolchain/getting-started/migrate-from-yaml). ## Environment [#environment] `LIBCASCADE_CONTAINER_CMD`, `LIBCASCADE_IMAGE`, and `LIBCASCADE_PLATFORM` are documented in [Driver environment](/docs/toolchain/reference/env-vars). ## Programmatic use [#programmatic-use] The container driver is exported for orchestration that needs to sequence runs itself: ```typescript notypecheck import { createContainerDriver } from '@libcascade/toolchain/driver'; ``` That is an escape hatch, not the supported path. Prefer the CLI. --- # Config reference URL: /docs/toolchain/reference/config `libcascade.config.ts` is the single source of truth for a custom build. The CLI looks for `libcascade.config.ts`, `libcascade.config.js`, or `libcascade.config.mjs` in the working directory, or the path you pass to `--config`. It is loaded with [jiti](https://github.com/unjs/jiti), so a TypeScript config needs no build step. ```typescript import { defineBuild } from '@libcascade/toolchain'; export default defineBuild({ name: 'myapp', bindings: ['gp_Pnt', 'TopoDS_Shape'], variants: [{ name: 'single' }], }); ``` `defineBuild` is an identity function — it returns its argument unchanged. Its entire job is to attach types. ## Fields [#fields] | Field | Type | Notes | | ------------------------------- | ---------------------------------------------------------- | ------------------------------------------------------------------------------------------------------- | | `name` | `string` | **Required.** Artifact base name. With no `outputName`, variant `v` produces `_.*`. | | `bindings` | `(OcctSymbol \| CustomSymbol)[]` | **Required.** Symbols to expose to JavaScript. Rendered verbatim into the container's `bindings:` list. | | `customBindings` | `{ file, symbols, scope? }[]` | Your own C++ files and the symbols each provides. | | `settings` | `EmccSettings` | Emscripten `-s` settings. Generated type — see below. | | `compilerFlags` | `{ optimize?, simd?, exceptions?, lto?, noEntry? }` | The closed set of non-`-s` flags worth typing. | | `rawFlags` | `string[]` | Escape hatch. Appended verbatim after every typed flag. | | `variants` | `{ name, outputName?, requires?, settings?, rawFlags? }[]` | **Required, non-empty.** One binary per entry. | | `assemble` | `{ exports: 'eager' \| 'factory' }` | Packaging mode for `libcascade assemble`. Defaults to `'factory'`. | | `image` | `string` | Container image override. `$LIBCASCADE_IMAGE` wins over it; both skip digest verification. | | `generateTypescriptDefinitions` | `boolean` | Defaults to `true`. Set `false` only for fast throwaway iteration — `assemble` needs the `.d.ts`. | ### `bindings` [#bindings] The element type is the generated `OcctSymbol` union — **6,257** string literals covering every OCCT class and enum in the release, every OCCT `typedef` alias the bindgen resolves (`TColgp_Array1OfPnt` → `NCollection_Array1_gp_Pnt`), and the Embind builtins — **plus** exactly the symbols this config's own `customBindings` declare. ```typescript notypecheck bindings: ['BRepPrimAPI_MakeBoox'], // ^ Type '"BRepPrimAPI_MakeBoox"' is not assignable… // Did you mean '"BRepPrimAPI_MakeBox"'? ``` Base classes and `NCollection_*` members of a bound class are auto-discovered at codegen time, so the list holds classes you instantiate or pass around, not their transitive closure. The custom-symbol union is inferred **only** from `customBindings[].symbols`. That is deliberate: a typo'd OCCT name cannot silently widen the union and type itself as valid. ### `customBindings` [#custombindings] ```typescript notypecheck customBindings: [ { file: 'wrappers/mesh-extractor.cpp', symbols: ['MyMeshData', 'MyMeshExtractor'] }, { file: 'build-configs/free-functions.cpp', symbols: ['TopoDS_Cast'], scope: 'main' }, ], ``` | Key | Meaning | | --------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | | `file` | Path to the `.cpp`, resolved relative to the config file's directory. Checked for existence at config load. | | `symbols` | The symbols this file provides. These are the only names `bindings` may carry that are not `OcctSymbol`. | | `scope` | `'all'` (default) renders the file into the container yml's top-level `additionalCppFiles`; `'main'` renders it into `mainBuild.additionalBindFiles`. | See [Extend with C++](/docs/toolchain/guides/extend-with-cpp) for which scope to pick. ### `settings` [#settings] `EmccSettings` is generated from the **image's own** emsdk `settings.js` (6.0.5) plus emcc's legacy/deprecated tables — 312 settings, each carrying its upstream documentation as JSDoc, so hovering a setting in your editor shows what emcc says about it. Legacy names such as `USE_PTHREADS` are typed and marked `@deprecated`. Values whose grammar emcc constrains are typed structurally rather than as `string`: | Setting family | Type | Why | | ------------------------------------------------------------------- | ------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------- | | `INITIAL_MEMORY`, `MAXIMUM_MEMORY`, `STACK_SIZE`, `TOTAL_MEMORY`, … | `MemorySize` = number \| `${number}KB` \| `${number}MB` \| `${number}GB` | `'100mb'` and `'100 MB'` are silent misparses in a yml. Here they do not compile. | | `ENVIRONMENT` | `readonly EmccEnvironment[]` | An array of literals cannot express a typo or a duplicate; the renderer owns the comma joining. | | `EXPORTED_RUNTIME_METHODS`, `EXPORTED_FUNCTIONS`, and 19 more | `readonly string[]` | The renderer emits emcc's bracketed-list syntax, so the yml-quoting fragility disappears. | | `PTHREAD_POOL_SIZE` | `number \| 'navigator.hardwareConcurrency'` | The JS-expression form is a documented emcc idiom; the literal keeps it discoverable without admitting arbitrary strings. | | flags emcc declares as 0/1 ints | `boolean` (rendered `1`/`0`) | Boolean semantics, int encoding. | Serialization at render time: | Value | Rendered | | -------------------------------- | -------------------------- | | `true` / `false` | `-sNAME=1` / `-sNAME=0` | | `number`, `string` | `-sNAME=` verbatim | | `string[]` | `-sNAME=["a","b"]` | | `ENVIRONMENT: ['web', 'worker']` | `-sENVIRONMENT=web,worker` | There is no index signature on `EmccSettings`. That is what makes an unknown `-s` name a compile error — and why anything the typed surface cannot express belongs in `rawFlags`. ### `compilerFlags` [#compilerflags] Only the non-`-s` flags the reference builds actually use are modelled. | Key | Emits | | -------------------------------------------------------- | ------------------------------------ | | `optimize: 'O0' \| 'O1' \| 'O2' \| 'O3' \| 'Os' \| 'Oz'` | `-O3` … | | `simd: true` | `-msimd128` | | `exceptions: 'wasm' \| 'emscripten'` | `-fwasm-exceptions` / `-fexceptions` | | `lto: true` | `-flto` | | `noEntry: true` | `--no-entry` | Anything else — `-mrelaxed-simd`, `-Wl,--allow-undefined`, `--emit-symbol-map`, `-pthread` — goes in `rawFlags`. The rendered flag order is fixed: exceptions → `-s` settings → `-flto` → `--no-entry` → `-msimd128` → optimisation → base `rawFlags` → variant `rawFlags`. emcc treats distinct flags as order-insensitive. ### `variants` [#variants] ```typescript notypecheck variants: [ { name: 'single', settings: { EVAL_CTORS: 2 } }, { name: 'multi', compilerFlags: { threads: true }, settings: { SHARED_MEMORY: true, PTHREAD_POOL_SIZE: 'navigator.hardwareConcurrency' }, }, ], ``` | Key | Meaning | | --------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `name` | Unique within the config. Names the rendered yml, the artifact, and the `--variant` selector. | | `outputName` | Overrides the default `_` artifact base name. | | `compilerFlags` | Merged key by key over the base `compilerFlags`, variant wins. `threads: true` renders `-pthread`. | | `requires` | Capabilities this variant needs from the host. Normally **inferred** from the build flags — a threaded build implies `['threads']` — so most configs never declare it. Declaring it *adds* to what is inferred; it cannot subtract. | | `settings` | Merged over the base `settings`. A value of `null` **removes** an inherited key. | | `rawFlags` | Appended after the base `rawFlags`. | A variant's capabilities select its container image at build time and become the probes the generated `createInstance` runs before returning an instance. Base keys keep their declaration order when a variant overrides them in place; variant-only keys are appended. `null` is how the multi-threaded variant drops the base `EVAL_CTORS` — constructor evaluation order is non-deterministic under pthread workers. Unsetting a key the base never declared is a configuration error, caught at load. A single-variant config is first class: `variants: [{ name: 'single' }]` emits no selector machinery at assemble time. Variants share one `bindings` list on purpose. That is what lets `assemble` emit one `types.d.ts` for all of them — see [Variants and assemble](/docs/toolchain/concepts/variants-and-assemble). ## Load-time validation [#load-time-validation] Types cannot see the filesystem. `libcascade build`, `assemble`, and `check` all validate the loaded config first and report every problem at once, with resolved absolute paths: * empty `name`, empty `bindings`, empty `variants` * duplicate variant names * a variant `null`-unsetting a setting the base never declared * a `customBindings.file` that does not exist on disk * a `customBindings` entry declaring no symbols ## Worked references [#worked-references] Two real configs ship in the open: * [`libcascade.config.ts`](https://github.com/taucad/opencascade.js/blob/main/libcascade.config.ts) — the full build itself: every OCCT symbol, one `scope: 'main'` wrapper file, single + multi variants, `assemble: { exports: 'eager' }`. * [`replicad-opencascadejs`](https://github.com/sgenoud/replicad) — a trimmed custom build: 11 wrapper files declaring 17 custom symbols, single + multi, `assemble: { exports: 'factory' }`. ## Related [#related] * [CLI reference](/docs/toolchain/reference/cli) — the commands that consume this file. * [Driver environment](/docs/toolchain/reference/env-vars) — engine discovery, image override, digest pinning. * [Quickstart](/docs/toolchain/getting-started/quick-start) — from install to `dist/`. --- # Named compile-time configurations URL: /docs/toolchain/reference/configurations **Maintainer track.** These presets are compiled into the published images. A custom build selects between them implicitly — a variant declaring `requires: ['threads']` gets the multi-threaded image, everything else the single-threaded one — and configures the link step through [`libcascade.config.ts`](/docs/toolchain/reference/config). 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 use native WASM exceptions (`OCJS_EXCEPTIONS=1`, `OCJS_EH_MODE=wasm`) and the Closure Compiler. Link-only features such as BigInt and eval-ctors belong to each build YAML's `emccFlags`, not these compile-time presets. ## Shipped presets [#shipped-presets] | Preset | Use case | Differentiating flags | | -------------------------- | ---------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------- | | `single-threaded` | Default custom-build preset when a variant does not require threads. | `OCJS_OPT=-O3`, `OCJS_WASM_OPT_LEVEL=-O4`, `THREADING=single-threaded` | | `single-threaded-smallest` | Size-tuned variant — benchmark it against your workload before shipping. | `OCJS_OPT=-Os`, `OCJS_WASM_OPT_LEVEL=-O3`, `THREADING=single-threaded` | | `multi-threaded` | Published `libcascade/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_CONVERGE": "true", "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_single.provenance.json` and `dist/opencascade_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. * [Emscripten settings and flags](/docs/toolchain/guides/custom-emcc-flags) — link-time `settings` rationale. * [Driver environment](/docs/toolchain/reference/env-vars) — driver variables and the `OCJS_*` catalogue. --- # 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. **You do not pull or run this image by hand.** `libcascade build` resolves the digest its toolchain version pinned, pulls it, verifies the digest, and runs it with the right mounts and UID mapping for your platform. This page documents the image itself — its tags, labels, and stages — for auditing and for building it from source. See [CLI reference](/docs/toolchain/reference/cli) and [Driver environment](/docs/toolchain/reference/env-vars) for the consumer path. ## 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 | | `:{{version}}-` when `{{version}}` is a canary | Immutable manually dispatched canary (e.g. `:3.0.0-canary.a1b2c3d4-single-threaded`), retained for seven days | | `:branch-main[-]` | Current or immutable `main`, single-threaded | | `:multi-threaded-branch-main[-]` | Current or immutable `main`, multi-threaded | | `:bindgen-base-branch-main[-]` | Current or immutable `main`, bindgen-base | | `@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. The toolchain does this for you: each published version embeds the resolved digests in `generated/images.json` and verifies them after pulling. See [Reproducible CI](/docs/toolchain/guides/reproducible-ci). The legacy `:beta`, `:rolling`, and `:latest` tags are **not published** by this project. Use a version-pinned tag (e.g. `:3.0.0-single-threaded`) or the manifest-list digest for explicit version control. ## Entrypoint [#entrypoint] The driver invokes the image as ` run … link `, with the config directory mounted at `/src` and a scratch directory mounted at `/out` via `OCJS_OUTPUT_DIR`. The invocation below is what it constructs — reproduce it by hand only when debugging the image itself: ```bash docker run --rm \ -v "$(pwd):/src" \ -v "$(pwd)/out:/out" \ -e OCJS_OUTPUT_DIR=/out \ ghcr.io/taucad/opencascade.js@sha256: \ 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 caches the canonical `link-core`, then always materializes its exact inventory into the requested output directory before validation and provenance. | | `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] | Event | Result | Built on | | ----------------------------- | ------------------------------- | ----------------------------------------------------------------------------------- | | Pull request targeting `main` | Three-stage validation, no tags | `ubuntu-latest` (amd64) | | `main`, release, or dispatch | `linux/amd64` + `linux/arm64` | `ubuntu-latest` (amd64) + `ubuntu-24.04-arm` (arm64), GitHub Actions native runners | `main`, release, and manually dispatched canary runs ship full manifest lists so Apple Silicon and ARM Linux hosts pull the native architecture transparently. CI links and smokes each image natively and requires every native stage to pass before promotion. The amd64 outputs are the canonical npm package inputs; host-specific compiler output is not compared byte-for-byte across architectures. ## 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. ```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. --- # Driver environment URL: /docs/toolchain/reference/env-vars The typed config is the only configuration channel for a build. The environment variables below control the **driver** — which engine runs, which image it runs, and on which platform. ## Driver variables [#driver-variables] | Variable | Effect | | -------------------------- | ----------------------------------------------------------------------------------------------------------------------- | | `LIBCASCADE_CONTAINER_CMD` | Container engine to probe first. Default probe order: `docker`, then `podman`. | | `LIBCASCADE_IMAGE` | Image reference override. Wins over a config-level `image:`. Skips digest verification and prints a provenance warning. | | `LIBCASCADE_PLATFORM` | Passed to the engine as `--platform`. Unset by default; the published images are multi-arch. | ### Engine discovery [#engine-discovery] The driver probes each candidate with ` version` and takes the first that exits 0. With none available it fails with install options for Docker Desktop, colima, and Podman — and the reminder that GitHub-hosted macOS runners ship no container engine at all. Point `LIBCASCADE_CONTAINER_CMD` at any binary that speaks the same CLI: ```bash LIBCASCADE_CONTAINER_CMD=podman npx libcascade build ``` ### UID mapping and mounts [#uid-mapping-and-mounts] `-u uid:gid` is emitted **only on Linux native engines**. On Docker Desktop for macOS and Windows the VM maps ownership itself and an explicit `-u` breaks the build, so the driver omits it. Mount paths are resolved absolute from the config file's directory. You do not configure any of this. It is the platform-edge handling that consumer `docker run` strings used to copy-paste, subtly differently, each time. ### Platform [#platform] The images are published as multi-arch manifest lists (`linux/amd64` + `linux/arm64`), so Apple Silicon and ARM Linux hosts pull the native architecture automatically. `LIBCASCADE_PLATFORM` exists as a narrow override for debugging cross-architecture issues: ```bash LIBCASCADE_PLATFORM=linux/amd64 npx libcascade build ``` ## Digest pinning [#digest-pinning] The toolchain package ships `generated/images.json`, in which each image tag was resolved to an immutable digest at publish time. The driver runs `ghcr.io/taucad/opencascade.js@sha256:…` — never a tag — and after pulling it inspects the local repo digests to prove the image really is that one. A mismatch is an error naming both the expected and the local digest. Which of the two pinned images a variant gets is decided by its config: `requires: ['threads']` selects the multi-threaded image, everything else the single-threaded one. Consequence worth stating plainly: `@libcascade/toolchain@X` names one reproducible build environment forever. Pinning the toolchain version in your lockfile is the entire pinning story — there is no tag to keep in sync by hand. ## Local-image dev loop [#local-image-dev-loop] Contributors who build the container image locally need the driver to run *that* image. `LIBCASCADE_IMAGE` accepts any reference the local engine can resolve: ```bash # Build the image locally, then point the CLI at it. docker buildx build --target final-single -t ocjs-local:single-threaded . LIBCASCADE_IMAGE=ocjs-local:single-threaded npx libcascade build --variant single ``` While an override is active the driver: * skips digest verification — a locally built image has no repo digest to verify against; * prints a one-line provenance warning naming the override, because the artifacts it produces carry no reproducible toolchain provenance. A config-level `image:` behaves the same way; the environment variable wins over it. Never set either in CI. See [Reproducible CI](/docs/toolchain/guides/reproducible-ci). ## Inside the image [#inside-the-image] The variables below are read by the build system **inside** the container. The driver sets `OCJS_OUTPUT_DIR` itself; the rest are maintainer-facing, relevant when you build the image from source rather than consume it. **Maintainer track.** Nothing here is part of the custom-build path. Your build is configured by `libcascade.config.ts`. ### Compile-time flags [#compile-time-flags] | Variable | Default | Effect | | ------------------- | --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `OCJS_EXCEPTIONS` | `1` | Compile every translation unit with `-fwasm-exceptions`. Mixed builds fail at link. | | `OCJS_SIMD` | `1` | Compile every translation unit with `-msimd128` (baseline SIMD). | | `OCJS_RELAXED_SIMD` | `0` | Additionally emit `-mrelaxed-simd`. Safari 26.x lacks support; keep a baseline build for Safari. | | `OCJS_LTO` | `0` | Enable LLVM LTO. The measured full build grew 21%, so shipped presets keep it off. | | `OCJS_STRICT_TYPES` | `0` (warn-only) | The link-time `.d.ts` post-processor always prints a triage summary to stderr when it rewrites signatures to `unknown`. 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`. | | `OCJS_COMPILE_WORKERS` | Up to 8 | Max parallel binding-object compile workers. Changes execution only, not cache identity. | | `BUILD_DIR` | `./build` | Canonical intermediate root. Custom values are rejected because Nx owns the fixed output graph. | | `OCJS_OUTPUT_DIR` | `/src` in the container | Destination for the uncached materialization step. `libcascade build` sets this to a mounted scratch directory and moves the artifacts into `dist/` afterwards. | ### Image internals [#image-internals] | Variable | Default | Effect | | ---------------------- | ---------------- | ------------------------------------------------------------------- | | `OCJS_DEPS_VERSION` | from `DEPS.json` | Override the dependency 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_VERBOSE` | `0` | Print every compile / link command. | | `OCJS_DUMP_CACHE_KEYS` | `0` | Print cache-key contents on a miss, to debug spurious invalidation. | ## Related [#related] * [CLI reference](/docs/toolchain/reference/cli) — the commands these variables affect. * [Config reference](/docs/toolchain/reference/config) — the only channel for build configuration. * [Named compile-time configurations](/docs/toolchain/reference/configurations) — the `OCJS_*` preset list. * [Two-channel config model](/docs/toolchain/concepts/two-channel-config-model) — why compile-time and link-time config are separate. --- # Container yml contract URL: /docs/toolchain/reference/yaml-schema **Internals.** You do not write this file. `libcascade build` renders one yml per variant from [`libcascade.config.ts`](/docs/toolchain/reference/config) into `.libcascade/` and hands it to the container. This page documents the contract between the two, for reading a rendered yml or debugging a build. The user-facing surface is the [config reference](/docs/toolchain/reference/config); `libcascade build --render-only` shows you what it produces. The yml 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: - additionalBindFiles: - bindings/custom.cpp extraBuilds: - name: bindings: [...] emccFlags: [...] additionalBindFiles: - bindings/variant.cpp additionalCppFiles: - path/to/extra.cpp generateTypescriptDefinitions: true ``` The canonical Cerberus definition lives in `src/customBuildSchema.py`; the renderer that emits this shape lives in the toolchain package. ## `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. The renderer emits these from `settings`, `compilerFlags`, and `rawFlags` in a canonical order. See [Emscripten settings and flags](/docs/toolchain/guides/custom-emcc-flags) for the recommended baseline and per-flag rationale. ### `mainBuild.additionalBindFiles` [#mainbuildadditionalbindfiles] Per-build `.cpp` files containing raw `EMSCRIPTEN_BINDINGS(...)` registrations. The files compile directly, may include their own helper implementation, and skip generated TypeScript bindings. 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-v3`) 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.** libcascade's built-in binding source (`OCJS`, `TopoDS`, `TColStd_IndexedDataMapOfStringString`) registers Embind class wrappers with no generated binding object of their own. Detected by `manifest_registry.builtin_binding_symbols` reading `build/additional-bind-symbols.json`. 4. **Consumer `additionalBindFiles`.** YAML's own `mainBuild.additionalBindFiles` undergoes the same Embind pathway. Each output compiles the built-in source plus its ordered consumer files as one translation unit; the AST producer records their registration-name union in `additional-bind-symbols.json`. 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-v2`). 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-api-reference.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::getAdditionalBindFilesO()` (libclang AST via `ocjs_bindgen.ast.parse_binding_source` + `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-api-reference.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 from one yml pass. The toolchain does not use this key: it renders **one yml per variant**, each with a single `mainBuild`, so a variant failure is isolated and `--variant` can select one. `extraBuilds` remains part of the container contract for hand-written ymls. ## `additionalCppFiles` [#additionalcppfiles] Top-level list of `.cpp` files inspected by bindgen before generated custom bindings are compiled. ```yaml additionalCppFiles: - wrappers/fair-curve.cpp - wrappers/shape-cast.cpp ``` * Paths resolve relative to the YAML file's directory; absolute paths are accepted. * File contents are concatenated in declaration order and read as UTF-8. * Missing, unreadable, or non-file paths fail validation. * Normalized paths and SHA-256 digests are recorded in manifests and provenance. `additionalBindFiles` follows the same path, ordering, validation, and identity rules, but belongs inside each build block. ## `generateTypescriptDefinitions` [#generatetypescriptdefinitions] Default `true`. Set to `false` to skip `.d.ts` generation (rare — useful only for ultra-fast iteration builds). ## Inspecting the rendered yml [#inspecting-the-rendered-yml] ```bash npx libcascade build --render-only ``` Renders one yml per variant into `.libcascade/` and prints their paths without needing a container engine. That is the supported way to read this contract for your own build — and the first gate when migrating an existing hand-written yml, since the flag *set* should match even though the renderer emits a canonical flag order. The config's own invariants — wrapper files existing on disk, unique variant names, `null` unsets with a base key to unset — are checked when the config loads, before anything is rendered. ## Related [#related] * [Config reference](/docs/toolchain/reference/config) — the file you actually write. * [Migrate from a yml build](/docs/toolchain/getting-started/migrate-from-yaml) — the field-by-field mapping. * [Extend with C++](/docs/toolchain/guides/extend-with-cpp) — which `customBindings` scope renders into which key. --- # API reference data URL: /docs/package/reference/libcascade-api/api-reference-data Every published package includes `libcascade/api-reference.json`. The feed is the portable build-time source for the bound OCCT hierarchy, declarations, provenance, and exact input hashes used by this site. ```ts import reference from 'libcascade/api-reference.json' with { type: 'json' }; console.log(reference.package.version); console.log(reference.source.commit); console.log(reference.modules); ``` The schema identifier is `ocjs-api-reference-v1`. Consumers should reject unknown schema identifiers and verify `package.name`, `package.version`, and the full 40-character source commit before generating derived data. The feed deliberately excludes site concerns such as route slugs, anchors, quick links, and search indexes. Those are derived locally. In this repository, `npm exec nx -- run ocjs:docs-sync` reads the installed package by default, or accepts an already-downloaded package directory or tarball: ```bash npm exec nx -- run ocjs:docs-sync -- --from ./libcascade-3.0.0-canary.5bf5e36c.tgz ``` For a complete local docs build from a downloaded CI or npm tarball, keep that source available to every automatic pre-script: ```bash OCJS_API_REFERENCE_SOURCE=../libcascade-3.0.0-canary.5bf5e36c.tgz \ pnpm --dir docs-site build ``` The command is offline after the package is available, replaces stale output atomically, removes deleted symbols, and is byte-idempotent for an unchanged feed. --- # Exception classes URL: /docs/package/reference/libcascade-api/exception-classes OCCT throws subclasses of `Standard_Failure`. Native WASM exception handling surfaces them as `WebAssembly.Exception` values and the initialized runtime decodes their C++ type and message. ```typescript import oc from 'libcascade'; try { riskyOcctCall(); } catch (error: unknown) { if (error instanceof WebAssembly.Exception) { const [type, message] = oc.getExceptionMessage(error); console.error(`${type}: ${message}`); } else { throw error; } } ``` `WebAssembly.Exception.message` does not contain the C++ failure text. Use the helper while the exception belongs to the current runtime. The related `incrementExceptionRefcount` and `decrementExceptionRefcount` instance helpers exist only for code that retains an exception beyond its catch scope. ## Common hierarchy [#common-hierarchy] ```text Standard_Failure ├── Standard_OutOfRange ├── Standard_NullObject ├── Standard_NoSuchObject ├── Standard_TypeMismatch ├── Standard_DomainError ├── Standard_DivideByZero └── Standard_ProgramError └── Standard_NotImplemented ``` See [Debugging WASM exceptions](/docs/package/guides/debugging-wasm-exceptions) for failure patterns and debug builds. --- # Entry points URL: /docs/package/reference/libcascade-api/init-function `libcascade` exposes three entry points. Which one you want depends on whether you need to control *when* the WASM module is instantiated. | Entry | Import | Gives you | | ------- | --------------------------------------- | --------------------------------------------------------------------------------- | | Root | `libcascade` | An already-initialised instance, plus every bound symbol as a named value export. | | Factory | `libcascade/init` | `createInstance(options)` — nothing is instantiated until you call it. | | Variant | `libcascade/single`, `libcascade/multi` | The raw Emscripten glue for one binary. Advanced. | ## Root — the initialised instance [#root--the-initialised-instance] The root entry probes the host, picks the most capable variant it supports, and initialises it with a top-level `await`: ```typescript import oc from 'libcascade'; using box = new oc.BRepPrimAPI_MakeBox(10, 10, 10); const shape = box.Shape(); ``` Every bound symbol is also a named value export, so you can import the classes you use directly: ```typescript import { BRepPrimAPI_MakeBox } from 'libcascade'; using box = new BRepPrimAPI_MakeBox(10, 10, 10); ``` Types are exported from the same specifier: ```typescript import oc, { type OpenCascadeInstance, type TopoDS_Shape } from 'libcascade'; ``` Because the module instantiates at import time, there is nothing to memoise — ES module evaluation happens once per realm. ## `libcascade/init` — the factory [#libcascadeinit--the-factory] Use this entry when you need options, a specific variant, or control over when the WASM comes up. Importing it never evaluates the eager root. ```typescript import { createInstance } from 'libcascade/init'; const oc = await createInstance(); const mt = await createInstance({ variant: 'multi' }); const capped = await createInstance({ variant: 'multi', threadCount: 4 }); ``` ### Options [#options] **`CreateInstanceOptions`** — Options accepted by `createInstance` from the `./init` subpath. - **`locateFile`** (`((path: string, scriptDirectory: string) => string) | undefined`, optional) - **`wasmBinary`** (`ArrayBuffer | Uint8Array | undefined`, optional) - **`wasmMemory`** (`WebAssembly.Memory | undefined`, optional) - **`print`** (`((text: string) => void) | undefined`, optional) - **`printErr`** (`((text: string) => void) | undefined`, optional) - **`variant`** (`LibcascadeVariant | undefined`, optional) — Variant to load. Omitted, the most capable variant the host supports is selected (see the `./init` entry for the capability probes). - **`threadCount`** (`number | undefined`, optional) — Size of OCCT's default thread pool for a threads variant. Omitted, OCCT sizes the pool itself and the launch cap is raised to match it. `createInstance` owns the plumbing that is otherwise the consumer's problem: the glue self-reference Emscripten's pthread workers spawn from, Node `file:` URL → path conversion, and OCCT thread-pool sizing for a threads variant. It throws with an actionable message when you request a variant this host cannot run. ### Memoised singleton pattern [#memoised-singleton-pattern] WASM instantiation is expensive. Memoise the Promise when several call sites share one runtime and you are not using the eager root: ```typescript import { createInstance } from 'libcascade/init'; let ocPromise: ReturnType | undefined; export const getOc = () => (ocPromise ??= createInstance()); ``` ### Variant selection [#variant-selection] Selection picks the most capable variant whose requirements the host meets; `multi` requires `SharedArrayBuffer` and cross-origin isolation (Node always qualifies). To force a choice before anything is imported: ```typescript globalThis[Symbol.for('libcascade.select')] = 'single'; ``` ## Return type [#return-type] The resolved object exposes every bound class and namespace as instance properties (`oc.BRepPrimAPI_MakeBox`, `oc.TopoDS`) together with `oc.FS`, `oc.wasmMemory`, and exception helpers such as `oc.getExceptionMessage`. One `OpenCascadeInstance` type describes every variant, so a shape produced by the single-threaded instance is assignable wherever the multi-threaded one is expected. ## Variant subpaths — advanced [#variant-subpaths--advanced] `libcascade/single` and `libcascade/multi` resolve to the raw Emscripten glue. Their default export is the module factory: ```typescript import init from 'libcascade/multi'; const oc = await init(); ``` This bypasses `createInstance`, so **you** own the pthread plumbing — including the `mainScriptUrlOrBlob` self-reference workers need. Prefer `createInstance({ variant: 'multi' })` unless you have a reason not to. The matching binaries are exported as `libcascade/single/wasm` and `libcascade/multi/wasm`. Browser deployments of the threaded variant also need cross-origin isolation. See [Multi-threaded build](/docs/package/guides/multi-threading). --- # Module shape URL: /docs/package/reference/libcascade-api/module-shape The root entry's default export is an initialised instance. All OCCT runtime values live on it, and each one is also a named value export of the same module. ```typescript import oc, { type TopoDS_Shape } from 'libcascade'; using box = new oc.BRepPrimAPI_MakeBox(10, 10, 10); const shape: TopoDS_Shape = box.Shape(); ``` Named imports resolve to real values as well as types, so `import { BRepPrimAPI_MakeBox } from 'libcascade'` and `oc.BRepPrimAPI_MakeBox` are the same binding. ## Bound classes and namespaces [#bound-classes-and-namespaces] Classes are direct instance properties. OCCT namespaces such as `TopoDS`, `Interface_Static`, and `XCAFDoc_DocumentTool` are static-method objects: ```typescript oc.Interface_Static.SetIVal('write.step.schema', 5); const edge = oc.TopoDS.Edge(genericShape); ``` ## Filesystem (`oc.FS`) [#filesystem-ocfs] Use the Emscripten virtual filesystem for OCCT file readers and writers: ```typescript const path = '/result.step'; writer.Write(path); const bytes = oc.FS.readFile(path) as Uint8Array; oc.FS.unlink(path); return bytes; ``` `FS.readFile()` returns an owned `Uint8Array`; its bytes remain valid after the file is unlinked. Copy only when your own API needs an independent buffer. | Method | Purpose | | --------------------------- | --------------------- | | `FS.readFile(path)` | Read owned bytes | | `FS.writeFile(path, bytes)` | Write an input file | | `FS.unlink(path)` | Remove a virtual path | | `FS.mkdir(path)` | Create a directory | | `FS.readdir(path)` | List a directory | ## Linear memory (`oc.wasmMemory`) [#linear-memory-ocwasmmemory] Advanced pointer interop uses typed arrays created from the live `WebAssembly.Memory` buffer: ```typescript const bytes = new Uint8Array(oc.wasmMemory.buffer); const values = new Float64Array(oc.wasmMemory.buffer); ``` A call that allocates may grow memory and detach an existing view. Create a fresh typed array from `oc.wasmMemory.buffer` after such calls. Most consumers never need direct memory access. ## Exception helpers [#exception-helpers] Exception helpers are also instance properties. See [Exception classes](/docs/package/reference/libcascade-api/exception-classes) and [Debugging WASM exceptions](/docs/package/guides/debugging-wasm-exceptions). ## Module re-init [#module-re-init] Calling `createInstance()` from `libcascade/init` creates another WASM instance with a separate C++ heap. Memoise it when an application should share one runtime. --- # API Reference The full bound OCCT API (5,348 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.