@meshioplusplus/wasm 10.21.1 → 12.0.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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@meshioplusplus/wasm",
3
- "version": "10.21.1",
4
- "description": "meshio++ mesh I/O compiled to WebAssembly: read/write 41 mesh formats (incl. MED, CGNS, Exodus) in the browser or Node.js",
3
+ "version": "12.0.0",
4
+ "description": "meshio++ mesh I/O compiled to WebAssembly: read 43 and write 46 mesh formats (incl. MED, CGNS, Exodus) in the browser or Node.js",
5
5
  "type": "module",
6
6
  "main": "./src/index.mjs",
7
7
  "module": "./src/index.mjs",
package/src/index.mjs CHANGED
@@ -30,6 +30,24 @@
30
30
  // bundlers (Vite) emit both chunks. Callers can force a build with the
31
31
  // `{ variant }` option.
32
32
 
33
+ // The raw xdmfSeriesWriteDataArrays embind call still converts every value
34
+ // with emscripten::convertJSArrayToNumberVector<double>, which throws on a
35
+ // BigInt64Array/BigUint64Array element (a JS `bigint` does not implicitly
36
+ // convert to `double`). Since data arrays now carry their source dtype
37
+ // (point_data/cell_data read off a mesh may be BigInt-backed), widen those to
38
+ // plain Float64-representable arrays here rather than in every caller.
39
+ function toDoubleConvertible(rArr) {
40
+ return rArr instanceof BigInt64Array || rArr instanceof BigUint64Array
41
+ ? Array.from(rArr, Number)
42
+ : rArr;
43
+ }
44
+
45
+ function widenBigIntArrays(rObj) {
46
+ const out = {};
47
+ for (const [name, arr] of Object.entries(rObj)) out[name] = toDoubleConvertible(arr);
48
+ return out;
49
+ }
50
+
33
51
  /**
34
52
  * Decide which native artifact to load.
35
53
  * @param {'auto'|'mt'|'seq'} variant
@@ -43,6 +61,35 @@ function resolveVariant(variant) {
43
61
  return crossOriginIsolated ? 'mt' : 'seq';
44
62
  }
45
63
 
64
+ /**
65
+ * Thrown by `loadMeshioPlusPlus()` when a WASM module fails to instantiate,
66
+ * so a caller can tell a wrong `.wasm` URL from an environment that cannot
67
+ * run Wasm threads from a plain network failure, instead of catching a bare
68
+ * Emscripten abort. See doc/wasm.md's "Loading" section.
69
+ */
70
+ export class MeshioPlusPlusLoadError extends Error {
71
+ /**
72
+ * @param {string} message
73
+ * @param {object} details
74
+ * @param {'mt'|'seq'} details.variant - which artifact was being loaded.
75
+ * @param {string} details.glue - the glue module specifier (e.g.
76
+ * '../dist/meshioplusplus_wasm_mt.mjs').
77
+ * @param {string} [details.requestedFile] - the filename Emscripten asked
78
+ * `locateFile` to resolve, if it got that far.
79
+ * @param {string} [details.resolvedUrl] - what `locateFile` returned for it.
80
+ * @param {unknown} [details.cause] - the underlying error/abort reason.
81
+ */
82
+ constructor(message, { variant, glue, requestedFile, resolvedUrl, cause }) {
83
+ super(message, cause === undefined ? undefined : { cause });
84
+ this.name = 'MeshioPlusPlusLoadError';
85
+ this.variant = variant;
86
+ this.glue = glue;
87
+ this.requestedFile = requestedFile;
88
+ this.resolvedUrl = resolvedUrl;
89
+ this.cause = cause;
90
+ }
91
+ }
92
+
46
93
  /**
47
94
  * A rectangular (uniform node count) group of cells.
48
95
  * @typedef {Object} RectangularCellBlock
@@ -90,27 +137,60 @@ function resolveVariant(variant) {
90
137
  * @property {() => void} close - finalize (if needed) and release the handle.
91
138
  */
92
139
 
140
+ /**
141
+ * A single sequence entry: one step of one file. See {@link SequenceReader}.
142
+ * @typedef {Object} SequenceEntry
143
+ * @property {string} path
144
+ * @property {number} step
145
+ * @property {number} time
146
+ * @property {'explicit'|'file'|'filename'|'index'} timeSource
147
+ */
148
+
149
+ /**
150
+ * @typedef {Object} SequenceReader
151
+ * @property {number} count
152
+ * @property {(i: number) => string} path
153
+ * @property {(i: number) => number} step
154
+ * @property {(i: number) => number} time
155
+ * @property {(i: number) => ('explicit'|'file'|'filename'|'index')} timeSource
156
+ * @property {(i: number) => SequenceEntry} entry
157
+ * @property {() => SequenceEntry[]} entries
158
+ * @property {(i: number, options?: {pointsOnly?: boolean, arrays?: string[]|null, lenient?: boolean}) => Mesh} read
159
+ * @property {() => void} close - release the handle; safe to call twice.
160
+ */
161
+
162
+ /**
163
+ * A point_data/cell_data/field_data array's JS type: it carries its source
164
+ * dtype crossing the WASM boundary rather than always widening to
165
+ * Float64Array (dtype carry, v11.2.0).
166
+ * @typedef {Float32Array|Float64Array|Int8Array|Int16Array|Int32Array|BigInt64Array|Uint8Array|Uint16Array|Uint32Array|BigUint64Array} DataArray
167
+ */
168
+
93
169
  /**
94
170
  * @typedef {Object} Mesh
95
171
  * @property {Float64Array} points - flat, row-major (numPoints * dim).
96
172
  * @property {number} dim - 2 or 3.
97
173
  * @property {CellBlock[]} cells
98
- * @property {Object<string, Float64Array>} [point_data]
174
+ * @property {Object<string, DataArray>} [point_data]
99
175
  * @property {Object<string, number>} [point_data_components] - per-entity width of any non-scalar point_data array, since a flat typed array carries no shape; absent name = 1 component.
100
- * @property {Object<string, Float64Array[]>} [cell_data] - one array per cell block, same order as `cells`.
176
+ * @property {Object<string, DataArray[]>} [cell_data] - one array per cell block, same order as `cells`; every block of one named array shares the same DataArray class.
101
177
  * @property {Object<string, number>} [cell_data_components] - per-entity width of any non-scalar cell_data array (one value per array, not per block).
102
- * @property {Object<string, Float64Array>} [field_data]
178
+ * @property {Object<string, DataArray>} [field_data]
103
179
  * @property {Object<string, number>} [field_data_components] - per-entity width of any non-scalar field_data array.
180
+ * @property {Array<{name: string, kind: string, dim: number, tag: number, entries: Int32Array}>} [regions] - named point/cell/side groups, see doc/regions.md.
181
+ * @property {Array<{id: number, values: Array<{key: string, values: Float64Array, text: string, isTable: boolean, components?: number}>}>} [propertySets] - `Begin Properties` blocks (currently MDPA only), see doc/wasm.md.
104
182
  */
