@msb235/blits-rapier 0.0.0-stage → 0.1.0

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/CHANGELOG.md ADDED
@@ -0,0 +1,14 @@
1
+ # Changelog
2
+
3
+ This package follows [semver](https://semver.org). Below 1.0.0, a breaking change bumps the minor
4
+ version and everything else the patch. The release workflow refuses a `rapier-v*` tag with no
5
+ section here.
6
+
7
+ ## 0.1.0
8
+
9
+ ### Added
10
+
11
+ - `physics`, which holds a Rapier world in a blits mix: bodies as subjects, `force` and `torque`
12
+ voices on each body's controls, a simulated voice in a role that crossfades with authored
13
+ motion, impulses and drops logged at mix time, and exact replay on a seek. 2D and 3D share the
14
+ API through `channels2` and `channels3`.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Michael Baker
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,91 @@
1
- # Temporary Holding Version
1
+ # @msb235/blits-rapier
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Holds a [Rapier](https://rapier.rs) rigid-body world in a [blits](https://github.com/orochi235/blits)
4
+ mix. Voices push bodies around, each body's simulated pose is a voice that blends with authored
5
+ motion, and a seek, a loop or a debugger's scrub replays the world exactly. 2D and 3D share one API;
6
+ the Rapier module you pass decides which.
7
+
8
+ ```
9
+ npm install @msb235/blits @msb235/blits-rapier @dimforge/rapier3d-compat # or rapier2d-compat
10
+ ```
11
+
12
+ ```ts
13
+ import RAPIER from '@dimforge/rapier3d-compat';
14
+ import { kit, patch } from '@msb235/blits';
15
+ import { channels3, type Pose3, physics } from '@msb235/blits-rapier';
16
+ import { createHistory } from '@weasel-js/history';
17
+
18
+ await RAPIER.init();
19
+ const sim = physics(kit<Pose3>(channels3), {
20
+ rapier: RAPIER,
21
+ history: { ms: 10_000, every: 250, tape: createHistory }, // only to seek
22
+ });
23
+ sim.body(RAPIER.RigidBodyDesc.fixed(), RAPIER.ColliderDesc.cuboid(10, 0.1, 10));
24
+ const box = sim.body(
25
+ RAPIER.RigidBodyDesc.dynamic().setTranslation(0, 4, 0),
26
+ RAPIER.ColliderDesc.cuboid(0.5, 0.5, 0.5),
27
+ );
28
+
29
+ const gust = patch<object, Pose3>(1500, (phase) => ({ force: [Math.sin(phase * Math.PI) * 8, 0, 0] }), {
30
+ writes: ['force'],
31
+ });
32
+ box.drive({ patch: gust });
33
+
34
+ // each frame
35
+ sim.sync(now); // in place of sim.mix.sync
36
+ const { position, rotation } = sim.mix.probe(box);
37
+ ```
38
+
39
+ ## Bodies
40
+
41
+ - **`sim.body(desc, ...colliders)`** adds a body from Rapier's own descriptions, built with the
42
+ module you passed. Nothing else about Rapier is wrapped. The body joins the world on the step
43
+ after the mix's current time, or on the first frame when the mix has not synced yet.
44
+ - **A body is the mix's subject for its pose**, `position` and `rotation`: `sim.mix.probe(body)`
45
+ reads it. Its **`controls`** is a second subject, whose `force` and `torque` push the body. They
46
+ are read before the world steps and the pose after, which is why they are two subjects.
47
+ - **`body.drive(spec)`** cues a voice on the controls; its weight scales the push.
48
+ **`body.cue(spec)`** cues one on the pose. Neither spec names `subjects`.
49
+ - **`body.simulated`** is the `Handle` of the voice that shows the body where the world puts it, at
50
+ weight 1, in the role `sim.role`. A voice cued in that role is an alternative to the simulation,
51
+ and the two crossfade by weight: fade `simulated` out and an authored pose in, and a body settles
52
+ from physics into a scripted place. A voice outside the role adds on top of wherever physics has
53
+ it.
54
+ - **`body.impulse(v)`** applies an impulse once, on the step after the mix's current time.
55
+ **`body.drop()`** takes the body out of the world then. Both are logged at that time, so a seek
56
+ back before it undoes them and playing forward through it does them again.
57
+ - **`sim.world()`** is the Rapier world as the last `sync` left it, for reading: raycasts, contact
58
+ queries, debugging draws. A change made to it directly is lost at the next seek.
59
+
60
+ ## Channels
61
+
62
+ | Channel | 2D (`channels2`) | 3D (`channels3`) | On |
63
+ |---|---|---|---|
64
+ | `position` | `vec(2, sum())` | `vec(3, sum())` | the body |
65
+ | `rotation` | `angle()`, radians | `quat()`, `[x, y, z, w]` | the body |
66
+ | `force` | `vec(2, sum())` | `vec(3, sum())` | its controls |
67
+ | `torque` | `sum()` | `vec(3, sum())` | its controls |
68
+
69
+ Spread them into a kit with your own channels. Forces and torques are in Rapier's units, newtons
70
+ and newton-meters for a body whose mass you set in kilograms.
71
+
72
+ ## The clock and seeking
73
+
74
+ `physics` makes the mix, with `stepMs` 1000 / 60 unless you set it; the world takes one Rapier step
75
+ per interval, so it runs at the same rate whatever the frame rate. Pass `history` and a `tape` to
76
+ seek. The world lives in the mix as the state of a stateful patch, so blits' history keeps a copy
77
+ every `every` ms and a seek replays from the nearest one. A copy is Rapier's snapshot, kept as
78
+ bytes and rebuilt into a world only when a replay needs it, so history costs memory in proportion
79
+ to the world's size and `ms / every`. Each frame's pushes are logged beside it for the replay and
80
+ pruned with history.
81
+
82
+ Replay matches the first pass exactly on one machine. To match across machines, pass Rapier's
83
+ deterministic build.
84
+
85
+ ## Not yet
86
+
87
+ - Kinematic bodies moved by a voice. They need a channel with no rest, which blits' `vec` of
88
+ `sum()` is not.
89
+ - Paging history to a `store`: a Rapier description is not plain data, so a body's add cannot be
90
+ written out.
91
+ - Soft bodies, cloth and ropes.
@@ -0,0 +1,23 @@
1
+ import { type Channel, type Vec } from '@msb235/blits';
2
+ /** The channels a 2D world reads and writes: a body's pose, and the push on its controls. */
3
+ export interface Pose2 {
4
+ position: Vec<2>;
5
+ rotation: number;
6
+ force: Vec<2>;
7
+ torque: number;
8
+ }
9
+ /** The channels a 3D world reads and writes: a body's pose, and the push on its controls. */
10
+ export interface Pose3 {
11
+ position: Vec<3>;
12
+ rotation: Vec<4>;
13
+ force: Vec<3>;
14
+ torque: Vec<3>;
15
+ }
16
+ /** A 2D world's channels, to spread into a kit. */
17
+ export declare const channels2: {
18
+ [K in keyof Pose2]: Channel<Pose2[K]>;
19
+ };
20
+ /** A 3D world's channels, to spread into a kit. */
21
+ export declare const channels3: {
22
+ [K in keyof Pose3]: Channel<Pose3[K]>;
23
+ };
@@ -0,0 +1,15 @@
1
+ import { angle, quat, sum, vec } from '@msb235/blits';
2
+ /** A 2D world's channels, to spread into a kit. */
3
+ export const channels2 = {
4
+ position: vec(2, sum()),
5
+ rotation: angle(),
6
+ force: vec(2, sum()),
7
+ torque: sum(),
8
+ };
9
+ /** A 3D world's channels, to spread into a kit. */
10
+ export const channels3 = {
11
+ position: vec(3, sum()),
12
+ rotation: quat(),
13
+ force: vec(3, sum()),
14
+ torque: vec(3, sum()),
15
+ };
@@ -0,0 +1,3 @@
1
+ export { channels2, channels3, type Pose2, type Pose3 } from './channels.js';
2
+ export { type Body, type Physics, type PhysicsOptions, physics } from './physics.js';
3
+ export type { RapierBody, RapierModule, RapierVector, RapierWorld } from './rapier.js';
package/dist/index.js ADDED
@@ -0,0 +1,2 @@
1
+ export { channels2, channels3 } from './channels.js';
2
+ export { physics } from './physics.js';
@@ -0,0 +1,43 @@
1
+ import { type Handle, type Kit, type Mix, type MixOptions, type VoiceSpec } from '@msb235/blits';
2
+ import type { Pose2, Pose3 } from './channels.js';
3
+ import { type RapierModule, type RapierWorld } from './rapier.js';
4
+ /** What `physics` takes: a mix's options, the Rapier module, and the world's gravity. */
5
+ export interface PhysicsOptions<H> extends MixOptions<H> {
6
+ /** An initialized Rapier module. `@dimforge/rapier3d-compat` makes a 3D world, `rapier2d-compat` a 2D one. */
7
+ rapier: RapierModule;
8
+ /** Default: `[0, -9.81]` or `[0, -9.81, 0]`. */
9
+ gravity?: readonly number[];
10
+ }
11
+ /** A rigid body: the mix's subject for its pose, with its controls beside it. */
12
+ export interface Body<O, H> {
13
+ readonly key: number;
14
+ /** The subject whose `force` and `torque` push the body, read before the world steps. */
15
+ readonly controls: object;
16
+ /** The voice that shows the body where the world puts it, in `Physics.role`. Weight 1. */
17
+ readonly simulated: Handle<object>;
18
+ /** Cues a voice on the body's pose. In `Physics.role` it is an alternative to the simulation; outside it, it adds on top. */
19
+ cue(spec: Omit<VoiceSpec<object, O, H>, 'subjects'>): Handle<object>;
20
+ /** Cues a voice on the body's controls. */
21
+ drive(spec: Omit<VoiceSpec<object, O, H>, 'subjects'>): Handle<object>;
22
+ /** Applies an impulse once, on the step after the mix's current time, and again on any replay through it. */
23
+ impulse(v: readonly number[]): void;
24
+ /** Takes the body out of the world on the step after the mix's current time. A seek back before then brings it back. */
25
+ drop(): void;
26
+ }
27
+ /** A Rapier world held in a blits mix. */
28
+ export interface Physics<O, H> {
29
+ readonly mix: Mix<object, O, H>;
30
+ /** The role every body's simulated voice plays. */
31
+ readonly role: string;
32
+ /**
33
+ * Adds a body made from Rapier's descriptions, on the step after the mix's current time, or on
34
+ * the first step before the mix has synced.
35
+ */
36
+ body(desc: unknown, ...colliders: unknown[]): Body<O, H>;
37
+ /** Syncs the mix to `now`, reads every body's controls, and steps the world. Call it in place of `mix.sync`. */
38
+ sync(now: number): void;
39
+ /** The world as it stands after the last `sync`, for reading. A change made to it is lost at the next seek. */
40
+ world(): RapierWorld | undefined;
41
+ }
42
+ /** Makes a mix that holds a Rapier world, stepped on its `stepMs` clock and replayed by its history. */
43
+ export declare function physics<O extends Pose2 | Pose3, H extends object = Record<string, never>>(kit: Kit<O>, opts: PhysicsOptions<H>): Physics<O, H>;
@@ -0,0 +1,148 @@
1
+ import { mix, patch, } from '@msb235/blits';
2
+ import { dimsOf } from './rapier.js';
3
+ import { applyLog, copy, insert, worldOf } from './world.js';
4
+ const ROLE = 'physics';
5
+ /** Makes a mix that holds a Rapier world, stepped on its `stepMs` clock and replayed by its history. */
6
+ export function physics(kit, opts) {
7
+ const { rapier, gravity, ...rest } = opts;
8
+ const dims = dimsOf(rapier);
9
+ const g = gravity ?? (dims.n === 2 ? [0, -9.81] : [0, -9.81, 0]);
10
+ const m = mix(kit, { ...rest, stepMs: rest.stepMs ?? 1000 / 60 });
11
+ const log = [];
12
+ // Each frame's pushes at its mix time. A seek replays steps on what the host reads now, so the
13
+ // world reads the pushes of the frame that caused each step from here instead: the first frame
14
+ // at or after the step's end, looked up half a step early since replayed times can round apart.
15
+ const pushLog = [];
16
+ const keep = rest.history?.ms ?? 0;
17
+ const pushesAt = (t) => {
18
+ let lo = 0;
19
+ let hi = pushLog.length;
20
+ while (lo < hi) {
21
+ const mid = (lo + hi) >> 1;
22
+ if (pushLog[mid].at < t)
23
+ lo = mid + 1;
24
+ else
25
+ hi = mid;
26
+ }
27
+ return (pushLog[lo] ?? pushLog[pushLog.length - 1])?.pushes ?? {};
28
+ };
29
+ let seen;
30
+ let live;
31
+ const world = patch(0, (_p, _s, setting) => {
32
+ const s = setting.state;
33
+ // A world first read before it has stepped holds the bodies added before then.
34
+ if (Number.isNaN(s.t)) {
35
+ applyLog(s, worldOf(s, rapier), log, setting.timestamp, 0, dims);
36
+ s.t = setting.timestamp;
37
+ }
38
+ seen = s;
39
+ return {};
40
+ }, {
41
+ writes: [],
42
+ state: () => ({
43
+ bytes: null,
44
+ world: new rapier.World(dims.vector(g)),
45
+ handles: new Map(),
46
+ t: Number.NaN,
47
+ }),
48
+ step: (s, dt, _subject, setting) => {
49
+ const w = worldOf(s, rapier);
50
+ w.timestep = dt / 1000;
51
+ applyLog(s, w, log, setting.timestamp, dt / 2, dims);
52
+ const pushes = pushesAt(setting.timestamp - dt / 2);
53
+ const pushed = [];
54
+ for (const k in pushes) {
55
+ const handle = s.handles.get(+k);
56
+ if (handle === undefined)
57
+ continue;
58
+ const v = pushes[k];
59
+ const b = w.getRigidBody(handle);
60
+ b.addForce(dims.vector(v), true);
61
+ dims.torque(b, dims.n === 2 ? (v[2] ?? 0) : v.slice(3));
62
+ pushed.push(handle);
63
+ }
64
+ w.step();
65
+ for (const handle of pushed) {
66
+ const b = w.getRigidBody(handle);
67
+ b.resetForces(false);
68
+ b.resetTorques(false);
69
+ }
70
+ s.t = setting.timestamp;
71
+ },
72
+ clone: copy,
73
+ });
74
+ const WORLD = {};
75
+ let started = false;
76
+ const simulated = patch(0, (_p, subject) => {
77
+ const handle = live?.handles.get(subject.key);
78
+ if (live === undefined || handle === undefined)
79
+ return {};
80
+ const b = worldOf(live, rapier).getRigidBody(handle);
81
+ return { position: dims.position(b), rotation: dims.rotation(b) };
82
+ }, { writes: ['position', 'rotation'] });
83
+ const now = () => (Number.isNaN(m.timestamp) ? Number.NEGATIVE_INFINITY : m.timestamp);
84
+ const driven = new Set();
85
+ const out = m.pose();
86
+ let next = 0;
87
+ return {
88
+ mix: m,
89
+ role: ROLE,
90
+ body(desc, ...colliders) {
91
+ const key = next++;
92
+ insert(log, { at: now(), key, kind: 'add', desc, colliders });
93
+ const controls = {};
94
+ const own = (spec, subject) => {
95
+ if ('subjects' in spec)
96
+ throw new Error('blits-rapier: a body cues on itself; its spec names no subjects');
97
+ return m.cue({ ...spec, subjects: [subject] });
98
+ };
99
+ const body = {
100
+ key,
101
+ controls,
102
+ simulated: undefined,
103
+ cue: (spec) => own(spec, body),
104
+ drive: (spec) => {
105
+ driven.add(body);
106
+ return own(spec, controls);
107
+ },
108
+ impulse: (v) => insert(log, { at: now(), key, kind: 'impulse', v: [...v] }),
109
+ drop: () => {
110
+ insert(log, { at: now(), key, kind: 'drop' });
111
+ },
112
+ };
113
+ body.simulated = m.cue({
114
+ patch: simulated,
115
+ subjects: [body],
116
+ role: ROLE,
117
+ });
118
+ return body;
119
+ },
120
+ sync(t) {
121
+ m.sync(t);
122
+ // Cued before the mix has a time, the world would catch up from mix time 0 on its first step.
123
+ if (!started) {
124
+ started = true;
125
+ m.cue({ patch: world, subjects: [WORLD] });
126
+ }
127
+ const pushes = {};
128
+ for (const body of driven) {
129
+ const p = m.probe(body.controls, out);
130
+ const torque = typeof p.torque === 'number' ? [p.torque] : p.torque;
131
+ if (p.force.some((x) => x !== 0) || torque.some((x) => x !== 0))
132
+ pushes[body.key] = [...p.force, ...torque];
133
+ }
134
+ const at = m.timestamp;
135
+ while (pushLog.length > 0 && pushLog[pushLog.length - 1].at >= at)
136
+ pushLog.pop();
137
+ pushLog.push({ at, pushes });
138
+ let old = 0;
139
+ while (old < pushLog.length - 1 && pushLog[old + 1].at < at - keep)
140
+ old++;
141
+ if (old > 0)
142
+ pushLog.splice(0, old);
143
+ m.probe(WORLD);
144
+ live = seen;
145
+ },
146
+ world: () => (live === undefined ? undefined : worldOf(live, rapier)),
147
+ };
148
+ }
@@ -0,0 +1,55 @@
1
+ /** A vector as Rapier takes and gives it: `z` in 3D only. */
2
+ export interface RapierVector {
3
+ x: number;
4
+ y: number;
5
+ z?: number;
6
+ }
7
+ /** What blits-rapier uses of a Rapier rigid body, in either build. */
8
+ export interface RapierBody {
9
+ readonly handle: number;
10
+ translation(): RapierVector;
11
+ rotation(): number | {
12
+ x: number;
13
+ y: number;
14
+ z: number;
15
+ w: number;
16
+ };
17
+ addForce(force: RapierVector, wake: boolean): void;
18
+ addTorque(torque: never, wake: boolean): void;
19
+ applyImpulse(impulse: RapierVector, wake: boolean): void;
20
+ resetForces(wake: boolean): void;
21
+ resetTorques(wake: boolean): void;
22
+ }
23
+ /** What blits-rapier uses of a Rapier world, in either build. */
24
+ export interface RapierWorld {
25
+ timestep: number;
26
+ step(): void;
27
+ takeSnapshot(): Uint8Array;
28
+ createRigidBody(desc: never): RapierBody;
29
+ createCollider(desc: never, parent?: never): unknown;
30
+ getRigidBody(handle: number): RapierBody;
31
+ removeRigidBody(body: never): void;
32
+ free(): void;
33
+ }
34
+ /**
35
+ * A Rapier module, `@dimforge/rapier2d-compat` or `@dimforge/rapier3d-compat` or a build like
36
+ * them, already initialized. Which one is passed decides the dimensions.
37
+ */
38
+ export interface RapierModule {
39
+ World: {
40
+ new (gravity: never): RapierWorld;
41
+ restoreSnapshot(data: Uint8Array): RapierWorld;
42
+ };
43
+ }
44
+ /** The arithmetic that differs between the 2D and 3D builds, behind one shape. */
45
+ export interface Dims {
46
+ readonly n: 2 | 3;
47
+ vector(v: readonly number[]): RapierVector;
48
+ position(body: RapierBody): number[];
49
+ /** An angle in 2D, `[x, y, z, w]` in 3D: what the `rotation` channel holds. */
50
+ rotation(body: RapierBody): number | number[];
51
+ /** Adds `torque`, a number in 2D and three numbers in 3D. */
52
+ torque(body: RapierBody, torque: number | readonly number[]): void;
53
+ }
54
+ /** The 3D build where the module has `Vector3`, else the 2D. */
55
+ export declare function dimsOf(rapier: RapierModule): Dims;
package/dist/rapier.js ADDED
@@ -0,0 +1,27 @@
1
+ const two = {
2
+ n: 2,
3
+ vector: (v) => ({ x: v[0] ?? 0, y: v[1] ?? 0 }),
4
+ position: (b) => {
5
+ const t = b.translation();
6
+ return [t.x, t.y];
7
+ },
8
+ rotation: (b) => b.rotation(),
9
+ torque: (b, t) => b.addTorque((typeof t === 'number' ? t : (t[0] ?? 0)), true),
10
+ };
11
+ const three = {
12
+ n: 3,
13
+ vector: (v) => ({ x: v[0] ?? 0, y: v[1] ?? 0, z: v[2] ?? 0 }),
14
+ position: (b) => {
15
+ const t = b.translation();
16
+ return [t.x, t.y, t.z ?? 0];
17
+ },
18
+ rotation: (b) => {
19
+ const q = b.rotation();
20
+ return [q.x, q.y, q.z, q.w];
21
+ },
22
+ torque: (b, t) => b.addTorque(three.vector(typeof t === 'number' ? [0, 0, t] : t), true),
23
+ };
24
+ /** The 3D build where the module has `Vector3`, else the 2D. */
25
+ export function dimsOf(rapier) {
26
+ return 'Vector3' in rapier ? three : two;
27
+ }
@@ -0,0 +1,43 @@
1
+ import type { Dims, RapierModule, RapierWorld } from './rapier.js';
2
+ /**
3
+ * A world as the world subject's state. A copy for history holds only `bytes`, Rapier's snapshot,
4
+ * so a copy blits lets go of is plain memory; `worldOf` rebuilds the world on first use.
5
+ */
6
+ export interface State {
7
+ bytes: Uint8Array | null;
8
+ world: RapierWorld | null;
9
+ /** Each present body's Rapier handle, by its key. */
10
+ handles: Map<number, number>;
11
+ /** The mix time of the last step, NaN before the first. */
12
+ t: number;
13
+ }
14
+ /** Something done to the world at a mix time, applied by whichever step reaches it, on every replay. */
15
+ export type Entry = {
16
+ at: number;
17
+ key: number;
18
+ kind: 'add';
19
+ desc: unknown;
20
+ colliders: readonly unknown[];
21
+ } | {
22
+ at: number;
23
+ key: number;
24
+ kind: 'drop';
25
+ } | {
26
+ at: number;
27
+ key: number;
28
+ kind: 'impulse';
29
+ v: readonly number[];
30
+ };
31
+ /** The state's world, rebuilt from its snapshot where it has none yet. */
32
+ export declare function worldOf(state: State, rapier: RapierModule): RapierWorld;
33
+ /** A copy sharing nothing with `state`, holding the world as a snapshot. */
34
+ export declare function copy(state: State): State;
35
+ /** Puts `entry` into `log` after every entry at or before its time. */
36
+ export declare function insert(log: Entry[], entry: Entry): void;
37
+ /**
38
+ * Applies the entries a step from the state's time to `to` passes: those at or after the state's
39
+ * time and before `to`, or every one before `to` on a state never stepped. An entry logged at the
40
+ * time the world last stepped to applies on the step after. Both ends are taken `slack` early: a
41
+ * replayed step's times can differ from the first pass's by rounding.
42
+ */
43
+ export declare function applyLog(state: State, world: RapierWorld, log: readonly Entry[], to: number, slack: number, dims: Dims): void;
package/dist/world.js ADDED
@@ -0,0 +1,57 @@
1
+ /** The state's world, rebuilt from its snapshot where it has none yet. */
2
+ export function worldOf(state, rapier) {
3
+ if (state.world === null) {
4
+ state.world = rapier.World.restoreSnapshot(state.bytes);
5
+ state.bytes = null;
6
+ }
7
+ return state.world;
8
+ }
9
+ /** A copy sharing nothing with `state`, holding the world as a snapshot. */
10
+ export function copy(state) {
11
+ return {
12
+ bytes: state.world === null ? state.bytes : state.world.takeSnapshot(),
13
+ world: null,
14
+ handles: new Map(state.handles),
15
+ t: state.t,
16
+ };
17
+ }
18
+ /** Puts `entry` into `log` after every entry at or before its time. */
19
+ export function insert(log, entry) {
20
+ let i = log.length;
21
+ while (i > 0 && log[i - 1].at > entry.at)
22
+ i--;
23
+ log.splice(i, 0, entry);
24
+ }
25
+ /**
26
+ * Applies the entries a step from the state's time to `to` passes: those at or after the state's
27
+ * time and before `to`, or every one before `to` on a state never stepped. An entry logged at the
28
+ * time the world last stepped to applies on the step after. Both ends are taken `slack` early: a
29
+ * replayed step's times can differ from the first pass's by rounding.
30
+ */
31
+ export function applyLog(state, world, log, to, slack, dims) {
32
+ const from = state.t - slack;
33
+ for (const e of log) {
34
+ if (e.at >= to - slack)
35
+ break;
36
+ if (e.at < from)
37
+ continue;
38
+ const handle = state.handles.get(e.key);
39
+ if (e.kind === 'add') {
40
+ if (handle !== undefined)
41
+ continue;
42
+ const body = world.createRigidBody(e.desc);
43
+ for (const c of e.colliders)
44
+ world.createCollider(c, body);
45
+ state.handles.set(e.key, body.handle);
46
+ }
47
+ else if (handle === undefined) {
48
+ }
49
+ else if (e.kind === 'drop') {
50
+ world.removeRigidBody(world.getRigidBody(handle));
51
+ state.handles.delete(e.key);
52
+ }
53
+ else {
54
+ world.getRigidBody(handle).applyImpulse(dims.vector(e.v), true);
55
+ }
56
+ }
57
+ }
package/package.json CHANGED
@@ -1,6 +1,58 @@
1
1
  {
2
2
  "name": "@msb235/blits-rapier",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "description": "Pairs Rapier rigid-body physics with a blits mix: voices push bodies, and each body's simulated pose blends with authored motion",
5
+ "keywords": [
6
+ "physics",
7
+ "rapier",
8
+ "blits",
9
+ "animation"
10
+ ],
11
+ "license": "MIT",
12
+ "author": "orochi235",
13
+ "repository": {
14
+ "type": "git",
15
+ "url": "git+https://github.com/orochi235/blits.git",
16
+ "directory": "packages/rapier"
17
+ },
18
+ "homepage": "https://github.com/orochi235/blits/tree/main/packages/rapier#readme",
19
+ "bugs": "https://github.com/orochi235/blits/issues",
20
+ "publishConfig": {
21
+ "access": "public"
22
+ },
23
+ "type": "module",
24
+ "exports": {
25
+ ".": {
26
+ "types": "./dist/index.d.ts",
27
+ "default": "./dist/index.js"
28
+ }
29
+ },
30
+ "files": [
31
+ "dist",
32
+ "!dist/.tsbuildinfo",
33
+ "CHANGELOG.md",
34
+ "LICENSE"
35
+ ],
36
+ "scripts": {
37
+ "build": "tsc -b",
38
+ "prepack": "rm -rf dist && npm run build"
39
+ },
40
+ "peerDependencies": {
41
+ "@msb235/blits": "^0.16.1",
42
+ "@dimforge/rapier2d-compat": ">=0.21.0 <1.0.0",
43
+ "@dimforge/rapier3d-compat": ">=0.21.0 <1.0.0"
44
+ },
45
+ "devDependencies": {
46
+ "@dimforge/rapier2d-compat": "0.21.0",
47
+ "@dimforge/rapier3d-compat": "0.21.0",
48
+ "@msb235/blits": "0.16.1"
49
+ },
50
+ "peerDependenciesMeta": {
51
+ "@dimforge/rapier2d-compat": {
52
+ "optional": true
53
+ },
54
+ "@dimforge/rapier3d-compat": {
55
+ "optional": true
56
+ }
57
+ }
58
+ }