okgeometry-api 0.4.3 → 0.4.5
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/Mesh.d.ts +100 -4
- package/dist/Mesh.d.ts.map +1 -1
- package/dist/Mesh.js +431 -60
- package/dist/Mesh.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/mesh-boolean.pool.d.ts +37 -0
- package/dist/mesh-boolean.pool.d.ts.map +1 -0
- package/dist/mesh-boolean.pool.js +336 -0
- package/dist/mesh-boolean.pool.js.map +1 -0
- package/dist/mesh-boolean.protocol.d.ts +85 -0
- package/dist/mesh-boolean.protocol.d.ts.map +1 -0
- package/dist/mesh-boolean.protocol.js +9 -0
- package/dist/mesh-boolean.protocol.js.map +1 -0
- package/dist/mesh-boolean.worker.d.ts +2 -0
- package/dist/mesh-boolean.worker.d.ts.map +1 -0
- package/dist/mesh-boolean.worker.js +105 -0
- package/dist/mesh-boolean.worker.js.map +1 -0
- 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/wasm/okgeometrycore.d.ts +143 -0
- package/wasm/okgeometrycore.js +1784 -7
- package/wasm/okgeometrycore_bg.js +5 -363
- package/wasm/okgeometrycore_bg.wasm +0 -0
- package/wasm/okgeometrycore_bg.wasm.d.ts +4 -0
- package/wasm/package.json +0 -2
package/dist/Mesh.js
CHANGED
|
@@ -9,7 +9,10 @@ import { Polygon } from "./Polygon.js";
|
|
|
9
9
|
import { NurbsCurve } from "./NurbsCurve.js";
|
|
10
10
|
import { PolyCurve } from "./PolyCurve.js";
|
|
11
11
|
import { pointsToCoords, parsePolylineBuffer as parsePolylineBuf } from "./BufferCodec.js";
|
|
12
|
+
import { configureDefaultMeshBooleanWorkerPool, disposeMeshBooleanWorkerPools, runMeshBooleanInWorkerPool, } from "./mesh-boolean.pool.js";
|
|
13
|
+
import { MeshBooleanExecutionError } from "./mesh-boolean.protocol.js";
|
|
12
14
|
import * as wasm from "../wasm/okgeometrycore_bg.js";
|
|
15
|
+
export { MeshBooleanExecutionError };
|
|
13
16
|
/**
|
|
14
17
|
* Buffer-backed triangle mesh with GPU-ready accessors.
|
|
15
18
|
* All geometry lives in a Float64Array from WASM.
|
|
@@ -27,6 +30,72 @@ export class Mesh {
|
|
|
27
30
|
this._buffer = buffer;
|
|
28
31
|
this._vertexCount = buffer.length > 0 ? buffer[0] : 0;
|
|
29
32
|
}
|
|
33
|
+
/**
|
|
34
|
+
* Configure default size for the shared boolean worker pool.
|
|
35
|
+
*/
|
|
36
|
+
static configureBooleanWorkerPool(options) {
|
|
37
|
+
configureDefaultMeshBooleanWorkerPool(options);
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Dispose all shared boolean worker pools.
|
|
41
|
+
*/
|
|
42
|
+
static disposeBooleanWorkerPools() {
|
|
43
|
+
disposeMeshBooleanWorkerPools();
|
|
44
|
+
}
|
|
45
|
+
static resolveBooleanLimits(overrides) {
|
|
46
|
+
const defaults = Mesh.DEFAULT_BOOLEAN_LIMITS;
|
|
47
|
+
return {
|
|
48
|
+
maxInputFacesPerMesh: overrides?.maxInputFacesPerMesh ?? defaults.maxInputFacesPerMesh,
|
|
49
|
+
maxCombinedInputFaces: overrides?.maxCombinedInputFaces ?? defaults.maxCombinedInputFaces,
|
|
50
|
+
maxFaceProduct: overrides?.maxFaceProduct ?? defaults.maxFaceProduct,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
static computeRawBounds(mesh) {
|
|
54
|
+
if (mesh._vertexCount <= 0)
|
|
55
|
+
return null;
|
|
56
|
+
const data = mesh._buffer;
|
|
57
|
+
const limit = 1 + mesh._vertexCount * 3;
|
|
58
|
+
let minX = data[1];
|
|
59
|
+
let minY = data[2];
|
|
60
|
+
let minZ = data[3];
|
|
61
|
+
let maxX = minX;
|
|
62
|
+
let maxY = minY;
|
|
63
|
+
let maxZ = minZ;
|
|
64
|
+
for (let i = 4; i < limit; i += 3) {
|
|
65
|
+
const x = data[i];
|
|
66
|
+
const y = data[i + 1];
|
|
67
|
+
const z = data[i + 2];
|
|
68
|
+
if (x < minX)
|
|
69
|
+
minX = x;
|
|
70
|
+
if (y < minY)
|
|
71
|
+
minY = y;
|
|
72
|
+
if (z < minZ)
|
|
73
|
+
minZ = z;
|
|
74
|
+
if (x > maxX)
|
|
75
|
+
maxX = x;
|
|
76
|
+
if (y > maxY)
|
|
77
|
+
maxY = y;
|
|
78
|
+
if (z > maxZ)
|
|
79
|
+
maxZ = z;
|
|
80
|
+
}
|
|
81
|
+
return { minX, minY, minZ, maxX, maxY, maxZ };
|
|
82
|
+
}
|
|
83
|
+
static boundsOverlap(a, b, eps = 1e-9) {
|
|
84
|
+
if (!a || !b)
|
|
85
|
+
return false;
|
|
86
|
+
return a.minX <= b.maxX + eps
|
|
87
|
+
&& a.maxX + eps >= b.minX
|
|
88
|
+
&& a.minY <= b.maxY + eps
|
|
89
|
+
&& a.maxY + eps >= b.minY
|
|
90
|
+
&& a.minZ <= b.maxZ + eps
|
|
91
|
+
&& a.maxZ + eps >= b.minZ;
|
|
92
|
+
}
|
|
93
|
+
static cloneMesh(mesh) {
|
|
94
|
+
return Mesh.fromBuffer(new Float64Array(mesh._buffer));
|
|
95
|
+
}
|
|
96
|
+
static emptyMesh() {
|
|
97
|
+
return Mesh.fromBuffer(new Float64Array(0));
|
|
98
|
+
}
|
|
30
99
|
// ── GPU-ready buffers ──────────────────────────────────────────
|
|
31
100
|
/**
|
|
32
101
|
* Float32 xyz positions for Three.js BufferGeometry.
|
|
@@ -110,6 +179,226 @@ export class Mesh {
|
|
|
110
179
|
static fromBuffer(buffer) {
|
|
111
180
|
return new Mesh(buffer);
|
|
112
181
|
}
|
|
182
|
+
/**
|
|
183
|
+
* Build an axis-aligned rectangle on a plane basis from opposite corners.
|
|
184
|
+
* The resulting corners are ordered [p0, p1, p2, p3] and form a closed loop.
|
|
185
|
+
*/
|
|
186
|
+
static buildPlanarRectangle(startPoint, endPoint, uAxis, vAxis) {
|
|
187
|
+
const delta = endPoint.sub(startPoint);
|
|
188
|
+
const uLen = delta.dot(uAxis);
|
|
189
|
+
const vLen = delta.dot(vAxis);
|
|
190
|
+
const p0 = startPoint;
|
|
191
|
+
const p1 = startPoint.add(uAxis.scale(uLen));
|
|
192
|
+
const p3 = startPoint.add(vAxis.scale(vLen));
|
|
193
|
+
const p2 = p1.add(vAxis.scale(vLen));
|
|
194
|
+
return {
|
|
195
|
+
corners: [p0, p1, p2, p3],
|
|
196
|
+
width: Math.abs(uLen),
|
|
197
|
+
height: Math.abs(vLen),
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Build a planar circle from center and a point on/near its radius direction.
|
|
202
|
+
* Radius is measured in the plane orthogonal to `normal`.
|
|
203
|
+
*/
|
|
204
|
+
static buildPlanarCircle(center, radiusPoint, normal, segments = 64) {
|
|
205
|
+
const n = normal.normalize();
|
|
206
|
+
const radial = radiusPoint.sub(center);
|
|
207
|
+
const radialInPlane = radial.sub(n.scale(radial.dot(n)));
|
|
208
|
+
const radius = radialInPlane.length();
|
|
209
|
+
if (radius < 1e-12 || segments < 3) {
|
|
210
|
+
return { points: [], radius: 0 };
|
|
211
|
+
}
|
|
212
|
+
const circle = new Circle(center, radius, n);
|
|
213
|
+
return {
|
|
214
|
+
points: circle.sample(Math.floor(segments)),
|
|
215
|
+
radius,
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Build a planar arc from center, start, and end points.
|
|
220
|
+
* Uses the shortest signed sweep between start and end around `normal`.
|
|
221
|
+
*/
|
|
222
|
+
static buildPlanarArcCenterStartEnd(center, startPoint, endPoint, normal, segments = 64) {
|
|
223
|
+
const n = normal.normalize();
|
|
224
|
+
const minSegments = Math.max(2, Math.floor(segments));
|
|
225
|
+
const startRaw = startPoint.sub(center);
|
|
226
|
+
const endRaw = endPoint.sub(center);
|
|
227
|
+
const startVec = startRaw.sub(n.scale(startRaw.dot(n)));
|
|
228
|
+
const endVec = endRaw.sub(n.scale(endRaw.dot(n)));
|
|
229
|
+
const radius = startVec.length();
|
|
230
|
+
const endLen = endVec.length();
|
|
231
|
+
if (radius < 1e-12 || endLen < 1e-12) {
|
|
232
|
+
return {
|
|
233
|
+
points: [],
|
|
234
|
+
center,
|
|
235
|
+
radius: 0,
|
|
236
|
+
startAngle: 0,
|
|
237
|
+
endAngle: 0,
|
|
238
|
+
sweepAngle: 0,
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
const basis = Mesh.resolveArcBasis(center, radius, n);
|
|
242
|
+
const startAngle = Mesh.angleOnBasis(startVec, basis.uAxis, basis.vAxis);
|
|
243
|
+
const endAngleRaw = Mesh.angleOnBasis(endVec.scale(radius / endLen), basis.uAxis, basis.vAxis);
|
|
244
|
+
const sweepAngle = Mesh.normalizeSignedAngle(endAngleRaw - startAngle);
|
|
245
|
+
if (Math.abs(sweepAngle) < 1e-9) {
|
|
246
|
+
return {
|
|
247
|
+
points: [],
|
|
248
|
+
center,
|
|
249
|
+
radius: 0,
|
|
250
|
+
startAngle: 0,
|
|
251
|
+
endAngle: 0,
|
|
252
|
+
sweepAngle: 0,
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
const endAngle = startAngle + sweepAngle;
|
|
256
|
+
const arc = new Arc(center, radius, startAngle, endAngle, n);
|
|
257
|
+
return {
|
|
258
|
+
points: arc.sample(minSegments),
|
|
259
|
+
center,
|
|
260
|
+
radius,
|
|
261
|
+
startAngle,
|
|
262
|
+
endAngle,
|
|
263
|
+
sweepAngle,
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* Build a planar arc from start/end points and a radius control point.
|
|
268
|
+
* Radius is measured from start point to radiusPoint in the active plane.
|
|
269
|
+
* The best of 4 candidates (2 centers x minor/major sweeps) is selected by
|
|
270
|
+
* midpoint proximity to radiusPoint.
|
|
271
|
+
*/
|
|
272
|
+
static buildPlanarArcStartEndRadius(startPoint, endPoint, radiusPoint, normal, segments = 64) {
|
|
273
|
+
const n = normal.normalize();
|
|
274
|
+
const minSegments = Math.max(2, Math.floor(segments));
|
|
275
|
+
const start = Mesh.projectPointToPlane(startPoint, startPoint, n);
|
|
276
|
+
const end = Mesh.projectPointToPlane(endPoint, startPoint, n);
|
|
277
|
+
const guide = Mesh.projectPointToPlane(radiusPoint, startPoint, n);
|
|
278
|
+
const chord = end.sub(start);
|
|
279
|
+
const chordLength = chord.length();
|
|
280
|
+
if (chordLength < 1e-9) {
|
|
281
|
+
return null;
|
|
282
|
+
}
|
|
283
|
+
const radius = guide.sub(start).length();
|
|
284
|
+
const halfChord = chordLength * 0.5;
|
|
285
|
+
if (!Number.isFinite(radius) || radius < halfChord + 1e-9) {
|
|
286
|
+
return null;
|
|
287
|
+
}
|
|
288
|
+
const chordDir = chord.scale(1 / chordLength);
|
|
289
|
+
const perpRaw = n.cross(chordDir);
|
|
290
|
+
const perpLen = perpRaw.length();
|
|
291
|
+
if (perpLen < 1e-12) {
|
|
292
|
+
return null;
|
|
293
|
+
}
|
|
294
|
+
const perp = perpRaw.scale(1 / perpLen);
|
|
295
|
+
const hSq = radius * radius - halfChord * halfChord;
|
|
296
|
+
if (hSq < 0) {
|
|
297
|
+
return null;
|
|
298
|
+
}
|
|
299
|
+
const h = Math.sqrt(Math.max(0, hSq));
|
|
300
|
+
const mid = start.add(chord.scale(0.5));
|
|
301
|
+
const centers = [mid.add(perp.scale(h)), mid.add(perp.scale(-h))];
|
|
302
|
+
const candidates = [];
|
|
303
|
+
for (const center of centers) {
|
|
304
|
+
const startVec = start.sub(center);
|
|
305
|
+
const endVec = end.sub(center);
|
|
306
|
+
if (startVec.length() < 1e-12 || endVec.length() < 1e-12) {
|
|
307
|
+
continue;
|
|
308
|
+
}
|
|
309
|
+
const basis = Mesh.resolveArcBasis(center, radius, n);
|
|
310
|
+
const startAngle = Mesh.angleOnBasis(startVec, basis.uAxis, basis.vAxis);
|
|
311
|
+
const endAngleRaw = Mesh.angleOnBasis(endVec, basis.uAxis, basis.vAxis);
|
|
312
|
+
const minorSweep = Mesh.normalizeSignedAngle(endAngleRaw - startAngle);
|
|
313
|
+
if (Math.abs(minorSweep) < 1e-9) {
|
|
314
|
+
continue;
|
|
315
|
+
}
|
|
316
|
+
const majorSweep = minorSweep > 0
|
|
317
|
+
? minorSweep - Math.PI * 2
|
|
318
|
+
: minorSweep + Math.PI * 2;
|
|
319
|
+
const sweepOptions = [minorSweep];
|
|
320
|
+
if (Math.abs(majorSweep) > 1e-9) {
|
|
321
|
+
sweepOptions.push(majorSweep);
|
|
322
|
+
}
|
|
323
|
+
for (const sweepAngle of sweepOptions) {
|
|
324
|
+
const endAngle = startAngle + sweepAngle;
|
|
325
|
+
const arc = new Arc(center, radius, startAngle, endAngle, n);
|
|
326
|
+
const midPoint = arc.pointAt(0.5);
|
|
327
|
+
candidates.push({
|
|
328
|
+
center,
|
|
329
|
+
radius,
|
|
330
|
+
startAngle,
|
|
331
|
+
endAngle,
|
|
332
|
+
sweepAngle,
|
|
333
|
+
points: arc.sample(minSegments),
|
|
334
|
+
score: midPoint.distanceTo(guide),
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
if (candidates.length === 0) {
|
|
339
|
+
return null;
|
|
340
|
+
}
|
|
341
|
+
candidates.sort((a, b) => a.score - b.score);
|
|
342
|
+
const winner = candidates[0];
|
|
343
|
+
return {
|
|
344
|
+
points: winner.points,
|
|
345
|
+
center: winner.center,
|
|
346
|
+
radius: winner.radius,
|
|
347
|
+
startAngle: winner.startAngle,
|
|
348
|
+
endAngle: winner.endAngle,
|
|
349
|
+
sweepAngle: winner.sweepAngle,
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
static resolveArcBasis(center, radius, normal) {
|
|
353
|
+
if (radius > 1e-12) {
|
|
354
|
+
const circle = new Circle(center, radius, normal);
|
|
355
|
+
const p0 = circle.pointAt(0);
|
|
356
|
+
const pQuarter = circle.pointAt(0.25);
|
|
357
|
+
const uRaw = p0.sub(center);
|
|
358
|
+
const vRaw = pQuarter.sub(center);
|
|
359
|
+
const uLen = uRaw.length();
|
|
360
|
+
const vLen = vRaw.length();
|
|
361
|
+
if (uLen > 1e-12 && vLen > 1e-12) {
|
|
362
|
+
return {
|
|
363
|
+
uAxis: uRaw.scale(1 / uLen),
|
|
364
|
+
vAxis: vRaw.scale(1 / vLen),
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
const n = normal.normalize();
|
|
369
|
+
const reference = Math.abs(n.z) < 0.9 ? Vec3.Z : Vec3.X;
|
|
370
|
+
const uRaw = n.cross(reference);
|
|
371
|
+
const uLen = uRaw.length();
|
|
372
|
+
if (uLen < 1e-12) {
|
|
373
|
+
return { uAxis: Vec3.X, vAxis: Vec3.Y };
|
|
374
|
+
}
|
|
375
|
+
const uAxis = uRaw.scale(1 / uLen);
|
|
376
|
+
const vRaw = n.cross(uAxis);
|
|
377
|
+
const vLen = vRaw.length();
|
|
378
|
+
if (vLen < 1e-12) {
|
|
379
|
+
return { uAxis, vAxis: Vec3.Y };
|
|
380
|
+
}
|
|
381
|
+
return { uAxis, vAxis: vRaw.scale(1 / vLen) };
|
|
382
|
+
}
|
|
383
|
+
static angleOnBasis(vector, uAxis, vAxis) {
|
|
384
|
+
return Math.atan2(vector.dot(vAxis), vector.dot(uAxis));
|
|
385
|
+
}
|
|
386
|
+
static normalizeSignedAngle(angle) {
|
|
387
|
+
const twoPi = Math.PI * 2;
|
|
388
|
+
let wrapped = angle % twoPi;
|
|
389
|
+
if (wrapped <= -Math.PI) {
|
|
390
|
+
wrapped += twoPi;
|
|
391
|
+
}
|
|
392
|
+
else if (wrapped > Math.PI) {
|
|
393
|
+
wrapped -= twoPi;
|
|
394
|
+
}
|
|
395
|
+
return wrapped;
|
|
396
|
+
}
|
|
397
|
+
static projectPointToPlane(point, planeOrigin, planeNormal) {
|
|
398
|
+
const offset = point.sub(planeOrigin);
|
|
399
|
+
const projected = offset.sub(planeNormal.scale(offset.dot(planeNormal)));
|
|
400
|
+
return planeOrigin.add(projected);
|
|
401
|
+
}
|
|
113
402
|
/**
|
|
114
403
|
* Create a planar patch mesh from boundary points using CDT (Constrained Delaunay Triangulation).
|
|
115
404
|
* Correctly handles both convex and concave polygons.
|
|
@@ -371,6 +660,35 @@ export class Mesh {
|
|
|
371
660
|
}
|
|
372
661
|
static mergeMeshes(meshes) {
|
|
373
662
|
ensureInit();
|
|
663
|
+
const packed = Mesh.packMeshes(meshes);
|
|
664
|
+
return Mesh.fromBuffer(wasm.mesh_merge(packed));
|
|
665
|
+
}
|
|
666
|
+
/**
|
|
667
|
+
* Raycast against many meshes and return all hits sorted by distance.
|
|
668
|
+
*/
|
|
669
|
+
static raycastMany(meshes, origin, direction, maxDistance = Number.POSITIVE_INFINITY) {
|
|
670
|
+
ensureInit();
|
|
671
|
+
if (meshes.length === 0)
|
|
672
|
+
return [];
|
|
673
|
+
const packed = Mesh.packMeshes(meshes);
|
|
674
|
+
const raycastMany = wasm.mesh_raycast_many;
|
|
675
|
+
const buf = raycastMany(packed, origin.x, origin.y, origin.z, direction.x, direction.y, direction.z, maxDistance);
|
|
676
|
+
const count = Math.max(0, Math.floor(buf[0] ?? 0));
|
|
677
|
+
const hits = [];
|
|
678
|
+
let off = 1;
|
|
679
|
+
for (let i = 0; i < count; i++) {
|
|
680
|
+
hits.push({
|
|
681
|
+
meshIndex: Math.floor(buf[off]),
|
|
682
|
+
point: new Point(buf[off + 1], buf[off + 2], buf[off + 3]),
|
|
683
|
+
normal: new Vec3(buf[off + 4], buf[off + 5], buf[off + 6]),
|
|
684
|
+
faceIndex: Math.floor(buf[off + 7]),
|
|
685
|
+
distance: buf[off + 8],
|
|
686
|
+
});
|
|
687
|
+
off += 9;
|
|
688
|
+
}
|
|
689
|
+
return hits;
|
|
690
|
+
}
|
|
691
|
+
static packMeshes(meshes) {
|
|
374
692
|
const totalLen = meshes.reduce((sum, m) => sum + 1 + m.rawBuffer.length, 1);
|
|
375
693
|
const packed = new Float64Array(totalLen);
|
|
376
694
|
packed[0] = meshes.length;
|
|
@@ -381,7 +699,7 @@ export class Mesh {
|
|
|
381
699
|
packed.set(raw, off);
|
|
382
700
|
off += raw.length;
|
|
383
701
|
}
|
|
384
|
-
return
|
|
702
|
+
return packed;
|
|
385
703
|
}
|
|
386
704
|
/**
|
|
387
705
|
* Unique undirected triangle edges as vertex-index pairs.
|
|
@@ -451,33 +769,107 @@ export class Mesh {
|
|
|
451
769
|
ensureInit();
|
|
452
770
|
return new Mesh(wasm.mesh_scale(this._vertexCount, this._buffer, sx, sy, sz));
|
|
453
771
|
}
|
|
772
|
+
runBoolean(other, operation, invoke, options) {
|
|
773
|
+
const faceCountA = this.faceCount;
|
|
774
|
+
const faceCountB = other.faceCount;
|
|
775
|
+
if (operation === "union") {
|
|
776
|
+
if (faceCountA === 0)
|
|
777
|
+
return Mesh.cloneMesh(other);
|
|
778
|
+
if (faceCountB === 0)
|
|
779
|
+
return Mesh.cloneMesh(this);
|
|
780
|
+
}
|
|
781
|
+
else if (operation === "intersection") {
|
|
782
|
+
if (faceCountA === 0 || faceCountB === 0)
|
|
783
|
+
return Mesh.emptyMesh();
|
|
784
|
+
}
|
|
785
|
+
else {
|
|
786
|
+
if (faceCountA === 0)
|
|
787
|
+
return Mesh.emptyMesh();
|
|
788
|
+
if (faceCountB === 0)
|
|
789
|
+
return Mesh.cloneMesh(this);
|
|
790
|
+
}
|
|
791
|
+
const boundsA = Mesh.computeRawBounds(this);
|
|
792
|
+
const boundsB = Mesh.computeRawBounds(other);
|
|
793
|
+
if (!Mesh.boundsOverlap(boundsA, boundsB)) {
|
|
794
|
+
if (operation === "union")
|
|
795
|
+
return Mesh.mergeMeshes([this, other]);
|
|
796
|
+
if (operation === "intersection")
|
|
797
|
+
return Mesh.emptyMesh();
|
|
798
|
+
return Mesh.cloneMesh(this);
|
|
799
|
+
}
|
|
800
|
+
if (!options?.allowUnsafe) {
|
|
801
|
+
const limits = Mesh.resolveBooleanLimits(options?.limits);
|
|
802
|
+
const maxInputFaces = Math.max(faceCountA, faceCountB);
|
|
803
|
+
const combinedInputFaces = faceCountA + faceCountB;
|
|
804
|
+
const faceProduct = faceCountA * faceCountB;
|
|
805
|
+
if (maxInputFaces > limits.maxInputFacesPerMesh
|
|
806
|
+
|| combinedInputFaces > limits.maxCombinedInputFaces
|
|
807
|
+
|| faceProduct > limits.maxFaceProduct) {
|
|
808
|
+
throw new Error(`Boolean ${operation} blocked by safety limits `
|
|
809
|
+
+ `(A faces=${faceCountA}, B faces=${faceCountB}, faceProduct=${faceProduct}). `
|
|
810
|
+
+ "Simplify inputs, run in a Worker, or pass allowUnsafe: true to force execution.");
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
const result = invoke();
|
|
814
|
+
if (operation === "union" && result.length === 0) {
|
|
815
|
+
throw new Error("Boolean union failed and returned an empty mesh buffer.");
|
|
816
|
+
}
|
|
817
|
+
return Mesh.fromBuffer(result);
|
|
818
|
+
}
|
|
454
819
|
// ── Booleans ───────────────────────────────────────────────────
|
|
455
820
|
/**
|
|
456
821
|
* Compute boolean union with another mesh.
|
|
457
822
|
* @param other - Mesh to union with
|
|
823
|
+
* @param options - Optional safety overrides
|
|
458
824
|
* @returns New mesh containing volume of both inputs
|
|
459
825
|
*/
|
|
460
|
-
union(other) {
|
|
826
|
+
union(other, options) {
|
|
461
827
|
ensureInit();
|
|
462
|
-
return
|
|
828
|
+
return this.runBoolean(other, "union", () => wasm.mesh_boolean_union(this._vertexCount, this._buffer, other._vertexCount, other._buffer), options);
|
|
463
829
|
}
|
|
464
830
|
/**
|
|
465
831
|
* Compute boolean subtraction with another mesh.
|
|
466
832
|
* @param other - Mesh to subtract
|
|
833
|
+
* @param options - Optional safety overrides
|
|
467
834
|
* @returns New mesh with other's volume removed from this
|
|
468
835
|
*/
|
|
469
|
-
subtract(other) {
|
|
836
|
+
subtract(other, options) {
|
|
470
837
|
ensureInit();
|
|
471
|
-
return
|
|
838
|
+
return this.runBoolean(other, "subtraction", () => wasm.mesh_boolean_subtraction(this._vertexCount, this._buffer, other._vertexCount, other._buffer), options);
|
|
472
839
|
}
|
|
473
840
|
/**
|
|
474
841
|
* Compute boolean intersection with another mesh.
|
|
475
842
|
* @param other - Mesh to intersect with
|
|
843
|
+
* @param options - Optional safety overrides
|
|
476
844
|
* @returns New mesh containing only the overlapping volume
|
|
477
845
|
*/
|
|
478
|
-
intersect(other) {
|
|
846
|
+
intersect(other, options) {
|
|
479
847
|
ensureInit();
|
|
480
|
-
return
|
|
848
|
+
return this.runBoolean(other, "intersection", () => wasm.mesh_boolean_intersection(this._vertexCount, this._buffer, other._vertexCount, other._buffer), options);
|
|
849
|
+
}
|
|
850
|
+
/**
|
|
851
|
+
* Compute boolean union in a dedicated Web Worker (non-blocking).
|
|
852
|
+
* Defaults to allowUnsafe=true so high-poly jobs can run off the UI thread.
|
|
853
|
+
*/
|
|
854
|
+
async unionAsync(other, options) {
|
|
855
|
+
const result = await runMeshBooleanInWorkerPool("union", this._buffer, other._buffer, options);
|
|
856
|
+
return Mesh.fromBuffer(result);
|
|
857
|
+
}
|
|
858
|
+
/**
|
|
859
|
+
* Compute boolean subtraction in a dedicated Web Worker (non-blocking).
|
|
860
|
+
* Defaults to allowUnsafe=true so high-poly jobs can run off the UI thread.
|
|
861
|
+
*/
|
|
862
|
+
async subtractAsync(other, options) {
|
|
863
|
+
const result = await runMeshBooleanInWorkerPool("subtraction", this._buffer, other._buffer, options);
|
|
864
|
+
return Mesh.fromBuffer(result);
|
|
865
|
+
}
|
|
866
|
+
/**
|
|
867
|
+
* Compute boolean intersection in a dedicated Web Worker (non-blocking).
|
|
868
|
+
* Defaults to allowUnsafe=true so high-poly jobs can run off the UI thread.
|
|
869
|
+
*/
|
|
870
|
+
async intersectAsync(other, options) {
|
|
871
|
+
const result = await runMeshBooleanInWorkerPool("intersection", this._buffer, other._buffer, options);
|
|
872
|
+
return Mesh.fromBuffer(result);
|
|
481
873
|
}
|
|
482
874
|
// ── Intersection queries ───────────────────────────────────────
|
|
483
875
|
/**
|
|
@@ -613,6 +1005,18 @@ export class Mesh {
|
|
|
613
1005
|
vMax: r[off + 3],
|
|
614
1006
|
};
|
|
615
1007
|
}
|
|
1008
|
+
/**
|
|
1009
|
+
* Centroid of the coplanar edge-connected face group containing faceIndex.
|
|
1010
|
+
*/
|
|
1011
|
+
getCoplanarFaceGroupCentroid(faceIndex) {
|
|
1012
|
+
ensureInit();
|
|
1013
|
+
if (!Number.isFinite(faceIndex) || faceIndex < 0)
|
|
1014
|
+
return null;
|
|
1015
|
+
const r = wasm.mesh_get_coplanar_face_group_centroid(this._vertexCount, this._buffer, Math.floor(faceIndex));
|
|
1016
|
+
if (!r || r.length < 3)
|
|
1017
|
+
return null;
|
|
1018
|
+
return new Point(r[0], r[1], r[2]);
|
|
1019
|
+
}
|
|
616
1020
|
/**
|
|
617
1021
|
* Unique edge count for this triangulated mesh.
|
|
618
1022
|
*/
|
|
@@ -703,66 +1107,28 @@ export class Mesh {
|
|
|
703
1107
|
findFaceByTriangleIndex(triangleIndex) {
|
|
704
1108
|
if (!Number.isFinite(triangleIndex) || triangleIndex < 0)
|
|
705
1109
|
return null;
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
1110
|
+
ensureInit();
|
|
1111
|
+
const r = wasm.mesh_find_face_group_by_triangle_index(this._vertexCount, this._buffer, Math.floor(triangleIndex));
|
|
1112
|
+
if (!r || r.length < 6)
|
|
1113
|
+
return null;
|
|
1114
|
+
return {
|
|
1115
|
+
centroid: new Point(r[0], r[1], r[2]),
|
|
1116
|
+
normal: new Vec3(r[3], r[4], r[5]),
|
|
1117
|
+
};
|
|
713
1118
|
}
|
|
714
1119
|
/**
|
|
715
1120
|
* Find the best matching coplanar + edge-connected face group by normal
|
|
716
1121
|
* similarity and optional point proximity.
|
|
717
1122
|
*/
|
|
718
1123
|
findFaceByNormal(targetNormal, nearPoint) {
|
|
719
|
-
const groups = this.buildCoplanarConnectedFaceGroups();
|
|
720
|
-
if (groups.length === 0)
|
|
721
|
-
return null;
|
|
722
|
-
let best = null;
|
|
723
|
-
let bestScore = Number.NEGATIVE_INFINITY;
|
|
724
|
-
for (const group of groups) {
|
|
725
|
-
const normal = group.normal;
|
|
726
|
-
const centroid = group.centroid;
|
|
727
|
-
const normalScore = Math.max(0, normal.dot(targetNormal));
|
|
728
|
-
let score;
|
|
729
|
-
if (nearPoint) {
|
|
730
|
-
const dx = centroid.x - nearPoint.x;
|
|
731
|
-
const dy = centroid.y - nearPoint.y;
|
|
732
|
-
const dz = centroid.z - nearPoint.z;
|
|
733
|
-
const distSq = dx * dx + dy * dy + dz * dz;
|
|
734
|
-
const proximityScore = 1.0 / (1.0 + distSq);
|
|
735
|
-
score = proximityScore * (0.5 + 0.5 * normalScore);
|
|
736
|
-
}
|
|
737
|
-
else {
|
|
738
|
-
score = normalScore;
|
|
739
|
-
}
|
|
740
|
-
if (score > bestScore) {
|
|
741
|
-
bestScore = score;
|
|
742
|
-
best = { centroid, normal };
|
|
743
|
-
}
|
|
744
|
-
}
|
|
745
|
-
return best;
|
|
746
|
-
}
|
|
747
|
-
buildCoplanarConnectedFaceGroups() {
|
|
748
1124
|
ensureInit();
|
|
749
|
-
const
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
for (let i = 0; i < triCount; i++) {
|
|
757
|
-
triangleIndices.push(Math.floor(buf[off++] ?? 0));
|
|
758
|
-
}
|
|
759
|
-
const centroid = new Point(buf[off], buf[off + 1], buf[off + 2]);
|
|
760
|
-
off += 3;
|
|
761
|
-
const normal = new Vec3(buf[off], buf[off + 1], buf[off + 2]);
|
|
762
|
-
off += 3;
|
|
763
|
-
groups.push({ triangleIndices, centroid, normal });
|
|
764
|
-
}
|
|
765
|
-
return groups;
|
|
1125
|
+
const r = wasm.mesh_find_face_group_by_normal(this._vertexCount, this._buffer, targetNormal.x, targetNormal.y, targetNormal.z, nearPoint?.x ?? 0, nearPoint?.y ?? 0, nearPoint?.z ?? 0, nearPoint !== undefined);
|
|
1126
|
+
if (!r || r.length < 6)
|
|
1127
|
+
return null;
|
|
1128
|
+
return {
|
|
1129
|
+
centroid: new Point(r[0], r[1], r[2]),
|
|
1130
|
+
normal: new Vec3(r[3], r[4], r[5]),
|
|
1131
|
+
};
|
|
766
1132
|
}
|
|
767
1133
|
// ── Export ──────────────────────────────────────────────────────
|
|
768
1134
|
/**
|
|
@@ -774,4 +1140,9 @@ export class Mesh {
|
|
|
774
1140
|
return wasm.mesh_export_obj(this._vertexCount, this._buffer);
|
|
775
1141
|
}
|
|
776
1142
|
}
|
|
1143
|
+
Mesh.DEFAULT_BOOLEAN_LIMITS = {
|
|
1144
|
+
maxInputFacesPerMesh: 120000,
|
|
1145
|
+
maxCombinedInputFaces: 180000,
|
|
1146
|
+
maxFaceProduct: 500000000,
|
|
1147
|
+
};
|
|
777
1148
|
//# sourceMappingURL=Mesh.js.map
|