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.
- package/README.md +124 -5
- package/dist/index.d.ts +405 -0
- package/dist/remesh-threejs.cjs +1 -1
- package/dist/remesh-threejs.cjs.map +1 -1
- package/dist/remesh-threejs.js +851 -0
- package/dist/remesh-threejs.js.map +1 -1
- package/package.json +1 -1
package/dist/remesh-threejs.js
CHANGED
|
@@ -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
|
};
|