105
183
 
106
184
  /**
107
185
  * Instantiate a fresh meshio++ WASM module.
108
186
  *
109
187
  * @param {object} [moduleOverrides] - forwarded to the Emscripten module
110
- * factory as-is (e.g. `{ locateFile: (p) => new URL(p, import.meta.url) }`
111
- * if you need to relocate the `.wasm` binary for a bundler/CDN setup).
112
- * `locateFile` receives the requested filename, so return the URL matching
113
- * the loaded variant (`meshioplusplus_wasm.wasm` or `_wasm_mt.wasm`).
188
+ * factory (with `locateFile`/`onAbort` wrapped for diagnostics -- see
189
+ * {@link MeshioPlusPlusLoadError} -- your own overrides still run first).
190
+ * `{ locateFile: (p) => new URL(p, import.meta.url) }` relocates the
191
+ * `.wasm` binary for a bundler/CDN setup; `locateFile` receives the
192
+ * requested filename, so return the URL matching the loaded variant
193
+ * (`meshioplusplus_wasm.wasm` or `_wasm_mt.wasm`).
114
194
  * @param {object} [options]
115
195
  * @param {'auto'|'mt'|'seq'} [options.variant='auto'] - which native artifact to
116
196
  * load. `auto` picks the threaded (`mt`) build under Node and in a
@@ -119,11 +199,11 @@ function resolveVariant(variant) {
119
199
  * @returns {Promise<{
120
200
  * FS: object,
121
201
  * readMesh: (path: string, format?: string) => Mesh,
122
- * readMeshSelective: (path: string, options?: {format?: string, pointsOnly?: boolean, arrays?: string[], timeStep?: number, lenient?: boolean}) => Mesh,
202
+ * readMeshSelective: (path: string, options?: {format?: string, pointsOnly?: boolean, arrays?: string[], timeStep?: number, lenient?: boolean, info?: boolean}) => Mesh,
123
203
  * readMetadata: (path: string, format?: string) => object,
124
204
  * readerSupportsOptions: (format: string) => boolean,
125
- * writeMesh: (path: string, mesh: Mesh, format?: string) => void,
126
- * convert: (inPath: string, outPath: string, options?: {inFormat?: string, outFormat?: string}) => void,
205
+ * writeMesh: (path: string, mesh: Mesh, format?: string, options?: {encoding?: string, codec?: string, floatFormat?: string, info?: object}) => string[],
206
+ * convert: (inPath: string, outPath: string, options?: {inFormat?: string, outFormat?: string, encoding?: string, codec?: string, floatFormat?: string}) => string[],
127
207
  * convertSurface: (inPath: string, outPath: string, options?: {inFormat?: string, outFormat?: string}) => void,
128
208
  * convertSurfaceOps: (inPath: string, outPath: string, ops?: object[], options?: {inFormat?: string, outFormat?: string, keepProvenance?: boolean}) => {steps: object[], warnings: string[]},
129
209
  * runPipeline: (settings: object|string) => {steps: object[], warnings: string[]},
@@ -144,13 +224,16 @@ function resolveVariant(variant) {
144
224
  * computeBandwidth: (mesh: Mesh) => number,
145
225
  * diff: (a: Mesh, b: Mesh, atol?: number, rtol?: number, unordered?: boolean) => object,
146
226
  * meshesEqual: (a: Mesh, b: Mesh, atol?: number, rtol?: number, unordered?: boolean) => boolean,
147
- * merge: (meshes: Mesh[], weld?: boolean, atol?: number, sourceTag?: boolean, dataPolicy?: string, dropDuplicateCells?: boolean) => Mesh,
227
+ * merge: (meshes: Mesh[], weld?: boolean, atol?: number, sourceTag?: boolean, dataPolicy?: string, dropDuplicateCells?: boolean, returnMaps?: boolean) => Mesh | {mesh: Mesh, pointMaps: Int32Array[], cellMaps: Int32Array[]},
148
228
  * transform: (mesh: Mesh, matrix: number[], rotateVectorData?: boolean) => Mesh,
149
- * clean: (mesh: Mesh, weld?: boolean, atol?: number, removeOrphans?: boolean, dropDegenerate?: boolean, dropDuplicateCells?: boolean) => {mesh: Mesh, pointsWelded: number, pointsRemovedOrphan: number, cellsDroppedDegenerate: number, cellsDroppedDuplicate: number},
150
- * smooth: (mesh: Mesh, method?: string, iterations?: number, lambda?: number, mu?: number, fixBoundary?: boolean, preserveFeatures?: boolean, featureAngle?: number, guardInversion?: boolean) => {mesh: Mesh, numNodesMoved: number, maxDisplacement: number, numSkippedInversion: number},
151
- * cropBbox: (mesh: Mesh, lo: number[], hi: number[], mode?: string, recordIds?: boolean) => Mesh,
152
- * cropPlane: (mesh: Mesh, point: number[], normal: number[], mode?: string, recordIds?: boolean) => Mesh,
153
- * cropPredicate: (mesh: Mesh, array: string, compare?: string, value?: number, recordIds?: boolean) => Mesh,
229
+ * clean: (mesh: Mesh, weld?: boolean, atol?: number, removeOrphans?: boolean, dropDegenerate?: boolean, dropDuplicateCells?: boolean, returnMaps?: boolean) => {mesh: Mesh, pointsWelded: number, pointsRemovedOrphan: number, cellsDroppedDegenerate: number, cellsDroppedDuplicate: number, pointMap?: Int32Array, cellMaps?: Int32Array[]},
230
+ * smooth: (mesh: Mesh, method?: string, iterations?: number, lambda?: number, mu?: number, fixBoundary?: boolean, preserveFeatures?: boolean, featureAngle?: number, guardInversion?: boolean, frozen?: number[]|Int32Array|null) => {mesh: Mesh, numNodesMoved: number, maxDisplacement: number, numSkippedInversion: number},
231
+ * interpolate: (source: Mesh, target: Mesh, method?: string, arrays?: string[], extrapolate?: boolean, defaultValue?: number, onConflict?: string) => Mesh,
232
+ * conservativeInterpolate: (source: Mesh, target: Mesh, arrays?: string[], defaultValue?: number, onConflict?: string) => Mesh,
233
+ * undoGreen: (coarse: Mesh, fine: Mesh) => {mesh: Mesh, numGroupsUndone: number, numCellsRemoved: number},
234
+ * cropBbox: (mesh: Mesh, lo: number[], hi: number[], mode?: string, recordIds?: boolean, returnMaps?: boolean) => Mesh | {mesh: Mesh, pointMap: Int32Array, cellMaps: Int32Array[]},
235
+ * cropPlane: (mesh: Mesh, point: number[], normal: number[], mode?: string, recordIds?: boolean, returnMaps?: boolean) => Mesh | {mesh: Mesh, pointMap: Int32Array, cellMaps: Int32Array[]},
236
+ * cropPredicate: (mesh: Mesh, array: string, compare?: string, value?: number, recordIds?: boolean, returnMaps?: boolean) => Mesh | {mesh: Mesh, pointMap: Int32Array, cellMaps: Int32Array[]},
154
237
  * slice: (mesh: Mesh, origin: number[], normal: number[], recordParentIds?: boolean) => Mesh,
155
238
  * isosurface: (mesh: Mesh, array: string, isovalues: number|number[], component?: number, recordParentIds?: boolean) => Mesh,
156
239
  * grid: (dims: number[], origin?: number[], spacing?: number[], maxCells?: number) => Mesh,
@@ -165,13 +248,20 @@ function resolveVariant(variant) {
165
248
  * remesh: (mesh: Mesh, numClusters: number, subdivide?: number, subsampleRatio?: number, maxSubdivide?: number, maxIterations?: number, maxRepairPasses?: number, metric?: string, gradation?: number, preserveBoundary?: boolean, maxAnisotropy?: number) => {mesh: Mesh, numClusters: number, numIterations: number, subdivideApplied: number, numIsolatedClusters: number, numNonManifoldVertices: number},
166
249
  * remeshVolume: (mesh: Mesh, resolution?: number[], cellSize?: number, bounds?: number[], padding?: number, paddingRelative?: number, maxCells?: number, maxTets?: number, warpFraction?: number, sign?: string, watertightCheck?: string) => {mesh: Mesh, numTets: number, numVerticesWarped: number, numTetsRejected: number, numNonManifoldEdges: number},
167
250
  * optimizeVolume: (mesh: Mesh, maxIterations?: number, relocate?: boolean, flip?: boolean, preserveBoundary?: boolean, minImprovement?: number) => {mesh: Mesh, numFlips: number, num23Flips: number, num32Flips: number, numVerticesMoved: number, numTets: number, minQualityBefore: number, minQualityAfter: number},
168
- * split: (mesh: Mesh, by: string, tagName?: string) => {key: string, mesh: Mesh}[],
169
- * convertCells: (mesh: Mesh, mode?: string, recordParentIds?: boolean) => Mesh,
170
- * subdivide: (mesh: Mesh, recordParentIds?: boolean) => Mesh,
171
- * agglomerate: (mesh: Mesh, targetGroupSize?: number) => Mesh,
251
+ * computeCurvature: (mesh: Mesh, mean?: boolean, gaussian?: boolean, dualArea?: string, includeBoundary?: boolean, recordArea?: boolean, recordPrincipal?: boolean, region?: string) => {mesh: Mesh, numBoundary: number, numIsolated: number, numDegenerate: number, totalAngleDefect: number, quality: {boundaryEdges: number, nonManifoldEdges: number, inconsistentPairs: number, degenerateTriangles: number, watertight: boolean}},
252
+ * repair: (mesh: Mesh, fixOrientation?: boolean, orientOutward?: boolean, fillHoles?: boolean, splitNonManifold?: boolean, maxHoleEdges?: number, weldTolerance?: number, recordProvenance?: boolean) => {mesh: Mesh, qualityBefore: object, qualityAfter: object, numFlipped: number, numComponents: number, largestComponent: number, numOrientedOutward: number, numUnorientable: number, numVerticesSplit: number, numHolesDetected: number, numHolesFilled: number, numHolesSkipped: number, numFacesAdded: number, numPointsAdded: number, pointsWelded: number},
253
+ * shrinkwrap: (mesh: Mesh, target: Mesh, offset?: number, maxDistance?: number, weights?: string, targetRegion?: string, normalWeight?: string, recordDistance?: boolean, recordClosestCell?: boolean) => {mesh: Mesh, quality: object, numProjected: number, numMissed: number, numSkipped: number, maxDisplacement: number},
254
+ * sobolevDeform: (mesh: Mesh, array: string, lengthScale: number, fixedPointsArray?: string, fixBoundary?: boolean, recordFiltered?: boolean, maxIterations?: number, tolerance?: number) => {mesh: Mesh, numIterations: number, residual: number, converged: boolean, numFixed: number, numIsolated: number, maxDisplacement: number},
255
+ * split: (mesh: Mesh, by: string, tagName?: string, returnMaps?: boolean) => {key: string, mesh: Mesh, pointMap?: Int32Array, cellMaps?: Int32Array[]}[],
256
+ * convertCells: (mesh: Mesh, mode?: string, recordParentIds?: boolean, returnMaps?: boolean) => Mesh | {mesh: Mesh, pointMap: Int32Array, cellMaps: Int32Array[]},
257
+ * subdivide: (mesh: Mesh, recordParentIds?: boolean, returnMaps?: boolean) => Mesh | {mesh: Mesh, cellMaps: Int32Array[]},
258
+ * agglomerate: (mesh: Mesh, targetGroupSize?: number, returnMaps?: boolean) => Mesh | {mesh: Mesh, cellMap: Int32Array},
172
259
  * refine: (mesh: Mesh, levels?: number, recordParentIds?: boolean,
173
- * options?: object) => Mesh,
174
- * decimate: (mesh: Mesh, ratio?: number, targetFaces?: number, maxError?: number, placement?: string, preserveBoundary?: boolean, preserveFeatures?: boolean, featureAngle?: number) => {mesh: Mesh, facesRemoved: number, pointsRemoved: number, collapsesRejected: number, maxErrorApplied: number},
260
+ * options?: object, returnMaps?: boolean) => Mesh | {mesh: Mesh, pointMap: Int32Array, cellMaps: Int32Array[]},
261
+ * decimate: (mesh: Mesh, ratio?: number, targetFaces?: number, maxError?: number, placement?: string, preserveBoundary?: boolean, preserveFeatures?: boolean, featureAngle?: number, frozen?: number[]|Int32Array|null, returnMaps?: boolean) => {mesh: Mesh, facesRemoved: number, pointsRemoved: number, collapsesRejected: number, maxErrorApplied: number, pointMap?: Int32Array, cellMaps?: Int32Array[]},
262
+ * decimateVolume: (mesh: Mesh, ratio?: number, targetCells?: number, maxError?: number, placement?: string, preserveBoundary?: boolean, preserveFeatures?: boolean, featureAngle?: number, frozen?: number[]|Int32Array|null, returnMaps?: boolean) => {mesh: Mesh, tetsRemoved: number, pointsRemoved: number, collapsesRejected: number, maxErrorApplied: number, pointMap?: Int32Array, cellMaps?: Int32Array[]},
263
+ * partition: (mesh: Mesh, nparts: number, method?: string, imbalance?: number, mode?: string, seed?: number, recordIds?: boolean, ghostLayers?: number, weightsKey?: string, returnMaps?: boolean) => {partId: number, mesh: Mesh, pointMap?: Int32Array, cellMaps?: Int32Array[]}[],
264
+ * partitionLabels: (mesh: Mesh, nparts: number, method?: string, imbalance?: number, mode?: string, seed?: number, weightsKey?: string) => number[][],
175
265
  * stats: (mesh: Mesh) => object,
176
266
  * withProvenance: <T>(mode: number|null|undefined, fn: () => T) => T,
177
267
  * provenanceBegin: (mode?: number) => void,
@@ -190,17 +280,75 @@ function resolveVariant(variant) {
190
280
  * dataInfo: (mesh: Mesh) => object[],
191
281
  * dataIntegrate: (mesh: Mesh, arrays?: string[]) => object[],
192
282
  * createXdmfTimeSeriesWriter: (path: string, options?: {dataFormat?: string, gzipLevel?: number, mode?: 'truncate'|'append', autoFlush?: boolean}) => XdmfTimeSeriesWriter,
283
+ * openSequence: (source: string|string[], options?: object) => SequenceReader,
193
284
  * }>}
285
+ * @throws {MeshioPlusPlusLoadError} if the WASM module fails to instantiate.
194
286
  */
