@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,1043 +1,1068 @@
1
- import { BLOCK_EMPTY, BLOCK_SOLID, BLOCK_MIXED, SOLID_LO, SOLID_HI, SparseVoxelGrid, readBlockType, writeBlockType } from './common.js';
2
- import { gpuDilate3 } from './gpu-dilation.js';
3
- import { logger } from '../Logger.js';
4
- const FACE_MASKS_LO = [
5
- 0x11111111 >>> 0, // -X
6
- 0x88888888 >>> 0, // +X
7
- 0x000F000F >>> 0, // -Y
8
- 0xF000F000 >>> 0, // +Y
9
- 0x0000FFFF >>> 0, // -Z
10
- 0x00000000 >>> 0, // +Z
11
- ];
12
- const FACE_MASKS_HI = [
13
- 0x11111111 >>> 0,
14
- 0x88888888 >>> 0,
15
- 0x000F000F >>> 0,
16
- 0xF000F000 >>> 0,
17
- 0x00000000 >>> 0,
18
- 0xFFFF0000 >>> 0,
19
- ];
20
- const forEachNonEmptyBlock = (grid, fn) => {
21
- const totalBlocks = grid.nbx * grid.nby * grid.nbz;
22
- for (let w = 0; w < grid.types.length; w++) {
23
- let nonEmpty = ((grid.types[w] & 0x55555555) | ((grid.types[w] >>> 1) & 0x55555555)) >>> 0;
24
- const baseIdx = w * 16;
25
- while (nonEmpty) {
26
- const bitPos = 31 - Math.clz32(nonEmpty & -nonEmpty);
27
- const blockIdx = baseIdx + (bitPos >>> 1);
28
- if (blockIdx >= totalBlocks) {
29
- break;
30
- }
31
- fn(blockIdx);
32
- nonEmpty &= nonEmpty - 1;
33
- }
34
- }
35
- };
36
- // Active block-pair extraction for separable dilation passes.
37
- function getActiveYZPairs(grid) {
38
- const pairs = new Set();
39
- const { nbx } = grid;
40
- forEachNonEmptyBlock(grid, (blockIdx) => pairs.add((blockIdx / nbx) | 0));
41
- return pairs;
42
- }
43
- function getActiveXZPairs(grid) {
44
- const pairs = new Set();
45
- const { nbx, bStride } = grid;
46
- forEachNonEmptyBlock(grid, (blockIdx) => {
47
- const bx = blockIdx % nbx;
48
- const bz = (blockIdx / bStride) | 0;
49
- pairs.add(bx + bz * nbx);
50
- });
51
- return pairs;
52
- }
53
- function getActiveXYPairs(grid) {
54
- const pairs = new Set();
55
- const { nbx, nby } = grid;
56
- forEachNonEmptyBlock(grid, (blockIdx) => {
57
- const bx = blockIdx % nbx;
58
- const by = ((blockIdx / nbx) | 0) % nby;
59
- pairs.add(bx + by * nbx);
60
- });
61
- return pairs;
62
- }
63
- // Line extraction/writeback helpers between sparse block masks and bit-packed 1D buffers.
64
- function extractLineX(grid, iy, iz, buf) {
65
- const by = iy >> 2, bz = iz >> 2;
66
- const bitBase = ((iz & 3) << 4) + ((iy & 3) << 2);
67
- const inHi = bitBase >= 32;
68
- const shift = inHi ? bitBase - 32 : bitBase;
69
- const lineBase = by * grid.nbx + bz * grid.bStride;
70
- for (let bx = 0; bx < grid.nbx; bx++) {
71
- const blockIdx = lineBase + bx;
72
- const bt = readBlockType(grid.types, blockIdx);
73
- if (bt === BLOCK_EMPTY) {
74
- continue;
75
- }
76
- let row4;
77
- if (bt === BLOCK_SOLID) {
78
- row4 = 0xF;
79
- }
80
- else {
81
- const s = grid.masks.slot(blockIdx);
82
- row4 = ((inHi ? grid.masks.hi[s] : grid.masks.lo[s]) >>> shift) & 0xF;
83
- }
84
- if (row4) {
85
- const ix = bx << 2;
86
- buf[ix >>> 5] |= (row4 << (ix & 31));
87
- }
88
- }
89
- }
90
- function writeLineX(grid, iy, iz, buf) {
91
- const by = iy >> 2, bz = iz >> 2;
92
- const bitBase = ((iz & 3) << 4) + ((iy & 3) << 2);
93
- const inHi = bitBase >= 32;
94
- const shift = inHi ? bitBase - 32 : bitBase;
95
- const lineBase = by * grid.nbx + bz * grid.bStride;
96
- for (let bx = 0; bx < grid.nbx; bx++) {
97
- const ix = bx << 2;
98
- const row4 = (buf[ix >>> 5] >>> (ix & 31)) & 0xF;
99
- if (!row4) {
100
- continue;
101
- }
102
- const blockIdx = lineBase + bx;
103
- grid.orBlock(blockIdx, inHi ? 0 : (row4 << shift) >>> 0, inHi ? (row4 << shift) >>> 0 : 0);
104
- }
105
- }
106
- function extractLineY(grid, ix, iz, buf) {
107
- const bx = ix >> 2, bz = iz >> 2;
108
- const lx = ix & 3, lz = iz & 3;
109
- const inHi = lz >= 2;
110
- const base = lx + (lz & 1) * 16;
111
- for (let by = 0; by < grid.nby; by++) {
112
- const blockIdx = bx + by * grid.nbx + bz * grid.bStride;
113
- const bt = readBlockType(grid.types, blockIdx);
114
- if (bt === BLOCK_EMPTY) {
115
- continue;
116
- }
117
- let row4;
118
- if (bt === BLOCK_SOLID) {
119
- row4 = 0xF;
120
- }
121
- else {
122
- const s = grid.masks.slot(blockIdx);
123
- const word = inHi ? grid.masks.hi[s] : grid.masks.lo[s];
124
- row4 = ((word >>> base) & 1) | (((word >>> (base + 4)) & 1) << 1) | (((word >>> (base + 8)) & 1) << 2) | (((word >>> (base + 12)) & 1) << 3);
125
- }
126
- if (row4) {
127
- const iy = by << 2;
128
- buf[iy >>> 5] |= (row4 << (iy & 31));
129
- }
130
- }
131
- }
132
- function writeLineY(grid, ix, iz, buf) {
133
- const bx = ix >> 2, bz = iz >> 2;
134
- const lx = ix & 3, lz = iz & 3;
135
- const inHi = lz >= 2;
136
- const base = lx + (lz & 1) * 16;
137
- for (let by = 0; by < grid.nby; by++) {
138
- const iy = by << 2;
139
- const row4 = (buf[iy >>> 5] >>> (iy & 31)) & 0xF;
140
- if (!row4) {
141
- continue;
142
- }
143
- const blockIdx = bx + by * grid.nbx + bz * grid.bStride;
144
- const bits = ((row4 & 1) << base) | (((row4 >>> 1) & 1) << (base + 4)) | (((row4 >>> 2) & 1) << (base + 8)) | (((row4 >>> 3) & 1) << (base + 12));
145
- grid.orBlock(blockIdx, inHi ? 0 : bits >>> 0, inHi ? bits >>> 0 : 0);
146
- }
147
- }
148
- function extractLineZ(grid, ix, iy, buf) {
149
- const bx = ix >> 2, by = iy >> 2;
150
- const base = (ix & 3) + ((iy & 3) << 2);
151
- for (let bz = 0; bz < grid.nbz; bz++) {
152
- const blockIdx = bx + by * grid.nbx + bz * grid.bStride;
153
- const bt = readBlockType(grid.types, blockIdx);
154
- if (bt === BLOCK_EMPTY) {
155
- continue;
156
- }
157
- let row4;
158
- if (bt === BLOCK_SOLID) {
159
- row4 = 0xF;
160
- }
161
- else {
162
- const s = grid.masks.slot(blockIdx);
163
- row4 = ((grid.masks.lo[s] >>> base) & 1) | (((grid.masks.lo[s] >>> (base + 16)) & 1) << 1) | (((grid.masks.hi[s] >>> base) & 1) << 2) | (((grid.masks.hi[s] >>> (base + 16)) & 1) << 3);
164
- }
165
- if (row4) {
166
- const iz = bz << 2;
167
- buf[iz >>> 5] |= (row4 << (iz & 31));
168
- }
169
- }
170
- }
171
- function writeLineZ(grid, ix, iy, buf) {
172
- const bx = ix >> 2, by = iy >> 2;
173
- const base = (ix & 3) + ((iy & 3) << 2);
174
- for (let bz = 0; bz < grid.nbz; bz++) {
175
- const iz = bz << 2;
176
- const row4 = (buf[iz >>> 5] >>> (iz & 31)) & 0xF;
177
- if (!row4) {
178
- continue;
179
- }
180
- const blockIdx = bx + by * grid.nbx + bz * grid.bStride;
181
- let lo = 0, hi = 0;
182
- if (row4 & 1) {
183
- lo |= (1 << base);
184
- }
185
- if (row4 & 2) {
186
- lo |= (1 << (base + 16));
187
- }
188
- if (row4 & 4) {
189
- hi |= (1 << base);
190
- }
191
- if (row4 & 8) {
192
- hi |= (1 << (base + 16));
193
- }
194
- grid.orBlock(blockIdx, lo >>> 0, hi >>> 0);
195
- }
196
- }
197
- /**
198
- * 1D binary dilation with a flat window using a sliding count.
199
- * A destination bit is set if any source bit is set within +/- halfExtent.
200
- */
201
- function flatDilate1D(src, dst, n, halfExtent) {
202
- let count = 0;
203
- const winEnd = Math.min(halfExtent, n - 1);
204
- for (let i = 0; i <= winEnd; i++) {
205
- if ((src[i >>> 5] >>> (i & 31)) & 1) {
206
- count++;
207
- }
208
- }
209
- for (let i = 0; i < n; i++) {
210
- if (count > 0) {
211
- dst[i >>> 5] |= (1 << (i & 31));
212
- }
213
- const exitI = i - halfExtent;
214
- if (exitI >= 0 && ((src[exitI >>> 5] >>> (exitI & 31)) & 1)) {
215
- count--;
216
- }
217
- const enterI = i + halfExtent + 1;
218
- if (enterI < n && ((src[enterI >>> 5] >>> (enterI & 31)) & 1)) {
219
- count++;
220
- }
221
- }
222
- }
223
- /**
224
- * Dilate along X by extracting X-lines from sparse blocks, dilating each line,
225
- * then writing back into destination blocks.
226
- */
227
- function sparseDilateX(src, dst, halfExtent) {
228
- const { nx, ny, nz, nbx, nby, bStride } = src;
229
- const lineWords = (nx + 31) >>> 5;
230
- const srcBuf = new Uint32Array(lineWords);
231
- const dstBuf = new Uint32Array(lineWords);
232
- const activePairs = getActiveYZPairs(src);
233
- for (const key of activePairs) {
234
- const by = key % nby;
235
- const bz = (key / nby) | 0;
236
- const lineBase = by * nbx + bz * bStride;
237
- let allSolid = true;
238
- for (let bx = 0; bx < nbx; bx++) {
239
- if (readBlockType(src.types, lineBase + bx) !== BLOCK_SOLID) {
240
- allSolid = false;
241
- break;
242
- }
243
- }
244
- if (allSolid) {
245
- for (let bx = 0; bx < nbx; bx++) {
246
- dst.orBlock(lineBase + bx, SOLID_LO, SOLID_HI);
247
- }
248
- continue;
249
- }
250
- for (let ly = 0; ly < 4; ly++) {
251
- const iy = (by << 2) + ly;
252
- if (iy >= ny) {
253
- continue;
254
- }
255
- for (let lz = 0; lz < 4; lz++) {
256
- const iz = (bz << 2) + lz;
257
- if (iz >= nz) {
258
- continue;
259
- }
260
- srcBuf.fill(0);
261
- dstBuf.fill(0);
262
- extractLineX(src, iy, iz, srcBuf);
263
- flatDilate1D(srcBuf, dstBuf, nx, halfExtent);
264
- writeLineX(dst, iy, iz, dstBuf);
265
- }
266
- }
267
- }
268
- }
269
- /**
270
- * Dilate along Y by extracting Y-lines from sparse blocks.
271
- */
272
- function sparseDilateY(src, dst, halfExtent) {
273
- const { nx, ny, nz, nbx, nby, bStride } = src;
274
- const lineWords = (ny + 31) >>> 5;
275
- const srcBuf = new Uint32Array(lineWords);
276
- const dstBuf = new Uint32Array(lineWords);
277
- const activePairs = getActiveXZPairs(src);
278
- for (const key of activePairs) {
279
- const bx = key % nbx;
280
- const bz = (key / nbx) | 0;
281
- const lineStart = bx + bz * bStride;
282
- let allSolid = true;
283
- for (let by = 0; by < nby; by++) {
284
- if (readBlockType(src.types, lineStart + by * nbx) !== BLOCK_SOLID) {
285
- allSolid = false;
286
- break;
287
- }
288
- }
289
- if (allSolid) {
290
- for (let by = 0; by < nby; by++) {
291
- dst.orBlock(lineStart + by * nbx, SOLID_LO, SOLID_HI);
292
- }
293
- continue;
294
- }
295
- for (let lx = 0; lx < 4; lx++) {
296
- const ix = (bx << 2) + lx;
297
- if (ix >= nx) {
298
- continue;
299
- }
300
- for (let lz = 0; lz < 4; lz++) {
301
- const iz = (bz << 2) + lz;
302
- if (iz >= nz) {
303
- continue;
304
- }
305
- srcBuf.fill(0);
306
- dstBuf.fill(0);
307
- extractLineY(src, ix, iz, srcBuf);
308
- flatDilate1D(srcBuf, dstBuf, ny, halfExtent);
309
- writeLineY(dst, ix, iz, dstBuf);
310
- }
311
- }
312
- }
313
- }
314
- /**
315
- * Dilate along Z by extracting Z-lines from sparse blocks.
316
- */
317
- function sparseDilateZ(src, dst, halfExtent) {
318
- const { nx, ny, nz, nbx, nbz, bStride } = src;
319
- const lineWords = (nz + 31) >>> 5;
320
- const srcBuf = new Uint32Array(lineWords);
321
- const dstBuf = new Uint32Array(lineWords);
322
- const activePairs = getActiveXYPairs(src);
323
- for (const key of activePairs) {
324
- const bx = key % nbx;
325
- const by = (key / nbx) | 0;
326
- const lineStart = bx + by * nbx;
327
- let allSolid = true;
328
- for (let bz = 0; bz < nbz; bz++) {
329
- if (readBlockType(src.types, lineStart + bz * bStride) !== BLOCK_SOLID) {
330
- allSolid = false;
331
- break;
332
- }
333
- }
334
- if (allSolid) {
335
- for (let bz = 0; bz < nbz; bz++) {
336
- dst.orBlock(lineStart + bz * bStride, SOLID_LO, SOLID_HI);
337
- }
338
- continue;
339
- }
340
- for (let lx = 0; lx < 4; lx++) {
341
- const ix = (bx << 2) + lx;
342
- if (ix >= nx) {
343
- continue;
344
- }
345
- for (let ly = 0; ly < 4; ly++) {
346
- const iy = (by << 2) + ly;
347
- if (iy >= ny) {
348
- continue;
349
- }
350
- srcBuf.fill(0);
351
- dstBuf.fill(0);
352
- extractLineZ(src, ix, iy, srcBuf);
353
- flatDilate1D(srcBuf, dstBuf, nz, halfExtent);
354
- writeLineZ(dst, ix, iy, dstBuf);
355
- }
356
- }
357
- }
358
- }
359
- /**
360
- * Separable 3D dilation: X pass, then Z pass, then Y pass.
361
- * X/Z share radius while Y can use a different half extent.
362
- */
363
- function sparseDilate3(src, halfExtentXZ, halfExtentY) {
364
- const a = new SparseVoxelGrid(src.nx, src.ny, src.nz);
365
- sparseDilateX(src, a, halfExtentXZ);
366
- const b = new SparseVoxelGrid(src.nx, src.ny, src.nz);
367
- sparseDilateZ(a, b, halfExtentXZ);
368
- a.clear();
369
- sparseDilateY(b, a, halfExtentY);
370
- b.clear();
371
- return a;
372
- }
373
- const dilate3 = async (src, halfExtentXZ, halfExtentY, backend) => (backend === 'gpu'
374
- ? gpuDilate3(src, halfExtentXZ, halfExtentY)
375
- : sparseDilate3(src, halfExtentXZ, halfExtentY));
376
- /**
377
- * Compute reachable empty voxels as visited \ blocked.
378
- * This keeps only flood-filled cells that are not blocked after dilation.
379
- */
380
- function computeEmptyGrid(visited, blocked) {
381
- const empty = new SparseVoxelGrid(visited.nx, visited.ny, visited.nz);
382
- forEachNonEmptyBlock(visited, (blockIdx) => {
383
- const vbt = readBlockType(visited.types, blockIdx);
384
- let vLo, vHi;
385
- if (vbt === BLOCK_SOLID) {
386
- vLo = SOLID_LO;
387
- vHi = SOLID_HI;
388
- }
389
- else {
390
- const vs = visited.masks.slot(blockIdx);
391
- vLo = visited.masks.lo[vs];
392
- vHi = visited.masks.hi[vs];
393
- }
394
- const bbt = readBlockType(blocked.types, blockIdx);
395
- let lo, hi;
396
- if (bbt === BLOCK_EMPTY) {
397
- lo = vLo;
398
- hi = vHi;
399
- }
400
- else if (bbt === BLOCK_SOLID) {
401
- lo = 0;
402
- hi = 0;
403
- }
404
- else {
405
- const bs = blocked.masks.slot(blockIdx);
406
- lo = (vLo & ~blocked.masks.lo[bs]) >>> 0;
407
- hi = (vHi & ~blocked.masks.hi[bs]) >>> 0;
408
- }
409
- if (lo || hi) {
410
- empty.orBlock(blockIdx, lo, hi);
411
- }
412
- });
413
- return empty;
414
- }
415
- /**
416
- * Sparse OR between two voxel grids (block masks are OR-combined).
417
- */
418
- function sparseOrGrids(a, b, consumeA = false) {
419
- const result = consumeA ? a : a.clone();
420
- forEachNonEmptyBlock(b, (blockIdx) => {
421
- const bt = readBlockType(b.types, blockIdx);
422
- if (bt === BLOCK_SOLID) {
423
- result.orBlock(blockIdx, SOLID_LO, SOLID_HI);
424
- }
425
- else {
426
- const s = b.masks.slot(blockIdx);
427
- result.orBlock(blockIdx, b.masks.lo[s], b.masks.hi[s]);
428
- }
429
- });
430
- return result;
431
- }
432
- /**
433
- * Flood fill on sparse voxel grids using two coupled queues:
434
- * - block queue for fully empty blocks
435
- * - voxel queue for mixed blocks
436
- * This mirrors the reference two-level BFS for performance on sparse data.
437
- */
438
- function twoLevelBFS(blocked, blockSeeds, voxelSeeds, nx, ny, nz) {
439
- const visited = new SparseVoxelGrid(nx, ny, nz);
440
- const nbx = nx >> 2;
441
- const nby = ny >> 2;
442
- const nbz = nz >> 2;
443
- const bStride = nbx * nby;
444
- const bMasks = blocked.masks;
445
- const vMasks = visited.masks;
446
- let bqCap = 1 << 14;
447
- let bqBuf = new Uint32Array(bqCap);
448
- let bqMask = bqCap - 1, bqHead = 0, bqTail = 0, bqSize = 0;
449
- let vqCap = 1 << 14;
450
- let vqIx = new Uint32Array(vqCap);
451
- let vqIy = new Uint32Array(vqCap);
452
- let vqIz = new Uint32Array(vqCap);
453
- let vqMask = vqCap - 1, vqHead = 0, vqTail = 0, vqSize = 0;
454
- const growBlockQueue = () => {
455
- const newCap = bqCap << 1;
456
- const nb = new Uint32Array(newCap);
457
- for (let i = 0; i < bqSize; i++) {
458
- nb[i] = bqBuf[(bqHead + i) & bqMask];
459
- }
460
- bqBuf = nb;
461
- bqCap = newCap;
462
- bqMask = newCap - 1;
463
- bqHead = 0;
464
- bqTail = bqSize;
465
- };
466
- const growVoxelQueue = () => {
467
- const newCap = vqCap << 1;
468
- const nix = new Uint32Array(newCap);
469
- const niy = new Uint32Array(newCap);
470
- const niz = new Uint32Array(newCap);
471
- for (let i = 0; i < vqSize; i++) {
472
- const j = (vqHead + i) & vqMask;
473
- nix[i] = vqIx[j];
474
- niy[i] = vqIy[j];
475
- niz[i] = vqIz[j];
476
- }
477
- vqIx = nix;
478
- vqIy = niy;
479
- vqIz = niz;
480
- vqCap = newCap;
481
- vqMask = newCap - 1;
482
- vqHead = 0;
483
- vqTail = vqSize;
484
- };
485
- const enqueueVoxel = (ix, iy, iz) => {
486
- if (vqSize >= vqCap) {
487
- growVoxelQueue();
488
- }
489
- vqIx[vqTail] = ix;
490
- vqIy[vqTail] = iy;
491
- vqIz[vqTail] = iz;
492
- vqTail = (vqTail + 1) & vqMask;
493
- vqSize++;
494
- };
495
- const tryFillBlock = (blockIdx) => {
496
- if (readBlockType(blocked.types, blockIdx) !== BLOCK_EMPTY) {
497
- return false;
498
- }
499
- if (readBlockType(visited.types, blockIdx) !== BLOCK_EMPTY) {
500
- return false;
501
- }
502
- writeBlockType(visited.types, blockIdx, BLOCK_SOLID);
503
- if (bqSize >= bqCap) {
504
- growBlockQueue();
505
- }
506
- bqBuf[bqTail] = blockIdx;
507
- bqTail = (bqTail + 1) & bqMask;
508
- bqSize++;
509
- return true;
510
- };
511
- const enqueueFaceVoxels = (nBlockIdx, face, nBx, nBy, nBz) => {
512
- const vbt = readBlockType(visited.types, nBlockIdx);
513
- if (vbt === BLOCK_SOLID) {
514
- return;
515
- }
516
- const bs = bMasks.slot(nBlockIdx);
517
- let vLo = 0, vHi = 0, vs = -1;
518
- if (vbt === BLOCK_MIXED) {
519
- vs = vMasks.slot(nBlockIdx);
520
- vLo = vMasks.lo[vs];
521
- vHi = vMasks.hi[vs];
522
- }
523
- const freeLo = (FACE_MASKS_LO[face] & ~bMasks.lo[bs] & ~vLo) >>> 0;
524
- const freeHi = (FACE_MASKS_HI[face] & ~bMasks.hi[bs] & ~vHi) >>> 0;
525
- if (freeLo === 0 && freeHi === 0) {
526
- return;
527
- }
528
- if (vbt === BLOCK_EMPTY) {
529
- writeBlockType(visited.types, nBlockIdx, BLOCK_MIXED);
530
- vMasks.set(nBlockIdx, freeLo, freeHi);
531
- }
532
- else {
533
- vMasks.lo[vs] = (vMasks.lo[vs] | freeLo) >>> 0;
534
- vMasks.hi[vs] = (vMasks.hi[vs] | freeHi) >>> 0;
535
- if (vMasks.lo[vs] === SOLID_LO && vMasks.hi[vs] === SOLID_HI) {
536
- vMasks.removeAt(vs);
537
- writeBlockType(visited.types, nBlockIdx, BLOCK_SOLID);
538
- }
539
- }
540
- const baseIx = nBx << 2, baseIy = nBy << 2, baseIz = nBz << 2;
541
- let bits = freeLo;
542
- while (bits) {
543
- const bp = 31 - Math.clz32(bits & -bits);
544
- enqueueVoxel(baseIx + (bp & 3), baseIy + ((bp >> 2) & 3), baseIz + (bp >> 4));
545
- bits &= bits - 1;
546
- }
547
- bits = freeHi;
548
- while (bits) {
549
- const bp = 31 - Math.clz32(bits & -bits);
550
- const bi = bp + 32;
551
- enqueueVoxel(baseIx + (bi & 3), baseIy + ((bi >> 2) & 3), baseIz + (bi >> 4));
552
- bits &= bits - 1;
553
- }
554
- };
555
- const processBlock = (blockIdx) => {
556
- const bx = blockIdx % nbx;
557
- const byBz = (blockIdx / nbx) | 0;
558
- const by = byBz % nby;
559
- const bz = (byBz / nby) | 0;
560
- if (bx > 0) {
561
- const ni = blockIdx - 1;
562
- const nbt = readBlockType(blocked.types, ni);
563
- if (nbt === BLOCK_EMPTY) {
564
- tryFillBlock(ni);
565
- }
566
- else if (nbt === BLOCK_MIXED) {
567
- enqueueFaceVoxels(ni, 1, bx - 1, by, bz);
568
- }
569
- }
570
- if (bx < nbx - 1) {
571
- const ni = blockIdx + 1;
572
- const nbt = readBlockType(blocked.types, ni);
573
- if (nbt === BLOCK_EMPTY) {
574
- tryFillBlock(ni);
575
- }
576
- else if (nbt === BLOCK_MIXED) {
577
- enqueueFaceVoxels(ni, 0, bx + 1, by, bz);
578
- }
579
- }
580
- if (by > 0) {
581
- const ni = blockIdx - nbx;
582
- const nbt = readBlockType(blocked.types, ni);
583
- if (nbt === BLOCK_EMPTY) {
584
- tryFillBlock(ni);
585
- }
586
- else if (nbt === BLOCK_MIXED) {
587
- enqueueFaceVoxels(ni, 3, bx, by - 1, bz);
588
- }
589
- }
590
- if (by < nby - 1) {
591
- const ni = blockIdx + nbx;
592
- const nbt = readBlockType(blocked.types, ni);
593
- if (nbt === BLOCK_EMPTY) {
594
- tryFillBlock(ni);
595
- }
596
- else if (nbt === BLOCK_MIXED) {
597
- enqueueFaceVoxels(ni, 2, bx, by + 1, bz);
598
- }
599
- }
600
- if (bz > 0) {
601
- const ni = blockIdx - bStride;
602
- const nbt = readBlockType(blocked.types, ni);
603
- if (nbt === BLOCK_EMPTY) {
604
- tryFillBlock(ni);
605
- }
606
- else if (nbt === BLOCK_MIXED) {
607
- enqueueFaceVoxels(ni, 5, bx, by, bz - 1);
608
- }
609
- }
610
- if (bz < nbz - 1) {
611
- const ni = blockIdx + bStride;
612
- const nbt = readBlockType(blocked.types, ni);
613
- if (nbt === BLOCK_EMPTY) {
614
- tryFillBlock(ni);
615
- }
616
- else if (nbt === BLOCK_MIXED) {
617
- enqueueFaceVoxels(ni, 4, bx, by, bz + 1);
618
- }
619
- }
620
- };
621
- const tryEnqueueVoxel = (ix, iy, iz) => {
622
- const blockIdx = (ix >> 2) + (iy >> 2) * nbx + (iz >> 2) * bStride;
623
- const bbt = readBlockType(blocked.types, blockIdx);
624
- if (bbt === BLOCK_SOLID) {
625
- return;
626
- }
627
- if (bbt === BLOCK_EMPTY) {
628
- tryFillBlock(blockIdx);
629
- return;
630
- }
631
- const bs = bMasks.slot(blockIdx);
632
- const bitIdx = (ix & 3) + ((iy & 3) << 2) + ((iz & 3) << 4);
633
- if (bitIdx < 32 ? (bMasks.lo[bs] >>> bitIdx) & 1 : (bMasks.hi[bs] >>> (bitIdx - 32)) & 1) {
634
- return;
635
- }
636
- const vbt = readBlockType(visited.types, blockIdx);
637
- if (vbt === BLOCK_SOLID) {
638
- return;
639
- }
640
- if (vbt === BLOCK_MIXED) {
641
- const vs = vMasks.slot(blockIdx);
642
- if (bitIdx < 32 ? (vMasks.lo[vs] >>> bitIdx) & 1 : (vMasks.hi[vs] >>> (bitIdx - 32)) & 1) {
643
- return;
644
- }
645
- if (bitIdx < 32) {
646
- vMasks.lo[vs] = (vMasks.lo[vs] | (1 << bitIdx)) >>> 0;
647
- }
648
- else {
649
- vMasks.hi[vs] = (vMasks.hi[vs] | (1 << (bitIdx - 32))) >>> 0;
650
- }
651
- if (vMasks.lo[vs] === SOLID_LO && vMasks.hi[vs] === SOLID_HI) {
652
- vMasks.removeAt(vs);
653
- writeBlockType(visited.types, blockIdx, BLOCK_SOLID);
654
- }
655
- }
656
- else {
657
- writeBlockType(visited.types, blockIdx, BLOCK_MIXED);
658
- vMasks.set(blockIdx, bitIdx < 32 ? (1 << bitIdx) >>> 0 : 0, bitIdx >= 32 ? (1 << (bitIdx - 32)) >>> 0 : 0);
659
- }
660
- enqueueVoxel(ix, iy, iz);
661
- };
662
- for (let i = 0; i < blockSeeds.length; i++) {
663
- tryFillBlock(blockSeeds[i]);
664
- }
665
- for (let i = 0; i < voxelSeeds.length; i++) {
666
- const s = voxelSeeds[i];
667
- tryEnqueueVoxel(s.ix, s.iy, s.iz);
668
- }
669
- while (bqSize > 0 || vqSize > 0) {
670
- while (bqSize > 0) {
671
- const blockIdx = bqBuf[bqHead];
672
- bqHead = (bqHead + 1) & bqMask;
673
- bqSize--;
674
- processBlock(blockIdx);
675
- }
676
- if (vqSize > 0) {
677
- const ix = vqIx[vqHead], iy = vqIy[vqHead], iz = vqIz[vqHead];
678
- vqHead = (vqHead + 1) & vqMask;
679
- vqSize--;
680
- if (ix > 0) {
681
- tryEnqueueVoxel(ix - 1, iy, iz);
682
- }
683
- if (ix < nx - 1) {
684
- tryEnqueueVoxel(ix + 1, iy, iz);
685
- }
686
- if (iy > 0) {
687
- tryEnqueueVoxel(ix, iy - 1, iz);
688
- }
689
- if (iy < ny - 1) {
690
- tryEnqueueVoxel(ix, iy + 1, iz);
691
- }
692
- if (iz > 0) {
693
- tryEnqueueVoxel(ix, iy, iz - 1);
694
- }
695
- if (iz < nz - 1) {
696
- tryEnqueueVoxel(ix, iy, iz + 1);
697
- }
698
- }
699
- }
700
- return visited;
701
- }
702
- function cloneBounds(b) {
703
- return { min: { ...b.min }, max: { ...b.max } };
704
- }
705
- /**
706
- * Fill exterior-reachable space from boundary seeds and merge it back into
707
- * occupancy after dilation. Returns cropped bounds around navigable volume.
708
- */
709
- export async function fillExterior(gridOriginal, gridBounds, voxelResolution, dilation, seed, backend = 'cpu') {
710
- if (!Number.isFinite(voxelResolution) || voxelResolution <= 0) {
711
- throw new Error(`fillExterior: voxelResolution must be finite and > 0, got ${voxelResolution}`);
712
- }
713
- if (!Number.isFinite(dilation) || dilation <= 0) {
714
- throw new Error(`fillExterior: dilation must be finite and > 0, got ${dilation}`);
715
- }
716
- const nx = Math.round((gridBounds.max.x - gridBounds.min.x) / voxelResolution);
717
- const ny = Math.round((gridBounds.max.y - gridBounds.min.y) / voxelResolution);
718
- const nz = Math.round((gridBounds.max.z - gridBounds.min.z) / voxelResolution);
719
- if (nx % 4 !== 0 || ny % 4 !== 0 || nz % 4 !== 0) {
720
- throw new Error(`fillExterior: grid dimensions must be multiples of 4, got ${nx}x${ny}x${nz}`);
721
- }
722
- const halfExtent = Math.ceil(dilation / voxelResolution);
723
- const nbx = nx >> 2, nby = ny >> 2, nbz = nz >> 2;
724
- const dilated = await dilate3(gridOriginal, halfExtent, halfExtent, backend);
725
- const bStride = nbx * nby;
726
- const blockSeeds = [];
727
- const faceVoxelSeeds = [];
728
- const seedBoundaryBlock = (blockIdx, bx, by, bz, face) => {
729
- const bt = readBlockType(dilated.types, blockIdx);
730
- if (bt === BLOCK_SOLID) {
731
- return;
732
- }
733
- if (bt === BLOCK_EMPTY) {
734
- blockSeeds.push(blockIdx);
735
- return;
736
- }
737
- const ms = dilated.masks.slot(blockIdx);
738
- let freeLo = (FACE_MASKS_LO[face] & ~dilated.masks.lo[ms]) >>> 0;
739
- let freeHi = (FACE_MASKS_HI[face] & ~dilated.masks.hi[ms]) >>> 0;
740
- if (freeLo === 0 && freeHi === 0) {
741
- return;
742
- }
743
- const baseIx = bx << 2, baseIy = by << 2, baseIz = bz << 2;
744
- while (freeLo) {
745
- const bp = 31 - Math.clz32(freeLo & -freeLo);
746
- faceVoxelSeeds.push({ ix: baseIx + (bp & 3), iy: baseIy + ((bp >> 2) & 3), iz: baseIz + (bp >> 4) });
747
- freeLo &= freeLo - 1;
748
- }
749
- while (freeHi) {
750
- const bp = 31 - Math.clz32(freeHi & -freeHi);
751
- const bi = bp + 32;
752
- faceVoxelSeeds.push({ ix: baseIx + (bi & 3), iy: baseIy + ((bi >> 2) & 3), iz: baseIz + (bi >> 4) });
753
- freeHi &= freeHi - 1;
754
- }
755
- };
756
- for (let bz = 0; bz < nbz; bz++) {
757
- for (let by = 0; by < nby; by++) {
758
- seedBoundaryBlock(by * nbx + bz * bStride, 0, by, bz, 0);
759
- }
760
- }
761
- for (let bz = 0; bz < nbz; bz++) {
762
- for (let by = 0; by < nby; by++) {
763
- seedBoundaryBlock((nbx - 1) + by * nbx + bz * bStride, nbx - 1, by, bz, 1);
764
- }
765
- }
766
- for (let bz = 0; bz < nbz; bz++) {
767
- for (let bx = 0; bx < nbx; bx++) {
768
- seedBoundaryBlock(bx + bz * bStride, bx, 0, bz, 2);
769
- }
770
- }
771
- for (let bz = 0; bz < nbz; bz++) {
772
- for (let bx = 0; bx < nbx; bx++) {
773
- seedBoundaryBlock(bx + (nby - 1) * nbx + bz * bStride, bx, nby - 1, bz, 3);
774
- }
775
- }
776
- for (let by = 0; by < nby; by++) {
777
- for (let bx = 0; bx < nbx; bx++) {
778
- seedBoundaryBlock(bx + by * nbx, bx, by, 0, 4);
779
- }
780
- }
781
- for (let by = 0; by < nby; by++) {
782
- for (let bx = 0; bx < nbx; bx++) {
783
- seedBoundaryBlock(bx + by * nbx + (nbz - 1) * bStride, bx, by, nbz - 1, 5);
784
- }
785
- }
786
- const visited = twoLevelBFS(dilated, blockSeeds, faceVoxelSeeds, nx, ny, nz);
787
- const seedIx = Math.floor((seed.x - gridBounds.min.x) / voxelResolution);
788
- const seedIy = Math.floor((seed.y - gridBounds.min.y) / voxelResolution);
789
- const seedIz = Math.floor((seed.z - gridBounds.min.z) / voxelResolution);
790
- if (seedIx >= 0 && seedIx < nx && seedIy >= 0 && seedIy < ny && seedIz >= 0 && seedIz < nz) {
791
- if (visited.getVoxel(seedIx, seedIy, seedIz)) {
792
- logger.info('fillExteriorMap: seed reachable from outside, skipping');
793
- return { grid: gridOriginal, gridBounds };
794
- }
795
- }
796
- else {
797
- logger.info('fillExteriorMap: seed outside grid bounds, skipping exterior fill');
798
- return { grid: gridOriginal, gridBounds };
799
- }
800
- const dilatedVisited = await dilate3(visited, halfExtent, halfExtent, backend);
801
- const combined = sparseOrGrids(gridOriginal, dilatedVisited);
802
- let minIx = nx, minIy = ny, minIz = nz;
803
- let maxIx = 0, maxIy = 0, maxIz = 0;
804
- for (let bz = 0; bz < nbz; bz++) {
805
- for (let by = 0; by < nby; by++) {
806
- for (let bx = 0; bx < nbx; bx++) {
807
- const blockIdx = bx + by * nbx + bz * combined.bStride;
808
- const bt = readBlockType(combined.types, blockIdx);
809
- if (bt === BLOCK_SOLID) {
810
- continue;
811
- }
812
- if (bt === BLOCK_MIXED) {
813
- const cs = combined.masks.slot(blockIdx);
814
- if (combined.masks.lo[cs] === SOLID_LO && combined.masks.hi[cs] === SOLID_HI) {
815
- continue;
816
- }
817
- }
818
- const baseX = bx << 2, baseY = by << 2, baseZ = bz << 2;
819
- if (baseX < minIx) {
820
- minIx = baseX;
821
- }
822
- if (baseX + 3 > maxIx) {
823
- maxIx = baseX + 3;
824
- }
825
- if (baseY < minIy) {
826
- minIy = baseY;
827
- }
828
- if (baseY + 3 > maxIy) {
829
- maxIy = baseY + 3;
830
- }
831
- if (baseZ < minIz) {
832
- minIz = baseZ;
833
- }
834
- if (baseZ + 3 > maxIz) {
835
- maxIz = baseZ + 3;
836
- }
837
- }
838
- }
839
- }
840
- if (minIx > maxIx) {
841
- logger.warn('fillExteriorMap: no navigable cells remain, returning empty result');
842
- return { grid: new SparseVoxelGrid(4, 4, 4), gridBounds: { min: { ...gridBounds.min }, max: { ...gridBounds.min } } };
843
- }
844
- const MARGIN = 1;
845
- const cropMinBx = Math.max(0, (minIx >> 2) - MARGIN);
846
- const cropMinBy = Math.max(0, (minIy >> 2) - MARGIN);
847
- const cropMinBz = Math.max(0, (minIz >> 2) - MARGIN);
848
- const cropMaxBx = Math.min(nbx, (maxIx >> 2) + 1 + MARGIN);
849
- const cropMaxBy = Math.min(nby, (maxIy >> 2) + 1 + MARGIN);
850
- const cropMaxBz = Math.min(nbz, (maxIz >> 2) + 1 + MARGIN);
851
- const blockSize = 4 * voxelResolution;
852
- const croppedMin = {
853
- x: gridBounds.min.x + cropMinBx * blockSize,
854
- y: gridBounds.min.y + cropMinBy * blockSize,
855
- z: gridBounds.min.z + cropMinBz * blockSize
856
- };
857
- const croppedBounds = {
858
- min: croppedMin,
859
- max: {
860
- x: croppedMin.x + (cropMaxBx - cropMinBx) * blockSize,
861
- y: croppedMin.y + (cropMaxBy - cropMinBy) * blockSize,
862
- z: croppedMin.z + (cropMaxBz - cropMinBz) * blockSize
863
- }
864
- };
865
- return { grid: combined.cropTo(cropMinBx, cropMinBy, cropMinBz, cropMaxBx, cropMaxBy, cropMaxBz), gridBounds: croppedBounds };
866
- }
867
- /**
868
- * Carve navigable space for a capsule by:
869
- * 1) dilating blocked voxels by capsule dimensions
870
- * 2) flood filling reachable empty space from the seed
871
- * 3) dilating and inverting to final occupancy representation.
872
- */
873
- export async function carve(grid, gridBounds, voxelResolution, capsuleHeight, capsuleRadius, seed, backend = 'cpu') {
874
- if (!Number.isFinite(voxelResolution) || voxelResolution <= 0) {
875
- throw new Error(`carve: voxelResolution must be finite and > 0, got ${voxelResolution}`);
876
- }
877
- if (!Number.isFinite(capsuleHeight) || capsuleHeight <= 0) {
878
- throw new Error(`carve: capsuleHeight must be finite and > 0, got ${capsuleHeight}`);
879
- }
880
- if (!Number.isFinite(capsuleRadius) || capsuleRadius < 0) {
881
- throw new Error(`carve: capsuleRadius must be finite and >= 0, got ${capsuleRadius}`);
882
- }
883
- const nx = Math.round((gridBounds.max.x - gridBounds.min.x) / voxelResolution);
884
- const ny = Math.round((gridBounds.max.y - gridBounds.min.y) / voxelResolution);
885
- const nz = Math.round((gridBounds.max.z - gridBounds.min.z) / voxelResolution);
886
- if (nx % 4 !== 0 || ny % 4 !== 0 || nz % 4 !== 0) {
887
- throw new Error(`carve: grid dimensions must be multiples of 4, got ${nx}x${ny}x${nz}`);
888
- }
889
- const kernelR = Math.ceil(capsuleRadius / voxelResolution);
890
- const yHalfExtent = Math.ceil(capsuleHeight / (2 * voxelResolution));
891
- const nbx = nx >> 2;
892
- const nby = ny >> 2;
893
- const nbz = nz >> 2;
894
- const blocked = await dilate3(grid, kernelR, yHalfExtent, backend);
895
- const seedIx = Math.floor((seed.x - gridBounds.min.x) / voxelResolution);
896
- const seedIy = Math.floor((seed.y - gridBounds.min.y) / voxelResolution);
897
- const seedIz = Math.floor((seed.z - gridBounds.min.z) / voxelResolution);
898
- if (seedIx < 0 || seedIx >= nx || seedIy < 0 || seedIy >= ny || seedIz < 0 || seedIz >= nz) {
899
- logger.warn(`carve: seed (${seed.x}, ${seed.y}, ${seed.z}) outside grid, skipping`);
900
- return { grid, gridBounds: cloneBounds(gridBounds) };
901
- }
902
- let useSeedIx = seedIx, useSeedIy = seedIy, useSeedIz = seedIz;
903
- if (blocked.getVoxel(seedIx, seedIy, seedIz)) {
904
- const maxRadius = Math.max(kernelR, yHalfExtent) * 2;
905
- const found = SparseVoxelGrid.findNearestFreeCell(blocked, seedIx, seedIy, seedIz, maxRadius);
906
- if (!found) {
907
- logger.warn(`carve: seed (${seed.x}, ${seed.y}, ${seed.z}) blocked after dilation, no free cell within ${maxRadius} voxels, skipping`);
908
- return { grid, gridBounds: cloneBounds(gridBounds) };
909
- }
910
- useSeedIx = found.ix;
911
- useSeedIy = found.iy;
912
- useSeedIz = found.iz;
913
- }
914
- const seedBlockIdx = (useSeedIx >> 2) + (useSeedIy >> 2) * nbx + (useSeedIz >> 2) * (nbx * nby);
915
- const seedBt = readBlockType(blocked.types, seedBlockIdx);
916
- const blockSeeds = seedBt === BLOCK_EMPTY ? [seedBlockIdx] : [];
917
- const voxelSeeds = seedBt === BLOCK_EMPTY ? [] : [{ ix: useSeedIx, iy: useSeedIy, iz: useSeedIz }];
918
- const visited = twoLevelBFS(blocked, blockSeeds, voxelSeeds, nx, ny, nz);
919
- // useless?
920
- const emptyGrid = computeEmptyGrid(visited, blocked);
921
- const navRegion = await dilate3(emptyGrid, kernelR, yHalfExtent, backend);
922
- const navBounds = navRegion.getOccupiedBlockBounds();
923
- if (!navBounds) {
924
- logger.warn('carve: no navigable cells remain, returning empty result');
925
- return { grid: new SparseVoxelGrid(4, 4, 4), gridBounds: { min: { ...gridBounds.min }, max: { ...gridBounds.min } } };
926
- }
927
- const MARGIN = 1;
928
- const cropMinBx = Math.max(0, navBounds.minBx - MARGIN);
929
- const cropMinBy = Math.max(0, navBounds.minBy - MARGIN);
930
- const cropMinBz = Math.max(0, navBounds.minBz - MARGIN);
931
- const cropMaxBx = Math.min(nbx, navBounds.maxBx + 1 + MARGIN);
932
- const cropMaxBy = Math.min(nby, navBounds.maxBy + 1 + MARGIN);
933
- const cropMaxBz = Math.min(nbz, navBounds.maxBz + 1 + MARGIN);
934
- const blockSize = 4 * voxelResolution;
935
- const croppedMin = {
936
- x: gridBounds.min.x + cropMinBx * blockSize,
937
- y: gridBounds.min.y + cropMinBy * blockSize,
938
- z: gridBounds.min.z + cropMinBz * blockSize
939
- };
940
- const croppedBounds = {
941
- min: croppedMin,
942
- max: {
943
- x: croppedMin.x + (cropMaxBx - cropMinBx) * blockSize,
944
- y: croppedMin.y + (cropMaxBy - cropMinBy) * blockSize,
945
- z: croppedMin.z + (cropMaxBz - cropMinBz) * blockSize
946
- }
947
- };
948
- return { grid: navRegion.cropToInverted(cropMinBx, cropMinBy, cropMinBz, cropMaxBx, cropMaxBy, cropMaxBz), gridBounds: croppedBounds };
949
- }
950
- /**
951
- * Floor-fill via XZ dilate -> per-column upward walk -> XZ dilate -> OR.
952
- * This mirrors upstream's block/bitmask walk instead of per-voxel getVoxel checks.
953
- */
954
- export async function fillFloor(gridOriginal, gridBounds, voxelResolution, dilation = 0, backend = 'cpu') {
955
- const nx = Math.round((gridBounds.max.x - gridBounds.min.x) / voxelResolution);
956
- const ny = Math.round((gridBounds.max.y - gridBounds.min.y) / voxelResolution);
957
- const nz = Math.round((gridBounds.max.z - gridBounds.min.z) / voxelResolution);
958
- if (nx % 4 !== 0 || ny % 4 !== 0 || nz % 4 !== 0) {
959
- return { grid: gridOriginal, gridBounds };
960
- }
961
- const halfExtent = Math.max(0, Math.ceil(dilation / voxelResolution));
962
- const dilatedSolid = halfExtent > 0 ? await dilate3(gridOriginal, halfExtent, 0, backend) : gridOriginal;
963
- const { nbx, nby, nbz, bStride } = gridOriginal;
964
- const foundEmpty = new SparseVoxelGrid(nx, ny, nz);
965
- const dilatedTypes = dilatedSolid.types;
966
- for (let bz = 0; bz < nbz; bz++) {
967
- for (let bx = 0; bx < nbx; bx++) {
968
- let walking = 0xFFFF;
969
- for (let by = 0; by < nby && walking; by++) {
970
- const blockIdx = bx + by * nbx + bz * bStride;
971
- const bt = readBlockType(dilatedTypes, blockIdx);
972
- if (bt === BLOCK_SOLID) {
973
- break;
974
- }
975
- if (bt === BLOCK_EMPTY) {
976
- if (walking === 0xFFFF) {
977
- foundEmpty.orBlock(blockIdx, SOLID_LO, SOLID_HI);
978
- }
979
- else {
980
- let lo = 0;
981
- let hi = 0;
982
- for (let lz = 0; lz < 4; lz++) {
983
- for (let lx = 0; lx < 4; lx++) {
984
- if (!(walking & (1 << (lz * 4 + lx)))) {
985
- continue;
986
- }
987
- for (let ly = 0; ly < 4; ly++) {
988
- const bitIdx = lx + (ly << 2) + (lz << 4);
989
- if (bitIdx < 32) {
990
- lo |= 1 << bitIdx;
991
- }
992
- else {
993
- hi |= 1 << (bitIdx - 32);
994
- }
995
- }
996
- }
997
- }
998
- foundEmpty.orBlock(blockIdx, lo >>> 0, hi >>> 0);
999
- }
1000
- continue;
1001
- }
1002
- const s = dilatedSolid.masks.slot(blockIdx);
1003
- const dLo = dilatedSolid.masks.lo[s];
1004
- const dHi = dilatedSolid.masks.hi[s];
1005
- let foundLo = 0;
1006
- let foundHi = 0;
1007
- for (let lz = 0; lz < 4; lz++) {
1008
- for (let lx = 0; lx < 4; lx++) {
1009
- const subCol = 1 << (lz * 4 + lx);
1010
- if (!(walking & subCol)) {
1011
- continue;
1012
- }
1013
- for (let ly = 0; ly < 4; ly++) {
1014
- const bitIdx = lx + (ly << 2) + (lz << 4);
1015
- const inHi = bitIdx >= 32;
1016
- const word = inHi ? dHi : dLo;
1017
- const bit = 1 << (inHi ? bitIdx - 32 : bitIdx);
1018
- if (word & bit) {
1019
- walking &= ~subCol;
1020
- break;
1021
- }
1022
- if (inHi) {
1023
- foundHi |= bit;
1024
- }
1025
- else {
1026
- foundLo |= bit;
1027
- }
1028
- }
1029
- }
1030
- }
1031
- if (foundLo || foundHi) {
1032
- foundEmpty.orBlock(blockIdx, foundLo >>> 0, foundHi >>> 0);
1033
- }
1034
- }
1035
- }
1036
- }
1037
- if (halfExtent > 0) {
1038
- dilatedSolid.clear();
1039
- }
1040
- const foundDilated = halfExtent > 0 ? await dilate3(foundEmpty, halfExtent, 0, backend) : foundEmpty;
1041
- const combined = sparseOrGrids(gridOriginal, foundDilated, true);
1042
- return { grid: combined, gridBounds: cloneBounds(gridBounds) };
1043
- }
1
+ import { BLOCK_EMPTY, BLOCK_SOLID, BLOCK_MIXED, SOLID_LO, SOLID_HI, SparseVoxelGrid, readBlockType, writeBlockType, } from './common.js';
2
+ import { gpuDilate3 } from './gpu-dilation.js';
3
+ import { logger } from '../Logger.js';
4
+ const FACE_MASKS_LO = [
5
+ 0x11111111 >>> 0, // -X
6
+ 0x88888888 >>> 0, // +X
7
+ 0x000f000f >>> 0, // -Y
8
+ 0xf000f000 >>> 0, // +Y
9
+ 0x0000ffff >>> 0, // -Z
10
+ 0x00000000 >>> 0, // +Z
11
+ ];
12
+ const FACE_MASKS_HI = [
13
+ 0x11111111 >>> 0,
14
+ 0x88888888 >>> 0,
15
+ 0x000f000f >>> 0,
16
+ 0xf000f000 >>> 0,
17
+ 0x00000000 >>> 0,
18
+ 0xffff0000 >>> 0,
19
+ ];
20
+ function forEachNonEmptyBlock(grid, fn) {
21
+ const totalBlocks = grid.nbx * grid.nby * grid.nbz;
22
+ for (let w = 0; w < grid.types.length; w++) {
23
+ let nonEmpty = ((grid.types[w] & 0x55555555) | ((grid.types[w] >>> 1) & 0x55555555)) >>> 0;
24
+ const baseIdx = w * 16;
25
+ while (nonEmpty) {
26
+ const bitPos = 31 - Math.clz32(nonEmpty & -nonEmpty);
27
+ const blockIdx = baseIdx + (bitPos >>> 1);
28
+ if (blockIdx >= totalBlocks) {
29
+ break;
30
+ }
31
+ fn(blockIdx);
32
+ nonEmpty &= nonEmpty - 1;
33
+ }
34
+ }
35
+ }
36
+ // Active block-pair extraction for separable dilation passes.
37
+ function getActiveYZPairs(grid) {
38
+ const pairs = new Set();
39
+ const { nbx } = grid;
40
+ forEachNonEmptyBlock(grid, blockIdx => pairs.add((blockIdx / nbx) | 0));
41
+ return pairs;
42
+ }
43
+ function getActiveXZPairs(grid) {
44
+ const pairs = new Set();
45
+ const { nbx, bStride } = grid;
46
+ forEachNonEmptyBlock(grid, blockIdx => {
47
+ const bx = blockIdx % nbx;
48
+ const bz = (blockIdx / bStride) | 0;
49
+ pairs.add(bx + bz * nbx);
50
+ });
51
+ return pairs;
52
+ }
53
+ function getActiveXYPairs(grid) {
54
+ const pairs = new Set();
55
+ const { nbx, nby } = grid;
56
+ forEachNonEmptyBlock(grid, blockIdx => {
57
+ const bx = blockIdx % nbx;
58
+ const by = ((blockIdx / nbx) | 0) % nby;
59
+ pairs.add(bx + by * nbx);
60
+ });
61
+ return pairs;
62
+ }
63
+ // Line extraction/writeback helpers between sparse block masks and bit-packed 1D buffers.
64
+ function extractLineX(grid, iy, iz, buf) {
65
+ const by = iy >> 2, bz = iz >> 2;
66
+ const bitBase = ((iz & 3) << 4) + ((iy & 3) << 2);
67
+ const inHi = bitBase >= 32;
68
+ const shift = inHi ? bitBase - 32 : bitBase;
69
+ const lineBase = by * grid.nbx + bz * grid.bStride;
70
+ for (let bx = 0; bx < grid.nbx; bx++) {
71
+ const blockIdx = lineBase + bx;
72
+ const bt = readBlockType(grid.types, blockIdx);
73
+ if (bt === BLOCK_EMPTY) {
74
+ continue;
75
+ }
76
+ let row4;
77
+ if (bt === BLOCK_SOLID) {
78
+ row4 = 0xf;
79
+ }
80
+ else {
81
+ const s = grid.masks.slot(blockIdx);
82
+ row4 = ((inHi ? grid.masks.hi[s] : grid.masks.lo[s]) >>> shift) & 0xf;
83
+ }
84
+ if (row4) {
85
+ const ix = bx << 2;
86
+ buf[ix >>> 5] |= row4 << (ix & 31);
87
+ }
88
+ }
89
+ }
90
+ function writeLineX(grid, iy, iz, buf) {
91
+ const by = iy >> 2, bz = iz >> 2;
92
+ const bitBase = ((iz & 3) << 4) + ((iy & 3) << 2);
93
+ const inHi = bitBase >= 32;
94
+ const shift = inHi ? bitBase - 32 : bitBase;
95
+ const lineBase = by * grid.nbx + bz * grid.bStride;
96
+ for (let bx = 0; bx < grid.nbx; bx++) {
97
+ const ix = bx << 2;
98
+ const row4 = (buf[ix >>> 5] >>> (ix & 31)) & 0xf;
99
+ if (!row4) {
100
+ continue;
101
+ }
102
+ const blockIdx = lineBase + bx;
103
+ grid.orBlock(blockIdx, inHi ? 0 : (row4 << shift) >>> 0, inHi ? (row4 << shift) >>> 0 : 0);
104
+ }
105
+ }
106
+ function extractLineY(grid, ix, iz, buf) {
107
+ const bx = ix >> 2, bz = iz >> 2;
108
+ const lx = ix & 3, lz = iz & 3;
109
+ const inHi = lz >= 2;
110
+ const base = lx + (lz & 1) * 16;
111
+ for (let by = 0; by < grid.nby; by++) {
112
+ const blockIdx = bx + by * grid.nbx + bz * grid.bStride;
113
+ const bt = readBlockType(grid.types, blockIdx);
114
+ if (bt === BLOCK_EMPTY) {
115
+ continue;
116
+ }
117
+ let row4;
118
+ if (bt === BLOCK_SOLID) {
119
+ row4 = 0xf;
120
+ }
121
+ else {
122
+ const s = grid.masks.slot(blockIdx);
123
+ const word = inHi ? grid.masks.hi[s] : grid.masks.lo[s];
124
+ row4 =
125
+ ((word >>> base) & 1) |
126
+ (((word >>> (base + 4)) & 1) << 1) |
127
+ (((word >>> (base + 8)) & 1) << 2) |
128
+ (((word >>> (base + 12)) & 1) << 3);
129
+ }
130
+ if (row4) {
131
+ const iy = by << 2;
132
+ buf[iy >>> 5] |= row4 << (iy & 31);
133
+ }
134
+ }
135
+ }
136
+ function writeLineY(grid, ix, iz, buf) {
137
+ const bx = ix >> 2, bz = iz >> 2;
138
+ const lx = ix & 3, lz = iz & 3;
139
+ const inHi = lz >= 2;
140
+ const base = lx + (lz & 1) * 16;
141
+ for (let by = 0; by < grid.nby; by++) {
142
+ const iy = by << 2;
143
+ const row4 = (buf[iy >>> 5] >>> (iy & 31)) & 0xf;
144
+ if (!row4) {
145
+ continue;
146
+ }
147
+ const blockIdx = bx + by * grid.nbx + bz * grid.bStride;
148
+ const bits = ((row4 & 1) << base) |
149
+ (((row4 >>> 1) & 1) << (base + 4)) |
150
+ (((row4 >>> 2) & 1) << (base + 8)) |
151
+ (((row4 >>> 3) & 1) << (base + 12));
152
+ grid.orBlock(blockIdx, inHi ? 0 : bits >>> 0, inHi ? bits >>> 0 : 0);
153
+ }
154
+ }
155
+ function extractLineZ(grid, ix, iy, buf) {
156
+ const bx = ix >> 2, by = iy >> 2;
157
+ const base = (ix & 3) + ((iy & 3) << 2);
158
+ for (let bz = 0; bz < grid.nbz; bz++) {
159
+ const blockIdx = bx + by * grid.nbx + bz * grid.bStride;
160
+ const bt = readBlockType(grid.types, blockIdx);
161
+ if (bt === BLOCK_EMPTY) {
162
+ continue;
163
+ }
164
+ let row4;
165
+ if (bt === BLOCK_SOLID) {
166
+ row4 = 0xf;
167
+ }
168
+ else {
169
+ const s = grid.masks.slot(blockIdx);
170
+ row4 =
171
+ ((grid.masks.lo[s] >>> base) & 1) |
172
+ (((grid.masks.lo[s] >>> (base + 16)) & 1) << 1) |
173
+ (((grid.masks.hi[s] >>> base) & 1) << 2) |
174
+ (((grid.masks.hi[s] >>> (base + 16)) & 1) << 3);
175
+ }
176
+ if (row4) {
177
+ const iz = bz << 2;
178
+ buf[iz >>> 5] |= row4 << (iz & 31);
179
+ }
180
+ }
181
+ }
182
+ function writeLineZ(grid, ix, iy, buf) {
183
+ const bx = ix >> 2, by = iy >> 2;
184
+ const base = (ix & 3) + ((iy & 3) << 2);
185
+ for (let bz = 0; bz < grid.nbz; bz++) {
186
+ const iz = bz << 2;
187
+ const row4 = (buf[iz >>> 5] >>> (iz & 31)) & 0xf;
188
+ if (!row4) {
189
+ continue;
190
+ }
191
+ const blockIdx = bx + by * grid.nbx + bz * grid.bStride;
192
+ let lo = 0, hi = 0;
193
+ if (row4 & 1) {
194
+ lo |= 1 << base;
195
+ }
196
+ if (row4 & 2) {
197
+ lo |= 1 << (base + 16);
198
+ }
199
+ if (row4 & 4) {
200
+ hi |= 1 << base;
201
+ }
202
+ if (row4 & 8) {
203
+ hi |= 1 << (base + 16);
204
+ }
205
+ grid.orBlock(blockIdx, lo >>> 0, hi >>> 0);
206
+ }
207
+ }
208
+ /**
209
+ * 1D binary dilation with a flat window using a sliding count.
210
+ * A destination bit is set if any source bit is set within +/- halfExtent.
211
+ */
212
+ function flatDilate1D(src, dst, n, halfExtent) {
213
+ let count = 0;
214
+ const winEnd = Math.min(halfExtent, n - 1);
215
+ for (let i = 0; i <= winEnd; i++) {
216
+ if ((src[i >>> 5] >>> (i & 31)) & 1) {
217
+ count++;
218
+ }
219
+ }
220
+ for (let i = 0; i < n; i++) {
221
+ if (count > 0) {
222
+ dst[i >>> 5] |= 1 << (i & 31);
223
+ }
224
+ const exitI = i - halfExtent;
225
+ if (exitI >= 0 && (src[exitI >>> 5] >>> (exitI & 31)) & 1) {
226
+ count--;
227
+ }
228
+ const enterI = i + halfExtent + 1;
229
+ if (enterI < n && (src[enterI >>> 5] >>> (enterI & 31)) & 1) {
230
+ count++;
231
+ }
232
+ }
233
+ }
234
+ /**
235
+ * Dilate along X by extracting X-lines from sparse blocks, dilating each line,
236
+ * then writing back into destination blocks.
237
+ */
238
+ function sparseDilateX(src, dst, halfExtent) {
239
+ const { nx, ny, nz, nbx, nby, bStride } = src;
240
+ const lineWords = (nx + 31) >>> 5;
241
+ const srcBuf = new Uint32Array(lineWords);
242
+ const dstBuf = new Uint32Array(lineWords);
243
+ const activePairs = getActiveYZPairs(src);
244
+ for (const key of activePairs) {
245
+ const by = key % nby;
246
+ const bz = (key / nby) | 0;
247
+ const lineBase = by * nbx + bz * bStride;
248
+ let allSolid = true;
249
+ for (let bx = 0; bx < nbx; bx++) {
250
+ if (readBlockType(src.types, lineBase + bx) !== BLOCK_SOLID) {
251
+ allSolid = false;
252
+ break;
253
+ }
254
+ }
255
+ if (allSolid) {
256
+ for (let bx = 0; bx < nbx; bx++) {
257
+ dst.orBlock(lineBase + bx, SOLID_LO, SOLID_HI);
258
+ }
259
+ continue;
260
+ }
261
+ for (let ly = 0; ly < 4; ly++) {
262
+ const iy = (by << 2) + ly;
263
+ if (iy >= ny) {
264
+ continue;
265
+ }
266
+ for (let lz = 0; lz < 4; lz++) {
267
+ const iz = (bz << 2) + lz;
268
+ if (iz >= nz) {
269
+ continue;
270
+ }
271
+ srcBuf.fill(0);
272
+ dstBuf.fill(0);
273
+ extractLineX(src, iy, iz, srcBuf);
274
+ flatDilate1D(srcBuf, dstBuf, nx, halfExtent);
275
+ writeLineX(dst, iy, iz, dstBuf);
276
+ }
277
+ }
278
+ }
279
+ }
280
+ /**
281
+ * Dilate along Y by extracting Y-lines from sparse blocks.
282
+ */
283
+ function sparseDilateY(src, dst, halfExtent) {
284
+ const { nx, ny, nz, nbx, nby, bStride } = src;
285
+ const lineWords = (ny + 31) >>> 5;
286
+ const srcBuf = new Uint32Array(lineWords);
287
+ const dstBuf = new Uint32Array(lineWords);
288
+ const activePairs = getActiveXZPairs(src);
289
+ for (const key of activePairs) {
290
+ const bx = key % nbx;
291
+ const bz = (key / nbx) | 0;
292
+ const lineStart = bx + bz * bStride;
293
+ let allSolid = true;
294
+ for (let by = 0; by < nby; by++) {
295
+ if (readBlockType(src.types, lineStart + by * nbx) !== BLOCK_SOLID) {
296
+ allSolid = false;
297
+ break;
298
+ }
299
+ }
300
+ if (allSolid) {
301
+ for (let by = 0; by < nby; by++) {
302
+ dst.orBlock(lineStart + by * nbx, SOLID_LO, SOLID_HI);
303
+ }
304
+ continue;
305
+ }
306
+ for (let lx = 0; lx < 4; lx++) {
307
+ const ix = (bx << 2) + lx;
308
+ if (ix >= nx) {
309
+ continue;
310
+ }
311
+ for (let lz = 0; lz < 4; lz++) {
312
+ const iz = (bz << 2) + lz;
313
+ if (iz >= nz) {
314
+ continue;
315
+ }
316
+ srcBuf.fill(0);
317
+ dstBuf.fill(0);
318
+ extractLineY(src, ix, iz, srcBuf);
319
+ flatDilate1D(srcBuf, dstBuf, ny, halfExtent);
320
+ writeLineY(dst, ix, iz, dstBuf);
321
+ }
322
+ }
323
+ }
324
+ }
325
+ /**
326
+ * Dilate along Z by extracting Z-lines from sparse blocks.
327
+ */
328
+ function sparseDilateZ(src, dst, halfExtent) {
329
+ const { nx, ny, nz, nbx, nbz, bStride } = src;
330
+ const lineWords = (nz + 31) >>> 5;
331
+ const srcBuf = new Uint32Array(lineWords);
332
+ const dstBuf = new Uint32Array(lineWords);
333
+ const activePairs = getActiveXYPairs(src);
334
+ for (const key of activePairs) {
335
+ const bx = key % nbx;
336
+ const by = (key / nbx) | 0;
337
+ const lineStart = bx + by * nbx;
338
+ let allSolid = true;
339
+ for (let bz = 0; bz < nbz; bz++) {
340
+ if (readBlockType(src.types, lineStart + bz * bStride) !== BLOCK_SOLID) {
341
+ allSolid = false;
342
+ break;
343
+ }
344
+ }
345
+ if (allSolid) {
346
+ for (let bz = 0; bz < nbz; bz++) {
347
+ dst.orBlock(lineStart + bz * bStride, SOLID_LO, SOLID_HI);
348
+ }
349
+ continue;
350
+ }
351
+ for (let lx = 0; lx < 4; lx++) {
352
+ const ix = (bx << 2) + lx;
353
+ if (ix >= nx) {
354
+ continue;
355
+ }
356
+ for (let ly = 0; ly < 4; ly++) {
357
+ const iy = (by << 2) + ly;
358
+ if (iy >= ny) {
359
+ continue;
360
+ }
361
+ srcBuf.fill(0);
362
+ dstBuf.fill(0);
363
+ extractLineZ(src, ix, iy, srcBuf);
364
+ flatDilate1D(srcBuf, dstBuf, nz, halfExtent);
365
+ writeLineZ(dst, ix, iy, dstBuf);
366
+ }
367
+ }
368
+ }
369
+ }
370
+ /**
371
+ * Separable 3D dilation: X pass, then Z pass, then Y pass.
372
+ * X/Z share radius while Y can use a different half extent.
373
+ */
374
+ function sparseDilate3(src, halfExtentXZ, halfExtentY) {
375
+ const a = new SparseVoxelGrid(src.nx, src.ny, src.nz);
376
+ sparseDilateX(src, a, halfExtentXZ);
377
+ const b = new SparseVoxelGrid(src.nx, src.ny, src.nz);
378
+ sparseDilateZ(a, b, halfExtentXZ);
379
+ a.clear();
380
+ sparseDilateY(b, a, halfExtentY);
381
+ b.clear();
382
+ return a;
383
+ }
384
+ async function dilate3(src, halfExtentXZ, halfExtentY, backend) {
385
+ return backend === 'gpu'
386
+ ? gpuDilate3(src, halfExtentXZ, halfExtentY)
387
+ : sparseDilate3(src, halfExtentXZ, halfExtentY);
388
+ }
389
+ /**
390
+ * Compute reachable empty voxels as visited \ blocked.
391
+ * This keeps only flood-filled cells that are not blocked after dilation.
392
+ */
393
+ function computeEmptyGrid(visited, blocked) {
394
+ const empty = new SparseVoxelGrid(visited.nx, visited.ny, visited.nz);
395
+ forEachNonEmptyBlock(visited, blockIdx => {
396
+ const vbt = readBlockType(visited.types, blockIdx);
397
+ let vLo, vHi;
398
+ if (vbt === BLOCK_SOLID) {
399
+ vLo = SOLID_LO;
400
+ vHi = SOLID_HI;
401
+ }
402
+ else {
403
+ const vs = visited.masks.slot(blockIdx);
404
+ vLo = visited.masks.lo[vs];
405
+ vHi = visited.masks.hi[vs];
406
+ }
407
+ const bbt = readBlockType(blocked.types, blockIdx);
408
+ let lo, hi;
409
+ if (bbt === BLOCK_EMPTY) {
410
+ lo = vLo;
411
+ hi = vHi;
412
+ }
413
+ else if (bbt === BLOCK_SOLID) {
414
+ lo = 0;
415
+ hi = 0;
416
+ }
417
+ else {
418
+ const bs = blocked.masks.slot(blockIdx);
419
+ lo = (vLo & ~blocked.masks.lo[bs]) >>> 0;
420
+ hi = (vHi & ~blocked.masks.hi[bs]) >>> 0;
421
+ }
422
+ if (lo || hi) {
423
+ empty.orBlock(blockIdx, lo, hi);
424
+ }
425
+ });
426
+ return empty;
427
+ }
428
+ /**
429
+ * Sparse OR between two voxel grids (block masks are OR-combined).
430
+ */
431
+ function sparseOrGrids(a, b, consumeA = false) {
432
+ const result = consumeA ? a : a.clone();
433
+ forEachNonEmptyBlock(b, blockIdx => {
434
+ const bt = readBlockType(b.types, blockIdx);
435
+ if (bt === BLOCK_SOLID) {
436
+ result.orBlock(blockIdx, SOLID_LO, SOLID_HI);
437
+ }
438
+ else {
439
+ const s = b.masks.slot(blockIdx);
440
+ result.orBlock(blockIdx, b.masks.lo[s], b.masks.hi[s]);
441
+ }
442
+ });
443
+ return result;
444
+ }
445
+ /**
446
+ * Flood fill on sparse voxel grids using two coupled queues:
447
+ * - block queue for fully empty blocks
448
+ * - voxel queue for mixed blocks
449
+ * This mirrors the reference two-level BFS for performance on sparse data.
450
+ */
451
+ function twoLevelBFS(blocked, blockSeeds, voxelSeeds, nx, ny, nz) {
452
+ const visited = new SparseVoxelGrid(nx, ny, nz);
453
+ const nbx = nx >> 2;
454
+ const nby = ny >> 2;
455
+ const nbz = nz >> 2;
456
+ const bStride = nbx * nby;
457
+ const bMasks = blocked.masks;
458
+ const vMasks = visited.masks;
459
+ let bqCap = 1 << 14;
460
+ let bqBuf = new Uint32Array(bqCap);
461
+ let bqMask = bqCap - 1, bqHead = 0, bqTail = 0, bqSize = 0;
462
+ let vqCap = 1 << 14;
463
+ let vqIx = new Uint32Array(vqCap);
464
+ let vqIy = new Uint32Array(vqCap);
465
+ let vqIz = new Uint32Array(vqCap);
466
+ let vqMask = vqCap - 1, vqHead = 0, vqTail = 0, vqSize = 0;
467
+ function growBlockQueue() {
468
+ const newCap = bqCap << 1;
469
+ const nb = new Uint32Array(newCap);
470
+ for (let i = 0; i < bqSize; i++) {
471
+ nb[i] = bqBuf[(bqHead + i) & bqMask];
472
+ }
473
+ bqBuf = nb;
474
+ bqCap = newCap;
475
+ bqMask = newCap - 1;
476
+ bqHead = 0;
477
+ bqTail = bqSize;
478
+ }
479
+ function growVoxelQueue() {
480
+ const newCap = vqCap << 1;
481
+ const nix = new Uint32Array(newCap);
482
+ const niy = new Uint32Array(newCap);
483
+ const niz = new Uint32Array(newCap);
484
+ for (let i = 0; i < vqSize; i++) {
485
+ const j = (vqHead + i) & vqMask;
486
+ nix[i] = vqIx[j];
487
+ niy[i] = vqIy[j];
488
+ niz[i] = vqIz[j];
489
+ }
490
+ vqIx = nix;
491
+ vqIy = niy;
492
+ vqIz = niz;
493
+ vqCap = newCap;
494
+ vqMask = newCap - 1;
495
+ vqHead = 0;
496
+ vqTail = vqSize;
497
+ }
498
+ function enqueueVoxel(ix, iy, iz) {
499
+ if (vqSize >= vqCap) {
500
+ growVoxelQueue();
501
+ }
502
+ vqIx[vqTail] = ix;
503
+ vqIy[vqTail] = iy;
504
+ vqIz[vqTail] = iz;
505
+ vqTail = (vqTail + 1) & vqMask;
506
+ vqSize++;
507
+ }
508
+ function tryFillBlock(blockIdx) {
509
+ if (readBlockType(blocked.types, blockIdx) !== BLOCK_EMPTY) {
510
+ return false;
511
+ }
512
+ if (readBlockType(visited.types, blockIdx) !== BLOCK_EMPTY) {
513
+ return false;
514
+ }
515
+ writeBlockType(visited.types, blockIdx, BLOCK_SOLID);
516
+ if (bqSize >= bqCap) {
517
+ growBlockQueue();
518
+ }
519
+ bqBuf[bqTail] = blockIdx;
520
+ bqTail = (bqTail + 1) & bqMask;
521
+ bqSize++;
522
+ return true;
523
+ }
524
+ function enqueueFaceVoxels(nBlockIdx, face, nBx, nBy, nBz) {
525
+ const vbt = readBlockType(visited.types, nBlockIdx);
526
+ if (vbt === BLOCK_SOLID) {
527
+ return;
528
+ }
529
+ const bs = bMasks.slot(nBlockIdx);
530
+ let vLo = 0, vHi = 0, vs = -1;
531
+ if (vbt === BLOCK_MIXED) {
532
+ vs = vMasks.slot(nBlockIdx);
533
+ vLo = vMasks.lo[vs];
534
+ vHi = vMasks.hi[vs];
535
+ }
536
+ const freeLo = (FACE_MASKS_LO[face] & ~bMasks.lo[bs] & ~vLo) >>> 0;
537
+ const freeHi = (FACE_MASKS_HI[face] & ~bMasks.hi[bs] & ~vHi) >>> 0;
538
+ if (freeLo === 0 && freeHi === 0) {
539
+ return;
540
+ }
541
+ if (vbt === BLOCK_EMPTY) {
542
+ writeBlockType(visited.types, nBlockIdx, BLOCK_MIXED);
543
+ vMasks.set(nBlockIdx, freeLo, freeHi);
544
+ }
545
+ else {
546
+ vMasks.lo[vs] = (vMasks.lo[vs] | freeLo) >>> 0;
547
+ vMasks.hi[vs] = (vMasks.hi[vs] | freeHi) >>> 0;
548
+ if (vMasks.lo[vs] === SOLID_LO && vMasks.hi[vs] === SOLID_HI) {
549
+ vMasks.removeAt(vs);
550
+ writeBlockType(visited.types, nBlockIdx, BLOCK_SOLID);
551
+ }
552
+ }
553
+ const baseIx = nBx << 2, baseIy = nBy << 2, baseIz = nBz << 2;
554
+ let bits = freeLo;
555
+ while (bits) {
556
+ const bp = 31 - Math.clz32(bits & -bits);
557
+ enqueueVoxel(baseIx + (bp & 3), baseIy + ((bp >> 2) & 3), baseIz + (bp >> 4));
558
+ bits &= bits - 1;
559
+ }
560
+ bits = freeHi;
561
+ while (bits) {
562
+ const bp = 31 - Math.clz32(bits & -bits);
563
+ const bi = bp + 32;
564
+ enqueueVoxel(baseIx + (bi & 3), baseIy + ((bi >> 2) & 3), baseIz + (bi >> 4));
565
+ bits &= bits - 1;
566
+ }
567
+ }
568
+ function processBlock(blockIdx) {
569
+ const bx = blockIdx % nbx;
570
+ const byBz = (blockIdx / nbx) | 0;
571
+ const by = byBz % nby;
572
+ const bz = (byBz / nby) | 0;
573
+ if (bx > 0) {
574
+ const ni = blockIdx - 1;
575
+ const nbt = readBlockType(blocked.types, ni);
576
+ if (nbt === BLOCK_EMPTY) {
577
+ tryFillBlock(ni);
578
+ }
579
+ else if (nbt === BLOCK_MIXED) {
580
+ enqueueFaceVoxels(ni, 1, bx - 1, by, bz);
581
+ }
582
+ }
583
+ if (bx < nbx - 1) {
584
+ const ni = blockIdx + 1;
585
+ const nbt = readBlockType(blocked.types, ni);
586
+ if (nbt === BLOCK_EMPTY) {
587
+ tryFillBlock(ni);
588
+ }
589
+ else if (nbt === BLOCK_MIXED) {
590
+ enqueueFaceVoxels(ni, 0, bx + 1, by, bz);
591
+ }
592
+ }
593
+ if (by > 0) {
594
+ const ni = blockIdx - nbx;
595
+ const nbt = readBlockType(blocked.types, ni);
596
+ if (nbt === BLOCK_EMPTY) {
597
+ tryFillBlock(ni);
598
+ }
599
+ else if (nbt === BLOCK_MIXED) {
600
+ enqueueFaceVoxels(ni, 3, bx, by - 1, bz);
601
+ }
602
+ }
603
+ if (by < nby - 1) {
604
+ const ni = blockIdx + nbx;
605
+ const nbt = readBlockType(blocked.types, ni);
606
+ if (nbt === BLOCK_EMPTY) {
607
+ tryFillBlock(ni);
608
+ }
609
+ else if (nbt === BLOCK_MIXED) {
610
+ enqueueFaceVoxels(ni, 2, bx, by + 1, bz);
611
+ }
612
+ }
613
+ if (bz > 0) {
614
+ const ni = blockIdx - bStride;
615
+ const nbt = readBlockType(blocked.types, ni);
616
+ if (nbt === BLOCK_EMPTY) {
617
+ tryFillBlock(ni);
618
+ }
619
+ else if (nbt === BLOCK_MIXED) {
620
+ enqueueFaceVoxels(ni, 5, bx, by, bz - 1);
621
+ }
622
+ }
623
+ if (bz < nbz - 1) {
624
+ const ni = blockIdx + bStride;
625
+ const nbt = readBlockType(blocked.types, ni);
626
+ if (nbt === BLOCK_EMPTY) {
627
+ tryFillBlock(ni);
628
+ }
629
+ else if (nbt === BLOCK_MIXED) {
630
+ enqueueFaceVoxels(ni, 4, bx, by, bz + 1);
631
+ }
632
+ }
633
+ }
634
+ function tryEnqueueVoxel(ix, iy, iz) {
635
+ const blockIdx = (ix >> 2) + (iy >> 2) * nbx + (iz >> 2) * bStride;
636
+ const bbt = readBlockType(blocked.types, blockIdx);
637
+ if (bbt === BLOCK_SOLID) {
638
+ return;
639
+ }
640
+ if (bbt === BLOCK_EMPTY) {
641
+ tryFillBlock(blockIdx);
642
+ return;
643
+ }
644
+ const bs = bMasks.slot(blockIdx);
645
+ const bitIdx = (ix & 3) + ((iy & 3) << 2) + ((iz & 3) << 4);
646
+ if (bitIdx < 32 ? (bMasks.lo[bs] >>> bitIdx) & 1 : (bMasks.hi[bs] >>> (bitIdx - 32)) & 1) {
647
+ return;
648
+ }
649
+ const vbt = readBlockType(visited.types, blockIdx);
650
+ if (vbt === BLOCK_SOLID) {
651
+ return;
652
+ }
653
+ if (vbt === BLOCK_MIXED) {
654
+ const vs = vMasks.slot(blockIdx);
655
+ if (bitIdx < 32 ? (vMasks.lo[vs] >>> bitIdx) & 1 : (vMasks.hi[vs] >>> (bitIdx - 32)) & 1) {
656
+ return;
657
+ }
658
+ if (bitIdx < 32) {
659
+ vMasks.lo[vs] = (vMasks.lo[vs] | (1 << bitIdx)) >>> 0;
660
+ }
661
+ else {
662
+ vMasks.hi[vs] = (vMasks.hi[vs] | (1 << (bitIdx - 32))) >>> 0;
663
+ }
664
+ if (vMasks.lo[vs] === SOLID_LO && vMasks.hi[vs] === SOLID_HI) {
665
+ vMasks.removeAt(vs);
666
+ writeBlockType(visited.types, blockIdx, BLOCK_SOLID);
667
+ }
668
+ }
669
+ else {
670
+ writeBlockType(visited.types, blockIdx, BLOCK_MIXED);
671
+ vMasks.set(blockIdx, bitIdx < 32 ? (1 << bitIdx) >>> 0 : 0, bitIdx >= 32 ? (1 << (bitIdx - 32)) >>> 0 : 0);
672
+ }
673
+ enqueueVoxel(ix, iy, iz);
674
+ }
675
+ for (let i = 0; i < blockSeeds.length; i++) {
676
+ tryFillBlock(blockSeeds[i]);
677
+ }
678
+ for (let i = 0; i < voxelSeeds.length; i++) {
679
+ const s = voxelSeeds[i];
680
+ tryEnqueueVoxel(s.ix, s.iy, s.iz);
681
+ }
682
+ while (bqSize > 0 || vqSize > 0) {
683
+ while (bqSize > 0) {
684
+ const blockIdx = bqBuf[bqHead];
685
+ bqHead = (bqHead + 1) & bqMask;
686
+ bqSize--;
687
+ processBlock(blockIdx);
688
+ }
689
+ if (vqSize > 0) {
690
+ const ix = vqIx[vqHead], iy = vqIy[vqHead], iz = vqIz[vqHead];
691
+ vqHead = (vqHead + 1) & vqMask;
692
+ vqSize--;
693
+ if (ix > 0) {
694
+ tryEnqueueVoxel(ix - 1, iy, iz);
695
+ }
696
+ if (ix < nx - 1) {
697
+ tryEnqueueVoxel(ix + 1, iy, iz);
698
+ }
699
+ if (iy > 0) {
700
+ tryEnqueueVoxel(ix, iy - 1, iz);
701
+ }
702
+ if (iy < ny - 1) {
703
+ tryEnqueueVoxel(ix, iy + 1, iz);
704
+ }
705
+ if (iz > 0) {
706
+ tryEnqueueVoxel(ix, iy, iz - 1);
707
+ }
708
+ if (iz < nz - 1) {
709
+ tryEnqueueVoxel(ix, iy, iz + 1);
710
+ }
711
+ }
712
+ }
713
+ return visited;
714
+ }
715
+ function cloneBounds(b) {
716
+ return { min: { ...b.min }, max: { ...b.max } };
717
+ }
718
+ /**
719
+ * Fill exterior-reachable space from boundary seeds and merge it back into
720
+ * occupancy after dilation. Returns cropped bounds around navigable volume.
721
+ */
722
+ export async function fillExterior(gridOriginal, gridBounds, voxelResolution, dilation, seed, backend = 'cpu') {
723
+ if (!Number.isFinite(voxelResolution) || voxelResolution <= 0) {
724
+ throw new Error(`fillExterior: voxelResolution must be finite and > 0, got ${voxelResolution}`);
725
+ }
726
+ if (!Number.isFinite(dilation) || dilation <= 0) {
727
+ throw new Error(`fillExterior: dilation must be finite and > 0, got ${dilation}`);
728
+ }
729
+ const nx = Math.round((gridBounds.max.x - gridBounds.min.x) / voxelResolution);
730
+ const ny = Math.round((gridBounds.max.y - gridBounds.min.y) / voxelResolution);
731
+ const nz = Math.round((gridBounds.max.z - gridBounds.min.z) / voxelResolution);
732
+ if (nx % 4 !== 0 || ny % 4 !== 0 || nz % 4 !== 0) {
733
+ throw new Error(`fillExterior: grid dimensions must be multiples of 4, got ${nx}x${ny}x${nz}`);
734
+ }
735
+ const halfExtent = Math.ceil(dilation / voxelResolution);
736
+ const nbx = nx >> 2, nby = ny >> 2, nbz = nz >> 2;
737
+ const dilated = await dilate3(gridOriginal, halfExtent, halfExtent, backend);
738
+ const bStride = nbx * nby;
739
+ const blockSeeds = [];
740
+ const faceVoxelSeeds = [];
741
+ function seedBoundaryBlock(blockIdx, bx, by, bz, face) {
742
+ const bt = readBlockType(dilated.types, blockIdx);
743
+ if (bt === BLOCK_SOLID) {
744
+ return;
745
+ }
746
+ if (bt === BLOCK_EMPTY) {
747
+ blockSeeds.push(blockIdx);
748
+ return;
749
+ }
750
+ const ms = dilated.masks.slot(blockIdx);
751
+ let freeLo = (FACE_MASKS_LO[face] & ~dilated.masks.lo[ms]) >>> 0;
752
+ let freeHi = (FACE_MASKS_HI[face] & ~dilated.masks.hi[ms]) >>> 0;
753
+ if (freeLo === 0 && freeHi === 0) {
754
+ return;
755
+ }
756
+ const baseIx = bx << 2, baseIy = by << 2, baseIz = bz << 2;
757
+ while (freeLo) {
758
+ const bp = 31 - Math.clz32(freeLo & -freeLo);
759
+ faceVoxelSeeds.push({ ix: baseIx + (bp & 3), iy: baseIy + ((bp >> 2) & 3), iz: baseIz + (bp >> 4) });
760
+ freeLo &= freeLo - 1;
761
+ }
762
+ while (freeHi) {
763
+ const bp = 31 - Math.clz32(freeHi & -freeHi);
764
+ const bi = bp + 32;
765
+ faceVoxelSeeds.push({ ix: baseIx + (bi & 3), iy: baseIy + ((bi >> 2) & 3), iz: baseIz + (bi >> 4) });
766
+ freeHi &= freeHi - 1;
767
+ }
768
+ }
769
+ for (let bz = 0; bz < nbz; bz++) {
770
+ for (let by = 0; by < nby; by++) {
771
+ seedBoundaryBlock(by * nbx + bz * bStride, 0, by, bz, 0);
772
+ }
773
+ }
774
+ for (let bz = 0; bz < nbz; bz++) {
775
+ for (let by = 0; by < nby; by++) {
776
+ seedBoundaryBlock(nbx - 1 + by * nbx + bz * bStride, nbx - 1, by, bz, 1);
777
+ }
778
+ }
779
+ for (let bz = 0; bz < nbz; bz++) {
780
+ for (let bx = 0; bx < nbx; bx++) {
781
+ seedBoundaryBlock(bx + bz * bStride, bx, 0, bz, 2);
782
+ }
783
+ }
784
+ for (let bz = 0; bz < nbz; bz++) {
785
+ for (let bx = 0; bx < nbx; bx++) {
786
+ seedBoundaryBlock(bx + (nby - 1) * nbx + bz * bStride, bx, nby - 1, bz, 3);
787
+ }
788
+ }
789
+ for (let by = 0; by < nby; by++) {
790
+ for (let bx = 0; bx < nbx; bx++) {
791
+ seedBoundaryBlock(bx + by * nbx, bx, by, 0, 4);
792
+ }
793
+ }
794
+ for (let by = 0; by < nby; by++) {
795
+ for (let bx = 0; bx < nbx; bx++) {
796
+ seedBoundaryBlock(bx + by * nbx + (nbz - 1) * bStride, bx, by, nbz - 1, 5);
797
+ }
798
+ }
799
+ const visited = twoLevelBFS(dilated, blockSeeds, faceVoxelSeeds, nx, ny, nz);
800
+ const seedIx = Math.floor((seed.x - gridBounds.min.x) / voxelResolution);
801
+ const seedIy = Math.floor((seed.y - gridBounds.min.y) / voxelResolution);
802
+ const seedIz = Math.floor((seed.z - gridBounds.min.z) / voxelResolution);
803
+ if (seedIx >= 0 && seedIx < nx && seedIy >= 0 && seedIy < ny && seedIz >= 0 && seedIz < nz) {
804
+ if (visited.getVoxel(seedIx, seedIy, seedIz)) {
805
+ logger.info('fillExteriorMap: seed reachable from outside, skipping');
806
+ return { grid: gridOriginal, gridBounds };
807
+ }
808
+ }
809
+ else {
810
+ logger.info('fillExteriorMap: seed outside grid bounds, skipping exterior fill');
811
+ return { grid: gridOriginal, gridBounds };
812
+ }
813
+ const dilatedVisited = await dilate3(visited, halfExtent, halfExtent, backend);
814
+ const combined = sparseOrGrids(gridOriginal, dilatedVisited);
815
+ let minIx = nx, minIy = ny, minIz = nz;
816
+ let maxIx = 0, maxIy = 0, maxIz = 0;
817
+ for (let bz = 0; bz < nbz; bz++) {
818
+ for (let by = 0; by < nby; by++) {
819
+ for (let bx = 0; bx < nbx; bx++) {
820
+ const blockIdx = bx + by * nbx + bz * combined.bStride;
821
+ const bt = readBlockType(combined.types, blockIdx);
822
+ if (bt === BLOCK_SOLID) {
823
+ continue;
824
+ }
825
+ if (bt === BLOCK_MIXED) {
826
+ const cs = combined.masks.slot(blockIdx);
827
+ if (combined.masks.lo[cs] === SOLID_LO && combined.masks.hi[cs] === SOLID_HI) {
828
+ continue;
829
+ }
830
+ }
831
+ const baseX = bx << 2, baseY = by << 2, baseZ = bz << 2;
832
+ if (baseX < minIx) {
833
+ minIx = baseX;
834
+ }
835
+ if (baseX + 3 > maxIx) {
836
+ maxIx = baseX + 3;
837
+ }
838
+ if (baseY < minIy) {
839
+ minIy = baseY;
840
+ }
841
+ if (baseY + 3 > maxIy) {
842
+ maxIy = baseY + 3;
843
+ }
844
+ if (baseZ < minIz) {
845
+ minIz = baseZ;
846
+ }
847
+ if (baseZ + 3 > maxIz) {
848
+ maxIz = baseZ + 3;
849
+ }
850
+ }
851
+ }
852
+ }
853
+ if (minIx > maxIx) {
854
+ logger.warn('fillExteriorMap: no navigable cells remain, returning empty result');
855
+ return {
856
+ grid: new SparseVoxelGrid(4, 4, 4),
857
+ gridBounds: { min: { ...gridBounds.min }, max: { ...gridBounds.min } },
858
+ };
859
+ }
860
+ const MARGIN = 1;
861
+ const cropMinBx = Math.max(0, (minIx >> 2) - MARGIN);
862
+ const cropMinBy = Math.max(0, (minIy >> 2) - MARGIN);
863
+ const cropMinBz = Math.max(0, (minIz >> 2) - MARGIN);
864
+ const cropMaxBx = Math.min(nbx, (maxIx >> 2) + 1 + MARGIN);
865
+ const cropMaxBy = Math.min(nby, (maxIy >> 2) + 1 + MARGIN);
866
+ const cropMaxBz = Math.min(nbz, (maxIz >> 2) + 1 + MARGIN);
867
+ const blockSize = 4 * voxelResolution;
868
+ const croppedMin = {
869
+ x: gridBounds.min.x + cropMinBx * blockSize,
870
+ y: gridBounds.min.y + cropMinBy * blockSize,
871
+ z: gridBounds.min.z + cropMinBz * blockSize,
872
+ };
873
+ const croppedBounds = {
874
+ min: croppedMin,
875
+ max: {
876
+ x: croppedMin.x + (cropMaxBx - cropMinBx) * blockSize,
877
+ y: croppedMin.y + (cropMaxBy - cropMinBy) * blockSize,
878
+ z: croppedMin.z + (cropMaxBz - cropMinBz) * blockSize,
879
+ },
880
+ };
881
+ return {
882
+ grid: combined.cropTo(cropMinBx, cropMinBy, cropMinBz, cropMaxBx, cropMaxBy, cropMaxBz),
883
+ gridBounds: croppedBounds,
884
+ };
885
+ }
886
+ /**
887
+ * Carve navigable space for a capsule by:
888
+ * 1) dilating blocked voxels by capsule dimensions
889
+ * 2) flood filling reachable empty space from the seed
890
+ * 3) dilating and inverting to final occupancy representation.
891
+ */
892
+ export async function carve(grid, gridBounds, voxelResolution, capsuleHeight, capsuleRadius, seed, backend = 'cpu') {
893
+ if (!Number.isFinite(voxelResolution) || voxelResolution <= 0) {
894
+ throw new Error(`carve: voxelResolution must be finite and > 0, got ${voxelResolution}`);
895
+ }
896
+ if (!Number.isFinite(capsuleHeight) || capsuleHeight <= 0) {
897
+ throw new Error(`carve: capsuleHeight must be finite and > 0, got ${capsuleHeight}`);
898
+ }
899
+ if (!Number.isFinite(capsuleRadius) || capsuleRadius < 0) {
900
+ throw new Error(`carve: capsuleRadius must be finite and >= 0, got ${capsuleRadius}`);
901
+ }
902
+ const nx = Math.round((gridBounds.max.x - gridBounds.min.x) / voxelResolution);
903
+ const ny = Math.round((gridBounds.max.y - gridBounds.min.y) / voxelResolution);
904
+ const nz = Math.round((gridBounds.max.z - gridBounds.min.z) / voxelResolution);
905
+ if (nx % 4 !== 0 || ny % 4 !== 0 || nz % 4 !== 0) {
906
+ throw new Error(`carve: grid dimensions must be multiples of 4, got ${nx}x${ny}x${nz}`);
907
+ }
908
+ const kernelR = Math.ceil(capsuleRadius / voxelResolution);
909
+ const yHalfExtent = Math.ceil(capsuleHeight / (2 * voxelResolution));
910
+ const nbx = nx >> 2;
911
+ const nby = ny >> 2;
912
+ const nbz = nz >> 2;
913
+ const blocked = await dilate3(grid, kernelR, yHalfExtent, backend);
914
+ const seedIx = Math.floor((seed.x - gridBounds.min.x) / voxelResolution);
915
+ const seedIy = Math.floor((seed.y - gridBounds.min.y) / voxelResolution);
916
+ const seedIz = Math.floor((seed.z - gridBounds.min.z) / voxelResolution);
917
+ if (seedIx < 0 || seedIx >= nx || seedIy < 0 || seedIy >= ny || seedIz < 0 || seedIz >= nz) {
918
+ logger.warn(`carve: seed (${seed.x}, ${seed.y}, ${seed.z}) outside grid, skipping`);
919
+ return { grid, gridBounds: cloneBounds(gridBounds) };
920
+ }
921
+ let useSeedIx = seedIx, useSeedIy = seedIy, useSeedIz = seedIz;
922
+ if (blocked.getVoxel(seedIx, seedIy, seedIz)) {
923
+ const maxRadius = Math.max(kernelR, yHalfExtent) * 2;
924
+ const found = SparseVoxelGrid.findNearestFreeCell(blocked, seedIx, seedIy, seedIz, maxRadius);
925
+ if (!found) {
926
+ logger.warn(`carve: seed (${seed.x}, ${seed.y}, ${seed.z}) blocked after dilation, no free cell within ${maxRadius} voxels, skipping`);
927
+ return { grid, gridBounds: cloneBounds(gridBounds) };
928
+ }
929
+ useSeedIx = found.ix;
930
+ useSeedIy = found.iy;
931
+ useSeedIz = found.iz;
932
+ }
933
+ const seedBlockIdx = (useSeedIx >> 2) + (useSeedIy >> 2) * nbx + (useSeedIz >> 2) * (nbx * nby);
934
+ const seedBt = readBlockType(blocked.types, seedBlockIdx);
935
+ const blockSeeds = seedBt === BLOCK_EMPTY ? [seedBlockIdx] : [];
936
+ const voxelSeeds = seedBt === BLOCK_EMPTY ? [] : [{ ix: useSeedIx, iy: useSeedIy, iz: useSeedIz }];
937
+ const visited = twoLevelBFS(blocked, blockSeeds, voxelSeeds, nx, ny, nz);
938
+ // useless?
939
+ const emptyGrid = computeEmptyGrid(visited, blocked);
940
+ const navRegion = await dilate3(emptyGrid, kernelR, yHalfExtent, backend);
941
+ const navBounds = navRegion.getOccupiedBlockBounds();
942
+ if (!navBounds) {
943
+ logger.warn('carve: no navigable cells remain, returning empty result');
944
+ return {
945
+ grid: new SparseVoxelGrid(4, 4, 4),
946
+ gridBounds: { min: { ...gridBounds.min }, max: { ...gridBounds.min } },
947
+ };
948
+ }
949
+ const MARGIN = 1;
950
+ const cropMinBx = Math.max(0, navBounds.minBx - MARGIN);
951
+ const cropMinBy = Math.max(0, navBounds.minBy - MARGIN);
952
+ const cropMinBz = Math.max(0, navBounds.minBz - MARGIN);
953
+ const cropMaxBx = Math.min(nbx, navBounds.maxBx + 1 + MARGIN);
954
+ const cropMaxBy = Math.min(nby, navBounds.maxBy + 1 + MARGIN);
955
+ const cropMaxBz = Math.min(nbz, navBounds.maxBz + 1 + MARGIN);
956
+ const blockSize = 4 * voxelResolution;
957
+ const croppedMin = {
958
+ x: gridBounds.min.x + cropMinBx * blockSize,
959
+ y: gridBounds.min.y + cropMinBy * blockSize,
960
+ z: gridBounds.min.z + cropMinBz * blockSize,
961
+ };
962
+ const croppedBounds = {
963
+ min: croppedMin,
964
+ max: {
965
+ x: croppedMin.x + (cropMaxBx - cropMinBx) * blockSize,
966
+ y: croppedMin.y + (cropMaxBy - cropMinBy) * blockSize,
967
+ z: croppedMin.z + (cropMaxBz - cropMinBz) * blockSize,
968
+ },
969
+ };
970
+ return {
971
+ grid: navRegion.cropToInverted(cropMinBx, cropMinBy, cropMinBz, cropMaxBx, cropMaxBy, cropMaxBz),
972
+ gridBounds: croppedBounds,
973
+ };
974
+ }
975
+ /**
976
+ * Floor-fill via XZ dilate -> per-column upward walk -> XZ dilate -> OR.
977
+ * This mirrors upstream's block/bitmask walk instead of per-voxel getVoxel checks.
978
+ */
979
+ export async function fillFloor(gridOriginal, gridBounds, voxelResolution, dilation = 0, backend = 'cpu') {
980
+ const nx = Math.round((gridBounds.max.x - gridBounds.min.x) / voxelResolution);
981
+ const ny = Math.round((gridBounds.max.y - gridBounds.min.y) / voxelResolution);
982
+ const nz = Math.round((gridBounds.max.z - gridBounds.min.z) / voxelResolution);
983
+ if (nx % 4 !== 0 || ny % 4 !== 0 || nz % 4 !== 0) {
984
+ return { grid: gridOriginal, gridBounds };
985
+ }
986
+ const halfExtent = Math.max(0, Math.ceil(dilation / voxelResolution));
987
+ const dilatedSolid = halfExtent > 0 ? await dilate3(gridOriginal, halfExtent, 0, backend) : gridOriginal;
988
+ const { nbx, nby, nbz, bStride } = gridOriginal;
989
+ const foundEmpty = new SparseVoxelGrid(nx, ny, nz);
990
+ const dilatedTypes = dilatedSolid.types;
991
+ for (let bz = 0; bz < nbz; bz++) {
992
+ for (let bx = 0; bx < nbx; bx++) {
993
+ let walking = 0xffff;
994
+ for (let by = 0; by < nby && walking; by++) {
995
+ const blockIdx = bx + by * nbx + bz * bStride;
996
+ const bt = readBlockType(dilatedTypes, blockIdx);
997
+ if (bt === BLOCK_SOLID) {
998
+ break;
999
+ }
1000
+ if (bt === BLOCK_EMPTY) {
1001
+ if (walking === 0xffff) {
1002
+ foundEmpty.orBlock(blockIdx, SOLID_LO, SOLID_HI);
1003
+ }
1004
+ else {
1005
+ let lo = 0;
1006
+ let hi = 0;
1007
+ for (let lz = 0; lz < 4; lz++) {
1008
+ for (let lx = 0; lx < 4; lx++) {
1009
+ if (!(walking & (1 << (lz * 4 + lx)))) {
1010
+ continue;
1011
+ }
1012
+ for (let ly = 0; ly < 4; ly++) {
1013
+ const bitIdx = lx + (ly << 2) + (lz << 4);
1014
+ if (bitIdx < 32) {
1015
+ lo |= 1 << bitIdx;
1016
+ }
1017
+ else {
1018
+ hi |= 1 << (bitIdx - 32);
1019
+ }
1020
+ }
1021
+ }
1022
+ }
1023
+ foundEmpty.orBlock(blockIdx, lo >>> 0, hi >>> 0);
1024
+ }
1025
+ continue;
1026
+ }
1027
+ const s = dilatedSolid.masks.slot(blockIdx);
1028
+ const dLo = dilatedSolid.masks.lo[s];
1029
+ const dHi = dilatedSolid.masks.hi[s];
1030
+ let foundLo = 0;
1031
+ let foundHi = 0;
1032
+ for (let lz = 0; lz < 4; lz++) {
1033
+ for (let lx = 0; lx < 4; lx++) {
1034
+ const subCol = 1 << (lz * 4 + lx);
1035
+ if (!(walking & subCol)) {
1036
+ continue;
1037
+ }
1038
+ for (let ly = 0; ly < 4; ly++) {
1039
+ const bitIdx = lx + (ly << 2) + (lz << 4);
1040
+ const inHi = bitIdx >= 32;
1041
+ const word = inHi ? dHi : dLo;
1042
+ const bit = 1 << (inHi ? bitIdx - 32 : bitIdx);
1043
+ if (word & bit) {
1044
+ walking &= ~subCol;
1045
+ break;
1046
+ }
1047
+ if (inHi) {
1048
+ foundHi |= bit;
1049
+ }
1050
+ else {
1051
+ foundLo |= bit;
1052
+ }
1053
+ }
1054
+ }
1055
+ }
1056
+ if (foundLo || foundHi) {
1057
+ foundEmpty.orBlock(blockIdx, foundLo >>> 0, foundHi >>> 0);
1058
+ }
1059
+ }
1060
+ }
1061
+ }
1062
+ if (halfExtent > 0) {
1063
+ dilatedSolid.clear();
1064
+ }
1065
+ const foundDilated = halfExtent > 0 ? await dilate3(foundEmpty, halfExtent, 0, backend) : foundEmpty;
1066
+ const combined = sparseOrGrids(gridOriginal, foundDilated, true);
1067
+ return { grid: combined, gridBounds: cloneBounds(gridBounds) };
1068
+ }