@msb235/blits-rapier 0.2.0 → 0.4.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 CHANGED
@@ -4,6 +4,39 @@ This package follows [semver](https://semver.org). Below 1.0.0, a breaking chang
4
4
  version and everything else the patch. The release workflow refuses a `rapier-v*` tag with no
5
5
  section here.
6
6
 
7
+ ## 0.4.0
8
+
9
+ ### Breaking
10
+
11
+ - `@msb235/blits` is a peer at `^0.19.0`, where it was `^0.18.0`. Built and tested against engine
12
+ 0.19.0; nothing here uses what that release changed.
13
+ - `PhysicsOptions` no longer takes `keyOf`: every subject in the mix is the driver's, and it keys
14
+ them itself.
15
+
16
+ ### Added
17
+
18
+ - Kinematic bodies moved by a voice: `kinematic2` and `kinematic3` add `goal` and `facing` channels
19
+ to a body's controls, which move a kinematic body over the next step. Neither has a rest, so a
20
+ body no voice writes stays where it is.
21
+ - History pages to a `store`. The world's state packs to plain data, its Rapier snapshot as base64,
22
+ and the log of each frame's pushes pages out beside it, so memory stays bounded.
23
+
24
+ ### Fixed
25
+
26
+ - Pushes, impulses, drops and added bodies are logged at the mix's time (`mix.now`), not the host's
27
+ timestamp, so a seek, `rebase` or rate other than 1 no longer files them at the wrong step.
28
+ - The log of each frame's pushes reaches back to the world copy a replay at the edge of memory
29
+ starts from, not just `history.ms`.
30
+ - `channels2`'s `rotation` takes a turn as `2 * Math.PI`. It was `angle()`, whose turn is 360, so
31
+ two radian angles either side of ±π blended the long way round, through 0.
32
+
33
+ ## 0.3.0
34
+
35
+ ### Breaking
36
+
37
+ - `@msb235/blits` is a peer at `^0.18.0`, where it was `^0.17.0`. Built and tested against engine
38
+ 0.18.0; nothing here uses what that release changed.
39
+
7
40
  ## 0.2.0
8
41
 
9
42
  ### Breaking
package/README.md CHANGED
@@ -65,13 +65,35 @@ const { position, rotation } = sim.mix.probe(box);
65
65
  | Channel | 2D (`channels2`) | 3D (`channels3`) | On |
66
66
  |---|---|---|---|
67
67
  | `position` | `vec(2, sum())` | `vec(3, sum())` | the body |
68
- | `rotation` | `angle()`, radians | `quat()`, `[x, y, z, w]` | the body |
68
+ | `rotation` | `angle({ turn: 2 * Math.PI })`, radians | `quat()`, `[x, y, z, w]` | the body |
69
69
  | `force` | `vec(2, sum())` | `vec(3, sum())` | its controls |
70
70
  | `torque` | `sum()` | `vec(3, sum())` | its controls |
71
71
 
72
72
  Spread them into a kit with your own channels. Forces and torques are in Rapier's units, newtons
73
73
  and newton-meters for a body whose mass you set in kilograms.
74
74
 
75
+ ## Kinematic bodies
76
+
77
+ A body made with `RigidBodyDesc.kinematicPositionBased()` goes where a voice on its controls says,
78
+ through two more channels, `kinematic2` or `kinematic3`, spread into the kit beside the others:
79
+
80
+ | Channel | 2D (`kinematic2`) | 3D (`kinematic3`) |
81
+ |---|---|---|
82
+ | `goal` | `vec(2, last())`, a position | `vec(3, last())`, a position |
83
+ | `facing` | `last()`, an angle in radians | `last()`, an `[x, y, z, w]` quaternion |
84
+
85
+ ```ts
86
+ const sim = physics(kit<Pose3 & Kinematic3>({ ...channels3, ...kinematic3 }), { rapier: RAPIER });
87
+ const door = sim.body(RAPIER.RigidBodyDesc.kinematicPositionBased(), RAPIER.ColliderDesc.cuboid(1, 2, 0.1));
88
+ door.drive({ patch: keys(800, [{ at: 0, delta: { goal: [0, 2, 0] } }, { at: 1, delta: { goal: [2, 2, 0] } }]) });
89
+ ```
90
+
91
+ Neither has a rest: a body no voice writes a goal for stays where it is, and one whose voice has
92
+ ended stays where it left it. The world moves the body there over the next step, at the speed that
93
+ takes, so it pushes what it meets. Since the channels are `last`, a voice's weight only switches its
94
+ goal on or off, and of the voices writing one, the last cued wins; keyframes between two goals
95
+ interpolate, the facing the short way round.
96
+
75
97
  ## The clock and seeking
76
98
 
77
99
  `physics` makes the mix, with `stepMs` 1000 / 60 unless you set it; the world takes one Rapier step
@@ -79,16 +101,17 @@ per interval, so it runs at the same rate whatever the frame rate. Pass `history
79
101
  seek. The world lives in the mix as the state of a stateful patch, so blits' history keeps a copy
80
102
  every `every` ms and a seek replays from the nearest one. A copy is Rapier's snapshot, kept as
81
103
  bytes and rebuilt into a world only when a replay needs it, so history costs memory in proportion
82
- to the world's size and `ms / every`. Each frame's pushes are logged beside it for the replay and
83
- pruned with history.
104
+ to the world's size and `ms / every`. Each frame's pushes are logged beside it for the replay, back
105
+ to the copy a replay at the edge of memory would start from.
106
+
107
+ `history.store` works: the world pages out as plain data, its snapshot in base64. Give the mix a
108
+ `name`, as every mix on a transport with a store needs; `physics` keys its own subjects, so it takes
109
+ no `keyOf`. The log of each frame's pushes pages out with it, one `host` record a frame under the
110
+ subject `'blits-rapier frames'`, and `prepare` loads it back for a seek past memory.
84
111
 
85
112
  Replay matches the first pass exactly on one machine. To match across machines, pass Rapier's
86
113
  deterministic build.
87
114
 
88
115
  ## Not yet
89
116
 
90
- - Kinematic bodies moved by a voice. They need a channel with no rest, which blits' `vec` of
91
- `sum()` is not.
92
- - Paging history to a `store`: a Rapier description is not plain data, so a body's add cannot be
93
- written out.
94
117
  - Soft bodies, cloth and ropes.
@@ -21,3 +21,25 @@ export declare const channels2: {
21
21
  export declare const channels3: {
22
22
  [K in keyof Pose3]: Channel<Pose3[K]>;
23
23
  };
24
+ /**
25
+ * Where a voice on a kinematic 2D body's controls moves it: a position and an angle in radians. Each
26
+ * has no rest, so a body with no voice writing it stays where it is. A voice's weight only switches
27
+ * its goal on or off, and of the voices writing one, the last cued wins.
28
+ */
29
+ export interface Kinematic2 {
30
+ goal?: Vec<2>;
31
+ facing?: number;
32
+ }
33
+ /** Where a voice on a kinematic 3D body's controls moves it: a position and an `[x, y, z, w]` quaternion. See `Kinematic2`. */
34
+ export interface Kinematic3 {
35
+ goal?: Vec<3>;
36
+ facing?: Vec<4>;
37
+ }
38
+ /** A kinematic 2D body's channels, to spread into a kit beside `channels2`. */
39
+ export declare const kinematic2: {
40
+ [K in keyof Kinematic2]-?: Channel<NonNullable<Kinematic2[K]>>;
41
+ };
42
+ /** A kinematic 3D body's channels, to spread into a kit beside `channels3`. */
43
+ export declare const kinematic3: {
44
+ [K in keyof Kinematic3]-?: Channel<NonNullable<Kinematic3[K]>>;
45
+ };
package/dist/channels.js CHANGED
@@ -1,8 +1,8 @@
1
- import { angle, quat, sum, vec } from '@msb235/blits';
1
+ import { angle, last, quat, sum, vec } from '@msb235/blits';
2
2
  /** A 2D world's channels, to spread into a kit. */
3
3
  export const channels2 = {
4
4
  position: vec(2, sum()),
5
- rotation: angle(),
5
+ rotation: angle({ turn: 2 * Math.PI }),
6
6
  force: vec(2, sum()),
7
7
  torque: sum(),
8
8
  };
@@ -13,3 +13,14 @@ export const channels3 = {
13
13
  force: vec(3, sum()),
14
14
  torque: vec(3, sum()),
15
15
  };
16
+ const along = (a, b, u) => a + (b - a) * u;
17
+ /** A kinematic 2D body's channels, to spread into a kit beside `channels2`. */
18
+ export const kinematic2 = {
19
+ goal: vec(2, last({ lerp: along })),
20
+ facing: last({ lerp: angle({ turn: 2 * Math.PI }).lerp }),
21
+ };
22
+ /** A kinematic 3D body's channels, to spread into a kit beside `channels3`. */
23
+ export const kinematic3 = {
24
+ goal: vec(3, last({ lerp: along })),
25
+ facing: last({ lerp: quat().lerp }),
26
+ };
package/dist/index.d.ts CHANGED
@@ -1,3 +1,3 @@
1
- export { channels2, channels3, type Pose2, type Pose3 } from './channels.js';
1
+ export { channels2, channels3, type Kinematic2, type Kinematic3, kinematic2, kinematic3, type Pose2, type Pose3, } from './channels.js';
2
2
  export { type Body, type Physics, type PhysicsOptions, physics } from './physics.js';
3
3
  export type { RapierBody, RapierModule, RapierVector, RapierWorld } from './rapier.js';
package/dist/index.js CHANGED
@@ -1,2 +1,2 @@
1
- export { channels2, channels3 } from './channels.js';
1
+ export { channels2, channels3, kinematic2, kinematic3, } from './channels.js';
2
2
  export { physics } from './physics.js';
package/dist/physics.d.ts CHANGED
@@ -1,8 +1,11 @@
1
1
  import { type Handle, type Kit, type Mix, type MixOptions, type Signal, type VoiceSpec } from '@msb235/blits';
2
2
  import type { Pose2, Pose3 } from './channels.js';
3
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> {
4
+ /**
5
+ * What `physics` takes: a mix's options, the Rapier module, and the world's gravity. There is no
6
+ * `keyOf`: every subject is the driver's own, and it keys them itself for a history store.
7
+ */
8
+ export interface PhysicsOptions<H> extends Omit<MixOptions<H>, 'keyOf'> {
6
9
  /** An initialized Rapier module. `@dimforge/rapier3d-compat` makes a 3D world, `rapier2d-compat` a 2D one. */
7
10
  rapier: RapierModule;
8
11
  /** Default: `[0, -9.81]` or `[0, -9.81, 0]`. */
package/dist/physics.js CHANGED
@@ -1,20 +1,66 @@
1
1
  import { mix, patch, } from '@msb235/blits';
2
2
  import { dimsOf } from './rapier.js';
3
- import { applyLog, copy, insert, worldOf } from './world.js';
3
+ import { applyLog, copy, insert, pack, unpack, worldOf } from './world.js';
4
4
  const ROLE = 'physics';
5
5
  const copyOf = (r) => typeof r === 'number' ? r : [...r];
6
+ /** The subject key the world's frame records go under in a history store, on the `host` stream. */
7
+ const FRAMES = 'blits-rapier frames';
6
8
  /** Makes a mix that holds a Rapier world, stepped on its `stepMs` clock and replayed by its history. */
7
9
  export function physics(kit, opts) {
8
10
  const { rapier, gravity, weight, ...rest } = opts;
9
11
  const dims = dimsOf(rapier);
10
12
  const g = gravity ?? (dims.n === 2 ? [0, -9.81] : [0, -9.81, 0]);
11
- const m = mix(kit, { ...rest, stepMs: rest.stepMs ?? 1000 / 60 });
12
- const log = [];
13
+ const stepMs = rest.stepMs ?? 1000 / 60;
14
+ const name = rest.name ?? '';
15
+ const history = rest.history;
16
+ const store = history?.store;
13
17
  // Each frame's pushes at its mix time. A seek replays steps on what the host reads now, so the
14
18
  // world reads the pushes of the frame that caused each step from here instead: the first frame
15
19
  // at or after the step's end, looked up half a step early since replayed times can round apart.
20
+ // It reaches back past memory to the world's copy a replay starts from; older frames go to the
21
+ // store as host records under `FRAMES`, and come back when `prepare` loads.
16
22
  const pushLog = [];
17
- const keep = rest.history?.ms ?? 0;
23
+ const keep = history === undefined ? 0 : history.ms + (history.every ?? 200) + 2 * stepMs;
24
+ const ours = (r) => r.mix === name && r.stream === 'host' && r.subject === FRAMES;
25
+ const restore = (records, from) => {
26
+ let start = Number.NEGATIVE_INFINITY;
27
+ for (const r of records)
28
+ if (ours(r) && r.at <= from && r.at > start)
29
+ start = r.at;
30
+ const byAt = new Map();
31
+ for (const r of records)
32
+ if (ours(r) && r.at >= start && (byAt.get(r.at)?.seq ?? -1) < r.seq)
33
+ byAt.set(r.at, r);
34
+ const first = pushLog[0]?.at ?? Number.POSITIVE_INFINITY;
35
+ const older = [...byAt.values()]
36
+ .filter((r) => r.at < first)
37
+ .sort((a, b) => a.at - b.at)
38
+ .map((r) => ({ ...r.data, at: r.at, seq: r.seq }));
39
+ pushLog.unshift(...older);
40
+ };
41
+ const paging = store && {
42
+ page: (out) => store.page(out),
43
+ cut: (seq) => store.cut(seq),
44
+ async load(t) {
45
+ const all = await store.load(t);
46
+ // A replay to t starts from the world's latest copy at or before it, which needs the frames from there.
47
+ let from = t;
48
+ for (const r of all)
49
+ if (r.mix === name && r.stream === 'snap' && r.subject === 'world' && r.at < from)
50
+ from = r.at;
51
+ restore(from < t ? await store.load(from) : all, from);
52
+ return all.filter((r) => !ours(r));
53
+ },
54
+ };
55
+ // Every subject is the driver's own: the world, each body and each body's controls.
56
+ const keys = new WeakMap();
57
+ const m = mix(kit, {
58
+ ...rest,
59
+ ...(paging ? { history: { ...history, store: paging } } : {}),
60
+ stepMs,
61
+ keyOf: (subject) => keys.get(subject),
62
+ });
63
+ const log = [];
18
64
  const frameAt = (t) => {
19
65
  let lo = 0;
20
66
  let hi = pushLog.length;
@@ -59,6 +105,13 @@ export function physics(kit, opts) {
59
105
  if (handle !== undefined && h !== undefined)
60
106
  dims.place(w.getRigidBody(handle), h.now, h.before, h.dt);
61
107
  }
108
+ const goals = frame?.goals ?? {};
109
+ for (const k in goals) {
110
+ const handle = s.handles.get(+k);
111
+ const g = goals[k];
112
+ if (handle !== undefined && g !== undefined)
113
+ dims.aim(w.getRigidBody(handle), g);
114
+ }
62
115
  const pushed = [];
63
116
  for (const k in pushes) {
64
117
  const handle = s.handles.get(+k);
@@ -79,8 +132,11 @@ export function physics(kit, opts) {
79
132
  s.t = setting.timestamp;
80
133
  },
81
134
  clone: copy,
135
+ pack,
136
+ unpack,
82
137
  });
83
138
  const WORLD = {};
139
+ keys.set(WORLD, 'world');
84
140
  let started = false;
85
141
  const simulated = patch(0, (_p, subject) => {
86
142
  const handle = live?.handles.get(subject.key);
@@ -89,7 +145,7 @@ export function physics(kit, opts) {
89
145
  const b = worldOf(live, rapier).getRigidBody(handle);
90
146
  return { position: dims.position(b), rotation: dims.rotation(b) };
91
147
  }, { writes: ['position', 'rotation'] });
92
- const now = () => (Number.isNaN(m.timestamp) ? Number.NEGATIVE_INFINITY : m.timestamp);
148
+ const now = () => (Number.isNaN(m.now) ? Number.NEGATIVE_INFINITY : m.now);
93
149
  const driven = new Set();
94
150
  // Each body's voices in the simulation's role besides its own, and its pose the last frame they held it.
95
151
  const authored = new Map();
@@ -133,6 +189,7 @@ export function physics(kit, opts) {
133
189
  const key = next++;
134
190
  insert(log, { at: now(), key, kind: 'add', desc, colliders });
135
191
  const controls = {};
192
+ keys.set(controls, `controls ${key}`);
136
193
  const own = (spec, subject) => {
137
194
  if ('subjects' in spec)
138
195
  throw new Error('blits-rapier: a body cues on itself; its spec names no subjects');
@@ -157,6 +214,7 @@ export function physics(kit, opts) {
157
214
  insert(log, { at: now(), key, kind: 'drop' });
158
215
  },
159
216
  };
217
+ keys.set(body, `body ${key}`);
160
218
  body.simulated = m.cue({
161
219
  patch: simulated,
162
220
  subjects: [body],
@@ -173,13 +231,22 @@ export function physics(kit, opts) {
173
231
  m.cue({ patch: world, subjects: [WORLD] });
174
232
  }
175
233
  const pushes = {};
234
+ const goals = {};
176
235
  for (const body of driven) {
177
236
  const p = m.probe(body.controls, out);
178
237
  const torque = typeof p.torque === 'number' ? [p.torque] : p.torque;
179
238
  if (p.force.some((x) => x !== 0) || torque.some((x) => x !== 0))
180
239
  pushes[body.key] = [...p.force, ...torque];
240
+ if (p.goal !== undefined || p.facing !== undefined) {
241
+ const g = {};
242
+ if (p.goal !== undefined)
243
+ g.position = [...p.goal];
244
+ if (p.facing !== undefined)
245
+ g.rotation = copyOf(p.facing);
246
+ goals[body.key] = g;
247
+ }
181
248
  }
182
- const at = m.timestamp;
249
+ const at = m.now;
183
250
  let back = false;
184
251
  while (pushLog.length > 0 && pushLog[pushLog.length - 1].at >= at) {
185
252
  pushLog.pop();
@@ -190,12 +257,20 @@ export function physics(kit, opts) {
190
257
  holding = {};
191
258
  held.clear();
192
259
  }
193
- pushLog.push({ at, pushes, holds: holding });
260
+ pushLog.push({ at, seq: m.seq, pushes, holds: holding, goals });
194
261
  let old = 0;
195
262
  while (old < pushLog.length - 1 && pushLog[old + 1].at < at - keep)
196
263
  old++;
197
- if (old > 0)
198
- pushLog.splice(0, old);
264
+ const gone = old > 0 ? pushLog.splice(0, old) : undefined;
265
+ if (gone !== undefined && store !== undefined)
266
+ store.page(gone.map(({ at, seq, pushes, holds, goals }) => ({
267
+ mix: name,
268
+ stream: 'host',
269
+ subject: FRAMES,
270
+ at,
271
+ seq,
272
+ data: { pushes, holds, goals },
273
+ })));
199
274
  m.probe(WORLD);
200
275
  live = seen;
201
276
  holding = holdsAt(at);
package/dist/rapier.d.ts CHANGED
@@ -23,6 +23,8 @@ export interface RapierBody {
23
23
  setRotation(r: never, wake: boolean): void;
24
24
  setLinvel(v: RapierVector, wake: boolean): void;
25
25
  setAngvel(v: never, wake: boolean): void;
26
+ setNextKinematicTranslation(v: RapierVector): void;
27
+ setNextKinematicRotation(r: never): void;
26
28
  }
27
29
  /** What blits-rapier uses of a Rapier world, in either build. */
28
30
  export interface RapierWorld {
@@ -56,6 +58,13 @@ export interface Dims {
56
58
  torque(body: RapierBody, torque: number | readonly number[]): void;
57
59
  /** Puts the body at a pose, moving at the speed it took to get there from `before` over `dt` ms. */
58
60
  place(body: RapierBody, now: Placed, before: Placed | undefined, dt: number): void;
61
+ /** Moves a kinematic body to whichever of a position and rotation is given, over the next step. */
62
+ aim(body: RapierBody, goal: Goal): void;
63
+ }
64
+ /** Where a kinematic body's controls ask it to be: either part may be absent. */
65
+ export interface Goal {
66
+ position?: readonly number[];
67
+ rotation?: number | readonly number[];
59
68
  }
60
69
  /** A body's position and rotation as the channels hold them. */
61
70
  export interface Placed {
package/dist/rapier.js CHANGED
@@ -17,6 +17,12 @@ const two = {
17
17
  turn -= 2 * Math.PI * Math.round(turn / (2 * Math.PI));
18
18
  b.setAngvel((dt > 0 ? turn / (dt / 1000) : 0), true);
19
19
  },
20
+ aim: (b, goal) => {
21
+ if (goal.position !== undefined)
22
+ b.setNextKinematicTranslation(two.vector(goal.position));
23
+ if (goal.rotation !== undefined)
24
+ b.setNextKinematicRotation(goal.rotation);
25
+ },
20
26
  };
21
27
  const three = {
22
28
  n: 3,
@@ -37,6 +43,16 @@ const three = {
37
43
  b.setLinvel(three.vector(speed(now.position, before?.position, dt)), true);
38
44
  b.setAngvel(three.vector(spin(now.rotation, before?.rotation, dt)), true);
39
45
  },
46
+ aim: (b, goal) => {
47
+ if (goal.position !== undefined)
48
+ b.setNextKinematicTranslation(three.vector(goal.position));
49
+ if (goal.rotation === undefined)
50
+ return;
51
+ const [x, y, z, w] = goal.rotation;
52
+ // A keyed facing need not be unit length, and Rapier takes it as given.
53
+ const n = Math.hypot(x, y, z, w) || 1;
54
+ b.setNextKinematicRotation({ x: x / n, y: y / n, z: z / n, w: w / n });
55
+ },
40
56
  };
41
57
  /** The angular velocity turning `before` into `now` over `dt` ms, both `[x, y, z, w]`. */
42
58
  function spin(now, before, dt) {
package/dist/world.d.ts CHANGED
@@ -32,6 +32,10 @@ export type Entry = {
32
32
  export declare function worldOf(state: State, rapier: RapierModule): RapierWorld;
33
33
  /** A copy sharing nothing with `state`, holding the world as a snapshot. */
34
34
  export declare function copy(state: State): State;
35
+ /** The state as plain data, for a history store: the snapshot as base64 and the handles as pairs. */
36
+ export declare function pack(state: State): unknown;
37
+ /** A state from what `pack` made, its world left to `worldOf` to rebuild. */
38
+ export declare function unpack(data: unknown): State;
35
39
  /** Puts `entry` into `log` after every entry at or before its time. */
36
40
  export declare function insert(log: Entry[], entry: Entry): void;
37
41
  /**
package/dist/world.js CHANGED
@@ -15,6 +15,28 @@ export function copy(state) {
15
15
  t: state.t,
16
16
  };
17
17
  }
18
+ /** The state as plain data, for a history store: the snapshot as base64 and the handles as pairs. */
19
+ export function pack(state) {
20
+ const bytes = state.world === null ? state.bytes : state.world.takeSnapshot();
21
+ let text = '';
22
+ // In chunks: a spread of a large snapshot overflows the call stack.
23
+ for (let i = 0; i < (bytes?.length ?? 0); i += 0x8000)
24
+ text += String.fromCharCode(...bytes.subarray(i, i + 0x8000));
25
+ return {
26
+ bytes: btoa(text),
27
+ handles: [...state.handles],
28
+ t: Number.isNaN(state.t) ? null : state.t,
29
+ };
30
+ }
31
+ /** A state from what `pack` made, its world left to `worldOf` to rebuild. */
32
+ export function unpack(data) {
33
+ const d = data;
34
+ const text = atob(d.bytes);
35
+ const bytes = new Uint8Array(text.length);
36
+ for (let i = 0; i < text.length; i++)
37
+ bytes[i] = text.charCodeAt(i);
38
+ return { bytes, world: null, handles: new Map(d.handles), t: d.t ?? Number.NaN };
39
+ }
18
40
  /** Puts `entry` into `log` after every entry at or before its time. */
19
41
  export function insert(log, entry) {
20
42
  let i = log.length;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@msb235/blits-rapier",
3
- "version": "0.2.0",
3
+ "version": "0.4.0",
4
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
5
  "keywords": [
6
6
  "physics",
@@ -38,14 +38,14 @@
38
38
  "prepack": "rm -rf dist && npm run build"
39
39
  },
40
40
  "peerDependencies": {
41
- "@msb235/blits": "^0.17.0",
41
+ "@msb235/blits": "^0.19.0",
42
42
  "@dimforge/rapier2d-compat": ">=0.21.0 <1.0.0",
43
43
  "@dimforge/rapier3d-compat": ">=0.21.0 <1.0.0"
44
44
  },
45
45
  "devDependencies": {
46
46
  "@dimforge/rapier2d-compat": "0.21.0",
47
47
  "@dimforge/rapier3d-compat": "0.21.0",
48
- "@msb235/blits": "0.17.0"
48
+ "@msb235/blits": "0.19.0"
49
49
  },
50
50
  "peerDependenciesMeta": {
51
51
  "@dimforge/rapier2d-compat": {