remesh-threejs 0.2.1 → 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.
@@ -4240,12 +4240,368 @@ 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 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
+ }
4243
4597
  export {
4244
4598
  AdaptiveRemesher,
4245
4599
  BVH,
4246
4600
  BufferGeometryExporter,
4247
4601
  BufferGeometryImporter,
4248
4602
  DEFAULT_REMESH_OPTIONS,
4603
+ DegenerateFaceRepair,
4604
+ DuplicateFaceRepair,
4249
4605
  Edge,
4250
4606
  EdgeContractor,
4251
4607
  EdgeFlipper,
@@ -4254,9 +4610,12 @@ export {
4254
4610
  Face,
4255
4611
  FeatureSkeleton,
4256
4612
  Halfedge,
4613
+ IsolatedVertexRepair,
4257
4614
  ManifoldAnalyzer,
4615
+ MeshRepairer,
4258
4616
  NonManifoldMesh,
4259
4617
  QualityMetrics,
4618
+ RepairOperation,
4260
4619
  SkeletonBuilder,
4261
4620
  SkeletonConstraints,
4262
4621
  SkeletonSegment,
@@ -4336,6 +4695,10 @@ export {
4336
4695
  reclassifyVertices,
4337
4696
  relocateVertex,
4338
4697
  remesh,
4698
+ removeDegenerateFaces,
4699
+ removeDuplicateFaces,
4700
+ removeIsolatedVertices,
4701
+ repairMesh,
4339
4702
  scale,
4340
4703
  smoothAllVertices,
4341
4704
  smoothVertex,