@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 CHANGED
@@ -46,19 +46,118 @@ going through an accessor.
46
46
  points, broadphase scratch buffers) lives in a pool owned by the `World` and is reused across calls,
47
47
  growing only when body count grows.
48
48
 
49
+ ## Record and replay
50
+
51
+ Semi-implicit Euler with friction and restitution is not time-reversible, so a fall you want to
52
+ scrub, reverse or replay at another speed is **baked once** into a `Timeline` and read back by
53
+ sampling. `record` takes a writer callback instead of a body array, so the timeline never has to know
54
+ what it is recording; with an `until` predicate it bakes in one call, without one it records a single
55
+ frame, which suits a time-budgeted loop that calls it once per tick.
56
+ `stepUntil(world, dt, { budgetMs: 6, done: isSettled })` is that loop: it steps until `done()` returns
57
+ true, `maxSteps` is reached or the budget is spent (always at least once), and reports
58
+ `{ steps, complete }`; `stepUntilInto` is the zero-alloc form.
59
+
60
+ ```ts
61
+ import { createTimeline, createWorld, step } from '@flare-engine/dynamics';
62
+
63
+ const world = createWorld(); // add a floor and some boxes as above
64
+ const dt = 1 / 60;
65
+ const timeline = createTimeline(world.bodies.length, dt); // stride 3: x, y, angle
66
+ timeline.record(
67
+ (buf, base) => {
68
+ step(world, dt);
69
+ for (let i = 0; i < world.bodies.length; i++) {
70
+ const b = world.bodies[i]!;
71
+ buf[base + i * 3] = b.position.x;
72
+ buf[base + i * 3 + 1] = b.position.y;
73
+ buf[base + i * 3 + 2] = b.angle;
74
+ }
75
+ },
76
+ () => world.bodies.every((b) => Math.abs(b.velocity.y) < 1), // your own settle rule
77
+ );
78
+
79
+ const out = new Float32Array(timeline.elementCount * 3);
80
+ timeline.sampleInto(0.5, out); // body i at out[i * 3 ..], lerped between frames
81
+ ```
82
+
83
+ Storage is `Float32Array` on purpose — playback precision at half the bytes of f64 — and angles are
84
+ kept exactly as written: `Body.angle` is cumulative and never wrapped, so a plain lerp is correct
85
+ across multi-turn spins where a shortest-path lerp would unravel them. `sampleInto` allocates
86
+ nothing; the one scratch buffer above serves the whole replay.
87
+
88
+ ## Sleeping
89
+
90
+ Sleeping is off by default (see Limits). `createWorld({ sleep })` opts in: a dynamic body whose
91
+ speed stays under `linearThreshold` (px/s) and whose angular speed stays under `angularThreshold`
92
+ (rad/s) for `steps` consecutive steps has its velocities set to exactly 0 and is skipped by
93
+ integration and by pair tests against other sleeping or static bodies. It wakes when an awake body
94
+ that moved on the previous step touches it, when you write its `velocity`, `angularVelocity`,
95
+ `position` or `angle`, or when you call `wakeBody`. `isSleeping(world, body)` reads one body (an
96
+ `indexOf` over `world.bodies`); `isAtRest(world)` is true once every dynamic body sleeps, which makes
97
+ it the stop rule for a `stepUntil` bake:
98
+
99
+ ```ts
100
+ import {
101
+ addBody,
102
+ createBody,
103
+ createWorld,
104
+ isAtRest,
105
+ isSleeping,
106
+ stepUntil,
107
+ wakeBody,
108
+ } from '@flare-engine/dynamics';
109
+
110
+ const world = createWorld({
111
+ gravity: { x: 0, y: 1800 },
112
+ sleep: { linearThreshold: 14, angularThreshold: 1.5, steps: 30 },
113
+ });
114
+ addBody(
115
+ world,
116
+ createBody({ position: { x: 0, y: 100 }, half: { x: 500, y: 10 }, static: true }),
117
+ );
118
+ const box = addBody(world, createBody({ position: { x: 0, y: 0 }, half: { x: 5, y: 5 } }));
119
+
120
+ const result = stepUntil(world, 1 / 120, {
121
+ budgetMs: 8,
122
+ maxSteps: 600,
123
+ done: () => isAtRest(world),
124
+ });
125
+ if (result.complete) console.log(isSleeping(world, box), box.velocity.y); // true 0
126
+
127
+ wakeBody(world, box); // e.g. after removing the floor from world.bodies
128
+ ```
129
+
130
+ Thresholds must clear the resting jitter described under Limits, which depends on body size and
131
+ `dt`: in the package's own tests (half-5 boxes, `dt = 1/120`) a resting box peaks at about 5.8 px/s
132
+ and 1.12 rad/s (the angular peak scales with 1 / half-width), so 14 / 1.5 sleeps it in 91 steps,
133
+ while 0.6 rad/s never sees 30 quiet steps in a row. Measure your own scene before pinning values.
134
+
135
+ What sleeping does not do:
136
+
137
+ - **No islands.** A woken body wakes the sleepers it touches on the *next* step, so a landed box
138
+ wakes a stack one level per step, and a sleeper touched only by a body that is itself settling
139
+ (under the thresholds, counting down) stays asleep.
140
+ - **A sleeper does not follow a moved static body.** Moving the floor under a sleeping box leaves
141
+ the box where it is; call `wakeBody` after moving or removing a support.
142
+ - **Removing a body from `world.bodies` wakes the bodies that shift down a slot** and nothing else.
143
+ A sleeper whose support was removed without shifting it stays asleep in mid-air until `wakeBody`.
144
+ Inserting a body before a sleeper likewise wakes it (it re-sleeps after `steps` quiet steps).
145
+ - **A world that never went through `createWorld`** (a spread copy or `structuredClone` handed to
146
+ `step`) is adopted with sleeping off; `isSleeping` and `isAtRest` are then false for any dynamic
147
+ body.
148
+
49
149
  ## Limits (v1)
