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