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