@series-inc/rundot-syncplay 5.23.0 → 5.24.0-beta.1

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 (131) hide show
  1. package/README.md +53 -12
  2. package/dist/authority-room.d.ts +11 -0
  3. package/dist/authority-room.js +43 -0
  4. package/dist/browser.d.ts +11 -1
  5. package/dist/browser.js +5 -0
  6. package/dist/checksum-basis.d.ts +4 -0
  7. package/dist/checksum-basis.js +17 -0
  8. package/dist/cjs/authority-room.js +43 -0
  9. package/dist/cjs/browser.js +18 -2
  10. package/dist/cjs/checksum-basis.js +22 -0
  11. package/dist/cjs/collider-cooking.js +834 -0
  12. package/dist/cjs/deterministic-session.js +2 -0
  13. package/dist/cjs/engine-complete.js +1 -4
  14. package/dist/cjs/engine-parity-matrix.js +41 -9
  15. package/dist/cjs/engine-parity.js +2 -2
  16. package/dist/cjs/index.js +24 -2
  17. package/dist/cjs/input-authority.js +34 -2
  18. package/dist/cjs/instant-replay.js +4 -9
  19. package/dist/cjs/local-authority-server-runner.js +3 -1
  20. package/dist/cjs/local-authority-wire.js +20 -1
  21. package/dist/cjs/match-log.js +27 -0
  22. package/dist/cjs/movement3d.js +17 -1
  23. package/dist/cjs/networked-client.js +70 -20
  24. package/dist/cjs/node.js +23 -1
  25. package/dist/cjs/offline-session.js +52 -24
  26. package/dist/cjs/physics-cert.js +8 -3
  27. package/dist/cjs/physics3d-heightfield.js +51 -0
  28. package/dist/cjs/physics3d-hull.js +381 -0
  29. package/dist/cjs/physics3d-joints.js +255 -49
  30. package/dist/cjs/physics3d-mass.js +131 -0
  31. package/dist/cjs/physics3d-recipes.js +197 -0
  32. package/dist/cjs/physics3d-shared.js +11 -1
  33. package/dist/cjs/physics3d-solver.js +845 -98
  34. package/dist/cjs/physics3d-stacking.js +154 -0
  35. package/dist/cjs/physics3d-toi.js +274 -0
  36. package/dist/cjs/physics3d-vehicle.js +35 -10
  37. package/dist/cjs/physics3d.js +601 -38
  38. package/dist/cjs/replay.js +25 -1
  39. package/dist/cjs/runtime-support.js +71 -0
  40. package/dist/cjs/sdk-offline.js +9 -1
  41. package/dist/cjs/sdk-session.js +8 -0
  42. package/dist/cjs/secret-authority.js +647 -0
  43. package/dist/cjs/secret-client.js +615 -0
  44. package/dist/cjs/secret-command-classification.js +8 -0
  45. package/dist/cjs/secret-config.js +211 -0
  46. package/dist/cjs/secret-crypto.js +208 -0
  47. package/dist/cjs/secret-protocol.js +238 -0
  48. package/dist/cjs/session-build.js +7 -4
  49. package/dist/cjs/session-promotion.js +5 -5
  50. package/dist/cjs/synctest.js +49 -0
  51. package/dist/cjs/testing.js +4 -1
  52. package/dist/cjs/tools/static-checker.js +61 -13
  53. package/dist/cli.js +193 -48
  54. package/dist/collider-cooking.d.ts +141 -0
  55. package/dist/collider-cooking.js +829 -0
  56. package/dist/deterministic-primitives.d.ts +1 -0
  57. package/dist/deterministic-session.js +2 -0
  58. package/dist/engine-complete.js +1 -4
  59. package/dist/engine-parity-matrix.js +41 -9
  60. package/dist/engine-parity.js +2 -2
  61. package/dist/index.d.ts +12 -1
  62. package/dist/index.js +6 -0
  63. package/dist/input-authority.d.ts +3 -0
  64. package/dist/input-authority.js +35 -3
  65. package/dist/instant-replay.d.ts +2 -2
  66. package/dist/instant-replay.js +4 -9
  67. package/dist/local-authority-server-runner.d.ts +1 -0
  68. package/dist/local-authority-server-runner.js +2 -1
  69. package/dist/local-authority-wire.js +20 -1
  70. package/dist/match-log.d.ts +4 -1
  71. package/dist/match-log.js +27 -0
  72. package/dist/movement3d.js +18 -2
  73. package/dist/networked-client.d.ts +8 -0
  74. package/dist/networked-client.js +70 -20
  75. package/dist/node.d.ts +7 -0
  76. package/dist/node.js +6 -0
  77. package/dist/offline-session.js +53 -25
  78. package/dist/physics-cert.js +8 -3
  79. package/dist/physics3d-heightfield.d.ts +17 -0
  80. package/dist/physics3d-heightfield.js +47 -0
  81. package/dist/physics3d-hull.d.ts +53 -0
  82. package/dist/physics3d-hull.js +376 -0
  83. package/dist/physics3d-joints.d.ts +13 -1
  84. package/dist/physics3d-joints.js +256 -50
  85. package/dist/physics3d-mass.d.ts +11 -0
  86. package/dist/physics3d-mass.js +126 -0
  87. package/dist/physics3d-recipes.d.ts +29 -0
  88. package/dist/physics3d-recipes.js +193 -0
  89. package/dist/physics3d-shared.d.ts +1 -0
  90. package/dist/physics3d-shared.js +10 -1
  91. package/dist/physics3d-solver.d.ts +78 -2
  92. package/dist/physics3d-solver.js +842 -98
  93. package/dist/physics3d-stacking.d.ts +26 -0
  94. package/dist/physics3d-stacking.js +147 -0
  95. package/dist/physics3d-toi.d.ts +79 -0
  96. package/dist/physics3d-toi.js +267 -0
  97. package/dist/physics3d-vehicle.js +35 -10
  98. package/dist/physics3d.d.ts +28 -3
  99. package/dist/physics3d.js +601 -40
  100. package/dist/replay.d.ts +5 -0
  101. package/dist/replay.js +25 -1
  102. package/dist/reports.d.ts +6 -1
  103. package/dist/runtime-support.d.ts +60 -0
  104. package/dist/runtime-support.js +66 -0
  105. package/dist/sdk-offline.d.ts +4 -0
  106. package/dist/sdk-offline.js +9 -1
  107. package/dist/sdk-session.d.ts +4 -0
  108. package/dist/sdk-session.js +8 -0
  109. package/dist/secret-authority.d.ts +90 -0
  110. package/dist/secret-authority.js +643 -0
  111. package/dist/secret-client.d.ts +89 -0
  112. package/dist/secret-client.js +611 -0
  113. package/dist/secret-command-classification.d.ts +1 -0
  114. package/dist/secret-command-classification.js +5 -0
  115. package/dist/secret-config.d.ts +45 -0
  116. package/dist/secret-config.js +206 -0
  117. package/dist/secret-crypto.d.ts +29 -0
  118. package/dist/secret-crypto.js +190 -0
  119. package/dist/secret-protocol.d.ts +181 -0
  120. package/dist/secret-protocol.js +230 -0
  121. package/dist/session-build.d.ts +16 -0
  122. package/dist/session-build.js +7 -4
  123. package/dist/session-promotion.js +5 -5
  124. package/dist/synctest.d.ts +1 -1
  125. package/dist/synctest.js +49 -0
  126. package/dist/testing.d.ts +2 -0
  127. package/dist/testing.js +1 -0
  128. package/dist/tools/static-checker.d.ts +11 -0
  129. package/dist/tools/static-checker.js +60 -13
  130. package/dist/types.d.ts +21 -2
  131. package/package.json +8 -3
