okgeometry-api 1.0.0 → 1.1.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.
@@ -1 +1 @@
1
- {"version":3,"file":"wasm-base64.js","sourceRoot":"","sources":["../src/wasm-base64.ts"],"names":[],"mappings":"AAAA,+BAA+B;AAC/B,MAAM,CAAC,MAAM,QAAQ,GAAG,028yDAA028yD,CAAC"}
1
+ {"version":3,"file":"wasm-base64.js","sourceRoot":"","sources":["../src/wasm-base64.ts"],"names":[],"mappings":"AAAA,+BAA+B;AAC/B,MAAM,CAAC,MAAM,QAAQ,GAAG,sz8tDAAsz8tD,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "okgeometry-api",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
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
@@ -18,20 +18,20 @@ import {
18
18
  runMeshBooleanInWorkerPool,
19
19
  } from "./mesh-boolean.pool.js";
20
20
  import { MeshBooleanExecutionError } from "./mesh-boolean.protocol.js";
21
- import type {
22
- MeshBooleanAsyncOptions,
23
- MeshBooleanLimits,
24
- MeshBooleanOptions,
25
- } from "./mesh-boolean.protocol.js";
26
- import * as wasm from "../wasm/okgeometrycore_bg.js";
27
- import { mesh_topology_metrics } from "../wasm/okgeometrycore.js";
21
+ import type {
22
+ MeshBooleanAsyncOptions,
23
+ MeshBooleanLimits,
24
+ MeshBooleanOptions,
25
+ } from "./mesh-boolean.protocol.js";
26
+ import * as wasm from "../wasm/okgeometrycore_bg.js";
27
+ import { mesh_topology_metrics } from "../wasm/okgeometrycore.js";
28
28
 
29
29
  export { MeshBooleanExecutionError };
30
- export type {
31
- MeshBooleanAsyncOptions,
32
- MeshBooleanLimits,
33
- MeshBooleanOptions,
34
- MeshBooleanErrorCode,
30
+ export type {
31
+ MeshBooleanAsyncOptions,
32
+ MeshBooleanLimits,
33
+ MeshBooleanOptions,
34
+ MeshBooleanErrorCode,
35
35
  MeshBooleanErrorPayload,
36
36
  MeshBooleanProgressEvent,
37
37
  } from "./mesh-boolean.protocol.js";
@@ -71,33 +71,33 @@ interface RawMeshBounds {
71
71
  *
72
72
  * Buffer format: [vertexCount, x1,y1,z1, ..., i1,i2,i3, ...]
73
73
  */
74
- export class Mesh {
75
- private _buffer: Float64Array;
76
- private _vertexCount: number;
77
- private _trustedBooleanInput: boolean;
78
-
79
- private static readonly DEFAULT_BOOLEAN_LIMITS: MeshBooleanLimits = {
80
- maxInputFacesPerMesh: 120_000,
81
- maxCombinedInputFaces: 180_000,
82
- maxFaceProduct: 500_000_000,
83
- };
84
- private static readonly TOPOLOGY_METRICS_CACHE_FACE_THRESHOLD = 20_000;
85
-
86
- // Lazy caches
87
- private _positionBuffer: Float32Array | null = null;
88
- private _indexBuffer: Uint32Array | null = null;
89
- private _vertices: Point[] | null = null;
90
- private _faces: number[][] | null = null;
91
- private _edgeVertexPairs: Array<[number, number]> | null = null;
92
- private _topologyMetricsCache: { boundaryEdges: number; nonManifoldEdges: number } | null = null;
93
- private _isClosedVolumeCache: boolean | null = null;
94
- private _rawBoundsCache: RawMeshBounds | null | undefined = undefined;
95
-
96
- private constructor(buffer: Float64Array, trustedBooleanInput = false) {
97
- this._buffer = buffer;
98
- this._vertexCount = buffer.length > 0 ? buffer[0] : 0;
99
- this._trustedBooleanInput = trustedBooleanInput;
100
- }
74
+ export class Mesh {
75
+ private _buffer: Float64Array;
76
+ private _vertexCount: number;
77
+ private _trustedBooleanInput: boolean;
78
+
79
+ private static readonly DEFAULT_BOOLEAN_LIMITS: MeshBooleanLimits = {
80
+ maxInputFacesPerMesh: 120_000,
81
+ maxCombinedInputFaces: 180_000,
82
+ maxFaceProduct: 500_000_000,
83
+ };
84
+ private static readonly TOPOLOGY_METRICS_CACHE_FACE_THRESHOLD = 20_000;
85
+
86
+ // Lazy caches
87
+ private _positionBuffer: Float32Array | null = null;
88
+ private _indexBuffer: Uint32Array | null = null;
89
+ private _vertices: Point[] | null = null;
90
+ private _faces: number[][] | null = null;
91
+ private _edgeVertexPairs: Array<[number, number]> | null = null;
92
+ private _topologyMetricsCache: { boundaryEdges: number; nonManifoldEdges: number } | null = null;
93
+ private _isClosedVolumeCache: boolean | null = null;
94
+ private _rawBoundsCache: RawMeshBounds | null | undefined = undefined;
95
+
96
+ private constructor(buffer: Float64Array, trustedBooleanInput = false) {
97
+ this._buffer = buffer;
98
+ this._vertexCount = buffer.length > 0 ? buffer[0] : 0;
99
+ this._trustedBooleanInput = trustedBooleanInput;
100
+ }
101
101
 
102
102
  /**
103
103
  * Configure default size for the shared boolean worker pool.
@@ -124,14 +124,14 @@ export class Mesh {
124
124
  };
125
125
  }
126
126
 
127
- private static computeRawBounds(mesh: Mesh): RawMeshBounds | null {
128
- if (mesh._rawBoundsCache !== undefined) return mesh._rawBoundsCache;
129
- if (mesh._vertexCount <= 0) {
130
- mesh._rawBoundsCache = null;
131
- return null;
132
- }
133
-
134
- const data = mesh._buffer;
127
+ private static computeRawBounds(mesh: Mesh): RawMeshBounds | null {
128
+ if (mesh._rawBoundsCache !== undefined) return mesh._rawBoundsCache;
129
+ if (mesh._vertexCount <= 0) {
130
+ mesh._rawBoundsCache = null;
131
+ return null;
132
+ }
133
+
134
+ const data = mesh._buffer;
135
135
  const limit = 1 + mesh._vertexCount * 3;
136
136
 
137
137
  let minX = data[1];
@@ -153,44 +153,44 @@ export class Mesh {
153
153
  if (z > maxZ) maxZ = z;
154
154
  }
155
155
 
156
- const bounds = { minX, minY, minZ, maxX, maxY, maxZ };
157
- mesh._rawBoundsCache = bounds;
158
- return bounds;
159
- }
160
-
161
- private static boundsOverlap(a: RawMeshBounds | null, b: RawMeshBounds | null, eps = 1e-9): boolean {
162
- if (!a || !b) return false;
163
- return a.minX <= b.maxX + eps
164
- && a.maxX + eps >= b.minX
165
- && a.minY <= b.maxY + eps
166
- && a.maxY + eps >= b.minY
167
- && a.minZ <= b.maxZ + eps
168
- && a.maxZ + eps >= b.minZ;
169
- }
170
-
171
- private static boundsDiag(bounds: RawMeshBounds | null): number {
172
- if (!bounds) return 1;
173
- const dx = bounds.maxX - bounds.minX;
174
- const dy = bounds.maxY - bounds.minY;
175
- const dz = bounds.maxZ - bounds.minZ;
176
- const diag = Math.hypot(dx, dy, dz);
177
- return Number.isFinite(diag) && diag > 0 ? diag : 1;
178
- }
179
-
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
- }
184
-
185
- private static cloneMesh(mesh: Mesh): Mesh {
186
- return Mesh.fromBuffer(new Float64Array(mesh._buffer), {
187
- trustedBooleanInput: mesh._trustedBooleanInput,
188
- });
189
- }
190
-
191
- private static emptyMesh(): Mesh {
192
- return Mesh.fromTrustedBuffer(new Float64Array(0));
193
- }
156
+ const bounds = { minX, minY, minZ, maxX, maxY, maxZ };
157
+ mesh._rawBoundsCache = bounds;
158
+ return bounds;
159
+ }
160
+
161
+ private static boundsOverlap(a: RawMeshBounds | null, b: RawMeshBounds | null, eps = 1e-9): boolean {
162
+ if (!a || !b) return false;
163
+ return a.minX <= b.maxX + eps
164
+ && a.maxX + eps >= b.minX
165
+ && a.minY <= b.maxY + eps
166
+ && a.maxY + eps >= b.minY
167
+ && a.minZ <= b.maxZ + eps
168
+ && a.maxZ + eps >= b.minZ;
169
+ }
170
+
171
+ private static boundsDiag(bounds: RawMeshBounds | null): number {
172
+ if (!bounds) return 1;
173
+ const dx = bounds.maxX - bounds.minX;
174
+ const dy = bounds.maxY - bounds.minY;
175
+ const dz = bounds.maxZ - bounds.minZ;
176
+ const diag = Math.hypot(dx, dy, dz);
177
+ return Number.isFinite(diag) && diag > 0 ? diag : 1;
178
+ }
179
+
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
+ }
184
+
185
+ private static cloneMesh(mesh: Mesh): Mesh {
186
+ return Mesh.fromBuffer(new Float64Array(mesh._buffer), {
187
+ trustedBooleanInput: mesh._trustedBooleanInput,
188
+ });
189
+ }
190
+
191
+ private static emptyMesh(): Mesh {
192
+ return Mesh.fromTrustedBuffer(new Float64Array(0));
193
+ }
194
194
 
195
195
  // ── GPU-ready buffers ──────────────────────────────────────────
196
196
 
@@ -230,12 +230,12 @@ export class Mesh {
230
230
  return this._vertexCount;
231
231
  }
232
232
 
233
- /** Number of triangular faces in this mesh */
234
- get faceCount(): number {
235
- const start = 1 + this._vertexCount * 3;
236
- if (this._buffer.length <= start) return 0;
237
- return Math.floor((this._buffer.length - start) / 3);
238
- }
233
+ /** Number of triangular faces in this mesh */
234
+ get faceCount(): number {
235
+ const start = 1 + this._vertexCount * 3;
236
+ if (this._buffer.length <= start) return 0;
237
+ return Math.floor((this._buffer.length - start) / 3);
238
+ }
239
239
 
240
240
  // ── High-level accessors (lazy) ────────────────────────────────
241
241
 
@@ -284,20 +284,20 @@ export class Mesh {
284
284
  * @param buffer - Float64Array in mesh buffer format
285
285
  * @returns New Mesh instance
286
286
  */
287
- static fromBuffer(
288
- buffer: Float64Array,
289
- options?: { trustedBooleanInput?: boolean },
290
- ): Mesh {
291
- return new Mesh(buffer, options?.trustedBooleanInput ?? false);
292
- }
293
-
294
- private static fromTrustedBuffer(buffer: Float64Array): Mesh {
295
- return new Mesh(buffer, true);
296
- }
297
-
298
- get trustedBooleanInput(): boolean {
299
- return this._trustedBooleanInput;
300
- }
287
+ static fromBuffer(
288
+ buffer: Float64Array,
289
+ options?: { trustedBooleanInput?: boolean },
290
+ ): Mesh {
291
+ return new Mesh(buffer, options?.trustedBooleanInput ?? false);
292
+ }
293
+
294
+ private static fromTrustedBuffer(buffer: Float64Array): Mesh {
295
+ return new Mesh(buffer, true);
296
+ }
297
+
298
+ get trustedBooleanInput(): boolean {
299
+ return this._trustedBooleanInput;
300
+ }
301
301
 
302
302
  /**
303
303
  * Build an axis-aligned rectangle on a plane basis from opposite corners.
@@ -580,11 +580,11 @@ export class Mesh {
580
580
  * @param pts - Ordered boundary points defining a closed polygon (minimum 3)
581
581
  * @returns New Mesh triangulating the polygon interior
582
582
  */
583
- static patchFromPoints(pts: Point[]): Mesh {
584
- ensureInit();
585
- const coords = pointsToCoords(pts);
586
- const buf = wasm.mesh_patch_from_points(coords);
587
- return Mesh.fromTrustedBuffer(buf);
583
+ static patchFromPoints(pts: Point[]): Mesh {
584
+ ensureInit();
585
+ const coords = pointsToCoords(pts);
586
+ const buf = wasm.mesh_patch_from_points(coords);
587
+ return Mesh.fromTrustedBuffer(buf);
588
588
  }
589
589
 
590
590
  /**
@@ -739,7 +739,7 @@ export class Mesh {
739
739
  height,
740
740
  closed,
741
741
  );
742
- return Mesh.fromTrustedBuffer(buf);
742
+ return Mesh.fromTrustedBuffer(buf);
743
743
  }
744
744
 
745
745
  /**
@@ -862,7 +862,7 @@ export class Mesh {
862
862
  private static mergeMeshes(meshes: Mesh[]): Mesh {
863
863
  ensureInit();
864
864
  const packed = Mesh.packMeshes(meshes);
865
- return Mesh.fromTrustedBuffer(wasm.mesh_merge(packed));
865
+ return Mesh.fromTrustedBuffer(wasm.mesh_merge(packed));
866
866
  }
867
867
 
868
868
  /**
@@ -1014,16 +1014,16 @@ export class Mesh {
1014
1014
  } else {
1015
1015
  if (faceCountA === 0) return Mesh.emptyMesh();
1016
1016
  if (faceCountB === 0) return Mesh.cloneMesh(this);
1017
- }
1018
-
1019
- const boundsA = Mesh.computeRawBounds(this);
1020
- const boundsB = Mesh.computeRawBounds(other);
1021
- const contactTol = Mesh.booleanContactTolerance(boundsA, boundsB);
1022
- if (!Mesh.boundsOverlap(boundsA, boundsB, contactTol)) {
1023
- if (operation === "union") return Mesh.mergeMeshes([this, other]);
1024
- if (operation === "intersection") return Mesh.emptyMesh();
1025
- return Mesh.cloneMesh(this);
1026
- }
1017
+ }
1018
+
1019
+ const boundsA = Mesh.computeRawBounds(this);
1020
+ const boundsB = Mesh.computeRawBounds(other);
1021
+ const contactTol = Mesh.booleanContactTolerance(boundsA, boundsB);
1022
+ if (!Mesh.boundsOverlap(boundsA, boundsB, contactTol)) {
1023
+ if (operation === "union") return Mesh.mergeMeshes([this, other]);
1024
+ if (operation === "intersection") return Mesh.emptyMesh();
1025
+ return Mesh.cloneMesh(this);
1026
+ }
1027
1027
 
1028
1028
  if (!options?.allowUnsafe) {
1029
1029
  const limits = Mesh.resolveBooleanLimits(options?.limits);
@@ -1051,24 +1051,24 @@ export class Mesh {
1051
1051
  if (!Number.isFinite(vertexCount) || vertexCount < 0) {
1052
1052
  throw new Error(`Boolean ${operation} failed and returned a corrupt mesh buffer.`);
1053
1053
  }
1054
- return Mesh.fromTrustedBuffer(result);
1055
- }
1056
-
1057
- static encodeBooleanOperationToken(
1058
- operation: "union" | "subtraction" | "intersection",
1059
- a: Mesh,
1060
- b: Mesh,
1061
- options?: MeshBooleanOptions,
1062
- ): string {
1063
- const tokens: string[] = [operation];
1064
- if (a._trustedBooleanInput && b._trustedBooleanInput) {
1065
- tokens.push("trustedInput");
1066
- }
1067
- if (options?.debugForceFaceID) {
1068
- tokens.push("forceFaceID");
1069
- }
1070
- return tokens.join("@");
1071
- }
1054
+ return Mesh.fromTrustedBuffer(result);
1055
+ }
1056
+
1057
+ static encodeBooleanOperationToken(
1058
+ operation: "union" | "subtraction" | "intersection",
1059
+ a: Mesh,
1060
+ b: Mesh,
1061
+ options?: MeshBooleanOptions,
1062
+ ): string {
1063
+ const tokens: string[] = [operation];
1064
+ if (a._trustedBooleanInput && b._trustedBooleanInput) {
1065
+ tokens.push("trustedInput");
1066
+ }
1067
+ if (options?.debugForceFaceID) {
1068
+ tokens.push("forceFaceID");
1069
+ }
1070
+ return tokens.join("@");
1071
+ }
1072
1072
 
1073
1073
  // ── Booleans ───────────────────────────────────────────────────
1074
1074
 
@@ -1078,9 +1078,9 @@ export class Mesh {
1078
1078
  * @param options - Optional safety overrides
1079
1079
  * @returns New mesh containing volume of both inputs
1080
1080
  */
1081
- union(other: Mesh, options?: MeshBooleanOptions): Mesh {
1082
- ensureInit();
1083
- const operationToken = Mesh.encodeBooleanOperationToken("union", this, other, options);
1081
+ union(other: Mesh, options?: MeshBooleanOptions): Mesh {
1082
+ ensureInit();
1083
+ const operationToken = Mesh.encodeBooleanOperationToken("union", this, other, options);
1084
1084
  return this.runBoolean(
1085
1085
  other,
1086
1086
  "union",
@@ -1101,9 +1101,9 @@ export class Mesh {
1101
1101
  * @param options - Optional safety overrides
1102
1102
  * @returns New mesh with other's volume removed from this
1103
1103
  */
1104
- subtract(other: Mesh, options?: MeshBooleanOptions): Mesh {
1105
- ensureInit();
1106
- const operationToken = Mesh.encodeBooleanOperationToken("subtraction", this, other, options);
1104
+ subtract(other: Mesh, options?: MeshBooleanOptions): Mesh {
1105
+ ensureInit();
1106
+ const operationToken = Mesh.encodeBooleanOperationToken("subtraction", this, other, options);
1107
1107
  return this.runBoolean(
1108
1108
  other,
1109
1109
  "subtraction",
@@ -1124,9 +1124,9 @@ export class Mesh {
1124
1124
  * @param options - Optional safety overrides
1125
1125
  * @returns New mesh containing only the overlapping volume
1126
1126
  */
1127
- intersect(other: Mesh, options?: MeshBooleanOptions): Mesh {
1128
- ensureInit();
1129
- const operationToken = Mesh.encodeBooleanOperationToken("intersection", this, other, options);
1127
+ intersect(other: Mesh, options?: MeshBooleanOptions): Mesh {
1128
+ ensureInit();
1129
+ const operationToken = Mesh.encodeBooleanOperationToken("intersection", this, other, options);
1130
1130
  return this.runBoolean(
1131
1131
  other,
1132
1132
  "intersection",
@@ -1146,15 +1146,15 @@ export class Mesh {
1146
1146
  * Defaults to allowUnsafe=true so high-poly jobs can run off the UI thread.
1147
1147
  */
1148
1148
  async unionAsync(other: Mesh, options?: MeshBooleanAsyncOptions): Promise<Mesh> {
1149
- const result = await runMeshBooleanInWorkerPool(
1150
- "union",
1151
- this._buffer,
1152
- other._buffer,
1153
- this._trustedBooleanInput,
1154
- other._trustedBooleanInput,
1155
- options,
1156
- );
1157
- return Mesh.fromTrustedBuffer(result);
1149
+ const result = await runMeshBooleanInWorkerPool(
1150
+ "union",
1151
+ this._buffer,
1152
+ other._buffer,
1153
+ this._trustedBooleanInput,
1154
+ other._trustedBooleanInput,
1155
+ options,
1156
+ );
1157
+ return Mesh.fromTrustedBuffer(result);
1158
1158
  }
1159
1159
 
1160
1160
  /**
@@ -1162,15 +1162,15 @@ export class Mesh {
1162
1162
  * Defaults to allowUnsafe=true so high-poly jobs can run off the UI thread.
1163
1163
  */
1164
1164
  async subtractAsync(other: Mesh, options?: MeshBooleanAsyncOptions): Promise<Mesh> {
1165
- const result = await runMeshBooleanInWorkerPool(
1166
- "subtraction",
1167
- this._buffer,
1168
- other._buffer,
1169
- this._trustedBooleanInput,
1170
- other._trustedBooleanInput,
1171
- options,
1172
- );
1173
- return Mesh.fromTrustedBuffer(result);
1165
+ const result = await runMeshBooleanInWorkerPool(
1166
+ "subtraction",
1167
+ this._buffer,
1168
+ other._buffer,
1169
+ this._trustedBooleanInput,
1170
+ other._trustedBooleanInput,
1171
+ options,
1172
+ );
1173
+ return Mesh.fromTrustedBuffer(result);
1174
1174
  }
1175
1175
 
1176
1176
  /**
@@ -1178,15 +1178,15 @@ export class Mesh {
1178
1178
  * Defaults to allowUnsafe=true so high-poly jobs can run off the UI thread.
1179
1179
  */
1180
1180
  async intersectAsync(other: Mesh, options?: MeshBooleanAsyncOptions): Promise<Mesh> {
1181
- const result = await runMeshBooleanInWorkerPool(
1182
- "intersection",
1183
- this._buffer,
1184
- other._buffer,
1185
- this._trustedBooleanInput,
1186
- other._trustedBooleanInput,
1187
- options,
1188
- );
1189
- return Mesh.fromTrustedBuffer(result);
1181
+ const result = await runMeshBooleanInWorkerPool(
1182
+ "intersection",
1183
+ this._buffer,
1184
+ other._buffer,
1185
+ this._trustedBooleanInput,
1186
+ other._trustedBooleanInput,
1187
+ options,
1188
+ );
1189
+ return Mesh.fromTrustedBuffer(result);
1190
1190
  }
1191
1191
 
1192
1192
  // ── Intersection queries ───────────────────────────────────────
@@ -1464,68 +1464,68 @@ export class Mesh {
1464
1464
  extrudeFace(faceIndex: number, distance: number): Mesh {
1465
1465
  ensureInit();
1466
1466
  if (!Number.isFinite(faceIndex) || faceIndex < 0) {
1467
- return Mesh.fromBuffer(new Float64Array(this._buffer), {
1468
- trustedBooleanInput: this._trustedBooleanInput,
1469
- });
1470
- }
1471
- return Mesh.fromTrustedBuffer(
1472
- wasm.mesh_extrude_face(this._vertexCount, this._buffer, Math.floor(faceIndex), distance),
1473
- );
1474
- }
1475
-
1476
- /**
1477
- * Check if this triangulated mesh represents a closed volume.
1478
- * Returns true when no welded topological boundary edges are found.
1479
- */
1480
- isClosedVolume(): boolean {
1481
- if (this._isClosedVolumeCache !== null) {
1482
- return this._isClosedVolumeCache;
1483
- }
1484
- if (this._topologyMetricsCache) {
1485
- const closedFromMetrics = this._topologyMetricsCache.boundaryEdges === 0;
1486
- this._isClosedVolumeCache = closedFromMetrics;
1487
- return closedFromMetrics;
1488
- }
1489
-
1490
- ensureInit();
1491
- if (this.faceCount >= Mesh.TOPOLOGY_METRICS_CACHE_FACE_THRESHOLD) {
1492
- const metrics = this.topologyMetrics();
1493
- const closedFromMetrics = metrics.boundaryEdges === 0;
1494
- this._isClosedVolumeCache = closedFromMetrics;
1495
- return closedFromMetrics;
1496
- }
1497
-
1498
- const closed = wasm.mesh_is_closed_volume(this._vertexCount, this._buffer);
1499
- this._isClosedVolumeCache = closed;
1500
- return closed;
1501
- }
1502
-
1503
- /**
1504
- * Return welded-edge topology metrics for this triangulated mesh.
1505
- * `boundaryEdges`: edges referenced by exactly one triangle.
1506
- * `nonManifoldEdges`: edges referenced by more than two triangles.
1507
- */
1508
- topologyMetrics(): { boundaryEdges: number; nonManifoldEdges: number } {
1509
- if (this._topologyMetricsCache) {
1510
- return {
1511
- boundaryEdges: this._topologyMetricsCache.boundaryEdges,
1512
- nonManifoldEdges: this._topologyMetricsCache.nonManifoldEdges,
1513
- };
1514
- }
1515
-
1516
- ensureInit();
1517
- const metrics = mesh_topology_metrics(this._vertexCount, this._buffer);
1518
- const boundaryEdges = Math.floor(metrics[0] ?? 0);
1519
- const nonManifoldEdges = Math.floor(metrics[1] ?? 0);
1520
- this._topologyMetricsCache = { boundaryEdges, nonManifoldEdges };
1521
- if (this._isClosedVolumeCache === null) {
1522
- this._isClosedVolumeCache = boundaryEdges === 0;
1523
- }
1524
- return {
1525
- boundaryEdges,
1526
- nonManifoldEdges,
1527
- };
1528
- }
1467
+ return Mesh.fromBuffer(new Float64Array(this._buffer), {
1468
+ trustedBooleanInput: this._trustedBooleanInput,
1469
+ });
1470
+ }
1471
+ return Mesh.fromTrustedBuffer(
1472
+ wasm.mesh_extrude_face(this._vertexCount, this._buffer, Math.floor(faceIndex), distance),
1473
+ );
1474
+ }
1475
+
1476
+ /**
1477
+ * Check if this triangulated mesh represents a closed volume.
1478
+ * Returns true when no welded topological boundary edges are found.
1479
+ */
1480
+ isClosedVolume(): boolean {
1481
+ if (this._isClosedVolumeCache !== null) {
1482
+ return this._isClosedVolumeCache;
1483
+ }
1484
+ if (this._topologyMetricsCache) {
1485
+ const closedFromMetrics = this._topologyMetricsCache.boundaryEdges === 0;
1486
+ this._isClosedVolumeCache = closedFromMetrics;
1487
+ return closedFromMetrics;
1488
+ }
1489
+
1490
+ ensureInit();
1491
+ if (this.faceCount >= Mesh.TOPOLOGY_METRICS_CACHE_FACE_THRESHOLD) {
1492
+ const metrics = this.topologyMetrics();
1493
+ const closedFromMetrics = metrics.boundaryEdges === 0;
1494
+ this._isClosedVolumeCache = closedFromMetrics;
1495
+ return closedFromMetrics;
1496
+ }
1497
+
1498
+ const closed = wasm.mesh_is_closed_volume(this._vertexCount, this._buffer);
1499
+ this._isClosedVolumeCache = closed;
1500
+ return closed;
1501
+ }
1502
+
1503
+ /**
1504
+ * Return welded-edge topology metrics for this triangulated mesh.
1505
+ * `boundaryEdges`: edges referenced by exactly one triangle.
1506
+ * `nonManifoldEdges`: edges referenced by more than two triangles.
1507
+ */
1508
+ topologyMetrics(): { boundaryEdges: number; nonManifoldEdges: number } {
1509
+ if (this._topologyMetricsCache) {
1510
+ return {
1511
+ boundaryEdges: this._topologyMetricsCache.boundaryEdges,
1512
+ nonManifoldEdges: this._topologyMetricsCache.nonManifoldEdges,
1513
+ };
1514
+ }
1515
+
1516
+ ensureInit();
1517
+ const metrics = mesh_topology_metrics(this._vertexCount, this._buffer);
1518
+ const boundaryEdges = Math.floor(metrics[0] ?? 0);
1519
+ const nonManifoldEdges = Math.floor(metrics[1] ?? 0);
1520
+ this._topologyMetricsCache = { boundaryEdges, nonManifoldEdges };
1521
+ if (this._isClosedVolumeCache === null) {
1522
+ this._isClosedVolumeCache = boundaryEdges === 0;
1523
+ }
1524
+ return {
1525
+ boundaryEdges,
1526
+ nonManifoldEdges,
1527
+ };
1528
+ }
1529
1529
 
1530
1530
  /**
1531
1531
  * Odd/even point containment test against a closed mesh.