50
150
 
51
- - **Resting bodies do not go perfectly still.** A box settling on a floor reaches a stable
52
- *position* — in the package's own test scenario, position moves less than 0.1 units over 600
53
- further steps — but `velocity` keeps a small, bounded oscillation (roughly 2.6 units/s in that same
54
- scenario) at the penetration-slop equilibrium. This is inherent to a Baumgarte-biased
55
- sequential-impulse solver run without sleeping, not a bug, and not something a bigger iteration
56
- count or a tuning tweak resolves. Sleeping and resting-velocity damping are deliberately out of
57
- scope for v1 (tracked as future work). If your use case needs pixel-exact stillness, clamp small
58
- resting velocities at your own layer once a body's position has stabilized.
151
+ - **Resting bodies do not go perfectly still unless you opt into sleeping.** A box settling on a
152
+ floor reaches a stable *position* — in the package's own test scenario, position moves less than
153
+ 0.1 units over 600 further steps — but `velocity` keeps a small, bounded oscillation (roughly 2.6
154
+ units/s in that same scenario) at the penetration-slop equilibrium. This is inherent to a
155
+ Baumgarte-biased sequential-impulse solver, not a bug, and not something a bigger iteration count
156
+ or a tuning tweak resolves. `createWorld({ sleep })` (above) zeroes the velocity of bodies that
157
+ stay under your thresholds; resting-velocity damping stays out of scope.
59
158
  - **Boxes only.** No circle or polygon shapes.
60
- - **No joints, no sleeping, no CCD (continuous collision detection).** Fast-moving thin bodies can
61
- tunnel through each other within a single step.
159
+ - **No joints, no CCD (continuous collision detection).** Fast-moving thin bodies can tunnel
160
+ through each other within a single step.
62
161
  - **No ECS coupling.** `World`/`Body` are plain data; wire them into an ECS yourself if you use one.
