okgeometry-api 0.4.1 → 0.4.3
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 +103 -15
- package/dist/Mesh.d.ts.map +1 -1
- package/dist/Mesh.js +290 -177
- 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 +99 -0
- package/wasm/okgeometrycore.js +1 -1
- package/wasm/okgeometrycore_bg.js +321 -0
- package/wasm/okgeometrycore_bg.wasm +0 -0
- package/wasm/okgeometrycore_bg.wasm.d.ts +18 -0
package/dist/Mesh.js
CHANGED
|
@@ -23,10 +23,11 @@ export class Mesh {
|
|
|
23
23
|
this._indexBuffer = null;
|
|
24
24
|
this._vertices = null;
|
|
25
25
|
this._faces = null;
|
|
26
|
+
this._edgeVertexPairs = null;
|
|
26
27
|
this._buffer = buffer;
|
|
27
28
|
this._vertexCount = buffer.length > 0 ? buffer[0] : 0;
|
|
28
29
|
}
|
|
29
|
-
//
|
|
30
|
+
// ── GPU-ready buffers ──────────────────────────────────────────
|
|
30
31
|
/**
|
|
31
32
|
* Float32 xyz positions for Three.js BufferGeometry.
|
|
32
33
|
* Lazy-computed and cached.
|
|
@@ -64,7 +65,7 @@ export class Mesh {
|
|
|
64
65
|
get faceCount() {
|
|
65
66
|
return this.indexBuffer.length / 3;
|
|
66
67
|
}
|
|
67
|
-
//
|
|
68
|
+
// ── High-level accessors (lazy) ────────────────────────────────
|
|
68
69
|
/**
|
|
69
70
|
* Get all vertices as Point objects.
|
|
70
71
|
* Lazy-computed and cached.
|
|
@@ -100,7 +101,7 @@ export class Mesh {
|
|
|
100
101
|
get rawBuffer() {
|
|
101
102
|
return this._buffer;
|
|
102
103
|
}
|
|
103
|
-
//
|
|
104
|
+
// ── Static factories ───────────────────────────────────────────
|
|
104
105
|
/**
|
|
105
106
|
* Create a Mesh from a raw WASM buffer.
|
|
106
107
|
* @param buffer - Float64Array in mesh buffer format
|
|
@@ -238,6 +239,42 @@ export class Mesh {
|
|
|
238
239
|
ensureInit();
|
|
239
240
|
return new Mesh(wasm.sweep_polylines(pointsToCoords(profilePoints), pointsToCoords(pathPoints), caps));
|
|
240
241
|
}
|
|
242
|
+
/**
|
|
243
|
+
* Compute a stable planar normal from ordered curve points.
|
|
244
|
+
* Closed curves use Newell's method; open curves use first non-collinear triple.
|
|
245
|
+
*/
|
|
246
|
+
static computePlanarCurveNormal(points, closed) {
|
|
247
|
+
ensureInit();
|
|
248
|
+
const r = wasm.mesh_compute_planar_curve_normal(pointsToCoords(points), closed);
|
|
249
|
+
return new Vec3(r[0] ?? 0, r[1] ?? 1, r[2] ?? 0);
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* Extrude a planar curve along a direction.
|
|
253
|
+
* Open curves return an uncapped polysurface; closed curves are capped solids.
|
|
254
|
+
*/
|
|
255
|
+
static extrudePlanarCurve(points, normal, height, closed) {
|
|
256
|
+
ensureInit();
|
|
257
|
+
const buf = wasm.mesh_extrude_planar_curve(pointsToCoords(points), normal.x, normal.y, normal.z, height, closed);
|
|
258
|
+
return Mesh.fromBuffer(buf);
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* Shift a closed cutter profile slightly opposite to travel direction and
|
|
262
|
+
* compensate height so the distal end remains at the user-intended depth.
|
|
263
|
+
*/
|
|
264
|
+
static prepareBooleanCutterCurve(points, closed, normal, height) {
|
|
265
|
+
ensureInit();
|
|
266
|
+
const buf = wasm.mesh_prepare_boolean_cutter_curve(pointsToCoords(points), closed, normal.x, normal.y, normal.z, height);
|
|
267
|
+
const outHeight = buf[0] ?? height;
|
|
268
|
+
const epsilon = buf[1] ?? 0;
|
|
269
|
+
const count = Math.max(0, Math.floor(buf[2] ?? 0));
|
|
270
|
+
const shifted = [];
|
|
271
|
+
let off = 3;
|
|
272
|
+
for (let i = 0; i < count; i++) {
|
|
273
|
+
shifted.push(new Point(buf[off], buf[off + 1], buf[off + 2]));
|
|
274
|
+
off += 3;
|
|
275
|
+
}
|
|
276
|
+
return { points: shifted, height: outHeight, epsilon };
|
|
277
|
+
}
|
|
241
278
|
/**
|
|
242
279
|
* Sweep any curve type along any curve type.
|
|
243
280
|
* Passes exact curve data to WASM for native evaluation (no pre-sampling).
|
|
@@ -332,7 +369,42 @@ export class Mesh {
|
|
|
332
369
|
data.set(coords, 2);
|
|
333
370
|
return data;
|
|
334
371
|
}
|
|
335
|
-
|
|
372
|
+
static mergeMeshes(meshes) {
|
|
373
|
+
ensureInit();
|
|
374
|
+
const totalLen = meshes.reduce((sum, m) => sum + 1 + m.rawBuffer.length, 1);
|
|
375
|
+
const packed = new Float64Array(totalLen);
|
|
376
|
+
packed[0] = meshes.length;
|
|
377
|
+
let off = 1;
|
|
378
|
+
for (const mesh of meshes) {
|
|
379
|
+
const raw = mesh.rawBuffer;
|
|
380
|
+
packed[off++] = raw.length;
|
|
381
|
+
packed.set(raw, off);
|
|
382
|
+
off += raw.length;
|
|
383
|
+
}
|
|
384
|
+
return Mesh.fromBuffer(wasm.mesh_merge(packed));
|
|
385
|
+
}
|
|
386
|
+
/**
|
|
387
|
+
* Unique undirected triangle edges as vertex-index pairs.
|
|
388
|
+
*/
|
|
389
|
+
getUniqueEdgeVertexPairs() {
|
|
390
|
+
if (this._edgeVertexPairs)
|
|
391
|
+
return this._edgeVertexPairs;
|
|
392
|
+
ensureInit();
|
|
393
|
+
const packed = wasm.mesh_get_edge_vertex_pairs(this._vertexCount, this._buffer);
|
|
394
|
+
const edgeCount = Math.max(0, Math.floor(packed[0] ?? 0));
|
|
395
|
+
const pairs = [];
|
|
396
|
+
let off = 1;
|
|
397
|
+
for (let i = 0; i < edgeCount; i++) {
|
|
398
|
+
const a = Math.floor(packed[off++] ?? -1);
|
|
399
|
+
const b = Math.floor(packed[off++] ?? -1);
|
|
400
|
+
if (a < 0 || b < 0)
|
|
401
|
+
break;
|
|
402
|
+
pairs.push([a, b]);
|
|
403
|
+
}
|
|
404
|
+
this._edgeVertexPairs = pairs;
|
|
405
|
+
return this._edgeVertexPairs;
|
|
406
|
+
}
|
|
407
|
+
// ── Transforms ────────────────────────────────────────────────
|
|
336
408
|
/**
|
|
337
409
|
* Translate this mesh by an offset vector.
|
|
338
410
|
* @param offset - Translation vector
|
|
@@ -379,7 +451,7 @@ export class Mesh {
|
|
|
379
451
|
ensureInit();
|
|
380
452
|
return new Mesh(wasm.mesh_scale(this._vertexCount, this._buffer, sx, sy, sz));
|
|
381
453
|
}
|
|
382
|
-
//
|
|
454
|
+
// ── Booleans ───────────────────────────────────────────────────
|
|
383
455
|
/**
|
|
384
456
|
* Compute boolean union with another mesh.
|
|
385
457
|
* @param other - Mesh to union with
|
|
@@ -407,7 +479,7 @@ export class Mesh {
|
|
|
407
479
|
ensureInit();
|
|
408
480
|
return new Mesh(wasm.mesh_boolean_intersection(this._vertexCount, this._buffer, other._vertexCount, other._buffer));
|
|
409
481
|
}
|
|
410
|
-
//
|
|
482
|
+
// ── Intersection queries ───────────────────────────────────────
|
|
411
483
|
/**
|
|
412
484
|
* Compute intersection curves with a plane.
|
|
413
485
|
* @param plane - Cutting plane
|
|
@@ -439,7 +511,7 @@ export class Mesh {
|
|
|
439
511
|
}
|
|
440
512
|
/**
|
|
441
513
|
* Evaluate a point on the mesh surface at parametric coordinates.
|
|
442
|
-
* Maps u
|
|
514
|
+
* Maps u ∈ [0,1] and v ∈ [0,1] across the mesh's bounding box and
|
|
443
515
|
* finds the corresponding surface point via ray casting.
|
|
444
516
|
*
|
|
445
517
|
* @param u - Parameter in first surface direction [0, 1]
|
|
@@ -462,196 +534,237 @@ export class Mesh {
|
|
|
462
534
|
return parsePolylineBuf(buf).map(pts => new Polyline(pts));
|
|
463
535
|
}
|
|
464
536
|
/**
|
|
465
|
-
*
|
|
466
|
-
* Returns true when no topological boundary edges are found.
|
|
467
|
-
*
|
|
468
|
-
* Uses kernel boundary extraction first, then falls back to a welded
|
|
469
|
-
* edge-incidence pass to tolerate duplicated seam vertices.
|
|
537
|
+
* Axis-aligned bounds of this mesh.
|
|
470
538
|
*/
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
539
|
+
getBounds() {
|
|
540
|
+
ensureInit();
|
|
541
|
+
const b = wasm.mesh_get_bounds(this._vertexCount, this._buffer);
|
|
542
|
+
return {
|
|
543
|
+
min: new Point(b[0] ?? 0, b[1] ?? 0, b[2] ?? 0),
|
|
544
|
+
max: new Point(b[3] ?? 0, b[4] ?? 0, b[5] ?? 0),
|
|
545
|
+
};
|
|
546
|
+
}
|
|
547
|
+
/**
|
|
548
|
+
* Unit normal of a triangle face.
|
|
549
|
+
*/
|
|
550
|
+
getFaceNormal(faceIndex) {
|
|
551
|
+
ensureInit();
|
|
552
|
+
if (!Number.isFinite(faceIndex) || faceIndex < 0)
|
|
553
|
+
return Vec3.Y;
|
|
554
|
+
const n = wasm.mesh_get_face_normal(this._vertexCount, this._buffer, Math.floor(faceIndex));
|
|
555
|
+
return new Vec3(n[0] ?? 0, n[1] ?? 1, n[2] ?? 0);
|
|
556
|
+
}
|
|
557
|
+
/**
|
|
558
|
+
* Centroid of a triangle face.
|
|
559
|
+
*/
|
|
560
|
+
getFaceCentroid(faceIndex) {
|
|
561
|
+
ensureInit();
|
|
562
|
+
if (!Number.isFinite(faceIndex) || faceIndex < 0)
|
|
563
|
+
return Point.ORIGIN;
|
|
564
|
+
const c = wasm.mesh_get_face_centroid(this._vertexCount, this._buffer, Math.floor(faceIndex));
|
|
565
|
+
return new Point(c[0] ?? 0, c[1] ?? 0, c[2] ?? 0);
|
|
566
|
+
}
|
|
567
|
+
/**
|
|
568
|
+
* Area of a triangle face.
|
|
569
|
+
*/
|
|
570
|
+
getFaceArea(faceIndex) {
|
|
571
|
+
ensureInit();
|
|
572
|
+
if (!Number.isFinite(faceIndex) || faceIndex < 0)
|
|
573
|
+
return 0;
|
|
574
|
+
return wasm.mesh_get_face_area(this._vertexCount, this._buffer, Math.floor(faceIndex));
|
|
575
|
+
}
|
|
576
|
+
/**
|
|
577
|
+
* Find edge-connected coplanar triangles that belong to the same planar face.
|
|
578
|
+
*/
|
|
579
|
+
getCoplanarFaceIndices(faceIndex) {
|
|
580
|
+
ensureInit();
|
|
581
|
+
if (!Number.isFinite(faceIndex) || faceIndex < 0)
|
|
582
|
+
return [];
|
|
583
|
+
const r = wasm.mesh_get_coplanar_face_indices(this._vertexCount, this._buffer, Math.floor(faceIndex));
|
|
584
|
+
const count = Math.max(0, Math.floor(r[0] ?? 0));
|
|
585
|
+
const out = [];
|
|
586
|
+
for (let i = 0; i < count; i++) {
|
|
587
|
+
out.push(Math.floor(r[1 + i] ?? 0));
|
|
477
588
|
}
|
|
478
|
-
|
|
479
|
-
|
|
589
|
+
return out;
|
|
590
|
+
}
|
|
591
|
+
/**
|
|
592
|
+
* Coplanar connected face region and its projection bounds on a plane basis.
|
|
593
|
+
* Returns null for invalid inputs or empty regions.
|
|
594
|
+
*/
|
|
595
|
+
getCoplanarFaceRegion(faceIndex, origin, uAxis, vAxis, marginScale = 0.1, minMargin = 0.2) {
|
|
596
|
+
ensureInit();
|
|
597
|
+
if (!Number.isFinite(faceIndex) || faceIndex < 0)
|
|
598
|
+
return null;
|
|
599
|
+
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);
|
|
600
|
+
const count = Math.max(0, Math.floor(r[0] ?? 0));
|
|
601
|
+
if (count <= 0 || r.length < 1 + count + 4)
|
|
602
|
+
return null;
|
|
603
|
+
const faceIndices = [];
|
|
604
|
+
for (let i = 0; i < count; i++) {
|
|
605
|
+
faceIndices.push(Math.floor(r[1 + i] ?? 0));
|
|
480
606
|
}
|
|
481
|
-
const
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
607
|
+
const off = 1 + count;
|
|
608
|
+
return {
|
|
609
|
+
faceIndices,
|
|
610
|
+
uMin: r[off],
|
|
611
|
+
uMax: r[off + 1],
|
|
612
|
+
vMin: r[off + 2],
|
|
613
|
+
vMax: r[off + 3],
|
|
614
|
+
};
|
|
615
|
+
}
|
|
616
|
+
/**
|
|
617
|
+
* Unique edge count for this triangulated mesh.
|
|
618
|
+
*/
|
|
619
|
+
getEdgeCount() {
|
|
620
|
+
return this.getUniqueEdgeVertexPairs().length;
|
|
621
|
+
}
|
|
622
|
+
/**
|
|
623
|
+
* Return edge endpoints by edge index in the unique edge list.
|
|
624
|
+
*/
|
|
625
|
+
getEdgeVertices(edgeIndex) {
|
|
626
|
+
const pairs = this.getUniqueEdgeVertexPairs();
|
|
627
|
+
if (!Number.isFinite(edgeIndex) || edgeIndex < 0 || edgeIndex >= pairs.length) {
|
|
628
|
+
return [Point.ORIGIN, Point.ORIGIN];
|
|
503
629
|
}
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
630
|
+
const [i0, i1] = pairs[Math.floor(edgeIndex)];
|
|
631
|
+
const pos = this.positionBuffer;
|
|
632
|
+
const off0 = i0 * 3;
|
|
633
|
+
const off1 = i1 * 3;
|
|
634
|
+
return [
|
|
635
|
+
new Point(pos[off0], pos[off0 + 1], pos[off0 + 2]),
|
|
636
|
+
new Point(pos[off1], pos[off1 + 1], pos[off1 + 2]),
|
|
637
|
+
];
|
|
638
|
+
}
|
|
639
|
+
/**
|
|
640
|
+
* Raycast against this mesh and return nearest hit.
|
|
641
|
+
*/
|
|
642
|
+
raycast(origin, direction, maxDistance = Number.POSITIVE_INFINITY) {
|
|
643
|
+
ensureInit();
|
|
644
|
+
const r = wasm.mesh_raycast(this._vertexCount, this._buffer, origin.x, origin.y, origin.z, direction.x, direction.y, direction.z, maxDistance);
|
|
645
|
+
if (!r || r.length < 8)
|
|
646
|
+
return null;
|
|
647
|
+
return {
|
|
648
|
+
point: new Point(r[0], r[1], r[2]),
|
|
649
|
+
normal: new Vec3(r[3], r[4], r[5]),
|
|
650
|
+
faceIndex: Math.floor(r[6]),
|
|
651
|
+
distance: r[7],
|
|
652
|
+
};
|
|
653
|
+
}
|
|
654
|
+
/**
|
|
655
|
+
* Raycast against this mesh and return all hits sorted by distance.
|
|
656
|
+
*/
|
|
657
|
+
raycastAll(origin, direction, maxDistance = Number.POSITIVE_INFINITY) {
|
|
658
|
+
ensureInit();
|
|
659
|
+
const buf = wasm.mesh_raycast_all(this._vertexCount, this._buffer, origin.x, origin.y, origin.z, direction.x, direction.y, direction.z, maxDistance);
|
|
660
|
+
const count = Math.max(0, Math.floor(buf[0] ?? 0));
|
|
661
|
+
const hits = [];
|
|
662
|
+
let off = 1;
|
|
663
|
+
for (let i = 0; i < count; i++) {
|
|
664
|
+
hits.push({
|
|
665
|
+
point: new Point(buf[off], buf[off + 1], buf[off + 2]),
|
|
666
|
+
normal: new Vec3(buf[off + 3], buf[off + 4], buf[off + 5]),
|
|
667
|
+
faceIndex: Math.floor(buf[off + 6]),
|
|
668
|
+
distance: buf[off + 7],
|
|
669
|
+
});
|
|
670
|
+
off += 8;
|
|
671
|
+
}
|
|
672
|
+
return hits;
|
|
673
|
+
}
|
|
674
|
+
/**
|
|
675
|
+
* Push/pull a planar face set by moving its coplanar connected region.
|
|
676
|
+
*/
|
|
677
|
+
extrudeFace(faceIndex, distance) {
|
|
678
|
+
ensureInit();
|
|
679
|
+
if (!Number.isFinite(faceIndex) || faceIndex < 0) {
|
|
680
|
+
return Mesh.fromBuffer(new Float64Array(this._buffer));
|
|
507
681
|
}
|
|
508
|
-
return
|
|
682
|
+
return Mesh.fromBuffer(wasm.mesh_extrude_face(this._vertexCount, this._buffer, Math.floor(faceIndex), distance));
|
|
683
|
+
}
|
|
684
|
+
/**
|
|
685
|
+
* Check if this triangulated mesh represents a closed volume.
|
|
686
|
+
* Returns true when no welded topological boundary edges are found.
|
|
687
|
+
*/
|
|
688
|
+
isClosedVolume() {
|
|
689
|
+
ensureInit();
|
|
690
|
+
return wasm.mesh_is_closed_volume(this._vertexCount, this._buffer);
|
|
509
691
|
}
|
|
510
692
|
/**
|
|
511
693
|
* Odd/even point containment test against a closed mesh.
|
|
512
694
|
* Uses majority vote across multiple ray directions for robustness.
|
|
513
695
|
*/
|
|
514
696
|
containsPoint(point) {
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
/**
|
|
529
|
-
* Count unique ray/triangle crossings from origin along direction.
|
|
530
|
-
* Distances are deduplicated to collapse paired hits on triangulated quads.
|
|
531
|
-
*/
|
|
532
|
-
countRayCrossings(origin, direction) {
|
|
533
|
-
const MIN_DISTANCE = 1e-6;
|
|
534
|
-
const MERGE_DISTANCE = 1e-5;
|
|
535
|
-
const distances = [];
|
|
536
|
-
const idx = this.indexBuffer;
|
|
537
|
-
const verts = this.positionBuffer;
|
|
538
|
-
for (let i = 0; i + 2 < idx.length; i += 3) {
|
|
539
|
-
const i0 = idx[i];
|
|
540
|
-
const i1 = idx[i + 1];
|
|
541
|
-
const i2 = idx[i + 2];
|
|
542
|
-
const off0 = i0 * 3;
|
|
543
|
-
const off1 = i1 * 3;
|
|
544
|
-
const off2 = i2 * 3;
|
|
545
|
-
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]);
|
|
546
|
-
if (t !== null && Number.isFinite(t) && t > MIN_DISTANCE) {
|
|
547
|
-
distances.push(t);
|
|
697
|
+
ensureInit();
|
|
698
|
+
return wasm.mesh_contains_point(this._vertexCount, this._buffer, point.x, point.y, point.z);
|
|
699
|
+
}
|
|
700
|
+
/**
|
|
701
|
+
* Find the coplanar + edge-connected face group containing a triangle.
|
|
702
|
+
*/
|
|
703
|
+
findFaceByTriangleIndex(triangleIndex) {
|
|
704
|
+
if (!Number.isFinite(triangleIndex) || triangleIndex < 0)
|
|
705
|
+
return null;
|
|
706
|
+
const groups = this.buildCoplanarConnectedFaceGroups();
|
|
707
|
+
for (const group of groups) {
|
|
708
|
+
if (group.triangleIndices.includes(Math.floor(triangleIndex))) {
|
|
709
|
+
return { centroid: group.centroid, normal: group.normal };
|
|
548
710
|
}
|
|
549
711
|
}
|
|
550
|
-
|
|
551
|
-
return 0;
|
|
552
|
-
distances.sort((a, b) => a - b);
|
|
553
|
-
let crossings = 0;
|
|
554
|
-
let lastDistance = -Infinity;
|
|
555
|
-
for (const d of distances) {
|
|
556
|
-
if (Math.abs(d - lastDistance) <= MERGE_DISTANCE)
|
|
557
|
-
continue;
|
|
558
|
-
crossings += 1;
|
|
559
|
-
lastDistance = d;
|
|
560
|
-
}
|
|
561
|
-
return crossings;
|
|
712
|
+
return null;
|
|
562
713
|
}
|
|
563
714
|
/**
|
|
564
|
-
*
|
|
565
|
-
*
|
|
715
|
+
* Find the best matching coplanar + edge-connected face group by normal
|
|
716
|
+
* similarity and optional point proximity.
|
|
566
717
|
*/
|
|
567
|
-
|
|
568
|
-
const
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
if (
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
if (z > maxZ)
|
|
594
|
-
maxZ = z;
|
|
718
|
+
findFaceByNormal(targetNormal, nearPoint) {
|
|
719
|
+
const groups = this.buildCoplanarConnectedFaceGroups();
|
|
720
|
+
if (groups.length === 0)
|
|
721
|
+
return null;
|
|
722
|
+
let best = null;
|
|
723
|
+
let bestScore = Number.NEGATIVE_INFINITY;
|
|
724
|
+
for (const group of groups) {
|
|
725
|
+
const normal = group.normal;
|
|
726
|
+
const centroid = group.centroid;
|
|
727
|
+
const normalScore = Math.max(0, normal.dot(targetNormal));
|
|
728
|
+
let score;
|
|
729
|
+
if (nearPoint) {
|
|
730
|
+
const dx = centroid.x - nearPoint.x;
|
|
731
|
+
const dy = centroid.y - nearPoint.y;
|
|
732
|
+
const dz = centroid.z - nearPoint.z;
|
|
733
|
+
const distSq = dx * dx + dy * dy + dz * dz;
|
|
734
|
+
const proximityScore = 1.0 / (1.0 + distSq);
|
|
735
|
+
score = proximityScore * (0.5 + 0.5 * normalScore);
|
|
736
|
+
}
|
|
737
|
+
else {
|
|
738
|
+
score = normalScore;
|
|
739
|
+
}
|
|
740
|
+
if (score > bestScore) {
|
|
741
|
+
bestScore = score;
|
|
742
|
+
best = { centroid, normal };
|
|
743
|
+
}
|
|
595
744
|
}
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
const
|
|
601
|
-
const
|
|
602
|
-
const
|
|
603
|
-
let
|
|
604
|
-
for (let
|
|
605
|
-
const
|
|
606
|
-
const
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
const qy = Math.round(y * invTol);
|
|
610
|
-
const qz = Math.round(z * invTol);
|
|
611
|
-
const key = `${qx}_${qy}_${qz}`;
|
|
612
|
-
let welded = weldedByKey.get(key);
|
|
613
|
-
if (welded === undefined) {
|
|
614
|
-
welded = nextWelded++;
|
|
615
|
-
weldedByKey.set(key, welded);
|
|
745
|
+
return best;
|
|
746
|
+
}
|
|
747
|
+
buildCoplanarConnectedFaceGroups() {
|
|
748
|
+
ensureInit();
|
|
749
|
+
const buf = wasm.mesh_build_coplanar_connected_face_groups(this._vertexCount, this._buffer);
|
|
750
|
+
const groupCount = Math.max(0, Math.floor(buf[0] ?? 0));
|
|
751
|
+
const groups = [];
|
|
752
|
+
let off = 1;
|
|
753
|
+
for (let g = 0; g < groupCount; g++) {
|
|
754
|
+
const triCount = Math.max(0, Math.floor(buf[off++] ?? 0));
|
|
755
|
+
const triangleIndices = [];
|
|
756
|
+
for (let i = 0; i < triCount; i++) {
|
|
757
|
+
triangleIndices.push(Math.floor(buf[off++] ?? 0));
|
|
616
758
|
}
|
|
617
|
-
|
|
759
|
+
const centroid = new Point(buf[off], buf[off + 1], buf[off + 2]);
|
|
760
|
+
off += 3;
|
|
761
|
+
const normal = new Vec3(buf[off], buf[off + 1], buf[off + 2]);
|
|
762
|
+
off += 3;
|
|
763
|
+
groups.push({ triangleIndices, centroid, normal });
|
|
618
764
|
}
|
|
619
|
-
return
|
|
620
|
-
}
|
|
621
|
-
/**
|
|
622
|
-
* Moller-Trumbore ray/triangle intersection distance.
|
|
623
|
-
*/
|
|
624
|
-
static rayTriangleDistance(ox, oy, oz, dx, dy, dz, v0x, v0y, v0z, v1x, v1y, v1z, v2x, v2y, v2z) {
|
|
625
|
-
const EPSILON = 1e-7;
|
|
626
|
-
const e1x = v1x - v0x;
|
|
627
|
-
const e1y = v1y - v0y;
|
|
628
|
-
const e1z = v1z - v0z;
|
|
629
|
-
const e2x = v2x - v0x;
|
|
630
|
-
const e2y = v2y - v0y;
|
|
631
|
-
const e2z = v2z - v0z;
|
|
632
|
-
const hx = dy * e2z - dz * e2y;
|
|
633
|
-
const hy = dz * e2x - dx * e2z;
|
|
634
|
-
const hz = dx * e2y - dy * e2x;
|
|
635
|
-
const a = e1x * hx + e1y * hy + e1z * hz;
|
|
636
|
-
if (a > -EPSILON && a < EPSILON)
|
|
637
|
-
return null;
|
|
638
|
-
const f = 1 / a;
|
|
639
|
-
const sx = ox - v0x;
|
|
640
|
-
const sy = oy - v0y;
|
|
641
|
-
const sz = oz - v0z;
|
|
642
|
-
const u = f * (sx * hx + sy * hy + sz * hz);
|
|
643
|
-
if (u < 0 || u > 1)
|
|
644
|
-
return null;
|
|
645
|
-
const qx = sy * e1z - sz * e1y;
|
|
646
|
-
const qy = sz * e1x - sx * e1z;
|
|
647
|
-
const qz = sx * e1y - sy * e1x;
|
|
648
|
-
const v = f * (dx * qx + dy * qy + dz * qz);
|
|
649
|
-
if (v < 0 || u + v > 1)
|
|
650
|
-
return null;
|
|
651
|
-
const t = f * (e2x * qx + e2y * qy + e2z * qz);
|
|
652
|
-
return t > EPSILON ? t : null;
|
|
765
|
+
return groups;
|
|
653
766
|
}
|
|
654
|
-
//
|
|
767
|
+
// ── Export ──────────────────────────────────────────────────────
|
|
655
768
|
/**
|
|
656
769
|
* Export this mesh to OBJ format.
|
|
657
770
|
* @returns OBJ file content as string
|