@driftengine/physics 3.61.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (136) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +9 -0
  3. package/README.md +503 -0
  4. package/dist/bodies.d.ts +121 -0
  5. package/dist/bodies.js +298 -0
  6. package/dist/buoyancy.d.ts +46 -0
  7. package/dist/buoyancy.js +45 -0
  8. package/dist/cloth.d.ts +226 -0
  9. package/dist/cloth.js +587 -0
  10. package/dist/collide/aabb.d.ts +10 -0
  11. package/dist/collide/aabb.js +11 -0
  12. package/dist/collide/body.d.ts +202 -0
  13. package/dist/collide/body.js +142 -0
  14. package/dist/collide/eject.d.ts +39 -0
  15. package/dist/collide/eject.js +142 -0
  16. package/dist/collide/index.d.ts +18 -0
  17. package/dist/collide/index.js +5 -0
  18. package/dist/collide/scratch.d.ts +8 -0
  19. package/dist/collide/scratch.js +9 -0
  20. package/dist/collide/segment.d.ts +12 -0
  21. package/dist/collide/segment.js +109 -0
  22. package/dist/collide/sweep.d.ts +3 -0
  23. package/dist/collide/sweep.js +1036 -0
  24. package/dist/colliderSet.d.ts +239 -0
  25. package/dist/colliderSet.js +546 -0
  26. package/dist/compoundContact.d.ts +70 -0
  27. package/dist/compoundContact.js +221 -0
  28. package/dist/controller.d.ts +351 -0
  29. package/dist/controller.js +641 -0
  30. package/dist/cylinder.d.ts +78 -0
  31. package/dist/cylinder.js +294 -0
  32. package/dist/cylinderContact.d.ts +7 -0
  33. package/dist/cylinderContact.js +704 -0
  34. package/dist/decompose.d.ts +105 -0
  35. package/dist/decompose.js +979 -0
  36. package/dist/events.d.ts +43 -0
  37. package/dist/events.js +95 -0
  38. package/dist/executor.d.ts +147 -0
  39. package/dist/executor.js +173 -0
  40. package/dist/faces.d.ts +26 -0
  41. package/dist/faces.js +34 -0
  42. package/dist/fingerprint.d.ts +61 -0
  43. package/dist/fingerprint.js +139 -0
  44. package/dist/groundMotor.d.ts +34 -0
  45. package/dist/groundMotor.js +41 -0
  46. package/dist/heightfieldShape.d.ts +128 -0
  47. package/dist/heightfieldShape.js +328 -0
  48. package/dist/index.d.ts +73 -0
  49. package/dist/index.js +47 -0
  50. package/dist/island.d.ts +55 -0
  51. package/dist/island.js +227 -0
  52. package/dist/islandSolve.d.ts +70 -0
  53. package/dist/islandSolve.js +171 -0
  54. package/dist/islandWorker.d.ts +18 -0
  55. package/dist/islandWorker.js +125 -0
  56. package/dist/joints.d.ts +180 -0
  57. package/dist/joints.js +1094 -0
  58. package/dist/manifold.d.ts +89 -0
  59. package/dist/manifold.js +608 -0
  60. package/dist/mass.d.ts +53 -0
  61. package/dist/mass.js +353 -0
  62. package/dist/meshContact.d.ts +57 -0
  63. package/dist/meshContact.js +393 -0
  64. package/dist/meshShape.d.ts +76 -0
  65. package/dist/meshShape.js +175 -0
  66. package/dist/pairs.d.ts +62 -0
  67. package/dist/pairs.js +154 -0
  68. package/dist/query.d.ts +71 -0
  69. package/dist/query.js +683 -0
  70. package/dist/ragdoll.d.ts +135 -0
  71. package/dist/ragdoll.js +489 -0
  72. package/dist/shape.d.ts +177 -0
  73. package/dist/shape.js +533 -0
  74. package/dist/solver.d.ts +136 -0
  75. package/dist/solver.js +601 -0
  76. package/dist/stage.d.ts +167 -0
  77. package/dist/stage.js +350 -0
  78. package/dist/stallEscape.d.ts +141 -0
  79. package/dist/stallEscape.js +254 -0
  80. package/dist/sweptStep.d.ts +173 -0
  81. package/dist/sweptStep.js +472 -0
  82. package/dist/tree.d.ts +61 -0
  83. package/dist/tree.js +348 -0
  84. package/dist/vehicle.d.ts +154 -0
  85. package/dist/vehicle.js +343 -0
  86. package/dist/workerPool.d.ts +107 -0
  87. package/dist/workerPool.js +239 -0
  88. package/dist/workers.d.ts +37 -0
  89. package/dist/workers.js +35 -0
  90. package/dist/world.d.ts +298 -0
  91. package/dist/world.js +712 -0
  92. package/package.json +55 -0
  93. package/src/bodies.ts +369 -0
  94. package/src/buoyancy.ts +78 -0
  95. package/src/cloth.ts +734 -0
  96. package/src/collide/aabb.ts +28 -0
  97. package/src/collide/body.ts +336 -0
  98. package/src/collide/eject.ts +156 -0
  99. package/src/collide/index.ts +26 -0
  100. package/src/collide/scratch.ts +10 -0
  101. package/src/collide/segment.ts +127 -0
  102. package/src/collide/sweep.ts +1132 -0
  103. package/src/colliderSet.ts +637 -0
  104. package/src/compoundContact.ts +284 -0
  105. package/src/controller.ts +817 -0
  106. package/src/cylinder.ts +340 -0
  107. package/src/cylinderContact.ts +825 -0
  108. package/src/decompose.ts +1198 -0
  109. package/src/events.ts +108 -0
  110. package/src/executor.ts +224 -0
  111. package/src/faces.ts +37 -0
  112. package/src/fingerprint.ts +146 -0
  113. package/src/groundMotor.ts +53 -0
  114. package/src/heightfieldShape.ts +395 -0
  115. package/src/index.ts +164 -0
  116. package/src/island.ts +219 -0
  117. package/src/islandSolve.ts +218 -0
  118. package/src/islandWorker.ts +141 -0
  119. package/src/joints.ts +1367 -0
  120. package/src/manifold.ts +798 -0
  121. package/src/mass.ts +424 -0
  122. package/src/meshContact.ts +443 -0
  123. package/src/meshShape.ts +261 -0
  124. package/src/pairs.ts +169 -0
  125. package/src/query.ts +844 -0
  126. package/src/ragdoll.ts +662 -0
  127. package/src/shape.ts +656 -0
  128. package/src/solver.ts +873 -0
  129. package/src/stage.ts +406 -0
  130. package/src/stallEscape.ts +362 -0
  131. package/src/sweptStep.ts +609 -0
  132. package/src/tree.ts +366 -0
  133. package/src/vehicle.ts +489 -0
  134. package/src/workerPool.ts +283 -0
  135. package/src/workers.ts +38 -0
  136. package/src/world.ts +961 -0
