okgeometry-api 1.20.0 → 1.21.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,59 +1,59 @@
1
- {
2
- "name": "okgeometry-api",
3
- "version": "1.20.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
- "bench:boolean-matrix": "tsx scripts/bench-boolean-matrix.ts",
27
- "smoke:brep-boolean": "tsx scripts/smoke-brep-boolean.ts",
28
- "prepublishOnly": "npm run build",
29
- "smoke:brep-parametric": "tsx scripts/smoke-brep-parametric.ts",
30
- "smoke:brep-from-mesh": "tsx scripts/smoke-brep-from-mesh.ts",
31
- "smoke:brep-faces": "tsx scripts/smoke-brep-faces.ts",
32
- "smoke:curve-intersections": "tsx scripts/smoke-curve-intersections.ts",
33
- "smoke:brep-intersections": "tsx scripts/smoke-brep-intersections.ts",
34
- "smoke:brep-sheet-boolean": "tsx scripts/smoke-brep-sheet-boolean.ts",
35
- "smoke:geometry-boolean": "tsx scripts/smoke-geometry-boolean.ts",
36
- "smoke:curve-region": "tsx scripts/smoke-curve-region.ts",
37
- "smoke:sculpt": "tsx scripts/smoke-sculpt.ts"
38
- },
39
- "keywords": [
40
- "geometry",
41
- "wasm",
42
- "nurbs",
43
- "mesh",
44
- "boolean",
45
- "cad",
46
- "aec",
47
- "3d"
48
- ],
49
- "author": "Mostafa El Ayoubi",
50
- "license": "Proprietary License",
51
- "repository": {
52
- "type": "git",
53
- "url": "https://www.orkestra.online"
54
- },
55
- "devDependencies": {
56
- "tsx": "^4.7.0",
57
- "typescript": "^5.4.0"
58
- }
59
- }
1
+ {
2
+ "name": "okgeometry-api",
3
+ "version": "1.21.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
+ "bench:boolean-matrix": "tsx scripts/bench-boolean-matrix.ts",
27
+ "smoke:brep-boolean": "tsx scripts/smoke-brep-boolean.ts",
28
+ "prepublishOnly": "npm run build",
29
+ "smoke:brep-parametric": "tsx scripts/smoke-brep-parametric.ts",
30
+ "smoke:brep-from-mesh": "tsx scripts/smoke-brep-from-mesh.ts",
31
+ "smoke:brep-faces": "tsx scripts/smoke-brep-faces.ts",
32
+ "smoke:curve-intersections": "tsx scripts/smoke-curve-intersections.ts",
33
+ "smoke:brep-intersections": "tsx scripts/smoke-brep-intersections.ts",
34
+ "smoke:brep-sheet-boolean": "tsx scripts/smoke-brep-sheet-boolean.ts",
35
+ "smoke:geometry-boolean": "tsx scripts/smoke-geometry-boolean.ts",
36
+ "smoke:curve-region": "tsx scripts/smoke-curve-region.ts",
37
+ "smoke:sculpt": "tsx scripts/smoke-sculpt.ts"
38
+ },
39
+ "keywords": [
40
+ "geometry",
41
+ "wasm",
42
+ "nurbs",
43
+ "mesh",
44
+ "boolean",
45
+ "cad",
46
+ "aec",
47
+ "3d"
48
+ ],
49
+ "author": "Mostafa El Ayoubi",
50
+ "license": "Proprietary License",
51
+ "repository": {
52
+ "type": "git",
53
+ "url": "https://www.orkestra.online"
54
+ },
55
+ "devDependencies": {
56
+ "tsx": "^4.7.0",
57
+ "typescript": "^5.4.0"
58
+ }
59
+ }
package/src/Mesh.ts CHANGED
@@ -230,6 +230,92 @@ function shouldFallbackFromWorkerFailure(error: unknown): boolean {
230
230
  return code === "worker_unavailable" || code === "worker_crashed" || code === "worker_protocol";
231
231
  }
232
232
 
