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.
@@ -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
  };