63
162
  - **Broadphase is brute-force** (all-pairs AABB reject) — fine at small body counts, not a spatial
64
163
  partition. There is no broadphase beyond that pass.
package/dist/index.d.mts CHANGED
@@ -34,28 +34,125 @@ interface BodyOptions {
34
34
  }
35
35
  export declare function createBody(opts: BodyOptions): Body;
36
36
  export declare function isStatic(b: Body): boolean;
37
+ interface SleepOptions {
38
+ /** Linear speed below which a body counts as resting (same units as `velocity`, px/s). */
39
+ linearThreshold: number;
40
+ /** Angular speed below which a body counts as resting (rad/s). */
41
+ angularThreshold: number;
42
+ /** Consecutive under-threshold steps before a body is put to sleep (integer >= 1). */
43
+ steps: number;
44
+ }
45
+ /** Bake-once / scrub-often recorder for a fixed-step simulation. Friction +
46
+ * restitution Euler is not time-reversible, so a fall is recorded once into a
47
+ * pre-allocated `Float32Array` and replayed by sampling — reverse, scrub and
48
+ * speed all read back through `sampleInto` instead of re-integrating.
49
+ *
50
+ * Angles are stored exactly as written. `Body.angle` is cumulative and never
51
+ * wrapped, so a plain lerp between adjacent frames is correct across
52
+ * multi-turn spins; a shortest-path angle lerp would unravel them.
53
+ *
54
+ * Float32 storage is deliberate: this is playback precision, not simulation
55
+ * precision, at half the bytes of f64. */
56
+ interface TimelineOptions {
57
+ /** Floats per element per frame. Default 3: `dx`, `dy`, `angle`. */
58
+ stride?: number;
59
+ /** Frame cap; the whole buffer is allocated at creation. Default 900. */
60
+ maxFrames?: number;
61
+ }
62
+ /** Fills one frame of `buf`: element `i` lives at `frameBase + i * stride`. */
63
+ type TimelineWriter = (buf: Float32Array, frameBase: number) => void;
64
+ interface Timeline {
65
+ readonly elementCount: number;
66
+ /** Frames recorded so far. */
67
+ readonly frameCount: number;
68
+ /** Recorded length in seconds: `(frameCount - 1) * dt`, 0 below two frames. */
69
+ readonly duration: number;
70
+ /** Writes frames until `until()` returns true or the buffer is full; without
71
+ * `until`, exactly one frame. `record(write)` once per tick suits a chunked,
72
+ * time-budgeted bake; `record(stepAndWrite, isSettled)` bakes in one call
73
+ * when the writer also advances the simulation. No-op once full. */
74
+ record(writer: TimelineWriter, until?: () => boolean): void;
75
+ /** Lerps the two frames bracketing `progress` (clamped to [0, 1], NaN reads
76
+ * as 0) into `out`, which must hold at least `elementCount * stride`
77
+ * floats. Zero-alloc. All zeros before the first frame. */
78
+ sampleInto(progress: number, out: Float32Array): void;
79
+ }
80
+ export declare function createTimeline(elementCount: number, dt: number, opts?: TimelineOptions): Timeline;
37
81
  interface WorldOptions {
38
82
  /** Acceleration applied to every dynamic body each step (px/s²). */
39
83
  gravity?: Vec2;
40
84
  /** Sequential-impulse iterations per step. More = stiffer stacks. */
41
85
  iterations?: number;
86
+ /** Opt-in per-body sleeping; off when absent. See `step` and `isSleeping`. */
87
+ sleep?: SleepOptions;
42
88
  }
43
89
  interface World {
44
90
  bodies: Body[];
45
91
  gravity: Vec2;
46
92
  iterations: number;
47
93
  }
