okgeometry-api 1.2.0 → 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 +82 -9
- package/dist/Mesh.d.ts.map +1 -1
- package/dist/Mesh.js +329 -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 +65 -2
- package/dist/wasm-bindings.d.ts.map +1 -1
- package/dist/wasm-bindings.js +100 -2
- package/dist/wasm-bindings.js.map +1 -1
- package/package.json +1 -1
- package/src/Line.ts +38 -20
- package/src/Mesh.ts +538 -184
- package/src/MeshSurface.ts +72 -0
- package/src/NurbsCurve.ts +80 -26
- 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 +5 -0
- package/src/types.ts +15 -0
- package/src/wasm-base64.ts +1 -1
- package/src/wasm-bindings.d.ts +43 -2
- package/src/wasm-bindings.js +105 -2
package/src/Mesh.ts
CHANGED
|
@@ -5,10 +5,11 @@ import { Plane } from "./Plane.js";
|
|
|
5
5
|
import { Polyline } from "./Polyline.js";
|
|
6
6
|
import { Line } from "./Line.js";
|
|
7
7
|
import { Circle } from "./Circle.js";
|
|
8
|
-
import { Arc } from "./Arc.js";
|
|
9
|
-
import { Polygon } from "./Polygon.js";
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
8
|
+
import { Arc } from "./Arc.js";
|
|
9
|
+
import { Polygon } from "./Polygon.js";
|
|
10
|
+
import { MeshSurface } from "./MeshSurface.js";
|
|
11
|
+
import { NurbsCurve } from "./NurbsCurve.js";
|
|
12
|
+
import { PolyCurve } from "./PolyCurve.js";
|
|
12
13
|
import type { SweepableCurve, RotationAxis } from "./types.js";
|
|
13
14
|
import { CurveTypeCode, SegmentTypeCode } from "./types.js";
|
|
14
15
|
import { pointsToCoords, parsePolylineBuffer as parsePolylineBuf } from "./BufferCodec.js";
|
|
@@ -47,14 +48,24 @@ export interface PlanarCircle {
|
|
|
47
48
|
radius: number;
|
|
48
49
|
}
|
|
49
50
|
|
|
50
|
-
export interface PlanarArc {
|
|
51
|
-
points: Point[];
|
|
52
|
-
center: Point;
|
|
53
|
-
radius: number;
|
|
54
|
-
startAngle: number;
|
|
55
|
-
endAngle: number;
|
|
56
|
-
sweepAngle: number;
|
|
57
|
-
}
|
|
51
|
+
export interface PlanarArc {
|
|
52
|
+
points: Point[];
|
|
53
|
+
center: Point;
|
|
54
|
+
radius: number;
|
|
55
|
+
startAngle: number;
|
|
56
|
+
endAngle: number;
|
|
57
|
+
sweepAngle: number;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface MeshPlanarFace {
|
|
61
|
+
index: number;
|
|
62
|
+
seedTriangleIndex: number;
|
|
63
|
+
triangleIndices: number[];
|
|
64
|
+
triangleCount: number;
|
|
65
|
+
centroid: Point;
|
|
66
|
+
normal: Vec3;
|
|
67
|
+
area: number;
|
|
68
|
+
}
|
|
58
69
|
|
|
59
70
|
interface RawMeshBounds {
|
|
60
71
|
minX: number;
|
|
@@ -247,13 +258,17 @@ export class Mesh {
|
|
|
247
258
|
|
|
248
259
|
// Lazy caches
|
|
249
260
|
private _positionBuffer: Float32Array | null = null;
|
|
250
|
-
private _indexBuffer: Uint32Array | null = null;
|
|
251
|
-
private _vertices: Point[] | null = null;
|
|
252
|
-
private _faces: number[][] | null = null;
|
|
253
|
-
private
|
|
254
|
-
private
|
|
255
|
-
private
|
|
256
|
-
private
|
|
261
|
+
private _indexBuffer: Uint32Array | null = null;
|
|
262
|
+
private _vertices: Point[] | null = null;
|
|
263
|
+
private _faces: number[][] | null = null;
|
|
264
|
+
private _planarFaces: MeshPlanarFace[] | null = null;
|
|
265
|
+
private _planarFacesByTriangleIndex: Array<MeshPlanarFace | null> | null = null;
|
|
266
|
+
private _surfaces: MeshSurface[] | null = null;
|
|
267
|
+
private _surfacesByTriangleIndex: Array<MeshSurface | null> | null = null;
|
|
268
|
+
private _edgeVertexPairs: Array<[number, number]> | null = null;
|
|
269
|
+
private _topologyMetricsCache: { boundaryEdges: number; nonManifoldEdges: number } | null = null;
|
|
270
|
+
private _isClosedVolumeCache: boolean | null = null;
|
|
271
|
+
private _rawBoundsCache: RawMeshBounds | null | undefined = undefined;
|
|
257
272
|
|
|
258
273
|
private constructor(buffer: Float64Array, trustedBooleanInput = false) {
|
|
259
274
|
this._buffer = buffer;
|
|
@@ -330,14 +345,68 @@ export class Mesh {
|
|
|
330
345
|
&& a.maxZ + eps >= b.minZ;
|
|
331
346
|
}
|
|
332
347
|
|
|
333
|
-
private static boundsDiag(bounds: RawMeshBounds | null): number {
|
|
334
|
-
if (!bounds) return 1;
|
|
335
|
-
const dx = bounds.maxX - bounds.minX;
|
|
336
|
-
const dy = bounds.maxY - bounds.minY;
|
|
337
|
-
const dz = bounds.maxZ - bounds.minZ;
|
|
338
|
-
const diag = Math.hypot(dx, dy, dz);
|
|
339
|
-
return Number.isFinite(diag) && diag > 0 ? diag : 1;
|
|
340
|
-
}
|
|
348
|
+
private static boundsDiag(bounds: RawMeshBounds | null): number {
|
|
349
|
+
if (!bounds) return 1;
|
|
350
|
+
const dx = bounds.maxX - bounds.minX;
|
|
351
|
+
const dy = bounds.maxY - bounds.minY;
|
|
352
|
+
const dz = bounds.maxZ - bounds.minZ;
|
|
353
|
+
const diag = Math.hypot(dx, dy, dz);
|
|
354
|
+
return Number.isFinite(diag) && diag > 0 ? diag : 1;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
private static computeSignedVolumeFromBuffer(buffer: Float64Array): number {
|
|
358
|
+
const vertexCount = Math.max(0, Math.floor(buffer[0] ?? 0));
|
|
359
|
+
if (vertexCount <= 0) return 0;
|
|
360
|
+
|
|
361
|
+
const indexStart = 1 + vertexCount * 3;
|
|
362
|
+
let signedVolume = 0;
|
|
363
|
+
for (let i = indexStart; i + 2 < buffer.length; i += 3) {
|
|
364
|
+
const ia = Math.floor(buffer[i] ?? -1);
|
|
365
|
+
const ib = Math.floor(buffer[i + 1] ?? -1);
|
|
366
|
+
const ic = Math.floor(buffer[i + 2] ?? -1);
|
|
367
|
+
if (ia < 0 || ib < 0 || ic < 0 || ia >= vertexCount || ib >= vertexCount || ic >= vertexCount) continue;
|
|
368
|
+
|
|
369
|
+
const ax = buffer[1 + ia * 3];
|
|
370
|
+
const ay = buffer[1 + ia * 3 + 1];
|
|
371
|
+
const az = buffer[1 + ia * 3 + 2];
|
|
372
|
+
const bx = buffer[1 + ib * 3];
|
|
373
|
+
const by = buffer[1 + ib * 3 + 1];
|
|
374
|
+
const bz = buffer[1 + ib * 3 + 2];
|
|
375
|
+
const cx = buffer[1 + ic * 3];
|
|
376
|
+
const cy = buffer[1 + ic * 3 + 1];
|
|
377
|
+
const cz = buffer[1 + ic * 3 + 2];
|
|
378
|
+
|
|
379
|
+
signedVolume += ax * (by * cz - bz * cy)
|
|
380
|
+
- ay * (bx * cz - bz * cx)
|
|
381
|
+
+ az * (bx * cy - by * cx);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
return signedVolume / 6;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
private static reverseTriangleWinding(buffer: Float64Array): Float64Array {
|
|
388
|
+
const flipped = new Float64Array(buffer);
|
|
389
|
+
const vertexCount = Math.max(0, Math.floor(flipped[0] ?? 0));
|
|
390
|
+
const indexStart = 1 + vertexCount * 3;
|
|
391
|
+
for (let i = indexStart; i + 2 < flipped.length; i += 3) {
|
|
392
|
+
const tmp = flipped[i + 1];
|
|
393
|
+
flipped[i + 1] = flipped[i + 2];
|
|
394
|
+
flipped[i + 2] = tmp;
|
|
395
|
+
}
|
|
396
|
+
return flipped;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
private static normalizeClosedVolumeOrientation(mesh: Mesh): Mesh {
|
|
400
|
+
if (mesh.faceCount === 0 || mesh._vertexCount <= 0) return mesh;
|
|
401
|
+
|
|
402
|
+
const topology = mesh.topologyMetrics();
|
|
403
|
+
if (topology.boundaryEdges !== 0 || topology.nonManifoldEdges !== 0) return mesh;
|
|
404
|
+
|
|
405
|
+
const signedVolume = Mesh.computeSignedVolumeFromBuffer(mesh._buffer);
|
|
406
|
+
if (!Number.isFinite(signedVolume) || signedVolume >= 0) return mesh;
|
|
407
|
+
|
|
408
|
+
return new Mesh(Mesh.reverseTriangleWinding(mesh._buffer), mesh._trustedBooleanInput);
|
|
409
|
+
}
|
|
341
410
|
|
|
342
411
|
private static booleanContactTolerance(a: RawMeshBounds | null, b: RawMeshBounds | null): number {
|
|
343
412
|
const scale = Math.max(Mesh.boundsDiag(a), Mesh.boundsDiag(b));
|
|
@@ -909,12 +978,12 @@ export class Mesh {
|
|
|
909
978
|
return this._vertexCount;
|
|
910
979
|
}
|
|
911
980
|
|
|
912
|
-
/** Number of triangular faces in this mesh */
|
|
913
|
-
get faceCount(): number {
|
|
914
|
-
const start = 1 + this._vertexCount * 3;
|
|
915
|
-
if (this._buffer.length <= start) return 0;
|
|
916
|
-
return Math.floor((this._buffer.length - start) / 3);
|
|
917
|
-
}
|
|
981
|
+
/** Number of triangular faces in this mesh. Use `planarFaceCount` for logical planar faces. */
|
|
982
|
+
get faceCount(): number {
|
|
983
|
+
const start = 1 + this._vertexCount * 3;
|
|
984
|
+
if (this._buffer.length <= start) return 0;
|
|
985
|
+
return Math.floor((this._buffer.length - start) / 3);
|
|
986
|
+
}
|
|
918
987
|
|
|
919
988
|
// ── High-level accessors (lazy) ────────────────────────────────
|
|
920
989
|
|
|
@@ -934,22 +1003,64 @@ export class Mesh {
|
|
|
934
1003
|
return this._vertices;
|
|
935
1004
|
}
|
|
936
1005
|
|
|
937
|
-
/**
|
|
938
|
-
* Get all faces as arrays of vertex indices.
|
|
939
|
-
* Each face is [i0, i1, i2] for the three triangle vertices.
|
|
940
|
-
*
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
1006
|
+
/**
|
|
1007
|
+
* Get all faces as arrays of vertex indices.
|
|
1008
|
+
* Each face is [i0, i1, i2] for the three triangle vertices.
|
|
1009
|
+
* For coplanar edge-connected logical faces, use `planarFaces`.
|
|
1010
|
+
* Lazy-computed and cached.
|
|
1011
|
+
*/
|
|
1012
|
+
get faces(): number[][] {
|
|
1013
|
+
if (!this._faces) {
|
|
1014
|
+
const idx = this.indexBuffer;
|
|
945
1015
|
const f: number[][] = [];
|
|
946
1016
|
for (let i = 0; i < idx.length; i += 3) {
|
|
947
1017
|
f.push([idx[i], idx[i + 1], idx[i + 2]]);
|
|
948
1018
|
}
|
|
949
1019
|
this._faces = f;
|
|
950
|
-
}
|
|
951
|
-
return this._faces;
|
|
952
|
-
}
|
|
1020
|
+
}
|
|
1021
|
+
return this._faces;
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
/**
|
|
1025
|
+
* Alias for triangle faces, mirroring `faces`.
|
|
1026
|
+
* Use `planarFaces` when you want logical mesh faces such as the 6 faces of a box.
|
|
1027
|
+
*/
|
|
1028
|
+
get triangles(): number[][] {
|
|
1029
|
+
return this.faces;
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
/**
|
|
1033
|
+
* Number of coplanar edge-connected planar faces in this mesh.
|
|
1034
|
+
* Examples: a box has 6, an L-shaped extrusion has 8.
|
|
1035
|
+
*/
|
|
1036
|
+
get planarFaceCount(): number {
|
|
1037
|
+
return this.planarFaces.length;
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
/**
|
|
1041
|
+
* Logical mesh faces built by grouping coplanar edge-connected triangles.
|
|
1042
|
+
* Cached after the first query to keep repeated face access cheap.
|
|
1043
|
+
*/
|
|
1044
|
+
get planarFaces(): MeshPlanarFace[] {
|
|
1045
|
+
this.ensurePlanarFaceCache();
|
|
1046
|
+
return this._planarFaces ?? [];
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
/**
|
|
1050
|
+
* Number of logical planar surfaces in this mesh.
|
|
1051
|
+
* Mirrors `planarFaceCount`, but returns wrapper objects with surface queries.
|
|
1052
|
+
*/
|
|
1053
|
+
get surfaceCount(): number {
|
|
1054
|
+
return this.surfaces.length;
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
/**
|
|
1058
|
+
* Logical planar mesh surfaces with `evaluate(u, v)` and `normal(u, v)` methods.
|
|
1059
|
+
*/
|
|
1060
|
+
get surfaces(): MeshSurface[] {
|
|
1061
|
+
this.ensureSurfaceCache();
|
|
1062
|
+
return this._surfaces ?? [];
|
|
1063
|
+
}
|
|
953
1064
|
|
|
954
1065
|
/** Raw WASM buffer (for advanced use / re-passing to WASM) */
|
|
955
1066
|
get rawBuffer(): Float64Array {
|
|
@@ -963,25 +1074,146 @@ export class Mesh {
|
|
|
963
1074
|
* @param buffer - Float64Array in mesh buffer format
|
|
964
1075
|
* @returns New Mesh instance
|
|
965
1076
|
*/
|
|
966
|
-
static fromBuffer(
|
|
967
|
-
buffer: Float64Array,
|
|
968
|
-
options?: { trustedBooleanInput?: boolean },
|
|
969
|
-
): Mesh {
|
|
970
|
-
return new Mesh(buffer, options?.trustedBooleanInput ?? false);
|
|
971
|
-
}
|
|
972
|
-
|
|
973
|
-
private static fromTrustedBuffer(buffer: Float64Array): Mesh {
|
|
974
|
-
return new Mesh(buffer, true);
|
|
975
|
-
}
|
|
1077
|
+
static fromBuffer(
|
|
1078
|
+
buffer: Float64Array,
|
|
1079
|
+
options?: { trustedBooleanInput?: boolean },
|
|
1080
|
+
): Mesh {
|
|
1081
|
+
return new Mesh(buffer, options?.trustedBooleanInput ?? false);
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
private static fromTrustedBuffer(buffer: Float64Array): Mesh {
|
|
1085
|
+
return Mesh.normalizeClosedVolumeOrientation(new Mesh(buffer, true));
|
|
1086
|
+
}
|
|
976
1087
|
|
|
977
|
-
get trustedBooleanInput(): boolean {
|
|
978
|
-
return this._trustedBooleanInput;
|
|
979
|
-
}
|
|
1088
|
+
get trustedBooleanInput(): boolean {
|
|
1089
|
+
return this._trustedBooleanInput;
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
private getTriangleVertexIndices(faceIndex: number): [number, number, number] | null {
|
|
1093
|
+
if (!Number.isFinite(faceIndex) || faceIndex < 0 || faceIndex >= this.faceCount) return null;
|
|
1094
|
+
const indexBuffer = this.indexBuffer;
|
|
1095
|
+
const offset = Math.floor(faceIndex) * 3;
|
|
1096
|
+
if (offset + 2 >= indexBuffer.length) return null;
|
|
1097
|
+
return [indexBuffer[offset], indexBuffer[offset + 1], indexBuffer[offset + 2]];
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
private computeTriangleAreaByIndex(faceIndex: number): number {
|
|
1101
|
+
const tri = this.getTriangleVertexIndices(faceIndex);
|
|
1102
|
+
if (!tri) return 0;
|
|
1103
|
+
|
|
1104
|
+
const [i0, i1, i2] = tri;
|
|
1105
|
+
const base = 1;
|
|
1106
|
+
const off0 = base + i0 * 3;
|
|
1107
|
+
const off1 = base + i1 * 3;
|
|
1108
|
+
const off2 = base + i2 * 3;
|
|
1109
|
+
|
|
1110
|
+
const ax = this._buffer[off0] ?? 0;
|
|
1111
|
+
const ay = this._buffer[off0 + 1] ?? 0;
|
|
1112
|
+
const az = this._buffer[off0 + 2] ?? 0;
|
|
1113
|
+
const bx = this._buffer[off1] ?? 0;
|
|
1114
|
+
const by = this._buffer[off1 + 1] ?? 0;
|
|
1115
|
+
const bz = this._buffer[off1 + 2] ?? 0;
|
|
1116
|
+
const cx = this._buffer[off2] ?? 0;
|
|
1117
|
+
const cy = this._buffer[off2 + 1] ?? 0;
|
|
1118
|
+
const cz = this._buffer[off2 + 2] ?? 0;
|
|
1119
|
+
|
|
1120
|
+
const abx = bx - ax;
|
|
1121
|
+
const aby = by - ay;
|
|
1122
|
+
const abz = bz - az;
|
|
1123
|
+
const acx = cx - ax;
|
|
1124
|
+
const acy = cy - ay;
|
|
1125
|
+
const acz = cz - az;
|
|
1126
|
+
|
|
1127
|
+
const crossX = aby * acz - abz * acy;
|
|
1128
|
+
const crossY = abz * acx - abx * acz;
|
|
1129
|
+
const crossZ = abx * acy - aby * acx;
|
|
1130
|
+
|
|
1131
|
+
return 0.5 * Math.sqrt(crossX * crossX + crossY * crossY + crossZ * crossZ);
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
private ensurePlanarFaceCache(): void {
|
|
1135
|
+
if (this._planarFaces && this._planarFacesByTriangleIndex) return;
|
|
1136
|
+
|
|
1137
|
+
ensureInit();
|
|
1138
|
+
const raw = wasm.mesh_build_coplanar_connected_face_groups(this._vertexCount, this._buffer);
|
|
1139
|
+
const declaredGroupCount = Math.max(0, Math.floor(raw[0] ?? 0));
|
|
1140
|
+
const planarFaces: MeshPlanarFace[] = [];
|
|
1141
|
+
const planarFacesByTriangleIndex: Array<MeshPlanarFace | null> = new Array(this.faceCount).fill(null);
|
|
1142
|
+
|
|
1143
|
+
let offset = 1;
|
|
1144
|
+
for (let groupIndex = 0; groupIndex < declaredGroupCount && offset < raw.length; groupIndex++) {
|
|
1145
|
+
const declaredTriangleCount = Math.max(0, Math.floor(raw[offset] ?? 0));
|
|
1146
|
+
offset += 1;
|
|
1147
|
+
|
|
1148
|
+
const triangleIndices: number[] = [];
|
|
1149
|
+
let area = 0;
|
|
1150
|
+
for (let i = 0; i < declaredTriangleCount && offset < raw.length; i++, offset++) {
|
|
1151
|
+
const triangleIndex = Math.floor(raw[offset] ?? -1);
|
|
1152
|
+
if (triangleIndex < 0 || triangleIndex >= this.faceCount) continue;
|
|
1153
|
+
triangleIndices.push(triangleIndex);
|
|
1154
|
+
area += this.computeTriangleAreaByIndex(triangleIndex);
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
if (offset + 5 >= raw.length) break;
|
|
1158
|
+
|
|
1159
|
+
const centroid = new Point(
|
|
1160
|
+
raw[offset] ?? 0,
|
|
1161
|
+
raw[offset + 1] ?? 0,
|
|
1162
|
+
raw[offset + 2] ?? 0,
|
|
1163
|
+
);
|
|
1164
|
+
const normal = new Vec3(
|
|
1165
|
+
raw[offset + 3] ?? 0,
|
|
1166
|
+
raw[offset + 4] ?? 0,
|
|
1167
|
+
raw[offset + 5] ?? 0,
|
|
1168
|
+
).normalize();
|
|
1169
|
+
offset += 6;
|
|
1170
|
+
|
|
1171
|
+
if (triangleIndices.length === 0) continue;
|
|
1172
|
+
|
|
1173
|
+
const planarFace: MeshPlanarFace = {
|
|
1174
|
+
index: planarFaces.length,
|
|
1175
|
+
seedTriangleIndex: triangleIndices[0],
|
|
1176
|
+
triangleIndices,
|
|
1177
|
+
triangleCount: triangleIndices.length,
|
|
1178
|
+
centroid,
|
|
1179
|
+
normal,
|
|
1180
|
+
area,
|
|
1181
|
+
};
|
|
1182
|
+
|
|
1183
|
+
planarFaces.push(planarFace);
|
|
1184
|
+
for (const triangleIndex of triangleIndices) {
|
|
1185
|
+
planarFacesByTriangleIndex[triangleIndex] = planarFace;
|
|
1186
|
+
}
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
this._planarFaces = planarFaces;
|
|
1190
|
+
this._planarFacesByTriangleIndex = planarFacesByTriangleIndex;
|
|
1191
|
+
this._surfaces = null;
|
|
1192
|
+
this._surfacesByTriangleIndex = null;
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
private ensureSurfaceCache(): void {
|
|
1196
|
+
if (this._surfaces && this._surfacesByTriangleIndex) return;
|
|
1197
|
+
|
|
1198
|
+
const surfaces = this.planarFaces.map(face => new MeshSurface(this, face));
|
|
1199
|
+
const surfacesByTriangleIndex: Array<MeshSurface | null> = new Array(this.faceCount).fill(null);
|
|
1200
|
+
|
|
1201
|
+
for (const surface of surfaces) {
|
|
1202
|
+
for (const triangleIndex of surface.triangleIndices) {
|
|
1203
|
+
if (triangleIndex >= 0 && triangleIndex < surfacesByTriangleIndex.length) {
|
|
1204
|
+
surfacesByTriangleIndex[triangleIndex] = surface;
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
this._surfaces = surfaces;
|
|
1210
|
+
this._surfacesByTriangleIndex = surfacesByTriangleIndex;
|
|
1211
|
+
}
|
|
980
1212
|
|
|
981
|
-
debugSummary(): MeshDebugSummary {
|
|
982
|
-
const bounds = Mesh.toDebugBounds(this.getBounds());
|
|
983
|
-
const topology = this.topologyMetrics();
|
|
984
|
-
return {
|
|
1213
|
+
debugSummary(): MeshDebugSummary {
|
|
1214
|
+
const bounds = Mesh.toDebugBounds(this.getBounds());
|
|
1215
|
+
const topology = this.topologyMetrics();
|
|
1216
|
+
return {
|
|
985
1217
|
vertexCount: this.vertexCount,
|
|
986
1218
|
faceCount: this.faceCount,
|
|
987
1219
|
trustedBooleanInput: this._trustedBooleanInput,
|
|
@@ -1759,11 +1991,11 @@ export class Mesh {
|
|
|
1759
1991
|
return Mesh.fromTrustedBuffer(wasm.mesh_scale(this._vertexCount, this._buffer, sx, sy, sz));
|
|
1760
1992
|
}
|
|
1761
1993
|
|
|
1762
|
-
private runBoolean(
|
|
1763
|
-
other: Mesh,
|
|
1764
|
-
operation: "union" | "subtraction" | "intersection",
|
|
1765
|
-
invoke: () => Float64Array,
|
|
1766
|
-
options?: MeshBooleanOptions,
|
|
1994
|
+
private runBoolean(
|
|
1995
|
+
other: Mesh,
|
|
1996
|
+
operation: "union" | "subtraction" | "intersection",
|
|
1997
|
+
invoke: () => Float64Array,
|
|
1998
|
+
options?: MeshBooleanOptions,
|
|
1767
1999
|
): Mesh {
|
|
1768
2000
|
const faceCountA = this.faceCount;
|
|
1769
2001
|
const faceCountB = other.faceCount;
|
|
@@ -1812,10 +2044,53 @@ export class Mesh {
|
|
|
1812
2044
|
const vertexCount = result[0];
|
|
1813
2045
|
if (!Number.isFinite(vertexCount) || vertexCount < 0) {
|
|
1814
2046
|
throw new Error(`Boolean ${operation} failed and returned a corrupt mesh buffer.`);
|
|
1815
|
-
}
|
|
1816
|
-
return Mesh.fromTrustedBuffer(result);
|
|
1817
|
-
}
|
|
1818
|
-
|
|
2047
|
+
}
|
|
2048
|
+
return Mesh.fromTrustedBuffer(result);
|
|
2049
|
+
}
|
|
2050
|
+
|
|
2051
|
+
private static mergeMeshComponents(meshes: Mesh[]): Mesh {
|
|
2052
|
+
if (meshes.length === 0) return Mesh.emptyMesh();
|
|
2053
|
+
if (meshes.length === 1) return Mesh.cloneMesh(meshes[0]);
|
|
2054
|
+
return Mesh.mergeMeshes(meshes);
|
|
2055
|
+
}
|
|
2056
|
+
|
|
2057
|
+
private static createUnsupportedOpenMeshBooleanError(
|
|
2058
|
+
operation: MeshBooleanOperation,
|
|
2059
|
+
aClosed: boolean,
|
|
2060
|
+
bClosed: boolean,
|
|
2061
|
+
): Error {
|
|
2062
|
+
const topology = `A=${aClosed ? "closed" : "open"}, B=${bClosed ? "closed" : "open"}`;
|
|
2063
|
+
if (operation === "union") {
|
|
2064
|
+
return new Error(
|
|
2065
|
+
`Boolean union requires both inputs to be closed volumes (${topology}). `
|
|
2066
|
+
+ "Close or cap the open mesh before calling union().",
|
|
2067
|
+
);
|
|
2068
|
+
}
|
|
2069
|
+
|
|
2070
|
+
return new Error(
|
|
2071
|
+
`Boolean ${operation} with open meshes is only supported when A is open and B is closed `
|
|
2072
|
+
+ `(surface trim mode). Got ${topology}. Use Mesh.split(...) and choose outside/inside explicitly `
|
|
2073
|
+
+ "for other open-mesh combinations.",
|
|
2074
|
+
);
|
|
2075
|
+
}
|
|
2076
|
+
|
|
2077
|
+
private resolveOpenMeshBooleanFallback(
|
|
2078
|
+
other: Mesh,
|
|
2079
|
+
operation: MeshBooleanOperation,
|
|
2080
|
+
options?: MeshBooleanOptions,
|
|
2081
|
+
): Mesh | null {
|
|
2082
|
+
const aClosed = this.isClosedVolume();
|
|
2083
|
+
const bClosed = other.isClosedVolume();
|
|
2084
|
+
if (aClosed && bClosed) return null;
|
|
2085
|
+
|
|
2086
|
+
if (!aClosed && bClosed && (operation === "subtraction" || operation === "intersection")) {
|
|
2087
|
+
const split = this.splitWithMesh(other, options);
|
|
2088
|
+
return Mesh.mergeMeshComponents(operation === "subtraction" ? split.outside : split.inside);
|
|
2089
|
+
}
|
|
2090
|
+
|
|
2091
|
+
throw Mesh.createUnsupportedOpenMeshBooleanError(operation, aClosed, bClosed);
|
|
2092
|
+
}
|
|
2093
|
+
|
|
1819
2094
|
static encodeBooleanOperationToken(
|
|
1820
2095
|
operation: MeshBooleanOperation,
|
|
1821
2096
|
a: Mesh,
|
|
@@ -1849,71 +2124,87 @@ export class Mesh {
|
|
|
1849
2124
|
|
|
1850
2125
|
// ── Booleans ───────────────────────────────────────────────────
|
|
1851
2126
|
|
|
1852
|
-
/**
|
|
1853
|
-
* Compute boolean union with another mesh.
|
|
1854
|
-
*
|
|
1855
|
-
* @param
|
|
1856
|
-
* @
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
2127
|
+
/**
|
|
2128
|
+
* Compute boolean union with another mesh.
|
|
2129
|
+
* Requires both inputs to be closed volumes.
|
|
2130
|
+
* @param other - Mesh to union with
|
|
2131
|
+
* @param options - Optional safety overrides
|
|
2132
|
+
* @returns New mesh containing volume of both inputs
|
|
2133
|
+
*/
|
|
2134
|
+
union(other: Mesh, options?: MeshBooleanOptions): Mesh {
|
|
2135
|
+
ensureInit();
|
|
2136
|
+
const lhs = Mesh.normalizeClosedVolumeOrientation(this);
|
|
2137
|
+
const rhs = Mesh.normalizeClosedVolumeOrientation(other);
|
|
2138
|
+
lhs.resolveOpenMeshBooleanFallback(rhs, "union", options);
|
|
2139
|
+
const operationToken = Mesh.encodeBooleanOperationToken("union", lhs, rhs, options);
|
|
2140
|
+
return lhs.runBoolean(
|
|
2141
|
+
rhs,
|
|
2142
|
+
"union",
|
|
2143
|
+
() => wasm.mesh_boolean_operation(
|
|
2144
|
+
lhs._vertexCount,
|
|
2145
|
+
lhs._buffer,
|
|
2146
|
+
rhs._vertexCount,
|
|
2147
|
+
rhs._buffer,
|
|
2148
|
+
operationToken,
|
|
2149
|
+
),
|
|
2150
|
+
options,
|
|
2151
|
+
);
|
|
2152
|
+
}
|
|
1874
2153
|
|
|
1875
|
-
/**
|
|
1876
|
-
* Compute boolean subtraction with another mesh.
|
|
1877
|
-
*
|
|
1878
|
-
*
|
|
1879
|
-
* @
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
)
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
2154
|
+
/**
|
|
2155
|
+
* Compute boolean subtraction with another mesh.
|
|
2156
|
+
* If this mesh is open and `other` is a closed cutter, this trims the open
|
|
2157
|
+
* host via `split()` and returns the outside surface pieces merged together.
|
|
2158
|
+
* @param other - Mesh to subtract
|
|
2159
|
+
* @param options - Optional safety overrides
|
|
2160
|
+
* @returns New mesh with other's volume removed from this
|
|
2161
|
+
*/
|
|
2162
|
+
subtract(other: Mesh, options?: MeshBooleanOptions): Mesh {
|
|
2163
|
+
ensureInit();
|
|
2164
|
+
const lhs = Mesh.normalizeClosedVolumeOrientation(this);
|
|
2165
|
+
const rhs = Mesh.normalizeClosedVolumeOrientation(other);
|
|
2166
|
+
const fallback = lhs.resolveOpenMeshBooleanFallback(rhs, "subtraction", options);
|
|
2167
|
+
if (fallback) return fallback;
|
|
2168
|
+
const operationToken = Mesh.encodeBooleanOperationToken("subtraction", lhs, rhs, options);
|
|
2169
|
+
return lhs.runBoolean(
|
|
2170
|
+
rhs,
|
|
2171
|
+
"subtraction",
|
|
2172
|
+
() => wasm.mesh_boolean_operation(
|
|
2173
|
+
lhs._vertexCount,
|
|
2174
|
+
lhs._buffer,
|
|
2175
|
+
rhs._vertexCount,
|
|
2176
|
+
rhs._buffer,
|
|
2177
|
+
operationToken,
|
|
2178
|
+
),
|
|
2179
|
+
options,
|
|
2180
|
+
);
|
|
2181
|
+
}
|
|
1897
2182
|
|
|
1898
|
-
/**
|
|
1899
|
-
* Compute boolean intersection with another mesh.
|
|
1900
|
-
*
|
|
1901
|
-
*
|
|
1902
|
-
* @
|
|
1903
|
-
|
|
2183
|
+
/**
|
|
2184
|
+
* Compute boolean intersection with another mesh.
|
|
2185
|
+
* If this mesh is open and `other` is a closed cutter, this trims the open
|
|
2186
|
+
* host via `split()` and returns the inside surface pieces merged together.
|
|
2187
|
+
* @param other - Mesh to intersect with
|
|
2188
|
+
* @param options - Optional safety overrides
|
|
2189
|
+
* @returns New mesh containing only the overlapping volume
|
|
2190
|
+
*/
|
|
1904
2191
|
intersect(other: Mesh, options?: MeshBooleanOptions): Mesh {
|
|
1905
2192
|
ensureInit();
|
|
1906
|
-
const
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
2193
|
+
const lhs = Mesh.normalizeClosedVolumeOrientation(this);
|
|
2194
|
+
const rhs = Mesh.normalizeClosedVolumeOrientation(other);
|
|
2195
|
+
const fallback = lhs.resolveOpenMeshBooleanFallback(rhs, "intersection", options);
|
|
2196
|
+
if (fallback) return fallback;
|
|
2197
|
+
const operationToken = Mesh.encodeBooleanOperationToken("intersection", lhs, rhs, options);
|
|
2198
|
+
return lhs.runBoolean(
|
|
2199
|
+
rhs,
|
|
2200
|
+
"intersection",
|
|
2201
|
+
() => wasm.mesh_boolean_operation(
|
|
2202
|
+
lhs._vertexCount,
|
|
2203
|
+
lhs._buffer,
|
|
2204
|
+
rhs._vertexCount,
|
|
2205
|
+
rhs._buffer,
|
|
2206
|
+
operationToken,
|
|
2207
|
+
),
|
|
1917
2208
|
options,
|
|
1918
2209
|
);
|
|
1919
2210
|
}
|
|
@@ -2451,9 +2742,9 @@ export class Mesh {
|
|
|
2451
2742
|
/**
|
|
2452
2743
|
* Centroid of the coplanar edge-connected face group containing faceIndex.
|
|
2453
2744
|
*/
|
|
2454
|
-
getCoplanarFaceGroupCentroid(faceIndex: number): Point | null {
|
|
2455
|
-
ensureInit();
|
|
2456
|
-
if (!Number.isFinite(faceIndex) || faceIndex < 0) return null;
|
|
2745
|
+
getCoplanarFaceGroupCentroid(faceIndex: number): Point | null {
|
|
2746
|
+
ensureInit();
|
|
2747
|
+
if (!Number.isFinite(faceIndex) || faceIndex < 0) return null;
|
|
2457
2748
|
const r = wasm.mesh_get_coplanar_face_group_centroid(
|
|
2458
2749
|
this._vertexCount,
|
|
2459
2750
|
this._buffer,
|
|
@@ -2623,62 +2914,125 @@ export class Mesh {
|
|
|
2623
2914
|
if (this._isClosedVolumeCache === null) {
|
|
2624
2915
|
this._isClosedVolumeCache = boundaryEdges === 0;
|
|
2625
2916
|
}
|
|
2626
|
-
return {
|
|
2627
|
-
boundaryEdges,
|
|
2628
|
-
nonManifoldEdges,
|
|
2629
|
-
};
|
|
2630
|
-
}
|
|
2631
|
-
|
|
2632
|
-
/**
|
|
2633
|
-
*
|
|
2634
|
-
|
|
2917
|
+
return {
|
|
2918
|
+
boundaryEdges,
|
|
2919
|
+
nonManifoldEdges,
|
|
2920
|
+
};
|
|
2921
|
+
}
|
|
2922
|
+
|
|
2923
|
+
/**
|
|
2924
|
+
* Get a logical planar face by its planar-face index.
|
|
2925
|
+
*/
|
|
2926
|
+
getPlanarFace(index: number): MeshPlanarFace | null {
|
|
2927
|
+
if (!Number.isFinite(index) || index < 0) return null;
|
|
2928
|
+
const planarFaces = this.planarFaces;
|
|
2929
|
+
return planarFaces[Math.floor(index)] ?? null;
|
|
2930
|
+
}
|
|
2931
|
+
|
|
2932
|
+
/**
|
|
2933
|
+
* Get a logical planar surface wrapper by its planar-face index.
|
|
2934
|
+
*/
|
|
2935
|
+
getSurface(index: number): MeshSurface | null {
|
|
2936
|
+
if (!Number.isFinite(index) || index < 0) return null;
|
|
2937
|
+
const surfaces = this.surfaces;
|
|
2938
|
+
return surfaces[Math.floor(index)] ?? null;
|
|
2939
|
+
}
|
|
2940
|
+
|
|
2941
|
+
/**
|
|
2942
|
+
* Get the logical planar face that contains a triangle face index.
|
|
2943
|
+
*/
|
|
2944
|
+
getPlanarFaceByTriangleIndex(triangleIndex: number): MeshPlanarFace | null {
|
|
2945
|
+
if (!Number.isFinite(triangleIndex) || triangleIndex < 0) return null;
|
|
2946
|
+
this.ensurePlanarFaceCache();
|
|
2947
|
+
return this._planarFacesByTriangleIndex?.[Math.floor(triangleIndex)] ?? null;
|
|
2948
|
+
}
|
|
2949
|
+
|
|
2950
|
+
/**
|
|
2951
|
+
* Get the logical planar surface wrapper that contains a triangle face index.
|
|
2952
|
+
*/
|
|
2953
|
+
getSurfaceByTriangleIndex(triangleIndex: number): MeshSurface | null {
|
|
2954
|
+
if (!Number.isFinite(triangleIndex) || triangleIndex < 0) return null;
|
|
2955
|
+
this.ensureSurfaceCache();
|
|
2956
|
+
return this._surfacesByTriangleIndex?.[Math.floor(triangleIndex)] ?? null;
|
|
2957
|
+
}
|
|
2958
|
+
|
|
2959
|
+
/**
|
|
2960
|
+
* Find the best matching logical planar face by normal and optional centroid proximity.
|
|
2961
|
+
*/
|
|
2962
|
+
findPlanarFaceByNormal(targetNormal: Vec3, nearPoint?: Point): MeshPlanarFace | null {
|
|
2963
|
+
const planarFaces = this.planarFaces;
|
|
2964
|
+
if (planarFaces.length === 0) return null;
|
|
2965
|
+
|
|
2966
|
+
const normalizedTarget = targetNormal.normalize();
|
|
2967
|
+
if (normalizedTarget.length() < 1e-12) return null;
|
|
2968
|
+
|
|
2969
|
+
const alignmentEps = 1e-9;
|
|
2970
|
+
const distanceEps = 1e-9;
|
|
2971
|
+
|
|
2972
|
+
let best: MeshPlanarFace | null = null;
|
|
2973
|
+
let bestAlignment = -Infinity;
|
|
2974
|
+
let bestDistance = Number.POSITIVE_INFINITY;
|
|
2975
|
+
let bestArea = -Infinity;
|
|
2976
|
+
|
|
2977
|
+
for (const planarFace of planarFaces) {
|
|
2978
|
+
const alignment = planarFace.normal.dot(normalizedTarget);
|
|
2979
|
+
const distance = nearPoint ? planarFace.centroid.distanceTo(nearPoint) : 0;
|
|
2980
|
+
|
|
2981
|
+
const betterAlignment = alignment > bestAlignment + alignmentEps;
|
|
2982
|
+
const similarAlignment = Math.abs(alignment - bestAlignment) <= alignmentEps;
|
|
2983
|
+
const betterDistance = nearPoint !== undefined && distance < bestDistance - distanceEps;
|
|
2984
|
+
const similarDistance = Math.abs(distance - bestDistance) <= distanceEps;
|
|
2985
|
+
const betterArea = planarFace.area > bestArea + distanceEps;
|
|
2986
|
+
|
|
2987
|
+
if (
|
|
2988
|
+
best === null
|
|
2989
|
+
|| betterAlignment
|
|
2990
|
+
|| (similarAlignment && betterDistance)
|
|
2991
|
+
|| (similarAlignment && (!nearPoint || similarDistance) && betterArea)
|
|
2992
|
+
) {
|
|
2993
|
+
best = planarFace;
|
|
2994
|
+
bestAlignment = alignment;
|
|
2995
|
+
bestDistance = distance;
|
|
2996
|
+
bestArea = planarFace.area;
|
|
2997
|
+
}
|
|
2998
|
+
}
|
|
2999
|
+
|
|
3000
|
+
return best;
|
|
3001
|
+
}
|
|
3002
|
+
|
|
3003
|
+
/**
|
|
3004
|
+
* Find the best matching logical planar surface wrapper by normal
|
|
3005
|
+
* similarity and optional point proximity.
|
|
3006
|
+
*/
|
|
3007
|
+
findSurfaceByNormal(targetNormal: Vec3, nearPoint?: Point): MeshSurface | null {
|
|
3008
|
+
const face = this.findPlanarFaceByNormal(targetNormal, nearPoint);
|
|
3009
|
+
if (!face) return null;
|
|
3010
|
+
return this.getSurface(face.index);
|
|
3011
|
+
}
|
|
3012
|
+
|
|
3013
|
+
/**
|
|
3014
|
+
* Odd/even point containment test against a closed mesh.
|
|
3015
|
+
* Uses majority vote across multiple ray directions for robustness.
|
|
2635
3016
|
*/
|
|
2636
3017
|
containsPoint(point: Point): boolean {
|
|
2637
3018
|
ensureInit();
|
|
2638
3019
|
return wasm.mesh_contains_point(this._vertexCount, this._buffer, point.x, point.y, point.z);
|
|
2639
3020
|
}
|
|
2640
3021
|
|
|
2641
|
-
/**
|
|
2642
|
-
* Find the coplanar + edge-connected face group containing a triangle.
|
|
2643
|
-
*/
|
|
2644
|
-
findFaceByTriangleIndex(triangleIndex: number):
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
|
|
2651
|
-
|
|
2652
|
-
|
|
2653
|
-
return
|
|
2654
|
-
|
|
2655
|
-
normal: new Vec3(r[3], r[4], r[5]),
|
|
2656
|
-
};
|
|
2657
|
-
}
|
|
2658
|
-
|
|
2659
|
-
/**
|
|
2660
|
-
* Find the best matching coplanar + edge-connected face group by normal
|
|
2661
|
-
* similarity and optional point proximity.
|
|
2662
|
-
*/
|
|
2663
|
-
findFaceByNormal(targetNormal: Vec3, nearPoint?: Point): { centroid: Point; normal: Vec3 } | null {
|
|
2664
|
-
ensureInit();
|
|
2665
|
-
const r = wasm.mesh_find_face_group_by_normal(
|
|
2666
|
-
this._vertexCount,
|
|
2667
|
-
this._buffer,
|
|
2668
|
-
targetNormal.x,
|
|
2669
|
-
targetNormal.y,
|
|
2670
|
-
targetNormal.z,
|
|
2671
|
-
nearPoint?.x ?? 0,
|
|
2672
|
-
nearPoint?.y ?? 0,
|
|
2673
|
-
nearPoint?.z ?? 0,
|
|
2674
|
-
nearPoint !== undefined,
|
|
2675
|
-
);
|
|
2676
|
-
if (!r || r.length < 6) return null;
|
|
2677
|
-
return {
|
|
2678
|
-
centroid: new Point(r[0], r[1], r[2]),
|
|
2679
|
-
normal: new Vec3(r[3], r[4], r[5]),
|
|
2680
|
-
};
|
|
2681
|
-
}
|
|
3022
|
+
/**
|
|
3023
|
+
* Find the coplanar + edge-connected face group containing a triangle.
|
|
3024
|
+
*/
|
|
3025
|
+
findFaceByTriangleIndex(triangleIndex: number): MeshPlanarFace | null {
|
|
3026
|
+
return this.getPlanarFaceByTriangleIndex(triangleIndex);
|
|
3027
|
+
}
|
|
3028
|
+
|
|
3029
|
+
/**
|
|
3030
|
+
* Find the best matching coplanar + edge-connected face group by normal
|
|
3031
|
+
* similarity and optional point proximity.
|
|
3032
|
+
*/
|
|
3033
|
+
findFaceByNormal(targetNormal: Vec3, nearPoint?: Point): MeshPlanarFace | null {
|
|
3034
|
+
return this.findPlanarFaceByNormal(targetNormal, nearPoint);
|
|
3035
|
+
}
|
|
2682
3036
|
|
|
2683
3037
|
// ── Export ──────────────────────────────────────────────────────
|
|
2684
3038
|
|