@driftengine/physics 3.61.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (136) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +9 -0
  3. package/README.md +503 -0
  4. package/dist/bodies.d.ts +121 -0
  5. package/dist/bodies.js +298 -0
  6. package/dist/buoyancy.d.ts +46 -0
  7. package/dist/buoyancy.js +45 -0
  8. package/dist/cloth.d.ts +226 -0
  9. package/dist/cloth.js +587 -0
  10. package/dist/collide/aabb.d.ts +10 -0
  11. package/dist/collide/aabb.js +11 -0
  12. package/dist/collide/body.d.ts +202 -0
  13. package/dist/collide/body.js +142 -0
  14. package/dist/collide/eject.d.ts +39 -0
  15. package/dist/collide/eject.js +142 -0
  16. package/dist/collide/index.d.ts +18 -0
  17. package/dist/collide/index.js +5 -0
  18. package/dist/collide/scratch.d.ts +8 -0
  19. package/dist/collide/scratch.js +9 -0
  20. package/dist/collide/segment.d.ts +12 -0
  21. package/dist/collide/segment.js +109 -0
  22. package/dist/collide/sweep.d.ts +3 -0
  23. package/dist/collide/sweep.js +1036 -0
  24. package/dist/colliderSet.d.ts +239 -0
  25. package/dist/colliderSet.js +546 -0
  26. package/dist/compoundContact.d.ts +70 -0
  27. package/dist/compoundContact.js +221 -0
  28. package/dist/controller.d.ts +351 -0
  29. package/dist/controller.js +641 -0
  30. package/dist/cylinder.d.ts +78 -0
  31. package/dist/cylinder.js +294 -0
  32. package/dist/cylinderContact.d.ts +7 -0
  33. package/dist/cylinderContact.js +704 -0
  34. package/dist/decompose.d.ts +105 -0
  35. package/dist/decompose.js +979 -0
  36. package/dist/events.d.ts +43 -0
  37. package/dist/events.js +95 -0
  38. package/dist/executor.d.ts +147 -0
  39. package/dist/executor.js +173 -0
  40. package/dist/faces.d.ts +26 -0
  41. package/dist/faces.js +34 -0
  42. package/dist/fingerprint.d.ts +61 -0
  43. package/dist/fingerprint.js +139 -0
  44. package/dist/groundMotor.d.ts +34 -0
  45. package/dist/groundMotor.js +41 -0
  46. package/dist/heightfieldShape.d.ts +128 -0
  47. package/dist/heightfieldShape.js +328 -0
  48. package/dist/index.d.ts +73 -0
  49. package/dist/index.js +47 -0
  50. package/dist/island.d.ts +55 -0
  51. package/dist/island.js +227 -0
  52. package/dist/islandSolve.d.ts +70 -0
  53. package/dist/islandSolve.js +171 -0
  54. package/dist/islandWorker.d.ts +18 -0
  55. package/dist/islandWorker.js +125 -0
  56. package/dist/joints.d.ts +180 -0
  57. package/dist/joints.js +1094 -0
  58. package/dist/manifold.d.ts +89 -0
  59. package/dist/manifold.js +608 -0
  60. package/dist/mass.d.ts +53 -0
  61. package/dist/mass.js +353 -0
  62. package/dist/meshContact.d.ts +57 -0
  63. package/dist/meshContact.js +393 -0
  64. package/dist/meshShape.d.ts +76 -0
  65. package/dist/meshShape.js +175 -0
  66. package/dist/pairs.d.ts +62 -0
  67. package/dist/pairs.js +154 -0
  68. package/dist/query.d.ts +71 -0
  69. package/dist/query.js +683 -0
  70. package/dist/ragdoll.d.ts +135 -0
  71. package/dist/ragdoll.js +489 -0
  72. package/dist/shape.d.ts +177 -0
  73. package/dist/shape.js +533 -0
  74. package/dist/solver.d.ts +136 -0
  75. package/dist/solver.js +601 -0
  76. package/dist/stage.d.ts +167 -0
  77. package/dist/stage.js +350 -0
  78. package/dist/stallEscape.d.ts +141 -0
  79. package/dist/stallEscape.js +254 -0
  80. package/dist/sweptStep.d.ts +173 -0
  81. package/dist/sweptStep.js +472 -0
  82. package/dist/tree.d.ts +61 -0
  83. package/dist/tree.js +348 -0
  84. package/dist/vehicle.d.ts +154 -0
  85. package/dist/vehicle.js +343 -0
  86. package/dist/workerPool.d.ts +107 -0
  87. package/dist/workerPool.js +239 -0
  88. package/dist/workers.d.ts +37 -0
  89. package/dist/workers.js +35 -0
  90. package/dist/world.d.ts +298 -0
  91. package/dist/world.js +712 -0
  92. package/package.json +55 -0
  93. package/src/bodies.ts +369 -0
  94. package/src/buoyancy.ts +78 -0
  95. package/src/cloth.ts +734 -0
  96. package/src/collide/aabb.ts +28 -0
  97. package/src/collide/body.ts +336 -0
  98. package/src/collide/eject.ts +156 -0
  99. package/src/collide/index.ts +26 -0
  100. package/src/collide/scratch.ts +10 -0
  101. package/src/collide/segment.ts +127 -0
  102. package/src/collide/sweep.ts +1132 -0
  103. package/src/colliderSet.ts +637 -0
  104. package/src/compoundContact.ts +284 -0
  105. package/src/controller.ts +817 -0
  106. package/src/cylinder.ts +340 -0
  107. package/src/cylinderContact.ts +825 -0
  108. package/src/decompose.ts +1198 -0
  109. package/src/events.ts +108 -0
  110. package/src/executor.ts +224 -0
  111. package/src/faces.ts +37 -0
  112. package/src/fingerprint.ts +146 -0
  113. package/src/groundMotor.ts +53 -0
  114. package/src/heightfieldShape.ts +395 -0
  115. package/src/index.ts +164 -0
  116. package/src/island.ts +219 -0
  117. package/src/islandSolve.ts +218 -0
  118. package/src/islandWorker.ts +141 -0
  119. package/src/joints.ts +1367 -0
  120. package/src/manifold.ts +798 -0
  121. package/src/mass.ts +424 -0
  122. package/src/meshContact.ts +443 -0
  123. package/src/meshShape.ts +261 -0
  124. package/src/pairs.ts +169 -0
  125. package/src/query.ts +844 -0
  126. package/src/ragdoll.ts +662 -0
  127. package/src/shape.ts +656 -0
  128. package/src/solver.ts +873 -0
  129. package/src/stage.ts +406 -0
  130. package/src/stallEscape.ts +362 -0
  131. package/src/sweptStep.ts +609 -0
  132. package/src/tree.ts +366 -0
  133. package/src/vehicle.ts +489 -0
  134. package/src/workerPool.ts +283 -0
  135. package/src/workers.ts +38 -0
  136. package/src/world.ts +961 -0
