okgeometry-api 0.4.2 → 0.4.4
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.map +1 -1
- package/dist/Line.js +3 -3
- package/dist/Line.js.map +1 -1
- package/dist/Mesh.d.ts +44 -24
- package/dist/Mesh.d.ts.map +1 -1
- package/dist/Mesh.js +205 -775
- package/dist/Mesh.js.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/package.json +1 -1
- package/wasm/okgeometrycore.d.ts +124 -0
- package/wasm/okgeometrycore.js +1 -1
- package/wasm/okgeometrycore_bg.js +401 -0
- package/wasm/okgeometrycore_bg.wasm +0 -0
- package/wasm/okgeometrycore_bg.wasm.d.ts +22 -0
package/dist/Mesh.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
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";
|
|
5
4
|
import { Polyline } from "./Polyline.js";
|
|
6
5
|
import { Line } from "./Line.js";
|
|
7
6
|
import { Circle } from "./Circle.js";
|
|
@@ -111,6 +110,42 @@ export class Mesh {
|
|
|
111
110
|
static fromBuffer(buffer) {
|
|
112
111
|
return new Mesh(buffer);
|
|
113
112
|
}
|
|
113
|
+
/**
|
|
114
|
+
* Build an axis-aligned rectangle on a plane basis from opposite corners.
|
|
115
|
+
* The resulting corners are ordered [p0, p1, p2, p3] and form a closed loop.
|
|
116
|
+
*/
|
|
117
|
+
static buildPlanarRectangle(startPoint, endPoint, uAxis, vAxis) {
|
|
118
|
+
const delta = endPoint.sub(startPoint);
|
|
119
|
+
const uLen = delta.dot(uAxis);
|
|
120
|
+
const vLen = delta.dot(vAxis);
|
|
121
|
+
const p0 = startPoint;
|
|
122
|
+
const p1 = startPoint.add(uAxis.scale(uLen));
|
|
123
|
+
const p3 = startPoint.add(vAxis.scale(vLen));
|
|
124
|
+
const p2 = p1.add(vAxis.scale(vLen));
|
|
125
|
+
return {
|
|
126
|
+
corners: [p0, p1, p2, p3],
|
|
127
|
+
width: Math.abs(uLen),
|
|
128
|
+
height: Math.abs(vLen),
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Build a planar circle from center and a point on/near its radius direction.
|
|
133
|
+
* Radius is measured in the plane orthogonal to `normal`.
|
|
134
|
+
*/
|
|
135
|
+
static buildPlanarCircle(center, radiusPoint, normal, segments = 64) {
|
|
136
|
+
const n = normal.normalize();
|
|
137
|
+
const radial = radiusPoint.sub(center);
|
|
138
|
+
const radialInPlane = radial.sub(n.scale(radial.dot(n)));
|
|
139
|
+
const radius = radialInPlane.length();
|
|
140
|
+
if (radius < 1e-12 || segments < 3) {
|
|
141
|
+
return { points: [], radius: 0 };
|
|
142
|
+
}
|
|
143
|
+
const circle = new Circle(center, radius, n);
|
|
144
|
+
return {
|
|
145
|
+
points: circle.sample(Math.floor(segments)),
|
|
146
|
+
radius,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
114
149
|
/**
|
|
115
150
|
* Create a planar patch mesh from boundary points using CDT (Constrained Delaunay Triangulation).
|
|
116
151
|
* Correctly handles both convex and concave polygons.
|
|
@@ -245,77 +280,36 @@ export class Mesh {
|
|
|
245
280
|
* Closed curves use Newell's method; open curves use first non-collinear triple.
|
|
246
281
|
*/
|
|
247
282
|
static computePlanarCurveNormal(points, closed) {
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
if (closed) {
|
|
252
|
-
let nx = 0;
|
|
253
|
-
let ny = 0;
|
|
254
|
-
let nz = 0;
|
|
255
|
-
for (let i = 0; i < n; i++) {
|
|
256
|
-
const a = points[i];
|
|
257
|
-
const b = points[(i + 1) % n];
|
|
258
|
-
nx += (a.y - b.y) * (a.z + b.z);
|
|
259
|
-
ny += (a.z - b.z) * (a.x + b.x);
|
|
260
|
-
nz += (a.x - b.x) * (a.y + b.y);
|
|
261
|
-
}
|
|
262
|
-
const normal = new Vec3(nx, ny, nz);
|
|
263
|
-
if (normal.length() >= 1e-10)
|
|
264
|
-
return normal.normalize();
|
|
265
|
-
}
|
|
266
|
-
for (let i = 0; i < n - 2; i++) {
|
|
267
|
-
const p0 = points[i];
|
|
268
|
-
const p1 = points[i + 1];
|
|
269
|
-
const p2 = points[i + 2];
|
|
270
|
-
const e1 = p1.sub(p0);
|
|
271
|
-
const e2 = p2.sub(p0);
|
|
272
|
-
const cross = e1.cross(e2);
|
|
273
|
-
if (cross.length() >= 1e-10)
|
|
274
|
-
return cross.normalize();
|
|
275
|
-
}
|
|
276
|
-
return Vec3.Y;
|
|
283
|
+
ensureInit();
|
|
284
|
+
const r = wasm.mesh_compute_planar_curve_normal(pointsToCoords(points), closed);
|
|
285
|
+
return new Vec3(r[0] ?? 0, r[1] ?? 1, r[2] ?? 0);
|
|
277
286
|
}
|
|
278
287
|
/**
|
|
279
288
|
* Extrude a planar curve along a direction.
|
|
280
289
|
* Open curves return an uncapped polysurface; closed curves are capped solids.
|
|
281
290
|
*/
|
|
282
291
|
static extrudePlanarCurve(points, normal, height, closed) {
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
if (!closed) {
|
|
287
|
-
const direction = new Vec3(normal.x * height, normal.y * height, normal.z * height);
|
|
288
|
-
return new Polyline(points).extrude(direction, 1, false);
|
|
289
|
-
}
|
|
290
|
-
const sideWalls = Mesh.loftPolylines([points, topPoints], 1, false);
|
|
291
|
-
const bottomCap = Mesh.patchFromPoints(points);
|
|
292
|
-
const topCap = Mesh.patchFromPoints(topPoints);
|
|
293
|
-
return Mesh.mergeMeshes([sideWalls, bottomCap, topCap]);
|
|
292
|
+
ensureInit();
|
|
293
|
+
const buf = wasm.mesh_extrude_planar_curve(pointsToCoords(points), normal.x, normal.y, normal.z, height, closed);
|
|
294
|
+
return Mesh.fromBuffer(buf);
|
|
294
295
|
}
|
|
295
296
|
/**
|
|
296
297
|
* Shift a closed cutter profile slightly opposite to travel direction and
|
|
297
298
|
* compensate height so the distal end remains at the user-intended depth.
|
|
298
299
|
*/
|
|
299
300
|
static prepareBooleanCutterCurve(points, closed, normal, height) {
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
const
|
|
304
|
-
|
|
305
|
-
|
|
301
|
+
ensureInit();
|
|
302
|
+
const buf = wasm.mesh_prepare_boolean_cutter_curve(pointsToCoords(points), closed, normal.x, normal.y, normal.z, height);
|
|
303
|
+
const outHeight = buf[0] ?? height;
|
|
304
|
+
const epsilon = buf[1] ?? 0;
|
|
305
|
+
const count = Math.max(0, Math.floor(buf[2] ?? 0));
|
|
306
|
+
const shifted = [];
|
|
307
|
+
let off = 3;
|
|
308
|
+
for (let i = 0; i < count; i++) {
|
|
309
|
+
shifted.push(new Point(buf[off], buf[off + 1], buf[off + 2]));
|
|
310
|
+
off += 3;
|
|
306
311
|
}
|
|
307
|
-
|
|
308
|
-
const baseEpsilon = Math.max(1e-6, scale * 1e-5);
|
|
309
|
-
const epsilon = Math.max(1e-8, Math.min(baseEpsilon, absHeight * 0.25));
|
|
310
|
-
const delta = height >= 0 ? -epsilon : epsilon;
|
|
311
|
-
const offset = normal.scale(delta);
|
|
312
|
-
const shifted = points.map((p) => new Point(p.x + offset.x, p.y + offset.y, p.z + offset.z));
|
|
313
|
-
const offsetAlongNormal = offset.dot(normal);
|
|
314
|
-
return {
|
|
315
|
-
points: shifted,
|
|
316
|
-
height: height - offsetAlongNormal,
|
|
317
|
-
epsilon,
|
|
318
|
-
};
|
|
312
|
+
return { points: shifted, height: outHeight, epsilon };
|
|
319
313
|
}
|
|
320
314
|
/**
|
|
321
315
|
* Sweep any curve type along any curve type.
|
|
@@ -412,79 +406,47 @@ export class Mesh {
|
|
|
412
406
|
return data;
|
|
413
407
|
}
|
|
414
408
|
static mergeMeshes(meshes) {
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
for (const mesh of meshes) {
|
|
419
|
-
const raw = mesh.rawBuffer;
|
|
420
|
-
const vertexCount = raw.length > 0 ? Math.max(0, Math.floor(raw[0])) : 0;
|
|
421
|
-
const positionValues = vertexCount * 3;
|
|
422
|
-
const indexValues = Math.max(0, raw.length - 1 - positionValues);
|
|
423
|
-
totalVertexCount += vertexCount;
|
|
424
|
-
totalPositionValues += positionValues;
|
|
425
|
-
totalIndexValues += indexValues;
|
|
426
|
-
}
|
|
427
|
-
const out = new Float64Array(1 + totalPositionValues + totalIndexValues);
|
|
428
|
-
out[0] = totalVertexCount;
|
|
429
|
-
let writePos = 1;
|
|
430
|
-
let writeIdx = 1 + totalPositionValues;
|
|
431
|
-
let vertexOffset = 0;
|
|
432
|
-
for (const mesh of meshes) {
|
|
433
|
-
const raw = mesh.rawBuffer;
|
|
434
|
-
const vertexCount = raw.length > 0 ? Math.max(0, Math.floor(raw[0])) : 0;
|
|
435
|
-
const positionValues = vertexCount * 3;
|
|
436
|
-
const indexValues = Math.max(0, raw.length - 1 - positionValues);
|
|
437
|
-
out.set(raw.subarray(1, 1 + positionValues), writePos);
|
|
438
|
-
for (let i = 0; i < indexValues; i++) {
|
|
439
|
-
out[writeIdx + i] = raw[1 + positionValues + i] + vertexOffset;
|
|
440
|
-
}
|
|
441
|
-
writePos += positionValues;
|
|
442
|
-
writeIdx += indexValues;
|
|
443
|
-
vertexOffset += vertexCount;
|
|
444
|
-
}
|
|
445
|
-
return new Mesh(out);
|
|
446
|
-
}
|
|
447
|
-
static estimateCurveScale(points) {
|
|
448
|
-
if (points.length === 0)
|
|
449
|
-
return 1;
|
|
450
|
-
let minX = Number.POSITIVE_INFINITY;
|
|
451
|
-
let minY = Number.POSITIVE_INFINITY;
|
|
452
|
-
let minZ = Number.POSITIVE_INFINITY;
|
|
453
|
-
let maxX = Number.NEGATIVE_INFINITY;
|
|
454
|
-
let maxY = Number.NEGATIVE_INFINITY;
|
|
455
|
-
let maxZ = Number.NEGATIVE_INFINITY;
|
|
456
|
-
for (const p of points) {
|
|
457
|
-
if (p.x < minX)
|
|
458
|
-
minX = p.x;
|
|
459
|
-
if (p.y < minY)
|
|
460
|
-
minY = p.y;
|
|
461
|
-
if (p.z < minZ)
|
|
462
|
-
minZ = p.z;
|
|
463
|
-
if (p.x > maxX)
|
|
464
|
-
maxX = p.x;
|
|
465
|
-
if (p.y > maxY)
|
|
466
|
-
maxY = p.y;
|
|
467
|
-
if (p.z > maxZ)
|
|
468
|
-
maxZ = p.z;
|
|
469
|
-
}
|
|
470
|
-
const dx = maxX - minX;
|
|
471
|
-
const dy = maxY - minY;
|
|
472
|
-
const dz = maxZ - minZ;
|
|
473
|
-
const diag = Math.sqrt(dx * dx + dy * dy + dz * dz);
|
|
474
|
-
return Math.max(diag, 1);
|
|
409
|
+
ensureInit();
|
|
410
|
+
const packed = Mesh.packMeshes(meshes);
|
|
411
|
+
return Mesh.fromBuffer(wasm.mesh_merge(packed));
|
|
475
412
|
}
|
|
476
413
|
/**
|
|
477
|
-
*
|
|
414
|
+
* Raycast against many meshes and return all hits sorted by distance.
|
|
478
415
|
*/
|
|
479
|
-
static
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
416
|
+
static raycastMany(meshes, origin, direction, maxDistance = Number.POSITIVE_INFINITY) {
|
|
417
|
+
ensureInit();
|
|
418
|
+
if (meshes.length === 0)
|
|
419
|
+
return [];
|
|
420
|
+
const packed = Mesh.packMeshes(meshes);
|
|
421
|
+
const raycastMany = wasm.mesh_raycast_many;
|
|
422
|
+
const buf = raycastMany(packed, origin.x, origin.y, origin.z, direction.x, direction.y, direction.z, maxDistance);
|
|
423
|
+
const count = Math.max(0, Math.floor(buf[0] ?? 0));
|
|
424
|
+
const hits = [];
|
|
425
|
+
let off = 1;
|
|
426
|
+
for (let i = 0; i < count; i++) {
|
|
427
|
+
hits.push({
|
|
428
|
+
meshIndex: Math.floor(buf[off]),
|
|
429
|
+
point: new Point(buf[off + 1], buf[off + 2], buf[off + 3]),
|
|
430
|
+
normal: new Vec3(buf[off + 4], buf[off + 5], buf[off + 6]),
|
|
431
|
+
faceIndex: Math.floor(buf[off + 7]),
|
|
432
|
+
distance: buf[off + 8],
|
|
433
|
+
});
|
|
434
|
+
off += 9;
|
|
486
435
|
}
|
|
487
|
-
return
|
|
436
|
+
return hits;
|
|
437
|
+
}
|
|
438
|
+
static packMeshes(meshes) {
|
|
439
|
+
const totalLen = meshes.reduce((sum, m) => sum + 1 + m.rawBuffer.length, 1);
|
|
440
|
+
const packed = new Float64Array(totalLen);
|
|
441
|
+
packed[0] = meshes.length;
|
|
442
|
+
let off = 1;
|
|
443
|
+
for (const mesh of meshes) {
|
|
444
|
+
const raw = mesh.rawBuffer;
|
|
445
|
+
packed[off++] = raw.length;
|
|
446
|
+
packed.set(raw, off);
|
|
447
|
+
off += raw.length;
|
|
448
|
+
}
|
|
449
|
+
return packed;
|
|
488
450
|
}
|
|
489
451
|
/**
|
|
490
452
|
* Unique undirected triangle edges as vertex-index pairs.
|
|
@@ -492,21 +454,19 @@ export class Mesh {
|
|
|
492
454
|
getUniqueEdgeVertexPairs() {
|
|
493
455
|
if (this._edgeVertexPairs)
|
|
494
456
|
return this._edgeVertexPairs;
|
|
495
|
-
|
|
496
|
-
const
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
const
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
}
|
|
509
|
-
this._edgeVertexPairs = Array.from(unique.values());
|
|
457
|
+
ensureInit();
|
|
458
|
+
const packed = wasm.mesh_get_edge_vertex_pairs(this._vertexCount, this._buffer);
|
|
459
|
+
const edgeCount = Math.max(0, Math.floor(packed[0] ?? 0));
|
|
460
|
+
const pairs = [];
|
|
461
|
+
let off = 1;
|
|
462
|
+
for (let i = 0; i < edgeCount; i++) {
|
|
463
|
+
const a = Math.floor(packed[off++] ?? -1);
|
|
464
|
+
const b = Math.floor(packed[off++] ?? -1);
|
|
465
|
+
if (a < 0 || b < 0)
|
|
466
|
+
break;
|
|
467
|
+
pairs.push([a, b]);
|
|
468
|
+
}
|
|
469
|
+
this._edgeVertexPairs = pairs;
|
|
510
470
|
return this._edgeVertexPairs;
|
|
511
471
|
}
|
|
512
472
|
// ── Transforms ────────────────────────────────────────────────
|
|
@@ -642,173 +602,93 @@ export class Mesh {
|
|
|
642
602
|
* Axis-aligned bounds of this mesh.
|
|
643
603
|
*/
|
|
644
604
|
getBounds() {
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
return { min: Point.ORIGIN, max: Point.ORIGIN };
|
|
648
|
-
}
|
|
649
|
-
let minX = Number.POSITIVE_INFINITY;
|
|
650
|
-
let minY = Number.POSITIVE_INFINITY;
|
|
651
|
-
let minZ = Number.POSITIVE_INFINITY;
|
|
652
|
-
let maxX = Number.NEGATIVE_INFINITY;
|
|
653
|
-
let maxY = Number.NEGATIVE_INFINITY;
|
|
654
|
-
let maxZ = Number.NEGATIVE_INFINITY;
|
|
655
|
-
for (let i = 0; i < pos.length; i += 3) {
|
|
656
|
-
const x = pos[i];
|
|
657
|
-
const y = pos[i + 1];
|
|
658
|
-
const z = pos[i + 2];
|
|
659
|
-
if (x < minX)
|
|
660
|
-
minX = x;
|
|
661
|
-
if (y < minY)
|
|
662
|
-
minY = y;
|
|
663
|
-
if (z < minZ)
|
|
664
|
-
minZ = z;
|
|
665
|
-
if (x > maxX)
|
|
666
|
-
maxX = x;
|
|
667
|
-
if (y > maxY)
|
|
668
|
-
maxY = y;
|
|
669
|
-
if (z > maxZ)
|
|
670
|
-
maxZ = z;
|
|
671
|
-
}
|
|
605
|
+
ensureInit();
|
|
606
|
+
const b = wasm.mesh_get_bounds(this._vertexCount, this._buffer);
|
|
672
607
|
return {
|
|
673
|
-
min: new Point(
|
|
674
|
-
max: new Point(
|
|
608
|
+
min: new Point(b[0] ?? 0, b[1] ?? 0, b[2] ?? 0),
|
|
609
|
+
max: new Point(b[3] ?? 0, b[4] ?? 0, b[5] ?? 0),
|
|
675
610
|
};
|
|
676
611
|
}
|
|
677
612
|
/**
|
|
678
613
|
* Unit normal of a triangle face.
|
|
679
614
|
*/
|
|
680
615
|
getFaceNormal(faceIndex) {
|
|
681
|
-
|
|
616
|
+
ensureInit();
|
|
617
|
+
if (!Number.isFinite(faceIndex) || faceIndex < 0)
|
|
682
618
|
return Vec3.Y;
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
const pos = this.positionBuffer;
|
|
686
|
-
const i0 = idx[faceIndex * 3];
|
|
687
|
-
const i1 = idx[faceIndex * 3 + 1];
|
|
688
|
-
const i2 = idx[faceIndex * 3 + 2];
|
|
689
|
-
const off0 = i0 * 3;
|
|
690
|
-
const off1 = i1 * 3;
|
|
691
|
-
const off2 = i2 * 3;
|
|
692
|
-
const normal = Mesh.triangleNormal(pos[off0], pos[off0 + 1], pos[off0 + 2], pos[off1], pos[off1 + 1], pos[off1 + 2], pos[off2], pos[off2 + 1], pos[off2 + 2]);
|
|
693
|
-
return normal ?? Vec3.Y;
|
|
619
|
+
const n = wasm.mesh_get_face_normal(this._vertexCount, this._buffer, Math.floor(faceIndex));
|
|
620
|
+
return new Vec3(n[0] ?? 0, n[1] ?? 1, n[2] ?? 0);
|
|
694
621
|
}
|
|
695
622
|
/**
|
|
696
623
|
* Centroid of a triangle face.
|
|
697
624
|
*/
|
|
698
625
|
getFaceCentroid(faceIndex) {
|
|
699
|
-
|
|
626
|
+
ensureInit();
|
|
627
|
+
if (!Number.isFinite(faceIndex) || faceIndex < 0)
|
|
700
628
|
return Point.ORIGIN;
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
const pos = this.positionBuffer;
|
|
704
|
-
const i0 = idx[faceIndex * 3];
|
|
705
|
-
const i1 = idx[faceIndex * 3 + 1];
|
|
706
|
-
const i2 = idx[faceIndex * 3 + 2];
|
|
707
|
-
const off0 = i0 * 3;
|
|
708
|
-
const off1 = i1 * 3;
|
|
709
|
-
const off2 = i2 * 3;
|
|
710
|
-
return new Point((pos[off0] + pos[off1] + pos[off2]) / 3, (pos[off0 + 1] + pos[off1 + 1] + pos[off2 + 1]) / 3, (pos[off0 + 2] + pos[off1 + 2] + pos[off2 + 2]) / 3);
|
|
629
|
+
const c = wasm.mesh_get_face_centroid(this._vertexCount, this._buffer, Math.floor(faceIndex));
|
|
630
|
+
return new Point(c[0] ?? 0, c[1] ?? 0, c[2] ?? 0);
|
|
711
631
|
}
|
|
712
632
|
/**
|
|
713
633
|
* Area of a triangle face.
|
|
714
634
|
*/
|
|
715
635
|
getFaceArea(faceIndex) {
|
|
716
|
-
|
|
636
|
+
ensureInit();
|
|
637
|
+
if (!Number.isFinite(faceIndex) || faceIndex < 0)
|
|
717
638
|
return 0;
|
|
718
|
-
|
|
719
|
-
const idx = this.indexBuffer;
|
|
720
|
-
const pos = this.positionBuffer;
|
|
721
|
-
const i0 = idx[faceIndex * 3];
|
|
722
|
-
const i1 = idx[faceIndex * 3 + 1];
|
|
723
|
-
const i2 = idx[faceIndex * 3 + 2];
|
|
724
|
-
const aoff = i0 * 3;
|
|
725
|
-
const boff = i1 * 3;
|
|
726
|
-
const coff = i2 * 3;
|
|
727
|
-
const e1x = pos[boff] - pos[aoff];
|
|
728
|
-
const e1y = pos[boff + 1] - pos[aoff + 1];
|
|
729
|
-
const e1z = pos[boff + 2] - pos[aoff + 2];
|
|
730
|
-
const e2x = pos[coff] - pos[aoff];
|
|
731
|
-
const e2y = pos[coff + 1] - pos[aoff + 1];
|
|
732
|
-
const e2z = pos[coff + 2] - pos[aoff + 2];
|
|
733
|
-
const cx = e1y * e2z - e1z * e2y;
|
|
734
|
-
const cy = e1z * e2x - e1x * e2z;
|
|
735
|
-
const cz = e1x * e2y - e1y * e2x;
|
|
736
|
-
return Math.sqrt(cx * cx + cy * cy + cz * cz) * 0.5;
|
|
639
|
+
return wasm.mesh_get_face_area(this._vertexCount, this._buffer, Math.floor(faceIndex));
|
|
737
640
|
}
|
|
738
641
|
/**
|
|
739
642
|
* Find edge-connected coplanar triangles that belong to the same planar face.
|
|
740
643
|
*/
|
|
741
644
|
getCoplanarFaceIndices(faceIndex) {
|
|
742
|
-
|
|
645
|
+
ensureInit();
|
|
646
|
+
if (!Number.isFinite(faceIndex) || faceIndex < 0)
|
|
743
647
|
return [];
|
|
648
|
+
const r = wasm.mesh_get_coplanar_face_indices(this._vertexCount, this._buffer, Math.floor(faceIndex));
|
|
649
|
+
const count = Math.max(0, Math.floor(r[0] ?? 0));
|
|
650
|
+
const out = [];
|
|
651
|
+
for (let i = 0; i < count; i++) {
|
|
652
|
+
out.push(Math.floor(r[1 + i] ?? 0));
|
|
744
653
|
}
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
if (planeDist >= EPSILON)
|
|
763
|
-
continue;
|
|
764
|
-
coplanarCandidates.push(i);
|
|
765
|
-
candidateSet.add(i);
|
|
766
|
-
}
|
|
767
|
-
if (!candidateSet.has(faceIndex)) {
|
|
768
|
-
return [faceIndex];
|
|
769
|
-
}
|
|
770
|
-
const edgeToFaces = new Map();
|
|
771
|
-
const faceEdges = new Map();
|
|
772
|
-
for (const fi of coplanarCandidates) {
|
|
773
|
-
const a = idx[fi * 3];
|
|
774
|
-
const b = idx[fi * 3 + 1];
|
|
775
|
-
const c = idx[fi * 3 + 2];
|
|
776
|
-
const edges = [[a, b], [b, c], [c, a]];
|
|
777
|
-
const keys = [];
|
|
778
|
-
for (const [u, v] of edges) {
|
|
779
|
-
const lo = Math.min(u, v);
|
|
780
|
-
const hi = Math.max(u, v);
|
|
781
|
-
const key = `${lo}_${hi}`;
|
|
782
|
-
keys.push(key);
|
|
783
|
-
const faces = edgeToFaces.get(key);
|
|
784
|
-
if (faces) {
|
|
785
|
-
faces.push(fi);
|
|
786
|
-
}
|
|
787
|
-
else {
|
|
788
|
-
edgeToFaces.set(key, [fi]);
|
|
789
|
-
}
|
|
790
|
-
}
|
|
791
|
-
faceEdges.set(fi, keys);
|
|
792
|
-
}
|
|
793
|
-
const connected = [];
|
|
794
|
-
const visited = new Set();
|
|
795
|
-
const queue = [faceIndex];
|
|
796
|
-
visited.add(faceIndex);
|
|
797
|
-
while (queue.length > 0) {
|
|
798
|
-
const current = queue.shift();
|
|
799
|
-
connected.push(current);
|
|
800
|
-
const keys = faceEdges.get(current) ?? [];
|
|
801
|
-
for (const key of keys) {
|
|
802
|
-
const neighbors = edgeToFaces.get(key) ?? [];
|
|
803
|
-
for (const neighbor of neighbors) {
|
|
804
|
-
if (!candidateSet.has(neighbor) || visited.has(neighbor))
|
|
805
|
-
continue;
|
|
806
|
-
visited.add(neighbor);
|
|
807
|
-
queue.push(neighbor);
|
|
808
|
-
}
|
|
809
|
-
}
|
|
654
|
+
return out;
|
|
655
|
+
}
|
|
656
|
+
/**
|
|
657
|
+
* Coplanar connected face region and its projection bounds on a plane basis.
|
|
658
|
+
* Returns null for invalid inputs or empty regions.
|
|
659
|
+
*/
|
|
660
|
+
getCoplanarFaceRegion(faceIndex, origin, uAxis, vAxis, marginScale = 0.1, minMargin = 0.2) {
|
|
661
|
+
ensureInit();
|
|
662
|
+
if (!Number.isFinite(faceIndex) || faceIndex < 0)
|
|
663
|
+
return null;
|
|
664
|
+
const r = wasm.mesh_get_coplanar_face_region(this._vertexCount, this._buffer, Math.floor(faceIndex), origin.x, origin.y, origin.z, uAxis.x, uAxis.y, uAxis.z, vAxis.x, vAxis.y, vAxis.z, marginScale, minMargin);
|
|
665
|
+
const count = Math.max(0, Math.floor(r[0] ?? 0));
|
|
666
|
+
if (count <= 0 || r.length < 1 + count + 4)
|
|
667
|
+
return null;
|
|
668
|
+
const faceIndices = [];
|
|
669
|
+
for (let i = 0; i < count; i++) {
|
|
670
|
+
faceIndices.push(Math.floor(r[1 + i] ?? 0));
|
|
810
671
|
}
|
|
811
|
-
|
|
672
|
+
const off = 1 + count;
|
|
673
|
+
return {
|
|
674
|
+
faceIndices,
|
|
675
|
+
uMin: r[off],
|
|
676
|
+
uMax: r[off + 1],
|
|
677
|
+
vMin: r[off + 2],
|
|
678
|
+
vMax: r[off + 3],
|
|
679
|
+
};
|
|
680
|
+
}
|
|
681
|
+
/**
|
|
682
|
+
* Centroid of the coplanar edge-connected face group containing faceIndex.
|
|
683
|
+
*/
|
|
684
|
+
getCoplanarFaceGroupCentroid(faceIndex) {
|
|
685
|
+
ensureInit();
|
|
686
|
+
if (!Number.isFinite(faceIndex) || faceIndex < 0)
|
|
687
|
+
return null;
|
|
688
|
+
const r = wasm.mesh_get_coplanar_face_group_centroid(this._vertexCount, this._buffer, Math.floor(faceIndex));
|
|
689
|
+
if (!r || r.length < 3)
|
|
690
|
+
return null;
|
|
691
|
+
return new Point(r[0], r[1], r[2]);
|
|
812
692
|
}
|
|
813
693
|
/**
|
|
814
694
|
* Unique edge count for this triangulated mesh.
|
|
@@ -837,207 +717,62 @@ export class Mesh {
|
|
|
837
717
|
* Raycast against this mesh and return nearest hit.
|
|
838
718
|
*/
|
|
839
719
|
raycast(origin, direction, maxDistance = Number.POSITIVE_INFINITY) {
|
|
840
|
-
|
|
841
|
-
|
|
720
|
+
ensureInit();
|
|
721
|
+
const r = wasm.mesh_raycast(this._vertexCount, this._buffer, origin.x, origin.y, origin.z, direction.x, direction.y, direction.z, maxDistance);
|
|
722
|
+
if (!r || r.length < 8)
|
|
723
|
+
return null;
|
|
724
|
+
return {
|
|
725
|
+
point: new Point(r[0], r[1], r[2]),
|
|
726
|
+
normal: new Vec3(r[3], r[4], r[5]),
|
|
727
|
+
faceIndex: Math.floor(r[6]),
|
|
728
|
+
distance: r[7],
|
|
729
|
+
};
|
|
842
730
|
}
|
|
843
731
|
/**
|
|
844
732
|
* Raycast against this mesh and return all hits sorted by distance.
|
|
845
733
|
*/
|
|
846
734
|
raycastAll(origin, direction, maxDistance = Number.POSITIVE_INFINITY) {
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
const dir = direction.scale(1 / dirLen);
|
|
735
|
+
ensureInit();
|
|
736
|
+
const buf = wasm.mesh_raycast_all(this._vertexCount, this._buffer, origin.x, origin.y, origin.z, direction.x, direction.y, direction.z, maxDistance);
|
|
737
|
+
const count = Math.max(0, Math.floor(buf[0] ?? 0));
|
|
851
738
|
const hits = [];
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
const triCount = idx.length / 3;
|
|
855
|
-
for (let f = 0; f < triCount; f++) {
|
|
856
|
-
const i0 = idx[f * 3];
|
|
857
|
-
const i1 = idx[f * 3 + 1];
|
|
858
|
-
const i2 = idx[f * 3 + 2];
|
|
859
|
-
const off0 = i0 * 3;
|
|
860
|
-
const off1 = i1 * 3;
|
|
861
|
-
const off2 = i2 * 3;
|
|
862
|
-
const t = Mesh.rayTriangleDistance(origin.x, origin.y, origin.z, dir.x, dir.y, dir.z, pos[off0], pos[off0 + 1], pos[off0 + 2], pos[off1], pos[off1 + 1], pos[off1 + 2], pos[off2], pos[off2 + 1], pos[off2 + 2]);
|
|
863
|
-
if (t === null || !Number.isFinite(t) || t > maxDistance)
|
|
864
|
-
continue;
|
|
739
|
+
let off = 1;
|
|
740
|
+
for (let i = 0; i < count; i++) {
|
|
865
741
|
hits.push({
|
|
866
|
-
point: new Point(
|
|
867
|
-
normal:
|
|
868
|
-
faceIndex:
|
|
869
|
-
distance:
|
|
742
|
+
point: new Point(buf[off], buf[off + 1], buf[off + 2]),
|
|
743
|
+
normal: new Vec3(buf[off + 3], buf[off + 4], buf[off + 5]),
|
|
744
|
+
faceIndex: Math.floor(buf[off + 6]),
|
|
745
|
+
distance: buf[off + 7],
|
|
870
746
|
});
|
|
747
|
+
off += 8;
|
|
871
748
|
}
|
|
872
|
-
hits.sort((a, b) => a.distance - b.distance);
|
|
873
749
|
return hits;
|
|
874
750
|
}
|
|
875
751
|
/**
|
|
876
752
|
* Push/pull a planar face set by moving its coplanar connected region.
|
|
877
753
|
*/
|
|
878
754
|
extrudeFace(faceIndex, distance) {
|
|
879
|
-
|
|
755
|
+
ensureInit();
|
|
756
|
+
if (!Number.isFinite(faceIndex) || faceIndex < 0) {
|
|
880
757
|
return Mesh.fromBuffer(new Float64Array(this._buffer));
|
|
881
758
|
}
|
|
882
|
-
|
|
883
|
-
const coplanarFaces = this.getCoplanarFaceIndices(faceIndex);
|
|
884
|
-
const coplanarFaceSet = new Set(coplanarFaces);
|
|
885
|
-
const idx = this.indexBuffer;
|
|
886
|
-
const pos = this.positionBuffer;
|
|
887
|
-
const vertexCount = this.vertexCount;
|
|
888
|
-
const faceCount = this.faceCount;
|
|
889
|
-
const verts = new Float64Array(vertexCount * 3);
|
|
890
|
-
for (let i = 0; i < verts.length; i++) {
|
|
891
|
-
verts[i] = pos[i];
|
|
892
|
-
}
|
|
893
|
-
const coplanarVertexSet = new Set();
|
|
894
|
-
for (const fi of coplanarFaces) {
|
|
895
|
-
coplanarVertexSet.add(idx[fi * 3]);
|
|
896
|
-
coplanarVertexSet.add(idx[fi * 3 + 1]);
|
|
897
|
-
coplanarVertexSet.add(idx[fi * 3 + 2]);
|
|
898
|
-
}
|
|
899
|
-
const POS_EPSILON_SQ = 1e-10;
|
|
900
|
-
const NORMAL_EPSILON = 0.01;
|
|
901
|
-
const uniquePositions = [];
|
|
902
|
-
const assignedCoplanar = new Set();
|
|
903
|
-
for (const ci of coplanarVertexSet) {
|
|
904
|
-
if (assignedCoplanar.has(ci))
|
|
905
|
-
continue;
|
|
906
|
-
const cx = verts[ci * 3];
|
|
907
|
-
const cy = verts[ci * 3 + 1];
|
|
908
|
-
const cz = verts[ci * 3 + 2];
|
|
909
|
-
const allAtPos = [];
|
|
910
|
-
for (let vi = 0; vi < vertexCount; vi++) {
|
|
911
|
-
const dx = verts[vi * 3] - cx;
|
|
912
|
-
const dy = verts[vi * 3 + 1] - cy;
|
|
913
|
-
const dz = verts[vi * 3 + 2] - cz;
|
|
914
|
-
if (dx * dx + dy * dy + dz * dz < POS_EPSILON_SQ) {
|
|
915
|
-
allAtPos.push(vi);
|
|
916
|
-
if (coplanarVertexSet.has(vi)) {
|
|
917
|
-
assignedCoplanar.add(vi);
|
|
918
|
-
}
|
|
919
|
-
}
|
|
920
|
-
}
|
|
921
|
-
uniquePositions.push({ x: cx, y: cy, z: cz, allVertexIndices: allAtPos });
|
|
922
|
-
}
|
|
923
|
-
const refVi = idx[faceIndex * 3];
|
|
924
|
-
const pushOrigin = new Point(verts[refVi * 3] + pushNormal.x * distance, verts[refVi * 3 + 1] + pushNormal.y * distance, verts[refVi * 3 + 2] + pushNormal.z * distance);
|
|
925
|
-
const pushPlane = new Plane(pushOrigin, pushNormal);
|
|
926
|
-
const newVerts = new Float64Array(verts);
|
|
927
|
-
for (const upos of uniquePositions) {
|
|
928
|
-
const adjacentFaceIndices = [];
|
|
929
|
-
for (let fi = 0; fi < faceCount; fi++) {
|
|
930
|
-
if (coplanarFaceSet.has(fi))
|
|
931
|
-
continue;
|
|
932
|
-
for (let k = 0; k < 3; k++) {
|
|
933
|
-
const vi = idx[fi * 3 + k];
|
|
934
|
-
const dx = verts[vi * 3] - upos.x;
|
|
935
|
-
const dy = verts[vi * 3 + 1] - upos.y;
|
|
936
|
-
const dz = verts[vi * 3 + 2] - upos.z;
|
|
937
|
-
if (dx * dx + dy * dy + dz * dz < POS_EPSILON_SQ) {
|
|
938
|
-
adjacentFaceIndices.push(fi);
|
|
939
|
-
break;
|
|
940
|
-
}
|
|
941
|
-
}
|
|
942
|
-
}
|
|
943
|
-
const adjacentPlanes = [];
|
|
944
|
-
for (const fi of adjacentFaceIndices) {
|
|
945
|
-
const n = this.getFaceNormal(fi);
|
|
946
|
-
let merged = false;
|
|
947
|
-
for (const existing of adjacentPlanes) {
|
|
948
|
-
const dot = n.dot(existing.normal);
|
|
949
|
-
if (dot > 1 - NORMAL_EPSILON) {
|
|
950
|
-
merged = true;
|
|
951
|
-
break;
|
|
952
|
-
}
|
|
953
|
-
}
|
|
954
|
-
if (!merged) {
|
|
955
|
-
const rv = idx[fi * 3];
|
|
956
|
-
const origin = new Point(verts[rv * 3], verts[rv * 3 + 1], verts[rv * 3 + 2]);
|
|
957
|
-
adjacentPlanes.push(new Plane(origin, n));
|
|
958
|
-
}
|
|
959
|
-
}
|
|
960
|
-
let newPos = null;
|
|
961
|
-
if (adjacentPlanes.length >= 2) {
|
|
962
|
-
const pt = Plane.intersect3(pushPlane, adjacentPlanes[0], adjacentPlanes[1]);
|
|
963
|
-
if (pt) {
|
|
964
|
-
newPos = pt;
|
|
965
|
-
}
|
|
966
|
-
}
|
|
967
|
-
if (!newPos) {
|
|
968
|
-
newPos = new Point(upos.x + pushNormal.x * distance, upos.y + pushNormal.y * distance, upos.z + pushNormal.z * distance);
|
|
969
|
-
}
|
|
970
|
-
for (const vi of upos.allVertexIndices) {
|
|
971
|
-
newVerts[vi * 3] = newPos.x;
|
|
972
|
-
newVerts[vi * 3 + 1] = newPos.y;
|
|
973
|
-
newVerts[vi * 3 + 2] = newPos.z;
|
|
974
|
-
}
|
|
975
|
-
}
|
|
976
|
-
return Mesh.fromPositionsAndIndices(newVerts, idx);
|
|
759
|
+
return Mesh.fromBuffer(wasm.mesh_extrude_face(this._vertexCount, this._buffer, Math.floor(faceIndex), distance));
|
|
977
760
|
}
|
|
978
761
|
/**
|
|
979
762
|
* Check if this triangulated mesh represents a closed volume.
|
|
980
|
-
* Returns true when no topological boundary edges are found.
|
|
981
|
-
*
|
|
982
|
-
* Uses kernel boundary extraction first, then falls back to a welded
|
|
983
|
-
* edge-incidence pass to tolerate duplicated seam vertices.
|
|
763
|
+
* Returns true when no welded topological boundary edges are found.
|
|
984
764
|
*/
|
|
985
765
|
isClosedVolume() {
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
try {
|
|
989
|
-
if (this.boundaryPolylines().length === 0)
|
|
990
|
-
return true;
|
|
991
|
-
}
|
|
992
|
-
catch {
|
|
993
|
-
// Fall through to welded index analysis.
|
|
994
|
-
}
|
|
995
|
-
const welded = this.buildWeldedVertexIndexMap();
|
|
996
|
-
const edgeCounts = new Map();
|
|
997
|
-
const idx = this.indexBuffer;
|
|
998
|
-
for (let i = 0; i + 2 < idx.length; i += 3) {
|
|
999
|
-
const ai = idx[i];
|
|
1000
|
-
const bi = idx[i + 1];
|
|
1001
|
-
const ci = idx[i + 2];
|
|
1002
|
-
if (ai >= welded.length || bi >= welded.length || ci >= welded.length) {
|
|
1003
|
-
return false;
|
|
1004
|
-
}
|
|
1005
|
-
const a = welded[ai];
|
|
1006
|
-
const b = welded[bi];
|
|
1007
|
-
const c = welded[ci];
|
|
1008
|
-
if (a === b || b === c || c === a)
|
|
1009
|
-
continue;
|
|
1010
|
-
const edges = [[a, b], [b, c], [c, a]];
|
|
1011
|
-
for (const [u, v] of edges) {
|
|
1012
|
-
const lo = Math.min(u, v);
|
|
1013
|
-
const hi = Math.max(u, v);
|
|
1014
|
-
const key = `${lo}_${hi}`;
|
|
1015
|
-
edgeCounts.set(key, (edgeCounts.get(key) ?? 0) + 1);
|
|
1016
|
-
}
|
|
1017
|
-
}
|
|
1018
|
-
for (const count of edgeCounts.values()) {
|
|
1019
|
-
if (count === 1)
|
|
1020
|
-
return false;
|
|
1021
|
-
}
|
|
1022
|
-
return true;
|
|
766
|
+
ensureInit();
|
|
767
|
+
return wasm.mesh_is_closed_volume(this._vertexCount, this._buffer);
|
|
1023
768
|
}
|
|
1024
769
|
/**
|
|
1025
770
|
* Odd/even point containment test against a closed mesh.
|
|
1026
771
|
* Uses majority vote across multiple ray directions for robustness.
|
|
1027
772
|
*/
|
|
1028
773
|
containsPoint(point) {
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
new Vec3(0.6227991553292184, -0.2075997184430728, 0.7547910549471614),
|
|
1032
|
-
new Vec3(-0.1708715315433522, 0.9405985944884371, 0.29377431425456585),
|
|
1033
|
-
];
|
|
1034
|
-
let insideVotes = 0;
|
|
1035
|
-
for (const dir of directions) {
|
|
1036
|
-
const crossings = this.countRayCrossings(point, dir);
|
|
1037
|
-
if ((crossings & 1) === 1)
|
|
1038
|
-
insideVotes += 1;
|
|
1039
|
-
}
|
|
1040
|
-
return insideVotes >= Math.ceil(directions.length / 2);
|
|
774
|
+
ensureInit();
|
|
775
|
+
return wasm.mesh_contains_point(this._vertexCount, this._buffer, point.x, point.y, point.z);
|
|
1041
776
|
}
|
|
1042
777
|
/**
|
|
1043
778
|
* Find the coplanar + edge-connected face group containing a triangle.
|
|
@@ -1045,333 +780,28 @@ export class Mesh {
|
|
|
1045
780
|
findFaceByTriangleIndex(triangleIndex) {
|
|
1046
781
|
if (!Number.isFinite(triangleIndex) || triangleIndex < 0)
|
|
1047
782
|
return null;
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
783
|
+
ensureInit();
|
|
784
|
+
const r = wasm.mesh_find_face_group_by_triangle_index(this._vertexCount, this._buffer, Math.floor(triangleIndex));
|
|
785
|
+
if (!r || r.length < 6)
|
|
786
|
+
return null;
|
|
787
|
+
return {
|
|
788
|
+
centroid: new Point(r[0], r[1], r[2]),
|
|
789
|
+
normal: new Vec3(r[3], r[4], r[5]),
|
|
790
|
+
};
|
|
1055
791
|
}
|
|
1056
792
|
/**
|
|
1057
793
|
* Find the best matching coplanar + edge-connected face group by normal
|
|
1058
794
|
* similarity and optional point proximity.
|
|
1059
795
|
*/
|
|
1060
796
|
findFaceByNormal(targetNormal, nearPoint) {
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
let best = null;
|
|
1065
|
-
let bestScore = Number.NEGATIVE_INFINITY;
|
|
1066
|
-
for (const group of groups) {
|
|
1067
|
-
const normal = group.normal;
|
|
1068
|
-
const centroid = group.centroid;
|
|
1069
|
-
const normalScore = Math.max(0, normal.dot(targetNormal));
|
|
1070
|
-
let score;
|
|
1071
|
-
if (nearPoint) {
|
|
1072
|
-
const dx = centroid.x - nearPoint.x;
|
|
1073
|
-
const dy = centroid.y - nearPoint.y;
|
|
1074
|
-
const dz = centroid.z - nearPoint.z;
|
|
1075
|
-
const distSq = dx * dx + dy * dy + dz * dz;
|
|
1076
|
-
const proximityScore = 1.0 / (1.0 + distSq);
|
|
1077
|
-
score = proximityScore * (0.5 + 0.5 * normalScore);
|
|
1078
|
-
}
|
|
1079
|
-
else {
|
|
1080
|
-
score = normalScore;
|
|
1081
|
-
}
|
|
1082
|
-
if (score > bestScore) {
|
|
1083
|
-
bestScore = score;
|
|
1084
|
-
best = { centroid, normal };
|
|
1085
|
-
}
|
|
1086
|
-
}
|
|
1087
|
-
return best;
|
|
1088
|
-
}
|
|
1089
|
-
/**
|
|
1090
|
-
* Count unique ray/triangle crossings from origin along direction.
|
|
1091
|
-
* Distances are deduplicated to collapse paired hits on triangulated quads.
|
|
1092
|
-
*/
|
|
1093
|
-
countRayCrossings(origin, direction) {
|
|
1094
|
-
const MIN_DISTANCE = 1e-6;
|
|
1095
|
-
const MERGE_DISTANCE = 1e-5;
|
|
1096
|
-
const distances = [];
|
|
1097
|
-
const idx = this.indexBuffer;
|
|
1098
|
-
const verts = this.positionBuffer;
|
|
1099
|
-
for (let i = 0; i + 2 < idx.length; i += 3) {
|
|
1100
|
-
const i0 = idx[i];
|
|
1101
|
-
const i1 = idx[i + 1];
|
|
1102
|
-
const i2 = idx[i + 2];
|
|
1103
|
-
const off0 = i0 * 3;
|
|
1104
|
-
const off1 = i1 * 3;
|
|
1105
|
-
const off2 = i2 * 3;
|
|
1106
|
-
const t = Mesh.rayTriangleDistance(origin.x, origin.y, origin.z, direction.x, direction.y, direction.z, verts[off0], verts[off0 + 1], verts[off0 + 2], verts[off1], verts[off1 + 1], verts[off1 + 2], verts[off2], verts[off2 + 1], verts[off2 + 2]);
|
|
1107
|
-
if (t !== null && Number.isFinite(t) && t > MIN_DISTANCE) {
|
|
1108
|
-
distances.push(t);
|
|
1109
|
-
}
|
|
1110
|
-
}
|
|
1111
|
-
if (distances.length === 0)
|
|
1112
|
-
return 0;
|
|
1113
|
-
distances.sort((a, b) => a - b);
|
|
1114
|
-
let crossings = 0;
|
|
1115
|
-
let lastDistance = -Infinity;
|
|
1116
|
-
for (const d of distances) {
|
|
1117
|
-
if (Math.abs(d - lastDistance) <= MERGE_DISTANCE)
|
|
1118
|
-
continue;
|
|
1119
|
-
crossings += 1;
|
|
1120
|
-
lastDistance = d;
|
|
1121
|
-
}
|
|
1122
|
-
return crossings;
|
|
1123
|
-
}
|
|
1124
|
-
buildCoplanarConnectedFaceGroups() {
|
|
1125
|
-
const NORMAL_GROUP_EPS = 0.05;
|
|
1126
|
-
const PLANE_EPS = 0.1;
|
|
1127
|
-
const idx = this.indexBuffer;
|
|
1128
|
-
const verts = this.positionBuffer;
|
|
1129
|
-
const triCount = idx.length / 3;
|
|
1130
|
-
const tris = [];
|
|
1131
|
-
for (let t = 0; t < triCount; t++) {
|
|
1132
|
-
const i0 = idx[t * 3];
|
|
1133
|
-
const i1 = idx[t * 3 + 1];
|
|
1134
|
-
const i2 = idx[t * 3 + 2];
|
|
1135
|
-
const aoff = i0 * 3;
|
|
1136
|
-
const boff = i1 * 3;
|
|
1137
|
-
const coff = i2 * 3;
|
|
1138
|
-
const ax = verts[aoff];
|
|
1139
|
-
const ay = verts[aoff + 1];
|
|
1140
|
-
const az = verts[aoff + 2];
|
|
1141
|
-
const bx = verts[boff];
|
|
1142
|
-
const by = verts[boff + 1];
|
|
1143
|
-
const bz = verts[boff + 2];
|
|
1144
|
-
const cx = verts[coff];
|
|
1145
|
-
const cy = verts[coff + 1];
|
|
1146
|
-
const cz = verts[coff + 2];
|
|
1147
|
-
const normal = Mesh.triangleNormal(ax, ay, az, bx, by, bz, cx, cy, cz);
|
|
1148
|
-
if (!normal)
|
|
1149
|
-
continue;
|
|
1150
|
-
tris.push({
|
|
1151
|
-
triIndex: t,
|
|
1152
|
-
i0,
|
|
1153
|
-
i1,
|
|
1154
|
-
i2,
|
|
1155
|
-
cx: (ax + bx + cx) / 3,
|
|
1156
|
-
cy: (ay + by + cy) / 3,
|
|
1157
|
-
cz: (az + bz + cz) / 3,
|
|
1158
|
-
nx: normal.x,
|
|
1159
|
-
ny: normal.y,
|
|
1160
|
-
nz: normal.z,
|
|
1161
|
-
});
|
|
1162
|
-
}
|
|
1163
|
-
if (tris.length === 0)
|
|
1164
|
-
return [];
|
|
1165
|
-
const triToNeighbors = new Map();
|
|
1166
|
-
const edgeToTris = new Map();
|
|
1167
|
-
for (let i = 0; i < tris.length; i++) {
|
|
1168
|
-
const tri = tris[i];
|
|
1169
|
-
const edges = [
|
|
1170
|
-
[tri.i0, tri.i1],
|
|
1171
|
-
[tri.i1, tri.i2],
|
|
1172
|
-
[tri.i2, tri.i0],
|
|
1173
|
-
];
|
|
1174
|
-
for (const [a, b] of edges) {
|
|
1175
|
-
const key = Mesh.edgeKey(a, b);
|
|
1176
|
-
const bucket = edgeToTris.get(key);
|
|
1177
|
-
if (bucket) {
|
|
1178
|
-
bucket.push(i);
|
|
1179
|
-
}
|
|
1180
|
-
else {
|
|
1181
|
-
edgeToTris.set(key, [i]);
|
|
1182
|
-
}
|
|
1183
|
-
}
|
|
1184
|
-
}
|
|
1185
|
-
for (const bucket of edgeToTris.values()) {
|
|
1186
|
-
for (let i = 0; i < bucket.length; i++) {
|
|
1187
|
-
const a = bucket[i];
|
|
1188
|
-
let set = triToNeighbors.get(a);
|
|
1189
|
-
if (!set) {
|
|
1190
|
-
set = new Set();
|
|
1191
|
-
triToNeighbors.set(a, set);
|
|
1192
|
-
}
|
|
1193
|
-
for (let j = 0; j < bucket.length; j++) {
|
|
1194
|
-
if (i === j)
|
|
1195
|
-
continue;
|
|
1196
|
-
set.add(bucket[j]);
|
|
1197
|
-
}
|
|
1198
|
-
}
|
|
1199
|
-
}
|
|
1200
|
-
const visited = new Uint8Array(tris.length);
|
|
1201
|
-
const groups = [];
|
|
1202
|
-
for (let seedIdx = 0; seedIdx < tris.length; seedIdx++) {
|
|
1203
|
-
if (visited[seedIdx])
|
|
1204
|
-
continue;
|
|
1205
|
-
const seed = tris[seedIdx];
|
|
1206
|
-
const queue = [seedIdx];
|
|
1207
|
-
visited[seedIdx] = 1;
|
|
1208
|
-
const groupTris = [];
|
|
1209
|
-
while (queue.length > 0) {
|
|
1210
|
-
const idxInTris = queue.pop();
|
|
1211
|
-
const tri = tris[idxInTris];
|
|
1212
|
-
groupTris.push(tri);
|
|
1213
|
-
const neighbors = triToNeighbors.get(idxInTris);
|
|
1214
|
-
if (!neighbors)
|
|
1215
|
-
continue;
|
|
1216
|
-
for (const nIdx of neighbors) {
|
|
1217
|
-
if (visited[nIdx])
|
|
1218
|
-
continue;
|
|
1219
|
-
const candidate = tris[nIdx];
|
|
1220
|
-
const dot = seed.nx * candidate.nx + seed.ny * candidate.ny + seed.nz * candidate.nz;
|
|
1221
|
-
if (dot < 1 - NORMAL_GROUP_EPS)
|
|
1222
|
-
continue;
|
|
1223
|
-
const seedPlane = seed.cx * seed.nx + seed.cy * seed.ny + seed.cz * seed.nz;
|
|
1224
|
-
const candidatePlane = candidate.cx * seed.nx + candidate.cy * seed.ny + candidate.cz * seed.nz;
|
|
1225
|
-
if (Math.abs(candidatePlane - seedPlane) >= PLANE_EPS)
|
|
1226
|
-
continue;
|
|
1227
|
-
visited[nIdx] = 1;
|
|
1228
|
-
queue.push(nIdx);
|
|
1229
|
-
}
|
|
1230
|
-
}
|
|
1231
|
-
if (groupTris.length === 0)
|
|
1232
|
-
continue;
|
|
1233
|
-
let cx = 0;
|
|
1234
|
-
let cy = 0;
|
|
1235
|
-
let cz = 0;
|
|
1236
|
-
let nx = 0;
|
|
1237
|
-
let ny = 0;
|
|
1238
|
-
let nz = 0;
|
|
1239
|
-
const triangleIndices = [];
|
|
1240
|
-
for (const tri of groupTris) {
|
|
1241
|
-
cx += tri.cx;
|
|
1242
|
-
cy += tri.cy;
|
|
1243
|
-
cz += tri.cz;
|
|
1244
|
-
nx += tri.nx;
|
|
1245
|
-
ny += tri.ny;
|
|
1246
|
-
nz += tri.nz;
|
|
1247
|
-
triangleIndices.push(tri.triIndex);
|
|
1248
|
-
}
|
|
1249
|
-
const invCount = 1 / groupTris.length;
|
|
1250
|
-
cx *= invCount;
|
|
1251
|
-
cy *= invCount;
|
|
1252
|
-
cz *= invCount;
|
|
1253
|
-
nx *= invCount;
|
|
1254
|
-
ny *= invCount;
|
|
1255
|
-
nz *= invCount;
|
|
1256
|
-
const len = Math.sqrt(nx * nx + ny * ny + nz * nz);
|
|
1257
|
-
if (len < 1e-12)
|
|
1258
|
-
continue;
|
|
1259
|
-
groups.push({
|
|
1260
|
-
triangleIndices,
|
|
1261
|
-
centroid: new Point(cx, cy, cz),
|
|
1262
|
-
normal: new Vec3(nx / len, ny / len, nz / len),
|
|
1263
|
-
});
|
|
1264
|
-
}
|
|
1265
|
-
return groups;
|
|
1266
|
-
}
|
|
1267
|
-
/**
|
|
1268
|
-
* Map each raw vertex index to a welded topological vertex index.
|
|
1269
|
-
* Uses scale-aware quantization to merge duplicate seam vertices.
|
|
1270
|
-
*/
|
|
1271
|
-
buildWeldedVertexIndexMap() {
|
|
1272
|
-
const pos = this.positionBuffer;
|
|
1273
|
-
const vertexCount = pos.length / 3;
|
|
1274
|
-
const map = new Uint32Array(vertexCount);
|
|
1275
|
-
if (vertexCount === 0)
|
|
1276
|
-
return map;
|
|
1277
|
-
let minX = Number.POSITIVE_INFINITY;
|
|
1278
|
-
let minY = Number.POSITIVE_INFINITY;
|
|
1279
|
-
let minZ = Number.POSITIVE_INFINITY;
|
|
1280
|
-
let maxX = Number.NEGATIVE_INFINITY;
|
|
1281
|
-
let maxY = Number.NEGATIVE_INFINITY;
|
|
1282
|
-
let maxZ = Number.NEGATIVE_INFINITY;
|
|
1283
|
-
for (let i = 0; i < vertexCount; i++) {
|
|
1284
|
-
const x = pos[i * 3];
|
|
1285
|
-
const y = pos[i * 3 + 1];
|
|
1286
|
-
const z = pos[i * 3 + 2];
|
|
1287
|
-
if (x < minX)
|
|
1288
|
-
minX = x;
|
|
1289
|
-
if (y < minY)
|
|
1290
|
-
minY = y;
|
|
1291
|
-
if (z < minZ)
|
|
1292
|
-
minZ = z;
|
|
1293
|
-
if (x > maxX)
|
|
1294
|
-
maxX = x;
|
|
1295
|
-
if (y > maxY)
|
|
1296
|
-
maxY = y;
|
|
1297
|
-
if (z > maxZ)
|
|
1298
|
-
maxZ = z;
|
|
1299
|
-
}
|
|
1300
|
-
const dx = maxX - minX;
|
|
1301
|
-
const dy = maxY - minY;
|
|
1302
|
-
const dz = maxZ - minZ;
|
|
1303
|
-
const diag = Math.sqrt(dx * dx + dy * dy + dz * dz);
|
|
1304
|
-
const tol = Math.max(1e-9, diag * 1e-8);
|
|
1305
|
-
const invTol = 1 / tol;
|
|
1306
|
-
const weldedByKey = new Map();
|
|
1307
|
-
let nextWelded = 0;
|
|
1308
|
-
for (let i = 0; i < vertexCount; i++) {
|
|
1309
|
-
const x = pos[i * 3];
|
|
1310
|
-
const y = pos[i * 3 + 1];
|
|
1311
|
-
const z = pos[i * 3 + 2];
|
|
1312
|
-
const qx = Math.round(x * invTol);
|
|
1313
|
-
const qy = Math.round(y * invTol);
|
|
1314
|
-
const qz = Math.round(z * invTol);
|
|
1315
|
-
const key = `${qx}_${qy}_${qz}`;
|
|
1316
|
-
let welded = weldedByKey.get(key);
|
|
1317
|
-
if (welded === undefined) {
|
|
1318
|
-
welded = nextWelded++;
|
|
1319
|
-
weldedByKey.set(key, welded);
|
|
1320
|
-
}
|
|
1321
|
-
map[i] = welded;
|
|
1322
|
-
}
|
|
1323
|
-
return map;
|
|
1324
|
-
}
|
|
1325
|
-
/**
|
|
1326
|
-
* Moller-Trumbore ray/triangle intersection distance.
|
|
1327
|
-
*/
|
|
1328
|
-
static rayTriangleDistance(ox, oy, oz, dx, dy, dz, v0x, v0y, v0z, v1x, v1y, v1z, v2x, v2y, v2z) {
|
|
1329
|
-
const EPSILON = 1e-7;
|
|
1330
|
-
const e1x = v1x - v0x;
|
|
1331
|
-
const e1y = v1y - v0y;
|
|
1332
|
-
const e1z = v1z - v0z;
|
|
1333
|
-
const e2x = v2x - v0x;
|
|
1334
|
-
const e2y = v2y - v0y;
|
|
1335
|
-
const e2z = v2z - v0z;
|
|
1336
|
-
const hx = dy * e2z - dz * e2y;
|
|
1337
|
-
const hy = dz * e2x - dx * e2z;
|
|
1338
|
-
const hz = dx * e2y - dy * e2x;
|
|
1339
|
-
const a = e1x * hx + e1y * hy + e1z * hz;
|
|
1340
|
-
if (a > -EPSILON && a < EPSILON)
|
|
1341
|
-
return null;
|
|
1342
|
-
const f = 1 / a;
|
|
1343
|
-
const sx = ox - v0x;
|
|
1344
|
-
const sy = oy - v0y;
|
|
1345
|
-
const sz = oz - v0z;
|
|
1346
|
-
const u = f * (sx * hx + sy * hy + sz * hz);
|
|
1347
|
-
if (u < 0 || u > 1)
|
|
1348
|
-
return null;
|
|
1349
|
-
const qx = sy * e1z - sz * e1y;
|
|
1350
|
-
const qy = sz * e1x - sx * e1z;
|
|
1351
|
-
const qz = sx * e1y - sy * e1x;
|
|
1352
|
-
const v = f * (dx * qx + dy * qy + dz * qz);
|
|
1353
|
-
if (v < 0 || u + v > 1)
|
|
1354
|
-
return null;
|
|
1355
|
-
const t = f * (e2x * qx + e2y * qy + e2z * qz);
|
|
1356
|
-
return t > EPSILON ? t : null;
|
|
1357
|
-
}
|
|
1358
|
-
static triangleNormal(ax, ay, az, bx, by, bz, cx, cy, cz) {
|
|
1359
|
-
const e1x = bx - ax;
|
|
1360
|
-
const e1y = by - ay;
|
|
1361
|
-
const e1z = bz - az;
|
|
1362
|
-
const e2x = cx - ax;
|
|
1363
|
-
const e2y = cy - ay;
|
|
1364
|
-
const e2z = cz - az;
|
|
1365
|
-
const nx = e1y * e2z - e1z * e2y;
|
|
1366
|
-
const ny = e1z * e2x - e1x * e2z;
|
|
1367
|
-
const nz = e1x * e2y - e1y * e2x;
|
|
1368
|
-
const len = Math.sqrt(nx * nx + ny * ny + nz * nz);
|
|
1369
|
-
if (len < 1e-12)
|
|
797
|
+
ensureInit();
|
|
798
|
+
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);
|
|
799
|
+
if (!r || r.length < 6)
|
|
1370
800
|
return null;
|
|
1371
|
-
return
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
801
|
+
return {
|
|
802
|
+
centroid: new Point(r[0], r[1], r[2]),
|
|
803
|
+
normal: new Vec3(r[3], r[4], r[5]),
|
|
804
|
+
};
|
|
1375
805
|
}
|
|
1376
806
|
// ── Export ──────────────────────────────────────────────────────
|
|
1377
807
|
/**
|