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