okgeometry-api 1.1.23 → 1.2.1
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/Line.d.ts +10 -1
- package/dist/Line.d.ts.map +1 -1
- package/dist/Line.js +11 -0
- package/dist/Line.js.map +1 -1
- package/dist/Mesh.d.ts +134 -9
- package/dist/Mesh.d.ts.map +1 -1
- package/dist/Mesh.js +589 -26
- package/dist/Mesh.js.map +1 -1
- package/dist/MeshSurface.d.ts +32 -0
- package/dist/MeshSurface.d.ts.map +1 -0
- package/dist/MeshSurface.js +51 -0
- package/dist/MeshSurface.js.map +1 -0
- package/dist/NurbsCurve.d.ts +24 -2
- package/dist/NurbsCurve.d.ts.map +1 -1
- package/dist/NurbsCurve.js +34 -2
- package/dist/NurbsCurve.js.map +1 -1
- package/dist/NurbsSurface.d.ts +9 -1
- package/dist/NurbsSurface.d.ts.map +1 -1
- package/dist/NurbsSurface.js +12 -3
- package/dist/NurbsSurface.js.map +1 -1
- package/dist/PolyCurve.d.ts +21 -3
- package/dist/PolyCurve.d.ts.map +1 -1
- package/dist/PolyCurve.js +82 -38
- package/dist/PolyCurve.js.map +1 -1
- package/dist/Polygon.d.ts +13 -2
- package/dist/Polygon.d.ts.map +1 -1
- package/dist/Polygon.js +21 -3
- package/dist/Polygon.js.map +1 -1
- package/dist/Polyline.d.ts +19 -2
- package/dist/Polyline.d.ts.map +1 -1
- package/dist/Polyline.js +38 -6
- package/dist/Polyline.js.map +1 -1
- package/dist/Surface.d.ts +17 -0
- package/dist/Surface.d.ts.map +1 -0
- package/dist/Surface.js +2 -0
- package/dist/Surface.js.map +1 -0
- package/dist/index.d.ts +4 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/types.d.ts +13 -0
- package/dist/types.d.ts.map +1 -1
- 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/dist/wasm-bindings.d.ts +105 -2
- package/dist/wasm-bindings.d.ts.map +1 -1
- package/dist/wasm-bindings.js +180 -2
- package/dist/wasm-bindings.js.map +1 -1
- package/package.json +1 -1
- package/src/Line.ts +38 -20
- package/src/Mesh.ts +1382 -639
- package/src/MeshSurface.ts +72 -0
- package/src/NurbsCurve.ts +91 -37
- package/src/NurbsSurface.ts +28 -13
- package/src/PolyCurve.ts +157 -85
- package/src/Polygon.ts +34 -4
- package/src/Polyline.ts +74 -24
- package/src/Surface.ts +18 -0
- package/src/index.ts +9 -0
- package/src/types.ts +15 -0
- package/src/wasm-base64.ts +1 -1
- package/src/wasm-bindings.d.ts +55 -2
- package/src/wasm-bindings.js +189 -2
package/dist/Mesh.js
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { ensureInit } from "./engine.js";
|
|
2
2
|
import { Point } from "./Point.js";
|
|
3
3
|
import { Vec3 } from "./Vec3.js";
|
|
4
|
+
import { Plane } from "./Plane.js";
|
|
4
5
|
import { Polyline } from "./Polyline.js";
|
|
5
6
|
import { Line } from "./Line.js";
|
|
6
7
|
import { Circle } from "./Circle.js";
|
|
7
8
|
import { Arc } from "./Arc.js";
|
|
8
9
|
import { Polygon } from "./Polygon.js";
|
|
10
|
+
import { MeshSurface } from "./MeshSurface.js";
|
|
9
11
|
import { NurbsCurve } from "./NurbsCurve.js";
|
|
10
12
|
import { PolyCurve } from "./PolyCurve.js";
|
|
11
13
|
import { pointsToCoords, parsePolylineBuffer as parsePolylineBuf } from "./BufferCodec.js";
|
|
@@ -33,6 +35,10 @@ export class Mesh {
|
|
|
33
35
|
this._indexBuffer = null;
|
|
34
36
|
this._vertices = null;
|
|
35
37
|
this._faces = null;
|
|
38
|
+
this._planarFaces = null;
|
|
39
|
+
this._planarFacesByTriangleIndex = null;
|
|
40
|
+
this._surfaces = null;
|
|
41
|
+
this._surfacesByTriangleIndex = null;
|
|
36
42
|
this._edgeVertexPairs = null;
|
|
37
43
|
this._topologyMetricsCache = null;
|
|
38
44
|
this._isClosedVolumeCache = null;
|
|
@@ -116,6 +122,55 @@ export class Mesh {
|
|
|
116
122
|
const diag = Math.hypot(dx, dy, dz);
|
|
117
123
|
return Number.isFinite(diag) && diag > 0 ? diag : 1;
|
|
118
124
|
}
|
|
125
|
+
static computeSignedVolumeFromBuffer(buffer) {
|
|
126
|
+
const vertexCount = Math.max(0, Math.floor(buffer[0] ?? 0));
|
|
127
|
+
if (vertexCount <= 0)
|
|
128
|
+
return 0;
|
|
129
|
+
const indexStart = 1 + vertexCount * 3;
|
|
130
|
+
let signedVolume = 0;
|
|
131
|
+
for (let i = indexStart; i + 2 < buffer.length; i += 3) {
|
|
132
|
+
const ia = Math.floor(buffer[i] ?? -1);
|
|
133
|
+
const ib = Math.floor(buffer[i + 1] ?? -1);
|
|
134
|
+
const ic = Math.floor(buffer[i + 2] ?? -1);
|
|
135
|
+
if (ia < 0 || ib < 0 || ic < 0 || ia >= vertexCount || ib >= vertexCount || ic >= vertexCount)
|
|
136
|
+
continue;
|
|
137
|
+
const ax = buffer[1 + ia * 3];
|
|
138
|
+
const ay = buffer[1 + ia * 3 + 1];
|
|
139
|
+
const az = buffer[1 + ia * 3 + 2];
|
|
140
|
+
const bx = buffer[1 + ib * 3];
|
|
141
|
+
const by = buffer[1 + ib * 3 + 1];
|
|
142
|
+
const bz = buffer[1 + ib * 3 + 2];
|
|
143
|
+
const cx = buffer[1 + ic * 3];
|
|
144
|
+
const cy = buffer[1 + ic * 3 + 1];
|
|
145
|
+
const cz = buffer[1 + ic * 3 + 2];
|
|
146
|
+
signedVolume += ax * (by * cz - bz * cy)
|
|
147
|
+
- ay * (bx * cz - bz * cx)
|
|
148
|
+
+ az * (bx * cy - by * cx);
|
|
149
|
+
}
|
|
150
|
+
return signedVolume / 6;
|
|
151
|
+
}
|
|
152
|
+
static reverseTriangleWinding(buffer) {
|
|
153
|
+
const flipped = new Float64Array(buffer);
|
|
154
|
+
const vertexCount = Math.max(0, Math.floor(flipped[0] ?? 0));
|
|
155
|
+
const indexStart = 1 + vertexCount * 3;
|
|
156
|
+
for (let i = indexStart; i + 2 < flipped.length; i += 3) {
|
|
157
|
+
const tmp = flipped[i + 1];
|
|
158
|
+
flipped[i + 1] = flipped[i + 2];
|
|
159
|
+
flipped[i + 2] = tmp;
|
|
160
|
+
}
|
|
161
|
+
return flipped;
|
|
162
|
+
}
|
|
163
|
+
static normalizeClosedVolumeOrientation(mesh) {
|
|
164
|
+
if (mesh.faceCount === 0 || mesh._vertexCount <= 0)
|
|
165
|
+
return mesh;
|
|
166
|
+
const topology = mesh.topologyMetrics();
|
|
167
|
+
if (topology.boundaryEdges !== 0 || topology.nonManifoldEdges !== 0)
|
|
168
|
+
return mesh;
|
|
169
|
+
const signedVolume = Mesh.computeSignedVolumeFromBuffer(mesh._buffer);
|
|
170
|
+
if (!Number.isFinite(signedVolume) || signedVolume >= 0)
|
|
171
|
+
return mesh;
|
|
172
|
+
return new Mesh(Mesh.reverseTriangleWinding(mesh._buffer), mesh._trustedBooleanInput);
|
|
173
|
+
}
|
|
119
174
|
static booleanContactTolerance(a, b) {
|
|
120
175
|
const scale = Math.max(Mesh.boundsDiag(a), Mesh.boundsDiag(b));
|
|
121
176
|
return Math.min(1e-4, Math.max(1e-9, scale * 1e-6));
|
|
@@ -338,6 +393,165 @@ export class Mesh {
|
|
|
338
393
|
static emptyMesh() {
|
|
339
394
|
return Mesh.fromTrustedBuffer(new Float64Array(0));
|
|
340
395
|
}
|
|
396
|
+
static createSplitResult(outside, inside, classification = "volume") {
|
|
397
|
+
const result = {
|
|
398
|
+
outside,
|
|
399
|
+
inside,
|
|
400
|
+
all: outside.concat(inside),
|
|
401
|
+
classification,
|
|
402
|
+
};
|
|
403
|
+
if (classification === "surface") {
|
|
404
|
+
result.negative = outside;
|
|
405
|
+
result.positive = inside;
|
|
406
|
+
}
|
|
407
|
+
return result;
|
|
408
|
+
}
|
|
409
|
+
static createSurfaceSplitResult(negative, positive) {
|
|
410
|
+
return Mesh.createSplitResult(negative, positive, "surface");
|
|
411
|
+
}
|
|
412
|
+
static createPlaneSplitResult(negative, positive, classification = "surface") {
|
|
413
|
+
return {
|
|
414
|
+
...Mesh.createSplitResult(negative, positive, classification),
|
|
415
|
+
negative,
|
|
416
|
+
positive,
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
static parseSplitResultBuffer(buffer) {
|
|
420
|
+
if (buffer.length < 2) {
|
|
421
|
+
throw new Error("Mesh split failed and returned a corrupt split buffer.");
|
|
422
|
+
}
|
|
423
|
+
const outsideCount = Math.max(0, Math.floor(buffer[0] ?? 0));
|
|
424
|
+
const insideCount = Math.max(0, Math.floor(buffer[1] ?? 0));
|
|
425
|
+
let offset = 2;
|
|
426
|
+
const decodeGroup = (count) => {
|
|
427
|
+
const meshes = [];
|
|
428
|
+
for (let i = 0; i < count; i++) {
|
|
429
|
+
const rawLen = Math.floor(buffer[offset++] ?? -1);
|
|
430
|
+
if (rawLen < 0 || offset + rawLen > buffer.length) {
|
|
431
|
+
throw new Error("Mesh split failed and returned an invalid component buffer.");
|
|
432
|
+
}
|
|
433
|
+
meshes.push(Mesh.fromTrustedBuffer(buffer.slice(offset, offset + rawLen)));
|
|
434
|
+
offset += rawLen;
|
|
435
|
+
}
|
|
436
|
+
return meshes;
|
|
437
|
+
};
|
|
438
|
+
const outside = decodeGroup(outsideCount);
|
|
439
|
+
const inside = decodeGroup(insideCount);
|
|
440
|
+
if (offset !== buffer.length) {
|
|
441
|
+
throw new Error("Mesh split failed and returned trailing buffer data.");
|
|
442
|
+
}
|
|
443
|
+
return Mesh.createSplitResult(outside, inside);
|
|
444
|
+
}
|
|
445
|
+
static orthonormalPlaneFrame(plane) {
|
|
446
|
+
const normal = plane.normal.normalize();
|
|
447
|
+
let uAxis = plane.getXAxis().normalize();
|
|
448
|
+
if (uAxis.length() < 1e-9) {
|
|
449
|
+
uAxis = Math.abs(normal.x) > 0.9 ? Vec3.Y : Vec3.X;
|
|
450
|
+
uAxis = uAxis.sub(normal.scale(uAxis.dot(normal))).normalize();
|
|
451
|
+
}
|
|
452
|
+
const vAxis = normal.cross(uAxis).normalize();
|
|
453
|
+
return { uAxis, vAxis, normal };
|
|
454
|
+
}
|
|
455
|
+
static createOrientedBox(center, uAxis, vAxis, wAxis, halfU, halfV, halfW) {
|
|
456
|
+
const axes = [uAxis.normalize(), vAxis.normalize(), wAxis.normalize()];
|
|
457
|
+
const [u, v, w] = axes;
|
|
458
|
+
const corners = [
|
|
459
|
+
center.add(u.scale(-halfU)).add(v.scale(-halfV)).add(w.scale(-halfW)),
|
|
460
|
+
center.add(u.scale(halfU)).add(v.scale(-halfV)).add(w.scale(-halfW)),
|
|
461
|
+
center.add(u.scale(halfU)).add(v.scale(halfV)).add(w.scale(-halfW)),
|
|
462
|
+
center.add(u.scale(-halfU)).add(v.scale(halfV)).add(w.scale(-halfW)),
|
|
463
|
+
center.add(u.scale(-halfU)).add(v.scale(-halfV)).add(w.scale(halfW)),
|
|
464
|
+
center.add(u.scale(halfU)).add(v.scale(-halfV)).add(w.scale(halfW)),
|
|
465
|
+
center.add(u.scale(halfU)).add(v.scale(halfV)).add(w.scale(halfW)),
|
|
466
|
+
center.add(u.scale(-halfU)).add(v.scale(halfV)).add(w.scale(halfW)),
|
|
467
|
+
];
|
|
468
|
+
const indices = [
|
|
469
|
+
0, 2, 1, 0, 3, 2,
|
|
470
|
+
4, 5, 6, 4, 6, 7,
|
|
471
|
+
0, 1, 5, 0, 5, 4,
|
|
472
|
+
3, 7, 6, 3, 6, 2,
|
|
473
|
+
0, 4, 7, 0, 7, 3,
|
|
474
|
+
1, 2, 6, 1, 6, 5,
|
|
475
|
+
];
|
|
476
|
+
const raw = new Float64Array(1 + corners.length * 3 + indices.length);
|
|
477
|
+
raw[0] = corners.length;
|
|
478
|
+
let offset = 1;
|
|
479
|
+
for (const corner of corners) {
|
|
480
|
+
raw[offset++] = corner.x;
|
|
481
|
+
raw[offset++] = corner.y;
|
|
482
|
+
raw[offset++] = corner.z;
|
|
483
|
+
}
|
|
484
|
+
for (const index of indices) {
|
|
485
|
+
raw[offset++] = index;
|
|
486
|
+
}
|
|
487
|
+
return Mesh.fromBuffer(raw, { trustedBooleanInput: true });
|
|
488
|
+
}
|
|
489
|
+
static classifyPlaneDistances(mesh, plane) {
|
|
490
|
+
if (mesh.vertexCount <= 0)
|
|
491
|
+
return { min: 0, max: 0 };
|
|
492
|
+
let min = Number.POSITIVE_INFINITY;
|
|
493
|
+
let max = Number.NEGATIVE_INFINITY;
|
|
494
|
+
for (const vertex of mesh.vertices) {
|
|
495
|
+
const distance = plane.distanceTo(vertex);
|
|
496
|
+
if (distance < min)
|
|
497
|
+
min = distance;
|
|
498
|
+
if (distance > max)
|
|
499
|
+
max = distance;
|
|
500
|
+
}
|
|
501
|
+
return { min, max };
|
|
502
|
+
}
|
|
503
|
+
static createPlaneHalfSpaceCutter(host, plane, options) {
|
|
504
|
+
const bounds = Mesh.toDebugBounds(host.getBounds());
|
|
505
|
+
const scale = Number.isFinite(options?.planePaddingScale)
|
|
506
|
+
? Math.max(1.1, options?.planePaddingScale ?? 2.5)
|
|
507
|
+
: 2.5;
|
|
508
|
+
const radius = Math.max(1e-6, bounds.diagonal * 0.5);
|
|
509
|
+
const halfU = radius * scale;
|
|
510
|
+
const halfV = radius * scale;
|
|
511
|
+
const halfW = radius * scale;
|
|
512
|
+
const { uAxis, vAxis, normal } = Mesh.orthonormalPlaneFrame(plane);
|
|
513
|
+
const projectedCenter = plane.projectPoint(bounds.center);
|
|
514
|
+
const boxCenter = projectedCenter.add(normal.scale(halfW));
|
|
515
|
+
return Mesh.createOrientedBox(boxCenter, uAxis, vAxis, normal, halfU, halfV, halfW);
|
|
516
|
+
}
|
|
517
|
+
static detectClosedCurveNormal(curve, points) {
|
|
518
|
+
if (curve instanceof Circle || curve instanceof Arc) {
|
|
519
|
+
return curve.normal.normalize();
|
|
520
|
+
}
|
|
521
|
+
const normal = Mesh.computePlanarCurveNormal(points, true).normalize();
|
|
522
|
+
if (!Number.isFinite(normal.x) || !Number.isFinite(normal.y) || !Number.isFinite(normal.z) || normal.length() < 1e-9) {
|
|
523
|
+
throw new Error("Mesh.split(curve) requires a planar closed curve with a valid normal.");
|
|
524
|
+
}
|
|
525
|
+
return normal;
|
|
526
|
+
}
|
|
527
|
+
static createCurveSplitCutter(host, curve, options) {
|
|
528
|
+
const segments = Number.isFinite(options?.curveSegments)
|
|
529
|
+
? Math.max(3, Math.floor(options?.curveSegments ?? 32))
|
|
530
|
+
: 32;
|
|
531
|
+
const points = Mesh.getClosedCurveLoopPoints(curve, segments);
|
|
532
|
+
const normal = Mesh.detectClosedCurveNormal(curve, points);
|
|
533
|
+
let extrusionDirection = options?.direction;
|
|
534
|
+
let basePoints = points;
|
|
535
|
+
if (extrusionDirection) {
|
|
536
|
+
if (!Number.isFinite(extrusionDirection.length()) || extrusionDirection.length() < 1e-9) {
|
|
537
|
+
throw new Error("Mesh.split(curve) requires a non-zero direction when direction is provided.");
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
else {
|
|
541
|
+
const bounds = Mesh.toDebugBounds(host.getBounds());
|
|
542
|
+
const margin = Math.max(1e-6, bounds.diagonal * 0.25);
|
|
543
|
+
const distances = host.vertices.map((vertex) => normal.dot(vertex.sub(points[0])));
|
|
544
|
+
const minDistance = Math.min(...distances);
|
|
545
|
+
const maxDistance = Math.max(...distances);
|
|
546
|
+
const halfDepth = Math.max(Math.abs(minDistance), Math.abs(maxDistance)) + margin;
|
|
547
|
+
extrusionDirection = normal.scale(halfDepth * 2);
|
|
548
|
+
basePoints = points.map((point) => point.add(normal.scale(-halfDepth)));
|
|
549
|
+
}
|
|
550
|
+
const height = extrusionDirection.length();
|
|
551
|
+
const unit = extrusionDirection.normalize();
|
|
552
|
+
const prepared = Mesh.prepareBooleanCutterCurve(basePoints, true, unit, height);
|
|
553
|
+
return Mesh.extrudePlanarCurve(prepared.points, unit, prepared.height, true);
|
|
554
|
+
}
|
|
341
555
|
// ── GPU-ready buffers ──────────────────────────────────────────
|
|
342
556
|
/**
|
|
343
557
|
* Float32 xyz positions for Three.js BufferGeometry.
|
|
@@ -372,7 +586,7 @@ export class Mesh {
|
|
|
372
586
|
get vertexCount() {
|
|
373
587
|
return this._vertexCount;
|
|
374
588
|
}
|
|
375
|
-
/** Number of triangular faces in this mesh */
|
|
589
|
+
/** Number of triangular faces in this mesh. Use `planarFaceCount` for logical planar faces. */
|
|
376
590
|
get faceCount() {
|
|
377
591
|
const start = 1 + this._vertexCount * 3;
|
|
378
592
|
if (this._buffer.length <= start)
|
|
@@ -398,6 +612,7 @@ export class Mesh {
|
|
|
398
612
|
/**
|
|
399
613
|
* Get all faces as arrays of vertex indices.
|
|
400
614
|
* Each face is [i0, i1, i2] for the three triangle vertices.
|
|
615
|
+
* For coplanar edge-connected logical faces, use `planarFaces`.
|
|
401
616
|
* Lazy-computed and cached.
|
|
402
617
|
*/
|
|
403
618
|
get faces() {
|
|
@@ -411,6 +626,42 @@ export class Mesh {
|
|
|
411
626
|
}
|
|
412
627
|
return this._faces;
|
|
413
628
|
}
|
|
629
|
+
/**
|
|
630
|
+
* Alias for triangle faces, mirroring `faces`.
|
|
631
|
+
* Use `planarFaces` when you want logical mesh faces such as the 6 faces of a box.
|
|
632
|
+
*/
|
|
633
|
+
get triangles() {
|
|
634
|
+
return this.faces;
|
|
635
|
+
}
|
|
636
|
+
/**
|
|
637
|
+
* Number of coplanar edge-connected planar faces in this mesh.
|
|
638
|
+
* Examples: a box has 6, an L-shaped extrusion has 8.
|
|
639
|
+
*/
|
|
640
|
+
get planarFaceCount() {
|
|
641
|
+
return this.planarFaces.length;
|
|
642
|
+
}
|
|
643
|
+
/**
|
|
644
|
+
* Logical mesh faces built by grouping coplanar edge-connected triangles.
|
|
645
|
+
* Cached after the first query to keep repeated face access cheap.
|
|
646
|
+
*/
|
|
647
|
+
get planarFaces() {
|
|
648
|
+
this.ensurePlanarFaceCache();
|
|
649
|
+
return this._planarFaces ?? [];
|
|
650
|
+
}
|
|
651
|
+
/**
|
|
652
|
+
* Number of logical planar surfaces in this mesh.
|
|
653
|
+
* Mirrors `planarFaceCount`, but returns wrapper objects with surface queries.
|
|
654
|
+
*/
|
|
655
|
+
get surfaceCount() {
|
|
656
|
+
return this.surfaces.length;
|
|
657
|
+
}
|
|
658
|
+
/**
|
|
659
|
+
* Logical planar mesh surfaces with `evaluate(u, v)` and `normal(u, v)` methods.
|
|
660
|
+
*/
|
|
661
|
+
get surfaces() {
|
|
662
|
+
this.ensureSurfaceCache();
|
|
663
|
+
return this._surfaces ?? [];
|
|
664
|
+
}
|
|
414
665
|
/** Raw WASM buffer (for advanced use / re-passing to WASM) */
|
|
415
666
|
get rawBuffer() {
|
|
416
667
|
return this._buffer;
|
|
@@ -425,11 +676,111 @@ export class Mesh {
|
|
|
425
676
|
return new Mesh(buffer, options?.trustedBooleanInput ?? false);
|
|
426
677
|
}
|
|
427
678
|
static fromTrustedBuffer(buffer) {
|
|
428
|
-
return new Mesh(buffer, true);
|
|
679
|
+
return Mesh.normalizeClosedVolumeOrientation(new Mesh(buffer, true));
|
|
429
680
|
}
|
|
430
681
|
get trustedBooleanInput() {
|
|
431
682
|
return this._trustedBooleanInput;
|
|
432
683
|
}
|
|
684
|
+
getTriangleVertexIndices(faceIndex) {
|
|
685
|
+
if (!Number.isFinite(faceIndex) || faceIndex < 0 || faceIndex >= this.faceCount)
|
|
686
|
+
return null;
|
|
687
|
+
const indexBuffer = this.indexBuffer;
|
|
688
|
+
const offset = Math.floor(faceIndex) * 3;
|
|
689
|
+
if (offset + 2 >= indexBuffer.length)
|
|
690
|
+
return null;
|
|
691
|
+
return [indexBuffer[offset], indexBuffer[offset + 1], indexBuffer[offset + 2]];
|
|
692
|
+
}
|
|
693
|
+
computeTriangleAreaByIndex(faceIndex) {
|
|
694
|
+
const tri = this.getTriangleVertexIndices(faceIndex);
|
|
695
|
+
if (!tri)
|
|
696
|
+
return 0;
|
|
697
|
+
const [i0, i1, i2] = tri;
|
|
698
|
+
const base = 1;
|
|
699
|
+
const off0 = base + i0 * 3;
|
|
700
|
+
const off1 = base + i1 * 3;
|
|
701
|
+
const off2 = base + i2 * 3;
|
|
702
|
+
const ax = this._buffer[off0] ?? 0;
|
|
703
|
+
const ay = this._buffer[off0 + 1] ?? 0;
|
|
704
|
+
const az = this._buffer[off0 + 2] ?? 0;
|
|
705
|
+
const bx = this._buffer[off1] ?? 0;
|
|
706
|
+
const by = this._buffer[off1 + 1] ?? 0;
|
|
707
|
+
const bz = this._buffer[off1 + 2] ?? 0;
|
|
708
|
+
const cx = this._buffer[off2] ?? 0;
|
|
709
|
+
const cy = this._buffer[off2 + 1] ?? 0;
|
|
710
|
+
const cz = this._buffer[off2 + 2] ?? 0;
|
|
711
|
+
const abx = bx - ax;
|
|
712
|
+
const aby = by - ay;
|
|
713
|
+
const abz = bz - az;
|
|
714
|
+
const acx = cx - ax;
|
|
715
|
+
const acy = cy - ay;
|
|
716
|
+
const acz = cz - az;
|
|
717
|
+
const crossX = aby * acz - abz * acy;
|
|
718
|
+
const crossY = abz * acx - abx * acz;
|
|
719
|
+
const crossZ = abx * acy - aby * acx;
|
|
720
|
+
return 0.5 * Math.sqrt(crossX * crossX + crossY * crossY + crossZ * crossZ);
|
|
721
|
+
}
|
|
722
|
+
ensurePlanarFaceCache() {
|
|
723
|
+
if (this._planarFaces && this._planarFacesByTriangleIndex)
|
|
724
|
+
return;
|
|
725
|
+
ensureInit();
|
|
726
|
+
const raw = wasm.mesh_build_coplanar_connected_face_groups(this._vertexCount, this._buffer);
|
|
727
|
+
const declaredGroupCount = Math.max(0, Math.floor(raw[0] ?? 0));
|
|
728
|
+
const planarFaces = [];
|
|
729
|
+
const planarFacesByTriangleIndex = new Array(this.faceCount).fill(null);
|
|
730
|
+
let offset = 1;
|
|
731
|
+
for (let groupIndex = 0; groupIndex < declaredGroupCount && offset < raw.length; groupIndex++) {
|
|
732
|
+
const declaredTriangleCount = Math.max(0, Math.floor(raw[offset] ?? 0));
|
|
733
|
+
offset += 1;
|
|
734
|
+
const triangleIndices = [];
|
|
735
|
+
let area = 0;
|
|
736
|
+
for (let i = 0; i < declaredTriangleCount && offset < raw.length; i++, offset++) {
|
|
737
|
+
const triangleIndex = Math.floor(raw[offset] ?? -1);
|
|
738
|
+
if (triangleIndex < 0 || triangleIndex >= this.faceCount)
|
|
739
|
+
continue;
|
|
740
|
+
triangleIndices.push(triangleIndex);
|
|
741
|
+
area += this.computeTriangleAreaByIndex(triangleIndex);
|
|
742
|
+
}
|
|
743
|
+
if (offset + 5 >= raw.length)
|
|
744
|
+
break;
|
|
745
|
+
const centroid = new Point(raw[offset] ?? 0, raw[offset + 1] ?? 0, raw[offset + 2] ?? 0);
|
|
746
|
+
const normal = new Vec3(raw[offset + 3] ?? 0, raw[offset + 4] ?? 0, raw[offset + 5] ?? 0).normalize();
|
|
747
|
+
offset += 6;
|
|
748
|
+
if (triangleIndices.length === 0)
|
|
749
|
+
continue;
|
|
750
|
+
const planarFace = {
|
|
751
|
+
index: planarFaces.length,
|
|
752
|
+
seedTriangleIndex: triangleIndices[0],
|
|
753
|
+
triangleIndices,
|
|
754
|
+
triangleCount: triangleIndices.length,
|
|
755
|
+
centroid,
|
|
756
|
+
normal,
|
|
757
|
+
area,
|
|
758
|
+
};
|
|
759
|
+
planarFaces.push(planarFace);
|
|
760
|
+
for (const triangleIndex of triangleIndices) {
|
|
761
|
+
planarFacesByTriangleIndex[triangleIndex] = planarFace;
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
this._planarFaces = planarFaces;
|
|
765
|
+
this._planarFacesByTriangleIndex = planarFacesByTriangleIndex;
|
|
766
|
+
this._surfaces = null;
|
|
767
|
+
this._surfacesByTriangleIndex = null;
|
|
768
|
+
}
|
|
769
|
+
ensureSurfaceCache() {
|
|
770
|
+
if (this._surfaces && this._surfacesByTriangleIndex)
|
|
771
|
+
return;
|
|
772
|
+
const surfaces = this.planarFaces.map(face => new MeshSurface(this, face));
|
|
773
|
+
const surfacesByTriangleIndex = new Array(this.faceCount).fill(null);
|
|
774
|
+
for (const surface of surfaces) {
|
|
775
|
+
for (const triangleIndex of surface.triangleIndices) {
|
|
776
|
+
if (triangleIndex >= 0 && triangleIndex < surfacesByTriangleIndex.length) {
|
|
777
|
+
surfacesByTriangleIndex[triangleIndex] = surface;
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
this._surfaces = surfaces;
|
|
782
|
+
this._surfacesByTriangleIndex = surfacesByTriangleIndex;
|
|
783
|
+
}
|
|
433
784
|
debugSummary() {
|
|
434
785
|
const bounds = Mesh.toDebugBounds(this.getBounds());
|
|
435
786
|
const topology = this.topologyMetrics();
|
|
@@ -1138,6 +1489,34 @@ export class Mesh {
|
|
|
1138
1489
|
}
|
|
1139
1490
|
return Mesh.fromTrustedBuffer(result);
|
|
1140
1491
|
}
|
|
1492
|
+
static mergeMeshComponents(meshes) {
|
|
1493
|
+
if (meshes.length === 0)
|
|
1494
|
+
return Mesh.emptyMesh();
|
|
1495
|
+
if (meshes.length === 1)
|
|
1496
|
+
return Mesh.cloneMesh(meshes[0]);
|
|
1497
|
+
return Mesh.mergeMeshes(meshes);
|
|
1498
|
+
}
|
|
1499
|
+
static createUnsupportedOpenMeshBooleanError(operation, aClosed, bClosed) {
|
|
1500
|
+
const topology = `A=${aClosed ? "closed" : "open"}, B=${bClosed ? "closed" : "open"}`;
|
|
1501
|
+
if (operation === "union") {
|
|
1502
|
+
return new Error(`Boolean union requires both inputs to be closed volumes (${topology}). `
|
|
1503
|
+
+ "Close or cap the open mesh before calling union().");
|
|
1504
|
+
}
|
|
1505
|
+
return new Error(`Boolean ${operation} with open meshes is only supported when A is open and B is closed `
|
|
1506
|
+
+ `(surface trim mode). Got ${topology}. Use Mesh.split(...) and choose outside/inside explicitly `
|
|
1507
|
+
+ "for other open-mesh combinations.");
|
|
1508
|
+
}
|
|
1509
|
+
resolveOpenMeshBooleanFallback(other, operation, options) {
|
|
1510
|
+
const aClosed = this.isClosedVolume();
|
|
1511
|
+
const bClosed = other.isClosedVolume();
|
|
1512
|
+
if (aClosed && bClosed)
|
|
1513
|
+
return null;
|
|
1514
|
+
if (!aClosed && bClosed && (operation === "subtraction" || operation === "intersection")) {
|
|
1515
|
+
const split = this.splitWithMesh(other, options);
|
|
1516
|
+
return Mesh.mergeMeshComponents(operation === "subtraction" ? split.outside : split.inside);
|
|
1517
|
+
}
|
|
1518
|
+
throw Mesh.createUnsupportedOpenMeshBooleanError(operation, aClosed, bClosed);
|
|
1519
|
+
}
|
|
1141
1520
|
static encodeBooleanOperationToken(operation, a, b, options) {
|
|
1142
1521
|
const tokens = [operation];
|
|
1143
1522
|
if (a._trustedBooleanInput && b._trustedBooleanInput) {
|
|
@@ -1148,39 +1527,157 @@ export class Mesh {
|
|
|
1148
1527
|
}
|
|
1149
1528
|
return tokens.join("@");
|
|
1150
1529
|
}
|
|
1530
|
+
static encodeBooleanSplitToken(a, b, options) {
|
|
1531
|
+
const tokens = ["split"];
|
|
1532
|
+
if (a._trustedBooleanInput && b._trustedBooleanInput) {
|
|
1533
|
+
tokens.push("trustedInput");
|
|
1534
|
+
}
|
|
1535
|
+
if (options?.debugForceFaceID) {
|
|
1536
|
+
tokens.push("forceFaceID");
|
|
1537
|
+
}
|
|
1538
|
+
return tokens.join("@");
|
|
1539
|
+
}
|
|
1151
1540
|
// ── Booleans ───────────────────────────────────────────────────
|
|
1152
1541
|
/**
|
|
1153
1542
|
* Compute boolean union with another mesh.
|
|
1543
|
+
* Requires both inputs to be closed volumes.
|
|
1154
1544
|
* @param other - Mesh to union with
|
|
1155
1545
|
* @param options - Optional safety overrides
|
|
1156
1546
|
* @returns New mesh containing volume of both inputs
|
|
1157
1547
|
*/
|
|
1158
1548
|
union(other, options) {
|
|
1159
1549
|
ensureInit();
|
|
1160
|
-
const
|
|
1161
|
-
|
|
1550
|
+
const lhs = Mesh.normalizeClosedVolumeOrientation(this);
|
|
1551
|
+
const rhs = Mesh.normalizeClosedVolumeOrientation(other);
|
|
1552
|
+
lhs.resolveOpenMeshBooleanFallback(rhs, "union", options);
|
|
1553
|
+
const operationToken = Mesh.encodeBooleanOperationToken("union", lhs, rhs, options);
|
|
1554
|
+
return lhs.runBoolean(rhs, "union", () => wasm.mesh_boolean_operation(lhs._vertexCount, lhs._buffer, rhs._vertexCount, rhs._buffer, operationToken), options);
|
|
1162
1555
|
}
|
|
1163
1556
|
/**
|
|
1164
1557
|
* Compute boolean subtraction with another mesh.
|
|
1558
|
+
* If this mesh is open and `other` is a closed cutter, this trims the open
|
|
1559
|
+
* host via `split()` and returns the outside surface pieces merged together.
|
|
1165
1560
|
* @param other - Mesh to subtract
|
|
1166
1561
|
* @param options - Optional safety overrides
|
|
1167
1562
|
* @returns New mesh with other's volume removed from this
|
|
1168
1563
|
*/
|
|
1169
1564
|
subtract(other, options) {
|
|
1170
1565
|
ensureInit();
|
|
1171
|
-
const
|
|
1172
|
-
|
|
1566
|
+
const lhs = Mesh.normalizeClosedVolumeOrientation(this);
|
|
1567
|
+
const rhs = Mesh.normalizeClosedVolumeOrientation(other);
|
|
1568
|
+
const fallback = lhs.resolveOpenMeshBooleanFallback(rhs, "subtraction", options);
|
|
1569
|
+
if (fallback)
|
|
1570
|
+
return fallback;
|
|
1571
|
+
const operationToken = Mesh.encodeBooleanOperationToken("subtraction", lhs, rhs, options);
|
|
1572
|
+
return lhs.runBoolean(rhs, "subtraction", () => wasm.mesh_boolean_operation(lhs._vertexCount, lhs._buffer, rhs._vertexCount, rhs._buffer, operationToken), options);
|
|
1173
1573
|
}
|
|
1174
1574
|
/**
|
|
1175
1575
|
* Compute boolean intersection with another mesh.
|
|
1576
|
+
* If this mesh is open and `other` is a closed cutter, this trims the open
|
|
1577
|
+
* host via `split()` and returns the inside surface pieces merged together.
|
|
1176
1578
|
* @param other - Mesh to intersect with
|
|
1177
1579
|
* @param options - Optional safety overrides
|
|
1178
1580
|
* @returns New mesh containing only the overlapping volume
|
|
1179
1581
|
*/
|
|
1180
1582
|
intersect(other, options) {
|
|
1181
1583
|
ensureInit();
|
|
1182
|
-
const
|
|
1183
|
-
|
|
1584
|
+
const lhs = Mesh.normalizeClosedVolumeOrientation(this);
|
|
1585
|
+
const rhs = Mesh.normalizeClosedVolumeOrientation(other);
|
|
1586
|
+
const fallback = lhs.resolveOpenMeshBooleanFallback(rhs, "intersection", options);
|
|
1587
|
+
if (fallback)
|
|
1588
|
+
return fallback;
|
|
1589
|
+
const operationToken = Mesh.encodeBooleanOperationToken("intersection", lhs, rhs, options);
|
|
1590
|
+
return lhs.runBoolean(rhs, "intersection", () => wasm.mesh_boolean_operation(lhs._vertexCount, lhs._buffer, rhs._vertexCount, rhs._buffer, operationToken), options);
|
|
1591
|
+
}
|
|
1592
|
+
splitWithMesh(other, options) {
|
|
1593
|
+
ensureInit();
|
|
1594
|
+
const faceCountA = this.faceCount;
|
|
1595
|
+
const faceCountB = other.faceCount;
|
|
1596
|
+
const hostClosed = this.isClosedVolume();
|
|
1597
|
+
const cutterClosed = other.isClosedVolume();
|
|
1598
|
+
const useVolumetricSplit = hostClosed && cutterClosed;
|
|
1599
|
+
if (faceCountA === 0)
|
|
1600
|
+
return Mesh.createSplitResult([], [], useVolumetricSplit ? "volume" : "surface");
|
|
1601
|
+
if (faceCountB === 0) {
|
|
1602
|
+
return Mesh.createSplitResult([Mesh.cloneMesh(this)], [], useVolumetricSplit || cutterClosed ? "volume" : "surface");
|
|
1603
|
+
}
|
|
1604
|
+
const boundsA = Mesh.computeRawBounds(this);
|
|
1605
|
+
const boundsB = Mesh.computeRawBounds(other);
|
|
1606
|
+
const contactTol = Mesh.booleanContactTolerance(boundsA, boundsB);
|
|
1607
|
+
if (useVolumetricSplit && !Mesh.boundsOverlap(boundsA, boundsB, contactTol)) {
|
|
1608
|
+
return Mesh.createSplitResult([Mesh.cloneMesh(this)], [], "volume");
|
|
1609
|
+
}
|
|
1610
|
+
if (!useVolumetricSplit && cutterClosed && !Mesh.boundsOverlap(boundsA, boundsB, contactTol)) {
|
|
1611
|
+
return Mesh.createSplitResult([Mesh.cloneMesh(this)], [], "volume");
|
|
1612
|
+
}
|
|
1613
|
+
if (!options?.allowUnsafe) {
|
|
1614
|
+
const limits = Mesh.resolveBooleanLimits(options?.limits);
|
|
1615
|
+
const maxInputFaces = Math.max(faceCountA, faceCountB);
|
|
1616
|
+
const combinedInputFaces = faceCountA + faceCountB;
|
|
1617
|
+
const faceProduct = faceCountA * faceCountB;
|
|
1618
|
+
if (maxInputFaces > limits.maxInputFacesPerMesh
|
|
1619
|
+
|| combinedInputFaces > limits.maxCombinedInputFaces
|
|
1620
|
+
|| faceProduct > limits.maxFaceProduct) {
|
|
1621
|
+
throw new Error(`Mesh split blocked by safety limits `
|
|
1622
|
+
+ `(A faces=${faceCountA}, B faces=${faceCountB}, faceProduct=${faceProduct}). `
|
|
1623
|
+
+ "Simplify inputs or pass allowUnsafe: true to force execution.");
|
|
1624
|
+
}
|
|
1625
|
+
}
|
|
1626
|
+
const splitToken = Mesh.encodeBooleanSplitToken(this, other, options);
|
|
1627
|
+
const result = useVolumetricSplit
|
|
1628
|
+
? wasm.mesh_boolean_split(this._vertexCount, this._buffer, other._vertexCount, other._buffer, splitToken)
|
|
1629
|
+
: wasm.mesh_surface_split(this._vertexCount, this._buffer, other._vertexCount, other._buffer, splitToken);
|
|
1630
|
+
if (result.length === 0) {
|
|
1631
|
+
throw new Error("Mesh split failed and returned an invalid result buffer.");
|
|
1632
|
+
}
|
|
1633
|
+
const parsed = Mesh.parseSplitResultBuffer(result);
|
|
1634
|
+
return cutterClosed
|
|
1635
|
+
? Mesh.createSplitResult(parsed.outside, parsed.inside, "volume")
|
|
1636
|
+
: Mesh.createSurfaceSplitResult(parsed.outside, parsed.inside);
|
|
1637
|
+
}
|
|
1638
|
+
splitWithPlane(plane, options) {
|
|
1639
|
+
ensureInit();
|
|
1640
|
+
const hostClosed = this.isClosedVolume();
|
|
1641
|
+
if (this.faceCount === 0)
|
|
1642
|
+
return Mesh.createPlaneSplitResult([], [], hostClosed ? "volume" : "surface");
|
|
1643
|
+
const distances = Mesh.classifyPlaneDistances(this, plane);
|
|
1644
|
+
const eps = Mesh.boundsDiag(Mesh.computeRawBounds(this)) * 1e-9 + 1e-9;
|
|
1645
|
+
if (distances.max <= eps) {
|
|
1646
|
+
return Mesh.createPlaneSplitResult([Mesh.cloneMesh(this)], [], hostClosed ? "volume" : "surface");
|
|
1647
|
+
}
|
|
1648
|
+
if (distances.min >= -eps) {
|
|
1649
|
+
return Mesh.createPlaneSplitResult([], [Mesh.cloneMesh(this)], hostClosed ? "volume" : "surface");
|
|
1650
|
+
}
|
|
1651
|
+
if (!hostClosed) {
|
|
1652
|
+
const result = wasm.mesh_surface_split_plane(this._vertexCount, this._buffer, plane.normal.x, plane.normal.y, plane.normal.z, plane.d, Mesh.encodeBooleanSplitToken(this, this, options));
|
|
1653
|
+
if (result.length === 0) {
|
|
1654
|
+
throw new Error("Mesh split by plane failed and returned an invalid result buffer.");
|
|
1655
|
+
}
|
|
1656
|
+
const split = Mesh.parseSplitResultBuffer(result);
|
|
1657
|
+
return Mesh.createPlaneSplitResult(split.outside, split.inside, "surface");
|
|
1658
|
+
}
|
|
1659
|
+
const result = wasm.mesh_solid_split_plane(this._vertexCount, this._buffer, plane.normal.x, plane.normal.y, plane.normal.z, plane.d, Mesh.encodeBooleanSplitToken(this, this, options));
|
|
1660
|
+
if (result.length === 0) {
|
|
1661
|
+
throw new Error("Mesh split by plane failed and returned an invalid result buffer.");
|
|
1662
|
+
}
|
|
1663
|
+
const split = Mesh.parseSplitResultBuffer(result);
|
|
1664
|
+
return Mesh.createPlaneSplitResult(split.outside, split.inside, "volume");
|
|
1665
|
+
}
|
|
1666
|
+
splitWithCurve(curve, options) {
|
|
1667
|
+
ensureInit();
|
|
1668
|
+
if (this.faceCount === 0)
|
|
1669
|
+
return Mesh.createSplitResult([], []);
|
|
1670
|
+
const cutter = Mesh.createCurveSplitCutter(this, curve, options);
|
|
1671
|
+
return this.splitWithMesh(cutter, options);
|
|
1672
|
+
}
|
|
1673
|
+
split(target, options) {
|
|
1674
|
+
if (target instanceof Mesh) {
|
|
1675
|
+
return this.splitWithMesh(target, options);
|
|
1676
|
+
}
|
|
1677
|
+
if (target instanceof Plane) {
|
|
1678
|
+
return this.splitWithPlane(target, options);
|
|
1679
|
+
}
|
|
1680
|
+
return this.splitWithCurve(target, options);
|
|
1184
1681
|
}
|
|
1185
1682
|
debugBoolean(other, operation, options, debugOptions) {
|
|
1186
1683
|
const result = operation === "union"
|
|
@@ -1581,6 +2078,88 @@ export class Mesh {
|
|
|
1581
2078
|
nonManifoldEdges,
|
|
1582
2079
|
};
|
|
1583
2080
|
}
|
|
2081
|
+
/**
|
|
2082
|
+
* Get a logical planar face by its planar-face index.
|
|
2083
|
+
*/
|
|
2084
|
+
getPlanarFace(index) {
|
|
2085
|
+
if (!Number.isFinite(index) || index < 0)
|
|
2086
|
+
return null;
|
|
2087
|
+
const planarFaces = this.planarFaces;
|
|
2088
|
+
return planarFaces[Math.floor(index)] ?? null;
|
|
2089
|
+
}
|
|
2090
|
+
/**
|
|
2091
|
+
* Get a logical planar surface wrapper by its planar-face index.
|
|
2092
|
+
*/
|
|
2093
|
+
getSurface(index) {
|
|
2094
|
+
if (!Number.isFinite(index) || index < 0)
|
|
2095
|
+
return null;
|
|
2096
|
+
const surfaces = this.surfaces;
|
|
2097
|
+
return surfaces[Math.floor(index)] ?? null;
|
|
2098
|
+
}
|
|
2099
|
+
/**
|
|
2100
|
+
* Get the logical planar face that contains a triangle face index.
|
|
2101
|
+
*/
|
|
2102
|
+
getPlanarFaceByTriangleIndex(triangleIndex) {
|
|
2103
|
+
if (!Number.isFinite(triangleIndex) || triangleIndex < 0)
|
|
2104
|
+
return null;
|
|
2105
|
+
this.ensurePlanarFaceCache();
|
|
2106
|
+
return this._planarFacesByTriangleIndex?.[Math.floor(triangleIndex)] ?? null;
|
|
2107
|
+
}
|
|
2108
|
+
/**
|
|
2109
|
+
* Get the logical planar surface wrapper that contains a triangle face index.
|
|
2110
|
+
*/
|
|
2111
|
+
getSurfaceByTriangleIndex(triangleIndex) {
|
|
2112
|
+
if (!Number.isFinite(triangleIndex) || triangleIndex < 0)
|
|
2113
|
+
return null;
|
|
2114
|
+
this.ensureSurfaceCache();
|
|
2115
|
+
return this._surfacesByTriangleIndex?.[Math.floor(triangleIndex)] ?? null;
|
|
2116
|
+
}
|
|
2117
|
+
/**
|
|
2118
|
+
* Find the best matching logical planar face by normal and optional centroid proximity.
|
|
2119
|
+
*/
|
|
2120
|
+
findPlanarFaceByNormal(targetNormal, nearPoint) {
|
|
2121
|
+
const planarFaces = this.planarFaces;
|
|
2122
|
+
if (planarFaces.length === 0)
|
|
2123
|
+
return null;
|
|
2124
|
+
const normalizedTarget = targetNormal.normalize();
|
|
2125
|
+
if (normalizedTarget.length() < 1e-12)
|
|
2126
|
+
return null;
|
|
2127
|
+
const alignmentEps = 1e-9;
|
|
2128
|
+
const distanceEps = 1e-9;
|
|
2129
|
+
let best = null;
|
|
2130
|
+
let bestAlignment = -Infinity;
|
|
2131
|
+
let bestDistance = Number.POSITIVE_INFINITY;
|
|
2132
|
+
let bestArea = -Infinity;
|
|
2133
|
+
for (const planarFace of planarFaces) {
|
|
2134
|
+
const alignment = planarFace.normal.dot(normalizedTarget);
|
|
2135
|
+
const distance = nearPoint ? planarFace.centroid.distanceTo(nearPoint) : 0;
|
|
2136
|
+
const betterAlignment = alignment > bestAlignment + alignmentEps;
|
|
2137
|
+
const similarAlignment = Math.abs(alignment - bestAlignment) <= alignmentEps;
|
|
2138
|
+
const betterDistance = nearPoint !== undefined && distance < bestDistance - distanceEps;
|
|
2139
|
+
const similarDistance = Math.abs(distance - bestDistance) <= distanceEps;
|
|
2140
|
+
const betterArea = planarFace.area > bestArea + distanceEps;
|
|
2141
|
+
if (best === null
|
|
2142
|
+
|| betterAlignment
|
|
2143
|
+
|| (similarAlignment && betterDistance)
|
|
2144
|
+
|| (similarAlignment && (!nearPoint || similarDistance) && betterArea)) {
|
|
2145
|
+
best = planarFace;
|
|
2146
|
+
bestAlignment = alignment;
|
|
2147
|
+
bestDistance = distance;
|
|
2148
|
+
bestArea = planarFace.area;
|
|
2149
|
+
}
|
|
2150
|
+
}
|
|
2151
|
+
return best;
|
|
2152
|
+
}
|
|
2153
|
+
/**
|
|
2154
|
+
* Find the best matching logical planar surface wrapper by normal
|
|
2155
|
+
* similarity and optional point proximity.
|
|
2156
|
+
*/
|
|
2157
|
+
findSurfaceByNormal(targetNormal, nearPoint) {
|
|
2158
|
+
const face = this.findPlanarFaceByNormal(targetNormal, nearPoint);
|
|
2159
|
+
if (!face)
|
|
2160
|
+
return null;
|
|
2161
|
+
return this.getSurface(face.index);
|
|
2162
|
+
}
|
|
1584
2163
|
/**
|
|
1585
2164
|
* Odd/even point containment test against a closed mesh.
|
|
1586
2165
|
* Uses majority vote across multiple ray directions for robustness.
|
|
@@ -1593,30 +2172,14 @@ export class Mesh {
|
|
|
1593
2172
|
* Find the coplanar + edge-connected face group containing a triangle.
|
|
1594
2173
|
*/
|
|
1595
2174
|
findFaceByTriangleIndex(triangleIndex) {
|
|
1596
|
-
|
|
1597
|
-
return null;
|
|
1598
|
-
ensureInit();
|
|
1599
|
-
const r = wasm.mesh_find_face_group_by_triangle_index(this._vertexCount, this._buffer, Math.floor(triangleIndex));
|
|
1600
|
-
if (!r || r.length < 6)
|
|
1601
|
-
return null;
|
|
1602
|
-
return {
|
|
1603
|
-
centroid: new Point(r[0], r[1], r[2]),
|
|
1604
|
-
normal: new Vec3(r[3], r[4], r[5]),
|
|
1605
|
-
};
|
|
2175
|
+
return this.getPlanarFaceByTriangleIndex(triangleIndex);
|
|
1606
2176
|
}
|
|
1607
2177
|
/**
|
|
1608
2178
|
* Find the best matching coplanar + edge-connected face group by normal
|
|
1609
2179
|
* similarity and optional point proximity.
|
|
1610
2180
|
*/
|
|
1611
2181
|
findFaceByNormal(targetNormal, nearPoint) {
|
|
1612
|
-
|
|
1613
|
-
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);
|
|
1614
|
-
if (!r || r.length < 6)
|
|
1615
|
-
return null;
|
|
1616
|
-
return {
|
|
1617
|
-
centroid: new Point(r[0], r[1], r[2]),
|
|
1618
|
-
normal: new Vec3(r[3], r[4], r[5]),
|
|
1619
|
-
};
|
|
2182
|
+
return this.findPlanarFaceByNormal(targetNormal, nearPoint);
|
|
1620
2183
|
}
|
|
1621
2184
|
// ── Export ──────────────────────────────────────────────────────
|
|
1622
2185
|
/**
|