94
+ interface StepUntilOptions {
95
+ /** Time budget in ms, compared against `now()` after every step. */
96
+ budgetMs: number;
97
+ /** Step cap per call (integer >= 1). */
98
+ maxSteps?: number;
99
+ /** Polled after each step; `true` ends the call with `complete: true`. */
100
+ done?: () => boolean;
101
+ /** Clock in ms. Default `performance.now`, else `Date.now`; inject for deterministic tests. */
102
+ now?: () => number;
103
+ }
104
+ interface StepUntilResult {
105
+ /** Steps taken by this call, always >= 1. */
106
+ steps: number;
107
+ /** `true` when `done()` ended the call; `false` for `maxSteps` or the budget. */
108
+ complete: boolean;
109
+ }
110
+ /** Throws `RangeError` when `opts.sleep` has a negative or non-finite
111
+ * threshold or a `steps` that is not an integer >= 1. */
48
112
  export declare function createWorld(opts?: WorldOptions): World;
49
113
  export declare function addBody(world: World, body: Body): Body;
50
114
  /** Advance the world by `dt` seconds (fixed steps recommended).
51
115
  *
52
- * Resting contacts settle to a stable *position* but retain a small, bounded
53
- * velocity oscillation at the penetration-slop equilibrium — a sequential-
54
- * impulse solver with Baumgarte bias and no sleeping does not converge
55
- * velocity to exactly zero at rest, only the position does. This is
56
- * inherent to the algorithm, not a bug; sleeping and damping are
57
- * deliberately out of scope for v1. A consumer needing pixel-exact
58
- * stillness should clamp small resting velocities at its own layer. */
116
+ * By default (no `WorldOptions.sleep`) resting contacts settle to a stable
117
+ * *position* but retain a small, bounded velocity oscillation at the
118
+ * penetration-slop equilibrium — a sequential-impulse solver with Baumgarte
119
+ * bias does not converge velocity to exactly zero at rest, only the position
120
+ * does. `WorldOptions.sleep` opts into per-body sleeping: a dynamic body
121
+ * under both thresholds for `steps` consecutive steps has its velocities
122
+ * zeroed and is skipped by integration and by pair tests against other
123
+ * resting bodies until an awake body touches it, the consumer writes its
124
+ * velocity, position or angle, or `wakeBody` is called. There are no
125
+ * islands, so a woken stack wakes one level per step, and a sleeper does not
126
+ * follow a static body that is moved under it. Damping stays out of scope.
127
+ *
128
+ * A world that never went through `createWorld` (a spread copy or a
129
+ * `structuredClone`) is adopted on its first `step` with sleeping off. */
59
130
  export declare function step(world: World, dt: number): void;
