@manycore/aholo-splat-transform 1.2.7 → 1.2.9

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.
Files changed (98) hide show
  1. package/CHANGELOG.md +120 -106
  2. package/COPYRIGHT.md +17 -0
  3. package/README.md +39 -39
  4. package/THIRD_PARTY_LICENSES.txt +1373 -0
  5. package/bin/cli.js +125 -118
  6. package/dist/SplatData.d.ts +67 -67
  7. package/dist/SplatData.js +167 -156
  8. package/dist/constant.d.ts +3 -3
  9. package/dist/constant.js +13 -13
  10. package/dist/file/IFile.d.ts +5 -5
  11. package/dist/file/IFile.js +1 -1
  12. package/dist/file/esz.d.ts +11 -0
  13. package/dist/file/esz.js +337 -0
  14. package/dist/file/index.d.ts +8 -7
  15. package/dist/file/index.js +7 -6
  16. package/dist/file/ksplat.d.ts +12 -12
  17. package/dist/file/ksplat.js +293 -232
  18. package/dist/file/lcc.d.ts +11 -11
  19. package/dist/file/lcc.js +161 -157
  20. package/dist/file/ply.d.ts +13 -13
  21. package/dist/file/ply.js +439 -388
  22. package/dist/file/sog.d.ts +80 -80
  23. package/dist/file/sog.js +525 -504
  24. package/dist/file/splat.d.ts +6 -6
  25. package/dist/file/splat.js +119 -99
  26. package/dist/file/spz.d.ts +11 -8
  27. package/dist/file/spz.js +597 -400
  28. package/dist/file/voxel.d.ts +43 -37
  29. package/dist/file/voxel.js +411 -280
  30. package/dist/index.d.ts +33 -33
  31. package/dist/index.js +54 -54
  32. package/dist/native/index.d.ts +54 -54
  33. package/dist/native/index.js +122 -128
  34. package/dist/native/utils.d.ts +1 -0
  35. package/dist/native/utils.js +54 -0
  36. package/dist/tasks/AutoChunkLodTask.d.ts +13 -13
  37. package/dist/tasks/AutoChunkLodTask.js +117 -117
  38. package/dist/tasks/AutoLodTask.d.ts +10 -10
  39. package/dist/tasks/AutoLodTask.js +20 -20
  40. package/dist/tasks/BaseTask.d.ts +15 -15
  41. package/dist/tasks/BaseTask.js +5 -5
  42. package/dist/tasks/FlexLodTask.d.ts +12 -12
  43. package/dist/tasks/FlexLodTask.js +54 -44
  44. package/dist/tasks/ModifyTask.d.ts +9 -9
  45. package/dist/tasks/ModifyTask.js +166 -156
  46. package/dist/tasks/ReadTask.d.ts +9 -9
  47. package/dist/tasks/ReadTask.js +29 -29
  48. package/dist/tasks/SkeletonLodTask.d.ts +10 -10
  49. package/dist/tasks/SkeletonLodTask.js +176 -156
  50. package/dist/tasks/VoxelTask.d.ts +35 -30
  51. package/dist/tasks/VoxelTask.js +40 -37
  52. package/dist/tasks/WriteTask.d.ts +12 -11
  53. package/dist/tasks/WriteTask.js +70 -70
  54. package/dist/utils/BufferReader.d.ts +12 -12
  55. package/dist/utils/BufferReader.js +45 -47
  56. package/dist/utils/Logger.d.ts +11 -11
  57. package/dist/utils/Logger.js +40 -38
  58. package/dist/utils/StreamChunkDecoder.d.ts +16 -16
  59. package/dist/utils/StreamChunkDecoder.js +31 -36
  60. package/dist/utils/index.d.ts +27 -27
  61. package/dist/utils/index.js +101 -101
  62. package/dist/utils/k-means.d.ts +4 -4
  63. package/dist/utils/k-means.js +340 -350
  64. package/dist/utils/math.d.ts +46 -46
  65. package/dist/utils/math.js +350 -351
  66. package/dist/utils/quantize-1d.d.ts +4 -4
  67. package/dist/utils/quantize-1d.js +164 -164
  68. package/dist/utils/sh-rotate.d.ts +2 -2
  69. package/dist/utils/sh-rotate.js +236 -175
  70. package/dist/utils/splat.d.ts +21 -20
  71. package/dist/utils/splat.js +397 -378
  72. package/dist/utils/voxel/binary.d.ts +8 -0
  73. package/dist/utils/voxel/binary.js +176 -0
  74. package/dist/utils/voxel/common.d.ts +178 -162
  75. package/dist/utils/voxel/common.js +1752 -1700
  76. package/dist/utils/voxel/coplanar-merge.d.ts +63 -63
  77. package/dist/utils/voxel/coplanar-merge.js +818 -819
  78. package/dist/utils/voxel/filter-cluster.d.ts +20 -0
  79. package/dist/utils/voxel/filter-cluster.js +628 -0
  80. package/dist/utils/voxel/gpu-dilation.d.ts +2 -2
  81. package/dist/utils/voxel/gpu-dilation.js +677 -665
  82. package/dist/utils/voxel/marching-cubes.d.ts +42 -42
  83. package/dist/utils/voxel/marching-cubes.js +1645 -1657
  84. package/dist/utils/voxel/mesh.d.ts +3 -3
  85. package/dist/utils/voxel/mesh.js +130 -130
  86. package/dist/utils/voxel/nav.d.ts +29 -29
  87. package/dist/utils/voxel/nav.js +1068 -1043
  88. package/dist/utils/voxel/postprocess.d.ts +23 -23
  89. package/dist/utils/voxel/postprocess.js +408 -375
  90. package/dist/utils/voxel/voxel-faces.d.ts +18 -18
  91. package/dist/utils/voxel/voxel-faces.js +662 -663
  92. package/dist/utils/voxel/voxelize.d.ts +34 -33
  93. package/dist/utils/voxel/voxelize.js +1208 -1193
  94. package/dist/utils/webgpu.d.ts +8 -8
  95. package/dist/utils/webgpu.js +122 -122
  96. package/package.json +37 -30
  97. package/dist/native/cpp/bin/linux/binding.node +0 -0
  98. package/dist/native/cpp/bin/windows/binding.node +0 -0