195
287
  export async function loadMeshioPlusPlus(moduleOverrides = {}, { variant = 'auto' } = {}) {
196
288
  const chosen = resolveVariant(variant);
289
+ const glue =
290
+ chosen === 'mt' ? '../dist/meshioplusplus_wasm_mt.mjs' : '../dist/meshioplusplus_wasm.mjs';
197
291
  // Literal specifiers so bundlers emit both chunks; the sequential one is
198
292
  // the fallback whenever threads are unavailable.
199
293
  const { default: createRawModule } =
200
294
  chosen === 'mt'
201
295
  ? await import('../dist/meshioplusplus_wasm_mt.mjs')
202
296
  : await import('../dist/meshioplusplus_wasm.mjs');
203
- const Module = await createRawModule(moduleOverrides);
297
+
298
+ // Wrap locateFile/onAbort (rather than passing moduleOverrides through
299
+ // untouched) so a failed instantiation can be reported with which file
300
+ // Emscripten actually asked for and what locateFile resolved it to,
301
+ // instead of a bare Emscripten abort message. onAbort is overridden even
302
+ // though the try/catch below is the primary path, because an abort under
303
+ // Node does not always surface as a catchable rejection in every
304
+ // Emscripten build -- this guarantees the reason is captured regardless.
305
+ const userLocateFile = moduleOverrides.locateFile;
306
+ const userOnAbort = moduleOverrides.onAbort;
307
+ let requestedFile;
308
+ let resolvedUrl;
309
+ let abortReason;
310
+ const wrappedOverrides = {
311
+ ...moduleOverrides,
312
+ locateFile: (path, prefix) => {
313
+ requestedFile = path;
314
+ resolvedUrl = userLocateFile ? userLocateFile(path, prefix) : prefix + path;
315
+ return resolvedUrl;
316
+ },
317
+ onAbort: (reason) => {
318
+ abortReason = reason;
319
+ if (userOnAbort) userOnAbort(reason);
320
+ },
321
+ };
322
+
323
+ let Module;
324
+ try {
325
+ Module = await createRawModule(wrappedOverrides);
326
+ } catch (cause) {
327
+ throw new MeshioPlusPlusLoadError(
328
+ `meshio++ (wasm): failed to instantiate the '${chosen}' variant (${glue}): ` +
329
+ `${abortReason ?? cause.message ?? cause}. Emscripten requested ` +
330
+ `'${requestedFile}' and locateFile returned '${resolvedUrl}'.`,
331
+ { variant: chosen, glue, requestedFile, resolvedUrl, cause },
332
+ );
333
+ }
334
+
335
+ // A mismatched locateFile (e.g. the seq .wasm handed to the mt glue) does
336
+ // not always fail instantiation with a LinkError -- the two binaries share
337
+ // enough of an export surface that it can link and then silently behave
338
+ // like the wrong variant. parallelBackend() is the cheap, always-present
339
+ // tell: catch it here rather than let every caller re-discover it.
340
+ const expectedBackend = chosen === 'mt' ? 'openmp' : 'seq';
341
+ const actualBackend = Module.parallelBackend();
342
+ if (actualBackend !== expectedBackend) {
343
+ throw new MeshioPlusPlusLoadError(
344
+ `meshio++ (wasm): failed to instantiate the '${chosen}' variant (${glue}): ` +
345
+ `loaded, but reports parallel backend '${actualBackend}' (expected ` +
346
+ `'${expectedBackend}') -- locateFile likely returned the wrong .wasm binary. ` +
347
+ `Emscripten requested '${requestedFile}' and locateFile returned '${resolvedUrl}'.`,
348
+ { variant: chosen, glue, requestedFile, resolvedUrl },
349
+ );
350
+ }
351
+
204
352
  return {
205
353
  FS: Module.FS,
206
354
  readMesh: (path, format = '') => Module.readMesh(path, format),
@@ -212,6 +360,11 @@ export async function loadMeshioPlusPlus(moduleOverrides = {}, { variant = 'auto
212
360
  // first, negative counts from the end, out of range throws.
213
361
  // `lenient` downgrades "this reader cannot represent construct X" to a
214
362
  // warning plus a skip; a malformed file still throws.
363
+ // `info: true` attaches the format's side channel as `mesh.info`
364
+ // (openfoam/med/mdpa/ansysinp/unv/gmsh/exodus; ignored, not thrown,
365
+ // for any other format) -- see doc/wasm.md's "Side channel (info)"
366
+ // section. `pointsOnly`/`arrays` reach it only for the formats whose
367
+ // info reader takes selective-read options (med/mdpa/gmsh/exodus).
215
368
  readMeshSelective: (
216
369
  path,
217
370
  {
@@ -220,15 +373,37 @@ export async function loadMeshioPlusPlus(moduleOverrides = {}, { variant = 'auto
220
373
  arrays = null,
221
374
  timeStep = 0,
222
375
  lenient = false,
376
+ info = false,
223
377
  } = {},
224
- ) => Module.readMeshSelective(path, format, pointsOnly, arrays, timeStep, lenient),
378
+ ) => Module.readMeshSelective(path, format, pointsOnly, arrays, timeStep, lenient, info),
225
379
  // Summarize a file without loading its heavy arrays. The returned
226
380
  // object's `fellBackToFullRead` says whether that was actually cheap.
227
381
  readMetadata: (path, format = '') => Module.readMetadata(path, format),
228
382
  readerSupportsOptions: (format) => Module.readerSupportsOptions(format),
229
- writeMesh: (path, mesh, format = '') => Module.writeMesh(path, mesh, format),
230
- convert: (inPath, outPath, { inFormat = '', outFormat = '' } = {}) =>
231
- Module.convert(inPath, inFormat, outPath, outFormat),
383
+ // `options`: `{encoding, codec, floatFormat, info}`, all optional --
384
+ // unset/empty reproduces the exact pre-v11.2.0 write. `encoding` is
385
+ // 'ascii'/'binary' (format-default otherwise); `codec` is a VTK-XML
386
+ // (vtu/vtp) block-compression codec, 'none'/'zlib'/'lz4'/'zstd'; an
387
+ // option a format cannot honour throws naming the format. `info`
388
+ // writes that format's side channel (openfoam/mdpa/ansysinp/unv/gmsh/
389
+ // med); when omitted, `mesh.info` is used instead if its own
390
+ // `format` matches this write's -- a read(info:true) round-trips
391
+ // back through a write with no extra plumbing. `info` given for a
392
+ // format with no side-channel writer (e.g. exodus, read-only, or any
393
+ // format without one at all) throws naming it. Returns every
394
+ // virtual-FS path the write touched (new or changed), sorted -- more
395
+ // than one for a multi-file writer (`.xdmf` + its `.h5` companion,
396
+ // an OpenFOAM `polyMesh` directory's files, ...).
397
+ writeMesh: (path, mesh, format = '', options = undefined) =>
398
+ Module.writeMesh(path, mesh, format, options),
399
+ // `options` adds `encoding`/`codec`/`floatFormat` to `inFormat`/
400
+ // `outFormat` (see `writeMesh`). Returns the written paths, as
401
+ // `writeMesh` does.
402
+ convert: (
403
+ inPath,
404
+ outPath,
405
+ { inFormat = '', outFormat = '', encoding, codec, floatFormat } = {},
406
+ ) => Module.convert(inPath, inFormat, outPath, outFormat, { encoding, codec, floatFormat }),
232
407
  // Like `convert`, but writes a renderable *surface*: a volume mesh
233
408
  // becomes its boundary, everything else passes through, and the result
234
409
  // is linearized. Prefer this over readMesh -> extractSkin -> writeMesh
@@ -303,6 +478,9 @@ export async function loadMeshioPlusPlus(moduleOverrides = {}, { variant = 'auto
303
478
  Module.diff(a, b, atol, rtol, unordered),
304
479
  meshesEqual: (a, b, atol = 0, rtol = 0, unordered = false) =>
305
480
  Module.meshesEqual(a, b, atol, rtol, unordered),
481
+ // `returnMaps` (default false): when true, returns
482
+ // `{mesh, pointMaps, cellMaps}` instead of a bare mesh -- one array
483
+ // per input mesh, in input order.
306
484
  merge: (
307
485
  meshes,
308
486
  weld = false,
@@ -310,9 +488,15 @@ export async function loadMeshioPlusPlus(moduleOverrides = {}, { variant = 'auto
310
488
  sourceTag = true,
311
489
  dataPolicy = 'intersection',
312
490
  dropDuplicateCells = false,
313
- ) => Module.merge(meshes, weld, atol, sourceTag, dataPolicy, dropDuplicateCells),
491
+ returnMaps = false,
492
+ ) =>
493
+ Module.merge(
494
+ meshes, weld, atol, sourceTag, dataPolicy, dropDuplicateCells, returnMaps,
495
+ ),
314
496
  transform: (mesh, matrix, rotateVectorData = false) =>
315
497
  Module.transform(mesh, matrix, rotateVectorData),
498
+ // `returnMaps` (default false): when true, the result also carries
499
+ // `pointMap`/`cellMaps` (input index -> output index, -1 if dropped).
316
500
  clean: (
317
501
  mesh,
318
502
  weld = false,
@@ -320,10 +504,16 @@ export async function loadMeshioPlusPlus(moduleOverrides = {}, { variant = 'auto
320
504
  removeOrphans = true,
321
505
  dropDegenerate = true,
322
506
  dropDuplicateCells = true,
323
- ) => Module.clean(mesh, weld, atol, removeOrphans, dropDegenerate, dropDuplicateCells),
324
- // A negative `lambda` means "this method's own default" (0.5 Laplacian,
325
- // 0.33 Taubin) and is forwarded unchanged; the frozen-node mask is not
326
- // exposed here, as on the other flat bindings.
507
+ returnMaps = false,
508
+ ) =>
509
+ Module.clean(
510
+ mesh, weld, atol, removeOrphans, dropDegenerate, dropDuplicateCells, returnMaps,
511
+ ),
512
+ // `method` is 'taubin', 'laplacian' or 'odt' (tet-only). A negative
513
+ // `lambda` means "this method's own default" (0.5 Laplacian, 0.33
514
+ // Taubin) and is forwarded unchanged. `frozen` is an optional array of
515
+ // 0-based point ids to pin outright, unioned with any boundary/feature
516
+ // pins; an out-of-range id throws by name.
327
517
  smooth: (
328
518
  mesh,
329
519
  method = 'taubin',
@@ -334,6 +524,7 @@ export async function loadMeshioPlusPlus(moduleOverrides = {}, { variant = 'auto
334
524
  preserveFeatures = true,
335
525
  featureAngle = 30,
336
526
  guardInversion = true,
527
+ frozen = null,
337
528
  ) =>
338
529
  Module.smooth(
339
530
  mesh,
@@ -345,6 +536,7 @@ export async function loadMeshioPlusPlus(moduleOverrides = {}, { variant = 'auto
345
536
  preserveFeatures,
346
537
  featureAngle,
347
538
  guardInversion,
539
+ frozen,
348
540
  ),
349
541
  // Cross-mesh field transfer: source point_data sampled at the target's
350
542
  // points, source cell_data by nearest source-cell centroid regardless
@@ -387,12 +579,15 @@ export async function loadMeshioPlusPlus(moduleOverrides = {}, { variant = 'auto
387
579
  // Restore `fine`'s transitional (green) cells to their coarse parent,
388
580
  // read verbatim from `coarse` (a lookup, not a reconstruction).
389
581
  undoGreen: (coarse, fine) => Module.undoGreen(coarse, fine),
390
- cropBbox: (mesh, lo, hi, mode = 'all', recordIds = false) =>
391
- Module.cropBbox(mesh, lo, hi, mode, recordIds),
392
- cropPlane: (mesh, point, normal, mode = 'all', recordIds = false) =>
393
- Module.cropPlane(mesh, point, normal, mode, recordIds),
394
- cropPredicate: (mesh, array, compare = '<', value = 0, recordIds = false) =>
395
- Module.cropPredicate(mesh, array, compare, value, recordIds),
582
+ // Each crop* returns a bare mesh, or `{mesh, pointMap, cellMaps}` when
583
+ // `returnMaps` (the trailing argument, default false) is set.
584
+ cropBbox: (mesh, lo, hi, mode = 'all', recordIds = false, returnMaps = false) =>
585
+ Module.cropBbox(mesh, lo, hi, mode, recordIds, returnMaps),
586
+ cropPlane: (mesh, point, normal, mode = 'all', recordIds = false, returnMaps = false) =>
587
+ Module.cropPlane(mesh, point, normal, mode, recordIds, returnMaps),
588
+ cropPredicate: (
589
+ mesh, array, compare = '<', value = 0, recordIds = false, returnMaps = false,
590
+ ) => Module.cropPredicate(mesh, array, compare, value, recordIds, returnMaps),
396
591
  slice: (mesh, origin, normal, recordParentIds = false) =>
397
592
  Module.slice(mesh, origin, normal, recordParentIds),
398
593
  grid: (dims, origin = null, spacing = null, maxCells = 20000000) =>
@@ -545,15 +740,82 @@ export async function loadMeshioPlusPlus(moduleOverrides = {}, { variant = 'auto
545
740
  ) =>
546
741
  Module.optimizeVolume(mesh, maxIterations, relocate, flip, preserveBoundary,
547
742
  minImprovement),
548
- split: (mesh, by, tagName = '') => Module.split(mesh, by, tagName),
549
- convertCells: (mesh, mode = 'linearize', recordParentIds = false) =>
550
- Module.convertCells(mesh, mode, recordParentIds),
551
- subdivide: (mesh, recordParentIds = false) => Module.subdivide(mesh, recordParentIds),
552
- agglomerate: (mesh, targetGroupSize = 8) => Module.agglomerate(mesh, targetGroupSize),
553
- refine: (mesh, levels = 1, recordParentIds = false, options = undefined) =>
554
- Module.refine(mesh, levels, recordParentIds, options),
555
- // Exactly one of ratio / targetFaces / maxError must be non-negative;
556
- // the frozen mask is not exposed here, as on the other flat bindings.
743
+ computeCurvature: (
744
+ mesh,
745
+ mean = true,
746
+ gaussian = true,
747
+ dualArea = 'mixed-voronoi',
748
+ includeBoundary = false,
749
+ recordArea = false,
750
+ recordPrincipal = false,
751
+ region = '',
752
+ ) =>
753
+ Module.computeCurvature(mesh, mean, gaussian, dualArea, includeBoundary,
754
+ recordArea, recordPrincipal, region),
755
+ repair: (
756
+ mesh,
757
+ fixOrientation = true,
758
+ orientOutward = true,
759
+ fillHoles = true,
760
+ splitNonManifold = true,
761
+ maxHoleEdges = 10,
762
+ weldTolerance = 0,
763
+ recordProvenance = false,
764
+ ) =>
765
+ Module.repair(mesh, fixOrientation, orientOutward, fillHoles, splitNonManifold,
766
+ maxHoleEdges, weldTolerance, recordProvenance),
767
+ shrinkwrap: (
768
+ mesh,
769
+ target,
770
+ offset = 0,
771
+ maxDistance = 0,
772
+ weights = '',
773
+ targetRegion = '',
774
+ normalWeight = 'angle',
775
+ recordDistance = false,
776
+ recordClosestCell = false,
777
+ ) =>
778
+ Module.shrinkwrap(mesh, target, offset, maxDistance, weights, targetRegion,
779
+ normalWeight, recordDistance, recordClosestCell),
780
+ sobolevDeform: (
781
+ mesh,
782
+ array,
783
+ lengthScale,
784
+ fixedPointsArray = '',
785
+ fixBoundary = false,
786
+ recordFiltered = false,
787
+ maxIterations = 128,
788
+ tolerance = 1e-10,
789
+ ) =>
790
+ Module.sobolevDeform(mesh, array, lengthScale, fixedPointsArray, fixBoundary,
791
+ recordFiltered, maxIterations, tolerance),
792
+ // `returnMaps` (default false): when true, each `{key, mesh}` piece
793
+ // also carries `pointMap`/`cellMaps`.
794
+ split: (mesh, by, tagName = '', returnMaps = false) =>
795
+ Module.split(mesh, by, tagName, returnMaps),
796
+ // `returnMaps` (default false): when true, returns
797
+ // `{mesh, pointMap, cellMaps}` instead of a bare mesh.
798
+ convertCells: (mesh, mode = 'linearize', recordParentIds = false, returnMaps = false) =>
799
+ Module.convertCells(mesh, mode, recordParentIds, returnMaps),
800
+ // `returnMaps` (default false): when true, returns `{mesh, cellMaps}`
801
+ // instead of a bare mesh -- there is no point map (subdivide never
802
+ // prunes or renumbers a point).
803
+ subdivide: (mesh, recordParentIds = false, returnMaps = false) =>
804
+ Module.subdivide(mesh, recordParentIds, returnMaps),
805
+ // `returnMaps` (default false): when true, returns `{mesh, cellMap}`
806
+ // instead of a bare mesh -- a single FLAT array (global cell index ->
807
+ // global cell index), unlike the other ops' per-block `cellMaps`.
808
+ agglomerate: (mesh, targetGroupSize = 8, returnMaps = false) =>
809
+ Module.agglomerate(mesh, targetGroupSize, returnMaps),
810
+ // `returnMaps` (default false): when true, returns
811
+ // `{mesh, pointMap, cellMaps}` instead of a bare mesh.
812
+ refine: (mesh, levels = 1, recordParentIds = false, options = undefined,
813
+ returnMaps = false) =>
814
+ Module.refine(mesh, levels, recordParentIds, options, returnMaps),
815
+ // Exactly one of ratio / targetFaces / maxError must be non-negative.
816
+ // `frozen` is an optional array of 0-based point ids to pin outright;
817
+ // an out-of-range id throws by name. `returnMaps` (default false):
818
+ // when true, the result also carries `pointMap`/`cellMaps`.
557
819
  decimate: (
558
820
  mesh,
559
821
  ratio = -1,
@@ -563,6 +825,8 @@ export async function loadMeshioPlusPlus(moduleOverrides = {}, { variant = 'auto
563
825
  preserveBoundary = true,
564
826
  preserveFeatures = true,
565
827
  featureAngle = 30,
828
+ frozen = null,
829
+ returnMaps = false,
566
830
  ) =>
567
831
  Module.decimate(
568
832
  mesh,
@@ -573,7 +837,42 @@ export async function loadMeshioPlusPlus(moduleOverrides = {}, { variant = 'auto
573
837
  preserveBoundary,
574
838
  preserveFeatures,
575
839
  featureAngle,
840
+ frozen,
841
+ returnMaps,
576
842
  ),
843
+ // Tetrahedral VOLUME decimation (decimate's tet-edge-collapse sibling).
844
+ // Exactly one of ratio / targetCells / maxError must be non-negative.
845
+ // `preserveBoundary` defaults to false here, matching the C++ default
846
+ // (decimate's own default is true) -- decimateVolume's boundary is
847
+ // usually interior geometry a solver still wants simplified.
848
+ // `returnMaps` (default false): when true, the result also carries
849
+ // `pointMap`/`cellMaps`.
850
+ decimateVolume: (
851
+ mesh,
852
+ ratio = -1,
853
+ targetCells = -1,
854
+ maxError = -1,
855
+ placement = 'optimal',
856
+ preserveBoundary = false,
857
+ preserveFeatures = true,
858
+ featureAngle = 30,
859
+ frozen = null,
860
+ returnMaps = false,
861
+ ) =>
862
+ Module.decimateVolume(
863
+ mesh,
864
+ ratio,
865
+ targetCells,
866
+ maxError,
867
+ placement,
868
+ preserveBoundary,
869
+ preserveFeatures,
870
+ featureAngle,
871
+ frozen,
872
+ returnMaps,
873
+ ),
874
+ // `returnMaps` (default false): when true, each `{partId, mesh}`
875
+ // piece also carries `pointMap`/`cellMaps`.
577
876
  partition: (
578
877
  mesh,
579
878
  nparts,
@@ -584,6 +883,7 @@ export async function loadMeshioPlusPlus(moduleOverrides = {}, { variant = 'auto
584
883
  recordIds = false,
585
884
  ghostLayers = 0,
586
885
  weightsKey = '',
886
+ returnMaps = false,
587
887
  ) =>
588
888
  Module.partition(
589
889
  mesh,
@@ -595,6 +895,7 @@ export async function loadMeshioPlusPlus(moduleOverrides = {}, { variant = 'auto
595
895
  recordIds,
596
896
  ghostLayers,
597
897
  weightsKey,
898
+ returnMaps,
598
899
  ),
599
900
  partitionLabels: (
600
901
  mesh,
@@ -680,7 +981,13 @@ export async function loadMeshioPlusPlus(moduleOverrides = {}, { variant = 'auto
680
981
  // Raw solver arrays instead of a mesh; `components` gives the
681
982
  // per-entity width of any array that is not a scalar.
682
983
  writeDataArrays: (time, pointData, cellData = {}, components = {}) =>
683
- Module.xdmfSeriesWriteDataArrays(handle, time, pointData, cellData, components),
984
+ Module.xdmfSeriesWriteDataArrays(
985
+ handle,
986
+ time,
987
+ widenBigIntArrays(pointData),
988
+ widenBigIntArrays(cellData),
989
+ components,
990
+ ),
684
991
  // Make the `.xdmf` readable now, without finalizing.
685
992
  flush: () => Module.xdmfSeriesFlush(handle),
686
993
  finalize: () => Module.xdmfSeriesFinalize(handle),
@@ -700,6 +1007,42 @@ export async function loadMeshioPlusPlus(moduleOverrides = {}, { variant = 'auto
700
1007
  },
701
1008
  };
702
1009
  },
1010
+ // Stateful sequence reader (see doc/sequences.md) -- the other
1011
+ // stateful thing in this API, the same opaque-handle-plus-free-
1012
+ // functions shape as createXdmfTimeSeriesWriter above. `source`/
1013
+ // `options` are exactly sequenceEntries'; this plans the sequence
1014
+ // once (`entries()`) and lets a caller read one step at a time
1015
+ // (`read()`) without holding more than one mesh alive, unlike
1016
+ // sequenceEntries + readMesh in a loop, which still requires the
1017
+ // caller to resolve the format/step itself.
1018
+ openSequence: (source, options = undefined) => {
1019
+ const handle = Module.sequenceOpen(source, options);
1020
+ const count = Module.sequenceCount(handle);
1021
+ const entry = (i) => ({
1022
+ path: Module.sequencePath(handle, i),
1023
+ step: Module.sequenceStep(handle, i),
1024
+ time: Module.sequenceTime(handle, i),
1025
+ timeSource: Module.sequenceTimeSource(handle, i),
1026
+ });
1027
+ let open = true;
1028
+ return {
1029
+ count,
1030
+ path: (i) => Module.sequencePath(handle, i),
1031
+ step: (i) => Module.sequenceStep(handle, i),
1032
+ time: (i) => Module.sequenceTime(handle, i),
1033
+ timeSource: (i) => Module.sequenceTimeSource(handle, i),
1034
+ entry,
1035
+ entries: () => Array.from({ length: count }, (_, i) => entry(i)),
1036
+ read: (i, { pointsOnly = false, arrays = null, lenient = false } = {}) =>
1037
+ Module.sequenceRead(handle, i, { pointsOnly, arrays, lenient }),
1038
+ // Safe to call twice; safe to call in a `finally`.
1039
+ close: () => {
1040
+ if (!open) return;
1041
+ open = false;
1042
+ Module.sequenceFree(handle);
1043
+ },
1044
+ };
1045
+ },
703
1046
  };
704
1047
  }
705
1048