60
- export type { Body, BodyOptions, World, WorldOptions };
131
+ /** `true` when `body` is asleep in `world`. `false` when the world has
132
+ * sleeping off or never went through `createWorld`, when `body` is not in
133
+ * `world.bodies`, or when its slot has not been bound to it by a `step` yet.
134
+ * O(n) in `world.bodies.length` (an `indexOf`); poll it per body, not per
135
+ * contact. */
136
+ export declare function isSleeping(world: World, body: Body): boolean;
137
+ /** `true` when every dynamic body in `world.bodies` is asleep — vacuously
138
+ * true with no dynamic bodies, `false` with sleeping off and at least one.
139
+ * The bake stop rule for `stepUntil`: `done: () => isAtRest(world)`. */
140
+ export declare function isAtRest(world: World): boolean;
141
+ /** Wakes `body` if it is asleep in `world` (flag cleared, counter 0, its
142
+ * velocities stay 0); the next `step` integrates it again. No-op otherwise,
143
+ * including with sleeping off or a body not in `world.bodies`. Use it for a
144
+ * sleeper whose support was removed from `world.bodies` or moved — nothing
145
+ * else wakes it. */
146
+ export declare function wakeBody(world: World, body: Body): void;
147
+ /** Repeats `step(world, dt)` until, checked in this order after each step,
148
+ * `done()` returns true (`complete: true`), `steps >= maxSteps`, or
149
+ * `now() - start >= budgetMs` (both `complete: false`). Always takes at least
150
+ * one step, so a zero budget still makes progress. Writes `out` and returns it;
151
+ * the zero-alloc per-frame form of `stepUntil`. Throws `RangeError` for a
152
+ * non-finite or non-positive `dt`, a negative or non-finite `budgetMs`, or a
153
+ * `maxSteps` that is not an integer >= 1. */
154
+ export declare function stepUntilInto(world: World, dt: number, opts: StepUntilOptions, out: StepUntilResult): StepUntilResult;
155
+ /** Allocating form of `stepUntilInto`; see it for the stop rules. */
156
+ export declare function stepUntil(world: World, dt: number, opts: StepUntilOptions): StepUntilResult;
157
+ export type { Body, BodyOptions, SleepOptions, StepUntilOptions, StepUntilResult, Timeline, TimelineOptions, TimelineWriter, World, WorldOptions };
61
158
  //# sourceMappingURL=index.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.mts","names":[],"sources":["../src/body.ts","../src/world.ts"],"mappings":";UAOiB;;EAEf,UAAU;;EAEV,UAAU;;EAEV;;EAEA;;EAEA,MAAM;;EAEN;;EAEA;;EAEA;;EAEA;;UAGe;EACf,UAAU;EACV,MAAM;;EAEN;EACA,WAAW;EACX;EACA;EACA;EACA;;EAEA;;wBAGc,WAAW,MAAM,cAAc;wBA6B/B,SAAS,GAAG;UCzDX;;EAEf,UAAU;;EAEV;;UAGe;EACf,QAAQ;EACR,SAAS;EACT;;wBAQc,YAAY,OAAM,eAAoB;wBAUtC,QAAQ,OAAO,OAAO,MAAM,OAAO;;;;;;;;;;wBAwKnC,KAAK,OAAO,OAAO"}
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../src/body.ts","../src/pool.ts","../src/timeline.ts","../src/world.ts"],"mappings":";UAOiB;;EAEf,UAAU;;EAEV,UAAU;;EAEV;;EAEA;;EAEA,MAAM;;EAEN;;EAEA;;EAEA;;EAEA;;UAGe;EACf,UAAU;EACV,MAAM;;EAEN;EACA,WAAW;EACX;EACA;EACA;EACA;;EAEA;;wBAGc,WAAW,MAAM,cAAc;wBA6B/B,SAAS,GAAG;UC/DX;;EAEf;;EAEA;;EAEA;;;;;;;;;;;;;UCFe;;EAEf;;EAEA;;;KAIU,kBAAkB,KAAK,cAAc;UAEhC;WACN;;WAEA;;WAEA;;;;;EAKT,OAAO,QAAQ,gBAAgB;;;;EAI/B,WAAW,kBAAkB,KAAK;;wBAYpB,eACd,sBACA,YACA,OAAM,kBACL;UChCc;;EAEf,UAAU;;EAEV;;EAEA,QAAQ;;UAGO;EACf,QAAQ;EACR,SAAS;EACT;;UAGe;;EAEf;;EAEA;;EAEA;;EAEA;;UAGe;;EAEf;;EAEA;;;;wBAmCc,YAAY,OAAM,eAAoB;wBAUtC,QAAQ,OAAO,OAAO,MAAM,OAAO;;;;;;;;;;;;;;;;;wBA0RnC,KAAK,OAAO,OAAO;;;;;;wBAyDnB,WAAW,OAAO,OAAO,MAAM;;;;wBAW/B,SAAS,OAAO;;;;;;wBAkBhB,SAAS,OAAO,OAAO,MAAM;;;;;;;;wBAgB7B,cACd,OAAO,OACP,YACA,MAAM,kBACN,KAAK,kBACJ;;wBA+Ba,UAAU,OAAO,OAAO,YAAY,MAAM,mBAAmB"}
package/dist/index.d.ts CHANGED
@@ -34,28 +34,125 @@ interface BodyOptions {
34
34
  }
