okgeometry-api 1.4.0 → 1.6.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/dist/Mesh.d.ts +32 -0
- package/dist/Mesh.d.ts.map +1 -1
- package/dist/Mesh.js +117 -0
- package/dist/Mesh.js.map +1 -1
- package/dist/wasm-base64.d.ts +1 -1
- package/dist/wasm-base64.d.ts.map +1 -1
- package/dist/wasm-base64.js +1 -1
- package/dist/wasm-base64.js.map +1 -1
- package/dist/wasm-bindings.d.ts +40 -0
- package/dist/wasm-bindings.d.ts.map +1 -1
- package/dist/wasm-bindings.js +67 -0
- package/dist/wasm-bindings.js.map +1 -1
- package/package.json +48 -47
- package/src/Mesh.ts +132 -0
- package/src/wasm-base64.ts +1 -1
- package/src/wasm-bindings.d.ts +37 -0
- package/src/wasm-bindings.js +70 -0
package/package.json
CHANGED
|
@@ -1,47 +1,48 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "okgeometry-api",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "Geometry engine API for AEC applications — NURBS, meshes, booleans, intersections. Powered by Rust/WASM.",
|
|
5
|
-
"type": "module",
|
|
6
|
-
"main": "dist/index.js",
|
|
7
|
-
"types": "dist/index.d.ts",
|
|
8
|
-
"exports": {
|
|
9
|
-
".": {
|
|
10
|
-
"types": "./dist/index.d.ts",
|
|
11
|
-
"import": "./dist/index.js"
|
|
12
|
-
}
|
|
13
|
-
},
|
|
14
|
-
"files": [
|
|
15
|
-
"dist/",
|
|
16
|
-
"src/"
|
|
17
|
-
],
|
|
18
|
-
"scripts": {
|
|
19
|
-
"build:wasm": "node -e \"require('fs').rmSync('wasm',{recursive:true,force:true})\" && cd .. && wasm-pack build --target web --out-dir okgeometry-api/wasm",
|
|
20
|
-
"sync:wasm-bindings": "node scripts/sync-wasm-bindings.mjs",
|
|
21
|
-
"inline-wasm": "tsx scripts/inline-wasm.ts",
|
|
22
|
-
"build": "npm run build:wasm && npm run sync:wasm-bindings && npm run inline-wasm && tsc",
|
|
23
|
-
"bench:boolean-heavy": "npm run build && tsx scripts/bench-boolean-heavy.ts",
|
|
24
|
-
"bench:boolean-ab": "npm run build && tsx scripts/bench-boolean-ab.ts",
|
|
25
|
-
"
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
"
|
|
30
|
-
"
|
|
31
|
-
"
|
|
32
|
-
"
|
|
33
|
-
"
|
|
34
|
-
"
|
|
35
|
-
"
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
"
|
|
39
|
-
"
|
|
40
|
-
|
|
41
|
-
"
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
"
|
|
46
|
-
|
|
47
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "okgeometry-api",
|
|
3
|
+
"version": "1.6.0",
|
|
4
|
+
"description": "Geometry engine API for AEC applications — NURBS, meshes, booleans, intersections. Powered by Rust/WASM.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.js"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"dist/",
|
|
16
|
+
"src/"
|
|
17
|
+
],
|
|
18
|
+
"scripts": {
|
|
19
|
+
"build:wasm": "node -e \"require('fs').rmSync('wasm',{recursive:true,force:true})\" && cd .. && wasm-pack build --target web --out-dir okgeometry-api/wasm",
|
|
20
|
+
"sync:wasm-bindings": "node scripts/sync-wasm-bindings.mjs",
|
|
21
|
+
"inline-wasm": "tsx scripts/inline-wasm.ts",
|
|
22
|
+
"build": "npm run build:wasm && npm run sync:wasm-bindings && npm run inline-wasm && tsc",
|
|
23
|
+
"bench:boolean-heavy": "npm run build && tsx scripts/bench-boolean-heavy.ts",
|
|
24
|
+
"bench:boolean-ab": "npm run build && tsx scripts/bench-boolean-ab.ts",
|
|
25
|
+
"bench:boolean-batch": "npm run build && tsx scripts/bench-boolean-batch.ts",
|
|
26
|
+
"prepublishOnly": "npm run build"
|
|
27
|
+
},
|
|
28
|
+
"keywords": [
|
|
29
|
+
"geometry",
|
|
30
|
+
"wasm",
|
|
31
|
+
"nurbs",
|
|
32
|
+
"mesh",
|
|
33
|
+
"boolean",
|
|
34
|
+
"cad",
|
|
35
|
+
"aec",
|
|
36
|
+
"3d"
|
|
37
|
+
],
|
|
38
|
+
"author": "Mostafa El Ayoubi",
|
|
39
|
+
"license": "Proprietary License",
|
|
40
|
+
"repository": {
|
|
41
|
+
"type": "git",
|
|
42
|
+
"url": "https://www.orkestra.online"
|
|
43
|
+
},
|
|
44
|
+
"devDependencies": {
|
|
45
|
+
"tsx": "^4.7.0",
|
|
46
|
+
"typescript": "^5.4.0"
|
|
47
|
+
}
|
|
48
|
+
}
|
package/src/Mesh.ts
CHANGED
|
@@ -2235,6 +2235,138 @@ 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 (cutters.some((mesh) => !mesh.isClosedVolume())) {
|
|
2300
|
+
// Open cutters carry per-pair trim/error semantics — sequential only.
|
|
2301
|
+
return cutters.reduce((acc: Mesh, cutter) => acc.subtract(cutter, options), host);
|
|
2302
|
+
}
|
|
2303
|
+
|
|
2304
|
+
// Cheap bounds cull before crossing into WASM at all.
|
|
2305
|
+
const hostBounds = Mesh.computeRawBounds(host);
|
|
2306
|
+
const relevant = cutters.filter((cutter) => {
|
|
2307
|
+
const cutterBounds = Mesh.computeRawBounds(cutter);
|
|
2308
|
+
const contactTol = Mesh.booleanContactTolerance(hostBounds, cutterBounds);
|
|
2309
|
+
return Mesh.boundsOverlap(hostBounds, cutterBounds, contactTol);
|
|
2310
|
+
});
|
|
2311
|
+
if (relevant.length === 0) return Mesh.cloneMesh(host);
|
|
2312
|
+
|
|
2313
|
+
Mesh.enforceBatchBooleanLimits("subtractAll", [host, ...relevant], options);
|
|
2314
|
+
|
|
2315
|
+
const flags = host._trustedBooleanInput && relevant.every((mesh) => mesh._trustedBooleanInput)
|
|
2316
|
+
? "trustedInput"
|
|
2317
|
+
: "";
|
|
2318
|
+
|
|
2319
|
+
if (!host.isClosedVolume()) {
|
|
2320
|
+
// Open host + closed cutters: batched surface trim. One pass over the
|
|
2321
|
+
// host with all cutters at once — replaces the sequential pairwise
|
|
2322
|
+
// fold, which re-split (and degraded) the growing host once per cutter.
|
|
2323
|
+
const result = wasm.mesh_surface_difference_all(Mesh.packMeshes([host, ...relevant]), flags);
|
|
2324
|
+
return Mesh.decodeBatchBooleanResult("subtractAll", result);
|
|
2325
|
+
}
|
|
2326
|
+
|
|
2327
|
+
const result = wasm.mesh_boolean_difference_all(Mesh.packMeshes([host, ...relevant]), flags);
|
|
2328
|
+
return Mesh.decodeBatchBooleanResult("subtractAll", result);
|
|
2329
|
+
}
|
|
2330
|
+
|
|
2331
|
+
private static enforceBatchBooleanLimits(
|
|
2332
|
+
operation: string,
|
|
2333
|
+
inputs: Mesh[],
|
|
2334
|
+
options?: MeshBooleanOptions,
|
|
2335
|
+
): void {
|
|
2336
|
+
if (options?.allowUnsafe) return;
|
|
2337
|
+
const limits = Mesh.resolveBooleanLimits(options?.limits);
|
|
2338
|
+
let combinedInputFaces = 0;
|
|
2339
|
+
for (const mesh of inputs) {
|
|
2340
|
+
if (mesh.faceCount > limits.maxInputFacesPerMesh) {
|
|
2341
|
+
throw new Error(
|
|
2342
|
+
`Boolean ${operation} blocked by safety limits `
|
|
2343
|
+
+ `(input faces=${mesh.faceCount} > maxInputFacesPerMesh=${limits.maxInputFacesPerMesh}). `
|
|
2344
|
+
+ "Simplify inputs, run in a Worker, or pass allowUnsafe: true to force execution.",
|
|
2345
|
+
);
|
|
2346
|
+
}
|
|
2347
|
+
combinedInputFaces += mesh.faceCount;
|
|
2348
|
+
}
|
|
2349
|
+
if (combinedInputFaces > limits.maxCombinedInputFaces) {
|
|
2350
|
+
throw new Error(
|
|
2351
|
+
`Boolean ${operation} blocked by safety limits `
|
|
2352
|
+
+ `(combined faces=${combinedInputFaces} > maxCombinedInputFaces=${limits.maxCombinedInputFaces}). `
|
|
2353
|
+
+ "Simplify inputs, run in a Worker, or pass allowUnsafe: true to force execution.",
|
|
2354
|
+
);
|
|
2355
|
+
}
|
|
2356
|
+
}
|
|
2357
|
+
|
|
2358
|
+
private static decodeBatchBooleanResult(operation: string, result: Float64Array): Mesh {
|
|
2359
|
+
if (result.length === 0) {
|
|
2360
|
+
throw new Error(`Boolean ${operation} failed and returned an invalid mesh buffer.`);
|
|
2361
|
+
}
|
|
2362
|
+
const vertexCount = result[0];
|
|
2363
|
+
if (!Number.isFinite(vertexCount) || vertexCount < 0) {
|
|
2364
|
+
throw new Error(`Boolean ${operation} failed and returned a corrupt mesh buffer.`);
|
|
2365
|
+
}
|
|
2366
|
+
if (vertexCount === 0) return Mesh.emptyMesh();
|
|
2367
|
+
return Mesh.fromTrustedBuffer(result);
|
|
2368
|
+
}
|
|
2369
|
+
|
|
2238
2370
|
private splitWithMesh(other: Mesh, options?: MeshBooleanOptions): MeshSplitResult {
|
|
2239
2371
|
ensureInit();
|
|
2240
2372
|
|