okgeometry-api 0.5.8 → 1.0.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,spwoCAAspwoC,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,028yDAA028yD,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "okgeometry-api",
3
- "version": "0.5.8",
3
+ "version": "1.0.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",
@@ -20,10 +20,13 @@
20
20
  "inline-wasm": "tsx scripts/inline-wasm.ts",
21
21
  "build": "npm run inline-wasm && tsc",
22
22
  "bench:boolean-manifold-baseline": "npm run build && tsx scripts/bench-boolean-manifold-baseline.ts",
23
- "bench:boolean-nextgen-heavy": "npm run build && tsx scripts/bench-boolean-nextgen-heavy.ts",
24
- "bench:boolean-nextgen": "npm run build && tsx scripts/gate-boolean-nextgen.ts",
25
- "gate:boolean-nextgen": "npm run build && tsx scripts/gate-boolean-nextgen.ts",
26
- "gate:boolean-nextgen-vs-manifold": "npm run bench:boolean-manifold-baseline && npm run gate:boolean-nextgen",
23
+ "bench:boolean-replacement-heavy": "npm run build && tsx scripts/bench-boolean-replacement-heavy.ts",
24
+ "bench:boolean-deterministic": "npm run build && tsx scripts/bench-boolean-deterministic.ts",
25
+ "bench:boolean-ab": "npm run build && tsx scripts/bench-boolean-ab.ts",
26
+ "bench:subtraction-bunny-parity": "npm run build && tsx scripts/bench-subtraction-bunny-parity.ts",
27
+ "bench:boolean-replacement": "npm run build && tsx scripts/gate-boolean-replacement.ts",
28
+ "gate:boolean-replacement": "npm run build && tsx scripts/gate-boolean-replacement.ts",
29
+ "gate:boolean-replacement-vs-manifold": "npm run bench:boolean-manifold-baseline && npm run gate:boolean-replacement",
27
30
  "prepublishOnly": "npm run build"
28
31
  },