35
35
  export declare function createBody(opts: BodyOptions): Body;
36
36
  export declare function isStatic(b: Body): boolean;
37
+ interface SleepOptions {
38
+ /** Linear speed below which a body counts as resting (same units as `velocity`, px/s). */
39
+ linearThreshold: number;
40
+ /** Angular speed below which a body counts as resting (rad/s). */
41
+ angularThreshold: number;
42
+ /** Consecutive under-threshold steps before a body is put to sleep (integer >= 1). */
43
+ steps: number;
44
+ }
45
+ /** Bake-once / scrub-often recorder for a fixed-step simulation. Friction +
46
+ * restitution Euler is not time-reversible, so a fall is recorded once into a
47
+ * pre-allocated `Float32Array` and replayed by sampling — reverse, scrub and
48
+ * speed all read back through `sampleInto` instead of re-integrating.
49
+ *
50
+ * Angles are stored exactly as written. `Body.angle` is cumulative and never
51
+ * wrapped, so a plain lerp between adjacent frames is correct across
52
+ * multi-turn spins; a shortest-path angle lerp would unravel them.
53
+ *
54
+ * Float32 storage is deliberate: this is playback precision, not simulation
55
+ * precision, at half the bytes of f64. */
56
+ interface TimelineOptions {
57
+ /** Floats per element per frame. Default 3: `dx`, `dy`, `angle`. */
58
+ stride?: number;
59
+ /** Frame cap; the whole buffer is allocated at creation. Default 900. */
60
+ maxFrames?: number;
61
+ }
62
+ /** Fills one frame of `buf`: element `i` lives at `frameBase + i * stride`. */
63
+ type TimelineWriter = (buf: Float32Array, frameBase: number) => void;
64
+ interface Timeline {
65
+ readonly elementCount: number;
66
+ /** Frames recorded so far. */
67
+ readonly frameCount: number;
68
+ /** Recorded length in seconds: `(frameCount - 1) * dt`, 0 below two frames. */
69
+ readonly duration: number;
70
+ /** Writes frames until `until()` returns true or the buffer is full; without
71
+ * `until`, exactly one frame. `record(write)` once per tick suits a chunked,
72
+ * time-budgeted bake; `record(stepAndWrite, isSettled)` bakes in one call
73
+ * when the writer also advances the simulation. No-op once full. */
74
+ record(writer: TimelineWriter, until?: () => boolean): void;
75
+ /** Lerps the two frames bracketing `progress` (clamped to [0, 1], NaN reads
76
+ * as 0) into `out`, which must hold at least `elementCount * stride`
77
+ * floats. Zero-alloc. All zeros before the first frame. */
78
+ sampleInto(progress: number, out: Float32Array): void;
79
+ }
80
+ export declare function createTimeline(elementCount: number, dt: number, opts?: TimelineOptions): Timeline;
37
81
  interface WorldOptions {
38
82
  /** Acceleration applied to every dynamic body each step (px/s²). */
39
83
  gravity?: Vec2;
40
84
  /** Sequential-impulse iterations per step. More = stiffer stacks. */
41
85
  iterations?: number;
86
+ /** Opt-in per-body sleeping; off when absent. See `step` and `isSleeping`. */
87
+ sleep?: SleepOptions;
42
88
  }
43
89
  interface World {
44
90
  bodies: Body[];
45
91
  gravity: Vec2;
46
92
  iterations: number;
47
93
  }