@@ -1,375 +1,408 @@
1
- import { BlockMaskBuffer, SOLID_HI, SOLID_LO } from './common.js';
2
- import { logger } from '../Logger.js';
3
- const FACE_X0 = 0x11111111;
4
- const FACE_X3 = 0x88888888;
5
- const FACE_Y0 = 0x000F000F;
6
- const FACE_Y3 = 0xF000F000;
7
- const FACE_Z0_LO = 0x0000FFFF;
8
- const FACE_Z3_HI = 0xFFFF0000 >>> 0;
9
- /** Count set bits in a 32-bit unsigned integer. */
10
- const popcount = (n) => {
11
- n >>>= 0;
12
- n -= (n >>> 1) & 0x55555555;
13
- n = (n & 0x33333333) + ((n >>> 2) & 0x33333333);
14
- return (((n + (n >>> 4)) & 0x0F0F0F0F) * 0x01010101) >>> 24;
15
- };
16
- const sortedUint32Has = (sorted, value) => {
17
- let lo = 0;
18
- let hi = sorted.length - 1;
19
- while (lo <= hi) {
20
- const mid = (lo + hi) >> 1;
21
- const v = sorted[mid];
22
- if (v < value) {
23
- lo = mid + 1;
24
- }
25
- else if (v > value) {
26
- hi = mid - 1;
27
- }
28
- else {
29
- return true;
30
- }
31
- }
32
- return false;
33
- };
34
- const findMixedBlockIndex = (sortedBlockIdx, target) => {
35
- let lo = 0;
36
- let hi = sortedBlockIdx.length - 1;
37
- while (lo <= hi) {
38
- const mid = (lo + hi) >> 1;
39
- const v = sortedBlockIdx[mid];
40
- if (v < target) {
41
- lo = mid + 1;
42
- }
43
- else if (v > target) {
44
- hi = mid - 1;
45
- }
46
- else {
47
- return mid;
48
- }
49
- }
50
- return undefined;
51
- };
52
- const sortMixedByBlockIdx = (blockIdx, masks) => {
53
- const n = blockIdx.length;
54
- if (n <= 1) {
55
- return;
56
- }
57
- const stackLo = [0];
58
- const stackHi = [n - 1];
59
- const swap = (a, b) => {
60
- const k = blockIdx[a];
61
- blockIdx[a] = blockIdx[b];
62
- blockIdx[b] = k;
63
- const alo = masks[a * 2];
64
- const ahi = masks[a * 2 + 1];
65
- masks[a * 2] = masks[b * 2];
66
- masks[a * 2 + 1] = masks[b * 2 + 1];
67
- masks[b * 2] = alo;
68
- masks[b * 2 + 1] = ahi;
69
- };
70
- while (stackLo.length > 0) {
71
- const lo = stackLo.pop();
72
- const hi = stackHi.pop();
73
- if (hi - lo < 16) {
74
- for (let i = lo + 1; i <= hi; i++) {
75
- const k = blockIdx[i];
76
- const m0 = masks[i * 2];
77
- const m1 = masks[i * 2 + 1];
78
- let j = i - 1;
79
- while (j >= lo && blockIdx[j] > k) {
80
- blockIdx[j + 1] = blockIdx[j];
81
- masks[(j + 1) * 2] = masks[j * 2];
82
- masks[(j + 1) * 2 + 1] = masks[j * 2 + 1];
83
- j--;
84
- }
85
- blockIdx[j + 1] = k;
86
- masks[(j + 1) * 2] = m0;
87
- masks[(j + 1) * 2 + 1] = m1;
88
- }
89
- continue;
90
- }
91
- const mid = (lo + hi) >>> 1;
92
- if (blockIdx[mid] < blockIdx[lo]) {
93
- swap(mid, lo);
94
- }
95
- if (blockIdx[hi] < blockIdx[lo]) {
96
- swap(hi, lo);
97
- }
98
- if (blockIdx[hi] < blockIdx[mid]) {
99
- swap(hi, mid);
100
- }
101
- const pivot = blockIdx[mid];
102
- let i = lo;
103
- let j = hi;
104
- while (i <= j) {
105
- while (blockIdx[i] < pivot) {
106
- i++;
107
- }
108
- while (blockIdx[j] > pivot) {
109
- j--;
110
- }
111
- if (i <= j) {
112
- if (i !== j) {
113
- swap(i, j);
114
- }
115
- i++;
116
- j--;
117
- }
118
- }
119
- if (j - lo > hi - i) {
120
- if (lo < j) {
121
- stackLo.push(lo);
122
- stackHi.push(j);
123
- }
124
- if (i < hi) {
125
- stackLo.push(i);
126
- stackHi.push(hi);
127
- }
128
- }
129
- else {
130
- if (i < hi) {
131
- stackLo.push(i);
132
- stackHi.push(hi);
133
- }
134
- if (lo < j) {
135
- stackLo.push(lo);
136
- stackHi.push(j);
137
- }
138
- }
139
- }
140
- };
141
- const addCrossFace = (nx, ny, nz, nbx, nby, nbz, hasSolid, getMixedIndex, masks, ourFaceMask, adjFaceMask, shiftAmount, shiftLeft, curLo, curHi, write) => {
142
- if (nx < 0 || ny < 0 || nz < 0 || nx >= nbx || ny >= nby || nz >= nbz) {
143
- write(curLo, curHi);
144
- return;
145
- }
146
- const adjBlockIdx = nx + ny * nbx + nz * nbx * nby;
147
- if (hasSolid(adjBlockIdx)) {
148
- write(curLo | ourFaceMask, curHi | ourFaceMask);
149
- return;
150
- }
151
- const adjIdx = getMixedIndex(adjBlockIdx);
152
- if (adjIdx === undefined) {
153
- write(curLo, curHi);
154
- return;
155
- }
156
- const adjLo = masks[adjIdx * 2];
157
- const adjHi = masks[adjIdx * 2 + 1];
158
- const faceLo = adjLo & adjFaceMask;
159
- const faceHi = adjHi & adjFaceMask;
160
- if (shiftLeft) {
161
- write(curLo | (faceLo << shiftAmount), curHi | (faceHi << shiftAmount));
162
- }
163
- else {
164
- write(curLo | (faceLo >>> shiftAmount), curHi | (faceHi >>> shiftAmount));
165
- }
166
- };
167
- const addCrossFaceZ = (nx, ny, nz, nbx, nby, nbz, hasSolid, getMixedIndex, masks, plusZ, curLo, curHi, write) => {
168
- if (nx < 0 || ny < 0 || nz < 0 || nx >= nbx || ny >= nby || nz >= nbz) {
169
- write(curLo, curHi);
170
- return;
171
- }
172
- const adjBlockIdx = nx + ny * nbx + nz * nbx * nby;
173
- if (hasSolid(adjBlockIdx)) {
174
- if (plusZ) {
175
- write(curLo, curHi | FACE_Z3_HI);
176
- }
177
- else {
178
- write(curLo | FACE_Z0_LO, curHi);
179
- }
180
- return;
181
- }
182
- const adjIdx = getMixedIndex(adjBlockIdx);
183
- if (adjIdx === undefined) {
184
- write(curLo, curHi);
185
- return;
186
- }
187
- const adjLo = masks[adjIdx * 2];
188
- const adjHi = masks[adjIdx * 2 + 1];
189
- if (plusZ) {
190
- write(curLo, curHi | ((adjLo & FACE_Z0_LO) << 16));
191
- }
192
- else {
193
- write(curLo | ((adjHi & FACE_Z3_HI) >>> 16), curHi);
194
- }
195
- };
196
- /**
197
- * Block cleanup pass:
198
- * - remove voxels that have no supporting 6-neighborhood occupancy
199
- * - fill single-voxel holes fully enclosed by 6 neighbors
200
- * Includes cross-block neighbor propagation for face-adjacent blocks.
201
- */
202
- export const filterAndFillBlocks = (blocks, nbx = Infinity, nby = Infinity, nbz = Infinity) => {
203
- const mixed = blocks.getMixedBlocks();
204
- const solids = blocks.getSolidBlocks();
205
- const mixedCount = mixed.blockIdx.length;
206
- const masks = mixed.masks;
207
- if (mixedCount === 0) {
208
- return blocks;
209
- }
210
- const mixedBlockIdx = new Float64Array(mixedCount);
211
- for (let i = 0; i < mixedCount; i++) {
212
- mixedBlockIdx[i] = mixed.blockIdx[i];
213
- }
214
- sortMixedByBlockIdx(mixedBlockIdx, masks);
215
- const sortedSolid = new Float64Array(solids.length);
216
- for (let i = 0; i < solids.length; i++) {
217
- sortedSolid[i] = solids[i];
218
- }
219
- sortedSolid.sort();
220
- const hasSolid = (blockIdx) => sortedUint32Has(sortedSolid, blockIdx);
221
- const getMixedIndex = (blockIdx) => findMixedBlockIndex(mixedBlockIdx, blockIdx);
222
- const newMasks = new Uint32Array(masks.length);
223
- let voxelsRemoved = 0;
224
- let voxelsFilled = 0;
225
- for (let i = 0; i < mixedCount; i++) {
226
- const blockIdx = mixedBlockIdx[i];
227
- const origLo = masks[i * 2];
228
- const origHi = masks[i * 2 + 1];
229
- const bx = blockIdx % nbx;
230
- const byBz = (blockIdx / nbx) | 0;
231
- const by = byBz % nby;
232
- const bz = (blockIdx / (nbx * nby)) | 0;
233
- let pxLo = (origLo >>> 1) & ~FACE_X3;
234
- let pxHi = (origHi >>> 1) & ~FACE_X3;
235
- let mxLo = (origLo << 1) & ~FACE_X0;
236
- let mxHi = (origHi << 1) & ~FACE_X0;
237
- let pyLo = (origLo >>> 4) & ~FACE_Y3;
238
- let pyHi = (origHi >>> 4) & ~FACE_Y3;
239
- let myLo = (origLo << 4) & ~FACE_Y0;
240
- let myHi = (origHi << 4) & ~FACE_Y0;
241
- let pzLo = (origLo >>> 16) | (origHi << 16);
242
- let pzHi = origHi >>> 16;
243
- let mzLo = origLo << 16;
244
- let mzHi = (origHi << 16) | (origLo >>> 16);
245
- addCrossFace(bx + 1, by, bz, nbx, nby, nbz, hasSolid, getMixedIndex, masks, FACE_X3, FACE_X0, 3, true, pxLo, pxHi, (lo, hi) => { pxLo = lo; pxHi = hi; });
246
- addCrossFace(bx - 1, by, bz, nbx, nby, nbz, hasSolid, getMixedIndex, masks, FACE_X0, FACE_X3, 3, false, mxLo, mxHi, (lo, hi) => { mxLo = lo; mxHi = hi; });
247
- addCrossFace(bx, by + 1, bz, nbx, nby, nbz, hasSolid, getMixedIndex, masks, FACE_Y3, FACE_Y0, 12, true, pyLo, pyHi, (lo, hi) => { pyLo = lo; pyHi = hi; });
248
- addCrossFace(bx, by - 1, bz, nbx, nby, nbz, hasSolid, getMixedIndex, masks, FACE_Y0, FACE_Y3, 12, false, myLo, myHi, (lo, hi) => { myLo = lo; myHi = hi; });
249
- addCrossFaceZ(bx, by, bz + 1, nbx, nby, nbz, hasSolid, getMixedIndex, masks, true, pzLo, pzHi, (lo, hi) => { pzLo = lo; pzHi = hi; });
250
- addCrossFaceZ(bx, by, bz - 1, nbx, nby, nbz, hasSolid, getMixedIndex, masks, false, mzLo, mzHi, (lo, hi) => { mzLo = lo; mzHi = hi; });
251
- const neighborLo = pxLo | mxLo | pyLo | myLo | pzLo | mzLo;
252
- const neighborHi = pxHi | mxHi | pyHi | myHi | pzHi | mzHi;
253
- let lo = origLo & neighborLo;
254
- let hi = origHi & neighborHi;
255
- const fillLo = ~lo & pxLo & mxLo & pyLo & myLo & pzLo & mzLo;
256
- const fillHi = ~hi & pxHi & mxHi & pyHi & myHi & pzHi & mzHi;
257
- lo |= fillLo;
258
- hi |= fillHi;
259
- voxelsRemoved += popcount(origLo & ~lo) + popcount(origHi & ~hi);
260
- voxelsFilled += popcount(lo & ~origLo) + popcount(hi & ~origHi);
261
- newMasks[i * 2] = lo >>> 0;
262
- newMasks[i * 2 + 1] = hi >>> 0;
263
- }
264
- const result = new BlockMaskBuffer();
265
- for (let i = 0; i < mixedCount; i++) {
266
- const lo = newMasks[i * 2];
267
- const hi = newMasks[i * 2 + 1];
268
- result.addBlock(mixedBlockIdx[i], lo, hi);
269
- }
270
- for (let i = 0; i < solids.length; i++) {
271
- result.addBlock(solids[i], SOLID_LO, SOLID_HI);
272
- }
273
- logger.info(`voxel filter: ${voxelsRemoved} voxels removed, ${voxelsFilled} voxels filled`);
274
- return result;
275
- };
276
- /** Crop blocks into [min, max) block range and rebase linear block coordinates. */
277
- export const cropBlocksToRange = (blocks, sourceNbx, sourceNby, cropMinBx, cropMinBy, cropMinBz, cropMaxBx, cropMaxBy, cropMaxBz) => {
278
- const cropped = new BlockMaskBuffer();
279
- const outNbx = cropMaxBx - cropMinBx;
280
- const outNby = cropMaxBy - cropMinBy;
281
- const sourceBStride = sourceNbx * sourceNby;
282
- const solids = blocks.getSolidBlocks();
283
- for (let i = 0; i < solids.length; i++) {
284
- const blockIdx = solids[i];
285
- const bx = blockIdx % sourceNbx;
286
- const byBz = (blockIdx / sourceNbx) | 0;
287
- const by = byBz % sourceNby;
288
- const bz = (blockIdx / sourceBStride) | 0;
289
- if (bx < cropMinBx || by < cropMinBy || bz < cropMinBz) {
290
- continue;
291
- }
292
- if (bx >= cropMaxBx || by >= cropMaxBy || bz >= cropMaxBz) {
293
- continue;
294
- }
295
- cropped.addBlock((bx - cropMinBx) + (by - cropMinBy) * outNbx + (bz - cropMinBz) * outNbx * outNby, SOLID_LO, SOLID_HI);
296
- }
297
- const mixed = blocks.getMixedBlocks();
298
- for (let i = 0; i < mixed.blockIdx.length; i++) {
299
- const blockIdx = mixed.blockIdx[i];
300
- const bx = blockIdx % sourceNbx;
301
- const byBz = (blockIdx / sourceNbx) | 0;
302
- const by = byBz % sourceNby;
303
- const bz = (blockIdx / sourceBStride) | 0;
304
- if (bx < cropMinBx || by < cropMinBy || bz < cropMinBz) {
305
- continue;
306
- }
307
- if (bx >= cropMaxBx || by >= cropMaxBy || bz >= cropMaxBz) {
308
- continue;
309
- }
310
- cropped.addBlock((bx - cropMinBx) + (by - cropMinBy) * outNbx + (bz - cropMinBz) * outNbx * outNby, mixed.masks[i * 2], mixed.masks[i * 2 + 1]);
311
- }
312
- return cropped;
313
- };
314
- /** Compute world-space bounds corresponding to a cropped block range. */
315
- export const cropBounds = (gridBounds, voxelResolution, cropMinBx, cropMinBy, cropMinBz, cropMaxBx, cropMaxBy, cropMaxBz) => {
316
- const blockSize = 4 * voxelResolution;
317
- const croppedMin = {
318
- x: gridBounds.min.x + cropMinBx * blockSize,
319
- y: gridBounds.min.y + cropMinBy * blockSize,
320
- z: gridBounds.min.z + cropMinBz * blockSize
321
- };
322
- return {
323
- min: croppedMin,
324
- max: {
325
- x: croppedMin.x + (cropMaxBx - cropMinBx) * blockSize,
326
- y: croppedMin.y + (cropMaxBy - cropMinBy) * blockSize,
327
- z: croppedMin.z + (cropMaxBz - cropMinBz) * blockSize
328
- }
329
- };
330
- };
331
- /** Tight crop to occupied block bounds. */
332
- export const cropToOccupied = (grid, gridBounds, voxelResolution) => {
333
- const occupied = grid.getOccupiedBlockBounds();
334
- if (!occupied) {
335
- return { grid, gridBounds };
336
- }
337
- const { minBx, minBy, minBz, maxBx, maxBy, maxBz } = occupied;
338
- const cropMaxBx = maxBx + 1;
339
- const cropMaxBy = maxBy + 1;
340
- const cropMaxBz = maxBz + 1;
341
- const { nbx, nby, nbz } = grid;
342
- if (minBx === 0 && minBy === 0 && minBz === 0 && cropMaxBx === nbx && cropMaxBy === nby && cropMaxBz === nbz) {
343
- return { grid, gridBounds };
344
- }
345
- return {
346
- grid: grid.cropTo(minBx, minBy, minBz, cropMaxBx, cropMaxBy, cropMaxBz),
347
- gridBounds: cropBounds(gridBounds, voxelResolution, minBx, minBy, minBz, cropMaxBx, cropMaxBy, cropMaxBz)
348
- };
349
- };
350
- /** Tight crop to navigable (non-fully-solid) block bounds. */
351
- export const cropToNavigable = (grid, gridBounds, voxelResolution) => {
352
- const navBounds = grid.getNavigableBlockBounds();
353
- if (!navBounds) {
354
- return { grid, gridBounds };
355
- }
356
- const { minBx, minBy, minBz, maxBx, maxBy, maxBz } = navBounds;
357
- const { nbx, nby, nbz } = grid;
358
- // Keep one solid wall block around the navigable cavity. The runtime
359
- // treats out-of-grid as solid, but collision extraction sees out-of-grid
360
- // as empty; this padding keeps collision meshes sealed at crop edges.
361
- const MARGIN = 1;
362
- const cropMinBx = Math.max(0, minBx - MARGIN);
363
- const cropMinBy = Math.max(0, minBy - MARGIN);
364
- const cropMinBz = Math.max(0, minBz - MARGIN);
365
- const cropMaxBx = Math.min(nbx, maxBx + 1 + MARGIN);
366
- const cropMaxBy = Math.min(nby, maxBy + 1 + MARGIN);
367
- const cropMaxBz = Math.min(nbz, maxBz + 1 + MARGIN);
368
- if (cropMinBx === 0 && cropMinBy === 0 && cropMinBz === 0 && cropMaxBx === nbx && cropMaxBy === nby && cropMaxBz === nbz) {
369
- return { grid, gridBounds };
370
- }
371
- return {
372
- grid: grid.cropTo(cropMinBx, cropMinBy, cropMinBz, cropMaxBx, cropMaxBy, cropMaxBz),
373
- gridBounds: cropBounds(gridBounds, voxelResolution, cropMinBx, cropMinBy, cropMinBz, cropMaxBx, cropMaxBy, cropMaxBz)
374
- };
375
- };
1
+ import { BlockMaskBuffer, SOLID_HI, SOLID_LO } from './common.js';
2
+ import { logger } from '../Logger.js';
3
+ const FACE_X0 = 0x11111111;
4
+ const FACE_X3 = 0x88888888;
5
+ const FACE_Y0 = 0x000f000f;
6
+ const FACE_Y3 = 0xf000f000;
7
+ const FACE_Z0_LO = 0x0000ffff;
8
+ const FACE_Z3_HI = 0xffff0000 >>> 0;
9
+ /** Count set bits in a 32-bit unsigned integer. */
10
+ function popcount(n) {
11
+ n >>>= 0;
12
+ n -= (n >>> 1) & 0x55555555;
13
+ n = (n & 0x33333333) + ((n >>> 2) & 0x33333333);
14
+ return (((n + (n >>> 4)) & 0x0f0f0f0f) * 0x01010101) >>> 24;
15
+ }
16
+ function sortedUint32Has(sorted, value) {
17
+ let lo = 0;
18
+ let hi = sorted.length - 1;
19
+ while (lo <= hi) {
20
+ const mid = (lo + hi) >> 1;
21
+ const v = sorted[mid];
22
+ if (v < value) {
23
+ lo = mid + 1;
24
+ }
25
+ else if (v > value) {
26
+ hi = mid - 1;
27
+ }
28
+ else {
29
+ return true;
30
+ }
31
+ }
32
+ return false;
33
+ }
34
+ function findMixedBlockIndex(sortedBlockIdx, target) {
35
+ let lo = 0;
36
+ let hi = sortedBlockIdx.length - 1;
37
+ while (lo <= hi) {
38
+ const mid = (lo + hi) >> 1;
39
+ const v = sortedBlockIdx[mid];
40
+ if (v < target) {
41
+ lo = mid + 1;
42
+ }
43
+ else if (v > target) {
44
+ hi = mid - 1;
45
+ }
46
+ else {
47
+ return mid;
48
+ }
49
+ }
50
+ return undefined;
51
+ }
52
+ function sortMixedByBlockIdx(blockIdx, masks) {
53
+ const n = blockIdx.length;
54
+ if (n <= 1) {
55
+ return;
56
+ }
57
+ const stackLo = [0];
58
+ const stackHi = [n - 1];
59
+ function swap(a, b) {
60
+ const k = blockIdx[a];
61
+ blockIdx[a] = blockIdx[b];
62
+ blockIdx[b] = k;
63
+ const alo = masks[a * 2];
64
+ const ahi = masks[a * 2 + 1];
65
+ masks[a * 2] = masks[b * 2];
66
+ masks[a * 2 + 1] = masks[b * 2 + 1];
67
+ masks[b * 2] = alo;
68
+ masks[b * 2 + 1] = ahi;
69
+ }
70
+ while (stackLo.length > 0) {
71
+ const lo = stackLo.pop();
72
+ const hi = stackHi.pop();
73
+ if (hi - lo < 16) {
74
+ for (let i = lo + 1; i <= hi; i++) {
75
+ const k = blockIdx[i];
76
+ const m0 = masks[i * 2];
77
+ const m1 = masks[i * 2 + 1];
78
+ let j = i - 1;
79
+ while (j >= lo && blockIdx[j] > k) {
80
+ blockIdx[j + 1] = blockIdx[j];
81
+ masks[(j + 1) * 2] = masks[j * 2];
82
+ masks[(j + 1) * 2 + 1] = masks[j * 2 + 1];
83
+ j--;
84
+ }
85
+ blockIdx[j + 1] = k;
86
+ masks[(j + 1) * 2] = m0;
87
+ masks[(j + 1) * 2 + 1] = m1;
88
+ }
89
+ continue;
90
+ }
91
+ const mid = (lo + hi) >>> 1;
92
+ if (blockIdx[mid] < blockIdx[lo]) {
93
+ swap(mid, lo);
94
+ }
95
+ if (blockIdx[hi] < blockIdx[lo]) {
96
+ swap(hi, lo);
97
+ }
98
+ if (blockIdx[hi] < blockIdx[mid]) {
99
+ swap(hi, mid);
100
+ }
101
+ const pivot = blockIdx[mid];
102
+ let i = lo;
103
+ let j = hi;
104
+ while (i <= j) {
105
+ while (blockIdx[i] < pivot) {
106
+ i++;
107
+ }
108
+ while (blockIdx[j] > pivot) {
109
+ j--;
110
+ }
111
+ if (i <= j) {
112
+ if (i !== j) {
113
+ swap(i, j);
114
+ }
115
+ i++;
116
+ j--;
117
+ }
118
+ }
119
+ if (j - lo > hi - i) {
120
+ if (lo < j) {
121
+ stackLo.push(lo);
122
+ stackHi.push(j);
123
+ }
124
+ if (i < hi) {
125
+ stackLo.push(i);
126
+ stackHi.push(hi);
127
+ }
128
+ }
129
+ else {
130
+ if (i < hi) {
131
+ stackLo.push(i);
132
+ stackHi.push(hi);
133
+ }
134
+ if (lo < j) {
135
+ stackLo.push(lo);
136
+ stackHi.push(j);
137
+ }
138
+ }
139
+ }
140
+ }
141
+ function addCrossFace(nx, ny, nz, nbx, nby, nbz, hasSolid, getMixedIndex, masks, ourFaceMask, adjFaceMask, shiftAmount, shiftLeft, curLo, curHi, write) {
142
+ if (nx < 0 || ny < 0 || nz < 0 || nx >= nbx || ny >= nby || nz >= nbz) {
143
+ write(curLo, curHi);
144
+ return;
145
+ }
146
+ const adjBlockIdx = nx + ny * nbx + nz * nbx * nby;
147
+ if (hasSolid(adjBlockIdx)) {
148
+ write(curLo | ourFaceMask, curHi | ourFaceMask);
149
+ return;
150
+ }
151
+ const adjIdx = getMixedIndex(adjBlockIdx);
152
+ if (adjIdx === undefined) {
153
+ write(curLo, curHi);
154
+ return;
155
+ }
156
+ const adjLo = masks[adjIdx * 2];
157
+ const adjHi = masks[adjIdx * 2 + 1];
158
+ const faceLo = adjLo & adjFaceMask;
159
+ const faceHi = adjHi & adjFaceMask;
160
+ if (shiftLeft) {
161
+ write(curLo | (faceLo << shiftAmount), curHi | (faceHi << shiftAmount));
162
+ }
163
+ else {
164
+ write(curLo | (faceLo >>> shiftAmount), curHi | (faceHi >>> shiftAmount));
165
+ }
166
+ }
167
+ function addCrossFaceZ(nx, ny, nz, nbx, nby, nbz, hasSolid, getMixedIndex, masks, plusZ, curLo, curHi, write) {
168
+ if (nx < 0 || ny < 0 || nz < 0 || nx >= nbx || ny >= nby || nz >= nbz) {
169
+ write(curLo, curHi);
170
+ return;
171
+ }
172
+ const adjBlockIdx = nx + ny * nbx + nz * nbx * nby;
173
+ if (hasSolid(adjBlockIdx)) {
174
+ if (plusZ) {
175
+ write(curLo, curHi | FACE_Z3_HI);
176
+ }
177
+ else {
178
+ write(curLo | FACE_Z0_LO, curHi);
179
+ }
180
+ return;
181
+ }
182
+ const adjIdx = getMixedIndex(adjBlockIdx);
183
+ if (adjIdx === undefined) {
184
+ write(curLo, curHi);
185
+ return;
186
+ }
187
+ const adjLo = masks[adjIdx * 2];
188
+ const adjHi = masks[adjIdx * 2 + 1];
189
+ if (plusZ) {
190
+ write(curLo, curHi | ((adjLo & FACE_Z0_LO) << 16));
191
+ }
192
+ else {
193
+ write(curLo | ((adjHi & FACE_Z3_HI) >>> 16), curHi);
194
+ }
195
+ }
196
+ /**
197
+ * Block cleanup pass:
198
+ * - remove voxels that have no supporting 6-neighborhood occupancy
199
+ * - fill single-voxel holes fully enclosed by 6 neighbors
200
+ * Includes cross-block neighbor propagation for face-adjacent blocks.
201
+ */
202
+ export function filterAndFillBlocks(blocks, nbx = Infinity, nby = Infinity, nbz = Infinity) {
203
+ const mixed = blocks.getMixedBlocks();
204
+ const solids = blocks.getSolidBlocks();
205
+ const mixedCount = mixed.blockIdx.length;
206
+ const masks = mixed.masks;
207
+ if (mixedCount === 0) {
208
+ return blocks;
209
+ }
210
+ const mixedBlockIdx = new Float64Array(mixedCount);
211
+ for (let i = 0; i < mixedCount; i++) {
212
+ mixedBlockIdx[i] = mixed.blockIdx[i];
213
+ }
214
+ sortMixedByBlockIdx(mixedBlockIdx, masks);
215
+ const sortedSolid = new Float64Array(solids.length);
216
+ for (let i = 0; i < solids.length; i++) {
217
+ sortedSolid[i] = solids[i];
218
+ }
219
+ sortedSolid.sort();
220
+ function hasSolid(blockIdx) {
221
+ return sortedUint32Has(sortedSolid, blockIdx);
222
+ }
223
+ function getMixedIndex(blockIdx) {
224
+ return findMixedBlockIndex(mixedBlockIdx, blockIdx);
225
+ }
226
+ const newMasks = new Uint32Array(masks.length);
227
+ let voxelsRemoved = 0;
228
+ let voxelsFilled = 0;
229
+ for (let i = 0; i < mixedCount; i++) {
230
+ const blockIdx = mixedBlockIdx[i];
231
+ const origLo = masks[i * 2];
232
+ const origHi = masks[i * 2 + 1];
233
+ const bx = blockIdx % nbx;
234
+ const byBz = (blockIdx / nbx) | 0;
235
+ const by = byBz % nby;
236
+ const bz = (blockIdx / (nbx * nby)) | 0;
237
+ let pxLo = (origLo >>> 1) & ~FACE_X3;
238
+ let pxHi = (origHi >>> 1) & ~FACE_X3;
239
+ let mxLo = (origLo << 1) & ~FACE_X0;
240
+ let mxHi = (origHi << 1) & ~FACE_X0;
241
+ let pyLo = (origLo >>> 4) & ~FACE_Y3;
242
+ let pyHi = (origHi >>> 4) & ~FACE_Y3;
243
+ let myLo = (origLo << 4) & ~FACE_Y0;
244
+ let myHi = (origHi << 4) & ~FACE_Y0;
245
+ let pzLo = (origLo >>> 16) | (origHi << 16);
246
+ let pzHi = origHi >>> 16;
247
+ let mzLo = origLo << 16;
248
+ let mzHi = (origHi << 16) | (origLo >>> 16);
249
+ function writePx(lo, hi) {
250
+ pxLo = lo;
251
+ pxHi = hi;
252
+ }
253
+ function writeMx(lo, hi) {
254
+ mxLo = lo;
255
+ mxHi = hi;
256
+ }
257
+ function writePy(lo, hi) {
258
+ pyLo = lo;
259
+ pyHi = hi;
260
+ }
261
+ function writeMy(lo, hi) {
262
+ myLo = lo;
263
+ myHi = hi;
264
+ }
265
+ function writePz(lo, hi) {
266
+ pzLo = lo;
267
+ pzHi = hi;
268
+ }
269
+ function writeMz(lo, hi) {
270
+ mzLo = lo;
271
+ mzHi = hi;
272
+ }
273
+ addCrossFace(bx + 1, by, bz, nbx, nby, nbz, hasSolid, getMixedIndex, masks, FACE_X3, FACE_X0, 3, true, pxLo, pxHi, writePx);
274
+ addCrossFace(bx - 1, by, bz, nbx, nby, nbz, hasSolid, getMixedIndex, masks, FACE_X0, FACE_X3, 3, false, mxLo, mxHi, writeMx);
275
+ addCrossFace(bx, by + 1, bz, nbx, nby, nbz, hasSolid, getMixedIndex, masks, FACE_Y3, FACE_Y0, 12, true, pyLo, pyHi, writePy);
276
+ addCrossFace(bx, by - 1, bz, nbx, nby, nbz, hasSolid, getMixedIndex, masks, FACE_Y0, FACE_Y3, 12, false, myLo, myHi, writeMy);
277
+ addCrossFaceZ(bx, by, bz + 1, nbx, nby, nbz, hasSolid, getMixedIndex, masks, true, pzLo, pzHi, writePz);
278
+ addCrossFaceZ(bx, by, bz - 1, nbx, nby, nbz, hasSolid, getMixedIndex, masks, false, mzLo, mzHi, writeMz);
279
+ const neighborLo = pxLo | mxLo | pyLo | myLo | pzLo | mzLo;
280
+ const neighborHi = pxHi | mxHi | pyHi | myHi | pzHi | mzHi;
281
+ let lo = origLo & neighborLo;
282
+ let hi = origHi & neighborHi;
283
+ const fillLo = ~lo & pxLo & mxLo & pyLo & myLo & pzLo & mzLo;
284
+ const fillHi = ~hi & pxHi & mxHi & pyHi & myHi & pzHi & mzHi;
285
+ lo |= fillLo;
286
+ hi |= fillHi;
287
+ voxelsRemoved += popcount(origLo & ~lo) + popcount(origHi & ~hi);
288
+ voxelsFilled += popcount(lo & ~origLo) + popcount(hi & ~origHi);
289
+ newMasks[i * 2] = lo >>> 0;
290
+ newMasks[i * 2 + 1] = hi >>> 0;
291
+ }
292
+ const result = new BlockMaskBuffer();
293
+ for (let i = 0; i < mixedCount; i++) {
294
+ const lo = newMasks[i * 2];
295
+ const hi = newMasks[i * 2 + 1];
296
+ result.addBlock(mixedBlockIdx[i], lo, hi);
297
+ }
298
+ for (let i = 0; i < solids.length; i++) {
299
+ result.addBlock(solids[i], SOLID_LO, SOLID_HI);
300
+ }
301
+ logger.info(`voxel filter: ${voxelsRemoved} voxels removed, ${voxelsFilled} voxels filled`);
302
+ return result;
303
+ }
304
+ /** Crop blocks into [min, max) block range and rebase linear block coordinates. */
305
+ export function cropBlocksToRange(blocks, sourceNbx, sourceNby, cropMinBx, cropMinBy, cropMinBz, cropMaxBx, cropMaxBy, cropMaxBz) {
306
+ const cropped = new BlockMaskBuffer();
307
+ const outNbx = cropMaxBx - cropMinBx;
308
+ const outNby = cropMaxBy - cropMinBy;
309
+ const sourceBStride = sourceNbx * sourceNby;
310
+ const solids = blocks.getSolidBlocks();
311
+ for (let i = 0; i < solids.length; i++) {
312
+ const blockIdx = solids[i];
313
+ const bx = blockIdx % sourceNbx;
314
+ const byBz = (blockIdx / sourceNbx) | 0;
315
+ const by = byBz % sourceNby;
316
+ const bz = (blockIdx / sourceBStride) | 0;
317
+ if (bx < cropMinBx || by < cropMinBy || bz < cropMinBz) {
318
+ continue;
319
+ }
320
+ if (bx >= cropMaxBx || by >= cropMaxBy || bz >= cropMaxBz) {
321
+ continue;
322
+ }
323
+ cropped.addBlock(bx - cropMinBx + (by - cropMinBy) * outNbx + (bz - cropMinBz) * outNbx * outNby, SOLID_LO, SOLID_HI);
324
+ }
325
+ const mixed = blocks.getMixedBlocks();
326
+ for (let i = 0; i < mixed.blockIdx.length; i++) {
327
+ const blockIdx = mixed.blockIdx[i];
328
+ const bx = blockIdx % sourceNbx;
329
+ const byBz = (blockIdx / sourceNbx) | 0;
330
+ const by = byBz % sourceNby;
331
+ const bz = (blockIdx / sourceBStride) | 0;
332
+ if (bx < cropMinBx || by < cropMinBy || bz < cropMinBz) {
333
+ continue;
334
+ }
335
+ if (bx >= cropMaxBx || by >= cropMaxBy || bz >= cropMaxBz) {
336
+ continue;
337
+ }
338
+ cropped.addBlock(bx - cropMinBx + (by - cropMinBy) * outNbx + (bz - cropMinBz) * outNbx * outNby, mixed.masks[i * 2], mixed.masks[i * 2 + 1]);
339
+ }
340
+ return cropped;
341
+ }
342
+ /** Compute world-space bounds corresponding to a cropped block range. */
343
+ export function cropBounds(gridBounds, voxelResolution, cropMinBx, cropMinBy, cropMinBz, cropMaxBx, cropMaxBy, cropMaxBz) {
344
+ const blockSize = 4 * voxelResolution;
345
+ const croppedMin = {
346
+ x: gridBounds.min.x + cropMinBx * blockSize,
347
+ y: gridBounds.min.y + cropMinBy * blockSize,
348
+ z: gridBounds.min.z + cropMinBz * blockSize,
349
+ };
350
+ return {
351
+ min: croppedMin,
352
+ max: {
353
+ x: croppedMin.x + (cropMaxBx - cropMinBx) * blockSize,
354
+ y: croppedMin.y + (cropMaxBy - cropMinBy) * blockSize,
355
+ z: croppedMin.z + (cropMaxBz - cropMinBz) * blockSize,
356
+ },
357
+ };
358
+ }
359
+ /** Tight crop to occupied block bounds. */
360
+ export function cropToOccupied(grid, gridBounds, voxelResolution) {
361
+ const occupied = grid.getOccupiedBlockBounds();
362
+ if (!occupied) {
363
+ return { grid, gridBounds };
364
+ }
365
+ const { minBx, minBy, minBz, maxBx, maxBy, maxBz } = occupied;
366
+ const cropMaxBx = maxBx + 1;
367
+ const cropMaxBy = maxBy + 1;
368
+ const cropMaxBz = maxBz + 1;
369
+ const { nbx, nby, nbz } = grid;
370
+ if (minBx === 0 && minBy === 0 && minBz === 0 && cropMaxBx === nbx && cropMaxBy === nby && cropMaxBz === nbz) {
371
+ return { grid, gridBounds };
372
+ }
373
+ return {
374
+ grid: grid.cropTo(minBx, minBy, minBz, cropMaxBx, cropMaxBy, cropMaxBz),
375
+ gridBounds: cropBounds(gridBounds, voxelResolution, minBx, minBy, minBz, cropMaxBx, cropMaxBy, cropMaxBz),
376
+ };
377
+ }
378
+ /** Tight crop to navigable (non-fully-solid) block bounds. */
379
+ export function cropToNavigable(grid, gridBounds, voxelResolution) {
380
+ const navBounds = grid.getNavigableBlockBounds();
381
+ if (!navBounds) {
382
+ return { grid, gridBounds };
383
+ }
384
+ const { minBx, minBy, minBz, maxBx, maxBy, maxBz } = navBounds;
385
+ const { nbx, nby, nbz } = grid;
386
+ // Keep one solid wall block around the navigable cavity. The runtime
387
+ // treats out-of-grid as solid, but collision extraction sees out-of-grid
388
+ // as empty; this padding keeps collision meshes sealed at crop edges.
389
+ const MARGIN = 1;
390
+ const cropMinBx = Math.max(0, minBx - MARGIN);
391
+ const cropMinBy = Math.max(0, minBy - MARGIN);
392
+ const cropMinBz = Math.max(0, minBz - MARGIN);
393
+ const cropMaxBx = Math.min(nbx, maxBx + 1 + MARGIN);
394
+ const cropMaxBy = Math.min(nby, maxBy + 1 + MARGIN);
395
+ const cropMaxBz = Math.min(nbz, maxBz + 1 + MARGIN);
396
+ if (cropMinBx === 0 &&
397
+ cropMinBy === 0 &&
398
+ cropMinBz === 0 &&
399
+ cropMaxBx === nbx &&
400
+ cropMaxBy === nby &&
401
+ cropMaxBz === nbz) {
402
+ return { grid, gridBounds };
403
+ }
404
+ return {
405
+ grid: grid.cropTo(cropMinBx, cropMinBy, cropMinBz, cropMaxBx, cropMaxBy, cropMaxBz),
406
+ gridBounds: cropBounds(gridBounds, voxelResolution, cropMinBx, cropMinBy, cropMinBz, cropMaxBx, cropMaxBy, cropMaxBz),
407
+ };
408
+ }