@@ -0,0 +1,546 @@
1
+ import { hullShape, shapeBounds } from './shape.js';
2
+ /**
3
+ * Static world collision in a form that scales: bounds packed into one
4
+ * Float32Array, indexed by a uniform spatial hash so a move query touches a
5
+ * handful of boxes instead of all of them.
6
+ *
7
+ * Built once at world generation; every query afterwards is allocation-free.
8
+ * The hash is an optimisation only — it may over-report a candidate, never
9
+ * under-report one, because a missed box is a box the character falls through.
10
+ *
11
+ * A collider may carry the shape it really is. The bounds then become the
12
+ * broad phase only — what decides whether the shape is worth asking — and the
13
+ * sweep in `collide.ts` resolves against the shape itself, so a banked slab of
14
+ * track is solid exactly where it is drawn and nowhere else. A collider
15
+ * without a shape is its box, exactly as before.
16
+ */
17
+ const DEFAULT_CELL_SIZE = 4;
18
+ const STRIDE = 6;
19
+ /**
20
+ * One slot's packed bounds in bytes, for a reader walking `data` a slot at a time.
21
+ *
22
+ * **The stride is otherwise private and stays that way.** This exists because
23
+ * `fingerprintColliders` has to step slot by slot to skip the dead ones, and a literal 24 over
24
+ * there would be a second place for the packing to be wrong — which is the failure the `bounds`
25
+ * accessor already exists to prevent.
26
+ */
27
+ export const STRIDE_BYTES = STRIDE * 4;
28
+ /** Wrap a world-space shape in the bounds the broad phase needs. */
29
+ export function colliderFromShape(shape) {
30
+ /*
31
+ * **A triangle mesh is refused here rather than bounded.** The collider set feeds the *kinematic*
32
+ * sweep, which tests a fixed list of separating axes and a mesh enumerates none — so a mesh
33
+ * turned into a collider would arrive as its own bounding box, and a hollow level would be a
34
+ * solid brick. A mesh belongs to `PhysicsWorld` as a static body, where `collideMesh` reads its
35
+ * triangles.
36
+ */
37
+ if (shape.triangles !== undefined) {
38
+ throw new Error('colliderFromShape: a triangle mesh is not a kinematic collider. Add it to a PhysicsWorld ' +
39
+ 'as a static body instead.');
40
+ }
41
+ const collider = { minX: 0, minY: 0, minZ: 0, maxX: 0, maxY: 0, maxZ: 0, shape };
42
+ shapeBounds(shape, collider);
43
+ return collider;
44
+ }
45
+ /**
46
+ * The collider of a drawn box, exactly where it is drawn.
47
+ *
48
+ * Same centre and half-extents a mesh builder takes, so a game that draws a
49
+ * box gets its collision from the same numbers — nothing authored twice. The
50
+ * shape routes the touch through the shape sweep, which is what lets a lid sit
51
+ * flush with a walkable surface: the old world-axis path fought a body for the
52
+ * ground it stood on, and lids had to be sunk out of reach to compensate.
53
+ */
54
+ export function boxCollider(cx, cy, cz, hx, hy, hz) {
55
+ const corners = [];
56
+ for (let i = 0; i < 8; i++) {
57
+ corners.push(cx + (i & 1 ? hx : -hx), cy + (i & 2 ? hy : -hy), cz + (i & 4 ? hz : -hz));
58
+ }
59
+ return colliderFromShape(hullShape(corners));
60
+ }
61
+ /**
62
+ * What one bucket's array costs beyond its entries, and what one `Map` entry costs.
63
+ *
64
+ * **Estimates, and they are the only estimates in `bytes()`.** A JavaScript engine's array and
65
+ * hash-table headers are not observable from inside it, so the alternative to a named guess is to
66
+ * report a number that silently omits them. The entry and bucket *counts* multiplied by these are
67
+ * exact; these two constants are not. **What would make them wrong** is a different engine or a
68
+ * V8 that changes its element backing store — at which point the shape of the answer still holds
69
+ * and the constant is off by a small factor.
70
+ */
71
+ const BUCKET_OVERHEAD_BYTES = 64;
72
+ const MAP_ENTRY_BYTES = 32;
73
+ export class ColliderSet {
74
+ static MAX_HITS = 512;
75
+ /**
76
+ * The group the constructor's own colliders belong to, which `add` never mints and `remove`
77
+ * refuses.
78
+ *
79
+ * **Reserved because of the slots nothing inserts.** The constructor skips an `undefined` entry
80
+ * without putting it in any cell, and still counts it — so those slots are live and are in no
81
+ * bucket, and a removal that reached one would walk buckets it was never written to. Keeping the
82
+ * constructor's slots in a group nothing can drop puts that case out of reach instead of making
83
+ * it survivable.
84
+ *
85
+ * **A set meant to stream is constructed empty** and takes everything through `add`, which is
86
+ * what the refusal points at when it fires.
87
+ */
88
+ static BASE_GROUP = 0;
89
+ /**
90
+ * The packed bounds, six floats a slot.
91
+ *
92
+ * **A reference to this is valid until the next `add`**, which may reallocate. `bounds()` is the
93
+ * reader that survives both that and a change of packing.
94
+ *
95
+ * **Its length is `capacity`, not `count`.** Those are equal on a set that has never had
96
+ * `remove` called on it, which is every set built the old way; after a removal the live slots
97
+ * are sparse and a walk of `0..count` reads the wrong ones. Walk `0..capacity` and skip
98
+ * `!liveAt(i)`.
99
+ */
100
+ data;
101
+ cellSize;
102
+ cells = new Map();
103
+ shapes;
104
+ /** Per-query stamp, so a box spanning several cells is reported once. */
105
+ seen;
106
+ stamp = 0;
107
+ /** Slots allocated, live or not. */
108
+ slots;
109
+ /** Slots in use. `count` reads this. */
110
+ live;
111
+ /**
112
+ * Who holds each slot: `-1` for a free one, otherwise the group.
113
+ *
114
+ * One array for both liveness and provenance, which is what `bytes`, `liveAt` and the
115
+ * fingerprint each need, at four bytes a slot instead of a flag array beside a group array.
116
+ */
117
+ owner;
118
+ freeSlots = [];
119
+ /**
120
+ * Each group's slots, for the only operation that needs them.
121
+ *
122
+ * **A `Map`, and `remove` is its only reader.** `fingerprint.ts` states the rule this respects:
123
+ * nothing feeding a hash may iterate a `Map` or a `Set`, because insertion order would leak into
124
+ * the answer.
125
+ */
126
+ groups = new Map();
127
+ nextGroup = 1;
128
+ constructor(boxes, cellSize = DEFAULT_CELL_SIZE) {
129
+ this.slots = boxes.length;
130
+ this.live = boxes.length;
131
+ this.cellSize = cellSize;
132
+ this.data = new Float32Array(boxes.length * STRIDE);
133
+ this.shapes = new Array(boxes.length);
134
+ this.seen = new Int32Array(boxes.length);
135
+ /*
136
+ * Every slot the constructor allocates is live and belongs to the base group — including the
137
+ * ones the `undefined` check below skips. They are live with zero bounds today, and keeping
138
+ * them so is what makes `fingerprintColliders` produce the byte stream it always has.
139
+ */
140
+ this.owner = new Int32Array(boxes.length).fill(ColliderSet.BASE_GROUP);
141
+ for (let i = 0; i < boxes.length; i++) {
142
+ const b = boxes[i];
143
+ if (b === undefined)
144
+ continue;
145
+ const o = i * STRIDE;
146
+ this.data[o] = b.minX;
147
+ this.data[o + 1] = b.minY;
148
+ this.data[o + 2] = b.minZ;
149
+ this.data[o + 3] = b.maxX;
150
+ this.data[o + 4] = b.maxY;
151
+ this.data[o + 5] = b.maxZ;
152
+ this.shapes[i] = b.shape;
153
+ this.insert(i, b);
154
+ }
155
+ }
156
+ /** How many colliders the set holds. */
157
+ get count() {
158
+ return this.live;
159
+ }
160
+ /** How many slots are allocated, live or not. The true bound of a walk over `data`. */
161
+ get capacity() {
162
+ return this.slots;
163
+ }
164
+ /** Whether slot `index` holds a collider. Every slot of a set that has never had `remove`
165
+ * called on it does. */
166
+ liveAt(index) {
167
+ return this.owner[index] !== -1;
168
+ }
169
+ /**
170
+ * Take a batch of colliders and hand back the handle that drops them again.
171
+ *
172
+ * **The grid is updated for these colliders and for no others**, which is the whole point: a
173
+ * consumer whose world streams was rebuilding a set at every region crossing, at about 2.1 µs a
174
+ * box, and paying a whole frame for it.
175
+ *
176
+ * Slots come off the free list before the arrays grow, so a set that loads a ring of regions
177
+ * around a player settles at the ring's high-water mark instead of climbing with distance
178
+ * travelled. Live indices are untouched by this: a slot already in use stays where it is.
179
+ */
180
+ add(boxes) {
181
+ const group = this.nextGroup++;
182
+ const slots = [];
183
+ this.ensureCapacity(this.live + boxes.length);
184
+ for (const b of boxes) {
185
+ if (b === undefined)
186
+ continue;
187
+ const slot = this.freeSlots.pop() ?? this.slots++;
188
+ /*
189
+ * **A recycled slot starts unmarked, and this is the one line that keeps the hash honest
190
+ * through a reuse.** `query` skips a slot whose stamp is already the current mark; a slot
191
+ * carrying a stamp from an earlier life could be skipped on the query that recycles it, and
192
+ * a skipped box is a box the character falls through — the under-report direction this class
193
+ * forbids. One store, unconditional, because working out whether it is needed costs more
194
+ * than doing it.
195
+ */
196
+ this.seen[slot] = 0;
197
+ const o = slot * STRIDE;
198
+ this.data[o] = b.minX;
199
+ this.data[o + 1] = b.minY;
200
+ this.data[o + 2] = b.minZ;
201
+ this.data[o + 3] = b.maxX;
202
+ this.data[o + 4] = b.maxY;
203
+ this.data[o + 5] = b.maxZ;
204
+ this.shapes[slot] = b.shape;
205
+ this.owner[slot] = group;
206
+ this.insert(slot, b);
207
+ slots.push(slot);
208
+ this.live++;
209
+ }
210
+ this.groups.set(group, slots);
211
+ return group;
212
+ }
213
+ /**
214
+ * Take everything in another set as one group, without hashing any of it again.
215
+ *
216
+ * **Copy and offset instead of recompute.** One `Float32Array.set` moves the packed bounds, the
217
+ * shapes are copied by reference, and each of the source's buckets is appended to this set's
218
+ * with a constant added to every index. Nothing is floored, no shape bounds are recomputed, and
219
+ * the result queries identically to a one-shot build of the union.
220
+ *
221
+ * **It appends past everything and skips the free list**, because a constant offset is what
222
+ * makes the bucket remap a copy: source slot `i` has to land at `base + i` and nothing may
223
+ * disturb that.
224
+ *
225
+ * **Which is exactly why the source's holes come across as holes.** A source that has had
226
+ * `remove` called on it — every streamed set — cannot be compacted on the way in, because
227
+ * skipping its holes would make the offset a function of position. Copying them while calling
228
+ * them live would be worse: those slots carry the bytes of colliders that were deleted, and a
229
+ * set reporting them would have resurrected geometry a consumer believed was gone. So liveness
230
+ * is copied with the bytes, the holes land on this set's free list, and the next `add` fills
231
+ * them. **Requiring a dense source instead** was the other option and was rejected: it would
232
+ * refuse the only input this was asked for.
233
+ *
234
+ * What a churned source does cost is its dead slots as bytes copied and space held until the
235
+ * free list drains them. Bounded, visible in `bytes()`, and not a leak.
236
+ *
237
+ * **A copy and not a move.** The source is unchanged and stays usable, which is what lets a
238
+ * caller keep a prototype region and stamp it more than once.
239
+ *
240
+ * `join(a, b, c)` is this in a loop, which is why there is no second method for it.
241
+ */
242
+ absorb(other) {
243
+ if (other.cellSize !== this.cellSize) {
244
+ throw new Error(`ColliderSet.absorb: this set hashes on a ${this.cellSize} m grid and that one on ` +
245
+ `${other.cellSize} m. The copy is only valid while both agree which cell a coordinate ` +
246
+ 'falls in, and rehashing quietly would turn the cheap path into the expensive one at ' +
247
+ 'the moment you most need to know.');
248
+ }
249
+ const group = this.nextGroup++;
250
+ const base = this.slots;
251
+ this.ensureCapacity(base + other.slots);
252
+ this.slots = base + other.slots;
253
+ this.data.set(other.data.subarray(0, other.slots * STRIDE), base * STRIDE);
254
+ /*
255
+ * **The absorbed range starts unmarked.** These slots are past everything handed out, so they
256
+ * are already zero unless a growth reused an array — and `query` skips a slot whose stamp is
257
+ * the current mark, so one arriving marked would be missed on the query that first sees it.
258
+ * Cheaper to fill the range than to reason about which paths can leave a stamp behind.
259
+ */
260
+ this.seen.fill(0, base, base + other.slots);
261
+ const slots = [];
262
+ for (let i = other.slots - 1; i >= 0; i--) {
263
+ const slot = base + i;
264
+ this.shapes[slot] = other.shapes[i];
265
+ if (other.owner[i] === -1) {
266
+ this.owner[slot] = -1;
267
+ this.freeSlots.push(slot);
268
+ continue;
269
+ }
270
+ this.owner[slot] = group;
271
+ slots.push(slot);
272
+ this.live++;
273
+ }
274
+ /* Recorded ascending, because `drop` walks a group's slots backwards to free them descending
275
+ and the pop is what hands them back in order. */
276
+ slots.reverse();
277
+ /*
278
+ * The source's buckets hold only live indices — `remove` unhooked the rest — so every remapped
279
+ * index names a live slot and the offset needs no filtering.
280
+ */
281
+ for (const [key, bucket] of other.cells) {
282
+ const mine = this.cells.get(key);
283
+ if (mine === undefined)
284
+ this.cells.set(key, bucket.map((index) => index + base));
285
+ else
286
+ for (const index of bucket)
287
+ mine.push(index + base);
288
+ }
289
+ this.groups.set(group, slots);
290
+ return group;
291
+ }
292
+ /**
293
+ * Drop a group and everything in it.
294
+ *
295
+ * Refuses the base group and a group it does not hold; a streamer double-freeing a region is a
296
+ * bug, and a silent no-op hides it until the memory it was meant to release turns up in a
297
+ * budget.
298
+ */
299
+ remove(group) {
300
+ if (group === ColliderSet.BASE_GROUP) {
301
+ throw new Error("ColliderSet.remove: the constructor's own colliders cannot be dropped. A set meant to " +
302
+ 'stream is constructed empty and takes everything through `add`.');
303
+ }
304
+ const slots = this.groups.get(group);
305
+ if (slots === undefined) {
306
+ throw new Error(`ColliderSet.remove: no group ${group} here. It was never added, or it was already ` +
307
+ 'removed — either way something is keeping a handle it should not.');
308
+ }
309
+ this.drop(slots);
310
+ this.groups.delete(group);
311
+ }
312
+ /** The volume collider `index` really is, or undefined where it is its box. */
313
+ shapeAt(index) {
314
+ return this.shapes[index];
315
+ }
316
+ /**
317
+ * The packed bounds of collider `index`, written into `out`. Allocation-free.
318
+ *
319
+ * `data` is public and the stride is not. A reader outside this file that decodes
320
+ * `data[i * 6 + 1]` by hand keeps working right up until the packing changes, and
321
+ * then reports confident nonsense rather than failing.
322
+ */
323
+ bounds(index, out) {
324
+ const o = index * STRIDE;
325
+ out.minX = this.data[o];
326
+ out.minY = this.data[o + 1];
327
+ out.minZ = this.data[o + 2];
328
+ out.maxX = this.data[o + 3];
329
+ out.maxY = this.data[o + 4];
330
+ out.maxZ = this.data[o + 5];
331
+ }
332
+ query(minX, minY, minZ, maxX, maxY, maxZ, out) {
333
+ const s = this.cellSize;
334
+ const x0 = Math.floor(minX / s);
335
+ const x1 = Math.floor(maxX / s);
336
+ const y0 = Math.floor(minY / s);
337
+ const y1 = Math.floor(maxY / s);
338
+ const z0 = Math.floor(minZ / s);
339
+ const z1 = Math.floor(maxZ / s);
340
+ /*
341
+ * **The stamp outlives a rebuild now, so it can reach the end of its range.** A set used to be
342
+ * rebuilt every few seconds and got a fresh stamp with it; one mutable set lives a whole
343
+ * session. `stampAfter` answers 0 when the next mark would truncate negative in `seen`, at
344
+ * which point the array is cleared and marking starts again at 1 — never at 0, because `add`
345
+ * writes `seen[slot] = 0` on a recycled slot and a 0 mark would skip it.
346
+ */
347
+ let mark = stampAfter(this.stamp);
348
+ if (mark === 0) {
349
+ this.seen.fill(0);
350
+ mark = 1;
351
+ }
352
+ this.stamp = mark;
353
+ let found = 0;
354
+ for (let x = x0; x <= x1; x++) {
355
+ for (let y = y0; y <= y1; y++) {
356
+ for (let z = z0; z <= z1; z++) {
357
+ const bucket = this.cells.get(hash(x, y, z));
358
+ if (bucket === undefined)
359
+ continue;
360
+ for (let k = 0; k < bucket.length; k++) {
361
+ const index = bucket[k];
362
+ if (index === undefined || this.seen[index] === mark)
363
+ continue;
364
+ this.seen[index] = mark;
365
+ if (found < out.length)
366
+ out[found++] = index;
367
+ }
368
+ }
369
+ }
370
+ }
371
+ return found;
372
+ }
373
+ /**
374
+ * What this set costs in memory.
375
+ *
376
+ * **Because a consumer can count its own bytes and cannot see the engine's**, which makes a
377
+ * streaming budget half-measurable: the question "does the ring of regions around the player fit
378
+ * on a phone" has no answer without this.
379
+ *
380
+ * O(n) over slots and over the index, and it allocates a `Set` to count each distinct shape
381
+ * once. Not a per-frame call.
382
+ */
383
+ bytes() {
384
+ const slots = this.data.byteLength + this.seen.byteLength + this.owner.byteLength;
385
+ /*
386
+ * **Each distinct shape once.** A streamer that shares one shape across a hundred colliders
387
+ * wants the truth and not a hundred copies of it. Identity is the object, which is exactly how
388
+ * the colliders share it.
389
+ *
390
+ * A shape's `triangles` is not summed because a collider cannot carry a mesh: `colliderFromShape`
391
+ * refuses one, and a mesh belongs to `PhysicsWorld` as a static body.
392
+ */
393
+ const counted = new Set();
394
+ let shapes = 0;
395
+ for (let slot = 0; slot < this.slots; slot++) {
396
+ const shape = this.shapes[slot];
397
+ if (shape === undefined || counted.has(shape))
398
+ continue;
399
+ counted.add(shape);
400
+ shapes +=
401
+ shape.vertices.byteLength +
402
+ shape.faceNormals.byteLength +
403
+ shape.edgeDirs.byteLength +
404
+ shape.facePlanes.byteLength +
405
+ shape.faceVertexStart.byteLength +
406
+ shape.faceVertexIndices.byteLength;
407
+ }
408
+ /* Buckets and entries are counted exactly; the two constants above are the estimate, and the
409
+ comment on them says which half is which. */
410
+ let buckets = 0;
411
+ let entries = 0;
412
+ for (const bucket of this.cells.values()) {
413
+ buckets++;
414
+ entries += bucket.length;
415
+ }
416
+ const index = buckets * (BUCKET_OVERHEAD_BYTES + MAP_ENTRY_BYTES) + entries * 8;
417
+ return { slots, shapes, index, total: slots + shapes + index };
418
+ }
419
+ /**
420
+ * How many bucket entries name a slot nothing holds. Always zero.
421
+ *
422
+ * **The one defect in this structure that has no symptom of its own**, which is why it gets a
423
+ * check of its own. A `remove` that misses a bucket entry leaves the slot marked dead — so the
424
+ * fingerprint skips it and is right, and nothing walks it by index — while the stale entry goes
425
+ * on naming it: `query` returns it, and the sweep reads its stale bounds and collides with
426
+ * geometry that is gone. No measurement of frame time, query cost or hash can see that.
427
+ *
428
+ * O(total entries) and not something to call in a frame. For tests and for the streaming check.
429
+ */
430
+ auditCells() {
431
+ let dead = 0;
432
+ for (const bucket of this.cells.values()) {
433
+ for (const index of bucket)
434
+ if (this.owner[index] === -1)
435
+ dead++;
436
+ }
437
+ return dead;
438
+ }
439
+ /**
440
+ * Unhook slots from every cell they occupy, then free them.
441
+ *
442
+ * **The same triple loop `insert` uses**, so the two cannot disagree about which cells a box
443
+ * occupies — which is the disagreement that would leave an entry behind. A bucket entry goes by
444
+ * swapping the last one into its place and popping, because bucket order carries no meaning, and
445
+ * an emptied bucket leaves `cells` so a dropped region costs a `Map.get` that misses instead of
446
+ * a scan.
447
+ *
448
+ * **Slots are freed in descending order** so the last-in-first-out pop hands them back
449
+ * ascending: a region added after another is dropped then lands on a contiguous run of that
450
+ * region's slots. Incidental locality and not a guarantee — see the compaction note in the
451
+ * design if a churned set's query cost ever drifts.
452
+ */
453
+ drop(slots) {
454
+ const s = this.cellSize;
455
+ for (let k = slots.length - 1; k >= 0; k--) {
456
+ const slot = slots[k];
457
+ const o = slot * STRIDE;
458
+ const minX = this.data[o];
459
+ const minY = this.data[o + 1];
460
+ const minZ = this.data[o + 2];
461
+ const maxX = this.data[o + 3];
462
+ const maxY = this.data[o + 4];
463
+ const maxZ = this.data[o + 5];
464
+ for (let x = Math.floor(minX / s); x <= Math.floor(maxX / s); x++) {
465
+ for (let y = Math.floor(minY / s); y <= Math.floor(maxY / s); y++) {
466
+ for (let z = Math.floor(minZ / s); z <= Math.floor(maxZ / s); z++) {
467
+ const key = hash(x, y, z);
468
+ const bucket = this.cells.get(key);
469
+ if (bucket === undefined)
470
+ continue;
471
+ const at = bucket.indexOf(slot);
472
+ if (at === -1)
473
+ continue;
474
+ const last = bucket.pop();
475
+ if (at < bucket.length)
476
+ bucket[at] = last;
477
+ if (bucket.length === 0)
478
+ this.cells.delete(key);
479
+ }
480
+ }
481
+ }
482
+ this.owner[slot] = -1;
483
+ /* So a dropped region's geometry can be collected. A streamer that held these would keep
484
+ every world it had ever driven through. */
485
+ this.shapes[slot] = undefined;
486
+ this.freeSlots.push(slot);
487
+ this.live--;
488
+ }
489
+ }
490
+ /**
491
+ * Room for `n` slots, growing by doubling.
492
+ *
493
+ * **The shape is `island.ts`'s `ensureBodies`, with one difference worth naming**: that one
494
+ * allocates and discards, because it rebuilds its arrays from scratch every tick, and these
495
+ * carry a world that has to survive the growth. So they copy.
496
+ *
497
+ * A grown `seen` is zero-filled by the allocation, which is the same thing `add` writes on a
498
+ * recycled slot and for the same reason.
499
+ */
500
+ ensureCapacity(n) {
501
+ if (n <= this.owner.length)
502
+ return;
503
+ let size = this.owner.length || 1;
504
+ while (size < n)
505
+ size *= 2;
506
+ const data = new Float32Array(size * STRIDE);
507
+ data.set(this.data);
508
+ this.data = data;
509
+ const seen = new Int32Array(size);
510
+ seen.set(this.seen);
511
+ this.seen = seen;
512
+ const owner = new Int32Array(size).fill(-1);
513
+ owner.set(this.owner);
514
+ this.owner = owner;
515
+ this.shapes.length = size;
516
+ }
517
+ insert(index, b) {
518
+ const s = this.cellSize;
519
+ for (let x = Math.floor(b.minX / s); x <= Math.floor(b.maxX / s); x++) {
520
+ for (let y = Math.floor(b.minY / s); y <= Math.floor(b.maxY / s); y++) {
521
+ for (let z = Math.floor(b.minZ / s); z <= Math.floor(b.maxZ / s); z++) {
522
+ const key = hash(x, y, z);
523
+ const bucket = this.cells.get(key);
524
+ if (bucket === undefined)
525
+ this.cells.set(key, [index]);
526
+ else
527
+ bucket.push(index);
528
+ }
529
+ }
530
+ }
531
+ }
532
+ }
533
+ /**
534
+ * The stamp a query marks with next, or 0 meaning `seen` has to be cleared first.
535
+ *
536
+ * **Its own function because it cannot be reached by driving `query`.** The condition needs 2^31
537
+ * queries, which no test will do, so the decision is asserted where it is made rather than through
538
+ * the behaviour it protects.
539
+ */
540
+ export function stampAfter(previous) {
541
+ return previous >= 0x7fffffff ? 0 : previous + 1;
542
+ }
543
+ /** Cheap 3D integer hash. A collision only costs an extra bounds test. */
544
+ function hash(x, y, z) {
545
+ return (Math.imul(x, 73856093) ^ Math.imul(y, 19349663) ^ Math.imul(z, 83492791)) | 0;
546
+ }
@@ -0,0 +1,70 @@
1
+ import type { Manifold, ShapePose } from './manifold.ts';
2
+ import type { ConvexShape } from './shape.ts';
3
+ /**
4
+ * The most parts one body may be made of.
5
+ *
6
+ * **It lives here rather than beside `decomposeConvex`, and that is a measurement rather than a
7
+ * preference.** It was declared in `decompose.ts`, which is offline and which nothing in a frame
8
+ * should reach; importing one integer from there put the whole decomposition — the voxeliser, the
9
+ * flood fill, the merge, the direction table — into every bundle that touches the physics runtime.
10
+ * `physics-only` moved outside its 3% band and an A/B against this one import line was what said so.
11
+ *
12
+ * A constant about how many shapes a body may hold is a fact about bodies. `decompose.ts` imports it
13
+ * from here to cap `maxHulls`, which is the direction the dependency should have run in from the
14
+ * start.
15
+ */
16
+ export declare const MAX_BODY_PARTS = 32;
17
+ /**
18
+ * A body made of several convex shapes, against whatever it meets.
19
+ *
20
+ * **The half of convex decomposition that lives in the tick.** `decomposeConvex` turns a concave
21
+ * mesh into hulls offline; without this there is nowhere for them to go, because `BodySet.shape`
22
+ * holds one shape per body and a decomposed body would have to become several bodies held together
23
+ * by joints. `world.ts`'s own refusal message said the runtime already accepted hulls, and it did
24
+ * not.
25
+ *
26
+ * **A part carries no offset and no rotation, and that is not a simplification.** A `ConvexShape` is
27
+ * a point cloud in the body's frame — `shape.ts` says so where it defines `vertices` — so a
28
+ * decomposition's parts are already where they belong. The kinematic `Body.parts` carries `ox/oy/oz`
29
+ * because it wraps shapes a caller built at the origin and placed afterwards; nothing here does
30
+ * that, and a field that is always zero is a field that is eventually set wrong.
31
+ *
32
+ * **This fills a set of manifolds where an ordinary pair fills one**, which is the shape
33
+ * `meshContact.ts` established: `prepareContact` appends a constraint rather than replacing one and
34
+ * `storeWarm` accumulates by pair, so the solver needed nothing new.
35
+ */
36
+ /**
37
+ * How many contact planes one pair may produce.
38
+ *
39
+ * **The same number as `MAX_BODY_PARTS`, so a compound against a single convex shape can never
40
+ * overflow**: every part gets a plane. Two compounds can, at up to 32 by 32 part pairs against 32
41
+ * slots, and the overflow rule there is the mesh path's — stop when the set is full. That is the one
42
+ * place in this design where a contact can be dropped without anybody being told, and what would
43
+ * make it matter is two 32-part bodies interpenetrating deeply.
44
+ */
45
+ export declare const MAX_PAIR_MANIFOLDS = 32;
46
+ /** Whether a body is made of parts rather than of one shape. */
47
+ export declare function isCompound(parts: readonly ConvexShape[] | undefined): parts is readonly ConvexShape[];
48
+ /** How many shapes a body presents to the narrow phase, whichever way it is built. */
49
+ export declare function partCount(shape: ConvexShape | undefined, parts: readonly ConvexShape[] | undefined): number;
50
+ /** One of them, by index. */
51
+ export declare function partAt(shape: ConvexShape | undefined, parts: readonly ConvexShape[] | undefined, index: number): ConvexShape;
52
+ /**
53
+ * The largest distance from a body's own origin to any point of any part.
54
+ *
55
+ * What `boundsOf` needs, and it is a maximum rather than a sum because every part is already in the
56
+ * body's frame: a part built off to one side carries that offset inside its own `boundRadius`.
57
+ */
58
+ export declare function partsBoundRadius(parts: readonly ConvexShape[]): number;
59
+ /**
60
+ * Every touching pair of parts, as one manifold each.
61
+ *
62
+ * **A mesh partner is the pair this row exists for**, and it gets a rule of its own. A decomposed
63
+ * body falling onto a level is a dynamic compound against a static mesh, so refusing it would refuse
64
+ * the case. Each part goes through `collideMesh` unchanged, and the budget is divided between the
65
+ * parts that find candidates, **never below one each**. That is a deliberate departure from the mesh
66
+ * path's own first-come rule: `collideMesh` stops at eight and drops the rest, which is defensible
67
+ * when the ninth candidate is a triangle and indefensible when it is a whole limb of the body — one
68
+ * leg of a chair sinking through a floor while the other three hold.
69
+ */
70
+ export declare function collideCompound(shapeA: ConvexShape | undefined, partsA: readonly ConvexShape[] | undefined, poseA: ShapePose, shapeB: ConvexShape | undefined, partsB: readonly ConvexShape[] | undefined, poseB: ShapePose, margin: number, out: readonly Manifold[]): number;