remesh-threejs 0.2.1 → 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.
@@ -4240,12 +4240,850 @@ function createRemesher(geometry, options = {}) {
4240
4240
  const mesh = NonManifoldMesh.fromBufferGeometry(geometry, options.featureEdges);
4241
4241
  return new AdaptiveRemesher(mesh, options);
4242
4242
  }
4243
+ class RepairOperation {
4244
+ constructor(mesh, verbose = false) {
4245
+ this.mesh = mesh;
4246
+ this.verbose = verbose;
4247
+ }
4248
+ /**
4249
+ * Execute the operation (detect + repair).
4250
+ * @returns Operation statistics
4251
+ */
4252
+ execute() {
4253
+ const startTime = performance.now();
4254
+ const defectsFound = this.detect();
4255
+ if (defectsFound === 0) {
4256
+ return {
4257
+ operation: this.getName(),
4258
+ defectsFound: 0,
4259
+ defectsFixed: 0,
4260
+ timeMs: performance.now() - startTime,
4261
+ success: true
4262
+ };
4263
+ }
4264
+ const defectsFixed = this.repair();
4265
+ const timeMs = performance.now() - startTime;
4266
+ const result = {
4267
+ operation: this.getName(),
4268
+ defectsFound,
4269
+ defectsFixed,
4270
+ timeMs,
4271
+ success: defectsFixed === defectsFound
4272
+ };
4273
+ if (defectsFixed < defectsFound) {
4274
+ result.reason = `Only fixed ${defectsFixed}/${defectsFound} defects`;
4275
+ }
4276
+ return result;
4277
+ }
4278
+ }
4279
+ class IsolatedVertexRepair extends RepairOperation {
4280
+ constructor() {
4281
+ super(...arguments);
4282
+ this.isolatedVertices = [];
4283
+ }
4284
+ detect() {
4285
+ this.isolatedVertices = [];
4286
+ for (const vertex of this.mesh.vertices.values()) {
4287
+ if (!vertex.halfedge || vertex.degree() === 0) {
4288
+ this.isolatedVertices.push(vertex);
4289
+ }
4290
+ }
4291
+ if (this.verbose && this.isolatedVertices.length > 0) {
4292
+ console.log(`Found ${this.isolatedVertices.length} isolated vertices`);
4293
+ }
4294
+ return this.isolatedVertices.length;
4295
+ }
4296
+ repair() {
4297
+ let fixedCount = 0;
4298
+ for (const vertex of this.isolatedVertices) {
4299
+ this.mesh.vertices.delete(vertex.id);
4300
+ fixedCount++;
4301
+ }
4302
+ if (this.verbose && fixedCount > 0) {
4303
+ console.log(`Removed ${fixedCount} isolated vertices`);
4304
+ }
4305
+ return fixedCount;
4306
+ }
4307
+ getName() {
4308
+ return "Remove Isolated Vertices";
4309
+ }
4310
+ canParallelize() {
4311
+ return this.isolatedVertices.length > 1e3;
4312
+ }
4313
+ }
4314
+ class DegenerateFaceRepair extends RepairOperation {
4315
+ constructor(mesh, verbose = false, areaThreshold = 1e-10) {
4316
+ super(mesh, verbose);
4317
+ this.degenerateFaces = [];
4318
+ this.areaThreshold = areaThreshold;
4319
+ }
4320
+ detect() {
4321
+ this.degenerateFaces = [];
4322
+ for (const face of this.mesh.faces.values()) {
4323
+ const vertices = face.getVertices();
4324
+ if (!vertices || vertices.length !== 3) continue;
4325
+ const [v0, v1, v2] = vertices;
4326
+ if (v0.id === v1.id || v1.id === v2.id || v2.id === v0.id) {
4327
+ this.degenerateFaces.push(face);
4328
+ continue;
4329
+ }
4330
+ const area = triangleArea(v0.position, v1.position, v2.position);
4331
+ if (area < this.areaThreshold) {
4332
+ this.degenerateFaces.push(face);
4333
+ }
4334
+ }
4335
+ if (this.verbose && this.degenerateFaces.length > 0) {
4336
+ console.log(`Found ${this.degenerateFaces.length} degenerate faces`);
4337
+ }
4338
+ return this.degenerateFaces.length;
4339
+ }
4340
+ repair() {
4341
+ let fixedCount = 0;
4342
+ for (const face of this.degenerateFaces) {
4343
+ const halfedges = face.getHalfedges();
4344
+ if (!halfedges) continue;
4345
+ this.mesh.faces.delete(face.id);
4346
+ for (const he of halfedges) {
4347
+ this.mesh.halfedges.delete(he.id);
4348
+ he.edge.allHalfedges = he.edge.allHalfedges.filter((h) => h.id !== he.id);
4349
+ }
4350
+ fixedCount++;
4351
+ }
4352
+ if (this.verbose && fixedCount > 0) {
4353
+ console.log(`Removed ${fixedCount} degenerate faces`);
4354
+ }
4355
+ return fixedCount;
4356
+ }
4357
+ getName() {
4358
+ return "Remove Degenerate Faces";
4359
+ }
4360
+ canParallelize() {
4361
+ return this.degenerateFaces.length > 1e3;
4362
+ }
4363
+ }
4364
+ class DuplicateFaceRepair extends RepairOperation {
4365
+ constructor() {
4366
+ super(...arguments);
4367
+ this.duplicates = /* @__PURE__ */ new Map();
4368
+ }
4369
+ detect() {
4370
+ this.duplicates.clear();
4371
+ const faceMap = /* @__PURE__ */ new Map();
4372
+ for (const face of this.mesh.faces.values()) {
4373
+ const vertices = face.getVertices();
4374
+ if (!vertices) continue;
4375
+ const key = this.makeFaceKey(vertices);
4376
+ if (!faceMap.has(key)) {
4377
+ faceMap.set(key, []);
4378
+ }
4379
+ faceMap.get(key).push(face);
4380
+ }
4381
+ let duplicateCount = 0;
4382
+ for (const [key, faces] of faceMap) {
4383
+ if (faces.length > 1) {
4384
+ this.duplicates.set(key, faces);
4385
+ duplicateCount += faces.length - 1;
4386
+ }
4387
+ }
4388
+ if (this.verbose && duplicateCount > 0) {
4389
+ console.log(`Found ${duplicateCount} duplicate faces in ${this.duplicates.size} groups`);
4390
+ }
4391
+ return duplicateCount;
4392
+ }
4393
+ repair() {
4394
+ let fixedCount = 0;
4395
+ for (const faces of this.duplicates.values()) {
4396
+ for (let i = 1; i < faces.length; i++) {
4397
+ const face = faces[i];
4398
+ if (!face) continue;
4399
+ this.mesh.faces.delete(face.id);
4400
+ const halfedges = face.getHalfedges();
4401
+ if (!halfedges) continue;
4402
+ for (const he of halfedges) {
4403
+ this.mesh.halfedges.delete(he.id);
4404
+ he.edge.allHalfedges = he.edge.allHalfedges.filter((h) => h.id !== he.id);
4405
+ }
4406
+ fixedCount++;
4407
+ }
4408
+ }
4409
+ if (this.verbose && fixedCount > 0) {
4410
+ console.log(`Removed ${fixedCount} duplicate faces`);
4411
+ }
4412
+ return fixedCount;
4413
+ }
4414
+ /**
4415
+ * Create canonical key from sorted vertex IDs.
4416
+ */
4417
+ makeFaceKey(vertices) {
4418
+ const ids = vertices.map((v) => v.id).sort((a, b) => a - b);
4419
+ return ids.join(",");
4420
+ }
4421
+ getName() {
4422
+ return "Remove Duplicate Faces";
4423
+ }
4424
+ canParallelize() {
4425
+ return false;
4426
+ }
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
+ }
4857
+ class MeshRepairer {
4858
+ constructor(meshOrGeometry, options) {
4859
+ this.operations = [];
4860
+ if (meshOrGeometry instanceof NonManifoldMesh) {
4861
+ this.mesh = meshOrGeometry;
4862
+ } else {
4863
+ this.mesh = NonManifoldMesh.fromBufferGeometry(meshOrGeometry);
4864
+ }
4865
+ this.options = {
4866
+ useWorkers: (options == null ? void 0 : options.useWorkers) ?? (typeof navigator !== "undefined" && this.mesh.faces.size > ((options == null ? void 0 : options.parallelThreshold) ?? 1e4)),
4867
+ workerCount: (options == null ? void 0 : options.workerCount) ?? (typeof navigator !== "undefined" ? navigator.hardwareConcurrency || 4 : 4),
4868
+ useAcceleration: (options == null ? void 0 : options.useAcceleration) ?? true,
4869
+ parallelThreshold: (options == null ? void 0 : options.parallelThreshold) ?? 1e4,
4870
+ verbose: (options == null ? void 0 : options.verbose) ?? false,
4871
+ validateAfterEach: (options == null ? void 0 : options.validateAfterEach) ?? false
4872
+ };
4873
+ this.stats = {
4874
+ input: {
4875
+ vertices: this.mesh.vertices.size,
4876
+ faces: this.mesh.faces.size,
4877
+ edges: this.mesh.edges.size
4878
+ },
4879
+ output: {
4880
+ vertices: this.mesh.vertices.size,
4881
+ faces: this.mesh.faces.size,
4882
+ edges: this.mesh.edges.size
4883
+ },
4884
+ operations: [],
4885
+ totalTimeMs: 0,
4886
+ success: true,
4887
+ totalDefectsFound: 0,
4888
+ totalDefectsFixed: 0
4889
+ };
4890
+ }
4891
+ /**
4892
+ * Remove isolated vertices (vertices with no faces).
4893
+ * @returns this for chaining
4894
+ */
4895
+ removeIsolatedVertices() {
4896
+ this.operations.push(new IsolatedVertexRepair(this.mesh, this.options.verbose));
4897
+ return this;
4898
+ }
4899
+ /**
4900
+ * Remove zero-area and degenerate triangles.
4901
+ * @param areaThreshold - Minimum area threshold (default: 1e-10)
4902
+ * @returns this for chaining
4903
+ */
4904
+ removeDegenerateFaces(areaThreshold) {
4905
+ this.operations.push(new DegenerateFaceRepair(this.mesh, this.options.verbose, areaThreshold));
4906
+ return this;
4907
+ }
4908
+ /**
4909
+ * Remove duplicate faces with identical vertices.
4910
+ * @returns this for chaining
4911
+ */
4912
+ removeDuplicateFaces() {
4913
+ this.operations.push(new DuplicateFaceRepair(this.mesh, this.options.verbose));
4914
+ return this;
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
+ }
4943
+ /**
4944
+ * Run all common repairs in optimal order.
4945
+ * @returns this for chaining
4946
+ */
4947
+ repairAll() {
4948
+ this.removeIsolatedVertices();
4949
+ this.removeDuplicateFaces();
4950
+ this.removeDegenerateFaces();
4951
+ this.fillHoles();
4952
+ this.unifyNormals();
4953
+ return this;
4954
+ }
4955
+ /**
4956
+ * Execute all queued operations.
4957
+ * @returns Repair statistics
4958
+ */
4959
+ execute() {
4960
+ const startTime = performance.now();
4961
+ this.stats.operations = [];
4962
+ this.stats.totalDefectsFound = 0;
4963
+ this.stats.totalDefectsFixed = 0;
4964
+ this.stats.success = true;
4965
+ for (const operation of this.operations) {
4966
+ const opStats = operation.execute();
4967
+ this.stats.operations.push(opStats);
4968
+ this.stats.totalDefectsFound += opStats.defectsFound;
4969
+ this.stats.totalDefectsFixed += opStats.defectsFixed;
4970
+ if (!opStats.success) {
4971
+ this.stats.success = false;
4972
+ }
4973
+ if (this.options.validateAfterEach) {
4974
+ const validation = validateTopology(this.mesh);
4975
+ if (!validation.isValid) {
4976
+ const errors = [...validation.errors, ...validation.warnings];
4977
+ console.warn(`Topology validation failed after ${opStats.operation}:`, errors);
4978
+ this.stats.success = false;
4979
+ }
4980
+ }
4981
+ }
4982
+ this.stats.totalTimeMs = performance.now() - startTime;
4983
+ this.stats.output = {
4984
+ vertices: this.mesh.vertices.size,
4985
+ faces: this.mesh.faces.size,
4986
+ edges: this.mesh.edges.size
4987
+ };
4988
+ this.operations = [];
4989
+ return this.stats;
4990
+ }
4991
+ /**
4992
+ * Get current statistics.
4993
+ */
4994
+ getStats() {
4995
+ return this.stats;
4996
+ }
4997
+ /**
4998
+ * Get the repaired mesh.
4999
+ */
5000
+ getMesh() {
5001
+ return this.mesh;
5002
+ }
5003
+ /**
5004
+ * Export to BufferGeometry.
5005
+ */
5006
+ toBufferGeometry() {
5007
+ return exportBufferGeometry(this.mesh);
5008
+ }
5009
+ /**
5010
+ * Validate the mesh after repairs.
5011
+ */
5012
+ validate() {
5013
+ const validation = validateTopology(this.mesh);
5014
+ return {
5015
+ isValid: validation.isValid,
5016
+ errors: [
5017
+ ...validation.errors.map((e) => e.message),
5018
+ ...validation.warnings.map((w) => w.message)
5019
+ ]
5020
+ };
5021
+ }
5022
+ }
5023
+ function repairMesh(geometry, options) {
5024
+ const repairer = new MeshRepairer(geometry, options);
5025
+ const stats = repairer.repairAll().execute();
5026
+ return {
5027
+ geometry: repairer.toBufferGeometry(),
5028
+ stats
5029
+ };
5030
+ }
5031
+ function removeIsolatedVertices(geometry, options) {
5032
+ const repairer = new MeshRepairer(geometry, options);
5033
+ const stats = repairer.removeIsolatedVertices().execute();
5034
+ return {
5035
+ geometry: repairer.toBufferGeometry(),
5036
+ stats
5037
+ };
5038
+ }
5039
+ function removeDegenerateFaces(geometry, options) {
5040
+ const repairer = new MeshRepairer(geometry, options);
5041
+ const stats = repairer.removeDegenerateFaces(options == null ? void 0 : options.areaThreshold).execute();
5042
+ return {
5043
+ geometry: repairer.toBufferGeometry(),
5044
+ stats
5045
+ };
5046
+ }
5047
+ function removeDuplicateFaces(geometry, options) {
5048
+ const repairer = new MeshRepairer(geometry, options);
5049
+ const stats = repairer.removeDuplicateFaces().execute();
5050
+ return {
5051
+ geometry: repairer.toBufferGeometry(),
5052
+ stats
5053
+ };
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
+ }
4243
5079
  export {
4244
5080
  AdaptiveRemesher,
4245
5081
  BVH,
4246
5082
  BufferGeometryExporter,
4247
5083
  BufferGeometryImporter,
4248
5084
  DEFAULT_REMESH_OPTIONS,
5085
+ DegenerateFaceRepair,
5086
+ DuplicateFaceRepair,
4249
5087
  Edge,
4250
5088
  EdgeContractor,
4251
5089
  EdgeFlipper,
@@ -4254,9 +5092,15 @@ export {
4254
5092
  Face,
4255
5093
  FeatureSkeleton,
4256
5094
  Halfedge,
5095
+ HoleFiller,
5096
+ IsolatedVertexRepair,
4257
5097
  ManifoldAnalyzer,
5098
+ MeshRepairer,
5099
+ NonManifoldEdgeRepair,
4258
5100
  NonManifoldMesh,
5101
+ NormalUnifier,
4259
5102
  QualityMetrics,
5103
+ RepairOperation,
4260
5104
  SkeletonBuilder,
4261
5105
  SkeletonConstraints,
4262
5106
  SkeletonSegment,
@@ -4304,6 +5148,7 @@ export {
4304
5148
  exportClassificationGeometry,
4305
5149
  exportQualityGeometry,
4306
5150
  exportSkeletonGeometry,
5151
+ fillHoles,
4307
5152
  flipEdge,
4308
5153
  fromVector3,
4309
5154
  getHighValenceVertices,
@@ -4336,6 +5181,11 @@ export {
4336
5181
  reclassifyVertices,
4337
5182
  relocateVertex,
4338
5183
  remesh,
5184
+ removeDegenerateFaces,
5185
+ removeDuplicateFaces,
5186
+ removeIsolatedVertices,
5187
+ removeNonManifoldEdges,
5188
+ repairMesh,
4339
5189
  scale,
4340
5190
  smoothAllVertices,
4341
5191
  smoothVertex,
@@ -4350,6 +5200,7 @@ export {
4350
5200
  triangleInradius,
4351
5201
  triangleNormal,
4352
5202
  triangleQuality,
5203
+ unifyNormals,
4353
5204
  validateGeometry,
4354
5205
  validateTopology
4355
5206
  };