@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/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
|
|
52
|
-
*position* — in the package's own test scenario, position moves less than
|
|
53
|
-
further steps — but `velocity` keeps a small, bounded oscillation (roughly 2.6
|
|
54
|
-
scenario) at the penetration-slop equilibrium. This is inherent to a
|
|
55
|
-
sequential-impulse solver
|
|
56
|
-
|
|
57
|
-
|
|
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
|
|
61
|
-
|
|
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
|
-
*
|
|
53
|
-
*
|
|
54
|
-
* impulse solver with Baumgarte
|
|
55
|
-
* velocity to exactly zero at rest, only the position
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
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
|
-
|
|
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
|
package/dist/index.d.mts.map
CHANGED
|
@@ -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;
|
|
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
|
-
*
|
|
53
|
-
*
|
|
54
|
-
* impulse solver with Baumgarte
|
|
55
|
-
* velocity to exactly zero at rest, only the position
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
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
|
-
|
|
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
|
package/dist/index.d.ts.map
CHANGED
|
@@ -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;
|
|
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"}
|