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.
- 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/package.json +1 -1
- package/src/Mesh.ts +238 -238
- package/src/mesh-boolean.pool.ts +51 -51
- package/src/mesh-boolean.protocol.ts +32 -32
- package/src/mesh-boolean.worker.ts +11 -11
- package/src/wasm-base64.ts +1 -1
- package/wasm/okgeometrycore.d.ts +2 -2
- package/wasm/okgeometrycore.js +1 -4
- package/wasm/okgeometrycore_bg.wasm +0 -0
- package/wasm/okgeometrycore_bg.wasm.d.ts +2 -2
- package/wasm/package.json +1 -1
package/dist/wasm-base64.js.map
CHANGED
|
@@ -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,
|
|
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
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.
|