remesh-threejs 0.3.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +19 -1
- package/dist/index.d.ts +152 -0
- package/dist/remesh-threejs.cjs +1 -1
- package/dist/remesh-threejs.cjs.map +1 -1
- package/dist/remesh-threejs.js +488 -0
- package/dist/remesh-threejs.js.map +1 -1
- package/package.json +1 -1
package/dist/remesh-threejs.js
CHANGED
|
@@ -4425,6 +4425,435 @@ class DuplicateFaceRepair extends RepairOperation {
|
|
|
4425
4425
|
return false;
|
|
4426
4426
|
}
|
|
4427
4427
|
}
|
|
4428
|
+
class NonManifoldEdgeRepair extends RepairOperation {
|
|
4429
|
+
constructor(mesh, verbose = false, strategy = "auto") {
|
|
4430
|
+
super(mesh, verbose);
|
|
4431
|
+
this.nonManifoldEdges = [];
|
|
4432
|
+
this.strategy = strategy;
|
|
4433
|
+
}
|
|
4434
|
+
detect() {
|
|
4435
|
+
this.nonManifoldEdges = [];
|
|
4436
|
+
for (const edge of this.mesh.edges.values()) {
|
|
4437
|
+
if (edge.allHalfedges.length > 2) {
|
|
4438
|
+
this.nonManifoldEdges.push(edge);
|
|
4439
|
+
}
|
|
4440
|
+
}
|
|
4441
|
+
if (this.verbose && this.nonManifoldEdges.length > 0) {
|
|
4442
|
+
console.log(`Found ${this.nonManifoldEdges.length} non-manifold edges`);
|
|
4443
|
+
}
|
|
4444
|
+
return this.nonManifoldEdges.length;
|
|
4445
|
+
}
|
|
4446
|
+
repair() {
|
|
4447
|
+
let fixedCount = 0;
|
|
4448
|
+
for (const edge of this.nonManifoldEdges) {
|
|
4449
|
+
if (!this.mesh.edges.has(edge.id)) continue;
|
|
4450
|
+
const repairStrategy = this.determineStrategy(edge);
|
|
4451
|
+
if (repairStrategy === "split") {
|
|
4452
|
+
if (this.splitNonManifoldEdge(edge)) {
|
|
4453
|
+
fixedCount++;
|
|
4454
|
+
}
|
|
4455
|
+
} else {
|
|
4456
|
+
if (this.collapseNonManifoldEdge(edge)) {
|
|
4457
|
+
fixedCount++;
|
|
4458
|
+
}
|
|
4459
|
+
}
|
|
4460
|
+
}
|
|
4461
|
+
if (this.verbose && fixedCount > 0) {
|
|
4462
|
+
console.log(`Repaired ${fixedCount} non-manifold edges`);
|
|
4463
|
+
}
|
|
4464
|
+
return fixedCount;
|
|
4465
|
+
}
|
|
4466
|
+
/**
|
|
4467
|
+
* Determine which strategy to use for a specific edge.
|
|
4468
|
+
*/
|
|
4469
|
+
determineStrategy(edge) {
|
|
4470
|
+
if (this.strategy !== "auto") {
|
|
4471
|
+
return this.strategy;
|
|
4472
|
+
}
|
|
4473
|
+
const avgEdgeLength = this.computeAverageEdgeLength();
|
|
4474
|
+
const edgeLength = edge.length;
|
|
4475
|
+
return edgeLength > avgEdgeLength ? "split" : "collapse";
|
|
4476
|
+
}
|
|
4477
|
+
/**
|
|
4478
|
+
* Compute average edge length in the mesh.
|
|
4479
|
+
*/
|
|
4480
|
+
computeAverageEdgeLength() {
|
|
4481
|
+
let totalLength = 0;
|
|
4482
|
+
let count = 0;
|
|
4483
|
+
for (const edge of this.mesh.edges.values()) {
|
|
4484
|
+
totalLength += edge.length;
|
|
4485
|
+
count++;
|
|
4486
|
+
}
|
|
4487
|
+
return count > 0 ? totalLength / count : 1;
|
|
4488
|
+
}
|
|
4489
|
+
/**
|
|
4490
|
+
* Split a non-manifold edge by duplicating vertices.
|
|
4491
|
+
*/
|
|
4492
|
+
splitNonManifoldEdge(edge) {
|
|
4493
|
+
try {
|
|
4494
|
+
const [v0, v1] = edge.getVertices();
|
|
4495
|
+
if (!v0 || !v1) return false;
|
|
4496
|
+
const halfedges = [...edge.allHalfedges];
|
|
4497
|
+
if (halfedges.length <= 2) return false;
|
|
4498
|
+
for (let i = 2; i < halfedges.length; i++) {
|
|
4499
|
+
const he = halfedges[i];
|
|
4500
|
+
if (!he || !he.face) continue;
|
|
4501
|
+
const newVertex = this.mesh.createVertex(v1.position.clone());
|
|
4502
|
+
const faceVertices = he.face.getVertices();
|
|
4503
|
+
if (!faceVertices) continue;
|
|
4504
|
+
const vertexIndices = [];
|
|
4505
|
+
for (const v of faceVertices) {
|
|
4506
|
+
if (v.id === v1.id) {
|
|
4507
|
+
vertexIndices.push(newVertex.id);
|
|
4508
|
+
} else {
|
|
4509
|
+
vertexIndices.push(v.id);
|
|
4510
|
+
}
|
|
4511
|
+
}
|
|
4512
|
+
this.mesh.faces.delete(he.face.id);
|
|
4513
|
+
const vertices = vertexIndices.map((id) => this.mesh.vertices.get(id));
|
|
4514
|
+
if (!vertices[0] || !vertices[1] || !vertices[2]) continue;
|
|
4515
|
+
this.mesh.createFace(vertices[0], vertices[1], vertices[2]);
|
|
4516
|
+
}
|
|
4517
|
+
return true;
|
|
4518
|
+
} catch {
|
|
4519
|
+
return false;
|
|
4520
|
+
}
|
|
4521
|
+
}
|
|
4522
|
+
/**
|
|
4523
|
+
* Collapse a non-manifold edge by removing excess faces.
|
|
4524
|
+
*/
|
|
4525
|
+
collapseNonManifoldEdge(edge) {
|
|
4526
|
+
try {
|
|
4527
|
+
const halfedges = [...edge.allHalfedges];
|
|
4528
|
+
if (halfedges.length <= 2) return false;
|
|
4529
|
+
let removedCount = 0;
|
|
4530
|
+
for (let i = 2; i < halfedges.length; i++) {
|
|
4531
|
+
const he = halfedges[i];
|
|
4532
|
+
if (!he || !he.face) continue;
|
|
4533
|
+
this.mesh.faces.delete(he.face.id);
|
|
4534
|
+
const faceHalfedges = he.face.getHalfedges();
|
|
4535
|
+
if (faceHalfedges) {
|
|
4536
|
+
for (const fhe of faceHalfedges) {
|
|
4537
|
+
this.mesh.halfedges.delete(fhe.id);
|
|
4538
|
+
fhe.edge.allHalfedges = fhe.edge.allHalfedges.filter((h) => h.id !== fhe.id);
|
|
4539
|
+
}
|
|
4540
|
+
}
|
|
4541
|
+
removedCount++;
|
|
4542
|
+
}
|
|
4543
|
+
return removedCount > 0;
|
|
4544
|
+
} catch {
|
|
4545
|
+
return false;
|
|
4546
|
+
}
|
|
4547
|
+
}
|
|
4548
|
+
getName() {
|
|
4549
|
+
return "Remove Non-Manifold Edges";
|
|
4550
|
+
}
|
|
4551
|
+
canParallelize() {
|
|
4552
|
+
return this.nonManifoldEdges.length > 100;
|
|
4553
|
+
}
|
|
4554
|
+
}
|
|
4555
|
+
class HoleFiller extends RepairOperation {
|
|
4556
|
+
constructor(mesh, verbose = false, maxHoleSize = 100, preserveBoundary = false) {
|
|
4557
|
+
super(mesh, verbose);
|
|
4558
|
+
this.holes = [];
|
|
4559
|
+
this.maxHoleSize = maxHoleSize;
|
|
4560
|
+
this.preserveBoundary = preserveBoundary;
|
|
4561
|
+
}
|
|
4562
|
+
detect() {
|
|
4563
|
+
this.holes = [];
|
|
4564
|
+
const boundaryEdges = [];
|
|
4565
|
+
for (const edge of this.mesh.edges.values()) {
|
|
4566
|
+
if (edge.allHalfedges.length === 1) {
|
|
4567
|
+
boundaryEdges.push(edge);
|
|
4568
|
+
}
|
|
4569
|
+
}
|
|
4570
|
+
if (boundaryEdges.length === 0) {
|
|
4571
|
+
return 0;
|
|
4572
|
+
}
|
|
4573
|
+
this.holes = this.extractBoundaryLoops(boundaryEdges);
|
|
4574
|
+
this.holes = this.holes.filter((hole) => hole.vertices.length <= this.maxHoleSize);
|
|
4575
|
+
if (this.verbose && this.holes.length > 0) {
|
|
4576
|
+
console.log(`Found ${this.holes.length} holes`);
|
|
4577
|
+
}
|
|
4578
|
+
return this.holes.length;
|
|
4579
|
+
}
|
|
4580
|
+
repair() {
|
|
4581
|
+
if (this.preserveBoundary) {
|
|
4582
|
+
return 0;
|
|
4583
|
+
}
|
|
4584
|
+
let fixedCount = 0;
|
|
4585
|
+
for (const hole of this.holes) {
|
|
4586
|
+
const triangles = this.triangulateBoundaryLoop(hole);
|
|
4587
|
+
if (triangles.length > 0) {
|
|
4588
|
+
for (const tri of triangles) {
|
|
4589
|
+
try {
|
|
4590
|
+
this.mesh.createFace(tri.v0, tri.v1, tri.v2);
|
|
4591
|
+
} catch {
|
|
4592
|
+
continue;
|
|
4593
|
+
}
|
|
4594
|
+
}
|
|
4595
|
+
fixedCount++;
|
|
4596
|
+
}
|
|
4597
|
+
}
|
|
4598
|
+
if (this.verbose && fixedCount > 0) {
|
|
4599
|
+
console.log(`Filled ${fixedCount} holes`);
|
|
4600
|
+
}
|
|
4601
|
+
return fixedCount;
|
|
4602
|
+
}
|
|
4603
|
+
/**
|
|
4604
|
+
* Extract boundary loops from a set of boundary edges.
|
|
4605
|
+
*/
|
|
4606
|
+
extractBoundaryLoops(boundaryEdges) {
|
|
4607
|
+
const loops = [];
|
|
4608
|
+
const visited = /* @__PURE__ */ new Set();
|
|
4609
|
+
for (const startEdge of boundaryEdges) {
|
|
4610
|
+
if (visited.has(startEdge.id)) continue;
|
|
4611
|
+
const loop = [];
|
|
4612
|
+
const loopEdges = [];
|
|
4613
|
+
let currentEdge = startEdge;
|
|
4614
|
+
let iterations = 0;
|
|
4615
|
+
const maxIterations = 1e4;
|
|
4616
|
+
do {
|
|
4617
|
+
if (iterations++ > maxIterations) break;
|
|
4618
|
+
visited.add(currentEdge.id);
|
|
4619
|
+
const [v0, v1] = currentEdge.getVertices();
|
|
4620
|
+
if (!v0 || !v1) break;
|
|
4621
|
+
loop.push(v0);
|
|
4622
|
+
loopEdges.push(currentEdge);
|
|
4623
|
+
const nextEdge = this.findNextBoundaryEdge(v1, currentEdge, boundaryEdges);
|
|
4624
|
+
if (!nextEdge || nextEdge === startEdge) {
|
|
4625
|
+
loop.push(v1);
|
|
4626
|
+
break;
|
|
4627
|
+
}
|
|
4628
|
+
currentEdge = nextEdge;
|
|
4629
|
+
} while (currentEdge !== startEdge);
|
|
4630
|
+
if (loop.length >= 3) {
|
|
4631
|
+
loops.push({ vertices: loop, edges: loopEdges });
|
|
4632
|
+
}
|
|
4633
|
+
}
|
|
4634
|
+
return loops;
|
|
4635
|
+
}
|
|
4636
|
+
/**
|
|
4637
|
+
* Find the next boundary edge connected to a vertex.
|
|
4638
|
+
*/
|
|
4639
|
+
findNextBoundaryEdge(vertex, currentEdge, boundaryEdges) {
|
|
4640
|
+
for (const edge of boundaryEdges) {
|
|
4641
|
+
if (edge === currentEdge) continue;
|
|
4642
|
+
const [v0, v1] = edge.getVertices();
|
|
4643
|
+
if (!v0 || !v1) continue;
|
|
4644
|
+
if (v0.id === vertex.id || v1.id === vertex.id) {
|
|
4645
|
+
return edge;
|
|
4646
|
+
}
|
|
4647
|
+
}
|
|
4648
|
+
return null;
|
|
4649
|
+
}
|
|
4650
|
+
/**
|
|
4651
|
+
* Triangulate a boundary loop using ear clipping algorithm.
|
|
4652
|
+
*/
|
|
4653
|
+
triangulateBoundaryLoop(loop) {
|
|
4654
|
+
const triangles = [];
|
|
4655
|
+
const vertices = [...loop.vertices];
|
|
4656
|
+
let iterations = 0;
|
|
4657
|
+
const maxIterations = vertices.length * vertices.length;
|
|
4658
|
+
while (vertices.length > 3 && iterations++ < maxIterations) {
|
|
4659
|
+
const earIndex = this.findEar(vertices);
|
|
4660
|
+
if (earIndex === -1) {
|
|
4661
|
+
break;
|
|
4662
|
+
}
|
|
4663
|
+
const prev = (earIndex - 1 + vertices.length) % vertices.length;
|
|
4664
|
+
const next = (earIndex + 1) % vertices.length;
|
|
4665
|
+
const v0 = vertices[prev];
|
|
4666
|
+
const v1 = vertices[earIndex];
|
|
4667
|
+
const v2 = vertices[next];
|
|
4668
|
+
if (!v0 || !v1 || !v2) break;
|
|
4669
|
+
triangles.push({ v0, v1, v2 });
|
|
4670
|
+
vertices.splice(earIndex, 1);
|
|
4671
|
+
}
|
|
4672
|
+
if (vertices.length === 3) {
|
|
4673
|
+
const v0 = vertices[0];
|
|
4674
|
+
const v1 = vertices[1];
|
|
4675
|
+
const v2 = vertices[2];
|
|
4676
|
+
if (v0 && v1 && v2) {
|
|
4677
|
+
triangles.push({ v0, v1, v2 });
|
|
4678
|
+
}
|
|
4679
|
+
}
|
|
4680
|
+
return triangles;
|
|
4681
|
+
}
|
|
4682
|
+
/**
|
|
4683
|
+
* Find an "ear" in the polygon (a triangle with no vertices inside).
|
|
4684
|
+
*/
|
|
4685
|
+
findEar(vertices) {
|
|
4686
|
+
const n = vertices.length;
|
|
4687
|
+
for (let i = 0; i < n; i++) {
|
|
4688
|
+
const prev = (i - 1 + n) % n;
|
|
4689
|
+
const next = (i + 1) % n;
|
|
4690
|
+
const v0 = vertices[prev];
|
|
4691
|
+
const v1 = vertices[i];
|
|
4692
|
+
const v2 = vertices[next];
|
|
4693
|
+
if (!v0 || !v1 || !v2) continue;
|
|
4694
|
+
const area = triangleArea(v0.position, v1.position, v2.position);
|
|
4695
|
+
if (area < 1e-10) continue;
|
|
4696
|
+
let isEar = true;
|
|
4697
|
+
for (let j = 0; j < n; j++) {
|
|
4698
|
+
if (j === prev || j === i || j === next) continue;
|
|
4699
|
+
const vj = vertices[j];
|
|
4700
|
+
if (!vj) continue;
|
|
4701
|
+
if (isPointInTriangle(vj.position, v0.position, v1.position, v2.position)) {
|
|
4702
|
+
isEar = false;
|
|
4703
|
+
break;
|
|
4704
|
+
}
|
|
4705
|
+
}
|
|
4706
|
+
if (isEar) {
|
|
4707
|
+
return i;
|
|
4708
|
+
}
|
|
4709
|
+
}
|
|
4710
|
+
return -1;
|
|
4711
|
+
}
|
|
4712
|
+
getName() {
|
|
4713
|
+
return "Fill Holes";
|
|
4714
|
+
}
|
|
4715
|
+
canParallelize() {
|
|
4716
|
+
return this.holes.length > 10;
|
|
4717
|
+
}
|
|
4718
|
+
}
|
|
4719
|
+
class NormalUnifier extends RepairOperation {
|
|
4720
|
+
constructor(mesh, verbose = false, seedFaceIndex = 0) {
|
|
4721
|
+
super(mesh, verbose);
|
|
4722
|
+
this.inconsistentFaces = [];
|
|
4723
|
+
this.seedFaceIndex = seedFaceIndex;
|
|
4724
|
+
}
|
|
4725
|
+
detect() {
|
|
4726
|
+
this.inconsistentFaces = [];
|
|
4727
|
+
const faces = Array.from(this.mesh.faces.values());
|
|
4728
|
+
if (faces.length === 0) return 0;
|
|
4729
|
+
const visited = /* @__PURE__ */ new Set();
|
|
4730
|
+
const queue = [];
|
|
4731
|
+
const seedFace = faces[this.seedFaceIndex] || faces[0];
|
|
4732
|
+
if (!seedFace) return 0;
|
|
4733
|
+
queue.push(seedFace);
|
|
4734
|
+
visited.add(seedFace.id);
|
|
4735
|
+
while (queue.length > 0) {
|
|
4736
|
+
const face = queue.shift();
|
|
4737
|
+
const neighbors = this.getNeighborFaces(face);
|
|
4738
|
+
for (const neighbor of neighbors) {
|
|
4739
|
+
const neighborId = neighbor.id;
|
|
4740
|
+
if (visited.has(neighborId)) continue;
|
|
4741
|
+
visited.add(neighborId);
|
|
4742
|
+
const sharedEdge = this.getSharedEdge(face, neighbor);
|
|
4743
|
+
if (sharedEdge && !this.areNormalsConsistent(face, neighbor, sharedEdge)) {
|
|
4744
|
+
this.inconsistentFaces.push(neighbor);
|
|
4745
|
+
}
|
|
4746
|
+
queue.push(neighbor);
|
|
4747
|
+
}
|
|
4748
|
+
}
|
|
4749
|
+
if (this.verbose && this.inconsistentFaces.length > 0) {
|
|
4750
|
+
console.log(`Found ${this.inconsistentFaces.length} faces with inconsistent normals`);
|
|
4751
|
+
}
|
|
4752
|
+
return this.inconsistentFaces.length;
|
|
4753
|
+
}
|
|
4754
|
+
repair() {
|
|
4755
|
+
let fixedCount = 0;
|
|
4756
|
+
for (const face of this.inconsistentFaces) {
|
|
4757
|
+
if (this.flipFaceOrientation(face)) {
|
|
4758
|
+
fixedCount++;
|
|
4759
|
+
}
|
|
4760
|
+
}
|
|
4761
|
+
if (this.verbose && fixedCount > 0) {
|
|
4762
|
+
console.log(`Unified normals for ${fixedCount} faces`);
|
|
4763
|
+
}
|
|
4764
|
+
return fixedCount;
|
|
4765
|
+
}
|
|
4766
|
+
/**
|
|
4767
|
+
* Get faces that share an edge with the given face.
|
|
4768
|
+
*/
|
|
4769
|
+
getNeighborFaces(face) {
|
|
4770
|
+
const neighbors = [];
|
|
4771
|
+
const halfedges = face.getHalfedges();
|
|
4772
|
+
if (!halfedges) return neighbors;
|
|
4773
|
+
for (const he of halfedges) {
|
|
4774
|
+
if (he.twin && he.twin.face && he.twin.face !== face) {
|
|
4775
|
+
neighbors.push(he.twin.face);
|
|
4776
|
+
}
|
|
4777
|
+
}
|
|
4778
|
+
return neighbors;
|
|
4779
|
+
}
|
|
4780
|
+
/**
|
|
4781
|
+
* Find the edge shared between two faces.
|
|
4782
|
+
*/
|
|
4783
|
+
getSharedEdge(face1, face2) {
|
|
4784
|
+
const halfedges1 = face1.getHalfedges();
|
|
4785
|
+
const halfedges2 = face2.getHalfedges();
|
|
4786
|
+
if (!halfedges1 || !halfedges2) return null;
|
|
4787
|
+
for (const he1 of halfedges1) {
|
|
4788
|
+
for (const he2 of halfedges2) {
|
|
4789
|
+
if (he1.edge === he2.edge) {
|
|
4790
|
+
return he1.edge;
|
|
4791
|
+
}
|
|
4792
|
+
}
|
|
4793
|
+
}
|
|
4794
|
+
return null;
|
|
4795
|
+
}
|
|
4796
|
+
/**
|
|
4797
|
+
* Check if two faces have consistent normal orientation across their shared edge.
|
|
4798
|
+
*/
|
|
4799
|
+
areNormalsConsistent(face1, face2, sharedEdge) {
|
|
4800
|
+
const he1 = this.getHalfedgeInFace(face1, sharedEdge);
|
|
4801
|
+
const he2 = this.getHalfedgeInFace(face2, sharedEdge);
|
|
4802
|
+
if (!he1 || !he2) return true;
|
|
4803
|
+
const [v1_start, v1_end] = this.getHalfedgeVertices(he1);
|
|
4804
|
+
const [v2_start, v2_end] = this.getHalfedgeVertices(he2);
|
|
4805
|
+
if (!v1_start || !v1_end || !v2_start || !v2_end) return true;
|
|
4806
|
+
return v1_start.id === v2_end.id && v1_end.id === v2_start.id;
|
|
4807
|
+
}
|
|
4808
|
+
/**
|
|
4809
|
+
* Get the halfedge in a face that corresponds to a given edge.
|
|
4810
|
+
*/
|
|
4811
|
+
getHalfedgeInFace(face, edge) {
|
|
4812
|
+
const halfedges = face.getHalfedges();
|
|
4813
|
+
if (!halfedges) return null;
|
|
4814
|
+
for (const he of halfedges) {
|
|
4815
|
+
if (he.edge === edge) {
|
|
4816
|
+
return he;
|
|
4817
|
+
}
|
|
4818
|
+
}
|
|
4819
|
+
return null;
|
|
4820
|
+
}
|
|
4821
|
+
/**
|
|
4822
|
+
* Get the start and end vertices of a halfedge.
|
|
4823
|
+
*/
|
|
4824
|
+
getHalfedgeVertices(he) {
|
|
4825
|
+
var _a;
|
|
4826
|
+
return [he.vertex, ((_a = he.next) == null ? void 0 : _a.vertex) || null];
|
|
4827
|
+
}
|
|
4828
|
+
/**
|
|
4829
|
+
* Flip the orientation of a face by reversing its halfedge order.
|
|
4830
|
+
*/
|
|
4831
|
+
flipFaceOrientation(face) {
|
|
4832
|
+
try {
|
|
4833
|
+
const halfedges = face.getHalfedges();
|
|
4834
|
+
if (!halfedges || halfedges.length !== 3) return false;
|
|
4835
|
+
const [he0, he1, he2] = halfedges;
|
|
4836
|
+
const temp0 = he0.next;
|
|
4837
|
+
he0.next = he0.prev;
|
|
4838
|
+
he0.prev = temp0;
|
|
4839
|
+
const temp1 = he1.next;
|
|
4840
|
+
he1.next = he1.prev;
|
|
4841
|
+
he1.prev = temp1;
|
|
4842
|
+
const temp2 = he2.next;
|
|
4843
|
+
he2.next = he2.prev;
|
|
4844
|
+
he2.prev = temp2;
|
|
4845
|
+
return true;
|
|
4846
|
+
} catch {
|
|
4847
|
+
return false;
|
|
4848
|
+
}
|
|
4849
|
+
}
|
|
4850
|
+
getName() {
|
|
4851
|
+
return "Unify Normals";
|
|
4852
|
+
}
|
|
4853
|
+
canParallelize() {
|
|
4854
|
+
return false;
|
|
4855
|
+
}
|
|
4856
|
+
}
|
|
4428
4857
|
class MeshRepairer {
|
|
4429
4858
|
constructor(meshOrGeometry, options) {
|
|
4430
4859
|
this.operations = [];
|
|
@@ -4484,6 +4913,33 @@ class MeshRepairer {
|
|
|
4484
4913
|
this.operations.push(new DuplicateFaceRepair(this.mesh, this.options.verbose));
|
|
4485
4914
|
return this;
|
|
4486
4915
|
}
|
|
4916
|
+
/**
|
|
4917
|
+
* Remove non-manifold edges by splitting or collapsing.
|
|
4918
|
+
* @param strategy - Repair strategy: 'split', 'collapse', or 'auto' (default: 'auto')
|
|
4919
|
+
* @returns this for chaining
|
|
4920
|
+
*/
|
|
4921
|
+
removeNonManifoldEdges(strategy) {
|
|
4922
|
+
this.operations.push(new NonManifoldEdgeRepair(this.mesh, this.options.verbose, strategy));
|
|
4923
|
+
return this;
|
|
4924
|
+
}
|
|
4925
|
+
/**
|
|
4926
|
+
* Fill boundary loops (holes) with triangulation.
|
|
4927
|
+
* @param maxHoleSize - Maximum number of edges in a hole to fill (default: 100)
|
|
4928
|
+
* @returns this for chaining
|
|
4929
|
+
*/
|
|
4930
|
+
fillHoles(maxHoleSize) {
|
|
4931
|
+
this.operations.push(new HoleFiller(this.mesh, this.options.verbose, maxHoleSize));
|
|
4932
|
+
return this;
|
|
4933
|
+
}
|
|
4934
|
+
/**
|
|
4935
|
+
* Unify face orientations to make normals consistent.
|
|
4936
|
+
* @param seedFaceIndex - Index of the face to use as orientation reference (default: 0)
|
|
4937
|
+
* @returns this for chaining
|
|
4938
|
+
*/
|
|
4939
|
+
unifyNormals(seedFaceIndex) {
|
|
4940
|
+
this.operations.push(new NormalUnifier(this.mesh, this.options.verbose, seedFaceIndex));
|
|
4941
|
+
return this;
|
|
4942
|
+
}
|
|
4487
4943
|
/**
|
|
4488
4944
|
* Run all common repairs in optimal order.
|
|
4489
4945
|
* @returns this for chaining
|
|
@@ -4492,6 +4948,8 @@ class MeshRepairer {
|
|
|
4492
4948
|
this.removeIsolatedVertices();
|
|
4493
4949
|
this.removeDuplicateFaces();
|
|
4494
4950
|
this.removeDegenerateFaces();
|
|
4951
|
+
this.fillHoles();
|
|
4952
|
+
this.unifyNormals();
|
|
4495
4953
|
return this;
|
|
4496
4954
|
}
|
|
4497
4955
|
/**
|
|
@@ -4594,6 +5052,30 @@ function removeDuplicateFaces(geometry, options) {
|
|
|
4594
5052
|
stats
|
|
4595
5053
|
};
|
|
4596
5054
|
}
|
|
5055
|
+
function removeNonManifoldEdges(geometry, options) {
|
|
5056
|
+
const repairer = new MeshRepairer(geometry, options);
|
|
5057
|
+
const stats = repairer.removeNonManifoldEdges(options == null ? void 0 : options.strategy).execute();
|
|
5058
|
+
return {
|
|
5059
|
+
geometry: repairer.toBufferGeometry(),
|
|
5060
|
+
stats
|
|
5061
|
+
};
|
|
5062
|
+
}
|
|
5063
|
+
function fillHoles(geometry, options) {
|
|
5064
|
+
const repairer = new MeshRepairer(geometry, options);
|
|
5065
|
+
const stats = repairer.fillHoles(options == null ? void 0 : options.maxHoleSize).execute();
|
|
5066
|
+
return {
|
|
5067
|
+
geometry: repairer.toBufferGeometry(),
|
|
5068
|
+
stats
|
|
5069
|
+
};
|
|
5070
|
+
}
|
|
5071
|
+
function unifyNormals(geometry, options) {
|
|
5072
|
+
const repairer = new MeshRepairer(geometry, options);
|
|
5073
|
+
const stats = repairer.unifyNormals(options == null ? void 0 : options.seedFaceIndex).execute();
|
|
5074
|
+
return {
|
|
5075
|
+
geometry: repairer.toBufferGeometry(),
|
|
5076
|
+
stats
|
|
5077
|
+
};
|
|
5078
|
+
}
|
|
4597
5079
|
export {
|
|
4598
5080
|
AdaptiveRemesher,
|
|
4599
5081
|
BVH,
|
|
@@ -4610,10 +5092,13 @@ export {
|
|
|
4610
5092
|
Face,
|
|
4611
5093
|
FeatureSkeleton,
|
|
4612
5094
|
Halfedge,
|
|
5095
|
+
HoleFiller,
|
|
4613
5096
|
IsolatedVertexRepair,
|
|
4614
5097
|
ManifoldAnalyzer,
|
|
4615
5098
|
MeshRepairer,
|
|
5099
|
+
NonManifoldEdgeRepair,
|
|
4616
5100
|
NonManifoldMesh,
|
|
5101
|
+
NormalUnifier,
|
|
4617
5102
|
QualityMetrics,
|
|
4618
5103
|
RepairOperation,
|
|
4619
5104
|
SkeletonBuilder,
|
|
@@ -4663,6 +5148,7 @@ export {
|
|
|
4663
5148
|
exportClassificationGeometry,
|
|
4664
5149
|
exportQualityGeometry,
|
|
4665
5150
|
exportSkeletonGeometry,
|
|
5151
|
+
fillHoles,
|
|
4666
5152
|
flipEdge,
|
|
4667
5153
|
fromVector3,
|
|
4668
5154
|
getHighValenceVertices,
|
|
@@ -4698,6 +5184,7 @@ export {
|
|
|
4698
5184
|
removeDegenerateFaces,
|
|
4699
5185
|
removeDuplicateFaces,
|
|
4700
5186
|
removeIsolatedVertices,
|
|
5187
|
+
removeNonManifoldEdges,
|
|
4701
5188
|
repairMesh,
|
|
4702
5189
|
scale,
|
|
4703
5190
|
smoothAllVertices,
|
|
@@ -4713,6 +5200,7 @@ export {
|
|
|
4713
5200
|
triangleInradius,
|
|
4714
5201
|
triangleNormal,
|
|
4715
5202
|
triangleQuality,
|
|
5203
|
+
unifyNormals,
|
|
4716
5204
|
validateGeometry,
|
|
4717
5205
|
validateTopology
|
|
4718
5206
|
};
|