remesh-threejs 0.2.0 → 0.3.0

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.
@@ -3925,6 +3925,24 @@ class VertexRelocator {
3925
3925
  return vertex.type === VertexType.Manifold || vertex.type === VertexType.OpenBook;
3926
3926
  }
3927
3927
  }
3928
+ function safeMin(arr) {
3929
+ if (arr.length === 0) return 0;
3930
+ let min = arr[0];
3931
+ for (let i = 1; i < arr.length; i++) {
3932
+ const val = arr[i];
3933
+ if (val < min) min = val;
3934
+ }
3935
+ return min;
3936
+ }
3937
+ function safeMax(arr) {
3938
+ if (arr.length === 0) return 0;
3939
+ let max = arr[0];
3940
+ for (let i = 1; i < arr.length; i++) {
3941
+ const val = arr[i];
3942
+ if (val > max) max = val;
3943
+ }
3944
+ return max;
3945
+ }
3928
3946
  function computeMeshQuality(mesh, poorQualityThreshold = 0.3) {
3929
3947
  const faces = mesh.getFaces();
3930
3948
  const edges = mesh.getEdges();
@@ -3941,17 +3959,17 @@ function computeMeshQuality(mesh, poorQualityThreshold = 0.3) {
3941
3959
  }
3942
3960
  }
3943
3961
  const edgeLengths = edges.map((e) => e.length);
3944
- const minQuality = qualities.length > 0 ? Math.min(...qualities) : 0;
3945
- const maxQuality = qualities.length > 0 ? Math.max(...qualities) : 0;
3962
+ const minQuality = safeMin(qualities);
3963
+ const maxQuality = safeMax(qualities);
3946
3964
  const averageQuality = qualities.length > 0 ? qualities.reduce((a, b) => a + b, 0) / qualities.length : 0;
3947
3965
  const variance = qualities.length > 0 ? qualities.reduce((sum, q) => sum + Math.pow(q - averageQuality, 2), 0) / qualities.length : 0;
3948
3966
  const stdDevQuality = Math.sqrt(variance);
3949
3967
  const poorQualityCount = qualities.filter((q) => q < poorQualityThreshold).length;
3950
- const minEdgeLength = edgeLengths.length > 0 ? Math.min(...edgeLengths) : 0;
3951
- const maxEdgeLength = edgeLengths.length > 0 ? Math.max(...edgeLengths) : 0;
3968
+ const minEdgeLength = safeMin(edgeLengths);
3969
+ const maxEdgeLength = safeMax(edgeLengths);
3952
3970
  const averageEdgeLength = edgeLengths.length > 0 ? edgeLengths.reduce((a, b) => a + b, 0) / edgeLengths.length : 0;
3953
- const minArea = areas.length > 0 ? Math.min(...areas) : 0;
3954
- const maxArea = areas.length > 0 ? Math.max(...areas) : 0;
3971
+ const minArea = safeMin(areas);
3972
+ const maxArea = safeMax(areas);
3955
3973
  const totalArea = areas.reduce((a, b) => a + b, 0);