29
32
  "keywords": [
@@ -40,7 +43,7 @@
40
43
  "license": "Proprietary License",
41
44
  "repository": {
42
45
  "type": "git",
43
- "url": "https://github.com/moel-ai/okgeometrycore"
46
+ "url": "https://www.orkestra.online"
44
47
  },
45
48
  "devDependencies": {
46
49
  "manifold-3d": "^3.3.2",
package/src/Mesh.ts CHANGED
@@ -18,21 +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
- MeshBooleanBackend,
24
- MeshBooleanLimits,
25
- MeshBooleanOptions,
26
- } from "./mesh-boolean.protocol.js";
27
- import * as wasm from "../wasm/okgeometrycore_bg.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
- MeshBooleanBackend,
33
- MeshBooleanLimits,
34
- MeshBooleanOptions,
35
- MeshBooleanErrorCode,
30
+ export type {
31
+ MeshBooleanAsyncOptions,
32
+ MeshBooleanLimits,
33
+ MeshBooleanOptions,
34
+ MeshBooleanErrorCode,
36
35
  MeshBooleanErrorPayload,
37
36
  MeshBooleanProgressEvent,
38
37
  } from "./mesh-boolean.protocol.js";
@@ -72,27 +71,33 @@ interface RawMeshBounds {
72
71
  *
73
72
  * Buffer format: [vertexCount, x1,y1,z1, ..., i1,i2,i3, ...]
74
73
  */
75
- export class Mesh {
76
- private _buffer: Float64Array;
77
- private _vertexCount: number;
78
-
79
- private static readonly DEFAULT_BOOLEAN_LIMITS: MeshBooleanLimits = {
80
- maxInputFacesPerMesh: 120_000,
81
- maxCombinedInputFaces: 180_000,
82
- maxFaceProduct: 500_000_000,
83
- };
84
-
85
- // Lazy caches
86
- private _positionBuffer: Float32Array | null = null;
87
- private _indexBuffer: Uint32Array | null = null;
88
- private _vertices: Point[] | null = null;
89
- private _faces: number[][] | null = null;
90
- private _edgeVertexPairs: Array<[number, number]> | null = null;
91
-
92
- private constructor(buffer: Float64Array) {
93
- this._buffer = buffer;
94
- this._vertexCount = buffer.length > 0 ? buffer[0] : 0;
95
- }
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
+ }
96
101
 
97
102
  /**
98
103
  * Configure default size for the shared boolean worker pool.
@@ -119,10 +124,14 @@ export class Mesh {
119
124
  };
120
125
  }
121
126
 
122
- private static computeRawBounds(mesh: Mesh): RawMeshBounds | null {
123
- if (mesh._vertexCount <= 0) return null;
124
-
125
- 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;
126
135
  const limit = 1 + mesh._vertexCount * 3;
127
136
 
128
137
  let minX = data[1];
@@ -144,8 +153,10 @@ export class Mesh {
144
153
  if (z > maxZ) maxZ = z;
145
154
  }
146
155
 
147
- return { minX, minY, minZ, maxX, maxY, maxZ };
148
- }
156
+ const bounds = { minX, minY, minZ, maxX, maxY, maxZ };
157
+ mesh._rawBoundsCache = bounds;
158
+ return bounds;
159
+ }
149
160
 
150
161
  private static boundsOverlap(a: RawMeshBounds | null, b: RawMeshBounds | null, eps = 1e-9): boolean {
151
162
  if (!a || !b) return false;
@@ -171,13 +182,15 @@ export class Mesh {
171
182
  return Math.min(1e-4, Math.max(1e-9, scale * 1e-6));
172
183
  }
173
184
 
174
- private static cloneMesh(mesh: Mesh): Mesh {
175
- return Mesh.fromBuffer(new Float64Array(mesh._buffer));
176
- }
177
-
178
- private static emptyMesh(): Mesh {
179
- return Mesh.fromBuffer(new Float64Array(0));
180
- }
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
+ }
181
194
 
182
195
  // ── GPU-ready buffers ──────────────────────────────────────────
183
196
 
@@ -217,10 +230,12 @@ export class Mesh {
217
230
  return this._vertexCount;
218
231
  }
219
232
 
220
- /** Number of triangular faces in this mesh */
221
- get faceCount(): number {
222
- return this.indexBuffer.length / 3;
223
- }
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
+ }
224
239
 
225
240
  // ── High-level accessors (lazy) ────────────────────────────────
226
241
 
@@ -269,9 +284,20 @@ export class Mesh {
269
284
  * @param buffer - Float64Array in mesh buffer format
270
285
  * @returns New Mesh instance
271
286
  */
272
- static fromBuffer(buffer: Float64Array): Mesh {
273
- return new Mesh(buffer);
274
- }
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
+ }
275
301
 
276
302
  /**
277
303
  * Build an axis-aligned rectangle on a plane basis from opposite corners.
@@ -554,11 +580,11 @@ export class Mesh {
554
580
  * @param pts - Ordered boundary points defining a closed polygon (minimum 3)
555
581
  * @returns New Mesh triangulating the polygon interior
556
582
  */
557
- static patchFromPoints(pts: Point[]): Mesh {
558
- ensureInit();
559
- const coords = pointsToCoords(pts);
560
- const buf = wasm.mesh_patch_from_points(coords);
561
- return new Mesh(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);
562
588
  }
563
589
 
564
590
  /**
@@ -570,7 +596,7 @@ export class Mesh {
570
596
  */
571
597
  static createBox(width: number, height: number, depth: number): Mesh {
572
598
  ensureInit();
573
- return new Mesh(wasm.mesh_create_box(width, height, depth));
599
+ return Mesh.fromTrustedBuffer(wasm.mesh_create_box(width, height, depth));
574
600
  }
575
601
 
576
602
  /**
@@ -582,7 +608,7 @@ export class Mesh {
582
608
  */
583
609
  static createSphere(radius: number, segments: number, rings: number): Mesh {
584
610
  ensureInit();
585
- return new Mesh(wasm.mesh_create_sphere(radius, segments, rings));
611
+ return Mesh.fromTrustedBuffer(wasm.mesh_create_sphere(radius, segments, rings));
586
612
  }
587
613
 
588
614
  /**
@@ -594,7 +620,7 @@ export class Mesh {
594
620
  */
595
621
  static createCylinder(radius: number, height: number, segments: number): Mesh {
596
622
  ensureInit();
597
- return new Mesh(wasm.mesh_create_cylinder(radius, height, segments));
623
+ return Mesh.fromTrustedBuffer(wasm.mesh_create_cylinder(radius, height, segments));
598
624
  }
599
625
 
600
626
  /**
@@ -606,7 +632,7 @@ export class Mesh {
606
632
  */
607
633
  static createPrism(radius: number, height: number, sides: number): Mesh {
608
634
  ensureInit();
609
- return new Mesh(wasm.mesh_create_prism(radius, height, sides));
635
+ return Mesh.fromTrustedBuffer(wasm.mesh_create_prism(radius, height, sides));
610
636
  }
611
637
 
612
638
  /**
@@ -618,7 +644,7 @@ export class Mesh {
618
644
  */
619
645
  static createCone(radius: number, height: number, segments: number): Mesh {
620
646
  ensureInit();
621
- return new Mesh(wasm.mesh_create_cone(radius, height, segments));
647
+ return Mesh.fromTrustedBuffer(wasm.mesh_create_cone(radius, height, segments));
622
648
  }
623
649
 
624
650
  /**
@@ -628,7 +654,7 @@ export class Mesh {
628
654
  */
629
655
  static fromOBJ(objString: string): Mesh {
630
656
  ensureInit();
631
- return new Mesh(wasm.mesh_import_obj(objString));
657
+ return Mesh.fromTrustedBuffer(wasm.mesh_import_obj(objString));
632
658
  }
633
659
 
634
660
  /**
@@ -656,7 +682,7 @@ export class Mesh {
656
682
  data[off + 5] = c.normal?.z ?? 0;
657
683
  data[off + 6] = c.radius;
658
684
  }
659
- return new Mesh(wasm.loft_circles(data, segments, caps));
685
+ return Mesh.fromTrustedBuffer(wasm.loft_circles(data, segments, caps));
660
686
  }
661
687
 
662
688
  /**
@@ -676,7 +702,7 @@ export class Mesh {
676
702
  parts.push(p.x, p.y, p.z);
677
703
  }
678
704
  }
679
- return new Mesh(wasm.loft_polylines(new Float64Array(parts), segments, caps));
705
+ return Mesh.fromTrustedBuffer(wasm.loft_polylines(new Float64Array(parts), segments, caps));
680
706
  }
681
707
 
682
708
  /**
@@ -688,7 +714,7 @@ export class Mesh {
688
714
  */
689
715
  static sweep(profilePoints: Point[], pathPoints: Point[], caps = false): Mesh {
690
716
  ensureInit();
691
- return new Mesh(wasm.sweep_polylines(pointsToCoords(profilePoints), pointsToCoords(pathPoints), caps));
717
+ return Mesh.fromTrustedBuffer(wasm.sweep_polylines(pointsToCoords(profilePoints), pointsToCoords(pathPoints), caps));
692
718
  }
693
719
 
694
720
  /**
@@ -713,7 +739,7 @@ export class Mesh {
713
739
  height,
714
740
  closed,
715
741
  );
716
- return Mesh.fromBuffer(buf);
742
+ return Mesh.fromTrustedBuffer(buf);
717
743
  }
718
744
 
719
745
  /**
@@ -758,7 +784,7 @@ export class Mesh {
758
784
  ensureInit();
759
785
  const profileData = Mesh.encodeCurve(profile);
760
786
  const pathData = Mesh.encodeCurve(path);
761
- return new Mesh(wasm.sweep_curves(profileData, pathData, segments, segments, caps));
787
+ return Mesh.fromTrustedBuffer(wasm.sweep_curves(profileData, pathData, segments, segments, caps));
762
788
  }
763
789
 
764
790
  /** Encode a curve into the WASM format for sweep_curves. */
@@ -836,7 +862,7 @@ export class Mesh {
836
862
  private static mergeMeshes(meshes: Mesh[]): Mesh {
837
863
  ensureInit();
838
864
  const packed = Mesh.packMeshes(meshes);
839
- return Mesh.fromBuffer(wasm.mesh_merge(packed));
865
+ return Mesh.fromTrustedBuffer(wasm.mesh_merge(packed));
840
866
  }
841
867
 
842
868
  /**
@@ -928,7 +954,7 @@ export class Mesh {
928
954
  */
929
955
  translate(offset: Vec3): Mesh {
930
956
  ensureInit();
931
- return new Mesh(wasm.mesh_translate(this._vertexCount, this._buffer, offset.x, offset.y, offset.z));
957
+ return Mesh.fromTrustedBuffer(wasm.mesh_translate(this._vertexCount, this._buffer, offset.x, offset.y, offset.z));
932
958
  }
933
959
 
934
960
  /**
@@ -946,7 +972,7 @@ export class Mesh {
946
972
  .rotate(dir, angleRadians)
947
973
  .translate(new Vec3(c.x, c.y, c.z));
948
974
  }
949
- return new Mesh(wasm.mesh_rotate(this._vertexCount, this._buffer, axis.x, axis.y, axis.z, angleRadians));
975
+ return Mesh.fromTrustedBuffer(wasm.mesh_rotate(this._vertexCount, this._buffer, axis.x, axis.y, axis.z, angleRadians));
950
976
  }
951
977
 
952
978
  /**
@@ -956,7 +982,7 @@ export class Mesh {
956
982
  */
957
983
  scale(factor: number): Mesh {
958
984
  ensureInit();
959
- return new Mesh(wasm.mesh_scale(this._vertexCount, this._buffer, factor, factor, factor));
985
+ return Mesh.fromTrustedBuffer(wasm.mesh_scale(this._vertexCount, this._buffer, factor, factor, factor));
960
986
  }
961
987
 
962
988
  /**
@@ -968,7 +994,7 @@ export class Mesh {
968
994
  */
969
995
  scaleXYZ(sx: number, sy: number, sz: number): Mesh {
970
996
  ensureInit();
971
- return new Mesh(wasm.mesh_scale(this._vertexCount, this._buffer, sx, sy, sz));
997
+ return Mesh.fromTrustedBuffer(wasm.mesh_scale(this._vertexCount, this._buffer, sx, sy, sz));
972
998
  }
973
999
 
974
1000
  private runBoolean(
@@ -1025,15 +1051,24 @@ export class Mesh {
1025
1051
  if (!Number.isFinite(vertexCount) || vertexCount < 0) {
1026
1052
  throw new Error(`Boolean ${operation} failed and returned a corrupt mesh buffer.`);
1027
1053
  }
1028
- return Mesh.fromBuffer(result);
1029
- }
1030
-
1031
- private static encodeBooleanOperationWithBackend(
1032
- operation: "union" | "subtraction" | "intersection",
1033
- _backend: MeshBooleanBackend | undefined,
1034
- ): string {
1035
- return `${operation}@nextgen`;
1036
- }
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
+ }
1037
1072
 
1038
1073
  // ── Booleans ───────────────────────────────────────────────────
1039
1074
 
@@ -1043,9 +1078,9 @@ export class Mesh {
1043
1078
  * @param options - Optional safety overrides
1044
1079
  * @returns New mesh containing volume of both inputs
1045
1080
  */
1046
- union(other: Mesh, options?: MeshBooleanOptions): Mesh {
1047
- ensureInit();
1048
- const operationToken = Mesh.encodeBooleanOperationWithBackend("union", options?.backend);
1081
+ union(other: Mesh, options?: MeshBooleanOptions): Mesh {
1082
+ ensureInit();
1083
+ const operationToken = Mesh.encodeBooleanOperationToken("union", this, other, options);
1049
1084
  return this.runBoolean(
1050
1085
  other,
1051
1086
  "union",
@@ -1066,9 +1101,9 @@ export class Mesh {
1066
1101
  * @param options - Optional safety overrides
1067
1102
  * @returns New mesh with other's volume removed from this
1068
1103
  */
1069
- subtract(other: Mesh, options?: MeshBooleanOptions): Mesh {
1070
- ensureInit();
1071
- const operationToken = Mesh.encodeBooleanOperationWithBackend("subtraction", options?.backend);
1104
+ subtract(other: Mesh, options?: MeshBooleanOptions): Mesh {
1105
+ ensureInit();
1106
+ const operationToken = Mesh.encodeBooleanOperationToken("subtraction", this, other, options);
1072
1107
  return this.runBoolean(
1073
1108
  other,
1074
1109
  "subtraction",
@@ -1089,9 +1124,9 @@ export class Mesh {
1089
1124
  * @param options - Optional safety overrides
1090
1125
  * @returns New mesh containing only the overlapping volume
1091
1126
  */
1092
- intersect(other: Mesh, options?: MeshBooleanOptions): Mesh {
1093
- ensureInit();
1094
- const operationToken = Mesh.encodeBooleanOperationWithBackend("intersection", options?.backend);
1127
+ intersect(other: Mesh, options?: MeshBooleanOptions): Mesh {
1128
+ ensureInit();
1129
+ const operationToken = Mesh.encodeBooleanOperationToken("intersection", this, other, options);
1095
1130
  return this.runBoolean(
1096
1131
  other,
1097
1132
  "intersection",
@@ -1111,8 +1146,15 @@ export class Mesh {
1111
1146
  * Defaults to allowUnsafe=true so high-poly jobs can run off the UI thread.
1112
1147
  */
1113
1148
  async unionAsync(other: Mesh, options?: MeshBooleanAsyncOptions): Promise<Mesh> {
1114
- const result = await runMeshBooleanInWorkerPool("union", this._buffer, other._buffer, options);
1115
- return Mesh.fromBuffer(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);
1116
1158
  }
1117
1159
 
1118
1160
  /**
@@ -1120,8 +1162,15 @@ export class Mesh {
1120
1162
  * Defaults to allowUnsafe=true so high-poly jobs can run off the UI thread.
1121
1163
  */
1122
1164
  async subtractAsync(other: Mesh, options?: MeshBooleanAsyncOptions): Promise<Mesh> {
1123
- const result = await runMeshBooleanInWorkerPool("subtraction", this._buffer, other._buffer, options);
1124
- return Mesh.fromBuffer(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);
1125
1174
  }
1126
1175
 
1127
1176
  /**
@@ -1129,8 +1178,15 @@ export class Mesh {
1129
1178
  * Defaults to allowUnsafe=true so high-poly jobs can run off the UI thread.
1130
1179
  */
1131
1180
  async intersectAsync(other: Mesh, options?: MeshBooleanAsyncOptions): Promise<Mesh> {
1132
- const result = await runMeshBooleanInWorkerPool("intersection", this._buffer, other._buffer, options);
1133
- return Mesh.fromBuffer(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);
1134
1190
  }
1135
1191
 
1136
1192
  // ── Intersection queries ───────────────────────────────────────
@@ -1176,7 +1232,7 @@ export class Mesh {
1176
1232
  */
1177
1233
  applyMatrix(matrix: number[]): Mesh {
1178
1234
  ensureInit();
1179
- return new Mesh(wasm.mesh_apply_matrix(this._vertexCount, this._buffer, new Float64Array(matrix)));
1235
+ return Mesh.fromTrustedBuffer(wasm.mesh_apply_matrix(this._vertexCount, this._buffer, new Float64Array(matrix)));
1180
1236
  }
1181
1237
 
1182
1238
  /**
@@ -1408,21 +1464,68 @@ export class Mesh {
1408
1464
  extrudeFace(faceIndex: number, distance: number): Mesh {
1409
1465
  ensureInit();
1410
1466
  if (!Number.isFinite(faceIndex) || faceIndex < 0) {
1411
- return Mesh.fromBuffer(new Float64Array(this._buffer));
1412
- }
1413
- return Mesh.fromBuffer(
1414
- wasm.mesh_extrude_face(this._vertexCount, this._buffer, Math.floor(faceIndex), distance),
1415
- );
1416
- }
1417
-
1418
- /**
1419
- * Check if this triangulated mesh represents a closed volume.
1420
- * Returns true when no welded topological boundary edges are found.
1421
- */
1422
- isClosedVolume(): boolean {
1423
- ensureInit();
1424
- return wasm.mesh_is_closed_volume(this._vertexCount, this._buffer);
1425
- }
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
+ }
1426
1529
 
1427
1530
  /**
1428
1531
  * Odd/even point containment test against a closed mesh.
@@ -1489,3 +1592,4 @@ export class Mesh {
1489
1592
 
1490
1593
 
1491
1594
 
1595
+