@meshioplusplus/wasm 17.0.0 → 17.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.d.ts CHANGED
@@ -367,6 +367,13 @@ export interface IgaOptions {
367
367
  trim?: boolean;
368
368
  }
369
369
 
370
+ /**
371
+ * A B-rep in CAD JSON's shape: what a `cad_json` file holds, what `fitCad`
372
+ * returns as `info`, and -- in the full artifact -- what reading a `step` or
373
+ * `iges` file with `{info: true}` returns (still `format: 'cad_json'`, so
374
+ * `writeMesh(path, mesh, 'cad_json', {info})` and the `iga*` operations take
375
+ * it unchanged).
376
+ */
370
377
  export interface CadJsonInfo {
371
378
  format: 'cad_json';
372
379
  /** The face surfaces: two degrees, two full knot vectors, (points, 3) control net (u fastest). */
@@ -1695,14 +1702,37 @@ export interface MeshioPlusPlusModule {
1695
1702
  */
1696
1703
  hasCgnslib(): boolean;
1697
1704
 
1705
+ /**
1706
+ * Whether OpenCASCADE is linked in, so `step` and `iges` read and `fitCad`
1707
+ * runs: true in the full artifact (`{full: true}`), false in the lean one.
1708
+ */
1709
+ hasOcct(): boolean;
1710
+
1711
+ /** Whether the VTK XML zstd block codec is linked in (the full artifact). */
1712
+ hasZstd(): boolean;
1713
+
1714
+ /** Whether the VTK XML lz4 block codec is linked in (the full artifact). */
1715
+ hasLz4(): boolean;
1716
+
1717
+ /** Whether bzip2 is linked in, for libMesh `.xda.bz2`/`.xdr.bz2` (the full artifact). */
1718
+ hasBzip2(): boolean;
1719
+
1720
+ /**
1721
+ * Which artifact loaded: `'lean'` (`meshioplusplus_wasm[_mt]`, MIT) or
1722
+ * `'full'` (`meshioplusplus_wasm_full[_mt]`, with OpenCASCADE, zstd, lz4 and
1723
+ * bzip2; mixed licence, see the package's THIRD_PARTY_NOTICES.md).
1724
+ */
1725
+ buildFlavour(): 'lean' | 'full';
1726
+
1698
1727
  /**
1699
1728
  * The format names this build can actually read and write, both sorted.
1700
1729
  *
1701
1730
  * Prefer this over a hardcoded table when building a file-picker filter or a
1702
- * "convert to" menu: the two lists genuinely differ (`openfoam` is
1703
- * read-only, `gltf` and `step` are write-only), and the set depends on how
1731
+ * "convert to" menu: the two lists genuinely differ (`gmsh22` is
1732
+ * write-only), and the set depends on how
1704
1733
  * the artifact was built -- an official build carries the HDF5/netCDF-backed
1705
- * formats, one built with `--without-hdf5` does not.
1734
+ * formats, one built with `--without-hdf5` does not, and `step` and `iges`
1735
+ * are readers only in the full artifact.
1706
1736
  */
1707
1737
  availableFormats(): { readers: string[]; writers: string[] };
1708
1738
 
@@ -2516,6 +2546,18 @@ export interface MeshioPlusPlusModule {
2516
2546
  numUnfitted: number;
2517
2547
  };
2518
2548
 
2549
+ /**
2550
+ * Build a CAD solid from the fitted patches of a closed skin (normally the
2551
+ * output of `segment` then `fitPrimitives`): planes, spheres, cylinders,
2552
+ * cones and tori, fillets and blends, threads and freeform B-spline faces,
2553
+ * sewn and checked by OpenCASCADE, optionally written as STEP to
2554
+ * `stepPath` on the virtual FS. Needs OpenCASCADE: the full artifact only.
2555
+ * See doc/fit_cad.md.
2556
+ * @throws {Error} in the lean artifact (naming `{full: true}`), on a bad
2557
+ * option, or on a skin that is not closed.
2558
+ */
2559
+ fitCad(mesh: Mesh, options?: FitCadOptions): FitCadResult;
2560
+
2519
2561
  /**
2520
2562
  * Refine every patch (every face surface of a B-rep) of a NURBS model
2521
2563
  * exactly: degree elevation by `elevate`, then knot insertion splitting each
@@ -3197,16 +3239,104 @@ export interface MeshioPlusPlusModule {
3197
3239
  * `locateFile`/`onAbort` wrapped for diagnostics (your own overrides still
3198
3240
  * run first) -- see {@link MeshioPlusPlusLoadError}. `locateFile` receives
3199
3241
  * the requested filename, so return the URL matching the loaded variant
3200
- * (`meshioplusplus_wasm.wasm` or `meshioplusplus_wasm_mt.wasm`).
3242
+ * (`meshioplusplus_wasm.wasm` or `meshioplusplus_wasm_mt.wasm`, and their
3243
+ * `_full` counterparts with `full`).
3201
3244
  * @param options.variant which native artifact to load: `'auto'` (default),
3202
3245
  * `'mt'` (force threaded), or `'seq'` (force sequential).
3246
+ * @param options.full load the full artifact (OpenCASCADE, zstd, lz4, bzip2;
3247
+ * mixed licence) rather than the lean, MIT one. Only the chosen artifact's
3248
+ * bytes are fetched.
3249
+ * @param options.fullGlue where the full artifact's glue module lives, for a
3250
+ * bundled app that copied `dist/meshioplusplus_wasm_full*.{mjs,wasm}`
3251
+ * itself; the loader never names the full glue literally, so a bundler
3252
+ * does not pull it into a lean bundle.
3203
3253
  * @throws {MeshioPlusPlusLoadError} if the WASM module fails to instantiate.
3204
3254
  */
3205
3255
  export function loadMeshioPlusPlus(
3206
3256
  moduleOverrides?: ModuleOverrides,
3207
- options?: { variant?: 'auto' | 'mt' | 'seq' },
3257
+ options?: LoadOptions,
3208
3258
  ): Promise<MeshioPlusPlusModule>;
3209
3259
 
3260
+ /** The second argument of {@link loadMeshioPlusPlus}. */
3261
+ export interface LoadOptions {
3262
+ variant?: 'auto' | 'mt' | 'seq';
3263
+ full?: boolean;
3264
+ fullGlue?: string | URL;
3265
+ }
3266
+
3267
+ /** The options of {@link MeshioPlusPlusModule.fitCad}; every key is optional. */
3268
+ export interface FitCadOptions {
3269
+ /** Write the solid as AP214 STEP here (virtual FS); omitted writes nothing. */
3270
+ stepPath?: string;
3271
+ /** The largest distance a face may lie from the skin; <= 0 is automatic. */
3272
+ tolerance?: number;
3273
+ /** Try the exact intersection of two analytic surfaces first (default true). */
3274
+ exactEdges?: boolean;
3275
+ /** The accepted primitive types that become analytic faces (default: all five). */
3276
+ types?: Array<'plane' | 'sphere' | 'cylinder' | 'cone' | 'torus'>;
3277
+ /** The re-meshed solid's chordal deflection; <= 0 takes tolerance / 4. */
3278
+ meshDeflection?: number;
3279
+ /** Rounds of demoting failed faces before every patch is faceted (default 8). */
3280
+ maxDemotionRounds?: number;
3281
+ /** Try a freeform B-spline face where no primitive fits (default true). */
3282
+ freeform?: boolean;
3283
+ /** The freeform B-spline's highest degree, 3 to 8 (default 3). */
3284
+ freeformDegree?: number;
3285
+ /** The most grid points per direction a freeform fit samples (default 64). */
3286
+ freeformMaxControl?: number;
3287
+ /** Split a freeform patch that folds over every flattening (default true). */
3288
+ freeformSplit?: boolean;
3289
+ /** Try fillets, blends, chamfers and vertex blends (default true). */
3290
+ fillets?: boolean;
3291
+ /** Try thread faces, tapered and of varying pitch included (default true). */
3292
+ threads?: boolean;
3293
+ }
3294
+
3295
+ /** What {@link MeshioPlusPlusModule.fitCad} built. */
3296
+ export interface FitCadResult {
3297
+ /** The solid re-meshed, with `fit_cad:face`, `fit_cad:type` and `fit_cad:patch` cell data. */
3298
+ mesh: Mesh;
3299
+ /** Whether the solid passed OpenCASCADE's B-rep check. */
3300
+ valid: boolean;
3301
+ tolerance: number;
3302
+ volume: number;
3303
+ skinVolume: number;
3304
+ volumeRatio: number;
3305
+ /** Symmetric Hausdorff distance, skin to re-meshed solid. */
3306
+ maxDeviation: number;
3307
+ numFaces: number;
3308
+ numPlane: number;
3309
+ numSphere: number;
3310
+ numCylinder: number;
3311
+ numCone: number;
3312
+ numTorus: number;
3313
+ numFacetedFaces: number;
3314
+ numPatches: number;
3315
+ numDemoted: number;
3316
+ numExactEdges: number;
3317
+ numApproxEdges: number;
3318
+ numSolids: number;
3319
+ numBspline: number;
3320
+ numSplitPatches: number;
3321
+ numFillet: number;
3322
+ numHelical: number;
3323
+ numVariableFillet: number;
3324
+ numBlend: number;
3325
+ numRoundedThread: number;
3326
+ /** Vertex blends (setback corners), type 9. */
3327
+ numVertexBlend: number;
3328
+ /** Chamfers (straight-section blends), type 10. */
3329
+ numChamfer: number;
3330
+ /** Fillet faces whose section is a conic rather than a circle. */
3331
+ numConicBlend: number;
3332
+ /** Thread flanks on a tapered (conical) thread. */
3333
+ numTaperedThread: number;
3334
+ /** Thread flanks whose pitch varies along the axis. */
3335
+ numVariablePitchThread: number;
3336
+ /** The solid's B-rep, for `writeMesh(path, mesh, 'cad_json', {info})`. */
3337
+ info: CadJsonInfo;
3338
+ }
3339
+
3210
3340
  /** Overrides forwarded to the underlying Emscripten module factory. */
3211
3341
  export interface ModuleOverrides {
3212
3342
  /**
@@ -3234,6 +3364,8 @@ export interface MeshioPlusPlusLoadError extends Error {
3234
3364
  name: 'MeshioPlusPlusLoadError';
3235
3365
  /** Which native artifact was being loaded. */
3236
3366
  variant: 'mt' | 'seq';
3367
+ /** Whether it was the full artifact. */
3368
+ full: boolean;
3237
3369
  /** The glue module specifier (e.g. '../dist/meshioplusplus_wasm_mt.mjs'). */
3238
3370
  glue: string;
3239
3371
  /** The filename Emscripten asked `locateFile` to resolve, if it got that far. */
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@meshioplusplus/wasm",
3
- "version": "17.0.0",
4
- "description": "meshio++ mesh I/O compiled to WebAssembly: read 76 and write 68 mesh formats (incl. MED, CGNS, Exodus) in the browser or Node.js",
3
+ "version": "17.2.0",
4
+ "description": "meshio++ mesh I/O compiled to WebAssembly: read 80 and write 75 mesh formats (incl. MED, CGNS, Exodus; STEP and IGES in the full build) in the browser or Node.js",
5
5
  "type": "module",
6
6
  "main": "./src/index.mjs",
7
7
  "module": "./src/index.mjs",
@@ -12,8 +12,13 @@
12
12
  "dist/meshioplusplus_wasm.wasm",
13
13
  "dist/meshioplusplus_wasm_mt.mjs",
14
14
  "dist/meshioplusplus_wasm_mt.wasm",
15
+ "dist/meshioplusplus_wasm_full.mjs",
16
+ "dist/meshioplusplus_wasm_full.wasm",
17
+ "dist/meshioplusplus_wasm_full_mt.mjs",
18
+ "dist/meshioplusplus_wasm_full_mt.wasm",
15
19
  "index.d.ts",
16
- "README.md"
20
+ "README.md",
21
+ "THIRD_PARTY_NOTICES.md"
17
22
  ],
18
23
  "engines": {
19
24
  "node": ">=18"
@@ -29,9 +34,12 @@
29
34
  "obj",
30
35
  "finite-elements",
31
36
  "fem",
37
+ "step",
38
+ "iges",
39
+ "cad",
32
40
  "scientific-computing"
33
41
  ],
34
- "license": "MIT",
42
+ "license": "MIT AND (LGPL-2.1-only WITH OCCT-exception-1.0)",
35
43
  "author": "Vicente Mataix Ferrandiz",
36
44
  "homepage": "https://github.com/loumalouomega/meshioplusplus",
37
45
  "repository": {
package/src/index.mjs CHANGED
@@ -29,6 +29,16 @@
29
29
  // context, else the sequential one. The dynamic import()s are literal so
30
30
  // bundlers (Vite) emit both chunks. Callers can force a build with the
31
31
  // `{ variant }` option.
32
+ //
33
+ // Beside that lean pair the package ships the FULL pair (roadmap 6.2),
34
+ // meshioplusplus_wasm_full and meshioplusplus_wasm_full_mt: the same API, with
35
+ // OpenCASCADE (STEP/IGES reading, fitCad), zstd, lz4 and bzip2 linked in, and
36
+ // a mixed licence (OpenCASCADE is LGPL-2.1 with its exception; see
37
+ // THIRD_PARTY_NOTICES.md). `{ full: true }` loads it, with the same variant
38
+ // choice. Its import() is deliberately NOT literal: a bundler that saw it would
39
+ // pull the full bytes into every lean consumer's bundle. Node and native
40
+ // browser ESM resolve it next to this file; a bundled app copies the full
41
+ // glue and .wasm itself and passes `fullGlue` (and `locateFile`).
32
42
 
33
43
  // The raw xdmfSeriesWriteDataArrays embind call still converts every value
34
44
  // with emscripten::convertJSArrayToNumberVector<double>, which throws on a
@@ -61,6 +71,17 @@ function resolveVariant(variant) {
61
71
  return crossOriginIsolated ? 'mt' : 'seq';
62
72
  }
63
73
 
74
+ /**
75
+ * The glue module of the full artifact for a variant, relative to this file.
76
+ * @param {'mt'|'seq'} chosen
77
+ * @returns {string}
78
+ */
79
+ function fullGlueFor(chosen) {
80
+ return chosen === 'mt'
81
+ ? '../dist/meshioplusplus_wasm_full_mt.mjs'
82
+ : '../dist/meshioplusplus_wasm_full.mjs';
83
+ }
84
+
64
85
  /**
65
86
  * Thrown by `loadMeshioPlusPlus()` when a WASM module fails to instantiate,
66
87
  * so a caller can tell a wrong `.wasm` URL from an environment that cannot
@@ -72,6 +93,7 @@ export class MeshioPlusPlusLoadError extends Error {
72
93
  * @param {string} message
73
94
  * @param {object} details
74
95
  * @param {'mt'|'seq'} details.variant - which artifact was being loaded.
96
+ * @param {boolean} [details.full] - whether it was the full artifact.
75
97
  * @param {string} details.glue - the glue module specifier (e.g.
76
98
  * '../dist/meshioplusplus_wasm_mt.mjs').
77
99
  * @param {string} [details.requestedFile] - the filename Emscripten asked
@@ -79,10 +101,11 @@ export class MeshioPlusPlusLoadError extends Error {
79
101
  * @param {string} [details.resolvedUrl] - what `locateFile` returned for it.
80
102
  * @param {unknown} [details.cause] - the underlying error/abort reason.
81
103
  */
82
- constructor(message, { variant, glue, requestedFile, resolvedUrl, cause }) {
104
+ constructor(message, { variant, full = false, glue, requestedFile, resolvedUrl, cause }) {
83
105
  super(message, cause === undefined ? undefined : { cause });
84
106
  this.name = 'MeshioPlusPlusLoadError';
85
107
  this.variant = variant;
108
+ this.full = full;
86
109
  this.glue = glue;
87
110
  this.requestedFile = requestedFile;
88
111
  this.resolvedUrl = resolvedUrl;
@@ -190,12 +213,19 @@ export class MeshioPlusPlusLoadError extends Error {
190
213
  * `{ locateFile: (p) => new URL(p, import.meta.url) }` relocates the
191
214
  * `.wasm` binary for a bundler/CDN setup; `locateFile` receives the
192
215
  * requested filename, so return the URL matching the loaded variant
193
- * (`meshioplusplus_wasm.wasm` or `_wasm_mt.wasm`).
216
+ * (`meshioplusplus_wasm.wasm` or `_wasm_mt.wasm`, and `_wasm_full.wasm` or
217
+ * `_wasm_full_mt.wasm` with `full`).
194
218
  * @param {object} [options]
195
219
  * @param {'auto'|'mt'|'seq'} [options.variant='auto'] - which native artifact to
196
220
  * load. `auto` picks the threaded (`mt`) build under Node and in a
197
221
  * cross-origin-isolated browser, else the sequential (`seq`) build; `mt`/`seq`
198
222
  * force one (`mt` aborts if the environment cannot host Wasm threads).
223
+ * @param {boolean} [options.full=false] - load the full artifact (OpenCASCADE,
224
+ * zstd, lz4, bzip2; mixed licence) instead of the lean, MIT one. The lean
225
+ * bytes are never fetched then, nor the full ones without it.
226
+ * @param {string|URL} [options.fullGlue] - where the full artifact's glue module
227
+ * lives, for a bundled app that copied it (default: `../dist/` beside this
228
+ * file). Pair it with a `locateFile` that finds the matching `.wasm`.
199
229
  * @returns {Promise<{
200
230
  * FS: object,
201
231
  * readMesh: (path: string, format?: string) => Mesh,
@@ -218,6 +248,11 @@ export class MeshioPlusPlusLoadError extends Error {
218
248
  * meshBackend: () => string,
219
249
  * parallelBackend: () => string,
220
250
  * hasCgnslib: () => boolean,
251
+ * hasOcct: () => boolean,
252
+ * hasZstd: () => boolean,
253
+ * hasLz4: () => boolean,
254
+ * hasBzip2: () => boolean,
255
+ * buildFlavour: () => 'lean' | 'full',
221
256
  * availableFormats: () => {readers: string[], writers: string[]},
222
257
  * extractSurface: (mesh: Mesh, recordParentIds?: boolean) => Mesh,
223
258
  * extractSkin: (mesh: Mesh, linearize?: boolean) => Mesh,
@@ -260,6 +295,7 @@ export class MeshioPlusPlusLoadError extends Error {
260
295
  * igaSample: (info: object, options?: {sampleSubdivisions?: number, trimSamples?: number, edges?: boolean, trim?: boolean}) => {mesh: Mesh, info: object},
261
296
  * igaEvaluate: (info: object, patch: number, params: ArrayLike<number>, options?: {derivatives?: 0 | 1, trimSamples?: number}) => object,
262
297
  * fitPrimitives: (mesh: Mesh, patchData?: string, tolerance?: number, relative?: boolean, minInlierFraction?: number, types?: string[], maxIterations?: number) => {mesh: Mesh, numPatches: number, numPlane: number, numSphere: number, numCylinder: number, numCone: number, numTorus: number, numUnfitted: number},
298
+ * fitCad: (mesh: Mesh, options?: {stepPath?: string, tolerance?: number, exactEdges?: boolean, types?: string[], meshDeflection?: number, maxDemotionRounds?: number, freeform?: boolean, freeformDegree?: number, freeformMaxControl?: number, freeformSplit?: boolean, fillets?: boolean, threads?: boolean}) => object,
263
299
  * tessellate: (mesh: Mesh, levels?: number, curved?: boolean, fields?: boolean, recordStencil?: boolean) => {mesh: Mesh, numSourcePoints: number, numSourceCells: number, numCurvedSourceCells: number, numPassThroughSourceCells: number, numPoints: number, numCells: number, numBoundaryFacets: number, numInteriorFacets: number, numFacetsWithBadCount: number},
264
300
  * render: (mesh: Mesh, options?: RenderOptions) => RenderedFrame,
265
301
  * renderText: (mesh: Mesh, options?: RenderOptions, text?: RenderTextOptions) => string,
@@ -314,16 +350,43 @@ export class MeshioPlusPlusLoadError extends Error {
314
350
  * }>}
315
351
  * @throws {MeshioPlusPlusLoadError} if the WASM module fails to instantiate.
316
352
  */
317
- export async function loadMeshioPlusPlus(moduleOverrides = {}, { variant = 'auto' } = {}) {
353
+ export async function loadMeshioPlusPlus(
354
+ moduleOverrides = {},
355
+ { variant = 'auto', full = false, fullGlue } = {},
356
+ ) {
318
357
  const chosen = resolveVariant(variant);
319
- const glue =
320
- chosen === 'mt' ? '../dist/meshioplusplus_wasm_mt.mjs' : '../dist/meshioplusplus_wasm.mjs';
321
- // Literal specifiers so bundlers emit both chunks; the sequential one is
322
- // the fallback whenever threads are unavailable.
323
- const { default: createRawModule } =
324
- chosen === 'mt'
325
- ? await import('../dist/meshioplusplus_wasm_mt.mjs')
326
- : await import('../dist/meshioplusplus_wasm.mjs');
358
+ const flavour = full ? 'full' : 'lean';
359
+ const label = full ? `full ${chosen}` : chosen;
360
+ let glue;
361
+ let createRawModule;
362
+ if (full) {
363
+ glue = String(fullGlue ?? fullGlueFor(chosen));
364
+ // A computed specifier on purpose (see the header): bundlers leave it
365
+ // alone, so a lean bundle never carries the full artifact.
366
+ try {
367
+ ({ default: createRawModule } = await import(
368
+ /* @vite-ignore */ /* webpackIgnore: true */ fullGlue === undefined
369
+ ? new URL(glue, import.meta.url).href
370
+ : glue
371
+ ));
372
+ } catch (cause) {
373
+ throw new MeshioPlusPlusLoadError(
374
+ `meshio++ (wasm): cannot import the full artifact's glue (${glue}): ` +
375
+ `${cause.message ?? cause}. A bundled app copies ` +
376
+ "dist/meshioplusplus_wasm_full*.{mjs,wasm} and passes `fullGlue`.",
377
+ { variant: chosen, full, glue, cause },
378
+ );
379
+ }
380
+ } else {
381
+ glue =
382
+ chosen === 'mt' ? '../dist/meshioplusplus_wasm_mt.mjs' : '../dist/meshioplusplus_wasm.mjs';
383
+ // Literal specifiers so bundlers emit both chunks; the sequential one
384
+ // is the fallback whenever threads are unavailable.
385
+ ({ default: createRawModule } =
386
+ chosen === 'mt'
387
+ ? await import('../dist/meshioplusplus_wasm_mt.mjs')
388
+ : await import('../dist/meshioplusplus_wasm.mjs'));
389
+ }
327
390
 
328
391
  // Wrap locateFile/onAbort (rather than passing moduleOverrides through
329
392
  // untouched) so a failed instantiation can be reported with which file
@@ -355,10 +418,10 @@ export async function loadMeshioPlusPlus(moduleOverrides = {}, { variant = 'auto
355
418
  Module = await createRawModule(wrappedOverrides);
356
419
  } catch (cause) {
357
420
  throw new MeshioPlusPlusLoadError(
358
- `meshio++ (wasm): failed to instantiate the '${chosen}' variant (${glue}): ` +
421
+ `meshio++ (wasm): failed to instantiate the '${label}' variant (${glue}): ` +
359
422
  `${abortReason ?? cause.message ?? cause}. Emscripten requested ` +
360
423
  `'${requestedFile}' and locateFile returned '${resolvedUrl}'.`,
361
- { variant: chosen, glue, requestedFile, resolvedUrl, cause },
424
+ { variant: chosen, full, glue, requestedFile, resolvedUrl, cause },
362
425
  );
363
426
  }
364
427
 
@@ -371,11 +434,25 @@ export async function loadMeshioPlusPlus(moduleOverrides = {}, { variant = 'auto
371
434
  const actualBackend = Module.parallelBackend();
372
435
  if (actualBackend !== expectedBackend) {
373
436
  throw new MeshioPlusPlusLoadError(
374
- `meshio++ (wasm): failed to instantiate the '${chosen}' variant (${glue}): ` +
437
+ `meshio++ (wasm): failed to instantiate the '${label}' variant (${glue}): ` +
375
438
  `loaded, but reports parallel backend '${actualBackend}' (expected ` +
376
439
  `'${expectedBackend}') -- locateFile likely returned the wrong .wasm binary. ` +
377
440
  `Emscripten requested '${requestedFile}' and locateFile returned '${resolvedUrl}'.`,
378
- { variant: chosen, glue, requestedFile, resolvedUrl },
441
+ { variant: chosen, full, glue, requestedFile, resolvedUrl },
442
+ );
443
+ }
444
+ // The same tell for the lean/full split: the two share the whole export
445
+ // surface, so a lean .wasm behind the full glue (or the reverse) can link.
446
+ // A binary from before buildFlavour existed is a lean one.
447
+ const actualFlavour =
448
+ typeof Module.buildFlavour === 'function' ? Module.buildFlavour() : 'lean';
449
+ if (actualFlavour !== flavour) {
450
+ throw new MeshioPlusPlusLoadError(
451
+ `meshio++ (wasm): failed to instantiate the '${label}' variant (${glue}): ` +
452
+ `loaded, but reports the '${actualFlavour}' artifact (expected '${flavour}') -- ` +
453
+ `locateFile likely returned the wrong .wasm binary. Emscripten requested ` +
454
+ `'${requestedFile}' and locateFile returned '${resolvedUrl}'.`,
455
+ { variant: chosen, full, glue, requestedFile, resolvedUrl },
379
456
  );
380
457
  }
381
458
 
@@ -523,10 +600,17 @@ export async function loadMeshioPlusPlus(moduleOverrides = {}, { variant = 'auto
523
600
  // meshioplusplus_wasm_mt one -- which of the two this instance loaded.
524
601
  parallelBackend: () => Module.parallelBackend(),
525
602
  hasCgnslib: () => Module.hasCgnslib(),
603
+ // The full artifact's libraries (roadmap 6.2): all four are true there
604
+ // and false in the lean one. buildFlavour() names which loaded.
605
+ hasOcct: () => Module.hasOcct(),
606
+ hasZstd: () => Module.hasZstd(),
607
+ hasLz4: () => Module.hasLz4(),
608
+ hasBzip2: () => Module.hasBzip2(),
609
+ buildFlavour: () => Module.buildFlavour(),
526
610
  // What this build can actually read/write, both sorted. Prefer this
527
- // over a hardcoded table: a few formats are read-only (openfoam) or
528
- // write-only (gltf), and the HDF5/netCDF-backed ones are present
529
- // only in a build that linked them (the official artifact does).
611
+ // over a hardcoded table: a few formats are write-only (gmsh22), and the
612
+ // HDF5/netCDF-backed ones are present only in a build that linked them
613
+ // (the official artifact does).
530
614
  availableFormats: () => Module.availableFormats(),
531
615
  // Mesh operations (see doc/*.md per operation): computations ON a mesh,
532
616
  // not file formats. Index maps (crop/split/convertCells) are not carried
@@ -862,6 +946,8 @@ export async function loadMeshioPlusPlus(moduleOverrides = {}, { variant = 'auto
862
946
  ) =>
863
947
  Module.fitPrimitives(mesh, patchData, tolerance, relative, minInlierFraction,
864
948
  Array.isArray(types) ? types : [types], maxIterations),
949
+ // OpenCASCADE: the full artifact only; the lean one throws by name.
950
+ fitCad: (mesh, options = {}) => Module.fitCad(mesh, options),
865
951
  igaRefine: (info, options = {}) => Module.igaRefine(info, options),
866
952
  igaSample: (info, options = {}) => Module.igaSample(info, options),
867
953
  igaEvaluate: (info, patch, params, options = {}) =>