@flare-engine/dynamics 0.4.8 → 0.4.11
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/README.md +109 -10
- package/dist/index.d.mts +105 -8
- package/dist/index.d.mts.map +1 -1
- package/dist/index.d.ts +105 -8
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +301 -32
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +296 -33
- package/dist/index.mjs.map +1 -1
- package/package.json +15 -3
package/dist/index.mjs
CHANGED
|
@@ -106,6 +106,58 @@ function writeAabb(verts, vOffset, out, offset) {
|
|
|
106
106
|
out[offset + 2] = maxX;
|
|
107
107
|
out[offset + 3] = maxY;
|
|
108
108
|
}
|
|
109
|
+
// src/timeline.ts
|
|
110
|
+
const DEFAULT_STRIDE = 3;
|
|
111
|
+
const DEFAULT_MAX_FRAMES = 900;
|
|
112
|
+
function assertCount(name, value, min) {
|
|
113
|
+
if (!Number.isInteger(value) || value < min) throw new RangeError(`createTimeline: ${name} must be an integer >= ${min}, got ${value}`);
|
|
114
|
+
}
|
|
115
|
+
function createTimeline(elementCount, dt, opts = {}) {
|
|
116
|
+
assertCount("elementCount", elementCount, 0);
|
|
117
|
+
if (!Number.isFinite(dt) || dt <= 0) throw new RangeError(`createTimeline: dt must be a finite number > 0, got ${dt}`);
|
|
118
|
+
const stride = opts.stride ?? DEFAULT_STRIDE;
|
|
119
|
+
const maxFrames = opts.maxFrames ?? DEFAULT_MAX_FRAMES;
|
|
120
|
+
assertCount("stride", stride, 1);
|
|
121
|
+
assertCount("maxFrames", maxFrames, 1);
|
|
122
|
+
const frameStride = elementCount * stride;
|
|
123
|
+
const buf = new Float32Array(Math.max(1, frameStride) * maxFrames);
|
|
124
|
+
let frameCount = 0;
|
|
125
|
+
return {
|
|
126
|
+
elementCount,
|
|
127
|
+
get frameCount() {
|
|
128
|
+
return frameCount;
|
|
129
|
+
},
|
|
130
|
+
get duration() {
|
|
131
|
+
return frameCount > 1 ? (frameCount - 1) * dt : 0;
|
|
132
|
+
},
|
|
133
|
+
record(writer, until) {
|
|
134
|
+
if (elementCount === 0) return;
|
|
135
|
+
while (frameCount < maxFrames) {
|
|
136
|
+
writer(buf, frameCount * frameStride);
|
|
137
|
+
frameCount++;
|
|
138
|
+
if (until === void 0 || until()) return;
|
|
139
|
+
}
|
|
140
|
+
},
|
|
141
|
+
sampleInto(progress, out) {
|
|
142
|
+
const n = frameStride;
|
|
143
|
+
if (frameCount === 0) {
|
|
144
|
+
out.fill(0, 0, n);
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
const f = (progress > 1 ? 1 : progress >= 0 ? progress : 0) * (frameCount - 1);
|
|
148
|
+
const i0 = Math.floor(f);
|
|
149
|
+
const a = f - i0;
|
|
150
|
+
const i1 = i0 + 1 < frameCount ? i0 + 1 : i0;
|
|
151
|
+
const b0 = i0 * n;
|
|
152
|
+
const b1 = i1 * n;
|
|
153
|
+
for (let o = 0; o < n; o++) {
|
|
154
|
+
const lo = buf[b0 + o];
|
|
155
|
+
const hi = buf[b1 + o];
|
|
156
|
+
out[o] = lo + (hi - lo) * a;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
}
|
|
109
161
|
// src/manifold.ts
|
|
110
162
|
/** OBB-OBB collision: separating-axis test + reference/incident face clipping,
|
|
111
163
|
* building a 1-2 point contact manifold with per-point penetration depth.
|
|
@@ -316,6 +368,12 @@ function createManifold() {
|
|
|
316
368
|
return {
|
|
317
369
|
a: void 0,
|
|
318
370
|
b: void 0,
|
|
371
|
+
ai: 0,
|
|
372
|
+
bi: 0,
|
|
373
|
+
aInvMass: 0,
|
|
374
|
+
aInvInertia: 0,
|
|
375
|
+
bInvMass: 0,
|
|
376
|
+
bInvInertia: 0,
|
|
319
377
|
normalX: 0,
|
|
320
378
|
normalY: 0,
|
|
321
379
|
tangentX: 0,
|
|
@@ -325,19 +383,24 @@ function createManifold() {
|
|
|
325
383
|
points: [createManifoldPoint(), createManifoldPoint()]
|
|
326
384
|
};
|
|
327
385
|
}
|
|
328
|
-
function createPool() {
|
|
386
|
+
function createPool(sleep = null) {
|
|
329
387
|
return {
|
|
330
388
|
manifolds: [],
|
|
331
389
|
manifoldCount: 0,
|
|
332
390
|
verts: /* @__PURE__ */ new Float64Array(0),
|
|
333
391
|
normals: /* @__PURE__ */ new Float64Array(0),
|
|
334
392
|
aabbs: /* @__PURE__ */ new Float64Array(0),
|
|
335
|
-
capacityBodies: 0
|
|
393
|
+
capacityBodies: 0,
|
|
394
|
+
sleep,
|
|
395
|
+
sleepCounter: /* @__PURE__ */ new Int32Array(0),
|
|
396
|
+
asleep: /* @__PURE__ */ new Uint8Array(0),
|
|
397
|
+
sleepSnapshot: /* @__PURE__ */ new Float64Array(0),
|
|
398
|
+
sleepOwner: []
|
|
336
399
|
};
|
|
337
400
|
}
|
|
338
|
-
/** Grows the per-body
|
|
339
|
-
*
|
|
340
|
-
* large enough. */
|
|
401
|
+
/** Grows the per-body buffers to hold at least `count` bodies (8 doubles per
|
|
402
|
+
* body for verts/normals, 4 for the AABB, 3 for the sleep snapshot). No-op
|
|
403
|
+
* if already large enough. */
|
|
341
404
|
function ensureBodyCapacity(pool, count) {
|
|
342
405
|
if (count <= pool.capacityBodies) return;
|
|
343
406
|
let cap = pool.capacityBodies === 0 ? 8 : pool.capacityBodies;
|
|
@@ -345,6 +408,16 @@ function ensureBodyCapacity(pool, count) {
|
|
|
345
408
|
pool.verts = new Float64Array(cap * 8);
|
|
346
409
|
pool.normals = new Float64Array(cap * 8);
|
|
347
410
|
pool.aabbs = new Float64Array(cap * 4);
|
|
411
|
+
const sleepCounter = new Int32Array(cap);
|
|
412
|
+
sleepCounter.set(pool.sleepCounter);
|
|
413
|
+
pool.sleepCounter = sleepCounter;
|
|
414
|
+
const asleep = new Uint8Array(cap);
|
|
415
|
+
asleep.set(pool.asleep);
|
|
416
|
+
pool.asleep = asleep;
|
|
417
|
+
const sleepSnapshot = new Float64Array(cap * 3);
|
|
418
|
+
sleepSnapshot.set(pool.sleepSnapshot);
|
|
419
|
+
pool.sleepSnapshot = sleepSnapshot;
|
|
420
|
+
while (pool.sleepOwner.length < cap) pool.sleepOwner.push(void 0);
|
|
348
421
|
pool.capacityBodies = cap;
|
|
349
422
|
}
|
|
350
423
|
/** Grows `pool.manifolds` to hold at least `count` entries, appending
|
|
@@ -358,6 +431,23 @@ function ensureManifoldCapacity(pool, count) {
|
|
|
358
431
|
const BAUMGARTE = .2;
|
|
359
432
|
const PENETRATION_SLOP = .5;
|
|
360
433
|
const pools = /* @__PURE__ */ new WeakMap();
|
|
434
|
+
function nowMs() {
|
|
435
|
+
if (typeof performance !== "undefined" && typeof performance.now === "function") return performance.now();
|
|
436
|
+
return Date.now();
|
|
437
|
+
}
|
|
438
|
+
function copySleepOptions(sleep) {
|
|
439
|
+
const { linearThreshold, angularThreshold, steps } = sleep;
|
|
440
|
+
if (!Number.isFinite(linearThreshold) || linearThreshold < 0) throw new RangeError(`createWorld: sleep.linearThreshold must be a finite number >= 0, got ${linearThreshold}`);
|
|
441
|
+
if (!Number.isFinite(angularThreshold) || angularThreshold < 0) throw new RangeError(`createWorld: sleep.angularThreshold must be a finite number >= 0, got ${angularThreshold}`);
|
|
442
|
+
if (!Number.isInteger(steps) || steps < 1) throw new RangeError(`createWorld: sleep.steps must be an integer >= 1, got ${steps}`);
|
|
443
|
+
return {
|
|
444
|
+
linearThreshold,
|
|
445
|
+
angularThreshold,
|
|
446
|
+
steps
|
|
447
|
+
};
|
|
448
|
+
}
|
|
449
|
+
/** Throws `RangeError` when `opts.sleep` has a negative or non-finite
|
|
450
|
+
* threshold or a `steps` that is not an integer >= 1. */
|
|
361
451
|
function createWorld(opts = {}) {
|
|
362
452
|
const world = {
|
|
363
453
|
bodies: [],
|
|
@@ -370,7 +460,7 @@ function createWorld(opts = {}) {
|
|
|
370
460
|
},
|
|
371
461
|
iterations: opts.iterations ?? 10
|
|
372
462
|
};
|
|
373
|
-
pools.set(world, createPool());
|
|
463
|
+
pools.set(world, createPool(opts.sleep ? copySleepOptions(opts.sleep) : null));
|
|
374
464
|
return world;
|
|
375
465
|
}
|
|
376
466
|
function addBody(world, body) {
|
|
@@ -386,16 +476,37 @@ function relativeVelocityAlong(a, b, raX, raY, rbX, rbY, dx, dy) {
|
|
|
386
476
|
const vby = b.velocity.y + b.angularVelocity * rbX;
|
|
387
477
|
return (vbx - vax) * dx + (vby - vay) * dy;
|
|
388
478
|
}
|
|
389
|
-
function applyImpulse(body, ix, iy, rx, ry) {
|
|
390
|
-
if (
|
|
391
|
-
body.velocity.x += ix *
|
|
392
|
-
body.velocity.y += iy *
|
|
393
|
-
body.angularVelocity +=
|
|
479
|
+
function applyImpulse(body, invMass, invInertia, ix, iy, rx, ry) {
|
|
480
|
+
if (invMass === 0) return;
|
|
481
|
+
body.velocity.x += ix * invMass;
|
|
482
|
+
body.velocity.y += iy * invMass;
|
|
483
|
+
body.angularVelocity += invInertia * (rx * iy - ry * ix);
|
|
484
|
+
}
|
|
485
|
+
/** Binds every slot to the body now at its index, resetting a slot whose body
|
|
486
|
+
* changed (awake, counter 0), then wakes any sleeper the consumer wrote to:
|
|
487
|
+
* a non-zero velocity (sleepers are held at exactly 0) or a position/angle
|
|
488
|
+
* that differs from the snapshot taken at sleep. */
|
|
489
|
+
function syncSleepSlots(pool, bodies) {
|
|
490
|
+
const { sleepOwner, asleep, sleepCounter, sleepSnapshot } = pool;
|
|
491
|
+
for (let i = 0; i < bodies.length; i++) {
|
|
492
|
+
const body = bodies[i];
|
|
493
|
+
if (sleepOwner[i] !== body) {
|
|
494
|
+
sleepOwner[i] = body;
|
|
495
|
+
asleep[i] = 0;
|
|
496
|
+
sleepCounter[i] = 0;
|
|
497
|
+
continue;
|
|
498
|
+
}
|
|
499
|
+
if (asleep[i] === 0) continue;
|
|
500
|
+
const s = i * 3;
|
|
501
|
+
if (body.velocity.x !== 0 || body.velocity.y !== 0 || body.angularVelocity !== 0 || body.position.x !== sleepSnapshot[s] || body.position.y !== sleepSnapshot[s + 1] || body.angle !== sleepSnapshot[s + 2]) {
|
|
502
|
+
asleep[i] = 0;
|
|
503
|
+
sleepCounter[i] = 0;
|
|
504
|
+
}
|
|
505
|
+
}
|
|
394
506
|
}
|
|
395
507
|
/** Writes every body's world-space vertices, outward normals, and AABB into
|
|
396
508
|
* `pool`, once per step, so the broadphase pair loop never recomputes them. */
|
|
397
509
|
function writeGeometry(pool, bodies) {
|
|
398
|
-
ensureBodyCapacity(pool, bodies.length);
|
|
399
510
|
for (let i = 0; i < bodies.length; i++) {
|
|
400
511
|
const b = bodies[i];
|
|
401
512
|
const vOffset = i * 8;
|
|
@@ -406,12 +517,14 @@ function writeGeometry(pool, bodies) {
|
|
|
406
517
|
}
|
|
407
518
|
}
|
|
408
519
|
/** Broadphase (precomputed-AABB reject) + narrowphase (OBB manifold),
|
|
409
|
-
* writing surviving contacts into `pool.manifolds[0..manifoldCount)`.
|
|
520
|
+
* writing surviving contacts into `pool.manifolds[0..manifoldCount)`. A pair
|
|
521
|
+
* whose two bodies are each static or asleep is skipped. */
|
|
410
522
|
function collectManifolds(pool, bodies) {
|
|
411
523
|
pool.manifoldCount = 0;
|
|
412
|
-
const aabbs = pool
|
|
524
|
+
const { aabbs, asleep } = pool;
|
|
413
525
|
for (let i = 0; i < bodies.length; i++) {
|
|
414
526
|
const a = bodies[i];
|
|
527
|
+
const aResting = isStatic(a) || asleep[i] === 1;
|
|
415
528
|
const aOff = i * 4;
|
|
416
529
|
const aMinX = aabbs[aOff];
|
|
417
530
|
const aMinY = aabbs[aOff + 1];
|
|
@@ -419,7 +532,7 @@ function collectManifolds(pool, bodies) {
|
|
|
419
532
|
const aMaxY = aabbs[aOff + 3];
|
|
420
533
|
for (let j = i + 1; j < bodies.length; j++) {
|
|
421
534
|
const b = bodies[j];
|
|
422
|
-
if (
|
|
535
|
+
if (aResting && (isStatic(b) || asleep[j] === 1)) continue;
|
|
423
536
|
const bOff = j * 4;
|
|
424
537
|
const bMinX = aabbs[bOff];
|
|
425
538
|
const bMinY = aabbs[bOff + 1];
|
|
@@ -428,17 +541,52 @@ function collectManifolds(pool, bodies) {
|
|
|
428
541
|
if (aMaxX < bMinX || aMinX > bMaxX || aMaxY < bMinY || aMinY > bMaxY) continue;
|
|
429
542
|
ensureManifoldCapacity(pool, pool.manifoldCount + 1);
|
|
430
543
|
const m = pool.manifolds[pool.manifoldCount];
|
|
431
|
-
if (collideInto(a, b, pool.verts, pool.normals, i, j, m))
|
|
544
|
+
if (collideInto(a, b, pool.verts, pool.normals, i, j, m)) {
|
|
545
|
+
m.ai = i;
|
|
546
|
+
m.bi = j;
|
|
547
|
+
pool.manifoldCount++;
|
|
548
|
+
}
|
|
432
549
|
}
|
|
433
550
|
}
|
|
434
551
|
}
|
|
552
|
+
/** Wakes a sleeper touched by an awake body that was moving on the previous
|
|
553
|
+
* step (sleep counter 0), giving it this step's gravity so it continues as
|
|
554
|
+
* if it had been awake for the integration. A sleeper touched only by a
|
|
555
|
+
* settling body stays asleep. Pairs of two resting bodies never reach here,
|
|
556
|
+
* so a manifold with one sleeper always has an awake dynamic partner. */
|
|
557
|
+
function wakeByContact(pool, gravity, dt) {
|
|
558
|
+
const { asleep, sleepCounter } = pool;
|
|
559
|
+
for (let mi = 0; mi < pool.manifoldCount; mi++) {
|
|
560
|
+
const m = pool.manifolds[mi];
|
|
561
|
+
const aAsleep = asleep[m.ai] === 1;
|
|
562
|
+
if (aAsleep === (asleep[m.bi] === 1)) continue;
|
|
563
|
+
const sleeperIdx = aAsleep ? m.ai : m.bi;
|
|
564
|
+
if (sleepCounter[aAsleep ? m.bi : m.ai] !== 0) continue;
|
|
565
|
+
asleep[sleeperIdx] = 0;
|
|
566
|
+
sleepCounter[sleeperIdx] = 0;
|
|
567
|
+
const sleeper = aAsleep ? m.a : m.b;
|
|
568
|
+
sleeper.velocity.x += gravity.x * dt;
|
|
569
|
+
sleeper.velocity.y += gravity.y * dt;
|
|
570
|
+
}
|
|
571
|
+
}
|
|
435
572
|
/** Derives per-point solver mass terms and the velocity/position bias for
|
|
436
573
|
* every live manifold, writing them onto the manifold's own contact points. */
|
|
437
574
|
function prepare(pool, gravity, dt) {
|
|
438
575
|
const restThreshold = Math.hypot(gravity.x, gravity.y) * dt;
|
|
576
|
+
const { asleep } = pool;
|
|
439
577
|
for (let mi = 0; mi < pool.manifoldCount; mi++) {
|
|
440
578
|
const m = pool.manifolds[mi];
|
|
441
579
|
const { a, b } = m;
|
|
580
|
+
const aAsleep = asleep[m.ai] === 1;
|
|
581
|
+
const bAsleep = asleep[m.bi] === 1;
|
|
582
|
+
const aInvMass = aAsleep ? 0 : a.invMass;
|
|
583
|
+
const aInvInertia = aAsleep ? 0 : a.invInertia;
|
|
584
|
+
const bInvMass = bAsleep ? 0 : b.invMass;
|
|
585
|
+
const bInvInertia = bAsleep ? 0 : b.invInertia;
|
|
586
|
+
m.aInvMass = aInvMass;
|
|
587
|
+
m.aInvInertia = aInvInertia;
|
|
588
|
+
m.bInvMass = bInvMass;
|
|
589
|
+
m.bInvInertia = bInvInertia;
|
|
442
590
|
const normalX = m.normalX;
|
|
443
591
|
const normalY = m.normalY;
|
|
444
592
|
const tangentX = m.tangentX;
|
|
@@ -452,10 +600,10 @@ function prepare(pool, gravity, dt) {
|
|
|
452
600
|
const rbY = cp.y - b.position.y;
|
|
453
601
|
const rnA = raX * normalY - raY * normalX;
|
|
454
602
|
const rnB = rbX * normalY - rbY * normalX;
|
|
455
|
-
const kNormal =
|
|
603
|
+
const kNormal = aInvMass + bInvMass + aInvInertia * rnA * rnA + bInvInertia * rnB * rnB;
|
|
456
604
|
const rtA = raX * tangentY - raY * tangentX;
|
|
457
605
|
const rtB = rbX * tangentY - rbY * tangentX;
|
|
458
|
-
const kTangent =
|
|
606
|
+
const kTangent = aInvMass + bInvMass + aInvInertia * rtA * rtA + bInvInertia * rtB * rtB;
|
|
459
607
|
const vn = relativeVelocityAlong(a, b, raX, raY, rbX, rbY, normalX, normalY);
|
|
460
608
|
const restitutionBias = vn < -restThreshold ? -restitution * vn : 0;
|
|
461
609
|
const baumgarte = BAUMGARTE / dt * Math.max(0, cp.penetration - PENETRATION_SLOP);
|
|
@@ -473,7 +621,7 @@ function prepare(pool, gravity, dt) {
|
|
|
473
621
|
function solve(pool) {
|
|
474
622
|
for (let mi = 0; mi < pool.manifoldCount; mi++) {
|
|
475
623
|
const m = pool.manifolds[mi];
|
|
476
|
-
const { a, b, friction } = m;
|
|
624
|
+
const { a, b, friction, aInvMass, aInvInertia, bInvMass, bInvInertia } = m;
|
|
477
625
|
const normalX = m.normalX;
|
|
478
626
|
const normalY = m.normalY;
|
|
479
627
|
const tangentX = m.tangentX;
|
|
@@ -491,8 +639,8 @@ function solve(pool) {
|
|
|
491
639
|
cp.normalImpulse = newNormal;
|
|
492
640
|
const pnX = normalX * dPn;
|
|
493
641
|
const pnY = normalY * dPn;
|
|
494
|
-
applyImpulse(a, -pnX, -pnY, raX, raY);
|
|
495
|
-
applyImpulse(b, pnX, pnY, rbX, rbY);
|
|
642
|
+
applyImpulse(a, aInvMass, aInvInertia, -pnX, -pnY, raX, raY);
|
|
643
|
+
applyImpulse(b, bInvMass, bInvInertia, pnX, pnY, rbX, rbY);
|
|
496
644
|
const vt = relativeVelocityAlong(a, b, raX, raY, rbX, rbY, tangentX, tangentY);
|
|
497
645
|
let dPt = cp.massTangent * -vt;
|
|
498
646
|
const maxPt = friction * cp.normalImpulse;
|
|
@@ -501,20 +649,55 @@ function solve(pool) {
|
|
|
501
649
|
cp.tangentImpulse = newTangent;
|
|
502
650
|
const ptX = tangentX * dPt;
|
|
503
651
|
const ptY = tangentY * dPt;
|
|
504
|
-
applyImpulse(a, -ptX, -ptY, raX, raY);
|
|
505
|
-
applyImpulse(b, ptX, ptY, rbX, rbY);
|
|
652
|
+
applyImpulse(a, aInvMass, aInvInertia, -ptX, -ptY, raX, raY);
|
|
653
|
+
applyImpulse(b, bInvMass, bInvInertia, ptX, ptY, rbX, rbY);
|
|
506
654
|
}
|
|
507
655
|
}
|
|
508
656
|
}
|
|
657
|
+
/** Puts every awake dynamic body that stayed under both thresholds for
|
|
658
|
+
* `sleep.steps` consecutive steps to sleep: velocities zeroed, flag set,
|
|
659
|
+
* position/angle snapshotted for the write-wake check. */
|
|
660
|
+
function updateSleep(pool, sleep, bodies) {
|
|
661
|
+
const { asleep, sleepCounter, sleepSnapshot } = pool;
|
|
662
|
+
const linear2 = sleep.linearThreshold * sleep.linearThreshold;
|
|
663
|
+
const angular2 = sleep.angularThreshold * sleep.angularThreshold;
|
|
664
|
+
for (let i = 0; i < bodies.length; i++) {
|
|
665
|
+
const body = bodies[i];
|
|
666
|
+
if (isStatic(body) || asleep[i] === 1) continue;
|
|
667
|
+
const vx = body.velocity.x;
|
|
668
|
+
const vy = body.velocity.y;
|
|
669
|
+
const w = body.angularVelocity;
|
|
670
|
+
if (vx * vx + vy * vy < linear2 && w * w < angular2) {
|
|
671
|
+
const count = sleepCounter[i] + 1;
|
|
672
|
+
sleepCounter[i] = count;
|
|
673
|
+
if (count < sleep.steps) continue;
|
|
674
|
+
body.velocity.x = 0;
|
|
675
|
+
body.velocity.y = 0;
|
|
676
|
+
body.angularVelocity = 0;
|
|
677
|
+
asleep[i] = 1;
|
|
678
|
+
const s = i * 3;
|
|
679
|
+
sleepSnapshot[s] = body.position.x;
|
|
680
|
+
sleepSnapshot[s + 1] = body.position.y;
|
|
681
|
+
sleepSnapshot[s + 2] = body.angle;
|
|
682
|
+
} else sleepCounter[i] = 0;
|
|
683
|
+
}
|
|
684
|
+
}
|
|
509
685
|
/** Advance the world by `dt` seconds (fixed steps recommended).
|
|
510
686
|
*
|
|
511
|
-
*
|
|
512
|
-
*
|
|
513
|
-
* impulse solver with Baumgarte
|
|
514
|
-
* velocity to exactly zero at rest, only the position
|
|
515
|
-
*
|
|
516
|
-
*
|
|
517
|
-
*
|
|
687
|
+
* By default (no `WorldOptions.sleep`) resting contacts settle to a stable
|
|
688
|
+
* *position* but retain a small, bounded velocity oscillation at the
|
|
689
|
+
* penetration-slop equilibrium — a sequential-impulse solver with Baumgarte
|
|
690
|
+
* bias does not converge velocity to exactly zero at rest, only the position
|
|
691
|
+
* does. `WorldOptions.sleep` opts into per-body sleeping: a dynamic body
|
|
692
|
+
* under both thresholds for `steps` consecutive steps has its velocities
|
|
693
|
+
* zeroed and is skipped by integration and by pair tests against other
|
|
694
|
+
* resting bodies until an awake body touches it, the consumer writes its
|
|
695
|
+
* velocity, position or angle, or `wakeBody` is called. There are no
|
|
696
|
+
* islands, so a woken stack wakes one level per step, and a sleeper does not
|
|
697
|
+
* follow a static body that is moved under it. Damping stays out of scope.
|
|
698
|
+
*
|
|
699
|
+
* A world that never went through `createWorld` (a spread copy or a
|
|
700
|
+
* `structuredClone`) is adopted on its first `step` with sleeping off. */
|
|
518
701
|
function step(world, dt) {
|
|
519
702
|
if (dt <= 0) return;
|
|
520
703
|
let pool = pools.get(world);
|
|
@@ -523,24 +706,104 @@ function step(world, dt) {
|
|
|
523
706
|
pools.set(world, pool);
|
|
524
707
|
}
|
|
525
708
|
const { bodies } = world;
|
|
709
|
+
ensureBodyCapacity(pool, bodies.length);
|
|
710
|
+
const { sleep, asleep } = pool;
|
|
711
|
+
if (sleep !== null) syncSleepSlots(pool, bodies);
|
|
526
712
|
for (let i = 0; i < bodies.length; i++) {
|
|
527
713
|
const body = bodies[i];
|
|
528
|
-
if (isStatic(body)) continue;
|
|
714
|
+
if (isStatic(body) || asleep[i] === 1) continue;
|
|
529
715
|
body.velocity.x += world.gravity.x * dt;
|
|
530
716
|
body.velocity.y += world.gravity.y * dt;
|
|
531
717
|
}
|
|
532
718
|
writeGeometry(pool, bodies);
|
|
533
719
|
collectManifolds(pool, bodies);
|
|
720
|
+
if (sleep !== null) wakeByContact(pool, world.gravity, dt);
|
|
534
721
|
prepare(pool, world.gravity, dt);
|
|
535
722
|
for (let iter = 0; iter < world.iterations; iter++) solve(pool);
|
|
536
723
|
for (let i = 0; i < bodies.length; i++) {
|
|
537
724
|
const body = bodies[i];
|
|
538
|
-
if (isStatic(body)) continue;
|
|
725
|
+
if (isStatic(body) || asleep[i] === 1) continue;
|
|
539
726
|
body.position.x += body.velocity.x * dt;
|
|
540
727
|
body.position.y += body.velocity.y * dt;
|
|
541
728
|
body.angle += body.angularVelocity * dt;
|
|
542
729
|
}
|
|
730
|
+
if (sleep !== null) updateSleep(pool, sleep, bodies);
|
|
731
|
+
}
|
|
732
|
+
/** `true` when `body` is asleep in `world`. `false` when the world has
|
|
733
|
+
* sleeping off or never went through `createWorld`, when `body` is not in
|
|
734
|
+
* `world.bodies`, or when its slot has not been bound to it by a `step` yet.
|
|
735
|
+
* O(n) in `world.bodies.length` (an `indexOf`); poll it per body, not per
|
|
736
|
+
* contact. */
|
|
737
|
+
function isSleeping(world, body) {
|
|
738
|
+
const pool = pools.get(world);
|
|
739
|
+
if (!pool || pool.sleep === null) return false;
|
|
740
|
+
const i = world.bodies.indexOf(body);
|
|
741
|
+
if (i < 0 || pool.sleepOwner[i] !== body) return false;
|
|
742
|
+
return pool.asleep[i] === 1;
|
|
743
|
+
}
|
|
744
|
+
/** `true` when every dynamic body in `world.bodies` is asleep — vacuously
|
|
745
|
+
* true with no dynamic bodies, `false` with sleeping off and at least one.
|
|
746
|
+
* The bake stop rule for `stepUntil`: `done: () => isAtRest(world)`. */
|
|
747
|
+
function isAtRest(world) {
|
|
748
|
+
const pool = pools.get(world);
|
|
749
|
+
const { bodies } = world;
|
|
750
|
+
for (let i = 0; i < bodies.length; i++) {
|
|
751
|
+
const body = bodies[i];
|
|
752
|
+
if (isStatic(body)) continue;
|
|
753
|
+
if (!pool || pool.sleep === null || pool.sleepOwner[i] !== body || pool.asleep[i] !== 1) return false;
|
|
754
|
+
}
|
|
755
|
+
return true;
|
|
756
|
+
}
|
|
757
|
+
/** Wakes `body` if it is asleep in `world` (flag cleared, counter 0, its
|
|
758
|
+
* velocities stay 0); the next `step` integrates it again. No-op otherwise,
|
|
759
|
+
* including with sleeping off or a body not in `world.bodies`. Use it for a
|
|
760
|
+
* sleeper whose support was removed from `world.bodies` or moved — nothing
|
|
761
|
+
* else wakes it. */
|
|
762
|
+
function wakeBody(world, body) {
|
|
763
|
+
const pool = pools.get(world);
|
|
764
|
+
if (!pool || pool.sleep === null) return;
|
|
765
|
+
const i = world.bodies.indexOf(body);
|
|
766
|
+
if (i < 0 || pool.sleepOwner[i] !== body || pool.asleep[i] !== 1) return;
|
|
767
|
+
pool.asleep[i] = 0;
|
|
768
|
+
pool.sleepCounter[i] = 0;
|
|
769
|
+
}
|
|
770
|
+
/** Repeats `step(world, dt)` until, checked in this order after each step,
|
|
771
|
+
* `done()` returns true (`complete: true`), `steps >= maxSteps`, or
|
|
772
|
+
* `now() - start >= budgetMs` (both `complete: false`). Always takes at least
|
|
773
|
+
* one step, so a zero budget still makes progress. Writes `out` and returns it;
|
|
774
|
+
* the zero-alloc per-frame form of `stepUntil`. Throws `RangeError` for a
|
|
775
|
+
* non-finite or non-positive `dt`, a negative or non-finite `budgetMs`, or a
|
|
776
|
+
* `maxSteps` that is not an integer >= 1. */
|
|
777
|
+
function stepUntilInto(world, dt, opts, out) {
|
|
778
|
+
if (!Number.isFinite(dt) || dt <= 0) throw new RangeError(`stepUntil: dt must be a finite number > 0, got ${dt}`);
|
|
779
|
+
const { budgetMs, maxSteps, done } = opts;
|
|
780
|
+
if (!Number.isFinite(budgetMs) || budgetMs < 0) throw new RangeError(`stepUntil: budgetMs must be a finite number >= 0, got ${budgetMs}`);
|
|
781
|
+
if (maxSteps !== void 0 && (!Number.isInteger(maxSteps) || maxSteps < 1)) throw new RangeError(`stepUntil: maxSteps must be an integer >= 1, got ${maxSteps}`);
|
|
782
|
+
const now = opts.now ?? nowMs;
|
|
783
|
+
const start = now();
|
|
784
|
+
let steps = 0;
|
|
785
|
+
let complete = false;
|
|
786
|
+
for (;;) {
|
|
787
|
+
step(world, dt);
|
|
788
|
+
steps++;
|
|
789
|
+
if (done?.()) {
|
|
790
|
+
complete = true;
|
|
791
|
+
break;
|
|
792
|
+
}
|
|
793
|
+
if (maxSteps !== void 0 && steps >= maxSteps) break;
|
|
794
|
+
if (now() - start >= budgetMs) break;
|
|
795
|
+
}
|
|
796
|
+
out.steps = steps;
|
|
797
|
+
out.complete = complete;
|
|
798
|
+
return out;
|
|
799
|
+
}
|
|
800
|
+
/** Allocating form of `stepUntilInto`; see it for the stop rules. */
|
|
801
|
+
function stepUntil(world, dt, opts) {
|
|
802
|
+
return stepUntilInto(world, dt, opts, {
|
|
803
|
+
steps: 0,
|
|
804
|
+
complete: false
|
|
805
|
+
});
|
|
543
806
|
}
|
|
544
|
-
export { addBody, createBody, createWorld, isStatic, step };
|
|
807
|
+
export { addBody, createBody, createTimeline, createWorld, isAtRest, isSleeping, isStatic, step, stepUntil, stepUntilInto, wakeBody };
|
|
545
808
|
|
|
546
809
|
//# sourceMappingURL=index.mjs.map
|