okgeometry-api 1.1.1 → 1.1.2

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,6 +1,6 @@
1
1
  {
2
2
  "name": "okgeometry-api",
3
- "version": "1.1.1",
3
+ "version": "1.1.2",
4
4
  "description": "Geometry engine API for AEC applications — NURBS, meshes, booleans, intersections. Powered by Rust/WASM.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
package/src/Mesh.ts CHANGED
@@ -47,23 +47,77 @@ export interface PlanarCircle {
47
47
  radius: number;
48
48
  }
49
49
 
50
- export interface PlanarArc {
51
- points: Point[];
52
- center: Point;
53
- radius: number;
54
- startAngle: number;
55
- endAngle: number;
56
- sweepAngle: number;
57
- }
58
-
59
- interface RawMeshBounds {
60
- minX: number;
61
- minY: number;
62
- minZ: number;
63
- maxX: number;
64
- maxY: number;
65
- maxZ: number;
66
- }
50
+ export interface PlanarArc {
51
+ points: Point[];
52
+ center: Point;
53
+ radius: number;
54
+ startAngle: number;
55
+ endAngle: number;
56
+ sweepAngle: number;
57
+ }
58
+
59
+ interface RawMeshBounds {
60
+ minX: number;
61
+ minY: number;
62
+ minZ: number;
63
+ maxX: number;
64
+ maxY: number;
65
+ maxZ: number;
66
+ }
67
+
68
+ export type MeshBooleanOperation = "union" | "subtraction" | "intersection";
69
+
70
+ export interface MeshDebugBounds {
71
+ min: Point;
72
+ max: Point;
73
+ center: Point;
74
+ size: Vec3;
75
+ diagonal: number;
76
+ }
77
+
78
+ export interface MeshDebugSummary {
79
+ vertexCount: number;
80
+ faceCount: number;
81
+ trustedBooleanInput: boolean;
82
+ isClosedVolume: boolean;
83
+ topology: {
84
+ boundaryEdges: number;
85
+ nonManifoldEdges: number;
86
+ };
87
+ bounds: MeshDebugBounds;
88
+ }
89
+
90
+ export interface MeshDebugRayHit {
91
+ point: Point;
92
+ normal: Vec3;
93
+ faceIndex: number;
94
+ distance: number;
95
+ }
96
+
97
+ export interface MeshBooleanDebugProbe {
98
+ label: string;
99
+ origin: Point;
100
+ direction: Vec3;
101
+ maxDistance: number;
102
+ inputAHits: MeshDebugRayHit[];
103
+ inputBHits: MeshDebugRayHit[];
104
+ resultHits: MeshDebugRayHit[];
105
+ }
106
+
107
+ export interface MeshBooleanDebugOptions {
108
+ rayPaddingScale?: number;
109
+ maxRayHits?: number;
110
+ probes?: Array<"posX" | "negX" | "posY" | "negY" | "posZ" | "negZ">;
111
+ }
112
+
113
+ export interface MeshBooleanDebugReport {
114
+ operation: MeshBooleanOperation;
115
+ inputA: MeshDebugSummary;
116
+ inputB: MeshDebugSummary;
117
+ result: Mesh;
118
+ resultSummary: MeshDebugSummary;
119
+ probes: MeshBooleanDebugProbe[];
120
+ }
67
121
 
68
122
  /**
69
123
  * Buffer-backed triangle mesh with GPU-ready accessors.
@@ -71,10 +125,13 @@ interface RawMeshBounds {
71
125
  *
72
126
  * Buffer format: [vertexCount, x1,y1,z1, ..., i1,i2,i3, ...]
73
127
  */
74
- export class Mesh {
75
- private _buffer: Float64Array;
76
- private _vertexCount: number;
77
- private _trustedBooleanInput: boolean;
128
+ export class Mesh {
129
+ private _buffer: Float64Array;
130
+ private _vertexCount: number;
131
+ private _trustedBooleanInput: boolean;
132
+ private static readonly DEFAULT_BOOLEAN_DEBUG_PROBES: Array<
133
+ "posX" | "negX" | "posY" | "negY" | "posZ" | "negZ"
134
+ > = ["posX", "negX", "posY", "negY", "posZ", "negZ"];
78
135
 
79
136
  private static readonly DEFAULT_BOOLEAN_LIMITS: MeshBooleanLimits = {
80
137
  maxInputFacesPerMesh: 120_000,
@@ -177,10 +234,191 @@ export class Mesh {
177
234
  return Number.isFinite(diag) && diag > 0 ? diag : 1;
178
235
  }
179
236
 
180
- private static booleanContactTolerance(a: RawMeshBounds | null, b: RawMeshBounds | null): number {
181
- const scale = Math.max(Mesh.boundsDiag(a), Mesh.boundsDiag(b));
182
- return Math.min(1e-4, Math.max(1e-9, scale * 1e-6));
183
- }
237
+ private static booleanContactTolerance(a: RawMeshBounds | null, b: RawMeshBounds | null): number {
238
+ const scale = Math.max(Mesh.boundsDiag(a), Mesh.boundsDiag(b));
239
+ return Math.min(1e-4, Math.max(1e-9, scale * 1e-6));
240
+ }
241
+
242
+ private static toDebugBounds(bounds: { min: Point; max: Point }): MeshDebugBounds {
243
+ const size = new Vec3(
244
+ bounds.max.x - bounds.min.x,
245
+ bounds.max.y - bounds.min.y,
246
+ bounds.max.z - bounds.min.z,
247
+ );
248
+ return {
249
+ min: bounds.min,
250
+ max: bounds.max,
251
+ center: new Point(
252
+ (bounds.min.x + bounds.max.x) * 0.5,
253
+ (bounds.min.y + bounds.max.y) * 0.5,
254
+ (bounds.min.z + bounds.max.z) * 0.5,
255
+ ),
256
+ size,
257
+ diagonal: Math.hypot(size.x, size.y, size.z),
258
+ };
259
+ }
260
+
261
+ private static clampDebugHitCount(count?: number): number {
262
+ if (!Number.isFinite(count)) return 4;
263
+ return Math.max(1, Math.min(32, Math.floor(count ?? 4)));
264
+ }
265
+
266
+ private static collectDebugHits(
267
+ mesh: Mesh,
268
+ origin: Point,
269
+ direction: Vec3,
270
+ maxDistance: number,
271
+ maxHits: number,
272
+ ): MeshDebugRayHit[] {
273
+ return mesh.raycastAll(origin, direction, maxDistance).slice(0, maxHits).map((hit) => ({
274
+ point: hit.point,
275
+ normal: hit.normal,
276
+ faceIndex: hit.faceIndex,
277
+ distance: hit.distance,
278
+ }));
279
+ }
280
+
281
+ private static buildBooleanDebugProbes(
282
+ inputA: Mesh,
283
+ inputB: Mesh,
284
+ result: Mesh,
285
+ options?: MeshBooleanDebugOptions,
286
+ ): MeshBooleanDebugProbe[] {
287
+ const boundsA = Mesh.toDebugBounds(inputA.getBounds());
288
+ const boundsB = Mesh.toDebugBounds(inputB.getBounds());
289
+ const resultBounds = Mesh.toDebugBounds(result.getBounds());
290
+ const min = new Point(
291
+ Math.min(boundsA.min.x, boundsB.min.x, resultBounds.min.x),
292
+ Math.min(boundsA.min.y, boundsB.min.y, resultBounds.min.y),
293
+ Math.min(boundsA.min.z, boundsB.min.z, resultBounds.min.z),
294
+ );
295
+ const max = new Point(
296
+ Math.max(boundsA.max.x, boundsB.max.x, resultBounds.max.x),
297
+ Math.max(boundsA.max.y, boundsB.max.y, resultBounds.max.y),
298
+ Math.max(boundsA.max.z, boundsB.max.z, resultBounds.max.z),
299
+ );
300
+ const center = new Point(
301
+ (min.x + max.x) * 0.5,
302
+ (min.y + max.y) * 0.5,
303
+ (min.z + max.z) * 0.5,
304
+ );
305
+ const diag = Math.max(
306
+ 1e-6,
307
+ Math.hypot(max.x - min.x, max.y - min.y, max.z - min.z),
308
+ );
309
+ const padScale = Number.isFinite(options?.rayPaddingScale)
310
+ ? Math.max(0.01, options?.rayPaddingScale ?? 0.25)
311
+ : 0.25;
312
+ const pad = Math.max(1e-3, diag * padScale);
313
+ const maxHits = Mesh.clampDebugHitCount(options?.maxRayHits);
314
+ const probeIds = options?.probes?.length
315
+ ? options.probes
316
+ : Mesh.DEFAULT_BOOLEAN_DEBUG_PROBES;
317
+ const probeSpecs: Record<
318
+ "posX" | "negX" | "posY" | "negY" | "posZ" | "negZ",
319
+ { origin: Point; direction: Vec3 }
320
+ > = {
321
+ posX: {
322
+ origin: new Point(max.x + pad, center.y, center.z),
323
+ direction: new Vec3(-1, 0, 0),
324
+ },
325
+ negX: {
326
+ origin: new Point(min.x - pad, center.y, center.z),
327
+ direction: new Vec3(1, 0, 0),
328
+ },
329
+ posY: {
330
+ origin: new Point(center.x, max.y + pad, center.z),
331
+ direction: new Vec3(0, -1, 0),
332
+ },
333
+ negY: {
334
+ origin: new Point(center.x, min.y - pad, center.z),
335
+ direction: new Vec3(0, 1, 0),
336
+ },
337
+ posZ: {
338
+ origin: new Point(center.x, center.y, max.z + pad),
339
+ direction: new Vec3(0, 0, -1),
340
+ },
341
+ negZ: {
342
+ origin: new Point(center.x, center.y, min.z - pad),
343
+ direction: new Vec3(0, 0, 1),
344
+ },
345
+ };
346
+ const maxDistance = diag + pad * 2;
347
+
348
+ return probeIds.map((id) => {
349
+ const spec = probeSpecs[id];
350
+ return {
351
+ label: id,
352
+ origin: spec.origin,
353
+ direction: spec.direction,
354
+ maxDistance,
355
+ inputAHits: Mesh.collectDebugHits(inputA, spec.origin, spec.direction, maxDistance, maxHits),
356
+ inputBHits: Mesh.collectDebugHits(inputB, spec.origin, spec.direction, maxDistance, maxHits),
357
+ resultHits: Mesh.collectDebugHits(result, spec.origin, spec.direction, maxDistance, maxHits),
358
+ };
359
+ });
360
+ }
361
+
362
+ private static createBooleanDebugReport(
363
+ inputA: Mesh,
364
+ inputB: Mesh,
365
+ result: Mesh,
366
+ operation: MeshBooleanOperation,
367
+ debugOptions?: MeshBooleanDebugOptions,
368
+ ): MeshBooleanDebugReport {
369
+ return {
370
+ operation,
371
+ inputA: inputA.debugSummary(),
372
+ inputB: inputB.debugSummary(),
373
+ result,
374
+ resultSummary: result.debugSummary(),
375
+ probes: Mesh.buildBooleanDebugProbes(inputA, inputB, result, debugOptions),
376
+ };
377
+ }
378
+
379
+ private static logSubtractDebugSuccess(
380
+ inputA: Mesh,
381
+ inputB: Mesh,
382
+ result: Mesh,
383
+ options?: MeshBooleanOptions,
384
+ ): void {
385
+ try {
386
+ const report = Mesh.createBooleanDebugReport(
387
+ inputA,
388
+ inputB,
389
+ result,
390
+ "subtraction",
391
+ { maxRayHits: 2, probes: ["posX", "negX", "posY"] },
392
+ );
393
+ console.log("[okgeometry-api] Mesh.subtract debug", {
394
+ options: options ?? null,
395
+ inputA: report.inputA,
396
+ inputB: report.inputB,
397
+ result: report.resultSummary,
398
+ probes: report.probes,
399
+ });
400
+ } catch (debugError) {
401
+ console.error("[okgeometry-api] Mesh.subtract debug logging failed", debugError);
402
+ }
403
+ }
404
+
405
+ private static logSubtractDebugFailure(
406
+ inputA: Mesh,
407
+ inputB: Mesh,
408
+ options: MeshBooleanOptions | undefined,
409
+ error: unknown,
410
+ ): void {
411
+ try {
412
+ console.error("[okgeometry-api] Mesh.subtract failed", {
413
+ options: options ?? null,
414
+ inputA: inputA.debugSummary(),
415
+ inputB: inputB.debugSummary(),
416
+ error,
417
+ });
418
+ } catch (debugError) {
419
+ console.error("[okgeometry-api] Mesh.subtract failure logging failed", debugError);
420
+ }
421
+ }
184
422
 
185
423
  private static cloneMesh(mesh: Mesh): Mesh {
186
424
  return Mesh.fromBuffer(new Float64Array(mesh._buffer), {
@@ -295,9 +533,22 @@ export class Mesh {
295
533
  return new Mesh(buffer, true);
296
534
  }
297
535
 
298
- get trustedBooleanInput(): boolean {
299
- return this._trustedBooleanInput;
300
- }
536
+ get trustedBooleanInput(): boolean {
537
+ return this._trustedBooleanInput;
538
+ }
539
+
540
+ debugSummary(): MeshDebugSummary {
541
+ const bounds = Mesh.toDebugBounds(this.getBounds());
542
+ const topology = this.topologyMetrics();
543
+ return {
544
+ vertexCount: this.vertexCount,
545
+ faceCount: this.faceCount,
546
+ trustedBooleanInput: this._trustedBooleanInput,
547
+ isClosedVolume: this.isClosedVolume(),
548
+ topology,
549
+ bounds,
550
+ };
551
+ }
301
552
 
302
553
  /**
303
554
  * Build an axis-aligned rectangle on a plane basis from opposite corners.
@@ -1054,12 +1305,12 @@ export class Mesh {
1054
1305
  return Mesh.fromTrustedBuffer(result);
1055
1306
  }
1056
1307
 
1057
- static encodeBooleanOperationToken(
1058
- operation: "union" | "subtraction" | "intersection",
1059
- a: Mesh,
1060
- b: Mesh,
1061
- options?: MeshBooleanOptions,
1062
- ): string {
1308
+ static encodeBooleanOperationToken(
1309
+ operation: MeshBooleanOperation,
1310
+ a: Mesh,
1311
+ b: Mesh,
1312
+ options?: MeshBooleanOptions,
1313
+ ): string {
1063
1314
  const tokens: string[] = [operation];
1064
1315
  if (a._trustedBooleanInput && b._trustedBooleanInput) {
1065
1316
  tokens.push("trustedInput");
@@ -1101,22 +1352,29 @@ export class Mesh {
1101
1352
  * @param options - Optional safety overrides
1102
1353
  * @returns New mesh with other's volume removed from this
1103
1354
  */
1104
- subtract(other: Mesh, options?: MeshBooleanOptions): Mesh {
1105
- ensureInit();
1106
- const operationToken = Mesh.encodeBooleanOperationToken("subtraction", this, other, options);
1107
- return this.runBoolean(
1108
- other,
1109
- "subtraction",
1110
- () => wasm.mesh_boolean_operation(
1111
- this._vertexCount,
1112
- this._buffer,
1113
- other._vertexCount,
1114
- other._buffer,
1115
- operationToken,
1116
- ),
1117
- options,
1118
- );
1119
- }
1355
+ subtract(other: Mesh, options?: MeshBooleanOptions): Mesh {
1356
+ ensureInit();
1357
+ const operationToken = Mesh.encodeBooleanOperationToken("subtraction", this, other, options);
1358
+ try {
1359
+ const result = this.runBoolean(
1360
+ other,
1361
+ "subtraction",
1362
+ () => wasm.mesh_boolean_operation(
1363
+ this._vertexCount,
1364
+ this._buffer,
1365
+ other._vertexCount,
1366
+ other._buffer,
1367
+ operationToken,
1368
+ ),
1369
+ options,
1370
+ );
1371
+ Mesh.logSubtractDebugSuccess(this, other, result, options);
1372
+ return result;
1373
+ } catch (error) {
1374
+ Mesh.logSubtractDebugFailure(this, other, options, error);
1375
+ throw error;
1376
+ }
1377
+ }
1120
1378
 
1121
1379
  /**
1122
1380
  * Compute boolean intersection with another mesh.
@@ -1124,9 +1382,9 @@ export class Mesh {
1124
1382
  * @param options - Optional safety overrides
1125
1383
  * @returns New mesh containing only the overlapping volume
1126
1384
  */
1127
- intersect(other: Mesh, options?: MeshBooleanOptions): Mesh {
1128
- ensureInit();
1129
- const operationToken = Mesh.encodeBooleanOperationToken("intersection", this, other, options);
1385
+ intersect(other: Mesh, options?: MeshBooleanOptions): Mesh {
1386
+ ensureInit();
1387
+ const operationToken = Mesh.encodeBooleanOperationToken("intersection", this, other, options);
1130
1388
  return this.runBoolean(
1131
1389
  other,
1132
1390
  "intersection",
@@ -1137,9 +1395,48 @@ export class Mesh {
1137
1395
  other._buffer,
1138
1396
  operationToken,
1139
1397
  ),
1140
- options,
1141
- );
1142
- }
1398
+ options,
1399
+ );
1400
+ }
1401
+
1402
+ debugBoolean(
1403
+ other: Mesh,
1404
+ operation: MeshBooleanOperation,
1405
+ options?: MeshBooleanOptions,
1406
+ debugOptions?: MeshBooleanDebugOptions,
1407
+ ): MeshBooleanDebugReport {
1408
+ const result = operation === "union"
1409
+ ? this.union(other, options)
1410
+ : operation === "subtraction"
1411
+ ? this.subtract(other, options)
1412
+ : this.intersect(other, options);
1413
+
1414
+ return Mesh.createBooleanDebugReport(this, other, result, operation, debugOptions);
1415
+ }
1416
+
1417
+ debugUnion(
1418
+ other: Mesh,
1419
+ options?: MeshBooleanOptions,
1420
+ debugOptions?: MeshBooleanDebugOptions,
1421
+ ): MeshBooleanDebugReport {
1422
+ return this.debugBoolean(other, "union", options, debugOptions);
1423
+ }
1424
+
1425
+ debugSubtract(
1426
+ other: Mesh,
1427
+ options?: MeshBooleanOptions,
1428
+ debugOptions?: MeshBooleanDebugOptions,
1429
+ ): MeshBooleanDebugReport {
1430
+ return this.debugBoolean(other, "subtraction", options, debugOptions);
1431
+ }
1432
+
1433
+ debugIntersect(
1434
+ other: Mesh,
1435
+ options?: MeshBooleanOptions,
1436
+ debugOptions?: MeshBooleanDebugOptions,
1437
+ ): MeshBooleanDebugReport {
1438
+ return this.debugBoolean(other, "intersection", options, debugOptions);
1439
+ }
1143
1440
 
1144
1441
  /**
1145
1442
  * Compute boolean union in a dedicated Web Worker (non-blocking).
package/src/index.ts CHANGED
@@ -35,6 +35,13 @@ export { NurbsSurface } from "./NurbsSurface.js";
35
35
  // Mesh and operations
36
36
  export { Mesh, MeshBooleanExecutionError } from "./Mesh.js";
37
37
  export type {
38
+ MeshBooleanOperation,
39
+ MeshBooleanDebugOptions,
40
+ MeshBooleanDebugProbe,
41
+ MeshBooleanDebugReport,
42
+ MeshDebugBounds,
43
+ MeshDebugRayHit,
44
+ MeshDebugSummary,
38
45
  MeshBooleanLimits,
39
46
  MeshBooleanOptions,
40
47
  MeshBooleanAsyncOptions,