@manycore/aholo-splat-transform 1.2.8 → 1.2.10
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/CHANGELOG.md +124 -113
- package/README.md +39 -39
- package/THIRD_PARTY_LICENSES.txt +1373 -1373
- package/bin/cli.js +125 -118
- package/dist/SplatData.d.ts +67 -67
- package/dist/SplatData.js +167 -150
- package/dist/constant.d.ts +3 -3
- package/dist/constant.js +13 -13
- package/dist/file/IFile.d.ts +5 -5
- package/dist/file/IFile.js +1 -1
- package/dist/file/esz.d.ts +11 -11
- package/dist/file/esz.js +337 -322
- package/dist/file/index.d.ts +8 -8
- package/dist/file/index.js +7 -7
- package/dist/file/ksplat.d.ts +12 -12
- package/dist/file/ksplat.js +293 -231
- package/dist/file/lcc.d.ts +11 -11
- package/dist/file/lcc.js +161 -158
- package/dist/file/ply.d.ts +13 -13
- package/dist/file/ply.js +439 -390
- package/dist/file/sog.d.ts +80 -80
- package/dist/file/sog.js +525 -494
- package/dist/file/splat.d.ts +6 -6
- package/dist/file/splat.js +119 -99
- package/dist/file/spz.d.ts +11 -11
- package/dist/file/spz.js +597 -583
- package/dist/file/voxel.d.ts +43 -37
- package/dist/file/voxel.js +411 -280
- package/dist/index.d.ts +33 -33
- package/dist/index.js +54 -54
- package/dist/native/index.d.ts +54 -54
- package/dist/native/index.js +122 -129
- package/dist/native/utils.d.ts +1 -0
- package/dist/native/utils.js +54 -0
- package/dist/tasks/AutoChunkLodTask.d.ts +13 -13
- package/dist/tasks/AutoChunkLodTask.js +117 -117
- package/dist/tasks/AutoLodTask.d.ts +10 -10
- package/dist/tasks/AutoLodTask.js +20 -20
- package/dist/tasks/BaseTask.d.ts +15 -15
- package/dist/tasks/BaseTask.js +5 -5
- package/dist/tasks/FlexLodTask.d.ts +12 -12
- package/dist/tasks/FlexLodTask.js +54 -44
- package/dist/tasks/ModifyTask.d.ts +9 -9
- package/dist/tasks/ModifyTask.js +166 -156
- package/dist/tasks/ReadTask.d.ts +9 -9
- package/dist/tasks/ReadTask.js +29 -29
- package/dist/tasks/SkeletonLodTask.d.ts +10 -10
- package/dist/tasks/SkeletonLodTask.js +176 -156
- package/dist/tasks/VoxelTask.d.ts +35 -30
- package/dist/tasks/VoxelTask.js +40 -37
- package/dist/tasks/WriteTask.d.ts +12 -12
- package/dist/tasks/WriteTask.js +70 -70
- package/dist/utils/BufferReader.d.ts +12 -12
- package/dist/utils/BufferReader.js +45 -45
- package/dist/utils/Logger.d.ts +11 -11
- package/dist/utils/Logger.js +40 -40
- package/dist/utils/StreamChunkDecoder.d.ts +16 -16
- package/dist/utils/StreamChunkDecoder.js +31 -31
- package/dist/utils/index.d.ts +27 -27
- package/dist/utils/index.js +101 -101
- package/dist/utils/k-means.d.ts +4 -4
- package/dist/utils/k-means.js +340 -341
- package/dist/utils/math.d.ts +46 -46
- package/dist/utils/math.js +350 -346
- package/dist/utils/quantize-1d.d.ts +4 -4
- package/dist/utils/quantize-1d.js +164 -164
- package/dist/utils/sh-rotate.d.ts +2 -2
- package/dist/utils/sh-rotate.js +236 -175
- package/dist/utils/splat.d.ts +21 -21
- package/dist/utils/splat.js +397 -387
- package/dist/utils/voxel/binary.d.ts +8 -0
- package/dist/utils/voxel/binary.js +176 -0
- package/dist/utils/voxel/common.d.ts +178 -162
- package/dist/utils/voxel/common.js +1752 -1682
- package/dist/utils/voxel/coplanar-merge.d.ts +63 -63
- package/dist/utils/voxel/coplanar-merge.js +818 -819
- package/dist/utils/voxel/filter-cluster.d.ts +20 -0
- package/dist/utils/voxel/filter-cluster.js +628 -0
- package/dist/utils/voxel/gpu-dilation.d.ts +2 -2
- package/dist/utils/voxel/gpu-dilation.js +677 -656
- package/dist/utils/voxel/marching-cubes.d.ts +42 -42
- package/dist/utils/voxel/marching-cubes.js +1645 -1657
- package/dist/utils/voxel/mesh.d.ts +3 -3
- package/dist/utils/voxel/mesh.js +130 -130
- package/dist/utils/voxel/nav.d.ts +29 -29
- package/dist/utils/voxel/nav.js +1068 -1043
- package/dist/utils/voxel/postprocess.d.ts +23 -23
- package/dist/utils/voxel/postprocess.js +408 -375
- package/dist/utils/voxel/voxel-faces.d.ts +18 -18
- package/dist/utils/voxel/voxel-faces.js +662 -663
- package/dist/utils/voxel/voxelize.d.ts +34 -33
- package/dist/utils/voxel/voxelize.js +1208 -1193
- package/dist/utils/webgpu.d.ts +8 -8
- package/dist/utils/webgpu.js +122 -122
- package/package.json +38 -39
- package/dist/native/cpp/bin/linux/binding.node +0 -0
- package/dist/native/cpp/bin/windows/binding.node +0 -0
|
@@ -1,819 +1,818 @@
|
|
|
1
|
-
const NORMAL_EPS = 1e-3;
|
|
2
|
-
const PLANE_REL_EPS = 1e-3;
|
|
3
|
-
const COLLINEAR_REL_EPS = 1e-3;
|
|
4
|
-
/**
|
|
5
|
-
* Losslessly reduce coplanar regions of a marching-cubes mesh by
|
|
6
|
-
* topology-preserving vertex removal.
|
|
7
|
-
*
|
|
8
|
-
* For a closed manifold MC mesh, a vertex `v` is "lossless-removable" iff
|
|
9
|
-
* its incident-tri fan, walked in cyclic order, falls into one of:
|
|
10
|
-
*
|
|
11
|
-
* 1. K=1 coplanar fan. Every triangle in v's fan lies on the same plane
|
|
12
|
-
* (same unit normal and same plane offset, within tolerance). Removing
|
|
13
|
-
* v is the inverse of vertex split: re-triangulate the boundary
|
|
14
|
-
* polygon in the same plane.
|
|
15
|
-
*
|
|
16
|
-
* 2. K=2 collinear seam. The fan splits into exactly two contiguous
|
|
17
|
-
* coplanar arcs (different planes). The two crease vertices `a` and
|
|
18
|
-
* `b` (the boundary points where the plane changes around v) are
|
|
19
|
-
* collinear with v in 3D, with v between them. Removing v collapses
|
|
20
|
-
* the two crease edges (v-a, v-b) into a single straight edge (a-b)
|
|
21
|
-
* that lies in both planes; each arc's polygon re-triangulates
|
|
22
|
-
* without v.
|
|
23
|
-
*
|
|
24
|
-
* Vertices with K >= 3 (multi-way corners) are kept.
|
|
25
|
-
*
|
|
26
|
-
* Removing a removable v is exact-lossless: the surface footprint is
|
|
27
|
-
* identical, no vertex moves and none are created. The transformation
|
|
28
|
-
* is the inverse of vertex split, so it is topology-preserving by
|
|
29
|
-
* construction:
|
|
30
|
-
*
|
|
31
|
-
* - No T-junctions. Every old vertex on the polygon boundary remains a
|
|
32
|
-
* vertex of every triangle that previously touched it. Adjacent fused
|
|
33
|
-
* regions and verbatim regions stay coupled at every shared vertex.
|
|
34
|
-
* - Watertight. The closed manifold structure is preserved across
|
|
35
|
-
* removal. Both the K=1 and K=2 cases preserve the K=2 seam edge as
|
|
36
|
-
* a single shared edge between the two plane groups.
|
|
37
|
-
* - Bit-exact. Every output position is a verbatim copy of an input
|
|
38
|
-
* position; no vertex is fabricated.
|
|
39
|
-
*
|
|
40
|
-
* Algorithm:
|
|
41
|
-
*
|
|
42
|
-
* 1. Build per-vertex incident-tri lists and per-tri normalized normals
|
|
43
|
-
* and plane offsets.
|
|
44
|
-
* 2. Process vertices via a dirty-flag worklist. Initially queue every
|
|
45
|
-
* vertex; after a successful removal, re-queue the ring neighbours
|
|
46
|
-
* so chains of K=1 / K=2 vertices collapse in one run.
|
|
47
|
-
* 3. For each dequeued vertex `v`:
|
|
48
|
-
* a. Walk the fan to extract the cyclic ring vertices and the
|
|
49
|
-
* cyclic ordered tris (each tri (v, ring[i], ring[(i+1)%k]) is
|
|
50
|
-
* the i-th tri in fan order).
|
|
51
|
-
* b. Decide K. If all tris share a plane: K=1. Otherwise count
|
|
52
|
-
* transitions in cyclic order; K=2 if exactly two arcs.
|
|
53
|
-
* c. K>=3: skip. K=2: verify ring[i1], v, ring[i2] are collinear.
|
|
54
|
-
* d. For each arc, project its polygon to 2D using the arc's
|
|
55
|
-
* plane basis, ear-clip, and append the new tris.
|
|
56
|
-
* e. Mark v's old tris dead, register the new tris in each polygon
|
|
57
|
-
* vertex's incident list, and re-queue the ring.
|
|
58
|
-
* 4. Compact: drop dead tris and unused vertices, remap indices.
|
|
59
|
-
*
|
|
60
|
-
* @param mesh - Input triangle mesh from {@link marchingCubes}.
|
|
61
|
-
* @param voxelResolution - Size of one voxel in world units. Used to scale the plane-offset tolerance. (The K=2 collinearity check is purely angular and has no voxel-scaled term.)
|
|
62
|
-
* @returns A new mesh with the same surface geometry, no T-junctions, and far fewer triangles.
|
|
63
|
-
*/
|
|
64
|
-
|
|
65
|
-
const { positions, indices } = mesh;
|
|
66
|
-
const inputTriCount = (indices.length / 3) | 0;
|
|
67
|
-
const vertCount = (positions.length / 3) | 0;
|
|
68
|
-
if (inputTriCount === 0) {
|
|
69
|
-
return { positions: new Float32Array(0), indices: new Uint32Array(0) };
|
|
70
|
-
}
|
|
71
|
-
// Plane-offset tolerance for the coplanarity test. An absolute floor
|
|
72
|
-
// (voxelResolution * PLANE_REL_EPS) handles near-origin planes, while
|
|
73
|
-
// a relative term scaled by max(|da|, |db|) handles large-offset
|
|
74
|
-
// planes where Float32 position precision in d = n . p causes
|
|
75
|
-
// relative error proportional to |d|. Without the relative term,
|
|
76
|
-
// coplanar fans far from the origin would be seen as distinct planes.
|
|
77
|
-
|
|
78
|
-
const absA = da < 0 ? -da : da;
|
|
79
|
-
const absB = db < 0 ? -db : db;
|
|
80
|
-
return PLANE_REL_EPS * (voxelResolution + (absA > absB ? absA : absB));
|
|
81
|
-
}
|
|
82
|
-
// Mutable triangle table. Flat typed arrays so we can append new tris
|
|
83
|
-
// (from ear-clipping) without per-tri allocations and without hitting
|
|
84
|
-
// V8's regular-Array backing-store size cap on large meshes.
|
|
85
|
-
// `triVertsArr` indexes are 3*t, everything else is t.
|
|
86
|
-
//
|
|
87
|
-
// Normals are stored as Float32 (unit vectors; ample precision for the
|
|
88
|
-
// dot-product coplanarity test against `1 - NORMAL_EPS`). The plane
|
|
89
|
-
// offset stays Float64 since it's an absolute world-space scalar that
|
|
90
|
-
// can be large for distant scenes.
|
|
91
|
-
let triCap = inputTriCount;
|
|
92
|
-
let triVertsArr = new Uint32Array(triCap * 3);
|
|
93
|
-
let triNxArr = new Float32Array(triCap);
|
|
94
|
-
let triNyArr = new Float32Array(triCap);
|
|
95
|
-
let triNzArr = new Float32Array(triCap);
|
|
96
|
-
let triDArr = new Float64Array(triCap);
|
|
97
|
-
let triAliveArr = new Uint8Array(triCap);
|
|
98
|
-
let triCount = inputTriCount;
|
|
99
|
-
// Capacity-doubling appenders for new tris generated by ear-clipping.
|
|
100
|
-
|
|
101
|
-
if (triCount < triCap) {
|
|
102
|
-
return;
|
|
103
|
-
}
|
|
104
|
-
const newCap = triCap * 2;
|
|
105
|
-
|
|
106
|
-
const out = new Float32Array(newCap);
|
|
107
|
-
out.set(src);
|
|
108
|
-
return out;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
const out = new Float64Array(newCap);
|
|
112
|
-
out.set(src);
|
|
113
|
-
return out;
|
|
114
|
-
}
|
|
115
|
-
const newVerts = new Uint32Array(newCap * 3);
|
|
116
|
-
newVerts.set(triVertsArr);
|
|
117
|
-
triVertsArr = newVerts;
|
|
118
|
-
triNxArr = growF32(triNxArr);
|
|
119
|
-
triNyArr = growF32(triNyArr);
|
|
120
|
-
triNzArr = growF32(triNzArr);
|
|
121
|
-
triDArr = growF64(triDArr);
|
|
122
|
-
const aliveOut = new Uint8Array(newCap);
|
|
123
|
-
aliveOut.set(triAliveArr);
|
|
124
|
-
triAliveArr = aliveOut;
|
|
125
|
-
triCap = newCap;
|
|
126
|
-
}
|
|
127
|
-
// Pass 0: compute per-tri normalized normal and plane offset.
|
|
128
|
-
for (let t = 0; t < inputTriCount; t++) {
|
|
129
|
-
const ia = indices[t * 3];
|
|
130
|
-
const ib = indices[t * 3 + 1];
|
|
131
|
-
const ic = indices[t * 3 + 2];
|
|
132
|
-
triVertsArr[t * 3] = ia;
|
|
133
|
-
triVertsArr[t * 3 + 1] = ib;
|
|
134
|
-
triVertsArr[t * 3 + 2] = ic;
|
|
135
|
-
const ax = positions[ia * 3];
|
|
136
|
-
const ay = positions[ia * 3 + 1];
|
|
137
|
-
const az = positions[ia * 3 + 2];
|
|
138
|
-
const bx = positions[ib * 3];
|
|
139
|
-
const by = positions[ib * 3 + 1];
|
|
140
|
-
const bz = positions[ib * 3 + 2];
|
|
141
|
-
const cx = positions[ic * 3];
|
|
142
|
-
const cy = positions[ic * 3 + 1];
|
|
143
|
-
const cz = positions[ic * 3 + 2];
|
|
144
|
-
const ex = bx - ax, ey = by - ay, ez = bz - az;
|
|
145
|
-
const fx = cx - ax, fy = cy - ay, fz = cz - az;
|
|
146
|
-
let nx = ey * fz - ez * fy;
|
|
147
|
-
let ny = ez * fx - ex * fz;
|
|
148
|
-
let nz = ex * fy - ey * fx;
|
|
149
|
-
const nLen = Math.sqrt(nx * nx + ny * ny + nz * nz);
|
|
150
|
-
if (nLen < 1e-12) {
|
|
151
|
-
// Degenerate input tri; drop it from the active set.
|
|
152
|
-
triAliveArr[t] = 0;
|
|
153
|
-
continue;
|
|
154
|
-
}
|
|
155
|
-
const inv = 1 / nLen;
|
|
156
|
-
nx *= inv;
|
|
157
|
-
ny *= inv;
|
|
158
|
-
nz *= inv;
|
|
159
|
-
triNxArr[t] = nx;
|
|
160
|
-
triNyArr[t] = ny;
|
|
161
|
-
triNzArr[t] = nz;
|
|
162
|
-
triDArr[t] = nx * ax + ny * ay + nz * az;
|
|
163
|
-
triAliveArr[t] = 1;
|
|
164
|
-
}
|
|
165
|
-
// Per-vertex incident-tri lists, stored as a singly-linked free-listed
|
|
166
|
-
// node pool backed by typed arrays. Each pool node `n` holds:
|
|
167
|
-
// poolTri[n] - the incident tri index
|
|
168
|
-
// poolNext[n] - next node in the same vertex's list (or -1 = end)
|
|
169
|
-
// `vertHead[v]` is the head node for vertex v (or -1 if v has no
|
|
170
|
-
// incident tris). Removed nodes are pushed onto the `freeHead` chain
|
|
171
|
-
// for reuse, so the pool's max occupancy is the initial fan-mention
|
|
172
|
-
// count (3 * inputTriCount): the worklist's K=1/K=2 collapses are
|
|
173
|
-
// monotonically tri-reducing, so the free list serves all subsequent
|
|
174
|
-
// ear-clip allocations without ever growing the backing arrays.
|
|
175
|
-
//
|
|
176
|
-
// Footprint: 8 B/node * 3 * inputTriCount + 4 B/vert * vertCount.
|
|
177
|
-
// For 25.7M raw tris / 12.9M raw verts that's ~615 MB pool +
|
|
178
|
-
// ~52 MB heads, vs ~1.4 GB for the prior `number[][]` adjacency
|
|
179
|
-
// (V8 Array headers, FixedArray headers, hidden classes per inner
|
|
180
|
-
// array all inflate the boxed-SMI payload).
|
|
181
|
-
const poolCap = inputTriCount * 3;
|
|
182
|
-
const poolTri = new Int32Array(poolCap);
|
|
183
|
-
const poolNext = new Int32Array(poolCap);
|
|
184
|
-
let poolLen = 0;
|
|
185
|
-
let freeHead = -1;
|
|
186
|
-
const vertHead = new Int32Array(vertCount).fill(-1);
|
|
187
|
-
|
|
188
|
-
if (freeHead !== -1) {
|
|
189
|
-
const n = freeHead;
|
|
190
|
-
freeHead = poolNext[n];
|
|
191
|
-
return n;
|
|
192
|
-
}
|
|
193
|
-
return poolLen++;
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
poolNext[n] = freeHead;
|
|
197
|
-
freeHead = n;
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
const n = allocNode();
|
|
201
|
-
poolTri[n] = tri;
|
|
202
|
-
poolNext[n] = vertHead[v];
|
|
203
|
-
vertHead[v] = n;
|
|
204
|
-
}
|
|
205
|
-
// O(degree) removal of `tri` from `v`'s incident list. Returns true
|
|
206
|
-
// if found and removed.
|
|
207
|
-
|
|
208
|
-
let prev = -1;
|
|
209
|
-
let cur = vertHead[v];
|
|
210
|
-
while (cur !== -1) {
|
|
211
|
-
if (poolTri[cur] === tri) {
|
|
212
|
-
const nxt = poolNext[cur];
|
|
213
|
-
if (prev === -1) {
|
|
214
|
-
vertHead[v] = nxt;
|
|
215
|
-
}
|
|
216
|
-
else {
|
|
217
|
-
poolNext[prev] = nxt;
|
|
218
|
-
}
|
|
219
|
-
freeNode(cur);
|
|
220
|
-
return true;
|
|
221
|
-
}
|
|
222
|
-
prev = cur;
|
|
223
|
-
cur = poolNext[cur];
|
|
224
|
-
}
|
|
225
|
-
return false;
|
|
226
|
-
}
|
|
227
|
-
for (let t = 0; t < inputTriCount; t++) {
|
|
228
|
-
if (triAliveArr[t] === 0) {
|
|
229
|
-
continue;
|
|
230
|
-
}
|
|
231
|
-
addTriToVert(triVertsArr[t * 3], t);
|
|
232
|
-
addTriToVert(triVertsArr[t * 3 + 1], t);
|
|
233
|
-
addTriToVert(triVertsArr[t * 3 + 2], t);
|
|
234
|
-
}
|
|
235
|
-
// Build a right-handed tangent / bitangent basis (t, b) on the plane
|
|
236
|
-
// with normal n, picking the cardinal axis least aligned with n to
|
|
237
|
-
// avoid precision loss in the cross product. By construction
|
|
238
|
-
// t x b = n, so a polygon traced CCW around +n in 3D projects to a
|
|
239
|
-
// CCW polygon in (t, b) coordinates (positive 2D signed area).
|
|
240
|
-
|
|
241
|
-
const ax = Math.abs(nx);
|
|
242
|
-
const ay = Math.abs(ny);
|
|
243
|
-
const az = Math.abs(nz);
|
|
244
|
-
let tx, ty, tz;
|
|
245
|
-
if (ax <= ay && ax <= az) {
|
|
246
|
-
// X axis least aligned: t = (1, 0, 0) x n
|
|
247
|
-
tx = 0;
|
|
248
|
-
ty = -nz;
|
|
249
|
-
tz = ny;
|
|
250
|
-
}
|
|
251
|
-
else if (ay <= az) {
|
|
252
|
-
// Y axis least aligned: t = (0, 1, 0) x n
|
|
253
|
-
tx = nz;
|
|
254
|
-
ty = 0;
|
|
255
|
-
tz = -nx;
|
|
256
|
-
}
|
|
257
|
-
else {
|
|
258
|
-
// Z axis least aligned: t = (0, 0, 1) x n
|
|
259
|
-
tx = -ny;
|
|
260
|
-
ty = nx;
|
|
261
|
-
tz = 0;
|
|
262
|
-
}
|
|
263
|
-
const tlen = Math.sqrt(tx * tx + ty * ty + tz * tz);
|
|
264
|
-
const inv = 1 / tlen;
|
|
265
|
-
tx *= inv;
|
|
266
|
-
ty *= inv;
|
|
267
|
-
tz *= inv;
|
|
268
|
-
// bitangent = n x t (unit length because n and t are unit and
|
|
269
|
-
// perpendicular).
|
|
270
|
-
const bx = ny * tz - nz * ty;
|
|
271
|
-
const by = nz * tx - nx * tz;
|
|
272
|
-
const bz = nx * ty - ny * tx;
|
|
273
|
-
return [tx, ty, tz, bx, by, bz];
|
|
274
|
-
}
|
|
275
|
-
// Reusable scratch buffers for extractFan. With typical MC fan sizes
|
|
276
|
-
// (k <= ~12), a linear scan over parallel typed arrays is cheaper than
|
|
277
|
-
// the Map-of-Map-of-Set allocations the previous version paid per
|
|
278
|
-
// vertex, and saves ~3 allocations per worklist iteration on meshes
|
|
279
|
-
// with tens of millions of vertices.
|
|
280
|
-
let fanScratchCap = 16;
|
|
281
|
-
let fanFromScratch = new Int32Array(fanScratchCap);
|
|
282
|
-
let fanToScratch = new Int32Array(fanScratchCap);
|
|
283
|
-
let fanTriScratch = new Int32Array(fanScratchCap);
|
|
284
|
-
let fanRingScratch = new Int32Array(fanScratchCap);
|
|
285
|
-
let fanTrisScratch = new Int32Array(fanScratchCap);
|
|
286
|
-
|
|
287
|
-
if (need <= fanScratchCap) {
|
|
288
|
-
return;
|
|
289
|
-
}
|
|
290
|
-
let c = fanScratchCap;
|
|
291
|
-
while (c < need) {
|
|
292
|
-
c *= 2;
|
|
293
|
-
}
|
|
294
|
-
fanFromScratch = new Int32Array(c);
|
|
295
|
-
fanToScratch = new Int32Array(c);
|
|
296
|
-
fanTriScratch = new Int32Array(c);
|
|
297
|
-
fanRingScratch = new Int32Array(c);
|
|
298
|
-
fanTrisScratch = new Int32Array(c);
|
|
299
|
-
fanScratchCap = c;
|
|
300
|
-
}
|
|
301
|
-
// Walk v's fan to extract its cyclic boundary polygon AND the matching
|
|
302
|
-
// cyclic ordered tris. ring[i] is the "from" vertex of the i-th tri
|
|
303
|
-
// (in fan order), and tris[i] = (v, ring[i], ring[(i+1) % k]) is the
|
|
304
|
-
// tri whose two non-v vertices are ring[i] and ring[(i+1) % k].
|
|
305
|
-
// Returns null when the fan is non-manifold (duplicate from-vertex,
|
|
306
|
-
// closes prematurely, or fails to close).
|
|
307
|
-
//
|
|
308
|
-
// Output aliases the module-level `fanRingScratch` / `fanTrisScratch`
|
|
309
|
-
// buffers; the caller must consume them before the next extractFan
|
|
310
|
-
// call. `k` is returned explicitly since the scratch buffers may be
|
|
311
|
-
// oversized.
|
|
312
|
-
|
|
313
|
-
let k = 0;
|
|
314
|
-
for (let n = vertHead[v]; n !== -1; n = poolNext[n]) {
|
|
315
|
-
k++;
|
|
316
|
-
}
|
|
317
|
-
if (k < 3) {
|
|
318
|
-
return -1;
|
|
319
|
-
}
|
|
320
|
-
growFanScratch(k);
|
|
321
|
-
// Collect (from, to, tri) triples into parallel scratch arrays.
|
|
322
|
-
// The O(k^2) duplicate-from scan here and the O(k^2) cyclic walk
|
|
323
|
-
// below are cheap for small k and avoid per-vertex Map allocs.
|
|
324
|
-
let i = 0;
|
|
325
|
-
for (let n = vertHead[v]; n !== -1; n = poolNext[n]) {
|
|
326
|
-
const t = poolTri[n];
|
|
327
|
-
const a = triVertsArr[t * 3];
|
|
328
|
-
const b = triVertsArr[t * 3 + 1];
|
|
329
|
-
const c = triVertsArr[t * 3 + 2];
|
|
330
|
-
let from, to;
|
|
331
|
-
if (a === v) {
|
|
332
|
-
from = b;
|
|
333
|
-
to = c;
|
|
334
|
-
}
|
|
335
|
-
else if (b === v) {
|
|
336
|
-
from = c;
|
|
337
|
-
to = a;
|
|
338
|
-
}
|
|
339
|
-
else {
|
|
340
|
-
from = a;
|
|
341
|
-
to = b;
|
|
342
|
-
}
|
|
343
|
-
for (let j = 0; j < i; j++) {
|
|
344
|
-
if (fanFromScratch[j] === from) {
|
|
345
|
-
return -1;
|
|
346
|
-
}
|
|
347
|
-
}
|
|
348
|
-
fanFromScratch[i] = from;
|
|
349
|
-
fanToScratch[i] = to;
|
|
350
|
-
fanTriScratch[i] = t;
|
|
351
|
-
i++;
|
|
352
|
-
}
|
|
353
|
-
const start = fanFromScratch[0];
|
|
354
|
-
let cur = start;
|
|
355
|
-
for (let step = 0; step < k; step++) {
|
|
356
|
-
fanRingScratch[step] = cur;
|
|
357
|
-
let found = -1;
|
|
358
|
-
for (let j = 0; j < k; j++) {
|
|
359
|
-
if (fanFromScratch[j] === cur) {
|
|
360
|
-
found = j;
|
|
361
|
-
break;
|
|
362
|
-
}
|
|
363
|
-
}
|
|
364
|
-
if (found === -1) {
|
|
365
|
-
return -1;
|
|
366
|
-
}
|
|
367
|
-
fanTrisScratch[step] = fanTriScratch[found];
|
|
368
|
-
const next = fanToScratch[found];
|
|
369
|
-
// Premature cycle close => fan is non-manifold (multi-component).
|
|
370
|
-
if (next === start && step < k - 1) {
|
|
371
|
-
return -1;
|
|
372
|
-
}
|
|
373
|
-
cur = next;
|
|
374
|
-
}
|
|
375
|
-
if (cur !== start) {
|
|
376
|
-
return -1;
|
|
377
|
-
}
|
|
378
|
-
return k;
|
|
379
|
-
}
|
|
380
|
-
// Reusable scratch for earClip's doubly-linked polygon traversal.
|
|
381
|
-
// Grows to the largest polygon seen and is reused across every
|
|
382
|
-
// worklist iteration.
|
|
383
|
-
let earPrevScratch = new Int32Array(16);
|
|
384
|
-
let earNextScratch = new Int32Array(16);
|
|
385
|
-
|
|
386
|
-
if (n <= earPrevScratch.length) {
|
|
387
|
-
return;
|
|
388
|
-
}
|
|
389
|
-
let c = earPrevScratch.length;
|
|
390
|
-
while (c < n) {
|
|
391
|
-
c *= 2;
|
|
392
|
-
}
|
|
393
|
-
earPrevScratch = new Int32Array(c);
|
|
394
|
-
earNextScratch = new Int32Array(c);
|
|
395
|
-
}
|
|
396
|
-
// Ear-clip a planar simple polygon. `px, py` are the projected 2D
|
|
397
|
-
// coordinates of the first `n` polygon vertices (in CCW order); the
|
|
398
|
-
// arrays may be larger than `n` (scratch-buffer aliasing is fine,
|
|
399
|
-
// only [0, n) is read). Writes (n - 2) * 3 polygon-relative vertex
|
|
400
|
-
// indices into `out[outOffset..]` and returns the number of indices
|
|
401
|
-
// written on success, or -1 if the polygon is degenerate / not
|
|
402
|
-
// simple.
|
|
403
|
-
//
|
|
404
|
-
// Strictly-collinear interior vertices (cross product == 0) are not
|
|
405
|
-
// considered convex ear apices and so produce slivers as side
|
|
406
|
-
// vertices of neighbouring ears. These are transient: any such
|
|
407
|
-
// vertex is itself K=1 in the same plane and the worklist removes
|
|
408
|
-
// it in a subsequent iteration, replacing the slivers with a clean
|
|
409
|
-
// re-triangulation of its updated fan. An in-earClip pre-pass that
|
|
410
|
-
// drops the vertex from this polygon would create a T-junction
|
|
411
|
-
// with the vertex's other incident tris (which still reference it),
|
|
412
|
-
// so we leave the cleanup to the worklist.
|
|
413
|
-
|
|
414
|
-
if (n < 3) {
|
|
415
|
-
return -1;
|
|
416
|
-
}
|
|
417
|
-
if (n === 3) {
|
|
418
|
-
out[outOffset] = 0;
|
|
419
|
-
out[outOffset + 1] = 1;
|
|
420
|
-
out[outOffset + 2] = 2;
|
|
421
|
-
return 3;
|
|
422
|
-
}
|
|
423
|
-
// Verify CCW orientation. By construction (right-handed basis)
|
|
424
|
-
// valid input polygons are CCW with positive signed area.
|
|
425
|
-
let area2 = 0;
|
|
426
|
-
for (let i = 0; i < n; i++) {
|
|
427
|
-
const j = (i + 1) % n;
|
|
428
|
-
area2 += px[i] * py[j] - px[j] * py[i];
|
|
429
|
-
}
|
|
430
|
-
if (area2 <= 0) {
|
|
431
|
-
return -1;
|
|
432
|
-
}
|
|
433
|
-
growEarInternalScratch(n);
|
|
434
|
-
const prev = earPrevScratch;
|
|
435
|
-
const next = earNextScratch;
|
|
436
|
-
for (let i = 0; i < n; i++) {
|
|
437
|
-
prev[i] = (i - 1 + n) % n;
|
|
438
|
-
next[i] = (i + 1) % n;
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
return (px[b] - px[a]) * (py[c] - py[a]) -
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
const
|
|
446
|
-
const
|
|
447
|
-
const
|
|
448
|
-
const
|
|
449
|
-
const
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
let
|
|
467
|
-
let
|
|
468
|
-
let
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
const
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
out[outOffset + resultLen++] =
|
|
478
|
-
out[outOffset + resultLen++] =
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
out[outOffset + resultLen++] =
|
|
492
|
-
out[outOffset + resultLen++] = i;
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
//
|
|
497
|
-
//
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
let
|
|
501
|
-
let
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
grown
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
//
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
//
|
|
529
|
-
//
|
|
530
|
-
// the
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
const
|
|
535
|
-
const
|
|
536
|
-
|
|
537
|
-
//
|
|
538
|
-
//
|
|
539
|
-
//
|
|
540
|
-
//
|
|
541
|
-
//
|
|
542
|
-
|
|
543
|
-
const
|
|
544
|
-
const
|
|
545
|
-
const
|
|
546
|
-
const
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
//
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
const
|
|
589
|
-
|
|
590
|
-
//
|
|
591
|
-
|
|
592
|
-
const
|
|
593
|
-
const
|
|
594
|
-
const
|
|
595
|
-
const
|
|
596
|
-
|
|
597
|
-
let
|
|
598
|
-
|
|
599
|
-
const
|
|
600
|
-
const
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
//
|
|
617
|
-
//
|
|
618
|
-
|
|
619
|
-
let
|
|
620
|
-
let
|
|
621
|
-
let
|
|
622
|
-
|
|
623
|
-
const
|
|
624
|
-
const
|
|
625
|
-
const
|
|
626
|
-
const
|
|
627
|
-
const
|
|
628
|
-
const
|
|
629
|
-
const
|
|
630
|
-
const
|
|
631
|
-
const
|
|
632
|
-
const
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
nTransitions
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
//
|
|
651
|
-
|
|
652
|
-
const
|
|
653
|
-
const
|
|
654
|
-
const
|
|
655
|
-
const
|
|
656
|
-
const
|
|
657
|
-
const
|
|
658
|
-
const
|
|
659
|
-
const
|
|
660
|
-
const
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
//
|
|
670
|
-
//
|
|
671
|
-
|
|
672
|
-
const
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
//
|
|
683
|
-
//
|
|
684
|
-
|
|
685
|
-
let
|
|
686
|
-
|
|
687
|
-
const
|
|
688
|
-
const
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
const
|
|
696
|
-
const
|
|
697
|
-
const
|
|
698
|
-
const
|
|
699
|
-
const
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
const
|
|
703
|
-
const
|
|
704
|
-
const
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
//
|
|
720
|
-
//
|
|
721
|
-
//
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
const
|
|
742
|
-
const
|
|
743
|
-
const
|
|
744
|
-
const
|
|
745
|
-
const
|
|
746
|
-
const
|
|
747
|
-
const
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
const
|
|
751
|
-
const
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
triVertsArr[newT * 3] =
|
|
756
|
-
triVertsArr[newT * 3 +
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
addTriToVert(
|
|
764
|
-
addTriToVert(
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
//
|
|
769
|
-
//
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
let
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
usedVerts[triVertsArr[t * 3]] = 1;
|
|
784
|
-
usedVerts[triVertsArr[t * 3 +
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
outPositions[o] = positions[v * 3];
|
|
804
|
-
outPositions[o +
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
let
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
outIndices[oi++] = vertRemap[triVertsArr[t * 3]];
|
|
814
|
-
outIndices[oi++] = vertRemap[triVertsArr[t * 3 +
|
|
815
|
-
|
|
816
|
-
}
|
|
817
|
-
|
|
818
|
-
};
|
|
819
|
-
export { coplanarMerge };
|
|
1
|
+
const NORMAL_EPS = 1e-3;
|
|
2
|
+
const PLANE_REL_EPS = 1e-3;
|
|
3
|
+
const COLLINEAR_REL_EPS = 1e-3;
|
|
4
|
+
/**
|
|
5
|
+
* Losslessly reduce coplanar regions of a marching-cubes mesh by
|
|
6
|
+
* topology-preserving vertex removal.
|
|
7
|
+
*
|
|
8
|
+
* For a closed manifold MC mesh, a vertex `v` is "lossless-removable" iff
|
|
9
|
+
* its incident-tri fan, walked in cyclic order, falls into one of:
|
|
10
|
+
*
|
|
11
|
+
* 1. K=1 coplanar fan. Every triangle in v's fan lies on the same plane
|
|
12
|
+
* (same unit normal and same plane offset, within tolerance). Removing
|
|
13
|
+
* v is the inverse of vertex split: re-triangulate the boundary
|
|
14
|
+
* polygon in the same plane.
|
|
15
|
+
*
|
|
16
|
+
* 2. K=2 collinear seam. The fan splits into exactly two contiguous
|
|
17
|
+
* coplanar arcs (different planes). The two crease vertices `a` and
|
|
18
|
+
* `b` (the boundary points where the plane changes around v) are
|
|
19
|
+
* collinear with v in 3D, with v between them. Removing v collapses
|
|
20
|
+
* the two crease edges (v-a, v-b) into a single straight edge (a-b)
|
|
21
|
+
* that lies in both planes; each arc's polygon re-triangulates
|
|
22
|
+
* without v.
|
|
23
|
+
*
|
|
24
|
+
* Vertices with K >= 3 (multi-way corners) are kept.
|
|
25
|
+
*
|
|
26
|
+
* Removing a removable v is exact-lossless: the surface footprint is
|
|
27
|
+
* identical, no vertex moves and none are created. The transformation
|
|
28
|
+
* is the inverse of vertex split, so it is topology-preserving by
|
|
29
|
+
* construction:
|
|
30
|
+
*
|
|
31
|
+
* - No T-junctions. Every old vertex on the polygon boundary remains a
|
|
32
|
+
* vertex of every triangle that previously touched it. Adjacent fused
|
|
33
|
+
* regions and verbatim regions stay coupled at every shared vertex.
|
|
34
|
+
* - Watertight. The closed manifold structure is preserved across
|
|
35
|
+
* removal. Both the K=1 and K=2 cases preserve the K=2 seam edge as
|
|
36
|
+
* a single shared edge between the two plane groups.
|
|
37
|
+
* - Bit-exact. Every output position is a verbatim copy of an input
|
|
38
|
+
* position; no vertex is fabricated.
|
|
39
|
+
*
|
|
40
|
+
* Algorithm:
|
|
41
|
+
*
|
|
42
|
+
* 1. Build per-vertex incident-tri lists and per-tri normalized normals
|
|
43
|
+
* and plane offsets.
|
|
44
|
+
* 2. Process vertices via a dirty-flag worklist. Initially queue every
|
|
45
|
+
* vertex; after a successful removal, re-queue the ring neighbours
|
|
46
|
+
* so chains of K=1 / K=2 vertices collapse in one run.
|
|
47
|
+
* 3. For each dequeued vertex `v`:
|
|
48
|
+
* a. Walk the fan to extract the cyclic ring vertices and the
|
|
49
|
+
* cyclic ordered tris (each tri (v, ring[i], ring[(i+1)%k]) is
|
|
50
|
+
* the i-th tri in fan order).
|
|
51
|
+
* b. Decide K. If all tris share a plane: K=1. Otherwise count
|
|
52
|
+
* transitions in cyclic order; K=2 if exactly two arcs.
|
|
53
|
+
* c. K>=3: skip. K=2: verify ring[i1], v, ring[i2] are collinear.
|
|
54
|
+
* d. For each arc, project its polygon to 2D using the arc's
|
|
55
|
+
* plane basis, ear-clip, and append the new tris.
|
|
56
|
+
* e. Mark v's old tris dead, register the new tris in each polygon
|
|
57
|
+
* vertex's incident list, and re-queue the ring.
|
|
58
|
+
* 4. Compact: drop dead tris and unused vertices, remap indices.
|
|
59
|
+
*
|
|
60
|
+
* @param mesh - Input triangle mesh from {@link marchingCubes}.
|
|
61
|
+
* @param voxelResolution - Size of one voxel in world units. Used to scale the plane-offset tolerance. (The K=2 collinearity check is purely angular and has no voxel-scaled term.)
|
|
62
|
+
* @returns A new mesh with the same surface geometry, no T-junctions, and far fewer triangles.
|
|
63
|
+
*/
|
|
64
|
+
function coplanarMerge(mesh, voxelResolution) {
|
|
65
|
+
const { positions, indices } = mesh;
|
|
66
|
+
const inputTriCount = (indices.length / 3) | 0;
|
|
67
|
+
const vertCount = (positions.length / 3) | 0;
|
|
68
|
+
if (inputTriCount === 0) {
|
|
69
|
+
return { positions: new Float32Array(0), indices: new Uint32Array(0) };
|
|
70
|
+
}
|
|
71
|
+
// Plane-offset tolerance for the coplanarity test. An absolute floor
|
|
72
|
+
// (voxelResolution * PLANE_REL_EPS) handles near-origin planes, while
|
|
73
|
+
// a relative term scaled by max(|da|, |db|) handles large-offset
|
|
74
|
+
// planes where Float32 position precision in d = n . p causes
|
|
75
|
+
// relative error proportional to |d|. Without the relative term,
|
|
76
|
+
// coplanar fans far from the origin would be seen as distinct planes.
|
|
77
|
+
function planeEps(da, db) {
|
|
78
|
+
const absA = da < 0 ? -da : da;
|
|
79
|
+
const absB = db < 0 ? -db : db;
|
|
80
|
+
return PLANE_REL_EPS * (voxelResolution + (absA > absB ? absA : absB));
|
|
81
|
+
}
|
|
82
|
+
// Mutable triangle table. Flat typed arrays so we can append new tris
|
|
83
|
+
// (from ear-clipping) without per-tri allocations and without hitting
|
|
84
|
+
// V8's regular-Array backing-store size cap on large meshes.
|
|
85
|
+
// `triVertsArr` indexes are 3*t, everything else is t.
|
|
86
|
+
//
|
|
87
|
+
// Normals are stored as Float32 (unit vectors; ample precision for the
|
|
88
|
+
// dot-product coplanarity test against `1 - NORMAL_EPS`). The plane
|
|
89
|
+
// offset stays Float64 since it's an absolute world-space scalar that
|
|
90
|
+
// can be large for distant scenes.
|
|
91
|
+
let triCap = inputTriCount;
|
|
92
|
+
let triVertsArr = new Uint32Array(triCap * 3);
|
|
93
|
+
let triNxArr = new Float32Array(triCap);
|
|
94
|
+
let triNyArr = new Float32Array(triCap);
|
|
95
|
+
let triNzArr = new Float32Array(triCap);
|
|
96
|
+
let triDArr = new Float64Array(triCap);
|
|
97
|
+
let triAliveArr = new Uint8Array(triCap);
|
|
98
|
+
let triCount = inputTriCount;
|
|
99
|
+
// Capacity-doubling appenders for new tris generated by ear-clipping.
|
|
100
|
+
function ensureTriCap() {
|
|
101
|
+
if (triCount < triCap) {
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
const newCap = triCap * 2;
|
|
105
|
+
function growF32(src) {
|
|
106
|
+
const out = new Float32Array(newCap);
|
|
107
|
+
out.set(src);
|
|
108
|
+
return out;
|
|
109
|
+
}
|
|
110
|
+
function growF64(src) {
|
|
111
|
+
const out = new Float64Array(newCap);
|
|
112
|
+
out.set(src);
|
|
113
|
+
return out;
|
|
114
|
+
}
|
|
115
|
+
const newVerts = new Uint32Array(newCap * 3);
|
|
116
|
+
newVerts.set(triVertsArr);
|
|
117
|
+
triVertsArr = newVerts;
|
|
118
|
+
triNxArr = growF32(triNxArr);
|
|
119
|
+
triNyArr = growF32(triNyArr);
|
|
120
|
+
triNzArr = growF32(triNzArr);
|
|
121
|
+
triDArr = growF64(triDArr);
|
|
122
|
+
const aliveOut = new Uint8Array(newCap);
|
|
123
|
+
aliveOut.set(triAliveArr);
|
|
124
|
+
triAliveArr = aliveOut;
|
|
125
|
+
triCap = newCap;
|
|
126
|
+
}
|
|
127
|
+
// Pass 0: compute per-tri normalized normal and plane offset.
|
|
128
|
+
for (let t = 0; t < inputTriCount; t++) {
|
|
129
|
+
const ia = indices[t * 3];
|
|
130
|
+
const ib = indices[t * 3 + 1];
|
|
131
|
+
const ic = indices[t * 3 + 2];
|
|
132
|
+
triVertsArr[t * 3] = ia;
|
|
133
|
+
triVertsArr[t * 3 + 1] = ib;
|
|
134
|
+
triVertsArr[t * 3 + 2] = ic;
|
|
135
|
+
const ax = positions[ia * 3];
|
|
136
|
+
const ay = positions[ia * 3 + 1];
|
|
137
|
+
const az = positions[ia * 3 + 2];
|
|
138
|
+
const bx = positions[ib * 3];
|
|
139
|
+
const by = positions[ib * 3 + 1];
|
|
140
|
+
const bz = positions[ib * 3 + 2];
|
|
141
|
+
const cx = positions[ic * 3];
|
|
142
|
+
const cy = positions[ic * 3 + 1];
|
|
143
|
+
const cz = positions[ic * 3 + 2];
|
|
144
|
+
const ex = bx - ax, ey = by - ay, ez = bz - az;
|
|
145
|
+
const fx = cx - ax, fy = cy - ay, fz = cz - az;
|
|
146
|
+
let nx = ey * fz - ez * fy;
|
|
147
|
+
let ny = ez * fx - ex * fz;
|
|
148
|
+
let nz = ex * fy - ey * fx;
|
|
149
|
+
const nLen = Math.sqrt(nx * nx + ny * ny + nz * nz);
|
|
150
|
+
if (nLen < 1e-12) {
|
|
151
|
+
// Degenerate input tri; drop it from the active set.
|
|
152
|
+
triAliveArr[t] = 0;
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
const inv = 1 / nLen;
|
|
156
|
+
nx *= inv;
|
|
157
|
+
ny *= inv;
|
|
158
|
+
nz *= inv;
|
|
159
|
+
triNxArr[t] = nx;
|
|
160
|
+
triNyArr[t] = ny;
|
|
161
|
+
triNzArr[t] = nz;
|
|
162
|
+
triDArr[t] = nx * ax + ny * ay + nz * az;
|
|
163
|
+
triAliveArr[t] = 1;
|
|
164
|
+
}
|
|
165
|
+
// Per-vertex incident-tri lists, stored as a singly-linked free-listed
|
|
166
|
+
// node pool backed by typed arrays. Each pool node `n` holds:
|
|
167
|
+
// poolTri[n] - the incident tri index
|
|
168
|
+
// poolNext[n] - next node in the same vertex's list (or -1 = end)
|
|
169
|
+
// `vertHead[v]` is the head node for vertex v (or -1 if v has no
|
|
170
|
+
// incident tris). Removed nodes are pushed onto the `freeHead` chain
|
|
171
|
+
// for reuse, so the pool's max occupancy is the initial fan-mention
|
|
172
|
+
// count (3 * inputTriCount): the worklist's K=1/K=2 collapses are
|
|
173
|
+
// monotonically tri-reducing, so the free list serves all subsequent
|
|
174
|
+
// ear-clip allocations without ever growing the backing arrays.
|
|
175
|
+
//
|
|
176
|
+
// Footprint: 8 B/node * 3 * inputTriCount + 4 B/vert * vertCount.
|
|
177
|
+
// For 25.7M raw tris / 12.9M raw verts that's ~615 MB pool +
|
|
178
|
+
// ~52 MB heads, vs ~1.4 GB for the prior `number[][]` adjacency
|
|
179
|
+
// (V8 Array headers, FixedArray headers, hidden classes per inner
|
|
180
|
+
// array all inflate the boxed-SMI payload).
|
|
181
|
+
const poolCap = inputTriCount * 3;
|
|
182
|
+
const poolTri = new Int32Array(poolCap);
|
|
183
|
+
const poolNext = new Int32Array(poolCap);
|
|
184
|
+
let poolLen = 0;
|
|
185
|
+
let freeHead = -1;
|
|
186
|
+
const vertHead = new Int32Array(vertCount).fill(-1);
|
|
187
|
+
function allocNode() {
|
|
188
|
+
if (freeHead !== -1) {
|
|
189
|
+
const n = freeHead;
|
|
190
|
+
freeHead = poolNext[n];
|
|
191
|
+
return n;
|
|
192
|
+
}
|
|
193
|
+
return poolLen++;
|
|
194
|
+
}
|
|
195
|
+
function freeNode(n) {
|
|
196
|
+
poolNext[n] = freeHead;
|
|
197
|
+
freeHead = n;
|
|
198
|
+
}
|
|
199
|
+
function addTriToVert(v, tri) {
|
|
200
|
+
const n = allocNode();
|
|
201
|
+
poolTri[n] = tri;
|
|
202
|
+
poolNext[n] = vertHead[v];
|
|
203
|
+
vertHead[v] = n;
|
|
204
|
+
}
|
|
205
|
+
// O(degree) removal of `tri` from `v`'s incident list. Returns true
|
|
206
|
+
// if found and removed.
|
|
207
|
+
function removeTriFromVert(v, tri) {
|
|
208
|
+
let prev = -1;
|
|
209
|
+
let cur = vertHead[v];
|
|
210
|
+
while (cur !== -1) {
|
|
211
|
+
if (poolTri[cur] === tri) {
|
|
212
|
+
const nxt = poolNext[cur];
|
|
213
|
+
if (prev === -1) {
|
|
214
|
+
vertHead[v] = nxt;
|
|
215
|
+
}
|
|
216
|
+
else {
|
|
217
|
+
poolNext[prev] = nxt;
|
|
218
|
+
}
|
|
219
|
+
freeNode(cur);
|
|
220
|
+
return true;
|
|
221
|
+
}
|
|
222
|
+
prev = cur;
|
|
223
|
+
cur = poolNext[cur];
|
|
224
|
+
}
|
|
225
|
+
return false;
|
|
226
|
+
}
|
|
227
|
+
for (let t = 0; t < inputTriCount; t++) {
|
|
228
|
+
if (triAliveArr[t] === 0) {
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
addTriToVert(triVertsArr[t * 3], t);
|
|
232
|
+
addTriToVert(triVertsArr[t * 3 + 1], t);
|
|
233
|
+
addTriToVert(triVertsArr[t * 3 + 2], t);
|
|
234
|
+
}
|
|
235
|
+
// Build a right-handed tangent / bitangent basis (t, b) on the plane
|
|
236
|
+
// with normal n, picking the cardinal axis least aligned with n to
|
|
237
|
+
// avoid precision loss in the cross product. By construction
|
|
238
|
+
// t x b = n, so a polygon traced CCW around +n in 3D projects to a
|
|
239
|
+
// CCW polygon in (t, b) coordinates (positive 2D signed area).
|
|
240
|
+
function buildBasis(nx, ny, nz) {
|
|
241
|
+
const ax = Math.abs(nx);
|
|
242
|
+
const ay = Math.abs(ny);
|
|
243
|
+
const az = Math.abs(nz);
|
|
244
|
+
let tx, ty, tz;
|
|
245
|
+
if (ax <= ay && ax <= az) {
|
|
246
|
+
// X axis least aligned: t = (1, 0, 0) x n
|
|
247
|
+
tx = 0;
|
|
248
|
+
ty = -nz;
|
|
249
|
+
tz = ny;
|
|
250
|
+
}
|
|
251
|
+
else if (ay <= az) {
|
|
252
|
+
// Y axis least aligned: t = (0, 1, 0) x n
|
|
253
|
+
tx = nz;
|
|
254
|
+
ty = 0;
|
|
255
|
+
tz = -nx;
|
|
256
|
+
}
|
|
257
|
+
else {
|
|
258
|
+
// Z axis least aligned: t = (0, 0, 1) x n
|
|
259
|
+
tx = -ny;
|
|
260
|
+
ty = nx;
|
|
261
|
+
tz = 0;
|
|
262
|
+
}
|
|
263
|
+
const tlen = Math.sqrt(tx * tx + ty * ty + tz * tz);
|
|
264
|
+
const inv = 1 / tlen;
|
|
265
|
+
tx *= inv;
|
|
266
|
+
ty *= inv;
|
|
267
|
+
tz *= inv;
|
|
268
|
+
// bitangent = n x t (unit length because n and t are unit and
|
|
269
|
+
// perpendicular).
|
|
270
|
+
const bx = ny * tz - nz * ty;
|
|
271
|
+
const by = nz * tx - nx * tz;
|
|
272
|
+
const bz = nx * ty - ny * tx;
|
|
273
|
+
return [tx, ty, tz, bx, by, bz];
|
|
274
|
+
}
|
|
275
|
+
// Reusable scratch buffers for extractFan. With typical MC fan sizes
|
|
276
|
+
// (k <= ~12), a linear scan over parallel typed arrays is cheaper than
|
|
277
|
+
// the Map-of-Map-of-Set allocations the previous version paid per
|
|
278
|
+
// vertex, and saves ~3 allocations per worklist iteration on meshes
|
|
279
|
+
// with tens of millions of vertices.
|
|
280
|
+
let fanScratchCap = 16;
|
|
281
|
+
let fanFromScratch = new Int32Array(fanScratchCap);
|
|
282
|
+
let fanToScratch = new Int32Array(fanScratchCap);
|
|
283
|
+
let fanTriScratch = new Int32Array(fanScratchCap);
|
|
284
|
+
let fanRingScratch = new Int32Array(fanScratchCap);
|
|
285
|
+
let fanTrisScratch = new Int32Array(fanScratchCap);
|
|
286
|
+
function growFanScratch(need) {
|
|
287
|
+
if (need <= fanScratchCap) {
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
let c = fanScratchCap;
|
|
291
|
+
while (c < need) {
|
|
292
|
+
c *= 2;
|
|
293
|
+
}
|
|
294
|
+
fanFromScratch = new Int32Array(c);
|
|
295
|
+
fanToScratch = new Int32Array(c);
|
|
296
|
+
fanTriScratch = new Int32Array(c);
|
|
297
|
+
fanRingScratch = new Int32Array(c);
|
|
298
|
+
fanTrisScratch = new Int32Array(c);
|
|
299
|
+
fanScratchCap = c;
|
|
300
|
+
}
|
|
301
|
+
// Walk v's fan to extract its cyclic boundary polygon AND the matching
|
|
302
|
+
// cyclic ordered tris. ring[i] is the "from" vertex of the i-th tri
|
|
303
|
+
// (in fan order), and tris[i] = (v, ring[i], ring[(i+1) % k]) is the
|
|
304
|
+
// tri whose two non-v vertices are ring[i] and ring[(i+1) % k].
|
|
305
|
+
// Returns null when the fan is non-manifold (duplicate from-vertex,
|
|
306
|
+
// closes prematurely, or fails to close).
|
|
307
|
+
//
|
|
308
|
+
// Output aliases the module-level `fanRingScratch` / `fanTrisScratch`
|
|
309
|
+
// buffers; the caller must consume them before the next extractFan
|
|
310
|
+
// call. `k` is returned explicitly since the scratch buffers may be
|
|
311
|
+
// oversized.
|
|
312
|
+
function extractFan(v) {
|
|
313
|
+
let k = 0;
|
|
314
|
+
for (let n = vertHead[v]; n !== -1; n = poolNext[n]) {
|
|
315
|
+
k++;
|
|
316
|
+
}
|
|
317
|
+
if (k < 3) {
|
|
318
|
+
return -1;
|
|
319
|
+
}
|
|
320
|
+
growFanScratch(k);
|
|
321
|
+
// Collect (from, to, tri) triples into parallel scratch arrays.
|
|
322
|
+
// The O(k^2) duplicate-from scan here and the O(k^2) cyclic walk
|
|
323
|
+
// below are cheap for small k and avoid per-vertex Map allocs.
|
|
324
|
+
let i = 0;
|
|
325
|
+
for (let n = vertHead[v]; n !== -1; n = poolNext[n]) {
|
|
326
|
+
const t = poolTri[n];
|
|
327
|
+
const a = triVertsArr[t * 3];
|
|
328
|
+
const b = triVertsArr[t * 3 + 1];
|
|
329
|
+
const c = triVertsArr[t * 3 + 2];
|
|
330
|
+
let from, to;
|
|
331
|
+
if (a === v) {
|
|
332
|
+
from = b;
|
|
333
|
+
to = c;
|
|
334
|
+
}
|
|
335
|
+
else if (b === v) {
|
|
336
|
+
from = c;
|
|
337
|
+
to = a;
|
|
338
|
+
}
|
|
339
|
+
else {
|
|
340
|
+
from = a;
|
|
341
|
+
to = b;
|
|
342
|
+
}
|
|
343
|
+
for (let j = 0; j < i; j++) {
|
|
344
|
+
if (fanFromScratch[j] === from) {
|
|
345
|
+
return -1;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
fanFromScratch[i] = from;
|
|
349
|
+
fanToScratch[i] = to;
|
|
350
|
+
fanTriScratch[i] = t;
|
|
351
|
+
i++;
|
|
352
|
+
}
|
|
353
|
+
const start = fanFromScratch[0];
|
|
354
|
+
let cur = start;
|
|
355
|
+
for (let step = 0; step < k; step++) {
|
|
356
|
+
fanRingScratch[step] = cur;
|
|
357
|
+
let found = -1;
|
|
358
|
+
for (let j = 0; j < k; j++) {
|
|
359
|
+
if (fanFromScratch[j] === cur) {
|
|
360
|
+
found = j;
|
|
361
|
+
break;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
if (found === -1) {
|
|
365
|
+
return -1;
|
|
366
|
+
}
|
|
367
|
+
fanTrisScratch[step] = fanTriScratch[found];
|
|
368
|
+
const next = fanToScratch[found];
|
|
369
|
+
// Premature cycle close => fan is non-manifold (multi-component).
|
|
370
|
+
if (next === start && step < k - 1) {
|
|
371
|
+
return -1;
|
|
372
|
+
}
|
|
373
|
+
cur = next;
|
|
374
|
+
}
|
|
375
|
+
if (cur !== start) {
|
|
376
|
+
return -1;
|
|
377
|
+
}
|
|
378
|
+
return k;
|
|
379
|
+
}
|
|
380
|
+
// Reusable scratch for earClip's doubly-linked polygon traversal.
|
|
381
|
+
// Grows to the largest polygon seen and is reused across every
|
|
382
|
+
// worklist iteration.
|
|
383
|
+
let earPrevScratch = new Int32Array(16);
|
|
384
|
+
let earNextScratch = new Int32Array(16);
|
|
385
|
+
function growEarInternalScratch(n) {
|
|
386
|
+
if (n <= earPrevScratch.length) {
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
let c = earPrevScratch.length;
|
|
390
|
+
while (c < n) {
|
|
391
|
+
c *= 2;
|
|
392
|
+
}
|
|
393
|
+
earPrevScratch = new Int32Array(c);
|
|
394
|
+
earNextScratch = new Int32Array(c);
|
|
395
|
+
}
|
|
396
|
+
// Ear-clip a planar simple polygon. `px, py` are the projected 2D
|
|
397
|
+
// coordinates of the first `n` polygon vertices (in CCW order); the
|
|
398
|
+
// arrays may be larger than `n` (scratch-buffer aliasing is fine,
|
|
399
|
+
// only [0, n) is read). Writes (n - 2) * 3 polygon-relative vertex
|
|
400
|
+
// indices into `out[outOffset..]` and returns the number of indices
|
|
401
|
+
// written on success, or -1 if the polygon is degenerate / not
|
|
402
|
+
// simple.
|
|
403
|
+
//
|
|
404
|
+
// Strictly-collinear interior vertices (cross product == 0) are not
|
|
405
|
+
// considered convex ear apices and so produce slivers as side
|
|
406
|
+
// vertices of neighbouring ears. These are transient: any such
|
|
407
|
+
// vertex is itself K=1 in the same plane and the worklist removes
|
|
408
|
+
// it in a subsequent iteration, replacing the slivers with a clean
|
|
409
|
+
// re-triangulation of its updated fan. An in-earClip pre-pass that
|
|
410
|
+
// drops the vertex from this polygon would create a T-junction
|
|
411
|
+
// with the vertex's other incident tris (which still reference it),
|
|
412
|
+
// so we leave the cleanup to the worklist.
|
|
413
|
+
function earClip(px, py, n, out, outOffset) {
|
|
414
|
+
if (n < 3) {
|
|
415
|
+
return -1;
|
|
416
|
+
}
|
|
417
|
+
if (n === 3) {
|
|
418
|
+
out[outOffset] = 0;
|
|
419
|
+
out[outOffset + 1] = 1;
|
|
420
|
+
out[outOffset + 2] = 2;
|
|
421
|
+
return 3;
|
|
422
|
+
}
|
|
423
|
+
// Verify CCW orientation. By construction (right-handed basis)
|
|
424
|
+
// valid input polygons are CCW with positive signed area.
|
|
425
|
+
let area2 = 0;
|
|
426
|
+
for (let i = 0; i < n; i++) {
|
|
427
|
+
const j = (i + 1) % n;
|
|
428
|
+
area2 += px[i] * py[j] - px[j] * py[i];
|
|
429
|
+
}
|
|
430
|
+
if (area2 <= 0) {
|
|
431
|
+
return -1;
|
|
432
|
+
}
|
|
433
|
+
growEarInternalScratch(n);
|
|
434
|
+
const prev = earPrevScratch;
|
|
435
|
+
const next = earNextScratch;
|
|
436
|
+
for (let i = 0; i < n; i++) {
|
|
437
|
+
prev[i] = (i - 1 + n) % n;
|
|
438
|
+
next[i] = (i + 1) % n;
|
|
439
|
+
}
|
|
440
|
+
function isConvex(a, b, c) {
|
|
441
|
+
return (px[b] - px[a]) * (py[c] - py[a]) - (py[b] - py[a]) * (px[c] - px[a]) > 0;
|
|
442
|
+
}
|
|
443
|
+
function inTri(p, a, b, c) {
|
|
444
|
+
const x = px[p], y = py[p];
|
|
445
|
+
const d1 = (x - px[b]) * (py[a] - py[b]) - (px[a] - px[b]) * (y - py[b]);
|
|
446
|
+
const d2 = (x - px[c]) * (py[b] - py[c]) - (px[b] - px[c]) * (y - py[c]);
|
|
447
|
+
const d3 = (x - px[a]) * (py[c] - py[a]) - (px[c] - px[a]) * (y - py[a]);
|
|
448
|
+
const hasNeg = d1 < 0 || d2 < 0 || d3 < 0;
|
|
449
|
+
const hasPos = d1 > 0 || d2 > 0 || d3 > 0;
|
|
450
|
+
return !(hasNeg && hasPos);
|
|
451
|
+
}
|
|
452
|
+
function isEar(a, b, c) {
|
|
453
|
+
if (!isConvex(a, b, c)) {
|
|
454
|
+
return false;
|
|
455
|
+
}
|
|
456
|
+
let p = next[c];
|
|
457
|
+
while (p !== a) {
|
|
458
|
+
if (inTri(p, a, b, c)) {
|
|
459
|
+
return false;
|
|
460
|
+
}
|
|
461
|
+
p = next[p];
|
|
462
|
+
}
|
|
463
|
+
return true;
|
|
464
|
+
}
|
|
465
|
+
let resultLen = 0;
|
|
466
|
+
let count = n;
|
|
467
|
+
let i = 0;
|
|
468
|
+
let stalls = 0;
|
|
469
|
+
while (count > 3) {
|
|
470
|
+
if (stalls > count) {
|
|
471
|
+
return -1;
|
|
472
|
+
}
|
|
473
|
+
const p = prev[i];
|
|
474
|
+
const nxt = next[i];
|
|
475
|
+
if (isEar(p, i, nxt)) {
|
|
476
|
+
out[outOffset + resultLen++] = p;
|
|
477
|
+
out[outOffset + resultLen++] = i;
|
|
478
|
+
out[outOffset + resultLen++] = nxt;
|
|
479
|
+
next[p] = nxt;
|
|
480
|
+
prev[nxt] = p;
|
|
481
|
+
count--;
|
|
482
|
+
i = nxt;
|
|
483
|
+
stalls = 0;
|
|
484
|
+
}
|
|
485
|
+
else {
|
|
486
|
+
i = next[i];
|
|
487
|
+
stalls++;
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
out[outOffset + resultLen++] = prev[i];
|
|
491
|
+
out[outOffset + resultLen++] = i;
|
|
492
|
+
out[outOffset + resultLen++] = next[i];
|
|
493
|
+
return resultLen;
|
|
494
|
+
}
|
|
495
|
+
// Worklist: iterative dirty-flag scheduler. Initially queue every
|
|
496
|
+
// vertex; on each successful removal, re-queue the ring neighbours so
|
|
497
|
+
// chains of K=1 / K=2 vertices collapse in a single run.
|
|
498
|
+
const inQueue = new Uint8Array(vertCount);
|
|
499
|
+
let queue = new Int32Array(Math.max(vertCount, 16));
|
|
500
|
+
let queueLen = 0;
|
|
501
|
+
let queueHead = 0;
|
|
502
|
+
function pushQueue(u) {
|
|
503
|
+
if (inQueue[u]) {
|
|
504
|
+
return;
|
|
505
|
+
}
|
|
506
|
+
inQueue[u] = 1;
|
|
507
|
+
if (queueLen >= queue.length) {
|
|
508
|
+
const grown = new Int32Array(queue.length * 2);
|
|
509
|
+
grown.set(queue);
|
|
510
|
+
queue = grown;
|
|
511
|
+
}
|
|
512
|
+
queue[queueLen++] = u;
|
|
513
|
+
}
|
|
514
|
+
function compactQueue() {
|
|
515
|
+
// Reclaim consumed prefix when slack exceeds 50% (and is large
|
|
516
|
+
// enough to be worth the copy). Bounded by O(total pushes).
|
|
517
|
+
if (queueHead > 4096 && queueHead * 2 > queueLen) {
|
|
518
|
+
queue.copyWithin(0, queueHead, queueLen);
|
|
519
|
+
queueLen -= queueHead;
|
|
520
|
+
queueHead = 0;
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
for (let v = 0; v < vertCount; v++) {
|
|
524
|
+
inQueue[v] = 1;
|
|
525
|
+
queue[queueLen++] = v;
|
|
526
|
+
}
|
|
527
|
+
// Tolerance for K=2 seam collinearity: cosine of the angle between
|
|
528
|
+
// (v -> a) and (v -> b) must be <= -(1 - COLLINEAR_REL_EPS), i.e.
|
|
529
|
+
// the two seam edges through v are nearly antiparallel (v lies on
|
|
530
|
+
// the segment from a to b in 3D).
|
|
531
|
+
const cosineMax = -1 + COLLINEAR_REL_EPS;
|
|
532
|
+
// Reusable scratch for the per-arc plane descriptor.
|
|
533
|
+
const arcStartIdx = new Int32Array(2);
|
|
534
|
+
const arcPolySize = new Int32Array(2);
|
|
535
|
+
const arcPlaneT = new Int32Array(2);
|
|
536
|
+
// Reusable per-arc scratch buffers (arcCount is always 1 or 2).
|
|
537
|
+
// Each slot holds the polygon vertex indices, the 2D-projected
|
|
538
|
+
// coordinates, and the ear-clip triangle indices for one arc. All
|
|
539
|
+
// grow monotonically to the largest polygon ever seen and are
|
|
540
|
+
// reused for every worklist iteration, so the hot loop is mostly
|
|
541
|
+
// allocation-free.
|
|
542
|
+
const arcPolyScratch = [new Int32Array(16), new Int32Array(16)];
|
|
543
|
+
const arcPxScratch = [new Float64Array(16), new Float64Array(16)];
|
|
544
|
+
const arcPyScratch = [new Float64Array(16), new Float64Array(16)];
|
|
545
|
+
const arcEarScratch = [new Int32Array(16), new Int32Array(16)];
|
|
546
|
+
const arcEarLen = new Int32Array(2);
|
|
547
|
+
function ensureArcScratch(g, polySize) {
|
|
548
|
+
if (polySize > arcPolyScratch[g].length) {
|
|
549
|
+
let c = arcPolyScratch[g].length;
|
|
550
|
+
while (c < polySize) {
|
|
551
|
+
c *= 2;
|
|
552
|
+
}
|
|
553
|
+
arcPolyScratch[g] = new Int32Array(c);
|
|
554
|
+
arcPxScratch[g] = new Float64Array(c);
|
|
555
|
+
arcPyScratch[g] = new Float64Array(c);
|
|
556
|
+
}
|
|
557
|
+
const earNeed = (polySize - 2) * 3;
|
|
558
|
+
if (earNeed > arcEarScratch[g].length) {
|
|
559
|
+
let c = arcEarScratch[g].length;
|
|
560
|
+
while (c < earNeed) {
|
|
561
|
+
c *= 2;
|
|
562
|
+
}
|
|
563
|
+
arcEarScratch[g] = new Int32Array(c);
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
while (queueHead < queueLen) {
|
|
567
|
+
const v = queue[queueHead++];
|
|
568
|
+
inQueue[v] = 0;
|
|
569
|
+
compactQueue();
|
|
570
|
+
// Cheap "fan size < 3" early-out without traversing the full
|
|
571
|
+
// linked list.
|
|
572
|
+
const h0 = vertHead[v];
|
|
573
|
+
if (h0 === -1) {
|
|
574
|
+
continue;
|
|
575
|
+
}
|
|
576
|
+
const h1 = poolNext[h0];
|
|
577
|
+
if (h1 === -1) {
|
|
578
|
+
continue;
|
|
579
|
+
}
|
|
580
|
+
if (poolNext[h1] === -1) {
|
|
581
|
+
continue;
|
|
582
|
+
}
|
|
583
|
+
const k = extractFan(v);
|
|
584
|
+
if (k === -1) {
|
|
585
|
+
continue;
|
|
586
|
+
}
|
|
587
|
+
const ring = fanRingScratch;
|
|
588
|
+
const fanTris = fanTrisScratch;
|
|
589
|
+
// Decide K. First check if all tris are coplanar with fanTris[0]
|
|
590
|
+
// (K=1 fast path).
|
|
591
|
+
const t0 = fanTris[0];
|
|
592
|
+
const n0x = triNxArr[t0];
|
|
593
|
+
const n0y = triNyArr[t0];
|
|
594
|
+
const n0z = triNzArr[t0];
|
|
595
|
+
const d0 = triDArr[t0];
|
|
596
|
+
let allCoplanar = true;
|
|
597
|
+
for (let i = 1; i < k; i++) {
|
|
598
|
+
const t = fanTris[i];
|
|
599
|
+
const dT = triDArr[t];
|
|
600
|
+
const dotN = triNxArr[t] * n0x + triNyArr[t] * n0y + triNzArr[t] * n0z;
|
|
601
|
+
if (dotN < 1 - NORMAL_EPS || Math.abs(dT - d0) > planeEps(dT, d0)) {
|
|
602
|
+
allCoplanar = false;
|
|
603
|
+
break;
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
let arcCount = 0;
|
|
607
|
+
if (allCoplanar) {
|
|
608
|
+
// K=1: single arc covering the whole ring (k vertices).
|
|
609
|
+
arcStartIdx[0] = 0;
|
|
610
|
+
arcPolySize[0] = k;
|
|
611
|
+
arcPlaneT[0] = t0;
|
|
612
|
+
arcCount = 1;
|
|
613
|
+
}
|
|
614
|
+
else {
|
|
615
|
+
// Walk cyclically; mark transitions where plane changes vs.
|
|
616
|
+
// the previous tri in fan order. Exactly 2 transitions =>
|
|
617
|
+
// K=2 candidate.
|
|
618
|
+
let nTransitions = 0;
|
|
619
|
+
let i1 = -1;
|
|
620
|
+
let i2 = -1;
|
|
621
|
+
for (let i = 0; i < k; i++) {
|
|
622
|
+
const t = fanTris[i];
|
|
623
|
+
const prevT = fanTris[(i - 1 + k) % k];
|
|
624
|
+
const nx1 = triNxArr[t];
|
|
625
|
+
const ny1 = triNyArr[t];
|
|
626
|
+
const nz1 = triNzArr[t];
|
|
627
|
+
const d1 = triDArr[t];
|
|
628
|
+
const nx0 = triNxArr[prevT];
|
|
629
|
+
const ny0 = triNyArr[prevT];
|
|
630
|
+
const nz0 = triNzArr[prevT];
|
|
631
|
+
const dPrev = triDArr[prevT];
|
|
632
|
+
const dotN = nx1 * nx0 + ny1 * ny0 + nz1 * nz0;
|
|
633
|
+
if (dotN < 1 - NORMAL_EPS || Math.abs(d1 - dPrev) > planeEps(d1, dPrev)) {
|
|
634
|
+
nTransitions++;
|
|
635
|
+
if (nTransitions === 1) {
|
|
636
|
+
i1 = i;
|
|
637
|
+
}
|
|
638
|
+
else if (nTransitions === 2) {
|
|
639
|
+
i2 = i;
|
|
640
|
+
}
|
|
641
|
+
else {
|
|
642
|
+
break;
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
if (nTransitions !== 2) {
|
|
647
|
+
continue;
|
|
648
|
+
}
|
|
649
|
+
// K=2 collinearity: ring[i1], v, ring[i2] must be collinear
|
|
650
|
+
// with v between (cosine of va-vb angle near -1).
|
|
651
|
+
const a = ring[i1];
|
|
652
|
+
const b = ring[i2];
|
|
653
|
+
const ax = positions[a * 3] - positions[v * 3];
|
|
654
|
+
const ay = positions[a * 3 + 1] - positions[v * 3 + 1];
|
|
655
|
+
const az = positions[a * 3 + 2] - positions[v * 3 + 2];
|
|
656
|
+
const bx = positions[b * 3] - positions[v * 3];
|
|
657
|
+
const by = positions[b * 3 + 1] - positions[v * 3 + 1];
|
|
658
|
+
const bz = positions[b * 3 + 2] - positions[v * 3 + 2];
|
|
659
|
+
const lenA = Math.sqrt(ax * ax + ay * ay + az * az);
|
|
660
|
+
const lenB = Math.sqrt(bx * bx + by * by + bz * bz);
|
|
661
|
+
if (lenA < 1e-12 || lenB < 1e-12) {
|
|
662
|
+
continue;
|
|
663
|
+
}
|
|
664
|
+
const cosine = (ax * bx + ay * by + az * bz) / (lenA * lenB);
|
|
665
|
+
if (cosine > cosineMax) {
|
|
666
|
+
continue;
|
|
667
|
+
}
|
|
668
|
+
// Two arcs: arc 0 covers tris[i1..i2-1] (mA tris, mA+1 polygon
|
|
669
|
+
// verts ring[i1..i2]); arc 1 covers tris[i2..i1-1] cyclically
|
|
670
|
+
// (mB tris, mB+1 polygon verts).
|
|
671
|
+
const mA = i2 - i1;
|
|
672
|
+
const mB = k - mA;
|
|
673
|
+
arcStartIdx[0] = i1;
|
|
674
|
+
arcPolySize[0] = mA + 1;
|
|
675
|
+
arcPlaneT[0] = fanTris[i1];
|
|
676
|
+
arcStartIdx[1] = i2;
|
|
677
|
+
arcPolySize[1] = mB + 1;
|
|
678
|
+
arcPlaneT[1] = fanTris[i2];
|
|
679
|
+
arcCount = 2;
|
|
680
|
+
}
|
|
681
|
+
// Build polygons and triangulations into the reusable per-arc
|
|
682
|
+
// scratch slots. Bail (without committing) if any arc fails to
|
|
683
|
+
// triangulate.
|
|
684
|
+
let allArcsOk = true;
|
|
685
|
+
for (let g = 0; g < arcCount; g++) {
|
|
686
|
+
const polySize = arcPolySize[g];
|
|
687
|
+
const startIdx = arcStartIdx[g];
|
|
688
|
+
const planeT = arcPlaneT[g];
|
|
689
|
+
ensureArcScratch(g, polySize);
|
|
690
|
+
const poly = arcPolyScratch[g];
|
|
691
|
+
for (let j = 0; j < polySize; j++) {
|
|
692
|
+
poly[j] = ring[(startIdx + j) % k];
|
|
693
|
+
}
|
|
694
|
+
const nx = triNxArr[planeT];
|
|
695
|
+
const ny = triNyArr[planeT];
|
|
696
|
+
const nz = triNzArr[planeT];
|
|
697
|
+
const [tx, ty, tz, bx, by, bz] = buildBasis(nx, ny, nz);
|
|
698
|
+
const px = arcPxScratch[g];
|
|
699
|
+
const py = arcPyScratch[g];
|
|
700
|
+
for (let j = 0; j < polySize; j++) {
|
|
701
|
+
const u = poly[j];
|
|
702
|
+
const x = positions[u * 3];
|
|
703
|
+
const y = positions[u * 3 + 1];
|
|
704
|
+
const z = positions[u * 3 + 2];
|
|
705
|
+
px[j] = x * tx + y * ty + z * tz;
|
|
706
|
+
py[j] = x * bx + y * by + z * bz;
|
|
707
|
+
}
|
|
708
|
+
const earCount = earClip(px, py, polySize, arcEarScratch[g], 0);
|
|
709
|
+
if (earCount !== (polySize - 2) * 3) {
|
|
710
|
+
allArcsOk = false;
|
|
711
|
+
break;
|
|
712
|
+
}
|
|
713
|
+
arcEarLen[g] = earCount;
|
|
714
|
+
}
|
|
715
|
+
if (!allArcsOk) {
|
|
716
|
+
continue;
|
|
717
|
+
}
|
|
718
|
+
// Commit: walk v's linked-list of incident tris in place. For each
|
|
719
|
+
// tri, mark it dead, unlink it from each non-v vertex's list, and
|
|
720
|
+
// free v's own node. Iteration is safe because we only mutate
|
|
721
|
+
// OTHER vertices' lists during the walk.
|
|
722
|
+
let cur = vertHead[v];
|
|
723
|
+
while (cur !== -1) {
|
|
724
|
+
const t = poolTri[cur];
|
|
725
|
+
triAliveArr[t] = 0;
|
|
726
|
+
for (let j = 0; j < 3; j++) {
|
|
727
|
+
const u = triVertsArr[t * 3 + j];
|
|
728
|
+
if (u === v) {
|
|
729
|
+
continue;
|
|
730
|
+
}
|
|
731
|
+
removeTriFromVert(u, t);
|
|
732
|
+
}
|
|
733
|
+
const nxt = poolNext[cur];
|
|
734
|
+
freeNode(cur);
|
|
735
|
+
cur = nxt;
|
|
736
|
+
}
|
|
737
|
+
vertHead[v] = -1;
|
|
738
|
+
// Append new tris per arc, each carrying its arc's plane.
|
|
739
|
+
for (let g = 0; g < arcCount; g++) {
|
|
740
|
+
const planeT = arcPlaneT[g];
|
|
741
|
+
const nx = triNxArr[planeT];
|
|
742
|
+
const ny = triNyArr[planeT];
|
|
743
|
+
const nz = triNzArr[planeT];
|
|
744
|
+
const d = triDArr[planeT];
|
|
745
|
+
const earIdx = arcEarScratch[g];
|
|
746
|
+
const earIdxLen = arcEarLen[g];
|
|
747
|
+
const poly = arcPolyScratch[g];
|
|
748
|
+
for (let i = 0; i < earIdxLen; i += 3) {
|
|
749
|
+
const ua = poly[earIdx[i]];
|
|
750
|
+
const ub = poly[earIdx[i + 1]];
|
|
751
|
+
const uc = poly[earIdx[i + 2]];
|
|
752
|
+
ensureTriCap();
|
|
753
|
+
const newT = triCount++;
|
|
754
|
+
triVertsArr[newT * 3] = ua;
|
|
755
|
+
triVertsArr[newT * 3 + 1] = ub;
|
|
756
|
+
triVertsArr[newT * 3 + 2] = uc;
|
|
757
|
+
triNxArr[newT] = nx;
|
|
758
|
+
triNyArr[newT] = ny;
|
|
759
|
+
triNzArr[newT] = nz;
|
|
760
|
+
triDArr[newT] = d;
|
|
761
|
+
triAliveArr[newT] = 1;
|
|
762
|
+
addTriToVert(ua, newT);
|
|
763
|
+
addTriToVert(ub, newT);
|
|
764
|
+
addTriToVert(uc, newT);
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
// Re-queue every ring vertex: each may now satisfy K=1 or K=2 in
|
|
768
|
+
// its updated fan, allowing the collapse to propagate along long
|
|
769
|
+
// collinear chains in a single sweep.
|
|
770
|
+
for (let i = 0; i < k; i++) {
|
|
771
|
+
pushQueue(ring[i]);
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
// Compact: drop dead tris and unused vertices, remap indices.
|
|
775
|
+
const usedVerts = new Uint8Array(vertCount);
|
|
776
|
+
let outTriCount = 0;
|
|
777
|
+
for (let t = 0; t < triCount; t++) {
|
|
778
|
+
if (triAliveArr[t] === 0) {
|
|
779
|
+
continue;
|
|
780
|
+
}
|
|
781
|
+
outTriCount++;
|
|
782
|
+
usedVerts[triVertsArr[t * 3]] = 1;
|
|
783
|
+
usedVerts[triVertsArr[t * 3 + 1]] = 1;
|
|
784
|
+
usedVerts[triVertsArr[t * 3 + 2]] = 1;
|
|
785
|
+
}
|
|
786
|
+
let outVertCount = 0;
|
|
787
|
+
const vertRemap = new Int32Array(vertCount);
|
|
788
|
+
for (let v = 0; v < vertCount; v++) {
|
|
789
|
+
if (usedVerts[v] === 1) {
|
|
790
|
+
vertRemap[v] = outVertCount++;
|
|
791
|
+
}
|
|
792
|
+
else {
|
|
793
|
+
vertRemap[v] = -1;
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
const outPositions = new Float32Array(outVertCount * 3);
|
|
797
|
+
for (let v = 0; v < vertCount; v++) {
|
|
798
|
+
if (usedVerts[v] === 0) {
|
|
799
|
+
continue;
|
|
800
|
+
}
|
|
801
|
+
const o = vertRemap[v] * 3;
|
|
802
|
+
outPositions[o] = positions[v * 3];
|
|
803
|
+
outPositions[o + 1] = positions[v * 3 + 1];
|
|
804
|
+
outPositions[o + 2] = positions[v * 3 + 2];
|
|
805
|
+
}
|
|
806
|
+
const outIndices = new Uint32Array(outTriCount * 3);
|
|
807
|
+
let oi = 0;
|
|
808
|
+
for (let t = 0; t < triCount; t++) {
|
|
809
|
+
if (triAliveArr[t] === 0) {
|
|
810
|
+
continue;
|
|
811
|
+
}
|
|
812
|
+
outIndices[oi++] = vertRemap[triVertsArr[t * 3]];
|
|
813
|
+
outIndices[oi++] = vertRemap[triVertsArr[t * 3 + 1]];
|
|
814
|
+
outIndices[oi++] = vertRemap[triVertsArr[t * 3 + 2]];
|
|
815
|
+
}
|
|
816
|
+
return { positions: outPositions, indices: outIndices };
|
|
817
|
+
}
|
|
818
|
+
export { coplanarMerge };
|