@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.
Files changed (97) hide show
  1. package/CHANGELOG.md +124 -113
  2. package/README.md +39 -39
  3. package/THIRD_PARTY_LICENSES.txt +1373 -1373
  4. package/bin/cli.js +125 -118
  5. package/dist/SplatData.d.ts +67 -67
  6. package/dist/SplatData.js +167 -150
  7. package/dist/constant.d.ts +3 -3
  8. package/dist/constant.js +13 -13
  9. package/dist/file/IFile.d.ts +5 -5
  10. package/dist/file/IFile.js +1 -1
  11. package/dist/file/esz.d.ts +11 -11
  12. package/dist/file/esz.js +337 -322
  13. package/dist/file/index.d.ts +8 -8
  14. package/dist/file/index.js +7 -7
  15. package/dist/file/ksplat.d.ts +12 -12
  16. package/dist/file/ksplat.js +293 -231
  17. package/dist/file/lcc.d.ts +11 -11
  18. package/dist/file/lcc.js +161 -158
  19. package/dist/file/ply.d.ts +13 -13
  20. package/dist/file/ply.js +439 -390
  21. package/dist/file/sog.d.ts +80 -80
  22. package/dist/file/sog.js +525 -494
  23. package/dist/file/splat.d.ts +6 -6
  24. package/dist/file/splat.js +119 -99
  25. package/dist/file/spz.d.ts +11 -11
  26. package/dist/file/spz.js +597 -583
  27. package/dist/file/voxel.d.ts +43 -37
  28. package/dist/file/voxel.js +411 -280
  29. package/dist/index.d.ts +33 -33
  30. package/dist/index.js +54 -54
  31. package/dist/native/index.d.ts +54 -54
  32. package/dist/native/index.js +122 -129
  33. package/dist/native/utils.d.ts +1 -0
  34. package/dist/native/utils.js +54 -0
  35. package/dist/tasks/AutoChunkLodTask.d.ts +13 -13
  36. package/dist/tasks/AutoChunkLodTask.js +117 -117
  37. package/dist/tasks/AutoLodTask.d.ts +10 -10
  38. package/dist/tasks/AutoLodTask.js +20 -20
  39. package/dist/tasks/BaseTask.d.ts +15 -15
  40. package/dist/tasks/BaseTask.js +5 -5
  41. package/dist/tasks/FlexLodTask.d.ts +12 -12
  42. package/dist/tasks/FlexLodTask.js +54 -44
  43. package/dist/tasks/ModifyTask.d.ts +9 -9
  44. package/dist/tasks/ModifyTask.js +166 -156
  45. package/dist/tasks/ReadTask.d.ts +9 -9
  46. package/dist/tasks/ReadTask.js +29 -29
  47. package/dist/tasks/SkeletonLodTask.d.ts +10 -10
  48. package/dist/tasks/SkeletonLodTask.js +176 -156
  49. package/dist/tasks/VoxelTask.d.ts +35 -30
  50. package/dist/tasks/VoxelTask.js +40 -37
  51. package/dist/tasks/WriteTask.d.ts +12 -12
  52. package/dist/tasks/WriteTask.js +70 -70
  53. package/dist/utils/BufferReader.d.ts +12 -12
  54. package/dist/utils/BufferReader.js +45 -45
  55. package/dist/utils/Logger.d.ts +11 -11
  56. package/dist/utils/Logger.js +40 -40
  57. package/dist/utils/StreamChunkDecoder.d.ts +16 -16
  58. package/dist/utils/StreamChunkDecoder.js +31 -31
  59. package/dist/utils/index.d.ts +27 -27
  60. package/dist/utils/index.js +101 -101
  61. package/dist/utils/k-means.d.ts +4 -4
  62. package/dist/utils/k-means.js +340 -341
  63. package/dist/utils/math.d.ts +46 -46
  64. package/dist/utils/math.js +350 -346
  65. package/dist/utils/quantize-1d.d.ts +4 -4
  66. package/dist/utils/quantize-1d.js +164 -164
  67. package/dist/utils/sh-rotate.d.ts +2 -2
  68. package/dist/utils/sh-rotate.js +236 -175
  69. package/dist/utils/splat.d.ts +21 -21
  70. package/dist/utils/splat.js +397 -387
  71. package/dist/utils/voxel/binary.d.ts +8 -0
  72. package/dist/utils/voxel/binary.js +176 -0
  73. package/dist/utils/voxel/common.d.ts +178 -162
  74. package/dist/utils/voxel/common.js +1752 -1682
  75. package/dist/utils/voxel/coplanar-merge.d.ts +63 -63
  76. package/dist/utils/voxel/coplanar-merge.js +818 -819
  77. package/dist/utils/voxel/filter-cluster.d.ts +20 -0
  78. package/dist/utils/voxel/filter-cluster.js +628 -0
  79. package/dist/utils/voxel/gpu-dilation.d.ts +2 -2
  80. package/dist/utils/voxel/gpu-dilation.js +677 -656
  81. package/dist/utils/voxel/marching-cubes.d.ts +42 -42
  82. package/dist/utils/voxel/marching-cubes.js +1645 -1657
  83. package/dist/utils/voxel/mesh.d.ts +3 -3
  84. package/dist/utils/voxel/mesh.js +130 -130
  85. package/dist/utils/voxel/nav.d.ts +29 -29
  86. package/dist/utils/voxel/nav.js +1068 -1043
  87. package/dist/utils/voxel/postprocess.d.ts +23 -23
  88. package/dist/utils/voxel/postprocess.js +408 -375
  89. package/dist/utils/voxel/voxel-faces.d.ts +18 -18
  90. package/dist/utils/voxel/voxel-faces.js +662 -663
  91. package/dist/utils/voxel/voxelize.d.ts +34 -33
  92. package/dist/utils/voxel/voxelize.js +1208 -1193
  93. package/dist/utils/webgpu.d.ts +8 -8
  94. package/dist/utils/webgpu.js +122 -122
  95. package/package.json +38 -39
  96. package/dist/native/cpp/bin/linux/binding.node +0 -0
  97. 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
+ }