@@ -0,0 +1,979 @@
1
+ import { hullShape } from './shape.js';
2
+ import { createMassProperties, shapeMassProperties } from './mass.js';
3
+ import { MAX_BODY_PARTS } from './compoundContact.js';
4
+ /** The default `maxHulls`, chosen so a decomposed body is well inside `MAX_BODY_PARTS`. */
5
+ export const DEFAULT_MAX_HULLS = 16;
6
+ /** The default and maximum grids. See `DecomposeOptions.resolution`. */
7
+ export const DEFAULT_RESOLUTION = 64;
8
+ export const MAX_RESOLUTION = 256;
9
+ const OUTSIDE = 0;
10
+ const SURFACE = 1;
11
+ const INSIDE = 2;
12
+ /**
13
+ * The directions a region's extreme corners are kept along, as integer vectors.
14
+ *
15
+ * **Fifty of them, and every one is integers**, so the table is built by arithmetic and nothing here
16
+ * reaches for a transcendental the determinism gate would refuse — `packages/physics/src` is a
17
+ * declared root of it and this file is inside the scan.
18
+ *
19
+ * They are an octahedron's own directions taken three ways: its six vertices, the twelve midpoints
20
+ * of its edges, the eight centres of its faces, and the twenty-four points halfway between a vertex
21
+ * and an edge midpoint. That is a 50-DOP, whose worst angular gap leaves a support hull within about
22
+ * two per cent of the true hull of the same corners — small against a region that is a fraction of
23
+ * the mesh, and measured by `Decomposition.coverage` rather than trusted.
24
+ *
25
+ * **Fifty and not sixty-four** because these four families are what an octahedron gives without
26
+ * inventing a direction; the next family would be twenty-four more and overrun `hullShape`'s cap of
27
+ * sixty-four points. Nothing is gained by padding the set to the cap.
28
+ */
29
+ const DIRECTIONS = buildDirections();
30
+ /** How many, which is also the most points any part can carry. */
31
+ export const SUPPORT_DIRECTIONS = DIRECTIONS.length / 3;
32
+ function buildDirections() {
33
+ const families = [
34
+ [1, 0, 0],
35
+ [1, 1, 0],
36
+ [1, 1, 1],
37
+ [2, 1, 0],
38
+ ];
39
+ const seen = new Set();
40
+ const out = [];
41
+ for (const [a, b, c] of families) {
42
+ /*
43
+ * Every distinct permutation of the family's magnitudes, with every sign on every non-zero
44
+ * component. A zero has one sign and not two, which is what keeps (1,0,0) from arriving twice.
45
+ */
46
+ for (const p of permutations(a, b, c)) {
47
+ for (let s = 0; s < 8; s++) {
48
+ const x = p[0] * (s & 1 ? -1 : 1);
49
+ const y = p[1] * (s & 2 ? -1 : 1);
50
+ const z = p[2] * (s & 4 ? -1 : 1);
51
+ const key = `${x},${y},${z}`;
52
+ if (seen.has(key))
53
+ continue;
54
+ seen.add(key);
55
+ out.push(x, y, z);
56
+ }
57
+ }
58
+ }
59
+ return new Int32Array(out);
60
+ }
61
+ function permutations(a, b, c) {
62
+ const all = [
63
+ [a, b, c],
64
+ [a, c, b],
65
+ [b, a, c],
66
+ [b, c, a],
67
+ [c, a, b],
68
+ [c, b, a],
69
+ ];
70
+ const seen = new Set();
71
+ const out = [];
72
+ for (const p of all) {
73
+ const key = p.join(',');
74
+ if (seen.has(key))
75
+ continue;
76
+ seen.add(key);
77
+ out.push(p);
78
+ }
79
+ return out;
80
+ }
81
+ export function decomposeConvex(positions, indices, options = {}) {
82
+ const resolution = Math.trunc(options.resolution ?? DEFAULT_RESOLUTION);
83
+ const maxHulls = Math.trunc(options.maxHulls ?? DEFAULT_MAX_HULLS);
84
+ const concavity = options.concavity ?? 0.05;
85
+ const fill = options.fill ?? 'solid';
86
+ if (resolution < 4 || resolution > MAX_RESOLUTION) {
87
+ throw new Error(`decomposeConvex: resolution must be between 4 and ${MAX_RESOLUTION}, got ${resolution}`);
88
+ }
89
+ if (maxHulls < 1 || maxHulls > MAX_BODY_PARTS) {
90
+ throw new Error(`decomposeConvex: maxHulls must be between 1 and ${MAX_BODY_PARTS}, got ${maxHulls}`);
91
+ }
92
+ if (!(concavity >= 0) || concavity >= 1) {
93
+ throw new Error(`decomposeConvex: concavity must be at least 0 and below 1, got ${concavity}`);
94
+ }
95
+ if (indices.length % 3 !== 0) {
96
+ throw new Error(`decomposeConvex: ${indices.length} indices is not a whole number of triangles`);
97
+ }
98
+ if (indices.length === 0)
99
+ throw new Error('decomposeConvex: no triangles');
100
+ const grid = buildGrid(positions, resolution);
101
+ const state = new Uint8Array(grid.nx * grid.ny * grid.nz);
102
+ voxelise(positions, indices, grid, state);
103
+ if (fill === 'solid')
104
+ fillOutside(grid, state);
105
+ /*
106
+ * `fillOutside` marks what the flood reached; everything it did not reach and did not already own
107
+ * is interior. Under `'surface'` the pass never ran, so nothing is interior and the shell stands
108
+ * alone — which is the whole difference between the two settings.
109
+ */
110
+ const solid = new Uint8Array(state.length);
111
+ let solidCells = 0;
112
+ for (let i = 0; i < state.length; i++) {
113
+ const s = state[i] ?? OUTSIDE;
114
+ if (s === SURFACE || (fill === 'solid' && s === INSIDE)) {
115
+ solid[i] = 1;
116
+ solidCells++;
117
+ }
118
+ }
119
+ if (solidCells === 0) {
120
+ throw new Error('decomposeConvex: the mesh occupied no cells — it is thinner than one cell at this resolution');
121
+ }
122
+ const regions = growRegions(grid, solid, concavity);
123
+ mergeRegions(regions, maxHulls, concavity, grid.cell * grid.cell * grid.cell);
124
+ const parts = buildParts(grid, solid, regions);
125
+ const cellVolume = grid.cell * grid.cell * grid.cell;
126
+ const sourceVolume = solidCells * cellVolume;
127
+ let hullVolume = 0;
128
+ for (const part of parts.parts)
129
+ hullVolume += part.volume;
130
+ return {
131
+ parts: parts.parts,
132
+ sourceVolume,
133
+ hullVolume,
134
+ bloat: (hullVolume - sourceVolume) / sourceVolume,
135
+ coverage: parts.covered / solidCells,
136
+ resolution,
137
+ cells: state.length,
138
+ };
139
+ }
140
+ /**
141
+ * Cubic cells, sized by the longest axis, with one cell of padding on every side.
142
+ *
143
+ * **The padding is what makes the flood fill correct rather than nearly correct.** A mesh whose face
144
+ * sits exactly on the grid's boundary would have no outside cell on that side to flood from, and the
145
+ * fill would leave the whole slab beyond it marked interior — a solid wall where the model has a
146
+ * flat back. One ring of empty cells removes the case entirely.
147
+ */
148
+ function buildGrid(positions, resolution) {
149
+ let minX = Infinity;
150
+ let minY = Infinity;
151
+ let minZ = Infinity;
152
+ let maxX = -Infinity;
153
+ let maxY = -Infinity;
154
+ let maxZ = -Infinity;
155
+ for (let i = 0; i + 2 < positions.length; i += 3) {
156
+ const x = positions[i];
157
+ const y = positions[i + 1];
158
+ const z = positions[i + 2];
159
+ if (x < minX)
160
+ minX = x;
161
+ if (y < minY)
162
+ minY = y;
163
+ if (z < minZ)
164
+ minZ = z;
165
+ if (x > maxX)
166
+ maxX = x;
167
+ if (y > maxY)
168
+ maxY = y;
169
+ if (z > maxZ)
170
+ maxZ = z;
171
+ }
172
+ if (!(maxX >= minX))
173
+ throw new Error('decomposeConvex: no positions');
174
+ const spanX = maxX - minX;
175
+ const spanY = maxY - minY;
176
+ const spanZ = maxZ - minZ;
177
+ const longest = Math.max(spanX, Math.max(spanY, spanZ));
178
+ if (!(longest > 0))
179
+ throw new Error('decomposeConvex: the mesh has no extent');
180
+ const cell = longest / resolution;
181
+ /* Two cells of padding in each axis, one at each end, hence the +2 on every count. */
182
+ const nx = Math.max(1, Math.ceil(spanX / cell)) + 2;
183
+ const ny = Math.max(1, Math.ceil(spanY / cell)) + 2;
184
+ const nz = Math.max(1, Math.ceil(spanZ / cell)) + 2;
185
+ return {
186
+ originX: minX - cell,
187
+ originY: minY - cell,
188
+ originZ: minZ - cell,
189
+ cell,
190
+ nx,
191
+ ny,
192
+ nz,
193
+ };
194
+ }
195
+ /** Cell index from coordinates. x fastest, then y, then z — the order every walk here uses. */
196
+ function at(grid, x, y, z) {
197
+ return x + grid.nx * (y + grid.ny * z);
198
+ }
199
+ /**
200
+ * Every cell a triangle touches, marked `SURFACE`.
201
+ *
202
+ * The overlap test is the standard thirteen-axis separating-axis test between a triangle and an
203
+ * axis-aligned box: the box's three face normals, the triangle's own normal, and the nine cross
204
+ * products of a triangle edge with a box axis. It uses multiplication, addition, `Math.abs`,
205
+ * `Math.min` and `Math.max` and nothing else, which is what keeps it inside the determinism gate.
206
+ *
207
+ * **Conservative on purpose.** A cell that the triangle only grazes is marked, so a surface is never
208
+ * one cell thin where it should be closed — a hole in the shell is what lets the flood fill escape
209
+ * and turn a solid into a hollow one, and it is much cheaper to over-mark than to detect that later.
210
+ */
211
+ function voxelise(positions, indices, grid, state) {
212
+ /*
213
+ * **The cell box is grown by one part in a million, and that is the difference between a shell
214
+ * that holds and one with a hole straight through it.**
215
+ *
216
+ * A face lying exactly in a cell boundary plane is not an edge case in this domain — it is what
217
+ * every floor, wall, lid and cap in every model does, and the fans that close a prism are two of
218
+ * them. Such a face is exactly tangent to the boxes on both sides, so the plane test compares two
219
+ * quantities that are equal in exact arithmetic and differ by a rounding step in this one. When it
220
+ * falls the wrong way *neither* neighbour is marked, the flood walks straight through the gap, and
221
+ * the whole interior comes back empty.
222
+ *
223
+ * Measured on a closed twenty-four-sided prism of true volume 3.106: the fill answered 3.57, 0.56,
224
+ * 0.47, 3.37, 3.33, 3.31, 0.20 and 0.17 at resolutions 32 through 96 — correct at four of them and
225
+ * the bare shell at the other four, with nothing in the shape to explain which. The escape path
226
+ * ran down the axis and out through the base, whose disc sits exactly on the boundary between two
227
+ * cell rows.
228
+ *
229
+ * **Growing the box is the right direction to be wrong in.** This marking is conservative by
230
+ * design: a cell marked that need not be costs a little accuracy at the surface, and a cell missed
231
+ * that should be marked is a hole. The epsilon is ten orders of magnitude above the rounding it
232
+ * covers and six below a cell, so it can decide a tangency and can decide nothing else.
233
+ */
234
+ const TANGENCY = 1e-6;
235
+ const half = grid.cell * (0.5 + TANGENCY);
236
+ for (let t = 0; t + 2 < indices.length; t += 3) {
237
+ const i0 = indices[t] * 3;
238
+ const i1 = indices[t + 1] * 3;
239
+ const i2 = indices[t + 2] * 3;
240
+ const ax = positions[i0];
241
+ const ay = positions[i0 + 1];
242
+ const az = positions[i0 + 2];
243
+ const bx = positions[i1];
244
+ const by = positions[i1 + 1];
245
+ const bz = positions[i1 + 2];
246
+ const cx = positions[i2];
247
+ const cy = positions[i2 + 1];
248
+ const cz = positions[i2 + 2];
249
+ const loX = cellOf(Math.min(ax, Math.min(bx, cx)), grid.originX, grid.cell, grid.nx);
250
+ const hiX = cellOf(Math.max(ax, Math.max(bx, cx)), grid.originX, grid.cell, grid.nx);
251
+ const loY = cellOf(Math.min(ay, Math.min(by, cy)), grid.originY, grid.cell, grid.ny);
252
+ const hiY = cellOf(Math.max(ay, Math.max(by, cy)), grid.originY, grid.cell, grid.ny);
253
+ const loZ = cellOf(Math.min(az, Math.min(bz, cz)), grid.originZ, grid.cell, grid.nz);
254
+ const hiZ = cellOf(Math.max(az, Math.max(bz, cz)), grid.originZ, grid.cell, grid.nz);
255
+ for (let z = loZ; z <= hiZ; z++) {
256
+ const centreZ = grid.originZ + (z + 0.5) * grid.cell;
257
+ for (let y = loY; y <= hiY; y++) {
258
+ const centreY = grid.originY + (y + 0.5) * grid.cell;
259
+ for (let x = loX; x <= hiX; x++) {
260
+ const index = at(grid, x, y, z);
261
+ if (state[index] === SURFACE)
262
+ continue;
263
+ const centreX = grid.originX + (x + 0.5) * grid.cell;
264
+ if (triangleTouchesBox(ax - centreX, ay - centreY, az - centreZ, bx - centreX, by - centreY, bz - centreZ, cx - centreX, cy - centreY, cz - centreZ, half)) {
265
+ state[index] = SURFACE;
266
+ }
267
+ }
268
+ }
269
+ }
270
+ }
271
+ }
272
+ /** Which cell a coordinate falls in, clamped into the grid. */
273
+ function cellOf(value, origin, cell, count) {
274
+ const raw = Math.floor((value - origin) / cell);
275
+ return Math.min(count - 1, Math.max(0, raw));
276
+ }
277
+ /**
278
+ * The thirteen-axis test, with the triangle already translated so the box is at the origin.
279
+ *
280
+ * `half` is the box's half-extent, the same on all three axes because the cells are cubes.
281
+ */
282
+ function triangleTouchesBox(ax, ay, az, bx, by, bz, cx, cy, cz, half) {
283
+ /* The box's own three axes first: they reject the great majority of candidates for one compare. */
284
+ if (Math.min(ax, Math.min(bx, cx)) > half || Math.max(ax, Math.max(bx, cx)) < -half)
285
+ return false;
286
+ if (Math.min(ay, Math.min(by, cy)) > half || Math.max(ay, Math.max(by, cy)) < -half)
287
+ return false;
288
+ if (Math.min(az, Math.min(bz, cz)) > half || Math.max(az, Math.max(bz, cz)) < -half)
289
+ return false;
290
+ const e0x = bx - ax;
291
+ const e0y = by - ay;
292
+ const e0z = bz - az;
293
+ const e1x = cx - bx;
294
+ const e1y = cy - by;
295
+ const e1z = cz - bz;
296
+ const e2x = ax - cx;
297
+ const e2y = ay - cy;
298
+ const e2z = az - cz;
299
+ /* The triangle's own plane against the box. */
300
+ const nx = e0y * e1z - e0z * e1y;
301
+ const ny = e0z * e1x - e0x * e1z;
302
+ const nz = e0x * e1y - e0y * e1x;
303
+ const d = nx * ax + ny * ay + nz * az;
304
+ const reach = half * (Math.abs(nx) + Math.abs(ny) + Math.abs(nz));
305
+ if (d > reach || d < -reach)
306
+ return false;
307
+ /* And the nine edge-by-axis crosses. */
308
+ if (!axisTest(e0z, -e0y, ay, az, by, bz, cy, cz, half))
309
+ return false;
310
+ if (!axisTest(-e0z, e0x, ax, az, bx, bz, cx, cz, half))
311
+ return false;
312
+ if (!axisTest(e0y, -e0x, ax, ay, bx, by, cx, cy, half))
313
+ return false;
314
+ if (!axisTest(e1z, -e1y, ay, az, by, bz, cy, cz, half))
315
+ return false;
316
+ if (!axisTest(-e1z, e1x, ax, az, bx, bz, cx, cz, half))
317
+ return false;
318
+ if (!axisTest(e1y, -e1x, ax, ay, bx, by, cx, cy, half))
319
+ return false;
320
+ if (!axisTest(e2z, -e2y, ay, az, by, bz, cy, cz, half))
321
+ return false;
322
+ if (!axisTest(-e2z, e2x, ax, az, bx, bz, cx, cz, half))
323
+ return false;
324
+ if (!axisTest(e2y, -e2x, ax, ay, bx, by, cx, cy, half))
325
+ return false;
326
+ return true;
327
+ }
328
+ /**
329
+ * One separating-axis test, in the plane the axis lives in.
330
+ *
331
+ * Every one of the nine crosses has a zero component, so each reduces to a two-dimensional dot
332
+ * product against the two triangle corners that are not on the edge — which is why this takes two
333
+ * multipliers and three pairs of coordinates instead of three vectors.
334
+ */
335
+ function axisTest(u, v, a0, a1, b0, b1, c0, c1, half) {
336
+ const pa = u * a0 + v * a1;
337
+ const pb = u * b0 + v * b1;
338
+ const pc = u * c0 + v * c1;
339
+ const lo = Math.min(pa, Math.min(pb, pc));
340
+ const hi = Math.max(pa, Math.max(pb, pc));
341
+ const reach = half * (Math.abs(u) + Math.abs(v));
342
+ return !(lo > reach || hi < -reach);
343
+ }
344
+ /**
345
+ * Everything the outside can reach, marked `OUTSIDE`; everything else becomes `INSIDE`.
346
+ *
347
+ * A breadth-first flood from every cell of the grid's boundary. The padding `buildGrid` added
348
+ * guarantees that boundary is empty, so the flood always has somewhere to start.
349
+ */
350
+ function fillOutside(grid, state) {
351
+ const visited = new Uint8Array(state.length);
352
+ const queue = new Int32Array(state.length);
353
+ let head = 0;
354
+ let tail = 0;
355
+ const push = (index) => {
356
+ if (visited[index] === 1 || state[index] === SURFACE)
357
+ return;
358
+ visited[index] = 1;
359
+ queue[tail++] = index;
360
+ };
361
+ for (let z = 0; z < grid.nz; z++) {
362
+ for (let y = 0; y < grid.ny; y++) {
363
+ for (let x = 0; x < grid.nx; x++) {
364
+ const onBoundary = x === 0 ||
365
+ y === 0 ||
366
+ z === 0 ||
367
+ x === grid.nx - 1 ||
368
+ y === grid.ny - 1 ||
369
+ z === grid.nz - 1;
370
+ if (onBoundary)
371
+ push(at(grid, x, y, z));
372
+ }
373
+ }
374
+ }
375
+ while (head < tail) {
376
+ const index = queue[head++];
377
+ const x = index % grid.nx;
378
+ const y = Math.floor(index / grid.nx) % grid.ny;
379
+ const z = Math.floor(index / (grid.nx * grid.ny));
380
+ if (x > 0)
381
+ push(index - 1);
382
+ if (x < grid.nx - 1)
383
+ push(index + 1);
384
+ if (y > 0)
385
+ push(index - grid.nx);
386
+ if (y < grid.ny - 1)
387
+ push(index + grid.nx);
388
+ if (z > 0)
389
+ push(index - grid.nx * grid.ny);
390
+ if (z < grid.nz - 1)
391
+ push(index + grid.nx * grid.ny);
392
+ }
393
+ for (let i = 0; i < state.length; i++) {
394
+ if (state[i] === SURFACE)
395
+ continue;
396
+ state[i] = visited[i] === 1 ? OUTSIDE : INSIDE;
397
+ }
398
+ }
399
+ /**
400
+ * Every solid cell claimed by a maximal box of solid cells.
401
+ *
402
+ * **Seeds are exact boxes, and that is what makes the result stable.** The first version of this
403
+ * grew a region by breadth-first search while its own bounding box stayed 95% full, which sounds
404
+ * like the same thing and is not: on a curved wall it produced regions that filled their box well
405
+ * enough to be accepted while wrapping far enough around the ring that the hull over them enclosed
406
+ * the cavity. The consequence was a decomposition that was excellent at one resolution and useless
407
+ * at the next — measured on a twenty-four-sided cup, **0.2% bloat at resolution 64 and 118% at 80**,
408
+ * with nothing between them to explain it. A box is convex, entirely, always, so a seed's hull is
409
+ * the seed and no resolution can make it otherwise. Every instability above came out of the seeding
410
+ * rather than out of the merge that follows it.
411
+ *
412
+ * **Cells are visited in index order, x fastest**, and each unclaimed one starts a box that extends
413
+ * a whole layer at a time in a fixed direction order while every cell of that layer is solid and
414
+ * unclaimed. A layer is accepted or it is not; a partial one is never taken, which is the property
415
+ * that keeps the box solid.
416
+ */
417
+ function growRegions(grid, solid, concavity) {
418
+ void concavity;
419
+ const of = new Int32Array(solid.length).fill(-1);
420
+ const count = [];
421
+ const bounds = [];
422
+ /** Whether every cell of an inclusive box is solid and unclaimed. */
423
+ const layerFree = (x0, y0, z0, x1, y1, z1) => {
424
+ if (x0 < 0 || y0 < 0 || z0 < 0)
425
+ return false;
426
+ if (x1 >= grid.nx || y1 >= grid.ny || z1 >= grid.nz)
427
+ return false;
428
+ for (let z = z0; z <= z1; z++) {
429
+ for (let y = y0; y <= y1; y++) {
430
+ for (let x = x0; x <= x1; x++) {
431
+ const index = at(grid, x, y, z);
432
+ if (solid[index] !== 1 || of[index] !== -1)
433
+ return false;
434
+ }
435
+ }
436
+ }
437
+ return true;
438
+ };
439
+ for (let seed = 0; seed < solid.length; seed++) {
440
+ if (solid[seed] !== 1 || of[seed] !== -1)
441
+ continue;
442
+ const region = count.length;
443
+ let minX = seed % grid.nx;
444
+ let minY = Math.floor(seed / grid.nx) % grid.ny;
445
+ let minZ = Math.floor(seed / (grid.nx * grid.ny));
446
+ let maxX = minX;
447
+ let maxY = minY;
448
+ let maxZ = minZ;
449
+ /*
450
+ * Six directions, in a fixed order, repeated until a whole pass adds nothing. Fixed order and
451
+ * whole layers are together what make two machines produce the same boxes from the same grid.
452
+ */
453
+ for (let growing = true; growing;) {
454
+ growing = false;
455
+ if (layerFree(minX - 1, minY, minZ, minX - 1, maxY, maxZ)) {
456
+ minX--;
457
+ growing = true;
458
+ }
459
+ if (layerFree(maxX + 1, minY, minZ, maxX + 1, maxY, maxZ)) {
460
+ maxX++;
461
+ growing = true;
462
+ }
463
+ if (layerFree(minX, minY - 1, minZ, maxX, minY - 1, maxZ)) {
464
+ minY--;
465
+ growing = true;
466
+ }
467
+ if (layerFree(minX, maxY + 1, minZ, maxX, maxY + 1, maxZ)) {
468
+ maxY++;
469
+ growing = true;
470
+ }
471
+ if (layerFree(minX, minY, minZ - 1, maxX, maxY, minZ - 1)) {
472
+ minZ--;
473
+ growing = true;
474
+ }
475
+ if (layerFree(minX, minY, maxZ + 1, maxX, maxY, maxZ + 1)) {
476
+ maxZ++;
477
+ growing = true;
478
+ }
479
+ }
480
+ for (let z = minZ; z <= maxZ; z++) {
481
+ for (let y = minY; y <= maxY; y++) {
482
+ for (let x = minX; x <= maxX; x++)
483
+ of[at(grid, x, y, z)] = region;
484
+ }
485
+ }
486
+ count.push((maxX - minX + 1) * (maxY - minY + 1) * (maxZ - minZ + 1));
487
+ bounds.push(minX, minY, minZ, maxX, maxY, maxZ);
488
+ }
489
+ const total = count.length;
490
+ const directions = SUPPORT_DIRECTIONS;
491
+ const supports = new Float64Array(total * directions * 3);
492
+ const bestDot = new Float64Array(total * directions).fill(-Infinity);
493
+ for (let region = 0; region < total; region++) {
494
+ const b = region * 6;
495
+ for (let corner = 0; corner < 8; corner++) {
496
+ const cx = bounds[b + (corner & 1 ? 3 : 0)] + (corner & 1 ? 1 : 0);
497
+ const cy = bounds[b + (corner & 2 ? 4 : 1)] + (corner & 2 ? 1 : 0);
498
+ const cz = bounds[b + (corner & 4 ? 5 : 2)] + (corner & 4 ? 1 : 0);
499
+ const px = grid.originX + cx * grid.cell;
500
+ const py = grid.originY + cy * grid.cell;
501
+ const pz = grid.originZ + cz * grid.cell;
502
+ offerSupport(supports, bestDot, region, px, py, pz);
503
+ }
504
+ }
505
+ const regions = {
506
+ of,
507
+ count: Int32Array.from(count),
508
+ bounds: Int32Array.from(bounds),
509
+ parent: new Int32Array(total).map((_, i) => i),
510
+ neighbours: [],
511
+ supports,
512
+ bestDot,
513
+ live: total,
514
+ total,
515
+ };
516
+ for (let i = 0; i < total; i++)
517
+ regions.neighbours.push(new Set());
518
+ /*
519
+ * Adjacency, from one walk over the grid looking only forward. Looking both ways would find every
520
+ * pair twice and the set would answer the same, so the walk is halved.
521
+ */
522
+ for (let z = 0; z < grid.nz; z++) {
523
+ for (let y = 0; y < grid.ny; y++) {
524
+ for (let x = 0; x < grid.nx; x++) {
525
+ const index = at(grid, x, y, z);
526
+ const a = of[index];
527
+ if (a < 0)
528
+ continue;
529
+ if (x + 1 < grid.nx)
530
+ link(regions, a, of[index + 1]);
531
+ if (y + 1 < grid.ny)
532
+ link(regions, a, of[index + grid.nx]);
533
+ if (z + 1 < grid.nz)
534
+ link(regions, a, of[index + grid.nx * grid.ny]);
535
+ }
536
+ }
537
+ }
538
+ return regions;
539
+ }
540
+ /** Keep a point if it reaches further along any direction than what the region already had. */
541
+ function offerSupport(supports, bestDot, region, x, y, z) {
542
+ const directions = SUPPORT_DIRECTIONS;
543
+ for (let d = 0; d < directions; d++) {
544
+ const dot = DIRECTIONS[d * 3] * x +
545
+ DIRECTIONS[d * 3 + 1] * y +
546
+ DIRECTIONS[d * 3 + 2] * z;
547
+ const k = region * directions + d;
548
+ if (dot <= bestDot[k])
549
+ continue;
550
+ bestDot[k] = dot;
551
+ supports[k * 3] = x;
552
+ supports[k * 3 + 1] = y;
553
+ supports[k * 3 + 2] = z;
554
+ }
555
+ }
556
+ function link(regions, a, b) {
557
+ if (b < 0 || a === b)
558
+ return;
559
+ regions.neighbours[a].add(b);
560
+ regions.neighbours[b].add(a);
561
+ }
562
+ function find(regions, id) {
563
+ let root = id;
564
+ while (regions.parent[root] !== root)
565
+ root = regions.parent[root];
566
+ let walk = id;
567
+ while (regions.parent[walk] !== walk) {
568
+ const next = regions.parent[walk];
569
+ regions.parent[walk] = root;
570
+ walk = next;
571
+ }
572
+ return root;
573
+ }
574
+ /**
575
+ * Neighbouring regions merged until the count is one a body can hold, then while it is free.
576
+ *
577
+ * **Two phases, measured two ways, and the split is the whole design.** Reaching the part budget
578
+ * takes as many merges as there are regions, which is thousands: those are ordered by the cheap box
579
+ * measure, which is good at saying *which* merge is least bad and useless at saying whether any
580
+ * merge is worth making. Going below the budget takes at most a handful, and each of those asks the
581
+ * real question — how much of the merged hull would not be solid — because that is the only measure
582
+ * that separates a convex solid, where merging costs nothing, from a cavity, where it costs the
583
+ * cavity.
584
+ *
585
+ * **The tie-break is the determinism.** Two candidates with the same cost are ordered by their lower
586
+ * region id and then their higher one. Without it a heap orders equal costs by insertion, and two
587
+ * runs that inserted in a different order decompose the same mesh differently.
588
+ */
589
+ function mergeRegions(regions, maxHulls, concavity, cellVolume) {
590
+ /*
591
+ * **Is the whole thing convex? Asked once, before anything is merged, because the pairwise merge
592
+ * below cannot answer it.** Greedy merging judges one pair at a time, and on a slanted solid every
593
+ * individual step looks bad while the end state is free: two boxes of a staircase have a notch
594
+ * between them, so their hull is a fifth empty and the merge is refused — even where merging *all*
595
+ * of them gives back the solid exactly.
596
+ *
597
+ * Measured on a tetrahedron, which is as convex as a shape can be: **sixteen parts and 20.3%
598
+ * bloat**, where the answer is one part and nothing. A crate, a wedge, a rock and a wheel are all
599
+ * this case, and it is much the most common thing a bake will meet.
600
+ *
601
+ * One hull over every region's supports answers it for the cost of a single `hullShape`.
602
+ */
603
+ if (regions.live > 1 && wholeConcavity(regions, cellVolume) <= concavity) {
604
+ for (let i = 1; i < regions.total; i++) {
605
+ const a = find(regions, 0);
606
+ const b = find(regions, i);
607
+ if (a !== b)
608
+ merge(regions, a, b);
609
+ }
610
+ return;
611
+ }
612
+ const heap = new CandidateHeap();
613
+ for (let a = 0; a < regions.total; a++) {
614
+ for (const b of regions.neighbours[a]) {
615
+ if (b > a)
616
+ heap.push(cost(regions, a, b), a, b);
617
+ }
618
+ }
619
+ while (regions.live > maxHulls) {
620
+ const top = heap.pop();
621
+ if (top === null)
622
+ break;
623
+ const a = find(regions, top.a);
624
+ const b = find(regions, top.b);
625
+ if (a === b)
626
+ continue;
627
+ /* A stale entry: one of the two has merged since, so its cost is no longer the pair's. */
628
+ const current = cost(regions, a, b);
629
+ if (current !== top.cost) {
630
+ heap.push(current, a, b);
631
+ continue;
632
+ }
633
+ merge(regions, a, b);
634
+ const root = find(regions, a);
635
+ for (const other of regions.neighbours[root]) {
636
+ heap.push(cost(regions, root, other), Math.min(root, other), Math.max(root, other));
637
+ }
638
+ }
639
+ /*
640
+ * The tail. At most `maxHulls` regions remain, so every adjacent pair can be measured properly and
641
+ * the answers cached; a merge invalidates only the pairs its region is part of.
642
+ */
643
+ const measured = new Map();
644
+ const keyOf = (a, b) => Math.min(a, b) * (regions.total + 1) + Math.max(a, b);
645
+ for (;;) {
646
+ let bestCost = Infinity;
647
+ let bestA = -1;
648
+ let bestB = -1;
649
+ for (let i = 0; i < regions.total; i++) {
650
+ const a = find(regions, i);
651
+ if (a !== i)
652
+ continue;
653
+ for (const raw of regions.neighbours[a]) {
654
+ const b = find(regions, raw);
655
+ if (b === a)
656
+ continue;
657
+ const lo = Math.min(a, b);
658
+ const hi = Math.max(a, b);
659
+ const key = keyOf(lo, hi);
660
+ let value = measured.get(key);
661
+ if (value === undefined) {
662
+ value = hullConcavity(regions, lo, hi, cellVolume);
663
+ measured.set(key, value);
664
+ }
665
+ /* Ties resolve by the pair's ids, for the reason the heap has a tie-break. */
666
+ if (value < bestCost ||
667
+ (value === bestCost && (lo < bestA || (lo === bestA && hi < bestB)))) {
668
+ bestCost = value;
669
+ bestA = lo;
670
+ bestB = hi;
671
+ }
672
+ }
673
+ }
674
+ if (bestA < 0 || bestCost > concavity)
675
+ break;
676
+ merge(regions, bestA, bestB);
677
+ if (regions.live <= 1)
678
+ break;
679
+ const root = find(regions, bestA);
680
+ for (const key of [...measured.keys()]) {
681
+ const lo = Math.floor(key / (regions.total + 1));
682
+ const hi = key % (regions.total + 1);
683
+ if (find(regions, lo) === root || find(regions, hi) === root)
684
+ measured.delete(key);
685
+ }
686
+ }
687
+ }
688
+ /**
689
+ * The fraction of the hull over two regions that would not be solid.
690
+ *
691
+ * **The real measure, and the reason the support sets exist.** A region is a union of boxes, so its
692
+ * extreme point along any direction is a corner of one of them, and the hull over those points is
693
+ * the hull over the region. Comparing that hull's volume against the cells actually held says
694
+ * exactly what merging would cost: nothing for two halves of a cylinder, and the whole cavity for
695
+ * two walls facing each other across one.
696
+ *
697
+ * Measured at 1.09 ms for a fifty-point hull, which is why `mergeRegions` asks it about a hundred
698
+ * and fifty times and not a hundred and fifty thousand.
699
+ */
700
+ function hullConcavity(regions, a, b, cellVolume) {
701
+ const directions = SUPPORT_DIRECTIONS;
702
+ const points = new Float32Array(directions * 3);
703
+ for (let d = 0; d < directions; d++) {
704
+ const ka = a * directions + d;
705
+ const kb = b * directions + d;
706
+ const from = regions.bestDot[kb] > regions.bestDot[ka] ? kb : ka;
707
+ points[d * 3] = regions.supports[from * 3];
708
+ points[d * 3 + 1] = regions.supports[from * 3 + 1];
709
+ points[d * 3 + 2] = regions.supports[from * 3 + 2];
710
+ }
711
+ const volume = shapeMassProperties(hullShape(points), 1, CONCAVITY_MASS).volume;
712
+ if (!(volume > 0))
713
+ return 1;
714
+ const solid = (regions.count[a] + regions.count[b]) * cellVolume;
715
+ return Math.max(0, (volume - solid) / volume);
716
+ }
717
+ /** The same question asked of every live region at once: is this solid already convex? */
718
+ function wholeConcavity(regions, cellVolume) {
719
+ const directions = SUPPORT_DIRECTIONS;
720
+ const points = new Float32Array(directions * 3);
721
+ const best = new Float64Array(directions).fill(-Infinity);
722
+ let cells = 0;
723
+ for (let i = 0; i < regions.total; i++) {
724
+ if (find(regions, i) !== i)
725
+ continue;
726
+ cells += regions.count[i];
727
+ for (let d = 0; d < directions; d++) {
728
+ const k = i * directions + d;
729
+ if (regions.bestDot[k] <= best[d])
730
+ continue;
731
+ best[d] = regions.bestDot[k];
732
+ points[d * 3] = regions.supports[k * 3];
733
+ points[d * 3 + 1] = regions.supports[k * 3 + 1];
734
+ points[d * 3 + 2] = regions.supports[k * 3 + 2];
735
+ }
736
+ }
737
+ const volume = shapeMassProperties(hullShape(points), 1, CONCAVITY_MASS).volume;
738
+ if (!(volume > 0))
739
+ return 1;
740
+ return Math.max(0, (volume - cells * cellVolume) / volume);
741
+ }
742
+ /** Reused by `hullConcavity`, which the tail calls once per candidate pair. */
743
+ const CONCAVITY_MASS = createMassProperties();
744
+ /**
745
+ * How empty the box around two regions would be if they became one.
746
+ *
747
+ * **The cheap half of a two-part rule, and it only ever orders merges the budget forces.** Six
748
+ * integer compares and a divide, against a hull whose volume costs a millisecond, so this is what
749
+ * can be asked thousands of times while a mesh's regions come down to the part budget.
750
+ *
751
+ * **What it cannot answer is whether a merge is *free*, and three attempts to make it were wrong in
752
+ * three different ways.** A fixed threshold on this number can never join the last two halves of a
753
+ * cylinder, whose box is 21.5% empty however it is cut — a convex prism came back as eight parts.
754
+ * Scoring the change against the emptier parent fixes that and then runs away: each merge licenses
755
+ * the next against a laxer number, and a cup's wall merged around its own ring until the vessel was
756
+ * one hull holding 57% more than the cup. Anchoring the allowance so it cannot rise stops the
757
+ * runaway and brings back the eight-part prism. **A box cannot tell a convex solid from a cavity**,
758
+ * and that is not a rule to be tuned into existence. `hullConcavity` is what answers it, and
759
+ * `mergeRegions` calls it only where the count is small enough to afford one.
760
+ */
761
+ function cost(regions, a, b) {
762
+ const ab = a * 6;
763
+ const bb = b * 6;
764
+ const minX = Math.min(regions.bounds[ab], regions.bounds[bb]);
765
+ const minY = Math.min(regions.bounds[ab + 1], regions.bounds[bb + 1]);
766
+ const minZ = Math.min(regions.bounds[ab + 2], regions.bounds[bb + 2]);
767
+ const maxX = Math.max(regions.bounds[ab + 3], regions.bounds[bb + 3]);
768
+ const maxY = Math.max(regions.bounds[ab + 4], regions.bounds[bb + 4]);
769
+ const maxZ = Math.max(regions.bounds[ab + 5], regions.bounds[bb + 5]);
770
+ const union = (maxX - minX + 1) * (maxY - minY + 1) * (maxZ - minZ + 1);
771
+ const taken = regions.count[a] + regions.count[b];
772
+ return (union - taken) / union;
773
+ }
774
+ function merge(regions, a, b) {
775
+ /* The lower id survives, so a decomposition's part order follows the grid walk that made it. */
776
+ const keep = Math.min(a, b);
777
+ const gone = Math.max(a, b);
778
+ regions.parent[gone] = keep;
779
+ regions.count[keep] = regions.count[keep] + regions.count[gone];
780
+ const kb = keep * 6;
781
+ const gb = gone * 6;
782
+ for (let i = 0; i < 3; i++) {
783
+ regions.bounds[kb + i] = Math.min(regions.bounds[kb + i], regions.bounds[gb + i]);
784
+ regions.bounds[kb + 3 + i] = Math.max(regions.bounds[kb + 3 + i], regions.bounds[gb + 3 + i]);
785
+ }
786
+ const directions = SUPPORT_DIRECTIONS;
787
+ for (let d = 0; d < directions; d++) {
788
+ const kk = keep * directions + d;
789
+ const kg = gone * directions + d;
790
+ if (regions.bestDot[kg] <= regions.bestDot[kk])
791
+ continue;
792
+ regions.bestDot[kk] = regions.bestDot[kg];
793
+ regions.supports[kk * 3] = regions.supports[kg * 3];
794
+ regions.supports[kk * 3 + 1] = regions.supports[kg * 3 + 1];
795
+ regions.supports[kk * 3 + 2] = regions.supports[kg * 3 + 2];
796
+ }
797
+ const into = regions.neighbours[keep];
798
+ for (const other of regions.neighbours[gone]) {
799
+ const root = find(regions, other);
800
+ if (root !== keep)
801
+ into.add(root);
802
+ }
803
+ into.delete(gone);
804
+ into.delete(keep);
805
+ regions.neighbours[gone] = new Set();
806
+ regions.live--;
807
+ }
808
+ /** A candidate merge, in a binary heap ordered by cost then by the pair's two ids. */
809
+ class CandidateHeap {
810
+ costs = [];
811
+ as = [];
812
+ bs = [];
813
+ push(cost, a, b) {
814
+ this.costs.push(cost);
815
+ this.as.push(a);
816
+ this.bs.push(b);
817
+ let i = this.costs.length - 1;
818
+ while (i > 0) {
819
+ const parent = (i - 1) >> 1;
820
+ if (!this.less(i, parent))
821
+ break;
822
+ this.swap(i, parent);
823
+ i = parent;
824
+ }
825
+ }
826
+ pop() {
827
+ if (this.costs.length === 0)
828
+ return null;
829
+ const top = { cost: this.costs[0], a: this.as[0], b: this.bs[0] };
830
+ const last = this.costs.length - 1;
831
+ this.swap(0, last);
832
+ this.costs.pop();
833
+ this.as.pop();
834
+ this.bs.pop();
835
+ let i = 0;
836
+ for (;;) {
837
+ const left = i * 2 + 1;
838
+ const right = left + 1;
839
+ let best = i;
840
+ if (left < this.costs.length && this.less(left, best))
841
+ best = left;
842
+ if (right < this.costs.length && this.less(right, best))
843
+ best = right;
844
+ if (best === i)
845
+ break;
846
+ this.swap(i, best);
847
+ i = best;
848
+ }
849
+ return top;
850
+ }
851
+ less(i, j) {
852
+ const ci = this.costs[i];
853
+ const cj = this.costs[j];
854
+ if (ci !== cj)
855
+ return ci < cj;
856
+ const ai = this.as[i];
857
+ const aj = this.as[j];
858
+ if (ai !== aj)
859
+ return ai < aj;
860
+ return this.bs[i] < this.bs[j];
861
+ }
862
+ swap(i, j) {
863
+ const c = this.costs[i];
864
+ this.costs[i] = this.costs[j];
865
+ this.costs[j] = c;
866
+ const a = this.as[i];
867
+ this.as[i] = this.as[j];
868
+ this.as[j] = a;
869
+ const b = this.bs[i];
870
+ this.bs[i] = this.bs[j];
871
+ this.bs[j] = b;
872
+ }
873
+ }
874
+ /**
875
+ * Each live region's extreme corners along `DIRECTIONS`, and the hull over them.
876
+ *
877
+ * One walk over the grid. A solid cell offers its eight corners to its region's support set, which
878
+ * keeps whichever reach furthest along each direction; the set is then deduplicated by `hullShape`
879
+ * itself, so a boxy region arrives as its eight corners and a rounded one as up to fifty.
880
+ *
881
+ * **Coverage is measured here rather than assumed**, by asking whether each cell's centre lies
882
+ * inside the hull its own region produced. A support set is inscribed in the true hull of the
883
+ * corners it was drawn from, so a cell at a corner no direction points at can fall outside — and
884
+ * that is the one way this function loses material instead of adding it.
885
+ */
886
+ function buildParts(grid, solid, regions) {
887
+ const directions = SUPPORT_DIRECTIONS;
888
+ const order = [];
889
+ const slotOf = new Map();
890
+ for (let i = 0; i < regions.total; i++) {
891
+ const root = find(regions, i);
892
+ if (slotOf.has(root))
893
+ continue;
894
+ slotOf.set(root, order.length);
895
+ order.push(root);
896
+ }
897
+ const best = new Float64Array(order.length * directions).fill(-Infinity);
898
+ const point = new Float64Array(order.length * directions * 3);
899
+ for (let z = 0; z < grid.nz; z++) {
900
+ for (let y = 0; y < grid.ny; y++) {
901
+ for (let x = 0; x < grid.nx; x++) {
902
+ const index = at(grid, x, y, z);
903
+ if (solid[index] !== 1)
904
+ continue;
905
+ const slot = slotOf.get(find(regions, regions.of[index]));
906
+ for (let corner = 0; corner < 8; corner++) {
907
+ const cxCell = x + (corner & 1);
908
+ const cyCell = y + (corner & 2 ? 1 : 0);
909
+ const czCell = z + (corner & 4 ? 1 : 0);
910
+ const px = grid.originX + cxCell * grid.cell;
911
+ const py = grid.originY + cyCell * grid.cell;
912
+ const pz = grid.originZ + czCell * grid.cell;
913
+ for (let d = 0; d < directions; d++) {
914
+ const dot = DIRECTIONS[d * 3] * px +
915
+ DIRECTIONS[d * 3 + 1] * py +
916
+ DIRECTIONS[d * 3 + 2] * pz;
917
+ const k = slot * directions + d;
918
+ if (dot <= best[k])
919
+ continue;
920
+ best[k] = dot;
921
+ point[k * 3] = px;
922
+ point[k * 3 + 1] = py;
923
+ point[k * 3 + 2] = pz;
924
+ }
925
+ }
926
+ }
927
+ }
928
+ }
929
+ const cellVolume = grid.cell * grid.cell * grid.cell;
930
+ /* A plane test wants a tolerance in the units the planes are in, which is the grid's own. */
931
+ const slack = grid.cell * 1e-3;
932
+ const mass = createMassProperties();
933
+ const parts = [];
934
+ const shapes = [];
935
+ for (let slot = 0; slot < order.length; slot++) {
936
+ const points = new Float32Array(directions * 3);
937
+ for (let d = 0; d < directions; d++) {
938
+ const k = (slot * directions + d) * 3;
939
+ points[d * 3] = point[k];
940
+ points[d * 3 + 1] = point[k + 1];
941
+ points[d * 3 + 2] = point[k + 2];
942
+ }
943
+ const shape = hullShape(points);
944
+ shapes.push(shape);
945
+ const volume = shapeMassProperties(shape, 1, mass).volume;
946
+ const cells = regions.count[order[slot]];
947
+ parts.push({ points: shape.vertices, volume, fill: (cells * cellVolume) / volume });
948
+ }
949
+ let covered = 0;
950
+ for (let z = 0; z < grid.nz; z++) {
951
+ for (let y = 0; y < grid.ny; y++) {
952
+ for (let x = 0; x < grid.nx; x++) {
953
+ const index = at(grid, x, y, z);
954
+ if (solid[index] !== 1)
955
+ continue;
956
+ const slot = slotOf.get(find(regions, regions.of[index]));
957
+ const px = grid.originX + (x + 0.5) * grid.cell;
958
+ const py = grid.originY + (y + 0.5) * grid.cell;
959
+ const pz = grid.originZ + (z + 0.5) * grid.cell;
960
+ if (insideHull(shapes[slot], px, py, pz, slack))
961
+ covered++;
962
+ }
963
+ }
964
+ }
965
+ return { parts, covered };
966
+ }
967
+ /** Whether a point is on the inner side of every one of a hull's face planes. */
968
+ function insideHull(shape, x, y, z, slack) {
969
+ const planes = shape.facePlanes;
970
+ for (let i = 0; i + 3 < planes.length; i += 4) {
971
+ const d = planes[i] * x +
972
+ planes[i + 1] * y +
973
+ planes[i + 2] * z -
974
+ planes[i + 3];
975
+ if (d > slack)
976
+ return false;
977
+ }
978
+ return true;
979
+ }