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.
- package/README.md +106 -5
- package/dist/index.d.ts +253 -0
- package/dist/remesh-threejs.cjs +1 -1
- package/dist/remesh-threejs.cjs.map +1 -1
- package/dist/remesh-threejs.js +363 -0
- package/dist/remesh-threejs.js.map +1 -1
- package/package.json +1 -1
package/dist/remesh-threejs.js
CHANGED
|
@@ -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,
|