94
+ interface StepUntilOptions {
95
+ /** Time budget in ms, compared against `now()` after every step. */
96
+ budgetMs: number;
97
+ /** Step cap per call (integer >= 1). */
98
+ maxSteps?: number;
99
+ /** Polled after each step; `true` ends the call with `complete: true`. */
100
+ done?: () => boolean;
101
+ /** Clock in ms. Default `performance.now`, else `Date.now`; inject for deterministic tests. */
102
+ now?: () => number;
103
+ }
104
+ interface StepUntilResult {
105
+ /** Steps taken by this call, always >= 1. */
106
+ steps: number;
107
+ /** `true` when `done()` ended the call; `false` for `maxSteps` or the budget. */
108
+ complete: boolean;
109
+ }
110
+ /** Throws `RangeError` when `opts.sleep` has a negative or non-finite
111
+ * threshold or a `steps` that is not an integer >= 1. */
48
112
  export declare function createWorld(opts?: WorldOptions): World;
49
113
  export declare function addBody(world: World, body: Body): Body;
50
114
  /** Advance the world by `dt` seconds (fixed steps recommended).
51
115
  *
52
- * Resting contacts settle to a stable *position* but retain a small, bounded
53
- * velocity oscillation at the penetration-slop equilibrium — a sequential-
54
- * impulse solver with Baumgarte bias and no sleeping does not converge
55
- * velocity to exactly zero at rest, only the position does. This is
56
- * inherent to the algorithm, not a bug; sleeping and damping are
57
- * deliberately out of scope for v1. A consumer needing pixel-exact
58
- * stillness should clamp small resting velocities at its own layer. */
116
+ * By default (no `WorldOptions.sleep`) resting contacts settle to a stable
117
+ * *position* but retain a small, bounded velocity oscillation at the
118
+ * penetration-slop equilibrium — a sequential-impulse solver with Baumgarte
119
+ * bias does not converge velocity to exactly zero at rest, only the position
120
+ * does. `WorldOptions.sleep` opts into per-body sleeping: a dynamic body
121
+ * under both thresholds for `steps` consecutive steps has its velocities
122
+ * zeroed and is skipped by integration and by pair tests against other
123
+ * resting bodies until an awake body touches it, the consumer writes its
124
+ * velocity, position or angle, or `wakeBody` is called. There are no
125
+ * islands, so a woken stack wakes one level per step, and a sleeper does not
126
+ * follow a static body that is moved under it. Damping stays out of scope.
127
+ *
128
+ * A world that never went through `createWorld` (a spread copy or a
129
+ * `structuredClone`) is adopted on its first `step` with sleeping off. */
59
130
  export declare function step(world: World, dt: number): void;