@@ -0,0 +1,834 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.__colliderCookingInternals = void 0;
4
+ exports.cookConvexDecomposition = cookConvexDecomposition;
5
+ exports.assertCookedColliderIsRuntimeValid = assertCookedColliderIsRuntimeValid;
6
+ const canonical_js_1 = require("./canonical.js");
7
+ const physics3d_hull_js_1 = require("./physics3d-hull.js");
8
+ /**
9
+ * Offline convex decomposition (V-HACD shaped): turn a concave triangle-soup
10
+ * collider into a `compound` of convex `hull` children the deterministic runtime
11
+ * can collide.
12
+ *
13
+ * This module is BUILD-TIME ONLY. It never runs inside a simulation step, so it
14
+ * is free to be expensive; what ships is its output — a fixed set of hull
15
+ * vertices — and *that* is what the runtime must reproduce bit-for-bit. The
16
+ * cook is nonetheless made reproducible on purpose: the source is canonicalised
17
+ * before any work happens, every interior computation runs on an integer voxel
18
+ * lattice (so the convex-hull orientation predicate is exact, never an epsilon
19
+ * vote), and every tie-break is lexicographic. Cooking the same mesh twice — or
20
+ * a permutation of its triangles — yields byte-identical output.
21
+ *
22
+ * Pipeline: canonicalise → voxelize (surface rasterisation + exterior flood
23
+ * fill) → recursively split the voxel set on the axis-aligned plane that best
24
+ * reduces concavity, worst part first, until every part is convex enough or the
25
+ * hull budget is spent → hull each part, reduce it to the runtime's vertex cap
26
+ * while inflating it to still contain the part, and quantize.
27
+ */
28
+ /** Runtime hard cap: `buildSolverHull` accepts 4..16 vertices. */
29
+ const RUNTIME_MAX_HULL_VERTICES = 16;
30
+ const RUNTIME_MIN_HULL_VERTICES = 4;
31
+ /**
32
+ * Lowest cap a cook can honour: the voxel-AABB fallback is an 8-corner box, so
33
+ * asking for fewer would make a fallback unsatisfiable.
34
+ */
35
+ const MIN_COOKED_HULL_VERTICES = 8;
36
+ const DEFAULTS = {
37
+ resolution: 24,
38
+ maxHulls: 8,
39
+ maxHullVertices: RUNTIME_MAX_HULL_VERTICES,
40
+ concavityTolerance: 0.05,
41
+ quantum: 1e-4,
42
+ splitCandidates: 7,
43
+ };
44
+ // ---------- entry point ----------
45
+ function cookConvexDecomposition(source, options = {}) {
46
+ const resolution = requireIntegerInRange(options.resolution ?? DEFAULTS.resolution, 4, 96, 'resolution');
47
+ const maxHulls = requireIntegerInRange(options.maxHulls ?? DEFAULTS.maxHulls, 1, 64, 'maxHulls');
48
+ const maxHullVertices = requireIntegerInRange(options.maxHullVertices ?? DEFAULTS.maxHullVertices, MIN_COOKED_HULL_VERTICES, RUNTIME_MAX_HULL_VERTICES, 'maxHullVertices');
49
+ const splitCandidates = requireIntegerInRange(options.splitCandidates ?? DEFAULTS.splitCandidates, 1, 32, 'splitCandidates');
50
+ const concavityTolerance = options.concavityTolerance ?? DEFAULTS.concavityTolerance;
51
+ if (!(concavityTolerance >= 0 && concavityTolerance < 1)) {
52
+ throw new Error('cookConvexDecomposition: concavityTolerance must be in [0, 1)');
53
+ }
54
+ const quantum = options.quantum ?? DEFAULTS.quantum;
55
+ if (!(quantum > 0)) {
56
+ throw new Error('cookConvexDecomposition: quantum must be positive');
57
+ }
58
+ const triangles = canonicalTriangles(source.vertices);
59
+ const lattice = voxelize(triangles, resolution);
60
+ if (quantum > lattice.voxelSize / 8) {
61
+ throw new Error(`cookConvexDecomposition: quantum ${quantum} is too coarse for voxel size ${lattice.voxelSize} `
62
+ + '— it would collapse hull faces; use a finer quantum or a lower resolution');
63
+ }
64
+ const parts = decompose(lattice, maxHulls, concavityTolerance, splitCandidates);
65
+ const children = [];
66
+ const hulls = [];
67
+ let totalVolume = 0;
68
+ let weightedConcavity = 0;
69
+ for (const part of parts) {
70
+ const vertices = finalizeHull(part, lattice, maxHullVertices, quantum);
71
+ const centroid = quantizePoint(meanPoint(vertices), quantum);
72
+ children.push({
73
+ shape: { type: 'hull', vertices: vertices.map((v) => quantizePoint(subtract(v, centroid), quantum)) },
74
+ offset: centroid,
75
+ });
76
+ hulls.push({ vertexCount: vertices.length, concavity: part.concavity });
77
+ totalVolume += part.voxels.length;
78
+ weightedConcavity += part.concavity * part.voxels.length;
79
+ }
80
+ const shape = { type: 'compound', children };
81
+ return {
82
+ schemaVersion: 1,
83
+ kind: 'deterministic-convex-decomposition',
84
+ shape,
85
+ hulls,
86
+ sourceTriangles: triangles.length,
87
+ resolution,
88
+ /* istanbul ignore next -- voxelize throws on an empty volume, so `parts` always covers at least one voxel. */
89
+ concavity: totalVolume === 0 ? 0 : weightedConcavity / totalVolume,
90
+ hash: (0, canonical_js_1.defaultChecksum)(shape),
91
+ };
92
+ }
93
+ /**
94
+ * Re-run every runtime guard the cooked collider will meet at world creation,
95
+ * so a bad cook fails in the asset pipeline rather than in a game.
96
+ */
97
+ function assertCookedColliderIsRuntimeValid(cooked) {
98
+ if (cooked.shape.children.length === 0) {
99
+ throw new Error('cooked collider: compound must have at least one child');
100
+ }
101
+ cooked.shape.children.forEach((child, index) => {
102
+ if (child.shape.type !== 'hull') {
103
+ throw new Error(`cooked collider: child ${index} must be a hull`);
104
+ }
105
+ try {
106
+ (0, physics3d_hull_js_1.buildSolverHull)(child.shape.vertices);
107
+ }
108
+ catch (error) {
109
+ throw new Error(`cooked collider: child ${index} is not a valid runtime hull — ${error.message}`);
110
+ }
111
+ });
112
+ if (cooked.hash !== (0, canonical_js_1.defaultChecksum)(cooked.shape)) {
113
+ throw new Error('cooked collider: hash does not match its shape');
114
+ }
115
+ }
116
+ // ---------- canonicalisation ----------
117
+ function canonicalTriangles(vertices) {
118
+ if (vertices.length < 3 || vertices.length % 3 !== 0) {
119
+ throw new Error('cookConvexDecomposition: source requires triangle-list vertices (a positive multiple of 3)');
120
+ }
121
+ for (const vertex of vertices) {
122
+ if (!Number.isFinite(vertex.x) || !Number.isFinite(vertex.y) || !Number.isFinite(vertex.z)) {
123
+ throw new Error('cookConvexDecomposition: source contains a non-finite vertex');
124
+ }
125
+ }
126
+ const triangles = [];
127
+ for (let index = 0; index + 2 < vertices.length; index += 3) {
128
+ triangles.push([vertices[index], vertices[index + 1], vertices[index + 2]]);
129
+ }
130
+ // Winding is irrelevant to voxelisation, so a triangle's canonical key is its
131
+ // lexicographically sorted vertex triple. Sorting by it makes the whole cook
132
+ // invariant to the authoring order of the source soup.
133
+ return [...triangles].sort((left, right) => comparePointLists(sortPoints(left), sortPoints(right)));
134
+ }
135
+ function sortPoints(triangle) {
136
+ return [...triangle].sort(comparePoints);
137
+ }
138
+ function comparePoints(left, right) {
139
+ return left.x - right.x || left.y - right.y || left.z - right.z;
140
+ }
141
+ function comparePointLists(left, right) {
142
+ for (let index = 0; index < left.length; index += 1) {
143
+ const order = comparePoints(left[index], right[index]);
144
+ if (order !== 0) {
145
+ return order;
146
+ }
147
+ }
148
+ return 0;
149
+ }
150
+ // ---------- voxelisation ----------
151
+ function voxelize(triangles, resolution) {
152
+ let minX = Infinity, minY = Infinity, minZ = Infinity;
153
+ let maxX = -Infinity, maxY = -Infinity, maxZ = -Infinity;
154
+ for (const triangle of triangles) {
155
+ for (const vertex of triangle) {
156
+ minX = Math.min(minX, vertex.x);
157
+ maxX = Math.max(maxX, vertex.x);
158
+ minY = Math.min(minY, vertex.y);
159
+ maxY = Math.max(maxY, vertex.y);
160
+ minZ = Math.min(minZ, vertex.z);
161
+ maxZ = Math.max(maxZ, vertex.z);
162
+ }
163
+ }
164
+ const extent = Math.max(maxX - minX, maxY - minY, maxZ - minZ);
165
+ if (!(extent > 0)) {
166
+ throw new Error('cookConvexDecomposition: source mesh is degenerate (zero extent)');
167
+ }
168
+ const voxelSize = extent / resolution;
169
+ // One voxel of padding on every side gives the exterior flood fill a
170
+ // guaranteed connected shell to start from.
171
+ const originX = minX - voxelSize;
172
+ const originY = minY - voxelSize;
173
+ const originZ = minZ - voxelSize;
174
+ const nx = Math.max(1, Math.ceil((maxX - minX) / voxelSize)) + 2;
175
+ const ny = Math.max(1, Math.ceil((maxY - minY) / voxelSize)) + 2;
176
+ const nz = Math.max(1, Math.ceil((maxZ - minZ) / voxelSize)) + 2;
177
+ const SURFACE = 1;
178
+ const EXTERIOR = 2;
179
+ const cells = new Uint8Array(nx * ny * nz);
180
+ const at = (x, y, z) => (z * ny + y) * nx + x;
181
+ const half = voxelSize / 2;
182
+ for (const triangle of triangles) {
183
+ const lo = [
184
+ Math.min(triangle[0].x, triangle[1].x, triangle[2].x),
185
+ Math.min(triangle[0].y, triangle[1].y, triangle[2].y),
186
+ Math.min(triangle[0].z, triangle[1].z, triangle[2].z),
187
+ ];
188
+ const hi = [
189
+ Math.max(triangle[0].x, triangle[1].x, triangle[2].x),
190
+ Math.max(triangle[0].y, triangle[1].y, triangle[2].y),
191
+ Math.max(triangle[0].z, triangle[1].z, triangle[2].z),
192
+ ];
193
+ const x0 = clampIndex(Math.floor((lo[0] - originX) / voxelSize), nx);
194
+ const x1 = clampIndex(Math.floor((hi[0] - originX) / voxelSize), nx);
195
+ const y0 = clampIndex(Math.floor((lo[1] - originY) / voxelSize), ny);
196
+ const y1 = clampIndex(Math.floor((hi[1] - originY) / voxelSize), ny);
197
+ const z0 = clampIndex(Math.floor((lo[2] - originZ) / voxelSize), nz);
198
+ const z1 = clampIndex(Math.floor((hi[2] - originZ) / voxelSize), nz);
199
+ for (let z = z0; z <= z1; z += 1) {
200
+ for (let y = y0; y <= y1; y += 1) {
201
+ for (let x = x0; x <= x1; x += 1) {
202
+ const center = {
203
+ x: originX + (x + 0.5) * voxelSize,
204
+ y: originY + (y + 0.5) * voxelSize,
205
+ z: originZ + (z + 0.5) * voxelSize,
206
+ };
207
+ if (triangleIntersectsBox(triangle, center, half)) {
208
+ cells[at(x, y, z)] = SURFACE;
209
+ }
210
+ }
211
+ }
212
+ }
213
+ }
214
+ // Flood fill the exterior from the padded shell; everything left that is not
215
+ // surface is enclosed interior. Non-manifold or open meshes simply leak, and
216
+ // then the decomposition covers only the surface shell — which is still a
217
+ // sound (if hollow) collider, not a crash.
218
+ const stack = [];
219
+ const pushIfOpen = (x, y, z) => {
220
+ const index = at(x, y, z);
221
+ if (cells[index] === 0) {
222
+ cells[index] = EXTERIOR;
223
+ stack.push(index);
224
+ }
225
+ };
226
+ for (let z = 0; z < nz; z += 1) {
227
+ for (let y = 0; y < ny; y += 1) {
228
+ for (let x = 0; x < nx; x += 1) {
229
+ if (x === 0 || y === 0 || z === 0 || x === nx - 1 || y === ny - 1 || z === nz - 1) {
230
+ pushIfOpen(x, y, z);
231
+ }
232
+ }
233
+ }
234
+ }
235
+ while (stack.length > 0) {
236
+ const index = stack.pop();
237
+ const x = index % nx;
238
+ const y = Math.floor(index / nx) % ny;
239
+ const z = Math.floor(index / (nx * ny));
240
+ if (x > 0)
241
+ pushIfOpen(x - 1, y, z);
242
+ if (x < nx - 1)
243
+ pushIfOpen(x + 1, y, z);
244
+ if (y > 0)
245
+ pushIfOpen(x, y - 1, z);
246
+ if (y < ny - 1)
247
+ pushIfOpen(x, y + 1, z);
248
+ if (z > 0)
249
+ pushIfOpen(x, y, z - 1);
250
+ if (z < nz - 1)
251
+ pushIfOpen(x, y, z + 1);
252
+ }
253
+ const occupied = [];
254
+ for (let index = 0; index < cells.length; index += 1) {
255
+ if (cells[index] !== EXTERIOR) {
256
+ occupied.push(index);
257
+ }
258
+ }
259
+ /* istanbul ignore next -- a non-degenerate triangle always rasterises at least one surface voxel. */
260
+ if (occupied.length === 0) {
261
+ throw new Error('cookConvexDecomposition: source mesh voxelized to an empty volume');
262
+ }
263
+ return { nx, ny, nz, voxelSize, originX, originY, originZ, occupied };
264
+ }
265
+ function clampIndex(value, count) {
266
+ return Math.min(Math.max(value, 0), count - 1);
267
+ }
268
+ /**
269
+ * Akenine-Möller triangle/AABB overlap, box centred at `center` with half-width
270
+ * `half`, biased outward by `tol`.
271
+ *
272
+ * The bias is load-bearing, not a fudge. Axis-aligned source faces land exactly
273
+ * on voxel boundaries whenever the voxel size is not a binary fraction (`1/24`
274
+ * is not), so the overlap test is decided at exact tangency and rounding rejects
275
+ * roughly half the cells. That punches holes in the surface shell, the exterior
276
+ * flood fill leaks through them, and the mesh cooks hollow. A conservative
277
+ * rasteriser must resolve tangency as "overlapping".
278
+ */
279
+ function triangleIntersectsBox(triangle, center, half) {
280
+ const tol = half * 1e-6;
281
+ const v0 = [triangle[0].x - center.x, triangle[0].y - center.y, triangle[0].z - center.z];
282
+ const v1 = [triangle[1].x - center.x, triangle[1].y - center.y, triangle[1].z - center.z];
283
+ const v2 = [triangle[2].x - center.x, triangle[2].y - center.y, triangle[2].z - center.z];
284
+ const edges = [
285
+ [v1[0] - v0[0], v1[1] - v0[1], v1[2] - v0[2]],
286
+ [v2[0] - v1[0], v2[1] - v1[1], v2[2] - v1[2]],
287
+ [v0[0] - v2[0], v0[1] - v2[1], v0[2] - v2[2]],
288
+ ];
289
+ for (let axis = 0; axis < 3; axis += 1) {
290
+ for (const edge of edges) {
291
+ // cross(unitAxis[axis], edge)
292
+ const a = [0, 0, 0];
293
+ a[(axis + 1) % 3] = -edge[(axis + 2) % 3];
294
+ a[(axis + 2) % 3] = edge[(axis + 1) % 3];
295
+ const p0 = a[0] * v0[0] + a[1] * v0[1] + a[2] * v0[2];
296
+ const p1 = a[0] * v1[0] + a[1] * v1[1] + a[2] * v1[2];
297
+ const p2 = a[0] * v2[0] + a[1] * v2[1] + a[2] * v2[2];
298
+ const radius = half * (Math.abs(a[0]) + Math.abs(a[1]) + Math.abs(a[2])) + tol;
299
+ if (Math.min(p0, p1, p2) > radius || Math.max(p0, p1, p2) < -radius) {
300
+ return false;
301
+ }
302
+ }
303
+ }
304
+ for (let axis = 0; axis < 3; axis += 1) {
305
+ const lo = Math.min(v0[axis], v1[axis], v2[axis]);
306
+ const hi = Math.max(v0[axis], v1[axis], v2[axis]);
307
+ if (lo > half + tol || hi < -half - tol) {
308
+ return false;
309
+ }
310
+ }
311
+ const normal = [
312
+ edges[0][1] * edges[1][2] - edges[0][2] * edges[1][1],
313
+ edges[0][2] * edges[1][0] - edges[0][0] * edges[1][2],
314
+ edges[0][0] * edges[1][1] - edges[0][1] * edges[1][0],
315
+ ];
316
+ const offset = normal[0] * v0[0] + normal[1] * v0[1] + normal[2] * v0[2];
317
+ const radius = half * (Math.abs(normal[0]) + Math.abs(normal[1]) + Math.abs(normal[2])) + tol;
318
+ return Math.abs(offset) <= radius;
319
+ }
320
+ // ---------- decomposition ----------
321
+ function decompose(lattice, maxHulls, tolerance, splitCandidates) {
322
+ let parts = [makePart(lattice.occupied, lattice)];
323
+ while (parts.length < maxHulls) {
324
+ // Split the part that contributes the most absolute volume error. Ties fall
325
+ // to the lowest index, which is stable because parts stay sorted below.
326
+ let worst = -1;
327
+ let worstScore = 0;
328
+ parts.forEach((part, index) => {
329
+ const score = part.concavity * part.voxels.length;
330
+ if (part.concavity > tolerance && score > worstScore) {
331
+ worst = index;
332
+ worstScore = score;
333
+ }
334
+ });
335
+ if (worst < 0) {
336
+ break;
337
+ }
338
+ const split = bestSplit(parts[worst], lattice, splitCandidates);
339
+ /* istanbul ignore next -- only a single-voxel part is unsplittable, and its concavity is 0, so it is never selected as `worst` above. The guard stays because the selection rule and the split rule are separate. */
340
+ if (split === undefined) {
341
+ // Unsplittable (single voxel slab); accept it as a leaf by retiring it
342
+ // from consideration via an exact-convex marker.
343
+ parts[worst] = { ...parts[worst], concavity: 0 };
344
+ continue;
345
+ }
346
+ parts = [...parts.slice(0, worst), ...split, ...parts.slice(worst + 1)];
347
+ }
348
+ return parts;
349
+ }
350
+ function makePart(voxels, lattice) {
351
+ const hull = latticeHullOfVoxels(voxels, lattice);
352
+ /* istanbul ignore next -- a part always holds at least one voxel, whose eight corners span a unit-volume hull. */
353
+ const concavity = hull.volume <= 0 ? 0 : Math.max(0, 1 - voxels.length / hull.volume);
354
+ return { voxels, hull, concavity };
355
+ }
356
+ function bestSplit(part, lattice, splitCandidates) {
357
+ const coords = part.voxels.map((index) => voxelCoord(index, lattice));
358
+ let best;
359
+ for (let axis = 0; axis < 3; axis += 1) {
360
+ let lo = Infinity;
361
+ let hi = -Infinity;
362
+ for (const coord of coords) {
363
+ lo = Math.min(lo, coord[axis]);
364
+ hi = Math.max(hi, coord[axis]);
365
+ }
366
+ if (hi <= lo) {
367
+ continue;
368
+ }
369
+ for (let candidate = 1; candidate <= splitCandidates; candidate += 1) {
370
+ const plane = lo + Math.floor(((hi - lo + 1) * candidate) / (splitCandidates + 1));
371
+ if (plane <= lo || plane > hi) {
372
+ continue;
373
+ }
374
+ const left = [];
375
+ const right = [];
376
+ part.voxels.forEach((index, ordinal) => {
377
+ (coords[ordinal][axis] < plane ? left : right).push(index);
378
+ });
379
+ /* istanbul ignore next -- `plane` is clamped to (lo, hi], so the voxel at `lo` always lands left and the one at `hi` always lands right. */
380
+ if (left.length === 0 || right.length === 0) {
381
+ continue;
382
+ }
383
+ const leftPart = makePart(left, lattice);
384
+ const rightPart = makePart(right, lattice);
385
+ // Volume-weighted concavity of the pair; lower is a cleaner cut.
386
+ const score = leftPart.concavity * left.length + rightPart.concavity * right.length;
387
+ if (best === undefined || score < best.score) {
388
+ best = { score, parts: [leftPart, rightPart] };
389
+ }
390
+ }
391
+ }
392
+ return best?.parts;
393
+ }
394
+ function voxelCoord(index, lattice) {
395
+ const x = index % lattice.nx;
396
+ const y = Math.floor(index / lattice.nx) % lattice.ny;
397
+ const z = Math.floor(index / (lattice.nx * lattice.ny));
398
+ return [x, y, z];
399
+ }
400
+ /**
401
+ * Corner-lattice points that can possibly be convex-hull vertices. A corner with
402
+ * occupied corners both below and above it in its own column is a convex
403
+ * combination of them, so it can never be a hull vertex — keeping only each
404
+ * column's extremes is exact, not an approximation, and cuts the hull input from
405
+ * O(n³) to O(n²).
406
+ */
407
+ function hullCandidateCorners(voxels, lattice) {
408
+ const corners = [];
409
+ for (const index of voxels) {
410
+ const [vx, vy, vz] = voxelCoord(index, lattice);
411
+ for (let dx = 0; dx <= 1; dx += 1) {
412
+ for (let dy = 0; dy <= 1; dy += 1) {
413
+ for (let dz = 0; dz <= 1; dz += 1) {
414
+ corners.push([vx + dx, vy + dy, vz + dz]);
415
+ }
416
+ }
417
+ }
418
+ }
419
+ // Each axis pass is individually exact, so composing all three is too.
420
+ return columnExtremes(columnExtremes(columnExtremes(corners, 2), 1), 0);
421
+ }
422
+ /** Keep only each column's two extremes along `axis` (see hullCandidateCorners). */
423
+ function columnExtremes(points, axis) {
424
+ const u = (axis + 1) % 3;
425
+ const v = (axis + 2) % 3;
426
+ const columns = new Map();
427
+ for (const point of points) {
428
+ const key = `${point[u]}:${point[v]}`;
429
+ const column = columns.get(key);
430
+ if (column === undefined) {
431
+ columns.set(key, { min: point[axis], max: point[axis] });
432
+ }
433
+ else {
434
+ column.min = Math.min(column.min, point[axis]);
435
+ column.max = Math.max(column.max, point[axis]);
436
+ }
437
+ }
438
+ const kept = [];
439
+ for (const point of points) {
440
+ const column = columns.get(`${point[u]}:${point[v]}`);
441
+ if (point[axis] === column.min || point[axis] === column.max) {
442
+ kept.push(point);
443
+ }
444
+ }
445
+ return dedupeIVecs(kept);
446
+ }
447
+ function latticeHullOfVoxels(voxels, lattice) {
448
+ return convexHullLattice(hullCandidateCorners(voxels, lattice));
449
+ }
450
+ function orient3d(a, b, c, d) {
451
+ // Corner-lattice coordinates are small integers, so this determinant is exact
452
+ // in a double — the hull has no epsilon anywhere.
453
+ const ax = a[0] - d[0], ay = a[1] - d[1], az = a[2] - d[2];
454
+ const bx = b[0] - d[0], by = b[1] - d[1], bz = b[2] - d[2];
455
+ const cx = c[0] - d[0], cy = c[1] - d[1], cz = c[2] - d[2];
456
+ return ax * (by * cz - bz * cy) - ay * (bx * cz - bz * cx) + az * (bx * cy - by * cx);
457
+ }
458
+ function latticeCentroid(vertices) {
459
+ let x = 0, y = 0, z = 0;
460
+ for (const vertex of vertices) {
461
+ x += vertex[0];
462
+ y += vertex[1];
463
+ z += vertex[2];
464
+ }
465
+ return [x / vertices.length, y / vertices.length, z / vertices.length];
466
+ }
467
+ /** orient3d against a non-lattice apex; used only to rank removal candidates. */
468
+ function orient3dFloat(a, b, c, d) {
469
+ const ax = a[0] - d[0], ay = a[1] - d[1], az = a[2] - d[2];
470
+ const bx = b[0] - d[0], by = b[1] - d[1], bz = b[2] - d[2];
471
+ const cx = c[0] - d[0], cy = c[1] - d[1], cz = c[2] - d[2];
472
+ return ax * (by * cz - bz * cy) - ay * (bx * cz - bz * cx) + az * (bx * cy - by * cx);
473
+ }
474
+ function compareIVec(left, right) {
475
+ return left[0] - right[0] || left[1] - right[1] || left[2] - right[2];
476
+ }
477
+ /**
478
+ * Incremental convex hull over integer points. Degenerate inputs (collinear or
479
+ * coplanar) return an empty face set with zero volume; callers treat that as a
480
+ * flat part and fall back to its voxel AABB.
481
+ */
482
+ function convexHullLattice(points) {
483
+ const unique = dedupeIVecs(points);
484
+ if (unique.length < 4) {
485
+ return { vertices: unique, faces: [], volume: 0 };
486
+ }
487
+ const seed = seedTetrahedron(unique);
488
+ if (seed === undefined) {
489
+ return { vertices: unique, faces: [], volume: 0 };
490
+ }
491
+ const faces = [];
492
+ const [i0, i1, i2, i3] = seed;
493
+ // Orient every face so an interior point scores positive; a point strictly
494
+ // outside a face then scores negative, which is the visibility test below.
495
+ const addSeedFace = (a, b, c, opposite) => {
496
+ faces.push(orient3d(unique[a], unique[b], unique[c], unique[opposite]) > 0 ? [a, b, c] : [a, c, b]);
497
+ };
498
+ addSeedFace(i0, i1, i2, i3);
499
+ addSeedFace(i0, i1, i3, i2);
500
+ addSeedFace(i0, i2, i3, i1);
501
+ addSeedFace(i1, i2, i3, i0);
502
+ const inSeed = new Set(seed);
503
+ let live = faces;
504
+ for (let index = 0; index < unique.length; index += 1) {
505
+ if (inSeed.has(index)) {
506
+ continue;
507
+ }
508
+ const point = unique[index];
509
+ const visible = [];
510
+ const kept = [];
511
+ for (const face of live) {
512
+ // A face is visible when the point lies strictly on its outward side.
513
+ if (orient3d(unique[face[0]], unique[face[1]], unique[face[2]], point) < 0) {
514
+ visible.push(face);
515
+ }
516
+ else {
517
+ kept.push(face);
518
+ }
519
+ }
520
+ /* istanbul ignore next -- points are inserted in lexicographic order, so a point is never interior to the partial hull that already exists when it is added. */
521
+ if (visible.length === 0) {
522
+ continue;
523
+ }
524
+ // Horizon = edges owned by exactly one visible face.
525
+ const edgeUse = new Map();
526
+ for (const face of visible) {
527
+ for (const [a, b] of [[face[0], face[1]], [face[1], face[2]], [face[2], face[0]]]) {
528
+ const key = a < b ? `${a}:${b}` : `${b}:${a}`;
529
+ if (edgeUse.has(key)) {
530
+ edgeUse.delete(key);
531
+ }
532
+ else {
533
+ edgeUse.set(key, [a, b]);
534
+ }
535
+ }
536
+ }
537
+ const horizon = [...edgeUse.entries()].sort((left, right) => (left[0] < right[0] ? -1 : 1)).map((entry) => entry[1]);
538
+ for (const [a, b] of horizon) {
539
+ kept.push([a, b, index]);
540
+ }
541
+ live = kept;
542
+ }
543
+ const usedIndices = [...new Set(live.flat())].sort((left, right) => left - right);
544
+ const remap = new Map(usedIndices.map((original, next) => [original, next]));
545
+ const vertices = usedIndices.map((original) => unique[original]);
546
+ const hullFaces = live
547
+ .map((face) => [remap.get(face[0]), remap.get(face[1]), remap.get(face[2])])
548
+ /* istanbul ignore next -- consistent outward winding means no two faces share their first two vertex indices in the same order (3000-hull fuzz: never); the third operand stays as the determinism guarantee. */
549
+ .sort((left, right) => left[0] - right[0] || left[1] - right[1] || left[2] - right[2]);
550
+ return { vertices, faces: hullFaces, volume: hullVolume(vertices, hullFaces) };
551
+ }
552
+ function dedupeIVecs(points) {
553
+ const sorted = [...points].sort(compareIVec);
554
+ const unique = [];
555
+ for (const point of sorted) {
556
+ const last = unique[unique.length - 1];
557
+ if (last === undefined || compareIVec(last, point) !== 0) {
558
+ unique.push(point);
559
+ }
560
+ }
561
+ return unique;
562
+ }
563
+ function seedTetrahedron(points) {
564
+ const i0 = 0;
565
+ let i1 = -1;
566
+ for (let index = 1; index < points.length; index += 1) {
567
+ /* istanbul ignore else -- `points` is deduped, so points[1] always differs from points[0]. */
568
+ if (compareIVec(points[i0], points[index]) !== 0) {
569
+ i1 = index;
570
+ break;
571
+ }
572
+ }
573
+ /* istanbul ignore next -- unreachable: the deduped set has at least two distinct points here. */
574
+ if (i1 < 0) {
575
+ return undefined;
576
+ }
577
+ let i2 = -1;
578
+ for (let index = 1; index < points.length; index += 1) {
579
+ if (index !== i1 && !collinear(points[i0], points[i1], points[index])) {
580
+ i2 = index;
581
+ break;
582
+ }
583
+ }
584
+ if (i2 < 0) {
585
+ return undefined;
586
+ }
587
+ for (let index = 1; index < points.length; index += 1) {
588
+ if (index !== i1 && index !== i2 && orient3d(points[i0], points[i1], points[i2], points[index]) !== 0) {
589
+ return [i0, i1, i2, index];
590
+ }
591
+ }
592
+ return undefined;
593
+ }
594
+ function collinear(a, b, c) {
595
+ const ux = b[0] - a[0], uy = b[1] - a[1], uz = b[2] - a[2];
596
+ const vx = c[0] - a[0], vy = c[1] - a[1], vz = c[2] - a[2];
597
+ return uy * vz - uz * vy === 0 && uz * vx - ux * vz === 0 && ux * vy - uy * vx === 0;
598
+ }
599
+ function hullVolume(vertices, faces) {
600
+ /* istanbul ignore next -- only called with the incremental hull's live face set, which always has at least four faces. */
601
+ if (faces.length === 0) {
602
+ return 0;
603
+ }
604
+ let sixVolume = 0;
605
+ const origin = vertices[0];
606
+ for (const face of faces) {
607
+ sixVolume += orient3d(vertices[face[0]], vertices[face[1]], vertices[face[2]], origin);
608
+ }
609
+ return Math.abs(sixVolume) / 6;
610
+ }
611
+ // ---------- hull finalisation ----------
612
+ function finalizeHull(part, lattice, maxHullVertices, quantum) {
613
+ // Every non-empty voxel set spans two corner layers on all three axes, so its
614
+ // lattice hull is always full-dimensional and `part.hull.faces` is non-empty.
615
+ // Reduce in integer lattice space, where the hull rebuild driving the greedy
616
+ // removal is exact and cheap, then inflate the survivor in world space until
617
+ // it contains every original hull vertex again.
618
+ const reducedLattice = reduceLatticeHull(part.hull, maxHullVertices);
619
+ const original = part.hull.vertices.map((corner) => cornerToWorld(corner, lattice));
620
+ const reduced = reducedLattice.length === part.hull.vertices.length
621
+ ? original
622
+ : inflateToContain(reducedLattice.map((corner) => cornerToWorld(corner, lattice)), original);
623
+ // Round each vertex AWAY from the hull's centre, never to the nearest grid
624
+ // point. Nearest-rounding pushes roughly half the vertices inside the hull they
625
+ // define, `buildSolverHull` then rejects them as interior, and the part
626
+ // degrades. Outward rounding keeps every vertex extreme and preserves the
627
+ // containment guarantee inflation just established.
628
+ const centre = meanPoint(reduced);
629
+ const boundary = boundaryVertices(dedupePoints(reduced.map((point) => quantizeOutward(point, centre, quantum))));
630
+ // Silently shipping a looser box here would hide a bad cook in a game rather
631
+ // than in the asset pipeline; `quantum <= voxelSize / 8` is validated up front,
632
+ // so reaching either throw means the caller asked for something incoherent.
633
+ if (boundary.length < RUNTIME_MIN_HULL_VERTICES || boundary.length > maxHullVertices) {
634
+ throw new Error(`cookConvexDecomposition: a part reduced to ${boundary.length} boundary vertices, outside the runtime's `
635
+ + `${RUNTIME_MIN_HULL_VERTICES}..${maxHullVertices} range — raise maxHullVertices or use a finer quantum`);
636
+ }
637
+ try {
638
+ (0, physics3d_hull_js_1.buildSolverHull)(boundary);
639
+ }
640
+ catch (error) {
641
+ /* istanbul ignore next -- quantum <= voxelSize / 8 is validated up front, and outward rounding keeps every reduced vertex extreme, so the boundary always builds. */
642
+ throw new Error(`cookConvexDecomposition: quantisation collapsed a part's hull (${error.message}) `
643
+ + '— use a finer quantum or a coarser resolution');
644
+ }
645
+ return boundary;
646
+ }
647
+ /**
648
+ * Drop the least load-bearing vertex until the cap is met.
649
+ *
650
+ * A vertex's cost is the volume of the cone its incident faces subtend from the
651
+ * hull's centroid — the part of the hull only that vertex carries. Scoring from
652
+ * the current hull's faces costs one hull rebuild per removal rather than one
653
+ * per candidate, which is the difference between a cook that takes a second and
654
+ * one that takes a minute. It is a greedy proxy, and it is allowed to be: the
655
+ * caller re-inflates the survivor until it contains the original hull, so a
656
+ * suboptimal choice costs tightness, never correctness.
657
+ */
658
+ function reduceLatticeHull(hull, maxHullVertices) {
659
+ let current = hull;
660
+ while (current.vertices.length > maxHullVertices && current.faces.length > 0) {
661
+ const cost = new Array(current.vertices.length).fill(0);
662
+ // Cone volumes are measured from the hull's centroid, never from one of its
663
+ // own vertices: every face incident to that vertex would subtend zero volume,
664
+ // so it would always score lowest and the greedy would just strip vertices in
665
+ // index order until the survivors went coplanar.
666
+ const apex = latticeCentroid(current.vertices);
667
+ for (const face of current.faces) {
668
+ const volume = Math.abs(orient3dFloat(current.vertices[face[0]], current.vertices[face[1]], current.vertices[face[2]], apex));
669
+ cost[face[0]] += volume;
670
+ cost[face[1]] += volume;
671
+ cost[face[2]] += volume;
672
+ }
673
+ let victim = 0;
674
+ for (let index = 1; index < cost.length; index += 1) {
675
+ if (cost[index] < cost[victim]) {
676
+ victim = index;
677
+ }
678
+ }
679
+ const kept = current.vertices.filter((_, index) => index !== victim);
680
+ const rebuilt = convexHullLattice(kept);
681
+ if (rebuilt.volume <= 0 || rebuilt.vertices.length >= current.vertices.length) {
682
+ break;
683
+ }
684
+ current = rebuilt;
685
+ }
686
+ return current.vertices;
687
+ }
688
+ function cornerToWorld(corner, lattice) {
689
+ return {
690
+ x: lattice.originX + corner[0] * lattice.voxelSize,
691
+ y: lattice.originY + corner[1] * lattice.voxelSize,
692
+ z: lattice.originZ + corner[2] * lattice.voxelSize,
693
+ };
694
+ }
695
+ /**
696
+ * Inflate `hullPoints` about their centroid by the smallest factor that puts
697
+ * every point of `mustContain` back inside. A cooked collider is never smaller
698
+ * than the geometry it stands for.
699
+ */
700
+ function inflateToContain(hullPoints, mustContain) {
701
+ const centroid = meanPoint(hullPoints);
702
+ const planes = supportingPlanes(hullPoints, centroid);
703
+ let scale = 1;
704
+ for (const point of mustContain) {
705
+ for (const plane of planes) {
706
+ const reach = dot(plane.normal, subtract(point, centroid));
707
+ if (reach > plane.offset * scale) {
708
+ scale = reach / plane.offset;
709
+ }
710
+ }
711
+ }
712
+ if (scale <= 1) {
713
+ return hullPoints;
714
+ }
715
+ // Scaling the vertices about the centroid scales every supporting plane's
716
+ // centroid-relative offset by the same factor, so containment is exact.
717
+ return hullPoints.map((point) => add(centroid, multiply(subtract(point, centroid), scale)));
718
+ }
719
+ function supportingPlanes(points, centroid) {
720
+ const planes = [];
721
+ for (let a = 0; a < points.length; a += 1) {
722
+ for (let b = a + 1; b < points.length; b += 1) {
723
+ for (let c = b + 1; c < points.length; c += 1) {
724
+ const normal = cross(subtract(points[b], points[a]), subtract(points[c], points[a]));
725
+ const length = Math.sqrt(dot(normal, normal));
726
+ if (length === 0) {
727
+ continue;
728
+ }
729
+ const unit = multiply(normal, 1 / length);
730
+ for (const oriented of [unit, multiply(unit, -1)]) {
731
+ const offset = dot(oriented, subtract(points[a], centroid));
732
+ if (offset <= 0) {
733
+ continue;
734
+ }
735
+ if (points.every((point) => dot(oriented, subtract(point, centroid)) <= offset + 1e-9)) {
736
+ planes.push({ normal: oriented, offset });
737
+ }
738
+ }
739
+ }
740
+ }
741
+ }
742
+ if (planes.length === 0) {
743
+ throw new Error('cookConvexDecomposition: hull reduction produced a degenerate (flat) hull');
744
+ }
745
+ return planes;
746
+ }
747
+ /** Vertices of `points` that survive on the convex boundary, in canonical order. */
748
+ function boundaryVertices(points) {
749
+ if (points.length < RUNTIME_MIN_HULL_VERTICES) {
750
+ return points;
751
+ }
752
+ const centroid = meanPoint(points);
753
+ let planes;
754
+ try {
755
+ planes = supportingPlanes(points, centroid);
756
+ }
757
+ catch {
758
+ return points;
759
+ }
760
+ const kept = points.filter((point) => planes.some((plane) => Math.abs(dot(plane.normal, subtract(point, centroid)) - plane.offset) <= 1e-9));
761
+ return [...kept].sort(comparePoints);
762
+ }
763
+ // ---------- small vector helpers ----------
764
+ function add(a, b) {
765
+ return { x: a.x + b.x, y: a.y + b.y, z: a.z + b.z };
766
+ }
767
+ function subtract(a, b) {
768
+ return { x: a.x - b.x, y: a.y - b.y, z: a.z - b.z };
769
+ }
770
+ function multiply(a, scalar) {
771
+ return { x: a.x * scalar, y: a.y * scalar, z: a.z * scalar };
772
+ }
773
+ function dot(a, b) {
774
+ return a.x * b.x + a.y * b.y + a.z * b.z;
775
+ }
776
+ function cross(a, b) {
777
+ return { x: a.y * b.z - a.z * b.y, y: a.z * b.x - a.x * b.z, z: a.x * b.y - a.y * b.x };
778
+ }
779
+ function meanPoint(points) {
780
+ let sum = { x: 0, y: 0, z: 0 };
781
+ for (const point of points) {
782
+ sum = add(sum, point);
783
+ }
784
+ return multiply(sum, 1 / points.length);
785
+ }
786
+ /** Quantize each axis away from `from`, so the point never moves inward. */
787
+ function quantizeOutward(point, from, quantum) {
788
+ const axis = (value, origin) => (value >= origin ? Math.ceil(value / quantum) * quantum : Math.floor(value / quantum) * quantum);
789
+ return { x: axis(point.x, from.x), y: axis(point.y, from.y), z: axis(point.z, from.z) };
790
+ }
791
+ function quantizePoint(point, quantum) {
792
+ return {
793
+ x: Math.round(point.x / quantum) * quantum,
794
+ y: Math.round(point.y / quantum) * quantum,
795
+ z: Math.round(point.z / quantum) * quantum,
796
+ };
797
+ }
798
+ function dedupePoints(points) {
799
+ const sorted = [...points].sort(comparePoints);
800
+ const unique = [];
801
+ for (const point of sorted) {
802
+ const last = unique[unique.length - 1];
803
+ if (last === undefined || comparePoints(last, point) !== 0) {
804
+ unique.push(point);
805
+ }
806
+ }
807
+ return unique;
808
+ }
809
+ function requireIntegerInRange(value, low, high, name) {
810
+ if (!Number.isInteger(value) || value < low || value > high) {
811
+ throw new Error(`cookConvexDecomposition: ${name} must be an integer in [${low}, ${high}]`);
812
+ }
813
+ return value;
814
+ }
815
+ /**
816
+ * Internals exposed for direct testing. The cook's public entry only ever feeds
817
+ * these well-formed inputs, so their degenerate branches (collinear/coplanar
818
+ * point sets, unsplittable parts, the voxel-AABB fallback) are unreachable
819
+ * through `cookConvexDecomposition` alone.
820
+ */
821
+ exports.__colliderCookingInternals = {
822
+ bestSplit,
823
+ boundaryVertices,
824
+ collinear,
825
+ convexHullLattice,
826
+ dedupePoints,
827
+ finalizeHull,
828
+ inflateToContain,
829
+ makePart,
830
+ reduceLatticeHull,
831
+ supportingPlanes,
832
+ triangleIntersectsBox,
833
+ voxelize,
834
+ };