3956
3974
  return {
3957
3975
  minQuality,
@@ -4013,8 +4031,8 @@ function computeTriangleAspectRatio(face) {
4013
4031
  return null;
4014
4032
  }
4015
4033
  const lengths = halfedges.map((he) => he.edge.length);
4016
- const minLen = Math.min(...lengths);
4017
- const maxLen = Math.max(...lengths);
4034
+ const minLen = safeMin(lengths);
4035
+ const maxLen = safeMax(lengths);
4018
4036
  if (minLen < 1e-10) {
4019
4037
  return Infinity;
4020
4038
  }
@@ -4222,12 +4240,368 @@ function createRemesher(geometry, options = {}) {
4222
4240
  const mesh = NonManifoldMesh.fromBufferGeometry(geometry, options.featureEdges);
4223
4241
  return new AdaptiveRemesher(mesh, options);
4224
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 MeshRepairer {
4429
+ constructor(meshOrGeometry, options) {
4430
+ this.operations = [];
4431
+ if (meshOrGeometry instanceof NonManifoldMesh) {
4432
+ this.mesh = meshOrGeometry;
4433
+ } else {
4434
+ this.mesh = NonManifoldMesh.fromBufferGeometry(meshOrGeometry);
4435
+ }
4436
+ this.options = {
4437
+ useWorkers: (options == null ? void 0 : options.useWorkers) ?? (typeof navigator !== "undefined" && this.mesh.faces.size > ((options == null ? void 0 : options.parallelThreshold) ?? 1e4)),
4438
+ workerCount: (options == null ? void 0 : options.workerCount) ?? (typeof navigator !== "undefined" ? navigator.hardwareConcurrency || 4 : 4),
4439
+ useAcceleration: (options == null ? void 0 : options.useAcceleration) ?? true,
4440
+ parallelThreshold: (options == null ? void 0 : options.parallelThreshold) ?? 1e4,
4441
+ verbose: (options == null ? void 0 : options.verbose) ?? false,
4442
+ validateAfterEach: (options == null ? void 0 : options.validateAfterEach) ?? false
4443
+ };
4444
+ this.stats = {
4445
+ input: {
4446
+ vertices: this.mesh.vertices.size,
4447
+ faces: this.mesh.faces.size,
4448
+ edges: this.mesh.edges.size
4449
+ },
4450
+ output: {
4451
+ vertices: this.mesh.vertices.size,
4452
+ faces: this.mesh.faces.size,
4453
+ edges: this.mesh.edges.size
4454
+ },
4455
+ operations: [],
4456
+ totalTimeMs: 0,
4457
+ success: true,
4458
+ totalDefectsFound: 0,
4459
+ totalDefectsFixed: 0
4460
+ };
4461
+ }
4462
+ /**
4463
+ * Remove isolated vertices (vertices with no faces).
4464
+ * @returns this for chaining
4465
+ */
4466
+ removeIsolatedVertices() {
4467
+ this.operations.push(new IsolatedVertexRepair(this.mesh, this.options.verbose));
4468
+ return this;
4469
+ }
4470
+ /**
4471
+ * Remove zero-area and degenerate triangles.
4472
+ * @param areaThreshold - Minimum area threshold (default: 1e-10)
4473
+ * @returns this for chaining
4474
+ */
4475
+ removeDegenerateFaces(areaThreshold) {
4476
+ this.operations.push(new DegenerateFaceRepair(this.mesh, this.options.verbose, areaThreshold));
4477
+ return this;
4478
+ }
4479
+ /**
4480
+ * Remove duplicate faces with identical vertices.
4481
+ * @returns this for chaining
4482
+ */
4483
+ removeDuplicateFaces() {
4484
+ this.operations.push(new DuplicateFaceRepair(this.mesh, this.options.verbose));
4485
+ return this;
4486
+ }
4487
+ /**
4488
+ * Run all common repairs in optimal order.
4489
+ * @returns this for chaining
4490
+ */
4491
+ repairAll() {
4492
+ this.removeIsolatedVertices();
4493
+ this.removeDuplicateFaces();
4494
+ this.removeDegenerateFaces();
4495
+ return this;
4496
+ }
4497
+ /**
4498
+ * Execute all queued operations.
4499
+ * @returns Repair statistics
4500
+ */
4501
+ execute() {
4502
+ const startTime = performance.now();
4503
+ this.stats.operations = [];
4504
+ this.stats.totalDefectsFound = 0;
4505
+ this.stats.totalDefectsFixed = 0;
4506
+ this.stats.success = true;
4507
+ for (const operation of this.operations) {
4508
+ const opStats = operation.execute();
4509
+ this.stats.operations.push(opStats);
4510
+ this.stats.totalDefectsFound += opStats.defectsFound;
4511
+ this.stats.totalDefectsFixed += opStats.defectsFixed;
4512
+ if (!opStats.success) {
4513
+ this.stats.success = false;
4514
+ }
4515
+ if (this.options.validateAfterEach) {
4516
+ const validation = validateTopology(this.mesh);
4517
+ if (!validation.isValid) {
4518
+ const errors = [...validation.errors, ...validation.warnings];
4519
+ console.warn(`Topology validation failed after ${opStats.operation}:`, errors);
4520
+ this.stats.success = false;
4521
+ }
4522
+ }
4523
+ }
4524
+ this.stats.totalTimeMs = performance.now() - startTime;
4525
+ this.stats.output = {
4526
+ vertices: this.mesh.vertices.size,
4527
+ faces: this.mesh.faces.size,
4528
+ edges: this.mesh.edges.size
4529
+ };
4530
+ this.operations = [];
4531
+ return this.stats;
4532
+ }
4533
+ /**
4534
+ * Get current statistics.
4535
+ */
4536
+ getStats() {
4537
+ return this.stats;
4538
+ }
4539
+ /**
4540
+ * Get the repaired mesh.
4541
+ */
4542
+ getMesh() {
4543
+ return this.mesh;
4544
+ }
4545
+ /**
4546
+ * Export to BufferGeometry.
4547
+ */
4548
+ toBufferGeometry() {
4549
+ return exportBufferGeometry(this.mesh);
4550
+ }
4551
+ /**
4552
+ * Validate the mesh after repairs.
4553
+ */
4554
+ validate() {
4555
+ const validation = validateTopology(this.mesh);
4556
+ return {
4557
+ isValid: validation.isValid,
4558
+ errors: [
4559
+ ...validation.errors.map((e) => e.message),
4560
+ ...validation.warnings.map((w) => w.message)
4561
+ ]
4562
+ };
4563
+ }
4564
+ }
4565
+ function repairMesh(geometry, options) {
4566
+ const repairer = new MeshRepairer(geometry, options);
4567
+ const stats = repairer.repairAll().execute();
4568
+ return {
4569
+ geometry: repairer.toBufferGeometry(),
4570
+ stats
4571
+ };
4572
+ }
4573
+ function removeIsolatedVertices(geometry, options) {
4574
+ const repairer = new MeshRepairer(geometry, options);
4575
+ const stats = repairer.removeIsolatedVertices().execute();
4576
+ return {
4577
+ geometry: repairer.toBufferGeometry(),
4578
+ stats
4579
+ };
4580
+ }
4581
+ function removeDegenerateFaces(geometry, options) {
4582
+ const repairer = new MeshRepairer(geometry, options);
4583
+ const stats = repairer.removeDegenerateFaces(options == null ? void 0 : options.areaThreshold).execute();
4584
+ return {
4585
+ geometry: repairer.toBufferGeometry(),
4586
+ stats
4587
+ };
4588
+ }
4589
+ function removeDuplicateFaces(geometry, options) {
4590
+ const repairer = new MeshRepairer(geometry, options);
4591
+ const stats = repairer.removeDuplicateFaces().execute();
4592
+ return {
4593
+ geometry: repairer.toBufferGeometry(),
4594
+ stats
4595
+ };
4596
+ }
4225
4597
  export {
4226
4598
  AdaptiveRemesher,
4227
4599
  BVH,
4228
4600
  BufferGeometryExporter,
4229
4601
  BufferGeometryImporter,
4230
4602
  DEFAULT_REMESH_OPTIONS,
4603
+ DegenerateFaceRepair,
4604
+ DuplicateFaceRepair,
4231
4605
  Edge,
4232
4606
  EdgeContractor,
4233
4607
  EdgeFlipper,
@@ -4236,9 +4610,12 @@ export {
4236
4610
  Face,
4237
4611
  FeatureSkeleton,
4238
4612
  Halfedge,
4613
+ IsolatedVertexRepair,
4239
4614
  ManifoldAnalyzer,
4615
+ MeshRepairer,
4240
4616
  NonManifoldMesh,
4241
4617
  QualityMetrics,
4618
+ RepairOperation,
4242
4619
  SkeletonBuilder,
4243
4620
  SkeletonConstraints,
4244
4621
  SkeletonSegment,
@@ -4318,6 +4695,10 @@ export {
4318
4695
  reclassifyVertices,
4319
4696
  relocateVertex,
4320
4697
  remesh,
4698
+ removeDegenerateFaces,
4699
+ removeDuplicateFaces,
4700
+ removeIsolatedVertices,
4701
+ repairMesh,
4321
4702
  scale,
4322
4703
  smoothAllVertices,
4323
4704
  smoothVertex,