60
- export type { Body, BodyOptions, World, WorldOptions };
131
+ /** `true` when `body` is asleep in `world`. `false` when the world has
132
+ * sleeping off or never went through `createWorld`, when `body` is not in
133
+ * `world.bodies`, or when its slot has not been bound to it by a `step` yet.
134
+ * O(n) in `world.bodies.length` (an `indexOf`); poll it per body, not per
135
+ * contact. */
136
+ export declare function isSleeping(world: World, body: Body): boolean;
137
+ /** `true` when every dynamic body in `world.bodies` is asleep — vacuously
138
+ * true with no dynamic bodies, `false` with sleeping off and at least one.
139
+ * The bake stop rule for `stepUntil`: `done: () => isAtRest(world)`. */
140
+ export declare function isAtRest(world: World): boolean;
141
+ /** Wakes `body` if it is asleep in `world` (flag cleared, counter 0, its
142
+ * velocities stay 0); the next `step` integrates it again. No-op otherwise,
143
+ * including with sleeping off or a body not in `world.bodies`. Use it for a
144
+ * sleeper whose support was removed from `world.bodies` or moved — nothing
145
+ * else wakes it. */
146
+ export declare function wakeBody(world: World, body: Body): void;
147
+ /** Repeats `step(world, dt)` until, checked in this order after each step,
148
+ * `done()` returns true (`complete: true`), `steps >= maxSteps`, or
149
+ * `now() - start >= budgetMs` (both `complete: false`). Always takes at least
150
+ * one step, so a zero budget still makes progress. Writes `out` and returns it;
151
+ * the zero-alloc per-frame form of `stepUntil`. Throws `RangeError` for a
152
+ * non-finite or non-positive `dt`, a negative or non-finite `budgetMs`, or a
153
+ * `maxSteps` that is not an integer >= 1. */
154
+ export declare function stepUntilInto(world: World, dt: number, opts: StepUntilOptions, out: StepUntilResult): StepUntilResult;
155
+ /** Allocating form of `stepUntilInto`; see it for the stop rules. */
156
+ export declare function stepUntil(world: World, dt: number, opts: StepUntilOptions): StepUntilResult;
157
+ export type { Body, BodyOptions, SleepOptions, StepUntilOptions, StepUntilResult, Timeline, TimelineOptions, TimelineWriter, World, WorldOptions };
61
158
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","names":[],"sources":["../src/body.ts","../src/world.ts"],"mappings":";UAOiB;;EAEf,UAAU;;EAEV,UAAU;;EAEV;;EAEA;;EAEA,MAAM;;EAEN;;EAEA;;EAEA;;EAEA;;UAGe;EACf,UAAU;EACV,MAAM;;EAEN;EACA,WAAW;EACX;EACA;EACA;EACA;;EAEA;;wBAGc,WAAW,MAAM,cAAc;wBA6B/B,SAAS,GAAG;UCzDX;;EAEf,UAAU;;EAEV;;UAGe;EACf,QAAQ;EACR,SAAS;EACT;;wBAQc,YAAY,OAAM,eAAoB;wBAUtC,QAAQ,OAAO,OAAO,MAAM,OAAO;;;;;;;;;;wBAwKnC,KAAK,OAAO,OAAO"}
1
+ {"version":3,"file":"index.d.ts","names":[],"sources":["../src/body.ts","../src/pool.ts","../src/timeline.ts","../src/world.ts"],"mappings":";UAOiB;;EAEf,UAAU;;EAEV,UAAU;;EAEV;;EAEA;;EAEA,MAAM;;EAEN;;EAEA;;EAEA;;EAEA;;UAGe;EACf,UAAU;EACV,MAAM;;EAEN;EACA,WAAW;EACX;EACA;EACA;EACA;;EAEA;;wBAGc,WAAW,MAAM,cAAc;wBA6B/B,SAAS,GAAG;UC/DX;;EAEf;;EAEA;;EAEA;;;;;;;;;;;;;UCFe;;EAEf;;EAEA;;;KAIU,kBAAkB,KAAK,cAAc;UAEhC;WACN;;WAEA;;WAEA;;;;;EAKT,OAAO,QAAQ,gBAAgB;;;;EAI/B,WAAW,kBAAkB,KAAK;;wBAYpB,eACd,sBACA,YACA,OAAM,kBACL;UChCc;;EAEf,UAAU;;EAEV;;EAEA,QAAQ;;UAGO;EACf,QAAQ;EACR,SAAS;EACT;;UAGe;;EAEf;;EAEA;;EAEA;;EAEA;;UAGe;;EAEf;;EAEA;;;;wBAmCc,YAAY,OAAM,eAAoB;wBAUtC,QAAQ,OAAO,OAAO,MAAM,OAAO;;;;;;;;;;;;;;;;;wBA0RnC,KAAK,OAAO,OAAO;;;;;;wBAyDnB,WAAW,OAAO,OAAO,MAAM;;;;wBAW/B,SAAS,OAAO;;;;;;wBAkBhB,SAAS,OAAO,OAAO,MAAM;;;;;;;;wBAgB7B,cACd,OAAO,OACP,YACA,MAAM,kBACN,KAAK,kBACJ;;wBA+Ba,UAAU,OAAO,OAAO,YAAY,MAAM,mBAAmB"}