okgeometry-api 1.4.0 → 1.5.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/src/Mesh.ts CHANGED
@@ -2235,6 +2235,128 @@ export class Mesh {
2235
2235
  );
2236
2236
  }
2237
2237
 
2238
+ /**
2239
+ * Union many meshes into a single mesh in ONE WASM call.
2240
+ *
2241
+ * Substantially faster than chaining pairwise `union()` calls: every input
2242
+ * crosses the JS/WASM boundary once, intermediate results never round-trip
2243
+ * through buffers, mutually disjoint inputs are concatenated instead of
2244
+ * run through CSG, and overlapping inputs are merged smallest-first.
2245
+ *
2246
+ * Requires all inputs to be closed volumes.
2247
+ * @param meshes - Meshes to union (order does not affect the result)
2248
+ * @param options - Optional safety overrides
2249
+ * @returns New mesh containing the combined volume of all inputs
2250
+ */
2251
+ static unionAll(meshes: Mesh[], options?: MeshBooleanOptions): Mesh {
2252
+ ensureInit();
2253
+ const inputs = meshes
2254
+ .filter((mesh) => mesh.faceCount > 0)
2255
+ .map((mesh) => Mesh.normalizeClosedVolumeOrientation(mesh));
2256
+ if (inputs.length === 0) return Mesh.emptyMesh();
2257
+ if (inputs.length === 1) return Mesh.cloneMesh(inputs[0]);
2258
+
2259
+ const openCount = inputs.reduce((count, mesh) => count + (mesh.isClosedVolume() ? 0 : 1), 0);
2260
+ if (openCount > 0) {
2261
+ throw new Error(
2262
+ `Boolean unionAll requires all inputs to be closed volumes (${openCount} of ${inputs.length} inputs are open). `
2263
+ + "Close or cap the open meshes before calling unionAll().",
2264
+ );
2265
+ }
2266
+
2267
+ Mesh.enforceBatchBooleanLimits("unionAll", inputs, options);
2268
+
2269
+ const flags = inputs.every((mesh) => mesh._trustedBooleanInput) ? "trustedInput" : "";
2270
+ const result = wasm.mesh_boolean_union_all(Mesh.packMeshes(inputs), flags);
2271
+ return Mesh.decodeBatchBooleanResult("unionAll", result);
2272
+ }
2273
+
2274
+ /**
2275
+ * Subtract many cutter meshes from this mesh in ONE WASM call.
2276
+ *
2277
+ * The kernel culls cutters that miss this mesh's bounds, unions mutually
2278
+ * overlapping cutters, combines disjoint cutters into one multi-shell
2279
+ * operand, and performs a single CSG subtraction — instead of N pairwise
2280
+ * subtractions that re-process the shrinking host every time.
2281
+ *
2282
+ * The fast batched path requires this mesh and all cutters to be closed
2283
+ * volumes; open-mesh combinations fall back to sequential pairwise
2284
+ * `subtract()` (which carries the split-based open-host trim semantics).
2285
+ * @param others - Cutter meshes to subtract
2286
+ * @param options - Optional safety overrides
2287
+ * @returns New mesh with all cutter volumes removed from this
2288
+ */
2289
+ subtractAll(others: Mesh[], options?: MeshBooleanOptions): Mesh {
2290
+ ensureInit();
2291
+ if (this.faceCount === 0) return Mesh.emptyMesh();
2292
+ const host = Mesh.normalizeClosedVolumeOrientation(this);
2293
+ const cutters = others
2294
+ .filter((mesh) => mesh.faceCount > 0)
2295
+ .map((mesh) => Mesh.normalizeClosedVolumeOrientation(mesh));
2296
+ if (cutters.length === 0) return Mesh.cloneMesh(host);
2297
+ if (cutters.length === 1) return host.subtract(cutters[0], options);
2298
+
2299
+ if (!host.isClosedVolume() || cutters.some((mesh) => !mesh.isClosedVolume())) {
2300
+ return cutters.reduce((acc: Mesh, cutter) => acc.subtract(cutter, options), host);
2301
+ }
2302
+
2303
+ // Cheap bounds cull before crossing into WASM at all.
2304
+ const hostBounds = Mesh.computeRawBounds(host);
2305
+ const relevant = cutters.filter((cutter) => {
2306
+ const cutterBounds = Mesh.computeRawBounds(cutter);
2307
+ const contactTol = Mesh.booleanContactTolerance(hostBounds, cutterBounds);
2308
+ return Mesh.boundsOverlap(hostBounds, cutterBounds, contactTol);
2309
+ });
2310
+ if (relevant.length === 0) return Mesh.cloneMesh(host);
2311
+
2312
+ Mesh.enforceBatchBooleanLimits("subtractAll", [host, ...relevant], options);
2313
+
2314
+ const flags = host._trustedBooleanInput && relevant.every((mesh) => mesh._trustedBooleanInput)
2315
+ ? "trustedInput"
2316
+ : "";
2317
+ const result = wasm.mesh_boolean_difference_all(Mesh.packMeshes([host, ...relevant]), flags);
2318
+ return Mesh.decodeBatchBooleanResult("subtractAll", result);
2319
+ }
2320
+
2321
+ private static enforceBatchBooleanLimits(
2322
+ operation: string,
2323
+ inputs: Mesh[],
2324
+ options?: MeshBooleanOptions,
2325
+ ): void {
2326
+ if (options?.allowUnsafe) return;
2327
+ const limits = Mesh.resolveBooleanLimits(options?.limits);
2328
+ let combinedInputFaces = 0;
2329
+ for (const mesh of inputs) {
2330
+ if (mesh.faceCount > limits.maxInputFacesPerMesh) {
2331
+ throw new Error(
2332
+ `Boolean ${operation} blocked by safety limits `
2333
+ + `(input faces=${mesh.faceCount} > maxInputFacesPerMesh=${limits.maxInputFacesPerMesh}). `
2334
+ + "Simplify inputs, run in a Worker, or pass allowUnsafe: true to force execution.",
2335
+ );
2336
+ }
2337
+ combinedInputFaces += mesh.faceCount;
2338
+ }
2339
+ if (combinedInputFaces > limits.maxCombinedInputFaces) {
2340
+ throw new Error(
2341
+ `Boolean ${operation} blocked by safety limits `
2342
+ + `(combined faces=${combinedInputFaces} > maxCombinedInputFaces=${limits.maxCombinedInputFaces}). `
2343
+ + "Simplify inputs, run in a Worker, or pass allowUnsafe: true to force execution.",
2344
+ );
2345
+ }
2346
+ }
2347
+
2348
+ private static decodeBatchBooleanResult(operation: string, result: Float64Array): Mesh {
2349
+ if (result.length === 0) {
2350
+ throw new Error(`Boolean ${operation} failed and returned an invalid mesh buffer.`);
2351
+ }
2352
+ const vertexCount = result[0];
2353
+ if (!Number.isFinite(vertexCount) || vertexCount < 0) {
2354
+ throw new Error(`Boolean ${operation} failed and returned a corrupt mesh buffer.`);
2355
+ }
2356
+ if (vertexCount === 0) return Mesh.emptyMesh();
2357
+ return Mesh.fromTrustedBuffer(result);
2358
+ }
2359
+
2238
2360
  private splitWithMesh(other: Mesh, options?: MeshBooleanOptions): MeshSplitResult {
2239
2361
  ensureInit();
2240
2362