233
+ /**
234
+ * Full mesh health report. Counts are on proximity-welded topology, so
235
+ * coincident-but-distinct seam vertices (geometrically watertight) are not
236
+ * mis-reported as open. `isWatertight` = closed 2-manifold with no degenerate
237
+ * or duplicate faces.
238
+ */
239
+ export interface MeshDiagnostics {
240
+ vertexCount: number;
241
+ triangleCount: number;
242
+ weldedVertexCount: number;
243
+ boundaryEdges: number;
244
+ nonManifoldEdges: number;
245
+ boundaryLoops: number;
246
+ degenerateTriangles: number;
247
+ duplicateTriangles: number;
248
+ unreferencedVertices: number;
249
+ connectedComponents: number;
250
+ isClosed: boolean;
251
+ isManifold: boolean;
252
+ signedVolume: number;
253
+ isWatertight: boolean;
254
+ }
255
+
256
+ /** Which geometry-preserving repair steps to run. */
257
+ export interface MeshRepairOptions {
258
+ weld?: boolean;
259
+ removeDegenerate?: boolean;
260
+ removeDuplicate?: boolean;
261
+ fillHoles?: boolean;
262
+ fixWinding?: boolean;
263
+ removeUnreferenced?: boolean;
264
+ /** `<= 0` = auto from bbox diagonal. */
265
+ weldTolerance?: number;
266
+ /** Skip holes whose boundary loop exceeds this many edges (`0` = fill all). */
267
+ maxHoleEdges?: number;
268
+ }
269
+
270
+ /** Result of {@link Mesh.repair}. */
271
+ export interface MeshRepairResult {
272
+ mesh: Mesh;
273
+ before: MeshDiagnostics;
274
+ after: MeshDiagnostics;
275
+ weldedVertices: number;
276
+ removedTriangles: number;
277
+ filledHoles: number;
278
+ }
279
+
280
+ /** Result of {@link Mesh.makeWatertight}. */
281
+ export interface MakeWatertightResult {
282
+ mesh: Mesh;
283
+ before: MeshDiagnostics;
284
+ after: MeshDiagnostics;
285
+ /** `'repair'` = closed by geometry-preserving repair; `'remesh'` = voxel remesh fallback. */
286
+ method: "repair" | "remesh";
287
+ /** Effective voxel resolution when `method === 'remesh'` (0 otherwise). */
288
+ effectiveResolution: number;
289
+ }
290
+
291
+ /** Parse the fixed 13-value diagnostic block at `offset` in a WASM result. */
292
+ function parseMeshDiagnostics(r: ArrayLike<number>, offset: number): MeshDiagnostics {
293
+ const boundaryEdges = Math.round(r[offset + 3] ?? 0);
294
+ const nonManifoldEdges = Math.round(r[offset + 4] ?? 0);
295
+ const degenerateTriangles = Math.round(r[offset + 6] ?? 0);
296
+ const duplicateTriangles = Math.round(r[offset + 7] ?? 0);
297
+ const isClosed = (r[offset + 10] ?? 0) > 0.5;
298
+ return {
299
+ vertexCount: Math.round(r[offset] ?? 0),
300
+ triangleCount: Math.round(r[offset + 1] ?? 0),
301
+ weldedVertexCount: Math.round(r[offset + 2] ?? 0),
302
+ boundaryEdges,
303
+ nonManifoldEdges,
304
+ boundaryLoops: Math.round(r[offset + 5] ?? 0),
305
+ degenerateTriangles,
306
+ duplicateTriangles,
307
+ unreferencedVertices: Math.round(r[offset + 8] ?? 0),
308
+ connectedComponents: Math.round(r[offset + 9] ?? 0),
309
+ isClosed,
310
+ isManifold: (r[offset + 11] ?? 0) > 0.5,
311
+ signedVolume: r[offset + 12] ?? 0,
312
+ isWatertight:
313
+ isClosed && nonManifoldEdges === 0 && degenerateTriangles === 0 && duplicateTriangles === 0,
314
+ };
315
+ }
316
+
317
+ const MESH_DIAG_LEN = 13;
318
+
233
319
  /**
234
320
  * Buffer-backed triangle mesh with GPU-ready accessors.
235
321
  * All geometry lives in a Float64Array from WASM.
@@ -3702,6 +3788,114 @@ export class Mesh {
3702
3788
  };
3703
3789
  }
3704
3790
 
3791
+ /**
3792
+ * Full mesh health report (open/closed, boundary & non-manifold edge counts,
3793
+ * boundary loops, degenerate / duplicate faces, orphan vertices, signed
3794
+ * volume, watertight verdict). Computed on proximity-welded topology.
3795
+ */
3796
+ diagnostics(): MeshDiagnostics {
3797
+ ensureInit();
3798
+ const r = wasm.mesh_repair_diagnostics(this._vertexCount, this._buffer);
3799
+ return parseMeshDiagnostics(r, 0);
3800
+ }
3801
+
3802
+ /**
3803
+ * Flat 3D endpoints of every open boundary edge, six values per segment
3804
+ * (`[x0,y0,z0, x1,y1,z1, ...]`). Empty when the mesh is closed. Drives the
3805
+ * red "open edge" highlight.
3806
+ */
3807
+ boundaryEdgeSegments(): Float64Array {
3808
+ ensureInit();
3809
+ return wasm.mesh_boundary_edge_segments(this._vertexCount, this._buffer);
3810
+ }
3811
+
3812
+ /**
3813
+ * Geometry-preserving repair: weld coincident vertices, drop degenerate /
3814
+ * duplicate triangles, remove orphan vertices, make winding consistent +
3815
+ * outward, and fill holes by triangulating their boundary loops. Returns the
3816
+ * repaired mesh plus before/after diagnostics.
3817
+ */
3818
+ repair(options: MeshRepairOptions = {}): MeshRepairResult {
3819
+ ensureInit();
3820
+ const bit = (on: boolean | undefined, dflt: boolean, mask: number) =>
3821
+ (on ?? dflt) ? mask : 0;
3822
+ const flags =
3823
+ bit(options.weld, true, 1) |
3824
+ bit(options.removeDegenerate, true, 2) |
3825
+ bit(options.removeDuplicate, true, 4) |
3826
+ bit(options.fillHoles, true, 8) |
3827
+ bit(options.fixWinding, true, 16) |
3828
+ bit(options.removeUnreferenced, true, 32);
3829
+ const r = wasm.mesh_repair(
3830
+ this._vertexCount,
3831
+ this._buffer,
3832
+ flags,
3833
+ options.weldTolerance ?? 0,
3834
+ options.maxHoleEdges ?? 0,
3835
+ );
3836
+ const before = parseMeshDiagnostics(r, 0);
3837
+ const after = parseMeshDiagnostics(r, MESH_DIAG_LEN);
3838
+ const base = MESH_DIAG_LEN * 2;
3839
+ const weldedVertices = Math.round(r[base] ?? 0);
3840
+ const removedTriangles = Math.round(r[base + 1] ?? 0);
3841
+ const filledHoles = Math.round(r[base + 2] ?? 0);
3842
+ const meshBuffer = (r as Float64Array).slice(base + 3);
3843
+ return {
3844
+ mesh: Mesh.fromTrustedBuffer(meshBuffer),
3845
+ before,
3846
+ after,
3847
+ weldedVertices,
3848
+ removedTriangles,
3849
+ filledHoles,
3850
+ };
3851
+ }
3852
+
3853
+ /**
3854
+ * Rebuild as a guaranteed-watertight solid via generalized-winding-number
3855
+ * voxel remeshing. `resolution` = voxels along the longest axis (`0` = 64
3856
+ * default, auto-capped by a work budget); `iso` = winding threshold
3857
+ * (`<=0`/`>=1` → 0.5). Always closes arbitrary open / non-manifold /
3858
+ * self-intersecting soup, at the cost of fine detail.
3859
+ */
3860
+ voxelRemesh(resolution = 0, iso = 0.5): Mesh {
3861
+ ensureInit();
3862
+ const r = wasm.mesh_voxel_remesh(
3863
+ this._vertexCount,
3864
+ this._buffer,
3865
+ Math.max(0, Math.floor(resolution)),
3866
+ iso,
3867
+ );
3868
+ return Mesh.fromTrustedBuffer(r as Float64Array);
3869
+ }
3870
+
3871
+ /**
3872
+ * Layered "make watertight": first geometry-preserving {@link repair}; if
3873
+ * that closes the mesh it is returned (`method: 'repair'`). Otherwise fall
3874
+ * back to {@link voxelRemesh} (`method: 'remesh'`). Always returns a solid.
3875
+ */
3876
+ makeWatertight(options: { resolution?: number; iso?: number } = {}): MakeWatertightResult {
3877
+ ensureInit();
3878
+ const r = wasm.mesh_make_watertight(
3879
+ this._vertexCount,
3880
+ this._buffer,
3881
+ Math.max(0, Math.floor(options.resolution ?? 0)),
3882
+ options.iso ?? 0.5,
3883
+ );
3884
+ const before = parseMeshDiagnostics(r, 0);
3885
+ const after = parseMeshDiagnostics(r, MESH_DIAG_LEN);
3886
+ const base = MESH_DIAG_LEN * 2;
3887
+ const method = (r[base] ?? 0) < 0.5 ? "repair" : "remesh";
3888
+ const effectiveResolution = Math.round(r[base + 1] ?? 0);
3889
+ const meshBuffer = (r as Float64Array).slice(base + 2);
3890
+ return {
3891
+ mesh: Mesh.fromTrustedBuffer(meshBuffer),
3892
+ before,
3893
+ after,
3894
+ method,
3895
+ effectiveResolution,
3896
+ };
3897
+ }
3898
+
3705
3899
  /**
3706
3900
  * Get a logical planar face by its planar-face index.
3707
3901
  */
package/src/index.ts CHANGED
@@ -90,6 +90,10 @@ export type {
90
90
  MeshBooleanErrorCode,
91
91
  MeshBooleanErrorPayload,
92
92
  MeshBooleanProgressEvent,
93
+ MeshDiagnostics,
94
+ MeshRepairOptions,
95
+ MeshRepairResult,
96
+ MakeWatertightResult,
93
97
  } from "./Mesh.js";
94
98
  export { intersect } from "./Geometry.js";
95
99