remesh-threejs 0.2.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/LICENSE +21 -0
- package/README.md +271 -0
- package/dist/index.d.ts +2505 -0
- package/dist/remesh-threejs.cjs +2 -0
- package/dist/remesh-threejs.cjs.map +1 -0
- package/dist/remesh-threejs.js +4338 -0
- package/dist/remesh-threejs.js.map +1 -0
- package/package.json +103 -0
|
@@ -0,0 +1,4338 @@
|
|
|
1
|
+
import { Vector3, BufferGeometry, BufferAttribute } from "three";
|
|
2
|
+
function createVertexId(id) {
|
|
3
|
+
return id;
|
|
4
|
+
}
|
|
5
|
+
function createEdgeId(id) {
|
|
6
|
+
return id;
|
|
7
|
+
}
|
|
8
|
+
function createHalfedgeId(id) {
|
|
9
|
+
return id;
|
|
10
|
+
}
|
|
11
|
+
function createFaceId(id) {
|
|
12
|
+
return id;
|
|
13
|
+
}
|
|
14
|
+
function createSegmentId(id) {
|
|
15
|
+
return id;
|
|
16
|
+
}
|
|
17
|
+
function toNumber(id) {
|
|
18
|
+
return id;
|
|
19
|
+
}
|
|
20
|
+
var VertexType = /* @__PURE__ */ ((VertexType2) => {
|
|
21
|
+
VertexType2["Manifold"] = "manifold";
|
|
22
|
+
VertexType2["OpenBook"] = "open_book";
|
|
23
|
+
VertexType2["SkeletonBranching"] = "skeleton_branching";
|
|
24
|
+
VertexType2["NonManifoldOther"] = "non_manifold_other";
|
|
25
|
+
return VertexType2;
|
|
26
|
+
})(VertexType || {});
|
|
27
|
+
var EdgeType = /* @__PURE__ */ ((EdgeType2) => {
|
|
28
|
+
EdgeType2["Manifold"] = "manifold";
|
|
29
|
+
EdgeType2["NonManifold"] = "non_manifold";
|
|
30
|
+
EdgeType2["Feature"] = "feature";
|
|
31
|
+
EdgeType2["Boundary"] = "boundary";
|
|
32
|
+
return EdgeType2;
|
|
33
|
+
})(EdgeType || {});
|
|
34
|
+
function canMoveFreely(type) {
|
|
35
|
+
return type === "manifold";
|
|
36
|
+
}
|
|
37
|
+
function isSkeletonConstrained(type) {
|
|
38
|
+
return type === "open_book";
|
|
39
|
+
}
|
|
40
|
+
function isPositionFixed(type) {
|
|
41
|
+
return type === "skeleton_branching" || type === "non_manifold_other";
|
|
42
|
+
}
|
|
43
|
+
function isSkeletonEdge(type) {
|
|
44
|
+
return type === "non_manifold" || type === "feature" || type === "boundary";
|
|
45
|
+
}
|
|
46
|
+
function canFlipEdge$1(type) {
|
|
47
|
+
return type === "manifold";
|
|
48
|
+
}
|
|
49
|
+
const DEFAULT_REMESH_OPTIONS = {
|
|
50
|
+
iterations: 5,
|
|
51
|
+
preserveBoundary: true,
|
|
52
|
+
minEdgeLengthRatio: 0.4,
|
|
53
|
+
maxEdgeLengthRatio: 1.333,
|
|
54
|
+
minTriangleQuality: 0.3,
|
|
55
|
+
maxNormalDeviation: Math.PI / 6,
|
|
56
|
+
useAcceleration: true,
|
|
57
|
+
chunkSize: 0,
|
|
58
|
+
memoryBudget: 0,
|
|
59
|
+
verbose: false
|
|
60
|
+
};
|
|
61
|
+
const MAX_ITERATIONS = 1e4;
|
|
62
|
+
class Vertex {
|
|
63
|
+
constructor(id, position) {
|
|
64
|
+
this.id = id;
|
|
65
|
+
this.position = position;
|
|
66
|
+
this.halfedge = null;
|
|
67
|
+
this.type = VertexType.Manifold;
|
|
68
|
+
this.isMarked = false;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Gets the degree of this vertex (number of incident edges).
|
|
72
|
+
* Returns null if the vertex has no incident halfedge.
|
|
73
|
+
*/
|
|
74
|
+
degree() {
|
|
75
|
+
if (!this.halfedge) {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
let count = 0;
|
|
79
|
+
let current = this.halfedge;
|
|
80
|
+
do {
|
|
81
|
+
count++;
|
|
82
|
+
if (!current.twin || !current.twin.next) {
|
|
83
|
+
break;
|
|
84
|
+
}
|
|
85
|
+
current = current.twin.next;
|
|
86
|
+
if (count > MAX_ITERATIONS) {
|
|
87
|
+
throw new Error(`Vertex ${this.id}: degree calculation exceeded maximum iterations`);
|
|
88
|
+
}
|
|
89
|
+
} while (current !== this.halfedge);
|
|
90
|
+
return count;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Iterates over all outgoing halfedges from this vertex.
|
|
94
|
+
*
|
|
95
|
+
* @param callback - Function called for each outgoing halfedge
|
|
96
|
+
*/
|
|
97
|
+
forEachOutgoingHalfedge(callback) {
|
|
98
|
+
if (!this.halfedge) {
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
let current = this.halfedge;
|
|
102
|
+
let iterationCount = 0;
|
|
103
|
+
do {
|
|
104
|
+
callback(current);
|
|
105
|
+
if (!current.twin || !current.twin.next) {
|
|
106
|
+
break;
|
|
107
|
+
}
|
|
108
|
+
current = current.twin.next;
|
|
109
|
+
iterationCount++;
|
|
110
|
+
if (iterationCount > MAX_ITERATIONS) {
|
|
111
|
+
throw new Error(`Vertex ${this.id}: halfedge iteration exceeded maximum iterations`);
|
|
112
|
+
}
|
|
113
|
+
} while (current !== this.halfedge);
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Collects all outgoing halfedges from this vertex into an array.
|
|
117
|
+
*
|
|
118
|
+
* @returns Array of outgoing halfedges
|
|
119
|
+
*/
|
|
120
|
+
getOutgoingHalfedges() {
|
|
121
|
+
const halfedges = [];
|
|
122
|
+
this.forEachOutgoingHalfedge((he) => halfedges.push(he));
|
|
123
|
+
return halfedges;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Iterates over all neighboring vertices.
|
|
127
|
+
*
|
|
128
|
+
* @param callback - Function called for each neighboring vertex
|
|
129
|
+
*/
|
|
130
|
+
forEachNeighbor(callback) {
|
|
131
|
+
this.forEachOutgoingHalfedge((he) => {
|
|
132
|
+
callback(he.vertex);
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Collects all neighboring vertices into an array.
|
|
137
|
+
*
|
|
138
|
+
* @returns Array of neighboring vertices
|
|
139
|
+
*/
|
|
140
|
+
getNeighbors() {
|
|
141
|
+
const neighbors = [];
|
|
142
|
+
this.forEachNeighbor((v) => neighbors.push(v));
|
|
143
|
+
return neighbors;
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Checks if this vertex is on the boundary of the mesh.
|
|
147
|
+
* A vertex is on the boundary if any of its incident halfedges has no face.
|
|
148
|
+
*/
|
|
149
|
+
isBoundary() {
|
|
150
|
+
if (!this.halfedge) {
|
|
151
|
+
return true;
|
|
152
|
+
}
|
|
153
|
+
let hasBoundaryHalfedge = false;
|
|
154
|
+
this.forEachOutgoingHalfedge((he) => {
|
|
155
|
+
if (!he.face) {
|
|
156
|
+
hasBoundaryHalfedge = true;
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
return hasBoundaryHalfedge;
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Checks if this vertex can move freely during remeshing.
|
|
163
|
+
* Only manifold vertices can move freely in 3D space.
|
|
164
|
+
*/
|
|
165
|
+
canMoveFreely() {
|
|
166
|
+
return this.type === VertexType.Manifold;
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Checks if this vertex is constrained to a skeleton segment.
|
|
170
|
+
* OpenBook vertices can only move along their skeleton segment.
|
|
171
|
+
*/
|
|
172
|
+
isSkeletonConstrained() {
|
|
173
|
+
return this.type === VertexType.OpenBook;
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Checks if this vertex has a fixed position.
|
|
177
|
+
* Branching and other non-manifold vertices cannot move.
|
|
178
|
+
*/
|
|
179
|
+
isPositionFixed() {
|
|
180
|
+
return this.type === VertexType.SkeletonBranching || this.type === VertexType.NonManifoldOther;
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Checks if this vertex is part of the feature skeleton.
|
|
184
|
+
*/
|
|
185
|
+
isOnSkeleton() {
|
|
186
|
+
return this.type !== VertexType.Manifold;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
class Halfedge {
|
|
190
|
+
constructor(id, vertex, edge) {
|
|
191
|
+
this.id = id;
|
|
192
|
+
this.twin = null;
|
|
193
|
+
this.next = null;
|
|
194
|
+
this.prev = null;
|
|
195
|
+
this.face = null;
|
|
196
|
+
this.vertex = vertex;
|
|
197
|
+
this.edge = edge;
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Gets the source vertex of this halfedge (the vertex it starts from).
|
|
201
|
+
* This is the target vertex of the twin halfedge.
|
|
202
|
+
*/
|
|
203
|
+
getSourceVertex() {
|
|
204
|
+
var _a;
|
|
205
|
+
return ((_a = this.twin) == null ? void 0 : _a.vertex) ?? null;
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Gets the target vertex of this halfedge.
|
|
209
|
+
*/
|
|
210
|
+
getTargetVertex() {
|
|
211
|
+
return this.vertex;
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Checks if this halfedge is on the boundary (has no face).
|
|
215
|
+
*/
|
|
216
|
+
isBoundary() {
|
|
217
|
+
return this.face === null;
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Gets the opposite halfedge in the same face (two edges away).
|
|
221
|
+
* For a triangle, this is the edge opposite to this halfedge's source vertex.
|
|
222
|
+
*/
|
|
223
|
+
getOppositeHalfedge() {
|
|
224
|
+
var _a;
|
|
225
|
+
return ((_a = this.next) == null ? void 0 : _a.next) ?? null;
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Gets the vertex opposite to this halfedge in its face.
|
|
229
|
+
* For a triangle, this is the vertex not on this halfedge.
|
|
230
|
+
*/
|
|
231
|
+
getOppositeVertex() {
|
|
232
|
+
var _a;
|
|
233
|
+
return ((_a = this.next) == null ? void 0 : _a.vertex) ?? null;
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* Computes the vector along this halfedge (from source to target).
|
|
237
|
+
* Returns null if source vertex is not available.
|
|
238
|
+
*/
|
|
239
|
+
getVector() {
|
|
240
|
+
const source = this.getSourceVertex();
|
|
241
|
+
if (!source) {
|
|
242
|
+
return null;
|
|
243
|
+
}
|
|
244
|
+
return {
|
|
245
|
+
x: this.vertex.position.x - source.position.x,
|
|
246
|
+
y: this.vertex.position.y - source.position.y,
|
|
247
|
+
z: this.vertex.position.z - source.position.z
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
class Edge {
|
|
252
|
+
constructor(id, halfedge, length2) {
|
|
253
|
+
this.id = id;
|
|
254
|
+
this.allHalfedges = [];
|
|
255
|
+
this.type = EdgeType.Manifold;
|
|
256
|
+
this.isInPath = false;
|
|
257
|
+
this.halfedge = halfedge;
|
|
258
|
+
this.length = length2;
|
|
259
|
+
this.allHalfedges.push(halfedge);
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* Adds a halfedge to this edge.
|
|
263
|
+
*/
|
|
264
|
+
addHalfedge(halfedge) {
|
|
265
|
+
this.allHalfedges.push(halfedge);
|
|
266
|
+
this.updateType();
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* Gets the number of halfedges (indicates non-manifoldness).
|
|
270
|
+
* 2 = manifold, >2 = non-manifold, 1 = boundary
|
|
271
|
+
*/
|
|
272
|
+
getHalfedgeCount() {
|
|
273
|
+
return this.allHalfedges.length;
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* Updates the edge type based on the number of incident faces.
|
|
277
|
+
*/
|
|
278
|
+
updateType() {
|
|
279
|
+
const faceCount = this.getFaceCount();
|
|
280
|
+
if (faceCount === 0) {
|
|
281
|
+
this.type = EdgeType.Boundary;
|
|
282
|
+
} else if (faceCount === 1) {
|
|
283
|
+
this.type = EdgeType.Boundary;
|
|
284
|
+
} else if (faceCount === 2) {
|
|
285
|
+
if (this.type !== EdgeType.Feature) {
|
|
286
|
+
this.type = EdgeType.Manifold;
|
|
287
|
+
}
|
|
288
|
+
} else {
|
|
289
|
+
this.type = EdgeType.NonManifold;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
293
|
+
* Gets the number of faces incident to this edge.
|
|
294
|
+
*/
|
|
295
|
+
getFaceCount() {
|
|
296
|
+
let count = 0;
|
|
297
|
+
for (const he of this.allHalfedges) {
|
|
298
|
+
if (he.face !== null) {
|
|
299
|
+
count++;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
return count;
|
|
303
|
+
}
|
|
304
|
+
/**
|
|
305
|
+
* Gets both vertices of this edge.
|
|
306
|
+
* Returns [v0, v1] where v0 is the source of halfedge and v1 is the target.
|
|
307
|
+
*/
|
|
308
|
+
getVertices() {
|
|
309
|
+
const v0 = this.halfedge.getSourceVertex();
|
|
310
|
+
const v1 = this.halfedge.getTargetVertex();
|
|
311
|
+
if (!v0) {
|
|
312
|
+
return [null, null];
|
|
313
|
+
}
|
|
314
|
+
return [v0, v1];
|
|
315
|
+
}
|
|
316
|
+
/**
|
|
317
|
+
* Gets all faces adjacent to this edge.
|
|
318
|
+
* For manifold edges, returns up to 2 faces.
|
|
319
|
+
* For non-manifold edges, returns all incident faces.
|
|
320
|
+
*/
|
|
321
|
+
getFaces() {
|
|
322
|
+
const faces = [];
|
|
323
|
+
for (const he of this.allHalfedges) {
|
|
324
|
+
if (he.face !== null) {
|
|
325
|
+
faces.push(he.face);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
return faces;
|
|
329
|
+
}
|
|
330
|
+
/**
|
|
331
|
+
* Gets both faces adjacent to this edge (for manifold edges).
|
|
332
|
+
* Returns [f0, f1] where f0 is the face of halfedge and f1 is the face of twin.
|
|
333
|
+
* Either face can be null for boundary edges.
|
|
334
|
+
*/
|
|
335
|
+
getTwoFaces() {
|
|
336
|
+
var _a;
|
|
337
|
+
const f0 = this.halfedge.face;
|
|
338
|
+
const f1 = ((_a = this.halfedge.twin) == null ? void 0 : _a.face) ?? null;
|
|
339
|
+
return [f0, f1];
|
|
340
|
+
}
|
|
341
|
+
/**
|
|
342
|
+
* Checks if this edge is on the boundary (has only one adjacent face).
|
|
343
|
+
*/
|
|
344
|
+
isBoundary() {
|
|
345
|
+
return this.type === EdgeType.Boundary;
|
|
346
|
+
}
|
|
347
|
+
/**
|
|
348
|
+
* Checks if this edge is non-manifold (has more than 2 adjacent faces).
|
|
349
|
+
*/
|
|
350
|
+
isNonManifold() {
|
|
351
|
+
return this.type === EdgeType.NonManifold;
|
|
352
|
+
}
|
|
353
|
+
/**
|
|
354
|
+
* Checks if this edge is part of the feature skeleton.
|
|
355
|
+
*/
|
|
356
|
+
isSkeletonEdge() {
|
|
357
|
+
return this.type === EdgeType.NonManifold || this.type === EdgeType.Feature || this.type === EdgeType.Boundary;
|
|
358
|
+
}
|
|
359
|
+
/**
|
|
360
|
+
* Checks if this edge can be flipped.
|
|
361
|
+
* An edge can be flipped if:
|
|
362
|
+
* 1. It has exactly two adjacent faces (manifold, not skeleton)
|
|
363
|
+
* 2. Both endpoints have degree > 1
|
|
364
|
+
* 3. The quadrilateral formed is convex (checked separately)
|
|
365
|
+
*/
|
|
366
|
+
canFlip() {
|
|
367
|
+
if (this.type !== EdgeType.Manifold) {
|
|
368
|
+
return false;
|
|
369
|
+
}
|
|
370
|
+
if (this.getFaceCount() !== 2) {
|
|
371
|
+
return false;
|
|
372
|
+
}
|
|
373
|
+
const [v0, v1] = this.getVertices();
|
|
374
|
+
if (!v0 || !v1) {
|
|
375
|
+
return false;
|
|
376
|
+
}
|
|
377
|
+
const degree0 = v0.degree();
|
|
378
|
+
const degree1 = v1.degree();
|
|
379
|
+
if (degree0 === null || degree1 === null || degree0 <= 1 || degree1 <= 1) {
|
|
380
|
+
return false;
|
|
381
|
+
}
|
|
382
|
+
return true;
|
|
383
|
+
}
|
|
384
|
+
/**
|
|
385
|
+
* Gets the other vertex of this edge (given one vertex).
|
|
386
|
+
*/
|
|
387
|
+
getOtherVertex(v) {
|
|
388
|
+
const [v0, v1] = this.getVertices();
|
|
389
|
+
if (!v0 || !v1) {
|
|
390
|
+
return null;
|
|
391
|
+
}
|
|
392
|
+
if (v.id === v0.id) {
|
|
393
|
+
return v1;
|
|
394
|
+
} else if (v.id === v1.id) {
|
|
395
|
+
return v0;
|
|
396
|
+
}
|
|
397
|
+
return null;
|
|
398
|
+
}
|
|
399
|
+
/**
|
|
400
|
+
* Marks this edge as a feature edge.
|
|
401
|
+
*/
|
|
402
|
+
markAsFeature() {
|
|
403
|
+
if (this.type === EdgeType.Manifold) {
|
|
404
|
+
this.type = EdgeType.Feature;
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
class Face {
|
|
409
|
+
constructor(id, halfedge) {
|
|
410
|
+
this.id = id;
|
|
411
|
+
this.isMarked = false;
|
|
412
|
+
this.halfedge = halfedge;
|
|
413
|
+
}
|
|
414
|
+
/**
|
|
415
|
+
* Gets all three vertices of this face.
|
|
416
|
+
* Returns vertices in counter-clockwise order.
|
|
417
|
+
*/
|
|
418
|
+
getVertices() {
|
|
419
|
+
const he0 = this.halfedge;
|
|
420
|
+
const he1 = he0.next;
|
|
421
|
+
const he2 = he1 == null ? void 0 : he1.next;
|
|
422
|
+
if (!he1 || !he2) {
|
|
423
|
+
return null;
|
|
424
|
+
}
|
|
425
|
+
return [he0.vertex, he1.vertex, he2.vertex];
|
|
426
|
+
}
|
|
427
|
+
/**
|
|
428
|
+
* Gets all three halfedges of this face.
|
|
429
|
+
* Returns halfedges in counter-clockwise order.
|
|
430
|
+
*/
|
|
431
|
+
getHalfedges() {
|
|
432
|
+
const he0 = this.halfedge;
|
|
433
|
+
const he1 = he0.next;
|
|
434
|
+
const he2 = he1 == null ? void 0 : he1.next;
|
|
435
|
+
if (!he1 || !he2) {
|
|
436
|
+
return null;
|
|
437
|
+
}
|
|
438
|
+
return [he0, he1, he2];
|
|
439
|
+
}
|
|
440
|
+
/**
|
|
441
|
+
* Iterates over the three halfedges of this face.
|
|
442
|
+
*
|
|
443
|
+
* @param callback - Function called for each halfedge
|
|
444
|
+
*/
|
|
445
|
+
forEachHalfedge(callback) {
|
|
446
|
+
const halfedges = this.getHalfedges();
|
|
447
|
+
if (!halfedges) {
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
for (const he of halfedges) {
|
|
451
|
+
callback(he);
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
/**
|
|
455
|
+
* Iterates over the three vertices of this face.
|
|
456
|
+
*
|
|
457
|
+
* @param callback - Function called for each vertex
|
|
458
|
+
*/
|
|
459
|
+
forEachVertex(callback) {
|
|
460
|
+
const vertices = this.getVertices();
|
|
461
|
+
if (!vertices) {
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
464
|
+
for (const v of vertices) {
|
|
465
|
+
callback(v);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
/**
|
|
469
|
+
* Computes the centroid (center of mass) of this face.
|
|
470
|
+
* Returns null if vertices cannot be retrieved.
|
|
471
|
+
*/
|
|
472
|
+
getCentroid() {
|
|
473
|
+
const vertices = this.getVertices();
|
|
474
|
+
if (!vertices) {
|
|
475
|
+
return null;
|
|
476
|
+
}
|
|
477
|
+
const [v0, v1, v2] = vertices;
|
|
478
|
+
return {
|
|
479
|
+
x: (v0.position.x + v1.position.x + v2.position.x) / 3,
|
|
480
|
+
y: (v0.position.y + v1.position.y + v2.position.y) / 3,
|
|
481
|
+
z: (v0.position.z + v1.position.z + v2.position.z) / 3
|
|
482
|
+
};
|
|
483
|
+
}
|
|
484
|
+
/**
|
|
485
|
+
* Computes the normal vector of this face.
|
|
486
|
+
* Uses the cross product of two edges.
|
|
487
|
+
* Returns null if vertices cannot be retrieved.
|
|
488
|
+
*/
|
|
489
|
+
getNormal() {
|
|
490
|
+
const vertices = this.getVertices();
|
|
491
|
+
if (!vertices) {
|
|
492
|
+
return null;
|
|
493
|
+
}
|
|
494
|
+
const [v0, v1, v2] = vertices;
|
|
495
|
+
const e1x = v1.position.x - v0.position.x;
|
|
496
|
+
const e1y = v1.position.y - v0.position.y;
|
|
497
|
+
const e1z = v1.position.z - v0.position.z;
|
|
498
|
+
const e2x = v2.position.x - v0.position.x;
|
|
499
|
+
const e2y = v2.position.y - v0.position.y;
|
|
500
|
+
const e2z = v2.position.z - v0.position.z;
|
|
501
|
+
const nx = e1y * e2z - e1z * e2y;
|
|
502
|
+
const ny = e1z * e2x - e1x * e2z;
|
|
503
|
+
const nz = e1x * e2y - e1y * e2x;
|
|
504
|
+
const len = Math.sqrt(nx * nx + ny * ny + nz * nz);
|
|
505
|
+
if (len < 1e-10) {
|
|
506
|
+
return { x: 0, y: 0, z: 1 };
|
|
507
|
+
}
|
|
508
|
+
return {
|
|
509
|
+
x: nx / len,
|
|
510
|
+
y: ny / len,
|
|
511
|
+
z: nz / len
|
|
512
|
+
};
|
|
513
|
+
}
|
|
514
|
+
/**
|
|
515
|
+
* Computes the area of this face.
|
|
516
|
+
* Returns null if vertices cannot be retrieved.
|
|
517
|
+
*/
|
|
518
|
+
getArea() {
|
|
519
|
+
const vertices = this.getVertices();
|
|
520
|
+
if (!vertices) {
|
|
521
|
+
return null;
|
|
522
|
+
}
|
|
523
|
+
const [v0, v1, v2] = vertices;
|
|
524
|
+
const e1x = v1.position.x - v0.position.x;
|
|
525
|
+
const e1y = v1.position.y - v0.position.y;
|
|
526
|
+
const e1z = v1.position.z - v0.position.z;
|
|
527
|
+
const e2x = v2.position.x - v0.position.x;
|
|
528
|
+
const e2y = v2.position.y - v0.position.y;
|
|
529
|
+
const e2z = v2.position.z - v0.position.z;
|
|
530
|
+
const cx = e1y * e2z - e1z * e2y;
|
|
531
|
+
const cy = e1z * e2x - e1x * e2z;
|
|
532
|
+
const cz = e1x * e2y - e1y * e2x;
|
|
533
|
+
return Math.sqrt(cx * cx + cy * cy + cz * cz) / 2;
|
|
534
|
+
}
|
|
535
|
+
/**
|
|
536
|
+
* Computes the quality of this triangle.
|
|
537
|
+
* Quality is measured as 2 * inradius / circumradius (ranges from 0 to 1).
|
|
538
|
+
* A value of 1 indicates an equilateral triangle.
|
|
539
|
+
* Returns null if vertices cannot be retrieved.
|
|
540
|
+
*/
|
|
541
|
+
getQuality() {
|
|
542
|
+
const vertices = this.getVertices();
|
|
543
|
+
if (!vertices) {
|
|
544
|
+
return null;
|
|
545
|
+
}
|
|
546
|
+
const [v0, v1, v2] = vertices;
|
|
547
|
+
const a = Math.sqrt(
|
|
548
|
+
Math.pow(v1.position.x - v2.position.x, 2) + Math.pow(v1.position.y - v2.position.y, 2) + Math.pow(v1.position.z - v2.position.z, 2)
|
|
549
|
+
);
|
|
550
|
+
const b = Math.sqrt(
|
|
551
|
+
Math.pow(v0.position.x - v2.position.x, 2) + Math.pow(v0.position.y - v2.position.y, 2) + Math.pow(v0.position.z - v2.position.z, 2)
|
|
552
|
+
);
|
|
553
|
+
const c = Math.sqrt(
|
|
554
|
+
Math.pow(v0.position.x - v1.position.x, 2) + Math.pow(v0.position.y - v1.position.y, 2) + Math.pow(v0.position.z - v1.position.z, 2)
|
|
555
|
+
);
|
|
556
|
+
const s = (a + b + c) / 2;
|
|
557
|
+
const areaSquared = s * (s - a) * (s - b) * (s - c);
|
|
558
|
+
if (areaSquared <= 0) {
|
|
559
|
+
return 0;
|
|
560
|
+
}
|
|
561
|
+
const area = Math.sqrt(areaSquared);
|
|
562
|
+
const inradius = area / s;
|
|
563
|
+
const circumradius = a * b * c / (4 * area);
|
|
564
|
+
if (circumradius < 1e-10) {
|
|
565
|
+
return 0;
|
|
566
|
+
}
|
|
567
|
+
const quality = 2 * inradius / circumradius;
|
|
568
|
+
return Math.max(0, Math.min(1, quality));
|
|
569
|
+
}
|
|
570
|
+
/**
|
|
571
|
+
* Checks if this face contains a given vertex.
|
|
572
|
+
*/
|
|
573
|
+
containsVertex(vertex) {
|
|
574
|
+
const vertices = this.getVertices();
|
|
575
|
+
if (!vertices) {
|
|
576
|
+
return false;
|
|
577
|
+
}
|
|
578
|
+
return vertices.some((v) => v.id === vertex.id);
|
|
579
|
+
}
|
|
580
|
+
/**
|
|
581
|
+
* Gets the halfedge opposite to a given vertex.
|
|
582
|
+
* Returns null if the vertex is not part of this face.
|
|
583
|
+
*/
|
|
584
|
+
getOppositeHalfedge(vertex) {
|
|
585
|
+
const halfedges = this.getHalfedges();
|
|
586
|
+
if (!halfedges) {
|
|
587
|
+
return null;
|
|
588
|
+
}
|
|
589
|
+
for (const he of halfedges) {
|
|
590
|
+
const source = he.getSourceVertex();
|
|
591
|
+
if (source && source.id !== vertex.id && he.vertex.id !== vertex.id) {
|
|
592
|
+
return he;
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
return null;
|
|
596
|
+
}
|
|
597
|
+
/**
|
|
598
|
+
* Checks if this face is degenerate (has zero or near-zero area).
|
|
599
|
+
*/
|
|
600
|
+
isDegenerate(epsilon = 1e-10) {
|
|
601
|
+
const area = this.getArea();
|
|
602
|
+
return area === null || area < epsilon;
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
class NonManifoldMesh {
|
|
606
|
+
constructor() {
|
|
607
|
+
this.vertices = /* @__PURE__ */ new Map();
|
|
608
|
+
this.edges = /* @__PURE__ */ new Map();
|
|
609
|
+
this.halfedges = /* @__PURE__ */ new Map();
|
|
610
|
+
this.faces = /* @__PURE__ */ new Map();
|
|
611
|
+
this.nextVertexId = 0;
|
|
612
|
+
this.nextEdgeId = 0;
|
|
613
|
+
this.nextHalfedgeId = 0;
|
|
614
|
+
this.nextFaceId = 0;
|
|
615
|
+
this.edgeMap = /* @__PURE__ */ new Map();
|
|
616
|
+
}
|
|
617
|
+
/**
|
|
618
|
+
* Creates a non-manifold mesh from a Three.js BufferGeometry.
|
|
619
|
+
*
|
|
620
|
+
* @param geometry - The input geometry (must be indexed triangles)
|
|
621
|
+
* @param featureEdges - Optional user-defined feature edges to preserve
|
|
622
|
+
*/
|
|
623
|
+
static fromBufferGeometry(geometry, featureEdges) {
|
|
624
|
+
const mesh = new NonManifoldMesh();
|
|
625
|
+
const positions = geometry.attributes["position"];
|
|
626
|
+
if (!positions) {
|
|
627
|
+
throw new Error("Geometry must have a position attribute");
|
|
628
|
+
}
|
|
629
|
+
const indices = geometry.index;
|
|
630
|
+
if (!indices) {
|
|
631
|
+
throw new Error("Geometry must be indexed");
|
|
632
|
+
}
|
|
633
|
+
const numVertices = positions.count;
|
|
634
|
+
const numFaces = indices.count / 3;
|
|
635
|
+
if (indices.count % 3 !== 0) {
|
|
636
|
+
throw new Error("Geometry must be triangulated (indices count must be divisible by 3)");
|
|
637
|
+
}
|
|
638
|
+
for (let i = 0; i < numVertices; i++) {
|
|
639
|
+
const x = positions.getX(i);
|
|
640
|
+
const y = positions.getY(i);
|
|
641
|
+
const z = positions.getZ(i);
|
|
642
|
+
const vertex = new Vertex(createVertexId(mesh.nextVertexId++), new Vector3(x, y, z));
|
|
643
|
+
mesh.vertices.set(vertex.id, vertex);
|
|
644
|
+
}
|
|
645
|
+
for (let i = 0; i < numFaces; i++) {
|
|
646
|
+
const i0 = indices.getX(i * 3 + 0);
|
|
647
|
+
const i1 = indices.getX(i * 3 + 1);
|
|
648
|
+
const i2 = indices.getX(i * 3 + 2);
|
|
649
|
+
const v0 = mesh.vertices.get(createVertexId(i0));
|
|
650
|
+
const v1 = mesh.vertices.get(createVertexId(i1));
|
|
651
|
+
const v2 = mesh.vertices.get(createVertexId(i2));
|
|
652
|
+
if (!v0 || !v1 || !v2) {
|
|
653
|
+
throw new Error(`Vertex not found in face ${i}: ${i0}, ${i1}, ${i2}`);
|
|
654
|
+
}
|
|
655
|
+
const edge01 = mesh.getOrCreateEdge(i0, i1);
|
|
656
|
+
const edge12 = mesh.getOrCreateEdge(i1, i2);
|
|
657
|
+
const edge20 = mesh.getOrCreateEdge(i2, i0);
|
|
658
|
+
const face = new Face(createFaceId(mesh.nextFaceId++), null);
|
|
659
|
+
mesh.faces.set(face.id, face);
|
|
660
|
+
const he01 = new Halfedge(createHalfedgeId(mesh.nextHalfedgeId++), v1, edge01);
|
|
661
|
+
const he12 = new Halfedge(createHalfedgeId(mesh.nextHalfedgeId++), v2, edge12);
|
|
662
|
+
const he20 = new Halfedge(createHalfedgeId(mesh.nextHalfedgeId++), v0, edge20);
|
|
663
|
+
mesh.halfedges.set(he01.id, he01);
|
|
664
|
+
mesh.halfedges.set(he12.id, he12);
|
|
665
|
+
mesh.halfedges.set(he20.id, he20);
|
|
666
|
+
edge01.addHalfedge(he01);
|
|
667
|
+
edge12.addHalfedge(he12);
|
|
668
|
+
edge20.addHalfedge(he20);
|
|
669
|
+
he01.next = he12;
|
|
670
|
+
he12.next = he20;
|
|
671
|
+
he20.next = he01;
|
|
672
|
+
he01.prev = he20;
|
|
673
|
+
he12.prev = he01;
|
|
674
|
+
he20.prev = he12;
|
|
675
|
+
he01.face = face;
|
|
676
|
+
he12.face = face;
|
|
677
|
+
he20.face = face;
|
|
678
|
+
face.halfedge = he01;
|
|
679
|
+
if (!v0.halfedge) v0.halfedge = he01;
|
|
680
|
+
if (!v1.halfedge) v1.halfedge = he12;
|
|
681
|
+
if (!v2.halfedge) v2.halfedge = he20;
|
|
682
|
+
edge01.halfedge = he01;
|
|
683
|
+
edge12.halfedge = he12;
|
|
684
|
+
edge20.halfedge = he20;
|
|
685
|
+
}
|
|
686
|
+
mesh.setupTwinHalfedges();
|
|
687
|
+
if (featureEdges && featureEdges.length > 0) {
|
|
688
|
+
mesh.markFeatureEdges(featureEdges);
|
|
689
|
+
}
|
|
690
|
+
mesh.classifyVertices();
|
|
691
|
+
return mesh;
|
|
692
|
+
}
|
|
693
|
+
/**
|
|
694
|
+
* Gets or creates an edge between two vertices.
|
|
695
|
+
*/
|
|
696
|
+
getOrCreateEdge(v0Id, v1Id) {
|
|
697
|
+
const key = this.makeEdgeKey(v0Id, v1Id);
|
|
698
|
+
let edge = this.edgeMap.get(key);
|
|
699
|
+
if (!edge) {
|
|
700
|
+
const v0 = this.vertices.get(createVertexId(v0Id));
|
|
701
|
+
const v1 = this.vertices.get(createVertexId(v1Id));
|
|
702
|
+
if (!v0 || !v1) {
|
|
703
|
+
throw new Error(`Vertex not found: ${v0Id} or ${v1Id}`);
|
|
704
|
+
}
|
|
705
|
+
const dx = v1.position.x - v0.position.x;
|
|
706
|
+
const dy = v1.position.y - v0.position.y;
|
|
707
|
+
const dz = v1.position.z - v0.position.z;
|
|
708
|
+
const length2 = Math.sqrt(dx * dx + dy * dy + dz * dz);
|
|
709
|
+
edge = new Edge(createEdgeId(this.nextEdgeId++), null, length2);
|
|
710
|
+
edge.allHalfedges = [];
|
|
711
|
+
this.edgeMap.set(key, edge);
|
|
712
|
+
this.edges.set(edge.id, edge);
|
|
713
|
+
}
|
|
714
|
+
return edge;
|
|
715
|
+
}
|
|
716
|
+
/**
|
|
717
|
+
* Creates a canonical key for an edge between two vertices.
|
|
718
|
+
*/
|
|
719
|
+
makeEdgeKey(v0Id, v1Id) {
|
|
720
|
+
return v0Id < v1Id ? `${v0Id},${v1Id}` : `${v1Id},${v0Id}`;
|
|
721
|
+
}
|
|
722
|
+
/**
|
|
723
|
+
* Sets up twin halfedge relationships.
|
|
724
|
+
* For non-manifold edges (>2 halfedges), creates a circular twin chain.
|
|
725
|
+
*/
|
|
726
|
+
setupTwinHalfedges() {
|
|
727
|
+
var _a;
|
|
728
|
+
for (const edge of this.edges.values()) {
|
|
729
|
+
const halfedges = edge.allHalfedges;
|
|
730
|
+
const count = halfedges.length;
|
|
731
|
+
if (count === 0) {
|
|
732
|
+
continue;
|
|
733
|
+
}
|
|
734
|
+
if (count === 1) {
|
|
735
|
+
halfedges[0].twin = null;
|
|
736
|
+
edge.type = EdgeType.Boundary;
|
|
737
|
+
} else if (count === 2) {
|
|
738
|
+
halfedges[0].twin = halfedges[1];
|
|
739
|
+
halfedges[1].twin = halfedges[0];
|
|
740
|
+
edge.type = EdgeType.Manifold;
|
|
741
|
+
} else {
|
|
742
|
+
this.setupNonManifoldTwins(edge, halfedges);
|
|
743
|
+
edge.type = EdgeType.NonManifold;
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
for (const he of this.halfedges.values()) {
|
|
747
|
+
if (he.twin !== null) {
|
|
748
|
+
const vertex = (_a = he.prev) == null ? void 0 : _a.vertex;
|
|
749
|
+
if (vertex) {
|
|
750
|
+
vertex.halfedge = he;
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
/**
|
|
756
|
+
* Sets up twin relationships for non-manifold edges.
|
|
757
|
+
* Groups halfedges by direction and pairs them appropriately.
|
|
758
|
+
*/
|
|
759
|
+
setupNonManifoldTwins(edge, halfedges) {
|
|
760
|
+
const [v0, v1] = edge.getVertices();
|
|
761
|
+
if (!v0 || !v1) return;
|
|
762
|
+
const toV0 = [];
|
|
763
|
+
const toV1 = [];
|
|
764
|
+
for (const he of halfedges) {
|
|
765
|
+
if (he.vertex.id === v0.id) {
|
|
766
|
+
toV0.push(he);
|
|
767
|
+
} else {
|
|
768
|
+
toV1.push(he);
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
const minCount = Math.min(toV0.length, toV1.length);
|
|
772
|
+
for (let i = 0; i < minCount; i++) {
|
|
773
|
+
toV0[i].twin = toV1[i];
|
|
774
|
+
toV1[i].twin = toV0[i];
|
|
775
|
+
}
|
|
776
|
+
for (let i = minCount; i < toV0.length; i++) {
|
|
777
|
+
toV0[i].twin = null;
|
|
778
|
+
}
|
|
779
|
+
for (let i = minCount; i < toV1.length; i++) {
|
|
780
|
+
toV1[i].twin = null;
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
/**
|
|
784
|
+
* Marks user-defined feature edges.
|
|
785
|
+
*/
|
|
786
|
+
markFeatureEdges(featureEdges) {
|
|
787
|
+
for (const [v0, v1] of featureEdges) {
|
|
788
|
+
const key = this.makeEdgeKey(v0, v1);
|
|
789
|
+
const edge = this.edgeMap.get(key);
|
|
790
|
+
if (edge && edge.type === EdgeType.Manifold) {
|
|
791
|
+
edge.markAsFeature();
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
/**
|
|
796
|
+
* Classifies all vertices based on their neighborhood topology.
|
|
797
|
+
*/
|
|
798
|
+
classifyVertices() {
|
|
799
|
+
for (const vertex of this.vertices.values()) {
|
|
800
|
+
vertex.type = this.classifyVertex(vertex);
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
/**
|
|
804
|
+
* Classifies a single vertex based on its neighborhood.
|
|
805
|
+
*/
|
|
806
|
+
classifyVertex(vertex) {
|
|
807
|
+
let skeletonEdgeCount = 0;
|
|
808
|
+
vertex.forEachOutgoingHalfedge((he) => {
|
|
809
|
+
if (he.edge.isSkeletonEdge()) {
|
|
810
|
+
skeletonEdgeCount++;
|
|
811
|
+
}
|
|
812
|
+
});
|
|
813
|
+
if (skeletonEdgeCount === 0) {
|
|
814
|
+
return VertexType.Manifold;
|
|
815
|
+
} else if (skeletonEdgeCount === 2) {
|
|
816
|
+
return VertexType.OpenBook;
|
|
817
|
+
} else {
|
|
818
|
+
return VertexType.SkeletonBranching;
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
/**
|
|
822
|
+
* Exports the mesh to a Three.js BufferGeometry.
|
|
823
|
+
*/
|
|
824
|
+
toBufferGeometry() {
|
|
825
|
+
const geometry = new Vector3.constructor();
|
|
826
|
+
return geometry;
|
|
827
|
+
}
|
|
828
|
+
/**
|
|
829
|
+
* Gets all vertices in the mesh.
|
|
830
|
+
*/
|
|
831
|
+
getVertices() {
|
|
832
|
+
return Array.from(this.vertices.values());
|
|
833
|
+
}
|
|
834
|
+
/**
|
|
835
|
+
* Gets all edges in the mesh.
|
|
836
|
+
*/
|
|
837
|
+
getEdges() {
|
|
838
|
+
return Array.from(this.edges.values());
|
|
839
|
+
}
|
|
840
|
+
/**
|
|
841
|
+
* Gets all faces in the mesh.
|
|
842
|
+
*/
|
|
843
|
+
getFaces() {
|
|
844
|
+
return Array.from(this.faces.values());
|
|
845
|
+
}
|
|
846
|
+
/**
|
|
847
|
+
* Gets all halfedges in the mesh.
|
|
848
|
+
*/
|
|
849
|
+
getHalfedges() {
|
|
850
|
+
return Array.from(this.halfedges.values());
|
|
851
|
+
}
|
|
852
|
+
/**
|
|
853
|
+
* Gets the vertex count.
|
|
854
|
+
*/
|
|
855
|
+
get vertexCount() {
|
|
856
|
+
return this.vertices.size;
|
|
857
|
+
}
|
|
858
|
+
/**
|
|
859
|
+
* Gets the edge count.
|
|
860
|
+
*/
|
|
861
|
+
get edgeCount() {
|
|
862
|
+
return this.edges.size;
|
|
863
|
+
}
|
|
864
|
+
/**
|
|
865
|
+
* Gets the face count.
|
|
866
|
+
*/
|
|
867
|
+
get faceCount() {
|
|
868
|
+
return this.faces.size;
|
|
869
|
+
}
|
|
870
|
+
/**
|
|
871
|
+
* Gets the halfedge count.
|
|
872
|
+
*/
|
|
873
|
+
get halfedgeCount() {
|
|
874
|
+
return this.halfedges.size;
|
|
875
|
+
}
|
|
876
|
+
/**
|
|
877
|
+
* Gets all non-manifold edges.
|
|
878
|
+
*/
|
|
879
|
+
getNonManifoldEdges() {
|
|
880
|
+
return this.getEdges().filter((e) => e.type === EdgeType.NonManifold);
|
|
881
|
+
}
|
|
882
|
+
/**
|
|
883
|
+
* Gets all boundary edges.
|
|
884
|
+
*/
|
|
885
|
+
getBoundaryEdges() {
|
|
886
|
+
return this.getEdges().filter((e) => e.type === EdgeType.Boundary);
|
|
887
|
+
}
|
|
888
|
+
/**
|
|
889
|
+
* Gets all feature edges.
|
|
890
|
+
*/
|
|
891
|
+
getFeatureEdges() {
|
|
892
|
+
return this.getEdges().filter((e) => e.type === EdgeType.Feature);
|
|
893
|
+
}
|
|
894
|
+
/**
|
|
895
|
+
* Gets all skeleton edges (non-manifold + boundary + feature).
|
|
896
|
+
*/
|
|
897
|
+
getSkeletonEdges() {
|
|
898
|
+
return this.getEdges().filter((e) => e.isSkeletonEdge());
|
|
899
|
+
}
|
|
900
|
+
/**
|
|
901
|
+
* Checks if the mesh is manifold (no non-manifold edges).
|
|
902
|
+
*/
|
|
903
|
+
isManifold() {
|
|
904
|
+
return this.getNonManifoldEdges().length === 0;
|
|
905
|
+
}
|
|
906
|
+
/**
|
|
907
|
+
* Checks if the mesh has boundaries.
|
|
908
|
+
*/
|
|
909
|
+
hasBoundary() {
|
|
910
|
+
return this.getBoundaryEdges().length > 0;
|
|
911
|
+
}
|
|
912
|
+
/**
|
|
913
|
+
* Gets a vertex by ID.
|
|
914
|
+
*/
|
|
915
|
+
getVertex(id) {
|
|
916
|
+
return this.vertices.get(id);
|
|
917
|
+
}
|
|
918
|
+
/**
|
|
919
|
+
* Gets an edge by ID.
|
|
920
|
+
*/
|
|
921
|
+
getEdge(id) {
|
|
922
|
+
return this.edges.get(id);
|
|
923
|
+
}
|
|
924
|
+
/**
|
|
925
|
+
* Gets a face by ID.
|
|
926
|
+
*/
|
|
927
|
+
getFace(id) {
|
|
928
|
+
return this.faces.get(id);
|
|
929
|
+
}
|
|
930
|
+
/**
|
|
931
|
+
* Gets a halfedge by ID.
|
|
932
|
+
*/
|
|
933
|
+
getHalfedge(id) {
|
|
934
|
+
return this.halfedges.get(id);
|
|
935
|
+
}
|
|
936
|
+
/**
|
|
937
|
+
* Gets the edge between two vertices (if it exists).
|
|
938
|
+
*/
|
|
939
|
+
getEdgeBetween(v0, v1) {
|
|
940
|
+
const key = this.makeEdgeKey(v0.id, v1.id);
|
|
941
|
+
return this.edgeMap.get(key);
|
|
942
|
+
}
|
|
943
|
+
/**
|
|
944
|
+
* Creates a new vertex and adds it to the mesh.
|
|
945
|
+
*/
|
|
946
|
+
createVertex(position) {
|
|
947
|
+
const vertex = new Vertex(createVertexId(this.nextVertexId++), position);
|
|
948
|
+
this.vertices.set(vertex.id, vertex);
|
|
949
|
+
return vertex;
|
|
950
|
+
}
|
|
951
|
+
/**
|
|
952
|
+
* Creates a new face from three vertices.
|
|
953
|
+
* Also creates the necessary halfedges and edges.
|
|
954
|
+
*/
|
|
955
|
+
createFace(v0, v1, v2) {
|
|
956
|
+
const edge01 = this.getOrCreateEdge(v0.id, v1.id);
|
|
957
|
+
const edge12 = this.getOrCreateEdge(v1.id, v2.id);
|
|
958
|
+
const edge20 = this.getOrCreateEdge(v2.id, v0.id);
|
|
959
|
+
const face = new Face(createFaceId(this.nextFaceId++), null);
|
|
960
|
+
this.faces.set(face.id, face);
|
|
961
|
+
const he01 = new Halfedge(createHalfedgeId(this.nextHalfedgeId++), v1, edge01);
|
|
962
|
+
const he12 = new Halfedge(createHalfedgeId(this.nextHalfedgeId++), v2, edge12);
|
|
963
|
+
const he20 = new Halfedge(createHalfedgeId(this.nextHalfedgeId++), v0, edge20);
|
|
964
|
+
this.halfedges.set(he01.id, he01);
|
|
965
|
+
this.halfedges.set(he12.id, he12);
|
|
966
|
+
this.halfedges.set(he20.id, he20);
|
|
967
|
+
edge01.addHalfedge(he01);
|
|
968
|
+
edge12.addHalfedge(he12);
|
|
969
|
+
edge20.addHalfedge(he20);
|
|
970
|
+
he01.next = he12;
|
|
971
|
+
he12.next = he20;
|
|
972
|
+
he20.next = he01;
|
|
973
|
+
he01.prev = he20;
|
|
974
|
+
he12.prev = he01;
|
|
975
|
+
he20.prev = he12;
|
|
976
|
+
he01.face = face;
|
|
977
|
+
he12.face = face;
|
|
978
|
+
he20.face = face;
|
|
979
|
+
face.halfedge = he01;
|
|
980
|
+
if (!v0.halfedge) v0.halfedge = he01;
|
|
981
|
+
if (!v1.halfedge) v1.halfedge = he12;
|
|
982
|
+
if (!v2.halfedge) v2.halfedge = he20;
|
|
983
|
+
edge01.halfedge = he01;
|
|
984
|
+
edge12.halfedge = he12;
|
|
985
|
+
edge20.halfedge = he20;
|
|
986
|
+
return face;
|
|
987
|
+
}
|
|
988
|
+
/**
|
|
989
|
+
* Computes mesh statistics.
|
|
990
|
+
*/
|
|
991
|
+
getStats() {
|
|
992
|
+
const nonManifoldEdgeCount = this.getNonManifoldEdges().length;
|
|
993
|
+
const boundaryEdgeCount = this.getBoundaryEdges().length;
|
|
994
|
+
const featureEdgeCount = this.getFeatureEdges().length;
|
|
995
|
+
const eulerCharacteristic = this.vertexCount - this.edgeCount + this.faceCount;
|
|
996
|
+
return {
|
|
997
|
+
vertexCount: this.vertexCount,
|
|
998
|
+
edgeCount: this.edgeCount,
|
|
999
|
+
faceCount: this.faceCount,
|
|
1000
|
+
nonManifoldEdgeCount,
|
|
1001
|
+
boundaryEdgeCount,
|
|
1002
|
+
featureEdgeCount,
|
|
1003
|
+
eulerCharacteristic
|
|
1004
|
+
};
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
function distanceSquared(a, b) {
|
|
1008
|
+
const dx = b.x - a.x;
|
|
1009
|
+
const dy = b.y - a.y;
|
|
1010
|
+
const dz = b.z - a.z;
|
|
1011
|
+
return dx * dx + dy * dy + dz * dz;
|
|
1012
|
+
}
|
|
1013
|
+
function distance(a, b) {
|
|
1014
|
+
return Math.sqrt(distanceSquared(a, b));
|
|
1015
|
+
}
|
|
1016
|
+
function dot(a, b) {
|
|
1017
|
+
return a.x * b.x + a.y * b.y + a.z * b.z;
|
|
1018
|
+
}
|
|
1019
|
+
function cross(a, b) {
|
|
1020
|
+
return {
|
|
1021
|
+
x: a.y * b.z - a.z * b.y,
|
|
1022
|
+
y: a.z * b.x - a.x * b.z,
|
|
1023
|
+
z: a.x * b.y - a.y * b.x
|
|
1024
|
+
};
|
|
1025
|
+
}
|
|
1026
|
+
function length(v) {
|
|
1027
|
+
return Math.sqrt(v.x * v.x + v.y * v.y + v.z * v.z);
|
|
1028
|
+
}
|
|
1029
|
+
function lengthSquared(v) {
|
|
1030
|
+
return v.x * v.x + v.y * v.y + v.z * v.z;
|
|
1031
|
+
}
|
|
1032
|
+
function normalize(v) {
|
|
1033
|
+
const len = length(v);
|
|
1034
|
+
if (len < 1e-10) {
|
|
1035
|
+
return { x: 0, y: 0, z: 0 };
|
|
1036
|
+
}
|
|
1037
|
+
return {
|
|
1038
|
+
x: v.x / len,
|
|
1039
|
+
y: v.y / len,
|
|
1040
|
+
z: v.z / len
|
|
1041
|
+
};
|
|
1042
|
+
}
|
|
1043
|
+
function add(a, b) {
|
|
1044
|
+
return {
|
|
1045
|
+
x: a.x + b.x,
|
|
1046
|
+
y: a.y + b.y,
|
|
1047
|
+
z: a.z + b.z
|
|
1048
|
+
};
|
|
1049
|
+
}
|
|
1050
|
+
function subtract(a, b) {
|
|
1051
|
+
return {
|
|
1052
|
+
x: a.x - b.x,
|
|
1053
|
+
y: a.y - b.y,
|
|
1054
|
+
z: a.z - b.z
|
|
1055
|
+
};
|
|
1056
|
+
}
|
|
1057
|
+
function scale(v, s) {
|
|
1058
|
+
return {
|
|
1059
|
+
x: v.x * s,
|
|
1060
|
+
y: v.y * s,
|
|
1061
|
+
z: v.z * s
|
|
1062
|
+
};
|
|
1063
|
+
}
|
|
1064
|
+
function lerp(a, b, t) {
|
|
1065
|
+
return {
|
|
1066
|
+
x: a.x + (b.x - a.x) * t,
|
|
1067
|
+
y: a.y + (b.y - a.y) * t,
|
|
1068
|
+
z: a.z + (b.z - a.z) * t
|
|
1069
|
+
};
|
|
1070
|
+
}
|
|
1071
|
+
function midpoint(a, b) {
|
|
1072
|
+
return {
|
|
1073
|
+
x: (a.x + b.x) / 2,
|
|
1074
|
+
y: (a.y + b.y) / 2,
|
|
1075
|
+
z: (a.z + b.z) / 2
|
|
1076
|
+
};
|
|
1077
|
+
}
|
|
1078
|
+
function angleBetween(a, b) {
|
|
1079
|
+
const lenA = length(a);
|
|
1080
|
+
const lenB = length(b);
|
|
1081
|
+
if (lenA < 1e-10 || lenB < 1e-10) {
|
|
1082
|
+
return 0;
|
|
1083
|
+
}
|
|
1084
|
+
const cosAngle = dot(a, b) / (lenA * lenB);
|
|
1085
|
+
return Math.acos(Math.max(-1, Math.min(1, cosAngle)));
|
|
1086
|
+
}
|
|
1087
|
+
function projectPointOnLine(point, lineStart, lineEnd) {
|
|
1088
|
+
const lineDir = subtract(lineEnd, lineStart);
|
|
1089
|
+
const lineLengthSq = lengthSquared(lineDir);
|
|
1090
|
+
if (lineLengthSq < 1e-10) {
|
|
1091
|
+
return { ...lineStart };
|
|
1092
|
+
}
|
|
1093
|
+
const t = dot(subtract(point, lineStart), lineDir) / lineLengthSq;
|
|
1094
|
+
return add(lineStart, scale(lineDir, t));
|
|
1095
|
+
}
|
|
1096
|
+
function projectPointOnSegment(point, segStart, segEnd) {
|
|
1097
|
+
const lineDir = subtract(segEnd, segStart);
|
|
1098
|
+
const lineLengthSq = lengthSquared(lineDir);
|
|
1099
|
+
if (lineLengthSq < 1e-10) {
|
|
1100
|
+
return { ...segStart };
|
|
1101
|
+
}
|
|
1102
|
+
const t = Math.max(0, Math.min(1, dot(subtract(point, segStart), lineDir) / lineLengthSq));
|
|
1103
|
+
return add(segStart, scale(lineDir, t));
|
|
1104
|
+
}
|
|
1105
|
+
function triangleArea(v0, v1, v2) {
|
|
1106
|
+
const e1 = subtract(v1, v0);
|
|
1107
|
+
const e2 = subtract(v2, v0);
|
|
1108
|
+
const crossProduct = cross(e1, e2);
|
|
1109
|
+
return length(crossProduct) / 2;
|
|
1110
|
+
}
|
|
1111
|
+
function triangleNormal(v0, v1, v2) {
|
|
1112
|
+
const e1 = subtract(v1, v0);
|
|
1113
|
+
const e2 = subtract(v2, v0);
|
|
1114
|
+
return normalize(cross(e1, e2));
|
|
1115
|
+
}
|
|
1116
|
+
function triangleCentroid$1(v0, v1, v2) {
|
|
1117
|
+
return {
|
|
1118
|
+
x: (v0.x + v1.x + v2.x) / 3,
|
|
1119
|
+
y: (v0.y + v1.y + v2.y) / 3,
|
|
1120
|
+
z: (v0.z + v1.z + v2.z) / 3
|
|
1121
|
+
};
|
|
1122
|
+
}
|
|
1123
|
+
function triangleCircumcenter(v0, v1, v2) {
|
|
1124
|
+
const a = subtract(v1, v0);
|
|
1125
|
+
const b = subtract(v2, v0);
|
|
1126
|
+
const crossAB = cross(a, b);
|
|
1127
|
+
const denom = 2 * lengthSquared(crossAB);
|
|
1128
|
+
if (denom < 1e-10) {
|
|
1129
|
+
return null;
|
|
1130
|
+
}
|
|
1131
|
+
const aLenSq = lengthSquared(a);
|
|
1132
|
+
const bLenSq = lengthSquared(b);
|
|
1133
|
+
const term1 = scale(cross(crossAB, a), bLenSq);
|
|
1134
|
+
const term2 = scale(cross(b, crossAB), aLenSq);
|
|
1135
|
+
const circumcenterOffset = scale(add(term1, term2), 1 / denom);
|
|
1136
|
+
return add(v0, circumcenterOffset);
|
|
1137
|
+
}
|
|
1138
|
+
function triangleCircumradius(v0, v1, v2) {
|
|
1139
|
+
const a = distance(v1, v2);
|
|
1140
|
+
const b = distance(v0, v2);
|
|
1141
|
+
const c = distance(v0, v1);
|
|
1142
|
+
const area = triangleArea(v0, v1, v2);
|
|
1143
|
+
if (area < 1e-10) {
|
|
1144
|
+
return null;
|
|
1145
|
+
}
|
|
1146
|
+
return a * b * c / (4 * area);
|
|
1147
|
+
}
|
|
1148
|
+
function triangleInradius(v0, v1, v2) {
|
|
1149
|
+
const a = distance(v1, v2);
|
|
1150
|
+
const b = distance(v0, v2);
|
|
1151
|
+
const c = distance(v0, v1);
|
|
1152
|
+
const s = (a + b + c) / 2;
|
|
1153
|
+
const area = triangleArea(v0, v1, v2);
|
|
1154
|
+
if (s < 1e-10) {
|
|
1155
|
+
return null;
|
|
1156
|
+
}
|
|
1157
|
+
return area / s;
|
|
1158
|
+
}
|
|
1159
|
+
function triangleQuality(v0, v1, v2) {
|
|
1160
|
+
const inr = triangleInradius(v0, v1, v2);
|
|
1161
|
+
const circumr = triangleCircumradius(v0, v1, v2);
|
|
1162
|
+
if (inr === null || circumr === null || circumr < 1e-10) {
|
|
1163
|
+
return 0;
|
|
1164
|
+
}
|
|
1165
|
+
return Math.max(0, Math.min(1, 2 * inr / circumr));
|
|
1166
|
+
}
|
|
1167
|
+
function isPointInTriangle(point, v0, v1, v2) {
|
|
1168
|
+
const e0 = subtract(v1, v0);
|
|
1169
|
+
const e1 = subtract(v2, v0);
|
|
1170
|
+
const e2 = subtract(point, v0);
|
|
1171
|
+
const d00 = dot(e0, e0);
|
|
1172
|
+
const d01 = dot(e0, e1);
|
|
1173
|
+
const d11 = dot(e1, e1);
|
|
1174
|
+
const d20 = dot(e2, e0);
|
|
1175
|
+
const d21 = dot(e2, e1);
|
|
1176
|
+
const denom = d00 * d11 - d01 * d01;
|
|
1177
|
+
if (Math.abs(denom) < 1e-10) {
|
|
1178
|
+
return false;
|
|
1179
|
+
}
|
|
1180
|
+
const v = (d11 * d20 - d01 * d21) / denom;
|
|
1181
|
+
const w = (d00 * d21 - d01 * d20) / denom;
|
|
1182
|
+
const u = 1 - v - w;
|
|
1183
|
+
return u >= 0 && v >= 0 && w >= 0;
|
|
1184
|
+
}
|
|
1185
|
+
function barycentricCoordinates(point, v0, v1, v2) {
|
|
1186
|
+
const e0 = subtract(v1, v0);
|
|
1187
|
+
const e1 = subtract(v2, v0);
|
|
1188
|
+
const e2 = subtract(point, v0);
|
|
1189
|
+
const d00 = dot(e0, e0);
|
|
1190
|
+
const d01 = dot(e0, e1);
|
|
1191
|
+
const d11 = dot(e1, e1);
|
|
1192
|
+
const d20 = dot(e2, e0);
|
|
1193
|
+
const d21 = dot(e2, e1);
|
|
1194
|
+
const denom = d00 * d11 - d01 * d01;
|
|
1195
|
+
if (Math.abs(denom) < 1e-10) {
|
|
1196
|
+
return null;
|
|
1197
|
+
}
|
|
1198
|
+
const v = (d11 * d20 - d01 * d21) / denom;
|
|
1199
|
+
const w = (d00 * d21 - d01 * d20) / denom;
|
|
1200
|
+
const u = 1 - v - w;
|
|
1201
|
+
return { u, v, w };
|
|
1202
|
+
}
|
|
1203
|
+
function isQuadConvex(v0, v1, v2, v3) {
|
|
1204
|
+
const n1 = triangleNormal(v0, v1, v2);
|
|
1205
|
+
const n2 = triangleNormal(v0, v3, v1);
|
|
1206
|
+
const normal = normalize(add(n1, n2));
|
|
1207
|
+
let tangent;
|
|
1208
|
+
if (Math.abs(normal.x) < 0.9) {
|
|
1209
|
+
tangent = normalize(cross(normal, { x: 1, y: 0, z: 0 }));
|
|
1210
|
+
} else {
|
|
1211
|
+
tangent = normalize(cross(normal, { x: 0, y: 1, z: 0 }));
|
|
1212
|
+
}
|
|
1213
|
+
const bitangent = cross(normal, tangent);
|
|
1214
|
+
const project = (v) => ({
|
|
1215
|
+
x: dot(v, tangent),
|
|
1216
|
+
y: dot(v, bitangent)
|
|
1217
|
+
});
|
|
1218
|
+
const p0 = project(v0);
|
|
1219
|
+
const p1 = project(v1);
|
|
1220
|
+
const p2 = project(v2);
|
|
1221
|
+
const p3 = project(v3);
|
|
1222
|
+
const cross2D = (a, b) => a.x * b.y - a.y * b.x;
|
|
1223
|
+
const d01 = { x: p1.x - p0.x, y: p1.y - p0.y };
|
|
1224
|
+
const d02 = { x: p2.x - p0.x, y: p2.y - p0.y };
|
|
1225
|
+
const d03 = { x: p3.x - p0.x, y: p3.y - p0.y };
|
|
1226
|
+
const sign2 = cross2D(d01, d02);
|
|
1227
|
+
const sign3 = cross2D(d01, d03);
|
|
1228
|
+
if (sign2 * sign3 >= 0) {
|
|
1229
|
+
return false;
|
|
1230
|
+
}
|
|
1231
|
+
const d23 = { x: p3.x - p2.x, y: p3.y - p2.y };
|
|
1232
|
+
const d20 = { x: p0.x - p2.x, y: p0.y - p2.y };
|
|
1233
|
+
const d21 = { x: p1.x - p2.x, y: p1.y - p2.y };
|
|
1234
|
+
const sign0 = cross2D(d23, d20);
|
|
1235
|
+
const sign1 = cross2D(d23, d21);
|
|
1236
|
+
return sign0 * sign1 < 0;
|
|
1237
|
+
}
|
|
1238
|
+
function fromVector3(v) {
|
|
1239
|
+
return { x: v.x, y: v.y, z: v.z };
|
|
1240
|
+
}
|
|
1241
|
+
function cotangent(v0, vertex, v1) {
|
|
1242
|
+
const e0 = subtract(v0, vertex);
|
|
1243
|
+
const e1 = subtract(v1, vertex);
|
|
1244
|
+
const cosAngle = dot(e0, e1);
|
|
1245
|
+
const sinAngle = length(cross(e0, e1));
|
|
1246
|
+
if (Math.abs(sinAngle) < 1e-10) {
|
|
1247
|
+
return 0;
|
|
1248
|
+
}
|
|
1249
|
+
return cosAngle / sinAngle;
|
|
1250
|
+
}
|
|
1251
|
+
function angleAtVertex(v0, vertex, v1) {
|
|
1252
|
+
const e0 = subtract(v0, vertex);
|
|
1253
|
+
const e1 = subtract(v1, vertex);
|
|
1254
|
+
return angleBetween(e0, e1);
|
|
1255
|
+
}
|
|
1256
|
+
class SkeletonSegment {
|
|
1257
|
+
constructor(id) {
|
|
1258
|
+
this.id = id;
|
|
1259
|
+
this.vertices = [];
|
|
1260
|
+
this.edges = [];
|
|
1261
|
+
this.isClosed = false;
|
|
1262
|
+
this._totalLength = 0;
|
|
1263
|
+
this._cumulativeLengths = [];
|
|
1264
|
+
}
|
|
1265
|
+
/**
|
|
1266
|
+
* Gets the start vertex of this segment.
|
|
1267
|
+
*/
|
|
1268
|
+
get startVertex() {
|
|
1269
|
+
return this.vertices[0];
|
|
1270
|
+
}
|
|
1271
|
+
/**
|
|
1272
|
+
* Gets the end vertex of this segment.
|
|
1273
|
+
*/
|
|
1274
|
+
get endVertex() {
|
|
1275
|
+
return this.vertices[this.vertices.length - 1];
|
|
1276
|
+
}
|
|
1277
|
+
/**
|
|
1278
|
+
* Gets the total arc length of this segment.
|
|
1279
|
+
*/
|
|
1280
|
+
get totalLength() {
|
|
1281
|
+
return this._totalLength;
|
|
1282
|
+
}
|
|
1283
|
+
/**
|
|
1284
|
+
* Gets the number of vertices in this segment.
|
|
1285
|
+
*/
|
|
1286
|
+
get vertexCount() {
|
|
1287
|
+
return this.vertices.length;
|
|
1288
|
+
}
|
|
1289
|
+
/**
|
|
1290
|
+
* Gets the number of edges in this segment.
|
|
1291
|
+
*/
|
|
1292
|
+
get edgeCount() {
|
|
1293
|
+
return this.edges.length;
|
|
1294
|
+
}
|
|
1295
|
+
/**
|
|
1296
|
+
* Adds a vertex to the end of the segment.
|
|
1297
|
+
*/
|
|
1298
|
+
addVertex(vertex) {
|
|
1299
|
+
if (this.vertices.length > 0) {
|
|
1300
|
+
const lastVertex = this.vertices[this.vertices.length - 1];
|
|
1301
|
+
const edgeLength = distance(
|
|
1302
|
+
{ x: lastVertex.position.x, y: lastVertex.position.y, z: lastVertex.position.z },
|
|
1303
|
+
{ x: vertex.position.x, y: vertex.position.y, z: vertex.position.z }
|
|
1304
|
+
);
|
|
1305
|
+
this._totalLength += edgeLength;
|
|
1306
|
+
}
|
|
1307
|
+
this._cumulativeLengths.push(this._totalLength);
|
|
1308
|
+
this.vertices.push(vertex);
|
|
1309
|
+
}
|
|
1310
|
+
/**
|
|
1311
|
+
* Adds an edge to the segment.
|
|
1312
|
+
*/
|
|
1313
|
+
addEdge(edge) {
|
|
1314
|
+
this.edges.push(edge);
|
|
1315
|
+
}
|
|
1316
|
+
/**
|
|
1317
|
+
* Recomputes the total length and cumulative lengths.
|
|
1318
|
+
*/
|
|
1319
|
+
recomputeLengths() {
|
|
1320
|
+
this._totalLength = 0;
|
|
1321
|
+
this._cumulativeLengths = [0];
|
|
1322
|
+
for (let i = 1; i < this.vertices.length; i++) {
|
|
1323
|
+
const v0 = this.vertices[i - 1];
|
|
1324
|
+
const v1 = this.vertices[i];
|
|
1325
|
+
const len = distance(
|
|
1326
|
+
{ x: v0.position.x, y: v0.position.y, z: v0.position.z },
|
|
1327
|
+
{ x: v1.position.x, y: v1.position.y, z: v1.position.z }
|
|
1328
|
+
);
|
|
1329
|
+
this._totalLength += len;
|
|
1330
|
+
this._cumulativeLengths.push(this._totalLength);
|
|
1331
|
+
}
|
|
1332
|
+
}
|
|
1333
|
+
/**
|
|
1334
|
+
* Gets the parameter t (0 to 1) for a vertex index.
|
|
1335
|
+
*/
|
|
1336
|
+
getParameterAtVertex(index) {
|
|
1337
|
+
if (this._totalLength === 0 || index < 0 || index >= this.vertices.length) {
|
|
1338
|
+
return 0;
|
|
1339
|
+
}
|
|
1340
|
+
return (this._cumulativeLengths[index] ?? 0) / this._totalLength;
|
|
1341
|
+
}
|
|
1342
|
+
/**
|
|
1343
|
+
* Gets the position at a parameter t along the segment.
|
|
1344
|
+
*
|
|
1345
|
+
* @param t - Parameter from 0 (start) to 1 (end)
|
|
1346
|
+
* @returns The interpolated position
|
|
1347
|
+
*/
|
|
1348
|
+
getPositionAt(t) {
|
|
1349
|
+
if (this.vertices.length === 0) {
|
|
1350
|
+
return null;
|
|
1351
|
+
}
|
|
1352
|
+
if (this.vertices.length === 1) {
|
|
1353
|
+
const v = this.vertices[0];
|
|
1354
|
+
return { x: v.position.x, y: v.position.y, z: v.position.z };
|
|
1355
|
+
}
|
|
1356
|
+
t = Math.max(0, Math.min(1, t));
|
|
1357
|
+
const targetLength = t * this._totalLength;
|
|
1358
|
+
for (let i = 1; i < this.vertices.length; i++) {
|
|
1359
|
+
const prevLen = this._cumulativeLengths[i - 1] ?? 0;
|
|
1360
|
+
const currLen = this._cumulativeLengths[i] ?? 0;
|
|
1361
|
+
if (targetLength <= currLen) {
|
|
1362
|
+
const v0 = this.vertices[i - 1];
|
|
1363
|
+
const v1 = this.vertices[i];
|
|
1364
|
+
const edgeLength = currLen - prevLen;
|
|
1365
|
+
if (edgeLength < 1e-10) {
|
|
1366
|
+
return { x: v0.position.x, y: v0.position.y, z: v0.position.z };
|
|
1367
|
+
}
|
|
1368
|
+
const localT = (targetLength - prevLen) / edgeLength;
|
|
1369
|
+
return lerp(
|
|
1370
|
+
{ x: v0.position.x, y: v0.position.y, z: v0.position.z },
|
|
1371
|
+
{ x: v1.position.x, y: v1.position.y, z: v1.position.z },
|
|
1372
|
+
localT
|
|
1373
|
+
);
|
|
1374
|
+
}
|
|
1375
|
+
}
|
|
1376
|
+
const lastV = this.vertices[this.vertices.length - 1];
|
|
1377
|
+
return { x: lastV.position.x, y: lastV.position.y, z: lastV.position.z };
|
|
1378
|
+
}
|
|
1379
|
+
/**
|
|
1380
|
+
* Projects a point onto this segment.
|
|
1381
|
+
* Returns the closest point on the segment and its parameter.
|
|
1382
|
+
*/
|
|
1383
|
+
projectPoint(point) {
|
|
1384
|
+
if (this.vertices.length === 0) {
|
|
1385
|
+
return null;
|
|
1386
|
+
}
|
|
1387
|
+
if (this.vertices.length === 1) {
|
|
1388
|
+
const v = this.vertices[0];
|
|
1389
|
+
const pos = { x: v.position.x, y: v.position.y, z: v.position.z };
|
|
1390
|
+
return {
|
|
1391
|
+
point: pos,
|
|
1392
|
+
parameter: 0,
|
|
1393
|
+
distance: distance(point, pos)
|
|
1394
|
+
};
|
|
1395
|
+
}
|
|
1396
|
+
let bestPoint = null;
|
|
1397
|
+
let bestParam = 0;
|
|
1398
|
+
let bestDist = Infinity;
|
|
1399
|
+
for (let i = 1; i < this.vertices.length; i++) {
|
|
1400
|
+
const v0 = this.vertices[i - 1];
|
|
1401
|
+
const v1 = this.vertices[i];
|
|
1402
|
+
const p0 = { x: v0.position.x, y: v0.position.y, z: v0.position.z };
|
|
1403
|
+
const p1 = { x: v1.position.x, y: v1.position.y, z: v1.position.z };
|
|
1404
|
+
const projected = projectPointOnSegment(point, p0, p1);
|
|
1405
|
+
const dist = distance(point, projected);
|
|
1406
|
+
if (dist < bestDist) {
|
|
1407
|
+
bestDist = dist;
|
|
1408
|
+
bestPoint = projected;
|
|
1409
|
+
const prevLen = this._cumulativeLengths[i - 1] ?? 0;
|
|
1410
|
+
const currLen = this._cumulativeLengths[i] ?? 0;
|
|
1411
|
+
const edgeLength = currLen - prevLen;
|
|
1412
|
+
if (edgeLength < 1e-10) {
|
|
1413
|
+
bestParam = prevLen / this._totalLength;
|
|
1414
|
+
} else {
|
|
1415
|
+
const localDist = distance(p0, projected);
|
|
1416
|
+
bestParam = (prevLen + localDist) / this._totalLength;
|
|
1417
|
+
}
|
|
1418
|
+
}
|
|
1419
|
+
}
|
|
1420
|
+
if (!bestPoint) {
|
|
1421
|
+
return null;
|
|
1422
|
+
}
|
|
1423
|
+
return {
|
|
1424
|
+
point: bestPoint,
|
|
1425
|
+
parameter: bestParam,
|
|
1426
|
+
distance: bestDist
|
|
1427
|
+
};
|
|
1428
|
+
}
|
|
1429
|
+
/**
|
|
1430
|
+
* Gets the vertex at a specific index.
|
|
1431
|
+
*/
|
|
1432
|
+
getVertex(index) {
|
|
1433
|
+
return this.vertices[index];
|
|
1434
|
+
}
|
|
1435
|
+
/**
|
|
1436
|
+
* Gets the edge at a specific index.
|
|
1437
|
+
*/
|
|
1438
|
+
getEdge(index) {
|
|
1439
|
+
return this.edges[index];
|
|
1440
|
+
}
|
|
1441
|
+
/**
|
|
1442
|
+
* Checks if a vertex is part of this segment.
|
|
1443
|
+
*/
|
|
1444
|
+
containsVertex(vertex) {
|
|
1445
|
+
return this.vertices.some((v) => v.id === vertex.id);
|
|
1446
|
+
}
|
|
1447
|
+
/**
|
|
1448
|
+
* Checks if an edge is part of this segment.
|
|
1449
|
+
*/
|
|
1450
|
+
containsEdge(edge) {
|
|
1451
|
+
return this.edges.some((e) => e.id === edge.id);
|
|
1452
|
+
}
|
|
1453
|
+
/**
|
|
1454
|
+
* Gets the index of a vertex in this segment.
|
|
1455
|
+
*/
|
|
1456
|
+
indexOfVertex(vertex) {
|
|
1457
|
+
return this.vertices.findIndex((v) => v.id === vertex.id);
|
|
1458
|
+
}
|
|
1459
|
+
/**
|
|
1460
|
+
* Iterates over vertices in this segment.
|
|
1461
|
+
*/
|
|
1462
|
+
forEachVertex(callback) {
|
|
1463
|
+
this.vertices.forEach(callback);
|
|
1464
|
+
}
|
|
1465
|
+
/**
|
|
1466
|
+
* Iterates over edges in this segment.
|
|
1467
|
+
*/
|
|
1468
|
+
forEachEdge(callback) {
|
|
1469
|
+
this.edges.forEach(callback);
|
|
1470
|
+
}
|
|
1471
|
+
/**
|
|
1472
|
+
* Creates a copy of this segment.
|
|
1473
|
+
*/
|
|
1474
|
+
clone(newId) {
|
|
1475
|
+
const segment = new SkeletonSegment(newId);
|
|
1476
|
+
segment.vertices = [...this.vertices];
|
|
1477
|
+
segment.edges = [...this.edges];
|
|
1478
|
+
segment.isClosed = this.isClosed;
|
|
1479
|
+
segment._totalLength = this._totalLength;
|
|
1480
|
+
segment._cumulativeLengths = [...this._cumulativeLengths];
|
|
1481
|
+
return segment;
|
|
1482
|
+
}
|
|
1483
|
+
}
|
|
1484
|
+
class SkeletonBuilder {
|
|
1485
|
+
constructor(mesh) {
|
|
1486
|
+
this.nextSegmentId = 0;
|
|
1487
|
+
this.mesh = mesh;
|
|
1488
|
+
}
|
|
1489
|
+
/**
|
|
1490
|
+
* Builds the skeleton and returns the result.
|
|
1491
|
+
*/
|
|
1492
|
+
build() {
|
|
1493
|
+
const skeletonEdges = this.mesh.getSkeletonEdges();
|
|
1494
|
+
const branchingVertices = [];
|
|
1495
|
+
const openBookVertices = [];
|
|
1496
|
+
for (const vertex of this.mesh.getVertices()) {
|
|
1497
|
+
if (vertex.type === VertexType.SkeletonBranching) {
|
|
1498
|
+
branchingVertices.push(vertex);
|
|
1499
|
+
} else if (vertex.type === VertexType.OpenBook) {
|
|
1500
|
+
openBookVertices.push(vertex);
|
|
1501
|
+
}
|
|
1502
|
+
}
|
|
1503
|
+
const segments = this.buildSegments(skeletonEdges, branchingVertices);
|
|
1504
|
+
return {
|
|
1505
|
+
segments,
|
|
1506
|
+
skeletonEdges,
|
|
1507
|
+
branchingVertices,
|
|
1508
|
+
openBookVertices
|
|
1509
|
+
};
|
|
1510
|
+
}
|
|
1511
|
+
/**
|
|
1512
|
+
* Builds segments from skeleton edges.
|
|
1513
|
+
* A segment is a path between two branching vertices.
|
|
1514
|
+
*/
|
|
1515
|
+
buildSegments(skeletonEdges, branchingVertices) {
|
|
1516
|
+
const segments = [];
|
|
1517
|
+
const visitedEdges = /* @__PURE__ */ new Set();
|
|
1518
|
+
const branchingIds = new Set(branchingVertices.map((v) => v.id));
|
|
1519
|
+
for (const startVertex of branchingVertices) {
|
|
1520
|
+
const incidentEdges = this.getIncidentSkeletonEdges(startVertex, skeletonEdges);
|
|
1521
|
+
for (const startEdge of incidentEdges) {
|
|
1522
|
+
if (visitedEdges.has(startEdge.id)) {
|
|
1523
|
+
continue;
|
|
1524
|
+
}
|
|
1525
|
+
const segment = this.traceSegment(startVertex, startEdge, branchingIds, visitedEdges);
|
|
1526
|
+
if (segment) {
|
|
1527
|
+
segments.push(segment);
|
|
1528
|
+
}
|
|
1529
|
+
}
|
|
1530
|
+
}
|
|
1531
|
+
for (const edge of skeletonEdges) {
|
|
1532
|
+
if (visitedEdges.has(edge.id)) {
|
|
1533
|
+
continue;
|
|
1534
|
+
}
|
|
1535
|
+
const segment = this.traceClosedLoop(edge, visitedEdges);
|
|
1536
|
+
if (segment) {
|
|
1537
|
+
segments.push(segment);
|
|
1538
|
+
}
|
|
1539
|
+
}
|
|
1540
|
+
return segments;
|
|
1541
|
+
}
|
|
1542
|
+
/**
|
|
1543
|
+
* Gets skeleton edges incident to a vertex.
|
|
1544
|
+
*/
|
|
1545
|
+
getIncidentSkeletonEdges(vertex, skeletonEdges) {
|
|
1546
|
+
const skeletonEdgeSet = new Set(skeletonEdges.map((e) => e.id));
|
|
1547
|
+
const result = [];
|
|
1548
|
+
vertex.forEachOutgoingHalfedge((he) => {
|
|
1549
|
+
if (skeletonEdgeSet.has(he.edge.id)) {
|
|
1550
|
+
if (!result.some((e) => e.id === he.edge.id)) {
|
|
1551
|
+
result.push(he.edge);
|
|
1552
|
+
}
|
|
1553
|
+
}
|
|
1554
|
+
});
|
|
1555
|
+
return result;
|
|
1556
|
+
}
|
|
1557
|
+
/**
|
|
1558
|
+
* Traces a segment from a starting vertex and edge.
|
|
1559
|
+
*/
|
|
1560
|
+
traceSegment(startVertex, startEdge, branchingIds, visitedEdges) {
|
|
1561
|
+
var _a, _b;
|
|
1562
|
+
const segment = new SkeletonSegment(this.createSegmentId());
|
|
1563
|
+
segment.addVertex(startVertex);
|
|
1564
|
+
let currentVertex = startVertex;
|
|
1565
|
+
let currentEdge = startEdge;
|
|
1566
|
+
while (currentEdge && !visitedEdges.has(currentEdge.id)) {
|
|
1567
|
+
visitedEdges.add(currentEdge.id);
|
|
1568
|
+
segment.addEdge(currentEdge);
|
|
1569
|
+
const nextVertex = currentEdge.getOtherVertex(currentVertex);
|
|
1570
|
+
if (!nextVertex) {
|
|
1571
|
+
break;
|
|
1572
|
+
}
|
|
1573
|
+
segment.addVertex(nextVertex);
|
|
1574
|
+
currentVertex = nextVertex;
|
|
1575
|
+
if (branchingIds.has(currentVertex.id)) {
|
|
1576
|
+
break;
|
|
1577
|
+
}
|
|
1578
|
+
currentEdge = this.getNextSkeletonEdge(currentVertex, currentEdge, visitedEdges);
|
|
1579
|
+
}
|
|
1580
|
+
if (segment.vertices.length > 2 && ((_a = segment.startVertex) == null ? void 0 : _a.id) === ((_b = segment.endVertex) == null ? void 0 : _b.id)) {
|
|
1581
|
+
segment.isClosed = true;
|
|
1582
|
+
segment.vertices.pop();
|
|
1583
|
+
}
|
|
1584
|
+
segment.recomputeLengths();
|
|
1585
|
+
return segment;
|
|
1586
|
+
}
|
|
1587
|
+
/**
|
|
1588
|
+
* Traces a closed loop starting from an edge.
|
|
1589
|
+
*/
|
|
1590
|
+
traceClosedLoop(startEdge, visitedEdges) {
|
|
1591
|
+
const [v0, v1] = startEdge.getVertices();
|
|
1592
|
+
if (!v0 || !v1) {
|
|
1593
|
+
return null;
|
|
1594
|
+
}
|
|
1595
|
+
const segment = new SkeletonSegment(this.createSegmentId());
|
|
1596
|
+
segment.addVertex(v0);
|
|
1597
|
+
segment.addEdge(startEdge);
|
|
1598
|
+
segment.addVertex(v1);
|
|
1599
|
+
visitedEdges.add(startEdge.id);
|
|
1600
|
+
let currentVertex = v1;
|
|
1601
|
+
let currentEdge = this.getNextSkeletonEdge(currentVertex, startEdge, visitedEdges);
|
|
1602
|
+
while (currentEdge && !visitedEdges.has(currentEdge.id)) {
|
|
1603
|
+
visitedEdges.add(currentEdge.id);
|
|
1604
|
+
segment.addEdge(currentEdge);
|
|
1605
|
+
const nextVertex = currentEdge.getOtherVertex(currentVertex);
|
|
1606
|
+
if (!nextVertex) {
|
|
1607
|
+
break;
|
|
1608
|
+
}
|
|
1609
|
+
if (nextVertex.id === v0.id) {
|
|
1610
|
+
segment.isClosed = true;
|
|
1611
|
+
break;
|
|
1612
|
+
}
|
|
1613
|
+
segment.addVertex(nextVertex);
|
|
1614
|
+
currentVertex = nextVertex;
|
|
1615
|
+
currentEdge = this.getNextSkeletonEdge(currentVertex, currentEdge, visitedEdges);
|
|
1616
|
+
}
|
|
1617
|
+
segment.recomputeLengths();
|
|
1618
|
+
return segment;
|
|
1619
|
+
}
|
|
1620
|
+
/**
|
|
1621
|
+
* Gets the next skeleton edge from a vertex, excluding the current edge.
|
|
1622
|
+
*/
|
|
1623
|
+
getNextSkeletonEdge(vertex, currentEdge, visitedEdges) {
|
|
1624
|
+
let result = null;
|
|
1625
|
+
vertex.forEachOutgoingHalfedge((he) => {
|
|
1626
|
+
if (he.edge.id !== currentEdge.id && he.edge.isSkeletonEdge() && !visitedEdges.has(he.edge.id)) {
|
|
1627
|
+
result = he.edge;
|
|
1628
|
+
}
|
|
1629
|
+
});
|
|
1630
|
+
return result;
|
|
1631
|
+
}
|
|
1632
|
+
/**
|
|
1633
|
+
* Creates a new segment ID.
|
|
1634
|
+
*/
|
|
1635
|
+
createSegmentId() {
|
|
1636
|
+
return createSegmentId(this.nextSegmentId++);
|
|
1637
|
+
}
|
|
1638
|
+
}
|
|
1639
|
+
function buildSkeleton(mesh) {
|
|
1640
|
+
const builder = new SkeletonBuilder(mesh);
|
|
1641
|
+
return builder.build();
|
|
1642
|
+
}
|
|
1643
|
+
class FeatureSkeleton {
|
|
1644
|
+
constructor(mesh) {
|
|
1645
|
+
this.segments = /* @__PURE__ */ new Map();
|
|
1646
|
+
this.skeletonEdges = [];
|
|
1647
|
+
this.branchingVertices = [];
|
|
1648
|
+
this.openBookVertices = [];
|
|
1649
|
+
this.vertexToSegment = /* @__PURE__ */ new Map();
|
|
1650
|
+
this.isBuilt = false;
|
|
1651
|
+
this.mesh = mesh;
|
|
1652
|
+
}
|
|
1653
|
+
/**
|
|
1654
|
+
* Builds the skeleton from the mesh.
|
|
1655
|
+
*/
|
|
1656
|
+
build() {
|
|
1657
|
+
const builder = new SkeletonBuilder(this.mesh);
|
|
1658
|
+
const result = builder.build();
|
|
1659
|
+
this.applyBuildResult(result);
|
|
1660
|
+
this.isBuilt = true;
|
|
1661
|
+
}
|
|
1662
|
+
/**
|
|
1663
|
+
* Applies a build result to this skeleton.
|
|
1664
|
+
*/
|
|
1665
|
+
applyBuildResult(result) {
|
|
1666
|
+
this.segments.clear();
|
|
1667
|
+
this.vertexToSegment.clear();
|
|
1668
|
+
for (const segment of result.segments) {
|
|
1669
|
+
this.segments.set(segment.id, segment);
|
|
1670
|
+
for (let i = 1; i < segment.vertices.length - 1; i++) {
|
|
1671
|
+
const vertex = segment.vertices[i];
|
|
1672
|
+
if (vertex) {
|
|
1673
|
+
this.vertexToSegment.set(vertex.id, segment);
|
|
1674
|
+
}
|
|
1675
|
+
}
|
|
1676
|
+
}
|
|
1677
|
+
this.skeletonEdges = result.skeletonEdges;
|
|
1678
|
+
this.branchingVertices = result.branchingVertices;
|
|
1679
|
+
this.openBookVertices = result.openBookVertices;
|
|
1680
|
+
}
|
|
1681
|
+
/**
|
|
1682
|
+
* Rebuilds the skeleton. Call after topology changes.
|
|
1683
|
+
*/
|
|
1684
|
+
rebuild() {
|
|
1685
|
+
this.build();
|
|
1686
|
+
}
|
|
1687
|
+
/**
|
|
1688
|
+
* Gets all segments.
|
|
1689
|
+
*/
|
|
1690
|
+
getSegments() {
|
|
1691
|
+
return Array.from(this.segments.values());
|
|
1692
|
+
}
|
|
1693
|
+
/**
|
|
1694
|
+
* Gets a segment by ID.
|
|
1695
|
+
*/
|
|
1696
|
+
getSegment(id) {
|
|
1697
|
+
return this.segments.get(id);
|
|
1698
|
+
}
|
|
1699
|
+
/**
|
|
1700
|
+
* Gets the segment containing a vertex.
|
|
1701
|
+
* Only works for open-book vertices.
|
|
1702
|
+
*/
|
|
1703
|
+
getSegmentForVertex(vertex) {
|
|
1704
|
+
return this.vertexToSegment.get(vertex.id);
|
|
1705
|
+
}
|
|
1706
|
+
/**
|
|
1707
|
+
* Gets all skeleton edges.
|
|
1708
|
+
*/
|
|
1709
|
+
getSkeletonEdges() {
|
|
1710
|
+
return this.skeletonEdges;
|
|
1711
|
+
}
|
|
1712
|
+
/**
|
|
1713
|
+
* Gets all branching vertices.
|
|
1714
|
+
*/
|
|
1715
|
+
getBranchingVertices() {
|
|
1716
|
+
return this.branchingVertices;
|
|
1717
|
+
}
|
|
1718
|
+
/**
|
|
1719
|
+
* Gets all open-book vertices.
|
|
1720
|
+
*/
|
|
1721
|
+
getOpenBookVertices() {
|
|
1722
|
+
return this.openBookVertices;
|
|
1723
|
+
}
|
|
1724
|
+
/**
|
|
1725
|
+
* Gets the number of segments.
|
|
1726
|
+
*/
|
|
1727
|
+
get segmentCount() {
|
|
1728
|
+
return this.segments.size;
|
|
1729
|
+
}
|
|
1730
|
+
/**
|
|
1731
|
+
* Gets the number of skeleton edges.
|
|
1732
|
+
*/
|
|
1733
|
+
get skeletonEdgeCount() {
|
|
1734
|
+
return this.skeletonEdges.length;
|
|
1735
|
+
}
|
|
1736
|
+
/**
|
|
1737
|
+
* Gets the number of branching vertices.
|
|
1738
|
+
*/
|
|
1739
|
+
get branchingVertexCount() {
|
|
1740
|
+
return this.branchingVertices.length;
|
|
1741
|
+
}
|
|
1742
|
+
/**
|
|
1743
|
+
* Gets the number of open-book vertices.
|
|
1744
|
+
*/
|
|
1745
|
+
get openBookVertexCount() {
|
|
1746
|
+
return this.openBookVertices.length;
|
|
1747
|
+
}
|
|
1748
|
+
/**
|
|
1749
|
+
* Checks if the skeleton has been built.
|
|
1750
|
+
*/
|
|
1751
|
+
get built() {
|
|
1752
|
+
return this.isBuilt;
|
|
1753
|
+
}
|
|
1754
|
+
/**
|
|
1755
|
+
* Projects a point onto the skeleton.
|
|
1756
|
+
* Returns the closest point on any segment.
|
|
1757
|
+
*/
|
|
1758
|
+
projectPoint(point) {
|
|
1759
|
+
let bestResult = null;
|
|
1760
|
+
for (const segment of this.segments.values()) {
|
|
1761
|
+
const projection = segment.projectPoint(point);
|
|
1762
|
+
if (projection && (!bestResult || projection.distance < bestResult.distance)) {
|
|
1763
|
+
bestResult = {
|
|
1764
|
+
point: projection.point,
|
|
1765
|
+
segment,
|
|
1766
|
+
parameter: projection.parameter,
|
|
1767
|
+
distance: projection.distance
|
|
1768
|
+
};
|
|
1769
|
+
}
|
|
1770
|
+
}
|
|
1771
|
+
return bestResult;
|
|
1772
|
+
}
|
|
1773
|
+
/**
|
|
1774
|
+
* Checks if a vertex is on the skeleton.
|
|
1775
|
+
*/
|
|
1776
|
+
isVertexOnSkeleton(vertex) {
|
|
1777
|
+
return vertex.isOnSkeleton();
|
|
1778
|
+
}
|
|
1779
|
+
/**
|
|
1780
|
+
* Checks if an edge is on the skeleton.
|
|
1781
|
+
*/
|
|
1782
|
+
isEdgeOnSkeleton(edge) {
|
|
1783
|
+
return edge.isSkeletonEdge();
|
|
1784
|
+
}
|
|
1785
|
+
/**
|
|
1786
|
+
* Gets all vertices on the skeleton.
|
|
1787
|
+
*/
|
|
1788
|
+
getAllSkeletonVertices() {
|
|
1789
|
+
return [...this.branchingVertices, ...this.openBookVertices];
|
|
1790
|
+
}
|
|
1791
|
+
/**
|
|
1792
|
+
* Computes the total length of the skeleton.
|
|
1793
|
+
*/
|
|
1794
|
+
getTotalLength() {
|
|
1795
|
+
let total = 0;
|
|
1796
|
+
for (const segment of this.segments.values()) {
|
|
1797
|
+
total += segment.totalLength;
|
|
1798
|
+
}
|
|
1799
|
+
return total;
|
|
1800
|
+
}
|
|
1801
|
+
/**
|
|
1802
|
+
* Gets statistics about the skeleton.
|
|
1803
|
+
*/
|
|
1804
|
+
getStats() {
|
|
1805
|
+
let closedLoopCount = 0;
|
|
1806
|
+
for (const segment of this.segments.values()) {
|
|
1807
|
+
if (segment.isClosed) {
|
|
1808
|
+
closedLoopCount++;
|
|
1809
|
+
}
|
|
1810
|
+
}
|
|
1811
|
+
return {
|
|
1812
|
+
segmentCount: this.segmentCount,
|
|
1813
|
+
skeletonEdgeCount: this.skeletonEdgeCount,
|
|
1814
|
+
branchingVertexCount: this.branchingVertexCount,
|
|
1815
|
+
openBookVertexCount: this.openBookVertexCount,
|
|
1816
|
+
totalLength: this.getTotalLength(),
|
|
1817
|
+
closedLoopCount
|
|
1818
|
+
};
|
|
1819
|
+
}
|
|
1820
|
+
}
|
|
1821
|
+
function createSkeleton(mesh) {
|
|
1822
|
+
const skeleton = new FeatureSkeleton(mesh);
|
|
1823
|
+
skeleton.build();
|
|
1824
|
+
return skeleton;
|
|
1825
|
+
}
|
|
1826
|
+
class SkeletonConstraints {
|
|
1827
|
+
constructor(skeleton) {
|
|
1828
|
+
this.skeleton = skeleton;
|
|
1829
|
+
}
|
|
1830
|
+
/**
|
|
1831
|
+
* Constrains a target position based on vertex type.
|
|
1832
|
+
*
|
|
1833
|
+
* - Manifold vertices: Can move freely (no constraint)
|
|
1834
|
+
* - Open-book vertices: Constrained to their skeleton segment
|
|
1835
|
+
* - Branching vertices: Fixed (cannot move)
|
|
1836
|
+
*
|
|
1837
|
+
* @param vertex - The vertex to constrain
|
|
1838
|
+
* @param targetPosition - The desired target position
|
|
1839
|
+
* @returns The constrained position
|
|
1840
|
+
*/
|
|
1841
|
+
constrainPosition(vertex, targetPosition) {
|
|
1842
|
+
switch (vertex.type) {
|
|
1843
|
+
case VertexType.Manifold:
|
|
1844
|
+
return {
|
|
1845
|
+
position: targetPosition,
|
|
1846
|
+
wasConstrained: false,
|
|
1847
|
+
constraintDistance: 0
|
|
1848
|
+
};
|
|
1849
|
+
case VertexType.OpenBook:
|
|
1850
|
+
return this.constrainToSegment(vertex, targetPosition);
|
|
1851
|
+
case VertexType.SkeletonBranching:
|
|
1852
|
+
case VertexType.NonManifoldOther:
|
|
1853
|
+
return {
|
|
1854
|
+
position: {
|
|
1855
|
+
x: vertex.position.x,
|
|
1856
|
+
y: vertex.position.y,
|
|
1857
|
+
z: vertex.position.z
|
|
1858
|
+
},
|
|
1859
|
+
wasConstrained: true,
|
|
1860
|
+
constraintDistance: distance(targetPosition, {
|
|
1861
|
+
x: vertex.position.x,
|
|
1862
|
+
y: vertex.position.y,
|
|
1863
|
+
z: vertex.position.z
|
|
1864
|
+
})
|
|
1865
|
+
};
|
|
1866
|
+
default:
|
|
1867
|
+
return {
|
|
1868
|
+
position: targetPosition,
|
|
1869
|
+
wasConstrained: false,
|
|
1870
|
+
constraintDistance: 0
|
|
1871
|
+
};
|
|
1872
|
+
}
|
|
1873
|
+
}
|
|
1874
|
+
/**
|
|
1875
|
+
* Constrains a position to a skeleton segment.
|
|
1876
|
+
*/
|
|
1877
|
+
constrainToSegment(vertex, targetPosition) {
|
|
1878
|
+
const segment = this.skeleton.getSegmentForVertex(vertex);
|
|
1879
|
+
if (!segment) {
|
|
1880
|
+
const projection2 = this.skeleton.projectPoint(targetPosition);
|
|
1881
|
+
if (projection2) {
|
|
1882
|
+
return {
|
|
1883
|
+
position: projection2.point,
|
|
1884
|
+
wasConstrained: true,
|
|
1885
|
+
segment: projection2.segment,
|
|
1886
|
+
constraintDistance: projection2.distance
|
|
1887
|
+
};
|
|
1888
|
+
}
|
|
1889
|
+
return {
|
|
1890
|
+
position: {
|
|
1891
|
+
x: vertex.position.x,
|
|
1892
|
+
y: vertex.position.y,
|
|
1893
|
+
z: vertex.position.z
|
|
1894
|
+
},
|
|
1895
|
+
wasConstrained: true,
|
|
1896
|
+
constraintDistance: distance(targetPosition, {
|
|
1897
|
+
x: vertex.position.x,
|
|
1898
|
+
y: vertex.position.y,
|
|
1899
|
+
z: vertex.position.z
|
|
1900
|
+
})
|
|
1901
|
+
};
|
|
1902
|
+
}
|
|
1903
|
+
const projection = segment.projectPoint(targetPosition);
|
|
1904
|
+
if (!projection) {
|
|
1905
|
+
return {
|
|
1906
|
+
position: {
|
|
1907
|
+
x: vertex.position.x,
|
|
1908
|
+
y: vertex.position.y,
|
|
1909
|
+
z: vertex.position.z
|
|
1910
|
+
},
|
|
1911
|
+
wasConstrained: true,
|
|
1912
|
+
segment,
|
|
1913
|
+
constraintDistance: 0
|
|
1914
|
+
};
|
|
1915
|
+
}
|
|
1916
|
+
return {
|
|
1917
|
+
position: projection.point,
|
|
1918
|
+
wasConstrained: true,
|
|
1919
|
+
segment,
|
|
1920
|
+
constraintDistance: projection.distance
|
|
1921
|
+
};
|
|
1922
|
+
}
|
|
1923
|
+
/**
|
|
1924
|
+
* Checks if a vertex can move freely.
|
|
1925
|
+
*/
|
|
1926
|
+
canMoveFreely(vertex) {
|
|
1927
|
+
return vertex.type === VertexType.Manifold;
|
|
1928
|
+
}
|
|
1929
|
+
/**
|
|
1930
|
+
* Checks if a vertex is fixed (cannot move).
|
|
1931
|
+
*/
|
|
1932
|
+
isFixed(vertex) {
|
|
1933
|
+
return vertex.type === VertexType.SkeletonBranching || vertex.type === VertexType.NonManifoldOther;
|
|
1934
|
+
}
|
|
1935
|
+
/**
|
|
1936
|
+
* Checks if a vertex is constrained to a segment.
|
|
1937
|
+
*/
|
|
1938
|
+
isConstrainedToSegment(vertex) {
|
|
1939
|
+
return vertex.type === VertexType.OpenBook;
|
|
1940
|
+
}
|
|
1941
|
+
/**
|
|
1942
|
+
* Gets the segment a vertex is constrained to.
|
|
1943
|
+
*/
|
|
1944
|
+
getConstraintSegment(vertex) {
|
|
1945
|
+
if (vertex.type !== VertexType.OpenBook) {
|
|
1946
|
+
return void 0;
|
|
1947
|
+
}
|
|
1948
|
+
return this.skeleton.getSegmentForVertex(vertex);
|
|
1949
|
+
}
|
|
1950
|
+
/**
|
|
1951
|
+
* Computes the allowed movement direction for a vertex.
|
|
1952
|
+
* For open-book vertices, this is the tangent direction of the segment.
|
|
1953
|
+
*/
|
|
1954
|
+
getAllowedDirection(vertex) {
|
|
1955
|
+
if (vertex.type !== VertexType.OpenBook) {
|
|
1956
|
+
return null;
|
|
1957
|
+
}
|
|
1958
|
+
const segment = this.skeleton.getSegmentForVertex(vertex);
|
|
1959
|
+
if (!segment || segment.vertices.length < 2) {
|
|
1960
|
+
return null;
|
|
1961
|
+
}
|
|
1962
|
+
const idx = segment.indexOfVertex(vertex);
|
|
1963
|
+
if (idx < 0) {
|
|
1964
|
+
return null;
|
|
1965
|
+
}
|
|
1966
|
+
let p0;
|
|
1967
|
+
let p1;
|
|
1968
|
+
if (idx === 0) {
|
|
1969
|
+
const v0 = segment.vertices[0];
|
|
1970
|
+
const v1 = segment.vertices[1];
|
|
1971
|
+
p0 = { x: v0.position.x, y: v0.position.y, z: v0.position.z };
|
|
1972
|
+
p1 = { x: v1.position.x, y: v1.position.y, z: v1.position.z };
|
|
1973
|
+
} else if (idx === segment.vertices.length - 1) {
|
|
1974
|
+
const v0 = segment.vertices[idx - 1];
|
|
1975
|
+
const v1 = segment.vertices[idx];
|
|
1976
|
+
p0 = { x: v0.position.x, y: v0.position.y, z: v0.position.z };
|
|
1977
|
+
p1 = { x: v1.position.x, y: v1.position.y, z: v1.position.z };
|
|
1978
|
+
} else {
|
|
1979
|
+
const vPrev = segment.vertices[idx - 1];
|
|
1980
|
+
const vNext = segment.vertices[idx + 1];
|
|
1981
|
+
p0 = { x: vPrev.position.x, y: vPrev.position.y, z: vPrev.position.z };
|
|
1982
|
+
p1 = { x: vNext.position.x, y: vNext.position.y, z: vNext.position.z };
|
|
1983
|
+
}
|
|
1984
|
+
const dx = p1.x - p0.x;
|
|
1985
|
+
const dy = p1.y - p0.y;
|
|
1986
|
+
const dz = p1.z - p0.z;
|
|
1987
|
+
const len = Math.sqrt(dx * dx + dy * dy + dz * dz);
|
|
1988
|
+
if (len < 1e-10) {
|
|
1989
|
+
return null;
|
|
1990
|
+
}
|
|
1991
|
+
return {
|
|
1992
|
+
x: dx / len,
|
|
1993
|
+
y: dy / len,
|
|
1994
|
+
z: dz / len
|
|
1995
|
+
};
|
|
1996
|
+
}
|
|
1997
|
+
}
|
|
1998
|
+
function createSkeletonConstraints(skeleton) {
|
|
1999
|
+
return new SkeletonConstraints(skeleton);
|
|
2000
|
+
}
|
|
2001
|
+
function validateGeometry(geometry) {
|
|
2002
|
+
const errors = [];
|
|
2003
|
+
const warnings = [];
|
|
2004
|
+
const positions = geometry.attributes["position"];
|
|
2005
|
+
if (!positions) {
|
|
2006
|
+
errors.push("Geometry must have a position attribute");
|
|
2007
|
+
return { isValid: false, errors, warnings };
|
|
2008
|
+
}
|
|
2009
|
+
const indices = geometry.index;
|
|
2010
|
+
if (!indices) {
|
|
2011
|
+
errors.push("Geometry must be indexed");
|
|
2012
|
+
return { isValid: false, errors, warnings };
|
|
2013
|
+
}
|
|
2014
|
+
if (indices.count % 3 !== 0) {
|
|
2015
|
+
errors.push(`Index count (${indices.count}) must be divisible by 3 for triangle mesh`);
|
|
2016
|
+
}
|
|
2017
|
+
const numVertices = positions.count;
|
|
2018
|
+
for (let i = 0; i < indices.count; i++) {
|
|
2019
|
+
const index = indices.getX(i);
|
|
2020
|
+
if (index < 0 || index >= numVertices) {
|
|
2021
|
+
errors.push(`Invalid index ${index} at position ${i} (vertex count: ${numVertices})`);
|
|
2022
|
+
break;
|
|
2023
|
+
}
|
|
2024
|
+
}
|
|
2025
|
+
let degenerateCount = 0;
|
|
2026
|
+
const numFaces = indices.count / 3;
|
|
2027
|
+
for (let i = 0; i < numFaces; i++) {
|
|
2028
|
+
const i0 = indices.getX(i * 3);
|
|
2029
|
+
const i1 = indices.getX(i * 3 + 1);
|
|
2030
|
+
const i2 = indices.getX(i * 3 + 2);
|
|
2031
|
+
if (i0 === i1 || i1 === i2 || i2 === i0) {
|
|
2032
|
+
degenerateCount++;
|
|
2033
|
+
}
|
|
2034
|
+
}
|
|
2035
|
+
if (degenerateCount > 0) {
|
|
2036
|
+
warnings.push(`Found ${degenerateCount} degenerate triangle(s) with repeated vertices`);
|
|
2037
|
+
}
|
|
2038
|
+
let invalidPositionCount = 0;
|
|
2039
|
+
for (let i = 0; i < numVertices; i++) {
|
|
2040
|
+
const x = positions.getX(i);
|
|
2041
|
+
const y = positions.getY(i);
|
|
2042
|
+
const z = positions.getZ(i);
|
|
2043
|
+
if (!isFinite(x) || !isFinite(y) || !isFinite(z)) {
|
|
2044
|
+
invalidPositionCount++;
|
|
2045
|
+
}
|
|
2046
|
+
}
|
|
2047
|
+
if (invalidPositionCount > 0) {
|
|
2048
|
+
errors.push(`Found ${invalidPositionCount} vertex position(s) with NaN or Infinity values`);
|
|
2049
|
+
}
|
|
2050
|
+
return {
|
|
2051
|
+
isValid: errors.length === 0,
|
|
2052
|
+
errors,
|
|
2053
|
+
warnings
|
|
2054
|
+
};
|
|
2055
|
+
}
|
|
2056
|
+
function importBufferGeometry(geometry, options = {}) {
|
|
2057
|
+
const { featureEdges, validate = true } = options;
|
|
2058
|
+
if (validate) {
|
|
2059
|
+
const validation = validateGeometry(geometry);
|
|
2060
|
+
if (!validation.isValid) {
|
|
2061
|
+
throw new Error(`Invalid geometry: ${validation.errors.join("; ")}`);
|
|
2062
|
+
}
|
|
2063
|
+
}
|
|
2064
|
+
return NonManifoldMesh.fromBufferGeometry(geometry, featureEdges);
|
|
2065
|
+
}
|
|
2066
|
+
class BufferGeometryImporter {
|
|
2067
|
+
constructor(options = {}) {
|
|
2068
|
+
this.options = options;
|
|
2069
|
+
}
|
|
2070
|
+
/**
|
|
2071
|
+
* Imports a BufferGeometry into a NonManifoldMesh.
|
|
2072
|
+
*/
|
|
2073
|
+
import(geometry) {
|
|
2074
|
+
return importBufferGeometry(geometry, this.options);
|
|
2075
|
+
}
|
|
2076
|
+
/**
|
|
2077
|
+
* Validates a BufferGeometry without importing.
|
|
2078
|
+
*/
|
|
2079
|
+
validate(geometry) {
|
|
2080
|
+
return validateGeometry(geometry);
|
|
2081
|
+
}
|
|
2082
|
+
/**
|
|
2083
|
+
* Sets feature edges to preserve during remeshing.
|
|
2084
|
+
*/
|
|
2085
|
+
setFeatureEdges(edges) {
|
|
2086
|
+
this.options.featureEdges = edges;
|
|
2087
|
+
return this;
|
|
2088
|
+
}
|
|
2089
|
+
/**
|
|
2090
|
+
* Enables or disables validation.
|
|
2091
|
+
*/
|
|
2092
|
+
setValidation(enabled) {
|
|
2093
|
+
this.options.validate = enabled;
|
|
2094
|
+
return this;
|
|
2095
|
+
}
|
|
2096
|
+
}
|
|
2097
|
+
function exportBufferGeometry(mesh, options = {}) {
|
|
2098
|
+
const { computeNormals = true, smoothNormals = true } = options;
|
|
2099
|
+
const geometry = new BufferGeometry();
|
|
2100
|
+
const faces = mesh.getFaces();
|
|
2101
|
+
if (faces.length === 0) {
|
|
2102
|
+
return geometry;
|
|
2103
|
+
}
|
|
2104
|
+
const vertices = mesh.getVertices();
|
|
2105
|
+
const vertexIndexMap = /* @__PURE__ */ new Map();
|
|
2106
|
+
vertices.forEach((v, i) => {
|
|
2107
|
+
vertexIndexMap.set(v.id, i);
|
|
2108
|
+
});
|
|
2109
|
+
const positions = new Float32Array(vertices.length * 3);
|
|
2110
|
+
vertices.forEach((v, i) => {
|
|
2111
|
+
positions[i * 3] = v.position.x;
|
|
2112
|
+
positions[i * 3 + 1] = v.position.y;
|
|
2113
|
+
positions[i * 3 + 2] = v.position.z;
|
|
2114
|
+
});
|
|
2115
|
+
const indices = [];
|
|
2116
|
+
for (const face of faces) {
|
|
2117
|
+
const verts = face.getVertices();
|
|
2118
|
+
if (!verts) continue;
|
|
2119
|
+
const [v0, v1, v2] = verts;
|
|
2120
|
+
const i0 = vertexIndexMap.get(v0.id);
|
|
2121
|
+
const i1 = vertexIndexMap.get(v1.id);
|
|
2122
|
+
const i2 = vertexIndexMap.get(v2.id);
|
|
2123
|
+
if (i0 !== void 0 && i1 !== void 0 && i2 !== void 0) {
|
|
2124
|
+
indices.push(i0, i1, i2);
|
|
2125
|
+
}
|
|
2126
|
+
}
|
|
2127
|
+
geometry.setAttribute("position", new BufferAttribute(positions, 3));
|
|
2128
|
+
geometry.setIndex(indices);
|
|
2129
|
+
if (computeNormals) {
|
|
2130
|
+
if (smoothNormals) {
|
|
2131
|
+
computeSmoothNormals(geometry, mesh, vertexIndexMap);
|
|
2132
|
+
} else {
|
|
2133
|
+
geometry.computeVertexNormals();
|
|
2134
|
+
}
|
|
2135
|
+
}
|
|
2136
|
+
return geometry;
|
|
2137
|
+
}
|
|
2138
|
+
function computeSmoothNormals(geometry, mesh, vertexIndexMap) {
|
|
2139
|
+
const vertices = mesh.getVertices();
|
|
2140
|
+
const normals = new Float32Array(vertices.length * 3);
|
|
2141
|
+
const normalCounts = new Uint32Array(vertices.length);
|
|
2142
|
+
for (const face of mesh.getFaces()) {
|
|
2143
|
+
const faceNormal = face.getNormal();
|
|
2144
|
+
if (!faceNormal) continue;
|
|
2145
|
+
const verts = face.getVertices();
|
|
2146
|
+
if (!verts) continue;
|
|
2147
|
+
for (const v of verts) {
|
|
2148
|
+
const idx = vertexIndexMap.get(v.id);
|
|
2149
|
+
if (idx === void 0) continue;
|
|
2150
|
+
const baseIdx = idx * 3;
|
|
2151
|
+
normals[baseIdx] = (normals[baseIdx] ?? 0) + faceNormal.x;
|
|
2152
|
+
normals[baseIdx + 1] = (normals[baseIdx + 1] ?? 0) + faceNormal.y;
|
|
2153
|
+
normals[baseIdx + 2] = (normals[baseIdx + 2] ?? 0) + faceNormal.z;
|
|
2154
|
+
normalCounts[idx] = (normalCounts[idx] ?? 0) + 1;
|
|
2155
|
+
}
|
|
2156
|
+
}
|
|
2157
|
+
for (let i = 0; i < vertices.length; i++) {
|
|
2158
|
+
const count = normalCounts[i] ?? 0;
|
|
2159
|
+
if (count === 0) continue;
|
|
2160
|
+
const baseIdx = i * 3;
|
|
2161
|
+
const nx = (normals[baseIdx] ?? 0) / count;
|
|
2162
|
+
const ny = (normals[baseIdx + 1] ?? 0) / count;
|
|
2163
|
+
const nz = (normals[baseIdx + 2] ?? 0) / count;
|
|
2164
|
+
const len = Math.sqrt(nx * nx + ny * ny + nz * nz);
|
|
2165
|
+
if (len > 1e-10) {
|
|
2166
|
+
normals[i * 3] = nx / len;
|
|
2167
|
+
normals[i * 3 + 1] = ny / len;
|
|
2168
|
+
normals[i * 3 + 2] = nz / len;
|
|
2169
|
+
} else {
|
|
2170
|
+
normals[i * 3] = 0;
|
|
2171
|
+
normals[i * 3 + 1] = 1;
|
|
2172
|
+
normals[i * 3 + 2] = 0;
|
|
2173
|
+
}
|
|
2174
|
+
}
|
|
2175
|
+
geometry.setAttribute("normal", new BufferAttribute(normals, 3));
|
|
2176
|
+
}
|
|
2177
|
+
function exportSkeletonGeometry(mesh) {
|
|
2178
|
+
const geometry = new BufferGeometry();
|
|
2179
|
+
const skeletonEdges = mesh.getSkeletonEdges();
|
|
2180
|
+
if (skeletonEdges.length === 0) {
|
|
2181
|
+
return geometry;
|
|
2182
|
+
}
|
|
2183
|
+
const positions = new Float32Array(skeletonEdges.length * 6);
|
|
2184
|
+
let offset = 0;
|
|
2185
|
+
for (const edge of skeletonEdges) {
|
|
2186
|
+
const [v0, v1] = edge.getVertices();
|
|
2187
|
+
if (!v0 || !v1) continue;
|
|
2188
|
+
positions[offset++] = v0.position.x;
|
|
2189
|
+
positions[offset++] = v0.position.y;
|
|
2190
|
+
positions[offset++] = v0.position.z;
|
|
2191
|
+
positions[offset++] = v1.position.x;
|
|
2192
|
+
positions[offset++] = v1.position.y;
|
|
2193
|
+
positions[offset++] = v1.position.z;
|
|
2194
|
+
}
|
|
2195
|
+
geometry.setAttribute("position", new BufferAttribute(positions.slice(0, offset), 3));
|
|
2196
|
+
return geometry;
|
|
2197
|
+
}
|
|
2198
|
+
function exportClassificationGeometry(mesh) {
|
|
2199
|
+
const baseGeometry = exportBufferGeometry(mesh, { computeNormals: true });
|
|
2200
|
+
const vertices = mesh.getVertices();
|
|
2201
|
+
const colors = new Float32Array(vertices.length * 3);
|
|
2202
|
+
const vertexIndexMap = /* @__PURE__ */ new Map();
|
|
2203
|
+
vertices.forEach((v, i) => {
|
|
2204
|
+
vertexIndexMap.set(v.id, i);
|
|
2205
|
+
});
|
|
2206
|
+
for (const vertex of vertices) {
|
|
2207
|
+
const idx = vertexIndexMap.get(vertex.id);
|
|
2208
|
+
if (idx === void 0) continue;
|
|
2209
|
+
let r = 0, g = 0, b = 0;
|
|
2210
|
+
switch (vertex.type) {
|
|
2211
|
+
case "manifold":
|
|
2212
|
+
r = 0.2;
|
|
2213
|
+
g = 0.8;
|
|
2214
|
+
b = 0.2;
|
|
2215
|
+
break;
|
|
2216
|
+
case "open_book":
|
|
2217
|
+
r = 0.2;
|
|
2218
|
+
g = 0.4;
|
|
2219
|
+
b = 0.9;
|
|
2220
|
+
break;
|
|
2221
|
+
case "skeleton_branching":
|
|
2222
|
+
r = 0.9;
|
|
2223
|
+
g = 0.2;
|
|
2224
|
+
b = 0.2;
|
|
2225
|
+
break;
|
|
2226
|
+
case "non_manifold_other":
|
|
2227
|
+
r = 0.9;
|
|
2228
|
+
g = 0.2;
|
|
2229
|
+
b = 0.9;
|
|
2230
|
+
break;
|
|
2231
|
+
}
|
|
2232
|
+
colors[idx * 3] = r;
|
|
2233
|
+
colors[idx * 3 + 1] = g;
|
|
2234
|
+
colors[idx * 3 + 2] = b;
|
|
2235
|
+
}
|
|
2236
|
+
baseGeometry.setAttribute("color", new BufferAttribute(colors, 3));
|
|
2237
|
+
return baseGeometry;
|
|
2238
|
+
}
|
|
2239
|
+
function exportQualityGeometry(mesh) {
|
|
2240
|
+
const faces = mesh.getFaces();
|
|
2241
|
+
if (faces.length === 0) {
|
|
2242
|
+
return new BufferGeometry();
|
|
2243
|
+
}
|
|
2244
|
+
const positions = new Float32Array(faces.length * 9);
|
|
2245
|
+
const colors = new Float32Array(faces.length * 9);
|
|
2246
|
+
const normals = new Float32Array(faces.length * 9);
|
|
2247
|
+
let offset = 0;
|
|
2248
|
+
for (const face of faces) {
|
|
2249
|
+
const verts = face.getVertices();
|
|
2250
|
+
const normal = face.getNormal();
|
|
2251
|
+
const quality = face.getQuality() ?? 0;
|
|
2252
|
+
if (!verts || !normal) continue;
|
|
2253
|
+
const r = quality < 0.5 ? 1 : 2 - quality * 2;
|
|
2254
|
+
const g = quality < 0.5 ? quality * 2 : 1;
|
|
2255
|
+
const b = 0.1;
|
|
2256
|
+
for (const v of verts) {
|
|
2257
|
+
positions[offset] = v.position.x;
|
|
2258
|
+
positions[offset + 1] = v.position.y;
|
|
2259
|
+
positions[offset + 2] = v.position.z;
|
|
2260
|
+
colors[offset] = r;
|
|
2261
|
+
colors[offset + 1] = g;
|
|
2262
|
+
colors[offset + 2] = b;
|
|
2263
|
+
normals[offset] = normal.x;
|
|
2264
|
+
normals[offset + 1] = normal.y;
|
|
2265
|
+
normals[offset + 2] = normal.z;
|
|
2266
|
+
offset += 3;
|
|
2267
|
+
}
|
|
2268
|
+
}
|
|
2269
|
+
const geometry = new BufferGeometry();
|
|
2270
|
+
geometry.setAttribute("position", new BufferAttribute(positions.slice(0, offset), 3));
|
|
2271
|
+
geometry.setAttribute("color", new BufferAttribute(colors.slice(0, offset), 3));
|
|
2272
|
+
geometry.setAttribute("normal", new BufferAttribute(normals.slice(0, offset), 3));
|
|
2273
|
+
return geometry;
|
|
2274
|
+
}
|
|
2275
|
+
class BufferGeometryExporter {
|
|
2276
|
+
constructor(options = {}) {
|
|
2277
|
+
this.options = options;
|
|
2278
|
+
}
|
|
2279
|
+
/**
|
|
2280
|
+
* Exports a NonManifoldMesh to BufferGeometry.
|
|
2281
|
+
*/
|
|
2282
|
+
export(mesh) {
|
|
2283
|
+
return exportBufferGeometry(mesh, this.options);
|
|
2284
|
+
}
|
|
2285
|
+
/**
|
|
2286
|
+
* Exports skeleton edges as LineSegments geometry.
|
|
2287
|
+
*/
|
|
2288
|
+
exportSkeleton(mesh) {
|
|
2289
|
+
return exportSkeletonGeometry(mesh);
|
|
2290
|
+
}
|
|
2291
|
+
/**
|
|
2292
|
+
* Exports with vertex classification colors.
|
|
2293
|
+
*/
|
|
2294
|
+
exportClassification(mesh) {
|
|
2295
|
+
return exportClassificationGeometry(mesh);
|
|
2296
|
+
}
|
|
2297
|
+
/**
|
|
2298
|
+
* Exports with quality visualization colors.
|
|
2299
|
+
*/
|
|
2300
|
+
exportQuality(mesh) {
|
|
2301
|
+
return exportQualityGeometry(mesh);
|
|
2302
|
+
}
|
|
2303
|
+
/**
|
|
2304
|
+
* Sets whether to compute normals.
|
|
2305
|
+
*/
|
|
2306
|
+
setComputeNormals(enabled) {
|
|
2307
|
+
this.options.computeNormals = enabled;
|
|
2308
|
+
return this;
|
|
2309
|
+
}
|
|
2310
|
+
/**
|
|
2311
|
+
* Sets whether to use smooth normals.
|
|
2312
|
+
*/
|
|
2313
|
+
setSmoothNormals(enabled) {
|
|
2314
|
+
this.options.smoothNormals = enabled;
|
|
2315
|
+
return this;
|
|
2316
|
+
}
|
|
2317
|
+
}
|
|
2318
|
+
class SpatialHash {
|
|
2319
|
+
/**
|
|
2320
|
+
* Creates a new spatial hash with the specified cell size.
|
|
2321
|
+
*
|
|
2322
|
+
* @param cellSize - The size of each cell in the grid
|
|
2323
|
+
*/
|
|
2324
|
+
constructor(cellSize) {
|
|
2325
|
+
this.cells = /* @__PURE__ */ new Map();
|
|
2326
|
+
this.itemPositions = /* @__PURE__ */ new Map();
|
|
2327
|
+
if (cellSize <= 0) {
|
|
2328
|
+
throw new Error("Cell size must be positive");
|
|
2329
|
+
}
|
|
2330
|
+
this.cellSize = cellSize;
|
|
2331
|
+
}
|
|
2332
|
+
/**
|
|
2333
|
+
* Computes the cell key for a position.
|
|
2334
|
+
*/
|
|
2335
|
+
getCellKey(x, y, z) {
|
|
2336
|
+
const ix = Math.floor(x / this.cellSize);
|
|
2337
|
+
const iy = Math.floor(y / this.cellSize);
|
|
2338
|
+
const iz = Math.floor(z / this.cellSize);
|
|
2339
|
+
return `${ix},${iy},${iz}`;
|
|
2340
|
+
}
|
|
2341
|
+
/**
|
|
2342
|
+
* Computes the cell indices for a position.
|
|
2343
|
+
*/
|
|
2344
|
+
getCellIndices(x, y, z) {
|
|
2345
|
+
return [
|
|
2346
|
+
Math.floor(x / this.cellSize),
|
|
2347
|
+
Math.floor(y / this.cellSize),
|
|
2348
|
+
Math.floor(z / this.cellSize)
|
|
2349
|
+
];
|
|
2350
|
+
}
|
|
2351
|
+
/**
|
|
2352
|
+
* Inserts an item at the specified position.
|
|
2353
|
+
*
|
|
2354
|
+
* @param item - The item to insert
|
|
2355
|
+
* @param position - The 3D position of the item
|
|
2356
|
+
*/
|
|
2357
|
+
insert(item, position) {
|
|
2358
|
+
const key = this.getCellKey(position.x, position.y, position.z);
|
|
2359
|
+
let cell = this.cells.get(key);
|
|
2360
|
+
if (!cell) {
|
|
2361
|
+
cell = [];
|
|
2362
|
+
this.cells.set(key, cell);
|
|
2363
|
+
}
|
|
2364
|
+
cell.push(item);
|
|
2365
|
+
this.itemPositions.set(item, position);
|
|
2366
|
+
}
|
|
2367
|
+
/**
|
|
2368
|
+
* Removes an item from the hash.
|
|
2369
|
+
*
|
|
2370
|
+
* @param item - The item to remove
|
|
2371
|
+
* @returns True if the item was found and removed
|
|
2372
|
+
*/
|
|
2373
|
+
remove(item) {
|
|
2374
|
+
const position = this.itemPositions.get(item);
|
|
2375
|
+
if (!position) {
|
|
2376
|
+
return false;
|
|
2377
|
+
}
|
|
2378
|
+
const key = this.getCellKey(position.x, position.y, position.z);
|
|
2379
|
+
const cell = this.cells.get(key);
|
|
2380
|
+
if (cell) {
|
|
2381
|
+
const index = cell.indexOf(item);
|
|
2382
|
+
if (index !== -1) {
|
|
2383
|
+
cell.splice(index, 1);
|
|
2384
|
+
if (cell.length === 0) {
|
|
2385
|
+
this.cells.delete(key);
|
|
2386
|
+
}
|
|
2387
|
+
}
|
|
2388
|
+
}
|
|
2389
|
+
this.itemPositions.delete(item);
|
|
2390
|
+
return true;
|
|
2391
|
+
}
|
|
2392
|
+
/**
|
|
2393
|
+
* Updates an item's position in the hash.
|
|
2394
|
+
*
|
|
2395
|
+
* @param item - The item to update
|
|
2396
|
+
* @param newPosition - The new position
|
|
2397
|
+
*/
|
|
2398
|
+
update(item, newPosition) {
|
|
2399
|
+
this.remove(item);
|
|
2400
|
+
this.insert(item, newPosition);
|
|
2401
|
+
}
|
|
2402
|
+
/**
|
|
2403
|
+
* Queries all items within a radius of a point.
|
|
2404
|
+
*
|
|
2405
|
+
* @param center - The center point of the query
|
|
2406
|
+
* @param radius - The search radius
|
|
2407
|
+
* @returns Array of items within the radius
|
|
2408
|
+
*/
|
|
2409
|
+
queryRadius(center, radius) {
|
|
2410
|
+
const results = [];
|
|
2411
|
+
const radiusSquared = radius * radius;
|
|
2412
|
+
const minCell = this.getCellIndices(center.x - radius, center.y - radius, center.z - radius);
|
|
2413
|
+
const maxCell = this.getCellIndices(center.x + radius, center.y + radius, center.z + radius);
|
|
2414
|
+
for (let ix = minCell[0]; ix <= maxCell[0]; ix++) {
|
|
2415
|
+
for (let iy = minCell[1]; iy <= maxCell[1]; iy++) {
|
|
2416
|
+
for (let iz = minCell[2]; iz <= maxCell[2]; iz++) {
|
|
2417
|
+
const key = `${ix},${iy},${iz}`;
|
|
2418
|
+
const cell = this.cells.get(key);
|
|
2419
|
+
if (cell) {
|
|
2420
|
+
for (const item of cell) {
|
|
2421
|
+
const pos = this.itemPositions.get(item);
|
|
2422
|
+
if (pos) {
|
|
2423
|
+
const dx = pos.x - center.x;
|
|
2424
|
+
const dy = pos.y - center.y;
|
|
2425
|
+
const dz = pos.z - center.z;
|
|
2426
|
+
const distSq = dx * dx + dy * dy + dz * dz;
|
|
2427
|
+
if (distSq <= radiusSquared) {
|
|
2428
|
+
results.push(item);
|
|
2429
|
+
}
|
|
2430
|
+
}
|
|
2431
|
+
}
|
|
2432
|
+
}
|
|
2433
|
+
}
|
|
2434
|
+
}
|
|
2435
|
+
}
|
|
2436
|
+
return results;
|
|
2437
|
+
}
|
|
2438
|
+
/**
|
|
2439
|
+
* Queries the k nearest neighbors to a point.
|
|
2440
|
+
*
|
|
2441
|
+
* @param center - The center point of the query
|
|
2442
|
+
* @param k - The number of neighbors to find
|
|
2443
|
+
* @param maxRadius - Optional maximum search radius
|
|
2444
|
+
* @returns Array of the k nearest items, sorted by distance
|
|
2445
|
+
*/
|
|
2446
|
+
queryKNearest(center, k, maxRadius) {
|
|
2447
|
+
let radius = this.cellSize;
|
|
2448
|
+
let results = [];
|
|
2449
|
+
while (results.length < k) {
|
|
2450
|
+
if (maxRadius !== void 0 && radius > maxRadius) {
|
|
2451
|
+
break;
|
|
2452
|
+
}
|
|
2453
|
+
results = [];
|
|
2454
|
+
const candidates = this.queryRadius(center, radius);
|
|
2455
|
+
for (const item of candidates) {
|
|
2456
|
+
const pos = this.itemPositions.get(item);
|
|
2457
|
+
if (pos) {
|
|
2458
|
+
const dx = pos.x - center.x;
|
|
2459
|
+
const dy = pos.y - center.y;
|
|
2460
|
+
const dz = pos.z - center.z;
|
|
2461
|
+
results.push({ item, distSq: dx * dx + dy * dy + dz * dz });
|
|
2462
|
+
}
|
|
2463
|
+
}
|
|
2464
|
+
radius *= 2;
|
|
2465
|
+
}
|
|
2466
|
+
results.sort((a, b) => a.distSq - b.distSq);
|
|
2467
|
+
return results.slice(0, k).map((r) => r.item);
|
|
2468
|
+
}
|
|
2469
|
+
/**
|
|
2470
|
+
* Clears all items from the hash.
|
|
2471
|
+
*/
|
|
2472
|
+
clear() {
|
|
2473
|
+
this.cells.clear();
|
|
2474
|
+
this.itemPositions.clear();
|
|
2475
|
+
}
|
|
2476
|
+
/**
|
|
2477
|
+
* Gets the number of items in the hash.
|
|
2478
|
+
*/
|
|
2479
|
+
get size() {
|
|
2480
|
+
return this.itemPositions.size;
|
|
2481
|
+
}
|
|
2482
|
+
/**
|
|
2483
|
+
* Gets the number of non-empty cells.
|
|
2484
|
+
*/
|
|
2485
|
+
get cellCount() {
|
|
2486
|
+
return this.cells.size;
|
|
2487
|
+
}
|
|
2488
|
+
/**
|
|
2489
|
+
* Gets the position of an item.
|
|
2490
|
+
*/
|
|
2491
|
+
getPosition(item) {
|
|
2492
|
+
return this.itemPositions.get(item);
|
|
2493
|
+
}
|
|
2494
|
+
/**
|
|
2495
|
+
* Checks if an item is in the hash.
|
|
2496
|
+
*/
|
|
2497
|
+
has(item) {
|
|
2498
|
+
return this.itemPositions.has(item);
|
|
2499
|
+
}
|
|
2500
|
+
/**
|
|
2501
|
+
* Iterates over all items in the hash.
|
|
2502
|
+
*/
|
|
2503
|
+
*[Symbol.iterator]() {
|
|
2504
|
+
for (const item of this.itemPositions.keys()) {
|
|
2505
|
+
yield item;
|
|
2506
|
+
}
|
|
2507
|
+
}
|
|
2508
|
+
/**
|
|
2509
|
+
* Gets all items in the hash.
|
|
2510
|
+
*/
|
|
2511
|
+
getAll() {
|
|
2512
|
+
return Array.from(this.itemPositions.keys());
|
|
2513
|
+
}
|
|
2514
|
+
}
|
|
2515
|
+
function createSpatialHash(items, getPosition, cellSize) {
|
|
2516
|
+
let computedCellSize = cellSize;
|
|
2517
|
+
if (computedCellSize === void 0 && items.length > 1) {
|
|
2518
|
+
let minX = Infinity, minY = Infinity, minZ = Infinity;
|
|
2519
|
+
let maxX = -Infinity, maxY = -Infinity, maxZ = -Infinity;
|
|
2520
|
+
for (const item of items) {
|
|
2521
|
+
const pos = getPosition(item);
|
|
2522
|
+
minX = Math.min(minX, pos.x);
|
|
2523
|
+
minY = Math.min(minY, pos.y);
|
|
2524
|
+
minZ = Math.min(minZ, pos.z);
|
|
2525
|
+
maxX = Math.max(maxX, pos.x);
|
|
2526
|
+
maxY = Math.max(maxY, pos.y);
|
|
2527
|
+
maxZ = Math.max(maxZ, pos.z);
|
|
2528
|
+
}
|
|
2529
|
+
const diagonal = Math.sqrt((maxX - minX) ** 2 + (maxY - minY) ** 2 + (maxZ - minZ) ** 2);
|
|
2530
|
+
computedCellSize = diagonal / Math.sqrt(items.length);
|
|
2531
|
+
}
|
|
2532
|
+
const hash = new SpatialHash(computedCellSize ?? 1);
|
|
2533
|
+
for (const item of items) {
|
|
2534
|
+
hash.insert(item, getPosition(item));
|
|
2535
|
+
}
|
|
2536
|
+
return hash;
|
|
2537
|
+
}
|
|
2538
|
+
function triangleBounds(tri) {
|
|
2539
|
+
return {
|
|
2540
|
+
min: {
|
|
2541
|
+
x: Math.min(tri.v0.x, tri.v1.x, tri.v2.x),
|
|
2542
|
+
y: Math.min(tri.v0.y, tri.v1.y, tri.v2.y),
|
|
2543
|
+
z: Math.min(tri.v0.z, tri.v1.z, tri.v2.z)
|
|
2544
|
+
},
|
|
2545
|
+
max: {
|
|
2546
|
+
x: Math.max(tri.v0.x, tri.v1.x, tri.v2.x),
|
|
2547
|
+
y: Math.max(tri.v0.y, tri.v1.y, tri.v2.y),
|
|
2548
|
+
z: Math.max(tri.v0.z, tri.v1.z, tri.v2.z)
|
|
2549
|
+
}
|
|
2550
|
+
};
|
|
2551
|
+
}
|
|
2552
|
+
function mergeBounds(a, b) {
|
|
2553
|
+
return {
|
|
2554
|
+
min: {
|
|
2555
|
+
x: Math.min(a.min.x, b.min.x),
|
|
2556
|
+
y: Math.min(a.min.y, b.min.y),
|
|
2557
|
+
z: Math.min(a.min.z, b.min.z)
|
|
2558
|
+
},
|
|
2559
|
+
max: {
|
|
2560
|
+
x: Math.max(a.max.x, b.max.x),
|
|
2561
|
+
y: Math.max(a.max.y, b.max.y),
|
|
2562
|
+
z: Math.max(a.max.z, b.max.z)
|
|
2563
|
+
}
|
|
2564
|
+
};
|
|
2565
|
+
}
|
|
2566
|
+
function triangleCentroid(tri) {
|
|
2567
|
+
return {
|
|
2568
|
+
x: (tri.v0.x + tri.v1.x + tri.v2.x) / 3,
|
|
2569
|
+
y: (tri.v0.y + tri.v1.y + tri.v2.y) / 3,
|
|
2570
|
+
z: (tri.v0.z + tri.v1.z + tri.v2.z) / 3
|
|
2571
|
+
};
|
|
2572
|
+
}
|
|
2573
|
+
function pointToAABBDistanceSquared(point, box) {
|
|
2574
|
+
let distSq = 0;
|
|
2575
|
+
if (point.x < box.min.x) {
|
|
2576
|
+
distSq += (box.min.x - point.x) ** 2;
|
|
2577
|
+
} else if (point.x > box.max.x) {
|
|
2578
|
+
distSq += (point.x - box.max.x) ** 2;
|
|
2579
|
+
}
|
|
2580
|
+
if (point.y < box.min.y) {
|
|
2581
|
+
distSq += (box.min.y - point.y) ** 2;
|
|
2582
|
+
} else if (point.y > box.max.y) {
|
|
2583
|
+
distSq += (point.y - box.max.y) ** 2;
|
|
2584
|
+
}
|
|
2585
|
+
if (point.z < box.min.z) {
|
|
2586
|
+
distSq += (box.min.z - point.z) ** 2;
|
|
2587
|
+
} else if (point.z > box.max.z) {
|
|
2588
|
+
distSq += (point.z - box.max.z) ** 2;
|
|
2589
|
+
}
|
|
2590
|
+
return distSq;
|
|
2591
|
+
}
|
|
2592
|
+
function closestPointOnTriangle(point, tri) {
|
|
2593
|
+
const a = tri.v0;
|
|
2594
|
+
const b = tri.v1;
|
|
2595
|
+
const c = tri.v2;
|
|
2596
|
+
const ab = { x: b.x - a.x, y: b.y - a.y, z: b.z - a.z };
|
|
2597
|
+
const ac = { x: c.x - a.x, y: c.y - a.y, z: c.z - a.z };
|
|
2598
|
+
const ap = { x: point.x - a.x, y: point.y - a.y, z: point.z - a.z };
|
|
2599
|
+
const d1 = ab.x * ap.x + ab.y * ap.y + ab.z * ap.z;
|
|
2600
|
+
const d2 = ac.x * ap.x + ac.y * ap.y + ac.z * ap.z;
|
|
2601
|
+
if (d1 <= 0 && d2 <= 0) return a;
|
|
2602
|
+
const bp = { x: point.x - b.x, y: point.y - b.y, z: point.z - b.z };
|
|
2603
|
+
const d3 = ab.x * bp.x + ab.y * bp.y + ab.z * bp.z;
|
|
2604
|
+
const d4 = ac.x * bp.x + ac.y * bp.y + ac.z * bp.z;
|
|
2605
|
+
if (d3 >= 0 && d4 <= d3) return b;
|
|
2606
|
+
const vc = d1 * d4 - d3 * d2;
|
|
2607
|
+
if (vc <= 0 && d1 >= 0 && d3 <= 0) {
|
|
2608
|
+
const v2 = d1 / (d1 - d3);
|
|
2609
|
+
return { x: a.x + v2 * ab.x, y: a.y + v2 * ab.y, z: a.z + v2 * ab.z };
|
|
2610
|
+
}
|
|
2611
|
+
const cp = { x: point.x - c.x, y: point.y - c.y, z: point.z - c.z };
|
|
2612
|
+
const d5 = ab.x * cp.x + ab.y * cp.y + ab.z * cp.z;
|
|
2613
|
+
const d6 = ac.x * cp.x + ac.y * cp.y + ac.z * cp.z;
|
|
2614
|
+
if (d6 >= 0 && d5 <= d6) return c;
|
|
2615
|
+
const vb = d5 * d2 - d1 * d6;
|
|
2616
|
+
if (vb <= 0 && d2 >= 0 && d6 <= 0) {
|
|
2617
|
+
const w2 = d2 / (d2 - d6);
|
|
2618
|
+
return { x: a.x + w2 * ac.x, y: a.y + w2 * ac.y, z: a.z + w2 * ac.z };
|
|
2619
|
+
}
|
|
2620
|
+
const va = d3 * d6 - d5 * d4;
|
|
2621
|
+
if (va <= 0 && d4 - d3 >= 0 && d5 - d6 >= 0) {
|
|
2622
|
+
const w2 = (d4 - d3) / (d4 - d3 + (d5 - d6));
|
|
2623
|
+
return {
|
|
2624
|
+
x: b.x + w2 * (c.x - b.x),
|
|
2625
|
+
y: b.y + w2 * (c.y - b.y),
|
|
2626
|
+
z: b.z + w2 * (c.z - b.z)
|
|
2627
|
+
};
|
|
2628
|
+
}
|
|
2629
|
+
const denom = 1 / (va + vb + vc);
|
|
2630
|
+
const v = vb * denom;
|
|
2631
|
+
const w = vc * denom;
|
|
2632
|
+
return {
|
|
2633
|
+
x: a.x + ab.x * v + ac.x * w,
|
|
2634
|
+
y: a.y + ab.y * v + ac.y * w,
|
|
2635
|
+
z: a.z + ab.z * v + ac.z * w
|
|
2636
|
+
};
|
|
2637
|
+
}
|
|
2638
|
+
class BVH {
|
|
2639
|
+
/**
|
|
2640
|
+
* Creates a new BVH.
|
|
2641
|
+
*
|
|
2642
|
+
* @param maxLeafSize - Maximum number of triangles per leaf node
|
|
2643
|
+
*/
|
|
2644
|
+
constructor(maxLeafSize = 4) {
|
|
2645
|
+
this.root = null;
|
|
2646
|
+
this.triangles = [];
|
|
2647
|
+
this.maxLeafSize = maxLeafSize;
|
|
2648
|
+
}
|
|
2649
|
+
/**
|
|
2650
|
+
* Builds the BVH from an array of triangles.
|
|
2651
|
+
*
|
|
2652
|
+
* @param triangles - Array of triangles to build from
|
|
2653
|
+
*/
|
|
2654
|
+
build(triangles) {
|
|
2655
|
+
this.triangles = triangles;
|
|
2656
|
+
if (triangles.length === 0) {
|
|
2657
|
+
this.root = null;
|
|
2658
|
+
return;
|
|
2659
|
+
}
|
|
2660
|
+
const indices = triangles.map((_, i) => i);
|
|
2661
|
+
this.root = this.buildNode(indices);
|
|
2662
|
+
}
|
|
2663
|
+
/**
|
|
2664
|
+
* Recursively builds a BVH node.
|
|
2665
|
+
*/
|
|
2666
|
+
buildNode(indices) {
|
|
2667
|
+
let bounds = triangleBounds(this.triangles[indices[0]]);
|
|
2668
|
+
for (let i = 1; i < indices.length; i++) {
|
|
2669
|
+
bounds = mergeBounds(bounds, triangleBounds(this.triangles[indices[i]]));
|
|
2670
|
+
}
|
|
2671
|
+
if (indices.length <= this.maxLeafSize) {
|
|
2672
|
+
return { bounds, triangleIndices: indices };
|
|
2673
|
+
}
|
|
2674
|
+
const extents = {
|
|
2675
|
+
x: bounds.max.x - bounds.min.x,
|
|
2676
|
+
y: bounds.max.y - bounds.min.y,
|
|
2677
|
+
z: bounds.max.z - bounds.min.z
|
|
2678
|
+
};
|
|
2679
|
+
let axis = "x";
|
|
2680
|
+
if (extents.y > extents.x && extents.y > extents.z) axis = "y";
|
|
2681
|
+
else if (extents.z > extents.x && extents.z > extents.y) axis = "z";
|
|
2682
|
+
const centroids = indices.map((i) => ({
|
|
2683
|
+
index: i,
|
|
2684
|
+
centroid: triangleCentroid(this.triangles[i])
|
|
2685
|
+
}));
|
|
2686
|
+
centroids.sort((a, b) => a.centroid[axis] - b.centroid[axis]);
|
|
2687
|
+
const mid = Math.floor(centroids.length / 2);
|
|
2688
|
+
const leftIndices = centroids.slice(0, mid).map((c) => c.index);
|
|
2689
|
+
const rightIndices = centroids.slice(mid).map((c) => c.index);
|
|
2690
|
+
if (leftIndices.length === 0 || rightIndices.length === 0) {
|
|
2691
|
+
return { bounds, triangleIndices: indices };
|
|
2692
|
+
}
|
|
2693
|
+
return {
|
|
2694
|
+
bounds,
|
|
2695
|
+
left: this.buildNode(leftIndices),
|
|
2696
|
+
right: this.buildNode(rightIndices)
|
|
2697
|
+
};
|
|
2698
|
+
}
|
|
2699
|
+
/**
|
|
2700
|
+
* Finds the closest point on the mesh to a query point.
|
|
2701
|
+
*
|
|
2702
|
+
* @param point - The query point
|
|
2703
|
+
* @returns The closest point result, or null if no triangles
|
|
2704
|
+
*/
|
|
2705
|
+
closestPoint(point) {
|
|
2706
|
+
if (!this.root || this.triangles.length === 0) {
|
|
2707
|
+
return null;
|
|
2708
|
+
}
|
|
2709
|
+
let bestResult = null;
|
|
2710
|
+
let bestDistSq = Infinity;
|
|
2711
|
+
const stack = [this.root];
|
|
2712
|
+
while (stack.length > 0) {
|
|
2713
|
+
const node = stack.pop();
|
|
2714
|
+
const boxDistSq = pointToAABBDistanceSquared(point, node.bounds);
|
|
2715
|
+
if (boxDistSq >= bestDistSq) {
|
|
2716
|
+
continue;
|
|
2717
|
+
}
|
|
2718
|
+
if (node.triangleIndices) {
|
|
2719
|
+
for (const idx of node.triangleIndices) {
|
|
2720
|
+
const tri = this.triangles[idx];
|
|
2721
|
+
const closest = closestPointOnTriangle(point, tri);
|
|
2722
|
+
const distSq = distanceSquared(point, closest);
|
|
2723
|
+
if (distSq < bestDistSq) {
|
|
2724
|
+
bestDistSq = distSq;
|
|
2725
|
+
bestResult = {
|
|
2726
|
+
point: closest,
|
|
2727
|
+
distance: Math.sqrt(distSq),
|
|
2728
|
+
triangleIndex: idx
|
|
2729
|
+
};
|
|
2730
|
+
}
|
|
2731
|
+
}
|
|
2732
|
+
} else {
|
|
2733
|
+
if (node.left && node.right) {
|
|
2734
|
+
const leftDist = pointToAABBDistanceSquared(point, node.left.bounds);
|
|
2735
|
+
const rightDist = pointToAABBDistanceSquared(point, node.right.bounds);
|
|
2736
|
+
if (leftDist < rightDist) {
|
|
2737
|
+
stack.push(node.right);
|
|
2738
|
+
stack.push(node.left);
|
|
2739
|
+
} else {
|
|
2740
|
+
stack.push(node.left);
|
|
2741
|
+
stack.push(node.right);
|
|
2742
|
+
}
|
|
2743
|
+
} else if (node.left) {
|
|
2744
|
+
stack.push(node.left);
|
|
2745
|
+
} else if (node.right) {
|
|
2746
|
+
stack.push(node.right);
|
|
2747
|
+
}
|
|
2748
|
+
}
|
|
2749
|
+
}
|
|
2750
|
+
return bestResult;
|
|
2751
|
+
}
|
|
2752
|
+
/**
|
|
2753
|
+
* Finds all triangles intersecting a sphere.
|
|
2754
|
+
*
|
|
2755
|
+
* @param center - Center of the sphere
|
|
2756
|
+
* @param radius - Radius of the sphere
|
|
2757
|
+
* @returns Array of triangle indices
|
|
2758
|
+
*/
|
|
2759
|
+
queryRadius(center, radius) {
|
|
2760
|
+
const results = [];
|
|
2761
|
+
const radiusSq = radius * radius;
|
|
2762
|
+
if (!this.root) {
|
|
2763
|
+
return results;
|
|
2764
|
+
}
|
|
2765
|
+
const stack = [this.root];
|
|
2766
|
+
while (stack.length > 0) {
|
|
2767
|
+
const node = stack.pop();
|
|
2768
|
+
const boxDistSq = pointToAABBDistanceSquared(center, node.bounds);
|
|
2769
|
+
if (boxDistSq > radiusSq) {
|
|
2770
|
+
continue;
|
|
2771
|
+
}
|
|
2772
|
+
if (node.triangleIndices) {
|
|
2773
|
+
for (const idx of node.triangleIndices) {
|
|
2774
|
+
const tri = this.triangles[idx];
|
|
2775
|
+
const closest = closestPointOnTriangle(center, tri);
|
|
2776
|
+
const distSq = distanceSquared(center, closest);
|
|
2777
|
+
if (distSq <= radiusSq) {
|
|
2778
|
+
results.push(idx);
|
|
2779
|
+
}
|
|
2780
|
+
}
|
|
2781
|
+
} else {
|
|
2782
|
+
if (node.left) stack.push(node.left);
|
|
2783
|
+
if (node.right) stack.push(node.right);
|
|
2784
|
+
}
|
|
2785
|
+
}
|
|
2786
|
+
return results;
|
|
2787
|
+
}
|
|
2788
|
+
/**
|
|
2789
|
+
* Gets the total number of triangles.
|
|
2790
|
+
*/
|
|
2791
|
+
get triangleCount() {
|
|
2792
|
+
return this.triangles.length;
|
|
2793
|
+
}
|
|
2794
|
+
/**
|
|
2795
|
+
* Gets a triangle by index.
|
|
2796
|
+
*/
|
|
2797
|
+
getTriangle(index) {
|
|
2798
|
+
return this.triangles[index];
|
|
2799
|
+
}
|
|
2800
|
+
}
|
|
2801
|
+
function createBVHFromMesh(mesh) {
|
|
2802
|
+
const triangles = [];
|
|
2803
|
+
for (const face of mesh.getFaces()) {
|
|
2804
|
+
const verts = face.getVertices();
|
|
2805
|
+
if (!verts) continue;
|
|
2806
|
+
triangles.push({
|
|
2807
|
+
v0: { x: verts[0].position.x, y: verts[0].position.y, z: verts[0].position.z },
|
|
2808
|
+
v1: { x: verts[1].position.x, y: verts[1].position.y, z: verts[1].position.z },
|
|
2809
|
+
v2: { x: verts[2].position.x, y: verts[2].position.y, z: verts[2].position.z }
|
|
2810
|
+
});
|
|
2811
|
+
}
|
|
2812
|
+
const bvh = new BVH();
|
|
2813
|
+
bvh.build(triangles);
|
|
2814
|
+
return bvh;
|
|
2815
|
+
}
|
|
2816
|
+
function analyzeManifold(geometry) {
|
|
2817
|
+
const mesh = NonManifoldMesh.fromBufferGeometry(geometry);
|
|
2818
|
+
return analyzeMesh(mesh);
|
|
2819
|
+
}
|
|
2820
|
+
function analyzeMesh(mesh) {
|
|
2821
|
+
const edges = mesh.getEdges();
|
|
2822
|
+
const vertices = mesh.getVertices();
|
|
2823
|
+
let manifoldEdgeCount = 0;
|
|
2824
|
+
let nonManifoldEdgeCount = 0;
|
|
2825
|
+
let boundaryEdgeCount = 0;
|
|
2826
|
+
const nonManifoldEdges = [];
|
|
2827
|
+
for (const edge of edges) {
|
|
2828
|
+
switch (edge.type) {
|
|
2829
|
+
case EdgeType.Manifold:
|
|
2830
|
+
case EdgeType.Feature:
|
|
2831
|
+
manifoldEdgeCount++;
|
|
2832
|
+
break;
|
|
2833
|
+
case EdgeType.NonManifold:
|
|
2834
|
+
nonManifoldEdgeCount++;
|
|
2835
|
+
nonManifoldEdges.push(extractEdgeInfo(edge));
|
|
2836
|
+
break;
|
|
2837
|
+
case EdgeType.Boundary:
|
|
2838
|
+
boundaryEdgeCount++;
|
|
2839
|
+
break;
|
|
2840
|
+
}
|
|
2841
|
+
}
|
|
2842
|
+
let manifoldVertexCount = 0;
|
|
2843
|
+
let nonManifoldVertexCount = 0;
|
|
2844
|
+
let totalDegree = 0;
|
|
2845
|
+
const nonManifoldVertices = [];
|
|
2846
|
+
for (const vertex of vertices) {
|
|
2847
|
+
const degree = vertex.degree() ?? 0;
|
|
2848
|
+
totalDegree += degree;
|
|
2849
|
+
if (vertex.type === VertexType.Manifold) {
|
|
2850
|
+
manifoldVertexCount++;
|
|
2851
|
+
} else {
|
|
2852
|
+
nonManifoldVertexCount++;
|
|
2853
|
+
nonManifoldVertices.push(extractVertexInfo(vertex));
|
|
2854
|
+
}
|
|
2855
|
+
}
|
|
2856
|
+
const averageVertexDegree = vertices.length > 0 ? totalDegree / vertices.length : 0;
|
|
2857
|
+
const eulerCharacteristic = mesh.vertexCount - mesh.edgeCount + mesh.faceCount;
|
|
2858
|
+
return {
|
|
2859
|
+
isManifold: nonManifoldEdgeCount === 0,
|
|
2860
|
+
hasBoundary: boundaryEdgeCount > 0,
|
|
2861
|
+
vertexCount: mesh.vertexCount,
|
|
2862
|
+
edgeCount: mesh.edgeCount,
|
|
2863
|
+
faceCount: mesh.faceCount,
|
|
2864
|
+
manifoldEdgeCount,
|
|
2865
|
+
nonManifoldEdgeCount,
|
|
2866
|
+
boundaryEdgeCount,
|
|
2867
|
+
manifoldVertexCount,
|
|
2868
|
+
nonManifoldVertexCount,
|
|
2869
|
+
nonManifoldEdges,
|
|
2870
|
+
nonManifoldVertices,
|
|
2871
|
+
eulerCharacteristic,
|
|
2872
|
+
averageVertexDegree
|
|
2873
|
+
};
|
|
2874
|
+
}
|
|
2875
|
+
function extractEdgeInfo(edge) {
|
|
2876
|
+
const [v0, v1] = edge.getVertices();
|
|
2877
|
+
return {
|
|
2878
|
+
edgeId: edge.id,
|
|
2879
|
+
vertexIndices: [v0 ? v0.id : -1, v1 ? v1.id : -1],
|
|
2880
|
+
faceCount: edge.getFaceCount(),
|
|
2881
|
+
positions: [
|
|
2882
|
+
v0 ? { x: v0.position.x, y: v0.position.y, z: v0.position.z } : { x: 0, y: 0, z: 0 },
|
|
2883
|
+
v1 ? { x: v1.position.x, y: v1.position.y, z: v1.position.z } : { x: 0, y: 0, z: 0 }
|
|
2884
|
+
]
|
|
2885
|
+
};
|
|
2886
|
+
}
|
|
2887
|
+
function extractVertexInfo(vertex) {
|
|
2888
|
+
let skeletonEdgeCount = 0;
|
|
2889
|
+
vertex.forEachOutgoingHalfedge((he) => {
|
|
2890
|
+
if (he.edge.isSkeletonEdge()) {
|
|
2891
|
+
skeletonEdgeCount++;
|
|
2892
|
+
}
|
|
2893
|
+
});
|
|
2894
|
+
return {
|
|
2895
|
+
vertexId: vertex.id,
|
|
2896
|
+
position: { x: vertex.position.x, y: vertex.position.y, z: vertex.position.z },
|
|
2897
|
+
type: vertex.type,
|
|
2898
|
+
skeletonEdgeCount
|
|
2899
|
+
};
|
|
2900
|
+
}
|
|
2901
|
+
function isManifold(geometry) {
|
|
2902
|
+
const mesh = NonManifoldMesh.fromBufferGeometry(geometry);
|
|
2903
|
+
return mesh.isManifold();
|
|
2904
|
+
}
|
|
2905
|
+
class ManifoldAnalyzer {
|
|
2906
|
+
constructor() {
|
|
2907
|
+
this.mesh = null;
|
|
2908
|
+
this.cachedResult = null;
|
|
2909
|
+
}
|
|
2910
|
+
/**
|
|
2911
|
+
* Loads a geometry for analysis.
|
|
2912
|
+
*/
|
|
2913
|
+
load(geometry) {
|
|
2914
|
+
this.mesh = NonManifoldMesh.fromBufferGeometry(geometry);
|
|
2915
|
+
this.cachedResult = null;
|
|
2916
|
+
return this;
|
|
2917
|
+
}
|
|
2918
|
+
/**
|
|
2919
|
+
* Loads an existing mesh for analysis.
|
|
2920
|
+
*/
|
|
2921
|
+
loadMesh(mesh) {
|
|
2922
|
+
this.mesh = mesh;
|
|
2923
|
+
this.cachedResult = null;
|
|
2924
|
+
return this;
|
|
2925
|
+
}
|
|
2926
|
+
/**
|
|
2927
|
+
* Gets the analysis result (cached).
|
|
2928
|
+
*/
|
|
2929
|
+
analyze() {
|
|
2930
|
+
if (!this.mesh) {
|
|
2931
|
+
throw new Error("No mesh loaded. Call load() first.");
|
|
2932
|
+
}
|
|
2933
|
+
if (!this.cachedResult) {
|
|
2934
|
+
this.cachedResult = analyzeMesh(this.mesh);
|
|
2935
|
+
}
|
|
2936
|
+
return this.cachedResult;
|
|
2937
|
+
}
|
|
2938
|
+
/**
|
|
2939
|
+
* Checks if the mesh is manifold.
|
|
2940
|
+
*/
|
|
2941
|
+
isManifold() {
|
|
2942
|
+
return this.analyze().isManifold;
|
|
2943
|
+
}
|
|
2944
|
+
/**
|
|
2945
|
+
* Checks if the mesh has boundary.
|
|
2946
|
+
*/
|
|
2947
|
+
hasBoundary() {
|
|
2948
|
+
return this.analyze().hasBoundary;
|
|
2949
|
+
}
|
|
2950
|
+
/**
|
|
2951
|
+
* Gets non-manifold edges.
|
|
2952
|
+
*/
|
|
2953
|
+
getNonManifoldEdges() {
|
|
2954
|
+
return this.analyze().nonManifoldEdges;
|
|
2955
|
+
}
|
|
2956
|
+
/**
|
|
2957
|
+
* Gets non-manifold vertices.
|
|
2958
|
+
*/
|
|
2959
|
+
getNonManifoldVertices() {
|
|
2960
|
+
return this.analyze().nonManifoldVertices;
|
|
2961
|
+
}
|
|
2962
|
+
/**
|
|
2963
|
+
* Gets the underlying mesh.
|
|
2964
|
+
*/
|
|
2965
|
+
getMesh() {
|
|
2966
|
+
return this.mesh;
|
|
2967
|
+
}
|
|
2968
|
+
/**
|
|
2969
|
+
* Clears cached results.
|
|
2970
|
+
*/
|
|
2971
|
+
clearCache() {
|
|
2972
|
+
this.cachedResult = null;
|
|
2973
|
+
return this;
|
|
2974
|
+
}
|
|
2975
|
+
}
|
|
2976
|
+
function classifyAllVertices(mesh) {
|
|
2977
|
+
const stats = {
|
|
2978
|
+
manifold: 0,
|
|
2979
|
+
openBook: 0,
|
|
2980
|
+
skeletonBranching: 0,
|
|
2981
|
+
nonManifoldOther: 0,
|
|
2982
|
+
total: 0
|
|
2983
|
+
};
|
|
2984
|
+
for (const vertex of mesh.getVertices()) {
|
|
2985
|
+
vertex.type = classifyVertex(vertex);
|
|
2986
|
+
stats.total++;
|
|
2987
|
+
switch (vertex.type) {
|
|
2988
|
+
case VertexType.Manifold:
|
|
2989
|
+
stats.manifold++;
|
|
2990
|
+
break;
|
|
2991
|
+
case VertexType.OpenBook:
|
|
2992
|
+
stats.openBook++;
|
|
2993
|
+
break;
|
|
2994
|
+
case VertexType.SkeletonBranching:
|
|
2995
|
+
stats.skeletonBranching++;
|
|
2996
|
+
break;
|
|
2997
|
+
case VertexType.NonManifoldOther:
|
|
2998
|
+
stats.nonManifoldOther++;
|
|
2999
|
+
break;
|
|
3000
|
+
}
|
|
3001
|
+
}
|
|
3002
|
+
return stats;
|
|
3003
|
+
}
|
|
3004
|
+
function classifyVertex(vertex) {
|
|
3005
|
+
let skeletonEdgeCount = 0;
|
|
3006
|
+
let totalEdgeCount = 0;
|
|
3007
|
+
let hasBoundary = false;
|
|
3008
|
+
let hasNonManifold = false;
|
|
3009
|
+
vertex.forEachOutgoingHalfedge((he) => {
|
|
3010
|
+
totalEdgeCount++;
|
|
3011
|
+
if (he.edge.isSkeletonEdge()) {
|
|
3012
|
+
skeletonEdgeCount++;
|
|
3013
|
+
}
|
|
3014
|
+
if (he.edge.isBoundary()) {
|
|
3015
|
+
hasBoundary = true;
|
|
3016
|
+
}
|
|
3017
|
+
if (he.edge.isNonManifold()) {
|
|
3018
|
+
hasNonManifold = true;
|
|
3019
|
+
}
|
|
3020
|
+
});
|
|
3021
|
+
if (totalEdgeCount === 0) {
|
|
3022
|
+
return VertexType.Manifold;
|
|
3023
|
+
}
|
|
3024
|
+
if (hasNonManifold) {
|
|
3025
|
+
if (skeletonEdgeCount === 2) {
|
|
3026
|
+
return VertexType.OpenBook;
|
|
3027
|
+
} else if (skeletonEdgeCount === 1 || skeletonEdgeCount > 2) {
|
|
3028
|
+
return VertexType.SkeletonBranching;
|
|
3029
|
+
}
|
|
3030
|
+
return VertexType.NonManifoldOther;
|
|
3031
|
+
}
|
|
3032
|
+
if (hasBoundary) {
|
|
3033
|
+
if (skeletonEdgeCount === 2) {
|
|
3034
|
+
return VertexType.OpenBook;
|
|
3035
|
+
} else if (skeletonEdgeCount === 1 || skeletonEdgeCount > 2) {
|
|
3036
|
+
return VertexType.SkeletonBranching;
|
|
3037
|
+
}
|
|
3038
|
+
}
|
|
3039
|
+
if (skeletonEdgeCount === 0) {
|
|
3040
|
+
return VertexType.Manifold;
|
|
3041
|
+
}
|
|
3042
|
+
if (skeletonEdgeCount === 2) {
|
|
3043
|
+
return VertexType.OpenBook;
|
|
3044
|
+
}
|
|
3045
|
+
return VertexType.SkeletonBranching;
|
|
3046
|
+
}
|
|
3047
|
+
function getVerticesByType(mesh, type) {
|
|
3048
|
+
return mesh.getVertices().filter((v) => v.type === type);
|
|
3049
|
+
}
|
|
3050
|
+
function getManifoldVertices(mesh) {
|
|
3051
|
+
return getVerticesByType(mesh, VertexType.Manifold);
|
|
3052
|
+
}
|
|
3053
|
+
function getOpenBookVertices(mesh) {
|
|
3054
|
+
return getVerticesByType(mesh, VertexType.OpenBook);
|
|
3055
|
+
}
|
|
3056
|
+
function getSkeletonBranchingVertices(mesh) {
|
|
3057
|
+
return getVerticesByType(mesh, VertexType.SkeletonBranching);
|
|
3058
|
+
}
|
|
3059
|
+
function getNonManifoldVertices(mesh) {
|
|
3060
|
+
return mesh.getVertices().filter((v) => v.type !== VertexType.Manifold);
|
|
3061
|
+
}
|
|
3062
|
+
function reclassifyVertices(mesh) {
|
|
3063
|
+
mesh.classifyVertices();
|
|
3064
|
+
}
|
|
3065
|
+
class VertexClassifier {
|
|
3066
|
+
constructor(mesh) {
|
|
3067
|
+
this.mesh = mesh;
|
|
3068
|
+
}
|
|
3069
|
+
/**
|
|
3070
|
+
* Classifies all vertices and returns statistics.
|
|
3071
|
+
*/
|
|
3072
|
+
classifyAll() {
|
|
3073
|
+
return classifyAllVertices(this.mesh);
|
|
3074
|
+
}
|
|
3075
|
+
/**
|
|
3076
|
+
* Gets vertices of a specific type.
|
|
3077
|
+
*/
|
|
3078
|
+
getByType(type) {
|
|
3079
|
+
return getVerticesByType(this.mesh, type);
|
|
3080
|
+
}
|
|
3081
|
+
/**
|
|
3082
|
+
* Gets all manifold vertices.
|
|
3083
|
+
*/
|
|
3084
|
+
getManifold() {
|
|
3085
|
+
return getManifoldVertices(this.mesh);
|
|
3086
|
+
}
|
|
3087
|
+
/**
|
|
3088
|
+
* Gets all non-manifold vertices.
|
|
3089
|
+
*/
|
|
3090
|
+
getNonManifold() {
|
|
3091
|
+
return getNonManifoldVertices(this.mesh);
|
|
3092
|
+
}
|
|
3093
|
+
/**
|
|
3094
|
+
* Reclassifies all vertices.
|
|
3095
|
+
*/
|
|
3096
|
+
reclassify() {
|
|
3097
|
+
reclassifyVertices(this.mesh);
|
|
3098
|
+
}
|
|
3099
|
+
}
|
|
3100
|
+
function validateTopology(mesh) {
|
|
3101
|
+
const errors = [];
|
|
3102
|
+
const warnings = [];
|
|
3103
|
+
validateVertices(mesh, errors);
|
|
3104
|
+
validateEdges(mesh, errors, warnings);
|
|
3105
|
+
validateFaces(mesh, errors, warnings);
|
|
3106
|
+
validateHalfedgeConnectivity(mesh, errors);
|
|
3107
|
+
return {
|
|
3108
|
+
isValid: errors.length === 0,
|
|
3109
|
+
errors,
|
|
3110
|
+
warnings
|
|
3111
|
+
};
|
|
3112
|
+
}
|
|
3113
|
+
function validateVertices(mesh, errors, _warnings) {
|
|
3114
|
+
for (const vertex of mesh.getVertices()) {
|
|
3115
|
+
if (vertex.halfedge) {
|
|
3116
|
+
if (!mesh.getHalfedge(vertex.halfedge.id)) {
|
|
3117
|
+
errors.push({
|
|
3118
|
+
type: "invalid_vertex_halfedge",
|
|
3119
|
+
message: `Vertex ${vertex.id} references non-existent halfedge ${vertex.halfedge.id}`,
|
|
3120
|
+
elementIds: [vertex.id]
|
|
3121
|
+
});
|
|
3122
|
+
}
|
|
3123
|
+
}
|
|
3124
|
+
if (!isFinite(vertex.position.x) || !isFinite(vertex.position.y) || !isFinite(vertex.position.z)) {
|
|
3125
|
+
errors.push({
|
|
3126
|
+
type: "invalid_vertex_position",
|
|
3127
|
+
message: `Vertex ${vertex.id} has invalid position`,
|
|
3128
|
+
elementIds: [vertex.id]
|
|
3129
|
+
});
|
|
3130
|
+
}
|
|
3131
|
+
}
|
|
3132
|
+
}
|
|
3133
|
+
function validateEdges(mesh, errors, warnings) {
|
|
3134
|
+
for (const edge of mesh.getEdges()) {
|
|
3135
|
+
if (edge.allHalfedges.length === 0) {
|
|
3136
|
+
errors.push({
|
|
3137
|
+
type: "edge_no_halfedges",
|
|
3138
|
+
message: `Edge ${edge.id} has no halfedges`,
|
|
3139
|
+
elementIds: [edge.id]
|
|
3140
|
+
});
|
|
3141
|
+
continue;
|
|
3142
|
+
}
|
|
3143
|
+
if (!mesh.getHalfedge(edge.halfedge.id)) {
|
|
3144
|
+
errors.push({
|
|
3145
|
+
type: "invalid_edge_halfedge",
|
|
3146
|
+
message: `Edge ${edge.id} references non-existent halfedge`,
|
|
3147
|
+
elementIds: [edge.id]
|
|
3148
|
+
});
|
|
3149
|
+
}
|
|
3150
|
+
if (edge.length <= 0 || !isFinite(edge.length)) {
|
|
3151
|
+
warnings.push({
|
|
3152
|
+
type: "invalid_edge_length",
|
|
3153
|
+
message: `Edge ${edge.id} has invalid length: ${edge.length}`,
|
|
3154
|
+
elementIds: [edge.id]
|
|
3155
|
+
});
|
|
3156
|
+
}
|
|
3157
|
+
for (const he of edge.allHalfedges) {
|
|
3158
|
+
if (he.edge.id !== edge.id) {
|
|
3159
|
+
errors.push({
|
|
3160
|
+
type: "halfedge_edge_mismatch",
|
|
3161
|
+
message: `Halfedge ${he.id} in edge ${edge.id} references different edge ${he.edge.id}`,
|
|
3162
|
+
elementIds: [edge.id, he.id]
|
|
3163
|
+
});
|
|
3164
|
+
}
|
|
3165
|
+
}
|
|
3166
|
+
}
|
|
3167
|
+
}
|
|
3168
|
+
function validateFaces(mesh, errors, warnings) {
|
|
3169
|
+
var _a;
|
|
3170
|
+
for (const face of mesh.getFaces()) {
|
|
3171
|
+
if (!face.halfedge) {
|
|
3172
|
+
errors.push({
|
|
3173
|
+
type: "face_no_halfedge",
|
|
3174
|
+
message: `Face ${face.id} has no halfedge`,
|
|
3175
|
+
elementIds: [face.id]
|
|
3176
|
+
});
|
|
3177
|
+
continue;
|
|
3178
|
+
}
|
|
3179
|
+
if (!mesh.getHalfedge(face.halfedge.id)) {
|
|
3180
|
+
errors.push({
|
|
3181
|
+
type: "invalid_face_halfedge",
|
|
3182
|
+
message: `Face ${face.id} references non-existent halfedge`,
|
|
3183
|
+
elementIds: [face.id]
|
|
3184
|
+
});
|
|
3185
|
+
continue;
|
|
3186
|
+
}
|
|
3187
|
+
const halfedges = face.getHalfedges();
|
|
3188
|
+
if (!halfedges) {
|
|
3189
|
+
errors.push({
|
|
3190
|
+
type: "face_invalid_loop",
|
|
3191
|
+
message: `Face ${face.id} has invalid halfedge loop`,
|
|
3192
|
+
elementIds: [face.id]
|
|
3193
|
+
});
|
|
3194
|
+
continue;
|
|
3195
|
+
}
|
|
3196
|
+
for (const he of halfedges) {
|
|
3197
|
+
if (((_a = he.face) == null ? void 0 : _a.id) !== face.id) {
|
|
3198
|
+
errors.push({
|
|
3199
|
+
type: "halfedge_face_mismatch",
|
|
3200
|
+
message: `Halfedge ${he.id} in face ${face.id} references different face`,
|
|
3201
|
+
elementIds: [face.id, he.id]
|
|
3202
|
+
});
|
|
3203
|
+
}
|
|
3204
|
+
}
|
|
3205
|
+
if (face.isDegenerate()) {
|
|
3206
|
+
warnings.push({
|
|
3207
|
+
type: "degenerate_face",
|
|
3208
|
+
message: `Face ${face.id} is degenerate (near-zero area)`,
|
|
3209
|
+
elementIds: [face.id]
|
|
3210
|
+
});
|
|
3211
|
+
}
|
|
3212
|
+
}
|
|
3213
|
+
}
|
|
3214
|
+
function validateHalfedgeConnectivity(mesh, errors, _warnings) {
|
|
3215
|
+
for (const he of mesh.getHalfedges()) {
|
|
3216
|
+
if (!he.next) {
|
|
3217
|
+
errors.push({
|
|
3218
|
+
type: "halfedge_no_next",
|
|
3219
|
+
message: `Halfedge ${he.id} has no next pointer`,
|
|
3220
|
+
elementIds: [he.id]
|
|
3221
|
+
});
|
|
3222
|
+
} else if (!mesh.getHalfedge(he.next.id)) {
|
|
3223
|
+
errors.push({
|
|
3224
|
+
type: "halfedge_invalid_next",
|
|
3225
|
+
message: `Halfedge ${he.id} has invalid next pointer`,
|
|
3226
|
+
elementIds: [he.id]
|
|
3227
|
+
});
|
|
3228
|
+
}
|
|
3229
|
+
if (!he.prev) {
|
|
3230
|
+
errors.push({
|
|
3231
|
+
type: "halfedge_no_prev",
|
|
3232
|
+
message: `Halfedge ${he.id} has no prev pointer`,
|
|
3233
|
+
elementIds: [he.id]
|
|
3234
|
+
});
|
|
3235
|
+
} else if (!mesh.getHalfedge(he.prev.id)) {
|
|
3236
|
+
errors.push({
|
|
3237
|
+
type: "halfedge_invalid_prev",
|
|
3238
|
+
message: `Halfedge ${he.id} has invalid prev pointer`,
|
|
3239
|
+
elementIds: [he.id]
|
|
3240
|
+
});
|
|
3241
|
+
}
|
|
3242
|
+
if (he.next && he.next.prev !== he) {
|
|
3243
|
+
errors.push({
|
|
3244
|
+
type: "halfedge_next_prev_mismatch",
|
|
3245
|
+
message: `Halfedge ${he.id}: next.prev does not point back`,
|
|
3246
|
+
elementIds: [he.id]
|
|
3247
|
+
});
|
|
3248
|
+
}
|
|
3249
|
+
if (he.prev && he.prev.next !== he) {
|
|
3250
|
+
errors.push({
|
|
3251
|
+
type: "halfedge_prev_next_mismatch",
|
|
3252
|
+
message: `Halfedge ${he.id}: prev.next does not point back`,
|
|
3253
|
+
elementIds: [he.id]
|
|
3254
|
+
});
|
|
3255
|
+
}
|
|
3256
|
+
if (he.twin) {
|
|
3257
|
+
if (!mesh.getHalfedge(he.twin.id)) {
|
|
3258
|
+
errors.push({
|
|
3259
|
+
type: "halfedge_invalid_twin",
|
|
3260
|
+
message: `Halfedge ${he.id} has invalid twin pointer`,
|
|
3261
|
+
elementIds: [he.id]
|
|
3262
|
+
});
|
|
3263
|
+
} else if (he.twin.twin !== he) {
|
|
3264
|
+
errors.push({
|
|
3265
|
+
type: "halfedge_twin_mismatch",
|
|
3266
|
+
message: `Halfedge ${he.id}: twin.twin does not point back`,
|
|
3267
|
+
elementIds: [he.id]
|
|
3268
|
+
});
|
|
3269
|
+
}
|
|
3270
|
+
}
|
|
3271
|
+
if (!mesh.getVertex(he.vertex.id)) {
|
|
3272
|
+
errors.push({
|
|
3273
|
+
type: "halfedge_invalid_vertex",
|
|
3274
|
+
message: `Halfedge ${he.id} references non-existent vertex`,
|
|
3275
|
+
elementIds: [he.id]
|
|
3276
|
+
});
|
|
3277
|
+
}
|
|
3278
|
+
if (!mesh.getEdge(he.edge.id)) {
|
|
3279
|
+
errors.push({
|
|
3280
|
+
type: "halfedge_invalid_edge",
|
|
3281
|
+
message: `Halfedge ${he.id} references non-existent edge`,
|
|
3282
|
+
elementIds: [he.id]
|
|
3283
|
+
});
|
|
3284
|
+
}
|
|
3285
|
+
}
|
|
3286
|
+
}
|
|
3287
|
+
function isTopologyValid(mesh) {
|
|
3288
|
+
return validateTopology(mesh).isValid;
|
|
3289
|
+
}
|
|
3290
|
+
class TopologyValidator {
|
|
3291
|
+
constructor(mesh) {
|
|
3292
|
+
this.mesh = mesh;
|
|
3293
|
+
}
|
|
3294
|
+
/**
|
|
3295
|
+
* Validates the mesh topology.
|
|
3296
|
+
*/
|
|
3297
|
+
validate() {
|
|
3298
|
+
return validateTopology(this.mesh);
|
|
3299
|
+
}
|
|
3300
|
+
/**
|
|
3301
|
+
* Quick check if topology is valid.
|
|
3302
|
+
*/
|
|
3303
|
+
isValid() {
|
|
3304
|
+
return isTopologyValid(this.mesh);
|
|
3305
|
+
}
|
|
3306
|
+
/**
|
|
3307
|
+
* Gets all errors.
|
|
3308
|
+
*/
|
|
3309
|
+
getErrors() {
|
|
3310
|
+
return this.validate().errors;
|
|
3311
|
+
}
|
|
3312
|
+
/**
|
|
3313
|
+
* Gets all warnings.
|
|
3314
|
+
*/
|
|
3315
|
+
getWarnings() {
|
|
3316
|
+
return this.validate().warnings;
|
|
3317
|
+
}
|
|
3318
|
+
}
|
|
3319
|
+
function canFlipEdge(edge) {
|
|
3320
|
+
if (edge.isSkeletonEdge()) {
|
|
3321
|
+
return false;
|
|
3322
|
+
}
|
|
3323
|
+
if (!edge.canFlip()) {
|
|
3324
|
+
return false;
|
|
3325
|
+
}
|
|
3326
|
+
const quad = getQuadVertices(edge);
|
|
3327
|
+
if (!quad) {
|
|
3328
|
+
return false;
|
|
3329
|
+
}
|
|
3330
|
+
return isQuadConvex(quad.v0, quad.v1, quad.v2, quad.v3);
|
|
3331
|
+
}
|
|
3332
|
+
function getQuadVertices(edge) {
|
|
3333
|
+
const h = edge.halfedge;
|
|
3334
|
+
const hTwin = h.twin;
|
|
3335
|
+
if (!hTwin || !h.face || !hTwin.face) {
|
|
3336
|
+
return null;
|
|
3337
|
+
}
|
|
3338
|
+
const hNext = h.next;
|
|
3339
|
+
const hTwinNext = hTwin.next;
|
|
3340
|
+
if (!hNext || !hTwinNext) {
|
|
3341
|
+
return null;
|
|
3342
|
+
}
|
|
3343
|
+
const v0 = hTwin.vertex;
|
|
3344
|
+
const v1 = h.vertex;
|
|
3345
|
+
const v2 = hNext.vertex;
|
|
3346
|
+
const v3 = hTwinNext.vertex;
|
|
3347
|
+
return {
|
|
3348
|
+
v0: { x: v0.position.x, y: v0.position.y, z: v0.position.z },
|
|
3349
|
+
v1: { x: v1.position.x, y: v1.position.y, z: v1.position.z },
|
|
3350
|
+
v2: { x: v2.position.x, y: v2.position.y, z: v2.position.z },
|
|
3351
|
+
v3: { x: v3.position.x, y: v3.position.y, z: v3.position.z }
|
|
3352
|
+
};
|
|
3353
|
+
}
|
|
3354
|
+
function flipEdge(_mesh, edge) {
|
|
3355
|
+
if (!canFlipEdge(edge)) {
|
|
3356
|
+
return { success: false, reason: "Edge cannot be flipped" };
|
|
3357
|
+
}
|
|
3358
|
+
const h = edge.halfedge;
|
|
3359
|
+
const hTwin = h.twin;
|
|
3360
|
+
if (!hTwin) {
|
|
3361
|
+
return { success: false, reason: "Edge has no twin" };
|
|
3362
|
+
}
|
|
3363
|
+
const hNext = h.next;
|
|
3364
|
+
const hPrev = h.prev;
|
|
3365
|
+
const hTwinNext = hTwin.next;
|
|
3366
|
+
const hTwinPrev = hTwin.prev;
|
|
3367
|
+
const vA = hTwin.vertex;
|
|
3368
|
+
const vB = h.vertex;
|
|
3369
|
+
const vC = hNext.vertex;
|
|
3370
|
+
const vD = hTwinNext.vertex;
|
|
3371
|
+
const f0 = h.face;
|
|
3372
|
+
const f1 = hTwin.face;
|
|
3373
|
+
if (!f0 || !f1) {
|
|
3374
|
+
return { success: false, reason: "Missing faces" };
|
|
3375
|
+
}
|
|
3376
|
+
const newLength = distance(
|
|
3377
|
+
{ x: vC.position.x, y: vC.position.y, z: vC.position.z },
|
|
3378
|
+
{ x: vD.position.x, y: vD.position.y, z: vD.position.z }
|
|
3379
|
+
);
|
|
3380
|
+
edge.length = newLength;
|
|
3381
|
+
h.vertex = vD;
|
|
3382
|
+
hTwin.vertex = vC;
|
|
3383
|
+
h.next = hTwinPrev;
|
|
3384
|
+
h.prev = hNext;
|
|
3385
|
+
hTwinPrev.next = hNext;
|
|
3386
|
+
hTwinPrev.prev = h;
|
|
3387
|
+
hNext.next = h;
|
|
3388
|
+
hNext.prev = hTwinPrev;
|
|
3389
|
+
hTwin.next = hPrev;
|
|
3390
|
+
hTwin.prev = hTwinNext;
|
|
3391
|
+
hPrev.next = hTwinNext;
|
|
3392
|
+
hPrev.prev = hTwin;
|
|
3393
|
+
hTwinNext.next = hTwin;
|
|
3394
|
+
hTwinNext.prev = hPrev;
|
|
3395
|
+
h.face = f0;
|
|
3396
|
+
hTwinPrev.face = f0;
|
|
3397
|
+
hNext.face = f0;
|
|
3398
|
+
hTwin.face = f1;
|
|
3399
|
+
hPrev.face = f1;
|
|
3400
|
+
hTwinNext.face = f1;
|
|
3401
|
+
f0.halfedge = h;
|
|
3402
|
+
f1.halfedge = hTwin;
|
|
3403
|
+
if (vA.halfedge === h) {
|
|
3404
|
+
vA.halfedge = hTwinNext;
|
|
3405
|
+
}
|
|
3406
|
+
if (vB.halfedge === hTwin) {
|
|
3407
|
+
vB.halfedge = hNext;
|
|
3408
|
+
}
|
|
3409
|
+
return { success: true, newLength };
|
|
3410
|
+
}
|
|
3411
|
+
function isDelaunay(edge) {
|
|
3412
|
+
const h = edge.halfedge;
|
|
3413
|
+
const hTwin = h.twin;
|
|
3414
|
+
if (!hTwin || !h.face || !hTwin.face) {
|
|
3415
|
+
return true;
|
|
3416
|
+
}
|
|
3417
|
+
const quad = getQuadVertices(edge);
|
|
3418
|
+
if (!quad) {
|
|
3419
|
+
return true;
|
|
3420
|
+
}
|
|
3421
|
+
const angle0 = computeAngle(quad.v2, quad.v0, quad.v1);
|
|
3422
|
+
const angle1 = computeAngle(quad.v3, quad.v0, quad.v1);
|
|
3423
|
+
return angle0 + angle1 <= Math.PI + 1e-10;
|
|
3424
|
+
}
|
|
3425
|
+
function computeAngle(v0, v1, v2) {
|
|
3426
|
+
const dx1 = v1.x - v0.x;
|
|
3427
|
+
const dy1 = v1.y - v0.y;
|
|
3428
|
+
const dz1 = v1.z - v0.z;
|
|
3429
|
+
const dx2 = v2.x - v0.x;
|
|
3430
|
+
const dy2 = v2.y - v0.y;
|
|
3431
|
+
const dz2 = v2.z - v0.z;
|
|
3432
|
+
const dot2 = dx1 * dx2 + dy1 * dy2 + dz1 * dz2;
|
|
3433
|
+
const len1 = Math.sqrt(dx1 * dx1 + dy1 * dy1 + dz1 * dz1);
|
|
3434
|
+
const len2 = Math.sqrt(dx2 * dx2 + dy2 * dy2 + dz2 * dz2);
|
|
3435
|
+
if (len1 < 1e-10 || len2 < 1e-10) {
|
|
3436
|
+
return 0;
|
|
3437
|
+
}
|
|
3438
|
+
const cosAngle = Math.max(-1, Math.min(1, dot2 / (len1 * len2)));
|
|
3439
|
+
return Math.acos(cosAngle);
|
|
3440
|
+
}
|
|
3441
|
+
function makeDelaunay(mesh, maxIterations) {
|
|
3442
|
+
const edges = mesh.getEdges();
|
|
3443
|
+
const maxIter = maxIterations ?? edges.length * 10;
|
|
3444
|
+
let flipCount = 0;
|
|
3445
|
+
let iteration = 0;
|
|
3446
|
+
while (iteration < maxIter) {
|
|
3447
|
+
iteration++;
|
|
3448
|
+
let flippedAny = false;
|
|
3449
|
+
for (const edge of edges) {
|
|
3450
|
+
if (!isDelaunay(edge) && canFlipEdge(edge)) {
|
|
3451
|
+
const result = flipEdge(mesh, edge);
|
|
3452
|
+
if (result.success) {
|
|
3453
|
+
flipCount++;
|
|
3454
|
+
flippedAny = true;
|
|
3455
|
+
}
|
|
3456
|
+
}
|
|
3457
|
+
}
|
|
3458
|
+
if (!flippedAny) {
|
|
3459
|
+
break;
|
|
3460
|
+
}
|
|
3461
|
+
}
|
|
3462
|
+
return flipCount;
|
|
3463
|
+
}
|
|
3464
|
+
class EdgeFlipper {
|
|
3465
|
+
constructor(mesh) {
|
|
3466
|
+
this.mesh = mesh;
|
|
3467
|
+
}
|
|
3468
|
+
/**
|
|
3469
|
+
* Flips an edge.
|
|
3470
|
+
*/
|
|
3471
|
+
flip(edge) {
|
|
3472
|
+
return flipEdge(this.mesh, edge);
|
|
3473
|
+
}
|
|
3474
|
+
/**
|
|
3475
|
+
* Checks if an edge can be flipped.
|
|
3476
|
+
*/
|
|
3477
|
+
canFlip(edge) {
|
|
3478
|
+
return canFlipEdge(edge);
|
|
3479
|
+
}
|
|
3480
|
+
/**
|
|
3481
|
+
* Checks if an edge satisfies the Delaunay condition.
|
|
3482
|
+
*/
|
|
3483
|
+
isDelaunay(edge) {
|
|
3484
|
+
return isDelaunay(edge);
|
|
3485
|
+
}
|
|
3486
|
+
/**
|
|
3487
|
+
* Makes the mesh Delaunay by flipping non-Delaunay edges.
|
|
3488
|
+
*/
|
|
3489
|
+
makeDelaunay(maxIterations) {
|
|
3490
|
+
return makeDelaunay(this.mesh, maxIterations);
|
|
3491
|
+
}
|
|
3492
|
+
}
|
|
3493
|
+
function splitEdge(mesh, edge, splitRatio = 0.5) {
|
|
3494
|
+
const [v0, v1] = edge.getVertices();
|
|
3495
|
+
if (!v0 || !v1) {
|
|
3496
|
+
return { success: false, reason: "Edge has no vertices" };
|
|
3497
|
+
}
|
|
3498
|
+
const p0 = { x: v0.position.x, y: v0.position.y, z: v0.position.z };
|
|
3499
|
+
const p1 = { x: v1.position.x, y: v1.position.y, z: v1.position.z };
|
|
3500
|
+
const mid = {
|
|
3501
|
+
x: p0.x + (p1.x - p0.x) * splitRatio,
|
|
3502
|
+
y: p0.y + (p1.y - p0.y) * splitRatio,
|
|
3503
|
+
z: p0.z + (p1.z - p0.z) * splitRatio
|
|
3504
|
+
};
|
|
3505
|
+
const newVertex = mesh.createVertex(new Vector3(mid.x, mid.y, mid.z));
|
|
3506
|
+
if (edge.isSkeletonEdge()) ;
|
|
3507
|
+
const newEdges = [];
|
|
3508
|
+
const newFaces = [];
|
|
3509
|
+
const faces = edge.getFaces();
|
|
3510
|
+
if (faces.length === 0) {
|
|
3511
|
+
return { success: true, newVertex, newEdges: [], newFaces: [] };
|
|
3512
|
+
}
|
|
3513
|
+
for (const face of faces) {
|
|
3514
|
+
if (!face) continue;
|
|
3515
|
+
const halfedges = face.getHalfedges();
|
|
3516
|
+
if (!halfedges) continue;
|
|
3517
|
+
let edgeHe = null;
|
|
3518
|
+
for (const he of halfedges) {
|
|
3519
|
+
if (he.edge.id === edge.id) {
|
|
3520
|
+
edgeHe = he;
|
|
3521
|
+
break;
|
|
3522
|
+
}
|
|
3523
|
+
}
|
|
3524
|
+
if (!edgeHe) continue;
|
|
3525
|
+
splitFaceAtEdge(mesh, face, edgeHe, newVertex, newEdges, newFaces);
|
|
3526
|
+
}
|
|
3527
|
+
edge.length = edge.length * splitRatio;
|
|
3528
|
+
mesh.classifyVertices();
|
|
3529
|
+
return { success: true, newVertex, newEdges, newFaces };
|
|
3530
|
+
}
|
|
3531
|
+
function splitFaceAtEdge(mesh, _face, edgeHe, newVertex, _newEdges, newFaces) {
|
|
3532
|
+
const heNext = edgeHe.next;
|
|
3533
|
+
const vOpposite = heNext.vertex;
|
|
3534
|
+
const vTarget = edgeHe.vertex;
|
|
3535
|
+
const newFace = mesh.createFace(newVertex, vTarget, vOpposite);
|
|
3536
|
+
newFaces.push(newFace);
|
|
3537
|
+
edgeHe.vertex = newVertex;
|
|
3538
|
+
if (!newVertex.halfedge) {
|
|
3539
|
+
newVertex.halfedge = edgeHe;
|
|
3540
|
+
}
|
|
3541
|
+
}
|
|
3542
|
+
function splitLongEdges(mesh, maxLength) {
|
|
3543
|
+
const newVertices = [];
|
|
3544
|
+
let splitCount = 0;
|
|
3545
|
+
const edgesToSplit = [];
|
|
3546
|
+
for (const edge of mesh.getEdges()) {
|
|
3547
|
+
if (edge.length > maxLength) {
|
|
3548
|
+
edgesToSplit.push(edge);
|
|
3549
|
+
}
|
|
3550
|
+
}
|
|
3551
|
+
for (const edge of edgesToSplit) {
|
|
3552
|
+
const result = splitEdge(mesh, edge);
|
|
3553
|
+
if (result.success && result.newVertex) {
|
|
3554
|
+
splitCount++;
|
|
3555
|
+
newVertices.push(result.newVertex);
|
|
3556
|
+
}
|
|
3557
|
+
}
|
|
3558
|
+
return { splitCount, newVertices };
|
|
3559
|
+
}
|
|
3560
|
+
class EdgeSplitter {
|
|
3561
|
+
constructor(mesh) {
|
|
3562
|
+
this.mesh = mesh;
|
|
3563
|
+
}
|
|
3564
|
+
/**
|
|
3565
|
+
* Splits an edge at the midpoint.
|
|
3566
|
+
*/
|
|
3567
|
+
split(edge, ratio = 0.5) {
|
|
3568
|
+
return splitEdge(this.mesh, edge, ratio);
|
|
3569
|
+
}
|
|
3570
|
+
/**
|
|
3571
|
+
* Splits all edges longer than a threshold.
|
|
3572
|
+
*/
|
|
3573
|
+
splitLongEdges(maxLength) {
|
|
3574
|
+
return splitLongEdges(this.mesh, maxLength);
|
|
3575
|
+
}
|
|
3576
|
+
/**
|
|
3577
|
+
* Checks if an edge should be split based on target length.
|
|
3578
|
+
*/
|
|
3579
|
+
shouldSplit(edge, targetLength, maxRatio = 1.333) {
|
|
3580
|
+
return edge.length > targetLength * maxRatio;
|
|
3581
|
+
}
|
|
3582
|
+
}
|
|
3583
|
+
function canContractEdge(edge) {
|
|
3584
|
+
const [v0, v1] = edge.getVertices();
|
|
3585
|
+
if (!v0 || !v1) {
|
|
3586
|
+
return false;
|
|
3587
|
+
}
|
|
3588
|
+
if (v0.type === VertexType.SkeletonBranching || v0.type === VertexType.NonManifoldOther) {
|
|
3589
|
+
if (v1.type === VertexType.SkeletonBranching || v1.type === VertexType.NonManifoldOther) {
|
|
3590
|
+
return false;
|
|
3591
|
+
}
|
|
3592
|
+
}
|
|
3593
|
+
if (v1.type === VertexType.SkeletonBranching || v1.type === VertexType.NonManifoldOther) ;
|
|
3594
|
+
if (!checkLinkCondition(v0, v1)) {
|
|
3595
|
+
return false;
|
|
3596
|
+
}
|
|
3597
|
+
return true;
|
|
3598
|
+
}
|
|
3599
|
+
function checkLinkCondition(v0, v1) {
|
|
3600
|
+
const link0 = /* @__PURE__ */ new Set();
|
|
3601
|
+
const link1 = /* @__PURE__ */ new Set();
|
|
3602
|
+
v0.forEachNeighbor((v) => {
|
|
3603
|
+
link0.add(v.id);
|
|
3604
|
+
});
|
|
3605
|
+
v1.forEachNeighbor((v) => {
|
|
3606
|
+
link1.add(v.id);
|
|
3607
|
+
});
|
|
3608
|
+
let commonCount = 0;
|
|
3609
|
+
for (const id of link0) {
|
|
3610
|
+
if (id !== v0.id && id !== v1.id && link1.has(id)) {
|
|
3611
|
+
commonCount++;
|
|
3612
|
+
}
|
|
3613
|
+
}
|
|
3614
|
+
const faces = getSharedFaces(v0, v1);
|
|
3615
|
+
const expectedCommon = faces.length;
|
|
3616
|
+
return commonCount <= expectedCommon;
|
|
3617
|
+
}
|
|
3618
|
+
function getSharedFaces(v0, v1) {
|
|
3619
|
+
const faces0 = /* @__PURE__ */ new Set();
|
|
3620
|
+
const sharedFaces = [];
|
|
3621
|
+
v0.forEachOutgoingHalfedge((he) => {
|
|
3622
|
+
if (he.face) {
|
|
3623
|
+
faces0.add(he.face.id);
|
|
3624
|
+
}
|
|
3625
|
+
});
|
|
3626
|
+
v1.forEachOutgoingHalfedge((he) => {
|
|
3627
|
+
if (he.face && faces0.has(he.face.id)) {
|
|
3628
|
+
sharedFaces.push(he.face);
|
|
3629
|
+
}
|
|
3630
|
+
});
|
|
3631
|
+
return sharedFaces;
|
|
3632
|
+
}
|
|
3633
|
+
function contractEdge(mesh, edge) {
|
|
3634
|
+
var _a;
|
|
3635
|
+
if (!canContractEdge(edge)) {
|
|
3636
|
+
return { success: false, reason: "Edge cannot be contracted" };
|
|
3637
|
+
}
|
|
3638
|
+
const [v0, v1] = edge.getVertices();
|
|
3639
|
+
if (!v0 || !v1) {
|
|
3640
|
+
return { success: false, reason: "Edge has no vertices" };
|
|
3641
|
+
}
|
|
3642
|
+
const { keepVertex, removeVertex, newPosition } = determineContractionResult(v0, v1);
|
|
3643
|
+
keepVertex.position.set(newPosition.x, newPosition.y, newPosition.z);
|
|
3644
|
+
const removedFaces = getSharedFaces(v0, v1);
|
|
3645
|
+
redirectHalfedges(removeVertex, keepVertex);
|
|
3646
|
+
for (const face of removedFaces) {
|
|
3647
|
+
mesh.faces.delete(face.id);
|
|
3648
|
+
}
|
|
3649
|
+
mesh.edges.delete(edge.id);
|
|
3650
|
+
for (const he of edge.allHalfedges) {
|
|
3651
|
+
mesh.halfedges.delete(he.id);
|
|
3652
|
+
}
|
|
3653
|
+
mesh.vertices.delete(removeVertex.id);
|
|
3654
|
+
if (keepVertex.halfedge && !mesh.halfedges.has(keepVertex.halfedge.id)) {
|
|
3655
|
+
for (const he of mesh.halfedges.values()) {
|
|
3656
|
+
if (((_a = he.getSourceVertex()) == null ? void 0 : _a.id) === keepVertex.id) {
|
|
3657
|
+
keepVertex.halfedge = he;
|
|
3658
|
+
break;
|
|
3659
|
+
}
|
|
3660
|
+
}
|
|
3661
|
+
}
|
|
3662
|
+
mesh.classifyVertices();
|
|
3663
|
+
return { success: true, remainingVertex: keepVertex, removedFaces };
|
|
3664
|
+
}
|
|
3665
|
+
function determineContractionResult(v0, v1) {
|
|
3666
|
+
const priority = (v) => {
|
|
3667
|
+
switch (v.type) {
|
|
3668
|
+
case VertexType.SkeletonBranching:
|
|
3669
|
+
case VertexType.NonManifoldOther:
|
|
3670
|
+
return 3;
|
|
3671
|
+
case VertexType.OpenBook:
|
|
3672
|
+
return 2;
|
|
3673
|
+
case VertexType.Manifold:
|
|
3674
|
+
return 1;
|
|
3675
|
+
default:
|
|
3676
|
+
return 0;
|
|
3677
|
+
}
|
|
3678
|
+
};
|
|
3679
|
+
const p0 = priority(v0);
|
|
3680
|
+
const p1 = priority(v1);
|
|
3681
|
+
let keepVertex;
|
|
3682
|
+
let removeVertex;
|
|
3683
|
+
let newPosition;
|
|
3684
|
+
if (p0 >= p1) {
|
|
3685
|
+
keepVertex = v0;
|
|
3686
|
+
removeVertex = v1;
|
|
3687
|
+
} else {
|
|
3688
|
+
keepVertex = v1;
|
|
3689
|
+
removeVertex = v0;
|
|
3690
|
+
}
|
|
3691
|
+
if (keepVertex.type === VertexType.SkeletonBranching || keepVertex.type === VertexType.NonManifoldOther) {
|
|
3692
|
+
newPosition = {
|
|
3693
|
+
x: keepVertex.position.x,
|
|
3694
|
+
y: keepVertex.position.y,
|
|
3695
|
+
z: keepVertex.position.z
|
|
3696
|
+
};
|
|
3697
|
+
} else if (keepVertex.type === VertexType.OpenBook && removeVertex.type === VertexType.Manifold) {
|
|
3698
|
+
newPosition = {
|
|
3699
|
+
x: keepVertex.position.x,
|
|
3700
|
+
y: keepVertex.position.y,
|
|
3701
|
+
z: keepVertex.position.z
|
|
3702
|
+
};
|
|
3703
|
+
} else {
|
|
3704
|
+
newPosition = midpoint(
|
|
3705
|
+
{ x: v0.position.x, y: v0.position.y, z: v0.position.z },
|
|
3706
|
+
{ x: v1.position.x, y: v1.position.y, z: v1.position.z }
|
|
3707
|
+
);
|
|
3708
|
+
}
|
|
3709
|
+
return { keepVertex, removeVertex, newPosition };
|
|
3710
|
+
}
|
|
3711
|
+
function redirectHalfedges(fromVertex, toVertex) {
|
|
3712
|
+
fromVertex.forEachOutgoingHalfedge((he) => {
|
|
3713
|
+
if (he.twin) {
|
|
3714
|
+
he.twin.vertex = toVertex;
|
|
3715
|
+
}
|
|
3716
|
+
});
|
|
3717
|
+
}
|
|
3718
|
+
function contractShortEdges(mesh, minLength) {
|
|
3719
|
+
let contractCount = 0;
|
|
3720
|
+
let removedVertices = 0;
|
|
3721
|
+
let edgesToContract = [];
|
|
3722
|
+
for (const edge of mesh.getEdges()) {
|
|
3723
|
+
if (edge.length < minLength && canContractEdge(edge)) {
|
|
3724
|
+
edgesToContract.push(edge);
|
|
3725
|
+
}
|
|
3726
|
+
}
|
|
3727
|
+
while (edgesToContract.length > 0) {
|
|
3728
|
+
const edge = edgesToContract.pop();
|
|
3729
|
+
if (!mesh.edges.has(edge.id) || !canContractEdge(edge)) {
|
|
3730
|
+
continue;
|
|
3731
|
+
}
|
|
3732
|
+
const result = contractEdge(mesh, edge);
|
|
3733
|
+
if (result.success) {
|
|
3734
|
+
contractCount++;
|
|
3735
|
+
removedVertices++;
|
|
3736
|
+
}
|
|
3737
|
+
}
|
|
3738
|
+
return { contractCount, removedVertices };
|
|
3739
|
+
}
|
|
3740
|
+
class EdgeContractor {
|
|
3741
|
+
constructor(mesh) {
|
|
3742
|
+
this.mesh = mesh;
|
|
3743
|
+
}
|
|
3744
|
+
/**
|
|
3745
|
+
* Contracts an edge.
|
|
3746
|
+
*/
|
|
3747
|
+
contract(edge) {
|
|
3748
|
+
return contractEdge(this.mesh, edge);
|
|
3749
|
+
}
|
|
3750
|
+
/**
|
|
3751
|
+
* Checks if an edge can be contracted.
|
|
3752
|
+
*/
|
|
3753
|
+
canContract(edge) {
|
|
3754
|
+
return canContractEdge(edge);
|
|
3755
|
+
}
|
|
3756
|
+
/**
|
|
3757
|
+
* Contracts all edges shorter than a threshold.
|
|
3758
|
+
*/
|
|
3759
|
+
contractShortEdges(minLength) {
|
|
3760
|
+
return contractShortEdges(this.mesh, minLength);
|
|
3761
|
+
}
|
|
3762
|
+
/**
|
|
3763
|
+
* Checks if an edge should be contracted based on target length.
|
|
3764
|
+
*/
|
|
3765
|
+
shouldContract(edge, targetLength, minRatio = 0.4) {
|
|
3766
|
+
return edge.length < targetLength * minRatio;
|
|
3767
|
+
}
|
|
3768
|
+
}
|
|
3769
|
+
function computeTangentialSmoothing(vertex) {
|
|
3770
|
+
const neighbors = vertex.getNeighbors();
|
|
3771
|
+
if (neighbors.length === 0) {
|
|
3772
|
+
return null;
|
|
3773
|
+
}
|
|
3774
|
+
let cx = 0, cy = 0, cz = 0;
|
|
3775
|
+
for (const n of neighbors) {
|
|
3776
|
+
cx += n.position.x;
|
|
3777
|
+
cy += n.position.y;
|
|
3778
|
+
cz += n.position.z;
|
|
3779
|
+
}
|
|
3780
|
+
cx /= neighbors.length;
|
|
3781
|
+
cy /= neighbors.length;
|
|
3782
|
+
cz /= neighbors.length;
|
|
3783
|
+
const barycenter = { x: cx, y: cy, z: cz };
|
|
3784
|
+
const normal = computeVertexNormal(vertex);
|
|
3785
|
+
if (!normal) {
|
|
3786
|
+
return barycenter;
|
|
3787
|
+
}
|
|
3788
|
+
const vertexPos = { x: vertex.position.x, y: vertex.position.y, z: vertex.position.z };
|
|
3789
|
+
const toBarycenter = subtract(barycenter, vertexPos);
|
|
3790
|
+
const normalComponent = scale(normal, dot(toBarycenter, normal));
|
|
3791
|
+
const tangentComponent = subtract(toBarycenter, normalComponent);
|
|
3792
|
+
return add(vertexPos, tangentComponent);
|
|
3793
|
+
}
|
|
3794
|
+
function computeVertexNormal(vertex) {
|
|
3795
|
+
let nx = 0, ny = 0, nz = 0;
|
|
3796
|
+
let count = 0;
|
|
3797
|
+
vertex.forEachOutgoingHalfedge((he) => {
|
|
3798
|
+
if (he.face) {
|
|
3799
|
+
const faceNormal = he.face.getNormal();
|
|
3800
|
+
if (faceNormal) {
|
|
3801
|
+
nx += faceNormal.x;
|
|
3802
|
+
ny += faceNormal.y;
|
|
3803
|
+
nz += faceNormal.z;
|
|
3804
|
+
count++;
|
|
3805
|
+
}
|
|
3806
|
+
}
|
|
3807
|
+
});
|
|
3808
|
+
if (count === 0) {
|
|
3809
|
+
return null;
|
|
3810
|
+
}
|
|
3811
|
+
return normalize({ x: nx / count, y: ny / count, z: nz / count });
|
|
3812
|
+
}
|
|
3813
|
+
function relocateVertex(_mesh, vertex, targetPosition, constraints) {
|
|
3814
|
+
if (vertex.type === VertexType.SkeletonBranching || vertex.type === VertexType.NonManifoldOther) {
|
|
3815
|
+
return { success: false, reason: "Vertex is fixed" };
|
|
3816
|
+
}
|
|
3817
|
+
const currentPos = { x: vertex.position.x, y: vertex.position.y, z: vertex.position.z };
|
|
3818
|
+
let finalPosition = targetPosition;
|
|
3819
|
+
let wasConstrained = false;
|
|
3820
|
+
if (constraints) {
|
|
3821
|
+
const constrained = constraints.constrainPosition(vertex, targetPosition);
|
|
3822
|
+
finalPosition = constrained.position;
|
|
3823
|
+
wasConstrained = constrained.wasConstrained;
|
|
3824
|
+
}
|
|
3825
|
+
if (!isValidRelocation(vertex, finalPosition)) {
|
|
3826
|
+
return { success: false, reason: "Relocation would create invalid geometry" };
|
|
3827
|
+
}
|
|
3828
|
+
vertex.position.set(finalPosition.x, finalPosition.y, finalPosition.z);
|
|
3829
|
+
updateIncidentEdgeLengths(vertex);
|
|
3830
|
+
const distanceMoved = distance(currentPos, finalPosition);
|
|
3831
|
+
return {
|
|
3832
|
+
success: true,
|
|
3833
|
+
newPosition: finalPosition,
|
|
3834
|
+
wasConstrained,
|
|
3835
|
+
distanceMoved
|
|
3836
|
+
};
|
|
3837
|
+
}
|
|
3838
|
+
function isValidRelocation(vertex, newPosition) {
|
|
3839
|
+
const originalPos = { x: vertex.position.x, y: vertex.position.y, z: vertex.position.z };
|
|
3840
|
+
vertex.position.set(newPosition.x, newPosition.y, newPosition.z);
|
|
3841
|
+
let isValid = true;
|
|
3842
|
+
vertex.forEachOutgoingHalfedge((he) => {
|
|
3843
|
+
if (he.face) {
|
|
3844
|
+
const area = he.face.getArea();
|
|
3845
|
+
if (area !== null && area < 1e-10) {
|
|
3846
|
+
isValid = false;
|
|
3847
|
+
}
|
|
3848
|
+
}
|
|
3849
|
+
});
|
|
3850
|
+
vertex.position.set(originalPos.x, originalPos.y, originalPos.z);
|
|
3851
|
+
return isValid;
|
|
3852
|
+
}
|
|
3853
|
+
function updateIncidentEdgeLengths(vertex) {
|
|
3854
|
+
vertex.forEachOutgoingHalfedge((he) => {
|
|
3855
|
+
const source = he.getSourceVertex();
|
|
3856
|
+
if (source) {
|
|
3857
|
+
he.edge.length = distance(
|
|
3858
|
+
{ x: source.position.x, y: source.position.y, z: source.position.z },
|
|
3859
|
+
{ x: he.vertex.position.x, y: he.vertex.position.y, z: he.vertex.position.z }
|
|
3860
|
+
);
|
|
3861
|
+
}
|
|
3862
|
+
});
|
|
3863
|
+
}
|
|
3864
|
+
function smoothVertex(mesh, vertex, constraints, dampingFactor = 0.5) {
|
|
3865
|
+
const target = computeTangentialSmoothing(vertex);
|
|
3866
|
+
if (!target) {
|
|
3867
|
+
return { success: false, reason: "Cannot compute smoothing target" };
|
|
3868
|
+
}
|
|
3869
|
+
const currentPos = { x: vertex.position.x, y: vertex.position.y, z: vertex.position.z };
|
|
3870
|
+
const dampedTarget = {
|
|
3871
|
+
x: currentPos.x + (target.x - currentPos.x) * dampingFactor,
|
|
3872
|
+
y: currentPos.y + (target.y - currentPos.y) * dampingFactor,
|
|
3873
|
+
z: currentPos.z + (target.z - currentPos.z) * dampingFactor
|
|
3874
|
+
};
|
|
3875
|
+
return relocateVertex(mesh, vertex, dampedTarget, constraints);
|
|
3876
|
+
}
|
|
3877
|
+
function smoothAllVertices(mesh, constraints, dampingFactor = 0.5) {
|
|
3878
|
+
let smoothedCount = 0;
|
|
3879
|
+
let totalDistance = 0;
|
|
3880
|
+
for (const vertex of mesh.getVertices()) {
|
|
3881
|
+
if (vertex.type === VertexType.Manifold || vertex.type === VertexType.OpenBook) {
|
|
3882
|
+
const result = smoothVertex(mesh, vertex, constraints, dampingFactor);
|
|
3883
|
+
if (result.success) {
|
|
3884
|
+
smoothedCount++;
|
|
3885
|
+
totalDistance += result.distanceMoved ?? 0;
|
|
3886
|
+
}
|
|
3887
|
+
}
|
|
3888
|
+
}
|
|
3889
|
+
return { smoothedCount, totalDistance };
|
|
3890
|
+
}
|
|
3891
|
+
class VertexRelocator {
|
|
3892
|
+
constructor(mesh, constraints) {
|
|
3893
|
+
this.constraints = null;
|
|
3894
|
+
this.mesh = mesh;
|
|
3895
|
+
this.constraints = constraints ?? null;
|
|
3896
|
+
}
|
|
3897
|
+
/**
|
|
3898
|
+
* Sets the skeleton constraints.
|
|
3899
|
+
*/
|
|
3900
|
+
setConstraints(constraints) {
|
|
3901
|
+
this.constraints = constraints;
|
|
3902
|
+
}
|
|
3903
|
+
/**
|
|
3904
|
+
* Relocates a vertex to a target position.
|
|
3905
|
+
*/
|
|
3906
|
+
relocate(vertex, targetPosition) {
|
|
3907
|
+
return relocateVertex(this.mesh, vertex, targetPosition, this.constraints ?? void 0);
|
|
3908
|
+
}
|
|
3909
|
+
/**
|
|
3910
|
+
* Applies tangential smoothing to a vertex.
|
|
3911
|
+
*/
|
|
3912
|
+
smooth(vertex, dampingFactor = 0.5) {
|
|
3913
|
+
return smoothVertex(this.mesh, vertex, this.constraints ?? void 0, dampingFactor);
|
|
3914
|
+
}
|
|
3915
|
+
/**
|
|
3916
|
+
* Applies tangential smoothing to all relocatable vertices.
|
|
3917
|
+
*/
|
|
3918
|
+
smoothAll(dampingFactor = 0.5) {
|
|
3919
|
+
return smoothAllVertices(this.mesh, this.constraints ?? void 0, dampingFactor);
|
|
3920
|
+
}
|
|
3921
|
+
/**
|
|
3922
|
+
* Checks if a vertex can be relocated.
|
|
3923
|
+
*/
|
|
3924
|
+
canRelocate(vertex) {
|
|
3925
|
+
return vertex.type === VertexType.Manifold || vertex.type === VertexType.OpenBook;
|
|
3926
|
+
}
|
|
3927
|
+
}
|
|
3928
|
+
function computeMeshQuality(mesh, poorQualityThreshold = 0.3) {
|
|
3929
|
+
const faces = mesh.getFaces();
|
|
3930
|
+
const edges = mesh.getEdges();
|
|
3931
|
+
const qualities = [];
|
|
3932
|
+
const areas = [];
|
|
3933
|
+
for (const face of faces) {
|
|
3934
|
+
const quality = face.getQuality();
|
|
3935
|
+
if (quality !== null) {
|
|
3936
|
+
qualities.push(quality);
|
|
3937
|
+
}
|
|
3938
|
+
const area = face.getArea();
|
|
3939
|
+
if (area !== null) {
|
|
3940
|
+
areas.push(area);
|
|
3941
|
+
}
|
|
3942
|
+
}
|
|
3943
|
+
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;
|
|
3946
|
+
const averageQuality = qualities.length > 0 ? qualities.reduce((a, b) => a + b, 0) / qualities.length : 0;
|
|
3947
|
+
const variance = qualities.length > 0 ? qualities.reduce((sum, q) => sum + Math.pow(q - averageQuality, 2), 0) / qualities.length : 0;
|
|
3948
|
+
const stdDevQuality = Math.sqrt(variance);
|
|
3949
|
+
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;
|
|
3952
|
+
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;
|
|
3955
|
+
const totalArea = areas.reduce((a, b) => a + b, 0);
|
|
3956
|
+
return {
|
|
3957
|
+
minQuality,
|
|
3958
|
+
maxQuality,
|
|
3959
|
+
averageQuality,
|
|
3960
|
+
stdDevQuality,
|
|
3961
|
+
poorQualityCount,
|
|
3962
|
+
minEdgeLength,
|
|
3963
|
+
maxEdgeLength,
|
|
3964
|
+
averageEdgeLength,
|
|
3965
|
+
minArea,
|
|
3966
|
+
maxArea,
|
|
3967
|
+
totalArea
|
|
3968
|
+
};
|
|
3969
|
+
}
|
|
3970
|
+
function getPoorQualityFaces(mesh, threshold = 0.3) {
|
|
3971
|
+
return mesh.getFaces().filter((face) => {
|
|
3972
|
+
const quality = face.getQuality();
|
|
3973
|
+
return quality !== null && quality < threshold;
|
|
3974
|
+
});
|
|
3975
|
+
}
|
|
3976
|
+
function getLongEdges(mesh, targetLength, maxRatio = 1.333) {
|
|
3977
|
+
const maxLength = targetLength * maxRatio;
|
|
3978
|
+
return mesh.getEdges().filter((e) => e.length > maxLength);
|
|
3979
|
+
}
|
|
3980
|
+
function getShortEdges(mesh, targetLength, minRatio = 0.4) {
|
|
3981
|
+
const minLength = targetLength * minRatio;
|
|
3982
|
+
return mesh.getEdges().filter((e) => e.length < minLength);
|
|
3983
|
+
}
|
|
3984
|
+
function computeTargetEdgeLength(mesh, numTargetVertices) {
|
|
3985
|
+
const vertices = mesh.getVertices();
|
|
3986
|
+
if (vertices.length === 0) {
|
|
3987
|
+
return 1;
|
|
3988
|
+
}
|
|
3989
|
+
let minX = Infinity, minY = Infinity, minZ = Infinity;
|
|
3990
|
+
let maxX = -Infinity, maxY = -Infinity, maxZ = -Infinity;
|
|
3991
|
+
for (const v of vertices) {
|
|
3992
|
+
minX = Math.min(minX, v.position.x);
|
|
3993
|
+
minY = Math.min(minY, v.position.y);
|
|
3994
|
+
minZ = Math.min(minZ, v.position.z);
|
|
3995
|
+
maxX = Math.max(maxX, v.position.x);
|
|
3996
|
+
maxY = Math.max(maxY, v.position.y);
|
|
3997
|
+
maxZ = Math.max(maxZ, v.position.z);
|
|
3998
|
+
}
|
|
3999
|
+
const diagonal = Math.sqrt(
|
|
4000
|
+
Math.pow(maxX - minX, 2) + Math.pow(maxY - minY, 2) + Math.pow(maxZ - minZ, 2)
|
|
4001
|
+
);
|
|
4002
|
+
if (numTargetVertices !== void 0 && numTargetVertices > 0) {
|
|
4003
|
+
const stats = computeMeshQuality(mesh);
|
|
4004
|
+
if (stats.totalArea > 0) {
|
|
4005
|
+
return Math.sqrt(stats.totalArea / (2 * numTargetVertices));
|
|
4006
|
+
}
|
|
4007
|
+
}
|
|
4008
|
+
return diagonal / Math.sqrt(vertices.length);
|
|
4009
|
+
}
|
|
4010
|
+
function computeTriangleAspectRatio(face) {
|
|
4011
|
+
const halfedges = face.getHalfedges();
|
|
4012
|
+
if (!halfedges) {
|
|
4013
|
+
return null;
|
|
4014
|
+
}
|
|
4015
|
+
const lengths = halfedges.map((he) => he.edge.length);
|
|
4016
|
+
const minLen = Math.min(...lengths);
|
|
4017
|
+
const maxLen = Math.max(...lengths);
|
|
4018
|
+
if (minLen < 1e-10) {
|
|
4019
|
+
return Infinity;
|
|
4020
|
+
}
|
|
4021
|
+
return maxLen / minLen;
|
|
4022
|
+
}
|
|
4023
|
+
function getHighValenceVertices(mesh, maxValence = 8) {
|
|
4024
|
+
return mesh.getVertices().filter((v) => {
|
|
4025
|
+
const degree = v.degree();
|
|
4026
|
+
return degree !== null && degree > maxValence;
|
|
4027
|
+
});
|
|
4028
|
+
}
|
|
4029
|
+
function getLowValenceVertices(mesh, minValence = 4) {
|
|
4030
|
+
return mesh.getVertices().filter((v) => {
|
|
4031
|
+
const degree = v.degree();
|
|
4032
|
+
return degree !== null && degree < minValence;
|
|
4033
|
+
});
|
|
4034
|
+
}
|
|
4035
|
+
class QualityMetrics {
|
|
4036
|
+
constructor(mesh) {
|
|
4037
|
+
this.mesh = mesh;
|
|
4038
|
+
}
|
|
4039
|
+
/**
|
|
4040
|
+
* Computes overall mesh quality statistics.
|
|
4041
|
+
*/
|
|
4042
|
+
computeStats(poorQualityThreshold = 0.3) {
|
|
4043
|
+
return computeMeshQuality(this.mesh, poorQualityThreshold);
|
|
4044
|
+
}
|
|
4045
|
+
/**
|
|
4046
|
+
* Gets poor quality faces.
|
|
4047
|
+
*/
|
|
4048
|
+
getPoorQualityFaces(threshold = 0.3) {
|
|
4049
|
+
return getPoorQualityFaces(this.mesh, threshold);
|
|
4050
|
+
}
|
|
4051
|
+
/**
|
|
4052
|
+
* Gets long edges that should be split.
|
|
4053
|
+
*/
|
|
4054
|
+
getLongEdges(targetLength, maxRatio = 1.333) {
|
|
4055
|
+
return getLongEdges(this.mesh, targetLength, maxRatio);
|
|
4056
|
+
}
|
|
4057
|
+
/**
|
|
4058
|
+
* Gets short edges that should be collapsed.
|
|
4059
|
+
*/
|
|
4060
|
+
getShortEdges(targetLength, minRatio = 0.4) {
|
|
4061
|
+
return getShortEdges(this.mesh, targetLength, minRatio);
|
|
4062
|
+
}
|
|
4063
|
+
/**
|
|
4064
|
+
* Computes target edge length.
|
|
4065
|
+
*/
|
|
4066
|
+
computeTargetEdgeLength(numTargetVertices) {
|
|
4067
|
+
return computeTargetEdgeLength(this.mesh, numTargetVertices);
|
|
4068
|
+
}
|
|
4069
|
+
/**
|
|
4070
|
+
* Gets high valence vertices.
|
|
4071
|
+
*/
|
|
4072
|
+
getHighValenceVertices(maxValence = 8) {
|
|
4073
|
+
return getHighValenceVertices(this.mesh, maxValence);
|
|
4074
|
+
}
|
|
4075
|
+
/**
|
|
4076
|
+
* Gets low valence vertices.
|
|
4077
|
+
*/
|
|
4078
|
+
getLowValenceVertices(minValence = 4) {
|
|
4079
|
+
return getLowValenceVertices(this.mesh, minValence);
|
|
4080
|
+
}
|
|
4081
|
+
}
|
|
4082
|
+
class AdaptiveRemesher {
|
|
4083
|
+
constructor(mesh, options = {}) {
|
|
4084
|
+
this.skeleton = null;
|
|
4085
|
+
this.constraints = null;
|
|
4086
|
+
this.mesh = mesh;
|
|
4087
|
+
const targetEdgeLength = options.targetEdgeLength ?? computeTargetEdgeLength(mesh);
|
|
4088
|
+
this.options = {
|
|
4089
|
+
...DEFAULT_REMESH_OPTIONS,
|
|
4090
|
+
...options,
|
|
4091
|
+
targetEdgeLength
|
|
4092
|
+
};
|
|
4093
|
+
this.state = {
|
|
4094
|
+
iteration: 0,
|
|
4095
|
+
edgeSplits: 0,
|
|
4096
|
+
edgeContractions: 0,
|
|
4097
|
+
edgeFlips: 0,
|
|
4098
|
+
vertexRelocations: 0,
|
|
4099
|
+
quality: computeMeshQuality(mesh)
|
|
4100
|
+
};
|
|
4101
|
+
if (!mesh.isManifold()) {
|
|
4102
|
+
this.skeleton = createSkeleton(mesh);
|
|
4103
|
+
this.constraints = createSkeletonConstraints(this.skeleton);
|
|
4104
|
+
}
|
|
4105
|
+
}
|
|
4106
|
+
/**
|
|
4107
|
+
* Runs one iteration of the remeshing algorithm.
|
|
4108
|
+
*
|
|
4109
|
+
* Each iteration performs:
|
|
4110
|
+
* 1. Split long edges
|
|
4111
|
+
* 2. Collapse short edges
|
|
4112
|
+
* 3. Flip edges for Delaunay
|
|
4113
|
+
* 4. Smooth vertex positions
|
|
4114
|
+
*/
|
|
4115
|
+
iterate() {
|
|
4116
|
+
this.state.iteration++;
|
|
4117
|
+
const targetLength = this.options.targetEdgeLength;
|
|
4118
|
+
const minLength = targetLength * this.options.minEdgeLengthRatio;
|
|
4119
|
+
const maxLength = targetLength * this.options.maxEdgeLengthRatio;
|
|
4120
|
+
const splitResult = splitLongEdges(this.mesh, maxLength);
|
|
4121
|
+
this.state.edgeSplits += splitResult.splitCount;
|
|
4122
|
+
const contractResult = contractShortEdges(this.mesh, minLength);
|
|
4123
|
+
this.state.edgeContractions += contractResult.contractCount;
|
|
4124
|
+
const flipCount = makeDelaunay(this.mesh);
|
|
4125
|
+
this.state.edgeFlips += flipCount;
|
|
4126
|
+
const smoothResult = smoothAllVertices(this.mesh, this.constraints ?? void 0, 0.5);
|
|
4127
|
+
this.state.vertexRelocations += smoothResult.smoothedCount;
|
|
4128
|
+
if (this.skeleton && (splitResult.splitCount > 0 || contractResult.contractCount > 0)) {
|
|
4129
|
+
this.skeleton.rebuild();
|
|
4130
|
+
}
|
|
4131
|
+
this.state.quality = computeMeshQuality(this.mesh, this.options.minTriangleQuality);
|
|
4132
|
+
if (this.options.verbose) {
|
|
4133
|
+
console.warn(
|
|
4134
|
+
`Iteration ${this.state.iteration}: splits=${splitResult.splitCount}, contractions=${contractResult.contractCount}, flips=${flipCount}, smoothed=${smoothResult.smoothedCount}, avgQuality=${this.state.quality.averageQuality.toFixed(3)}`
|
|
4135
|
+
);
|
|
4136
|
+
}
|
|
4137
|
+
return { ...this.state };
|
|
4138
|
+
}
|
|
4139
|
+
/**
|
|
4140
|
+
* Runs multiple iterations until convergence or max iterations.
|
|
4141
|
+
*/
|
|
4142
|
+
run(maxIterations) {
|
|
4143
|
+
const iterations = maxIterations ?? this.options.iterations;
|
|
4144
|
+
const startTime = Date.now();
|
|
4145
|
+
const inputStats = {
|
|
4146
|
+
vertices: this.mesh.vertexCount,
|
|
4147
|
+
faces: this.mesh.faceCount
|
|
4148
|
+
};
|
|
4149
|
+
for (let i = 0; i < iterations; i++) {
|
|
4150
|
+
const prevQuality = this.state.quality.averageQuality;
|
|
4151
|
+
this.iterate();
|
|
4152
|
+
const qualityImprovement = this.state.quality.averageQuality - prevQuality;
|
|
4153
|
+
if (Math.abs(qualityImprovement) < 1e-3 && i > 0) {
|
|
4154
|
+
if (this.options.verbose) {
|
|
4155
|
+
console.warn(`Converged after ${i + 1} iterations`);
|
|
4156
|
+
}
|
|
4157
|
+
break;
|
|
4158
|
+
}
|
|
4159
|
+
}
|
|
4160
|
+
const processingTimeMs = Date.now() - startTime;
|
|
4161
|
+
return {
|
|
4162
|
+
inputVertices: inputStats.vertices,
|
|
4163
|
+
inputFaces: inputStats.faces,
|
|
4164
|
+
outputVertices: this.mesh.vertexCount,
|
|
4165
|
+
outputFaces: this.mesh.faceCount,
|
|
4166
|
+
iterations: this.state.iteration,
|
|
4167
|
+
finalQuality: this.state.quality.averageQuality,
|
|
4168
|
+
nonManifoldEdges: this.mesh.getNonManifoldEdges().length,
|
|
4169
|
+
skeletonEdges: this.mesh.getSkeletonEdges().length,
|
|
4170
|
+
edgeFlips: this.state.edgeFlips,
|
|
4171
|
+
edgeSplits: this.state.edgeSplits,
|
|
4172
|
+
edgeContractions: this.state.edgeContractions,
|
|
4173
|
+
vertexRelocations: this.state.vertexRelocations,
|
|
4174
|
+
processingTimeMs
|
|
4175
|
+
};
|
|
4176
|
+
}
|
|
4177
|
+
/**
|
|
4178
|
+
* Checks if the remeshing has converged.
|
|
4179
|
+
*/
|
|
4180
|
+
hasConverged() {
|
|
4181
|
+
return this.state.quality.averageQuality > 0.9 || this.state.quality.poorQualityCount === 0;
|
|
4182
|
+
}
|
|
4183
|
+
/**
|
|
4184
|
+
* Gets the current mesh.
|
|
4185
|
+
*/
|
|
4186
|
+
getMesh() {
|
|
4187
|
+
return this.mesh;
|
|
4188
|
+
}
|
|
4189
|
+
/**
|
|
4190
|
+
* Gets the skeleton (if built).
|
|
4191
|
+
*/
|
|
4192
|
+
getSkeleton() {
|
|
4193
|
+
return this.skeleton;
|
|
4194
|
+
}
|
|
4195
|
+
/**
|
|
4196
|
+
* Gets the current quality stats.
|
|
4197
|
+
*/
|
|
4198
|
+
getQuality() {
|
|
4199
|
+
return this.state.quality;
|
|
4200
|
+
}
|
|
4201
|
+
/**
|
|
4202
|
+
* Gets the current state.
|
|
4203
|
+
*/
|
|
4204
|
+
getState() {
|
|
4205
|
+
return { ...this.state };
|
|
4206
|
+
}
|
|
4207
|
+
/**
|
|
4208
|
+
* Exports the mesh to BufferGeometry.
|
|
4209
|
+
*/
|
|
4210
|
+
toBufferGeometry() {
|
|
4211
|
+
return exportBufferGeometry(this.mesh);
|
|
4212
|
+
}
|
|
4213
|
+
}
|
|
4214
|
+
function remesh(geometry, options = {}) {
|
|
4215
|
+
const mesh = NonManifoldMesh.fromBufferGeometry(geometry, options.featureEdges);
|
|
4216
|
+
const remesher = new AdaptiveRemesher(mesh, options);
|
|
4217
|
+
const stats = remesher.run();
|
|
4218
|
+
const outputGeometry = remesher.toBufferGeometry();
|
|
4219
|
+
return { geometry: outputGeometry, stats };
|
|
4220
|
+
}
|
|
4221
|
+
function createRemesher(geometry, options = {}) {
|
|
4222
|
+
const mesh = NonManifoldMesh.fromBufferGeometry(geometry, options.featureEdges);
|
|
4223
|
+
return new AdaptiveRemesher(mesh, options);
|
|
4224
|
+
}
|
|
4225
|
+
export {
|
|
4226
|
+
AdaptiveRemesher,
|
|
4227
|
+
BVH,
|
|
4228
|
+
BufferGeometryExporter,
|
|
4229
|
+
BufferGeometryImporter,
|
|
4230
|
+
DEFAULT_REMESH_OPTIONS,
|
|
4231
|
+
Edge,
|
|
4232
|
+
EdgeContractor,
|
|
4233
|
+
EdgeFlipper,
|
|
4234
|
+
EdgeSplitter,
|
|
4235
|
+
EdgeType,
|
|
4236
|
+
Face,
|
|
4237
|
+
FeatureSkeleton,
|
|
4238
|
+
Halfedge,
|
|
4239
|
+
ManifoldAnalyzer,
|
|
4240
|
+
NonManifoldMesh,
|
|
4241
|
+
QualityMetrics,
|
|
4242
|
+
SkeletonBuilder,
|
|
4243
|
+
SkeletonConstraints,
|
|
4244
|
+
SkeletonSegment,
|
|
4245
|
+
SpatialHash,
|
|
4246
|
+
TopologyValidator,
|
|
4247
|
+
Vertex,
|
|
4248
|
+
VertexClassifier,
|
|
4249
|
+
VertexRelocator,
|
|
4250
|
+
VertexType,
|
|
4251
|
+
add,
|
|
4252
|
+
analyzeManifold,
|
|
4253
|
+
analyzeMesh,
|
|
4254
|
+
angleAtVertex,
|
|
4255
|
+
angleBetween,
|
|
4256
|
+
barycentricCoordinates,
|
|
4257
|
+
buildSkeleton,
|
|
4258
|
+
canContractEdge,
|
|
4259
|
+
canFlipEdge$1 as canFlipEdge,
|
|
4260
|
+
canFlipEdge as canFlipEdgeGeometric,
|
|
4261
|
+
canMoveFreely,
|
|
4262
|
+
classifyAllVertices,
|
|
4263
|
+
classifyVertex,
|
|
4264
|
+
computeMeshQuality,
|
|
4265
|
+
computeTangentialSmoothing,
|
|
4266
|
+
computeTargetEdgeLength,
|
|
4267
|
+
computeTriangleAspectRatio,
|
|
4268
|
+
contractEdge,
|
|
4269
|
+
contractShortEdges,
|
|
4270
|
+
cotangent,
|
|
4271
|
+
createBVHFromMesh,
|
|
4272
|
+
createEdgeId,
|
|
4273
|
+
createFaceId,
|
|
4274
|
+
createHalfedgeId,
|
|
4275
|
+
createRemesher,
|
|
4276
|
+
createSegmentId,
|
|
4277
|
+
createSkeleton,
|
|
4278
|
+
createSkeletonConstraints,
|
|
4279
|
+
createSpatialHash,
|
|
4280
|
+
createVertexId,
|
|
4281
|
+
cross,
|
|
4282
|
+
distance,
|
|
4283
|
+
distanceSquared,
|
|
4284
|
+
dot,
|
|
4285
|
+
exportBufferGeometry,
|
|
4286
|
+
exportClassificationGeometry,
|
|
4287
|
+
exportQualityGeometry,
|
|
4288
|
+
exportSkeletonGeometry,
|
|
4289
|
+
flipEdge,
|
|
4290
|
+
fromVector3,
|
|
4291
|
+
getHighValenceVertices,
|
|
4292
|
+
getLongEdges,
|
|
4293
|
+
getLowValenceVertices,
|
|
4294
|
+
getManifoldVertices,
|
|
4295
|
+
getNonManifoldVertices,
|
|
4296
|
+
getOpenBookVertices,
|
|
4297
|
+
getPoorQualityFaces,
|
|
4298
|
+
getShortEdges,
|
|
4299
|
+
getSkeletonBranchingVertices,
|
|
4300
|
+
getVerticesByType,
|
|
4301
|
+
importBufferGeometry,
|
|
4302
|
+
isDelaunay,
|
|
4303
|
+
isManifold,
|
|
4304
|
+
isPointInTriangle,
|
|
4305
|
+
isPositionFixed,
|
|
4306
|
+
isQuadConvex,
|
|
4307
|
+
isSkeletonConstrained,
|
|
4308
|
+
isSkeletonEdge,
|
|
4309
|
+
isTopologyValid,
|
|
4310
|
+
length,
|
|
4311
|
+
lengthSquared,
|
|
4312
|
+
lerp,
|
|
4313
|
+
makeDelaunay,
|
|
4314
|
+
midpoint,
|
|
4315
|
+
normalize,
|
|
4316
|
+
projectPointOnLine,
|
|
4317
|
+
projectPointOnSegment,
|
|
4318
|
+
reclassifyVertices,
|
|
4319
|
+
relocateVertex,
|
|
4320
|
+
remesh,
|
|
4321
|
+
scale,
|
|
4322
|
+
smoothAllVertices,
|
|
4323
|
+
smoothVertex,
|
|
4324
|
+
splitEdge,
|
|
4325
|
+
splitLongEdges,
|
|
4326
|
+
subtract,
|
|
4327
|
+
toNumber,
|
|
4328
|
+
triangleArea,
|
|
4329
|
+
triangleCentroid$1 as triangleCentroid,
|
|
4330
|
+
triangleCircumcenter,
|
|
4331
|
+
triangleCircumradius,
|
|
4332
|
+
triangleInradius,
|
|
4333
|
+
triangleNormal,
|
|
4334
|
+
triangleQuality,
|
|
4335
|
+
validateGeometry,
|
|
4336
|
+
validateTopology
|
|
4337
|
+
};
|
|
4338
|
+
//# sourceMappingURL=remesh-threejs.js.map
|