@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/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 geometry buffers to hold at least `count` bodies (8
340
- * doubles per body for verts/normals, 4 for the AABB). No-op if already
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 (body.invMass === 0) return;
392
- body.velocity.x += ix * body.invMass;
393
- body.velocity.y += iy * body.invMass;
394
- body.angularVelocity += body.invInertia * (rx * iy - ry * ix);
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.aabbs;
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 (isStatic(a) && isStatic(b)) continue;
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)) pool.manifoldCount++;
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 = a.invMass + b.invMass + a.invInertia * rnA * rnA + b.invInertia * rnB * rnB;
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 = a.invMass + b.invMass + a.invInertia * rtA * rtA + b.invInertia * rtB * rtB;
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
- * Resting contacts settle to a stable *position* but retain a small, bounded
513
- * velocity oscillation at the penetration-slop equilibrium — a sequential-
514
- * impulse solver with Baumgarte bias and no sleeping does not converge
515
- * velocity to exactly zero at rest, only the position does. This is
516
- * inherent to the algorithm, not a bug; sleeping and damping are
517
- * deliberately out of scope for v1. A consumer needing pixel-exact
518
- * stillness should clamp small resting velocities at its own layer. */
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