okgeometry-api 0.4.2 → 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 +12 -25
- package/dist/Mesh.d.ts.map +1 -1
- package/dist/Mesh.js +127 -736
- 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
|
@@ -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";
|
|
@@ -245,77 +244,36 @@ export class Mesh {
|
|
|
245
244
|
* Closed curves use Newell's method; open curves use first non-collinear triple.
|
|
246
245
|
*/
|
|
247
246
|
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;
|
|
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);
|
|
277
250
|
}
|
|
278
251
|
/**
|
|
279
252
|
* Extrude a planar curve along a direction.
|
|
280
253
|
* Open curves return an uncapped polysurface; closed curves are capped solids.
|
|
281
254
|
*/
|
|
282
255
|
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]);
|
|
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);
|
|
294
259
|
}
|
|
295
260
|
/**
|
|
296
261
|
* Shift a closed cutter profile slightly opposite to travel direction and
|
|
297
262
|
* compensate height so the distal end remains at the user-intended depth.
|
|
298
263
|
*/
|
|
299
264
|
static prepareBooleanCutterCurve(points, closed, normal, height) {
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
const
|
|
304
|
-
|
|
305
|
-
|
|
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;
|
|
306
275
|
}
|
|
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
|
-
};
|
|
276
|
+
return { points: shifted, height: outHeight, epsilon };
|
|
319
277
|
}
|
|
320
278
|
/**
|
|
321
279
|
* Sweep any curve type along any curve type.
|
|
@@ -412,79 +370,18 @@ export class Mesh {
|
|
|
412
370
|
return data;
|
|
413
371
|
}
|
|
414
372
|
static mergeMeshes(meshes) {
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
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;
|
|
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;
|
|
432
378
|
for (const mesh of meshes) {
|
|
433
379
|
const raw = mesh.rawBuffer;
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
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;
|
|
380
|
+
packed[off++] = raw.length;
|
|
381
|
+
packed.set(raw, off);
|
|
382
|
+
off += raw.length;
|
|
444
383
|
}
|
|
445
|
-
return
|
|
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);
|
|
475
|
-
}
|
|
476
|
-
/**
|
|
477
|
-
* Build a mesh from raw position and index buffers.
|
|
478
|
-
*/
|
|
479
|
-
static fromPositionsAndIndices(positions, indices) {
|
|
480
|
-
const vertexCount = positions.length / 3;
|
|
481
|
-
const out = new Float64Array(1 + positions.length + indices.length);
|
|
482
|
-
out[0] = vertexCount;
|
|
483
|
-
out.set(positions, 1);
|
|
484
|
-
for (let i = 0; i < indices.length; i++) {
|
|
485
|
-
out[1 + positions.length + i] = indices[i];
|
|
486
|
-
}
|
|
487
|
-
return new Mesh(out);
|
|
384
|
+
return Mesh.fromBuffer(wasm.mesh_merge(packed));
|
|
488
385
|
}
|
|
489
386
|
/**
|
|
490
387
|
* Unique undirected triangle edges as vertex-index pairs.
|
|
@@ -492,21 +389,19 @@ export class Mesh {
|
|
|
492
389
|
getUniqueEdgeVertexPairs() {
|
|
493
390
|
if (this._edgeVertexPairs)
|
|
494
391
|
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());
|
|
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;
|
|
510
405
|
return this._edgeVertexPairs;
|
|
511
406
|
}
|
|
512
407
|
// ── Transforms ────────────────────────────────────────────────
|
|
@@ -642,173 +537,81 @@ export class Mesh {
|
|
|
642
537
|
* Axis-aligned bounds of this mesh.
|
|
643
538
|
*/
|
|
644
539
|
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
|
-
}
|
|
540
|
+
ensureInit();
|
|
541
|
+
const b = wasm.mesh_get_bounds(this._vertexCount, this._buffer);
|
|
672
542
|
return {
|
|
673
|
-
min: new Point(
|
|
674
|
-
max: new Point(
|
|
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),
|
|
675
545
|
};
|
|
676
546
|
}
|
|
677
547
|
/**
|
|
678
548
|
* Unit normal of a triangle face.
|
|
679
549
|
*/
|
|
680
550
|
getFaceNormal(faceIndex) {
|
|
681
|
-
|
|
551
|
+
ensureInit();
|
|
552
|
+
if (!Number.isFinite(faceIndex) || faceIndex < 0)
|
|
682
553
|
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;
|
|
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);
|
|
694
556
|
}
|
|
695
557
|
/**
|
|
696
558
|
* Centroid of a triangle face.
|
|
697
559
|
*/
|
|
698
560
|
getFaceCentroid(faceIndex) {
|
|
699
|
-
|
|
561
|
+
ensureInit();
|
|
562
|
+
if (!Number.isFinite(faceIndex) || faceIndex < 0)
|
|
700
563
|
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);
|
|
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);
|
|
711
566
|
}
|
|
712
567
|
/**
|
|
713
568
|
* Area of a triangle face.
|
|
714
569
|
*/
|
|
715
570
|
getFaceArea(faceIndex) {
|
|
716
|
-
|
|
571
|
+
ensureInit();
|
|
572
|
+
if (!Number.isFinite(faceIndex) || faceIndex < 0)
|
|
717
573
|
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;
|
|
574
|
+
return wasm.mesh_get_face_area(this._vertexCount, this._buffer, Math.floor(faceIndex));
|
|
737
575
|
}
|
|
738
576
|
/**
|
|
739
577
|
* Find edge-connected coplanar triangles that belong to the same planar face.
|
|
740
578
|
*/
|
|
741
579
|
getCoplanarFaceIndices(faceIndex) {
|
|
742
|
-
|
|
580
|
+
ensureInit();
|
|
581
|
+
if (!Number.isFinite(faceIndex) || faceIndex < 0)
|
|
743
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));
|
|
744
588
|
}
|
|
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
|
-
}
|
|
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));
|
|
810
606
|
}
|
|
811
|
-
|
|
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
|
+
};
|
|
812
615
|
}
|
|
813
616
|
/**
|
|
814
617
|
* Unique edge count for this triangulated mesh.
|
|
@@ -837,207 +640,62 @@ export class Mesh {
|
|
|
837
640
|
* Raycast against this mesh and return nearest hit.
|
|
838
641
|
*/
|
|
839
642
|
raycast(origin, direction, maxDistance = Number.POSITIVE_INFINITY) {
|
|
840
|
-
|
|
841
|
-
|
|
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
|
+
};
|
|
842
653
|
}
|
|
843
654
|
/**
|
|
844
655
|
* Raycast against this mesh and return all hits sorted by distance.
|
|
845
656
|
*/
|
|
846
657
|
raycastAll(origin, direction, maxDistance = Number.POSITIVE_INFINITY) {
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
const dir = direction.scale(1 / dirLen);
|
|
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));
|
|
851
661
|
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;
|
|
662
|
+
let off = 1;
|
|
663
|
+
for (let i = 0; i < count; i++) {
|
|
865
664
|
hits.push({
|
|
866
|
-
point: new Point(
|
|
867
|
-
normal:
|
|
868
|
-
faceIndex:
|
|
869
|
-
distance:
|
|
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],
|
|
870
669
|
});
|
|
670
|
+
off += 8;
|
|
871
671
|
}
|
|
872
|
-
hits.sort((a, b) => a.distance - b.distance);
|
|
873
672
|
return hits;
|
|
874
673
|
}
|
|
875
674
|
/**
|
|
876
675
|
* Push/pull a planar face set by moving its coplanar connected region.
|
|
877
676
|
*/
|
|
878
677
|
extrudeFace(faceIndex, distance) {
|
|
879
|
-
|
|
678
|
+
ensureInit();
|
|
679
|
+
if (!Number.isFinite(faceIndex) || faceIndex < 0) {
|
|
880
680
|
return Mesh.fromBuffer(new Float64Array(this._buffer));
|
|
881
681
|
}
|
|
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);
|
|
682
|
+
return Mesh.fromBuffer(wasm.mesh_extrude_face(this._vertexCount, this._buffer, Math.floor(faceIndex), distance));
|
|
977
683
|
}
|
|
978
684
|
/**
|
|
979
685
|
* 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.
|
|
686
|
+
* Returns true when no welded topological boundary edges are found.
|
|
984
687
|
*/
|
|
985
688
|
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;
|
|
689
|
+
ensureInit();
|
|
690
|
+
return wasm.mesh_is_closed_volume(this._vertexCount, this._buffer);
|
|
1023
691
|
}
|
|
1024
692
|
/**
|
|
1025
693
|
* Odd/even point containment test against a closed mesh.
|
|
1026
694
|
* Uses majority vote across multiple ray directions for robustness.
|
|
1027
695
|
*/
|
|
1028
696
|
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);
|
|
697
|
+
ensureInit();
|
|
698
|
+
return wasm.mesh_contains_point(this._vertexCount, this._buffer, point.x, point.y, point.z);
|
|
1041
699
|
}
|
|
1042
700
|
/**
|
|
1043
701
|
* Find the coplanar + edge-connected face group containing a triangle.
|
|
@@ -1086,293 +744,26 @@ export class Mesh {
|
|
|
1086
744
|
}
|
|
1087
745
|
return best;
|
|
1088
746
|
}
|
|
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
747
|
buildCoplanarConnectedFaceGroups() {
|
|
1125
|
-
|
|
1126
|
-
const
|
|
1127
|
-
const
|
|
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);
|
|
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));
|
|
1201
751
|
const groups = [];
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
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;
|
|
752
|
+
let off = 1;
|
|
753
|
+
for (let g = 0; g < groupCount; g++) {
|
|
754
|
+
const triCount = Math.max(0, Math.floor(buf[off++] ?? 0));
|
|
1239
755
|
const triangleIndices = [];
|
|
1240
|
-
for (
|
|
1241
|
-
|
|
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);
|
|
756
|
+
for (let i = 0; i < triCount; i++) {
|
|
757
|
+
triangleIndices.push(Math.floor(buf[off++] ?? 0));
|
|
1248
758
|
}
|
|
1249
|
-
const
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
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
|
-
});
|
|
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 });
|
|
1264
764
|
}
|
|
1265
765
|
return groups;
|
|
1266
766
|
}
|
|
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)
|
|
1370
|
-
return null;
|
|
1371
|
-
return new Vec3(nx / len, ny / len, nz / len);
|
|
1372
|
-
}
|
|
1373
|
-
static edgeKey(a, b) {
|
|
1374
|
-
return a < b ? `${a}:${b}` : `${b}:${a}`;
|
|
1375
|
-
}
|
|
1376
767
|
// ── Export ──────────────────────────────────────────────────────
|
|
1377
768
|
/**
|
|
1378
769
|
* Export this mesh to OBJ format.
|